class PdfOpenDialog {
    Q_OBJECT
public:
    enum Result {
        Cancel,
        CreateNewFolder,
        UseExistingFolder
    };
    explicit PdfOpenDialog(const QString &pdfPath, QWidget *parent = nullptr) {
    setWindowTitle(tr("Open PDF with SpeedyNote"));
    setWindowIcon(QIcon(":/resources/icons/mainicon.png"));
    setModal(true);
    QScreen *screen = QGuiApplication::primaryScreen();
    qreal dpr = screen ? screen->devicePixelRatio() : 1.0;
    int baseWidth = 500;
    int baseHeight = 200;
    int scaledWidth = static_cast<int>(baseWidth * qMax(1.0, dpr * 0.8));
    int scaledHeight = static_cast<int>(baseHeight * qMax(1.0, dpr * 0.8));
    setMinimumSize(scaledWidth, scaledHeight);
    setMaximumSize(scaledWidth + 100, scaledHeight + 50);
    setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
    setupUI();
    adjustSize();
    if (parent) {
        move(parent->geometry().center() - rect().center());
    } else {
        move(screen->geometry().center() - rect().center());
    }
}
    static bool hasValidNotebookFolder(const QString &pdfPath, QString &folderPath) {
    QFileInfo fileInfo(pdfPath);
    QString suggestedFolderName = fileInfo.baseName();
    QString pdfDir = fileInfo.absolutePath();
    QString potentialFolderPath = pdfDir + "/" + suggestedFolderName;
    if (isValidNotebookFolder(potentialFolderPath, pdfPath)) {
        folderPath = potentialFolderPath;
        return true;
    }
    return false;
}
  protected:
    void resizeEvent(QResizeEvent *event) override {
    if (event->size() != event->oldSize()) {
        QDialog::resizeEvent(event);
        QSize newSize = event->size();
        QSize minSize = minimumSize();
        QSize maxSize = maximumSize();
        newSize = newSize.expandedTo(minSize).boundedTo(maxSize);
        if (newSize != event->size()) {
            resize(newSize);
        }
    } else {
        QDialog::resizeEvent(event);
    }
}
  private:
    void onCreateNewFolder() {
    QFileInfo fileInfo(pdfPath);
    QString suggestedFolderName = fileInfo.baseName();
    QString pdfDir = fileInfo.absolutePath();
    QString newFolderPath = pdfDir + "/" + suggestedFolderName;
    if (QDir(newFolderPath).exists()) {
        QMessageBox::StandardButton reply = QMessageBox::question(
            this,
            tr("Folder Exists"),
            tr("A folder named \"%1\" already exists in the same directory as the PDF.\n\nDo you want to use this existing folder?").arg(suggestedFolderName),
            QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel
        );
        if (reply == QMessageBox::Yes) {
            selectedFolder = newFolderPath;
            result = UseExistingFolder;
            accept();
            return;
        } else if (reply == QMessageBox::Cancel) {
            return;
        }
        int counter = 1;
        do {
            newFolderPath = pdfDir + "/" + suggestedFolderName + QString("_%1").arg(counter);
            counter++;
        } while (QDir(newFolderPath).exists() && counter < 100);
        if (counter >= 100) {
            QMessageBox::warning(this, tr("Error"), tr("Could not create a unique folder name."));
            return;
        }
    }
    QDir dir;
    if (dir.mkpath(newFolderPath)) {
        selectedFolder = newFolderPath;
        result = CreateNewFolder;
        accept();
    } else {
        QMessageBox::warning(this, tr("Error"), tr("Failed to create folder: %1").arg(newFolderPath));
    }
}
    void onUseExistingFolder() {
    QString folder = QFileDialog::getExistingDirectory(
        this,
        tr("Select Existing Notebook Folder"),
        QFileInfo(pdfPath).absolutePath()
    );
    if (!folder.isEmpty()) {
        selectedFolder = folder;
        result = UseExistingFolder;
        accept();
    }
}
    void onCancel() {
    result = Cancel;
    reject();
}
  private:
    void setupUI() {
    QVBoxLayout *mainLayout = new QVBoxLayout(this);
    mainLayout->setSpacing(15);
    mainLayout->setContentsMargins(20, 20, 20, 20);
    mainLayout->setSizeConstraint(QLayout::SetMinAndMaxSize);
    QHBoxLayout *headerLayout = new QHBoxLayout();
    headerLayout->setSpacing(10);
    QLabel *iconLabel = new QLabel();
    QPixmap icon = QIcon(":/resources/icons/mainicon.png").pixmap(48, 48);
    iconLabel->setPixmap(icon);
    iconLabel->setFixedSize(48, 48);
    iconLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
    QLabel *titleLabel = new QLabel(tr("Open PDF with SpeedyNote"));
    titleLabel->setStyleSheet("font-size: 16px; font-weight: bold;");
    titleLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
    headerLayout->addWidget(iconLabel);
    headerLayout->addWidget(titleLabel);
    headerLayout->addStretch();
    mainLayout->addLayout(headerLayout);
    QFileInfo fileInfo(pdfPath);
    QString fileName = fileInfo.fileName();
    QString folderName = fileInfo.baseName();
    QLabel *messageLabel = new QLabel(tr("PDF File: %1\n\nHow would you like to open this PDF?").arg(fileName));
    messageLabel->setWordWrap(true);
    messageLabel->setStyleSheet("font-size: 12px; color: #666;");
    messageLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
    mainLayout->addWidget(messageLabel);
    QVBoxLayout *buttonLayout = new QVBoxLayout();
    buttonLayout->setSpacing(10);
    QPushButton *createFolderBtn = new QPushButton(tr("Create New Notebook Folder (\"%1\")").arg(folderName));
    createFolderBtn->setIcon(QApplication::style()->standardIcon(QStyle::SP_DirIcon));
    createFolderBtn->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
    createFolderBtn->setMinimumHeight(40);
    createFolderBtn->setStyleSheet(R"(
        QPushButton {
            text-align: left;
            padding: 10px;
            border: 1px solid palette(mid);
            border-radius: 5px;
            background: palette(button);
        }
        QPushButton:hover {
            background: palette(light);
            border-color: palette(dark);
        }
        QPushButton:pressed {
            background: palette(midlight);
        }
    )");
    connect(createFolderBtn, &QPushButton::clicked, this, &PdfOpenDialog::onCreateNewFolder);
    QPushButton *existingFolderBtn = new QPushButton(tr("Use Existing Notebook Folder..."));
    existingFolderBtn->setIcon(QApplication::style()->standardIcon(QStyle::SP_DirOpenIcon));
    existingFolderBtn->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
    existingFolderBtn->setMinimumHeight(40);
    existingFolderBtn->setStyleSheet(R"(
        QPushButton {
            text-align: left;
            padding: 10px;
            border: 1px solid palette(mid);
            border-radius: 5px;
            background: palette(button);
        }
        QPushButton:hover {
            background: palette(light);
            border-color: palette(dark);
        }
        QPushButton:pressed {
            background: palette(midlight);
        }
    )");
    connect(existingFolderBtn, &QPushButton::clicked, this, &PdfOpenDialog::onUseExistingFolder);
    buttonLayout->addWidget(createFolderBtn);
    buttonLayout->addWidget(existingFolderBtn);
    mainLayout->addLayout(buttonLayout);
    QHBoxLayout *cancelLayout = new QHBoxLayout();
    cancelLayout->addStretch();
    QPushButton *cancelBtn = new QPushButton(tr("Cancel"));
    cancelBtn->setIcon(QApplication::style()->standardIcon(QStyle::SP_DialogCancelButton));
    cancelBtn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
    cancelBtn->setMinimumSize(80, 30);
    cancelBtn->setStyleSheet(R"(
        QPushButton {
            padding: 8px 20px;
            border: 1px solid palette(mid);
            border-radius: 3px;
            background: palette(button);
        }
        QPushButton:hover {
            background: palette(light);
        }
        QPushButton:pressed {
            background: palette(midlight);
        }
    )");
    connect(cancelBtn, &QPushButton::clicked, this, &PdfOpenDialog::onCancel);
    cancelLayout->addWidget(cancelBtn);
    mainLayout->addLayout(cancelLayout);
}
    static bool isValidNotebookFolder(const QString &folderPath, const QString &pdfPath) {
    QDir folder(folderPath);
    if (!folder.exists()) {
        return false;
    }
    QString pdfPathFile = folderPath + "/.pdf_path.txt";
    if (QFile::exists(pdfPathFile)) {
        QFile file(pdfPathFile);
        if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
            QTextStream in(&file);
            QString storedPdfPath = in.readLine().trimmed();
            file.close();
            if (!storedPdfPath.isEmpty()) {
                QFileInfo currentPdf(pdfPath);
                QFileInfo storedPdf(storedPdfPath);
                if (storedPdf.exists() &&
                    currentPdf.absoluteFilePath() == storedPdf.absoluteFilePath()) {
                    return true;
                }
            }
        }
    }
    QString notebookIdFile = folderPath + "/.notebook_id.txt";
    if (QFile::exists(notebookIdFile)) {
        QStringList contentFiles = folder.entryList(
            QStringList() << "*.png" << ".pdf_path.txt" << ".bookmarks.txt" << ".background_config.txt",
            QDir::Files
        );
        if (!contentFiles.isEmpty()) {
            return false;
        }
    }
    return false;
}
    Result result;
    QString selectedFolder;
    QString pdfPath;
}

class QTreeWidgetItem {
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = nullptr);
    virtual ~MainWindow() {

    saveButtonMappings();  // ✅ Save on exit, as backup
    delete canvas;
}
    int getCurrentPageForCanvas(InkCanvas *canvas) {
    return pageMap.contains(canvas) ? pageMap[canvas] : 0;
}
    bool lowResPreviewEnabled = true;
    void setLowResPreviewEnabled(bool enabled) {
    lowResPreviewEnabled = enabled;

    QSettings settings("SpeedyNote", "App");
    settings.setValue("lowResPreviewEnabled", enabled);
}
    bool isLowResPreviewEnabled() const {
    return lowResPreviewEnabled;
}
    bool areBenchmarkControlsVisible() const {
    return benchmarkButton->isVisible() && benchmarkLabel->isVisible();
}
    void setBenchmarkControlsVisible(bool visible) {
    benchmarkButton->setVisible(visible);
    benchmarkLabel->setVisible(visible);
}
    bool zoomButtonsVisible = true;
    bool areZoomButtonsVisible() const {
    return zoomButtonsVisible;
}
    void setZoomButtonsVisible(bool visible) {
    zoom50Button->setVisible(visible);
    dezoomButton->setVisible(visible);
    zoom200Button->setVisible(visible);

    QSettings settings("SpeedyNote", "App");
    settings.setValue("zoomButtonsVisible", visible);

    // Update zoomButtonsVisible flag and trigger layout update
    zoomButtonsVisible = visible;

    // Trigger layout update to adjust responsive thresholds
    if (layoutUpdateTimer) {
        layoutUpdateTimer->stop();
        layoutUpdateTimer->start(50); // Quick update for settings change
    } else {
        updateToolbarLayout(); // Direct update if no timer exists yet
    }
}
    bool scrollOnTopEnabled = false;
    bool isScrollOnTopEnabled() const {
    return scrollOnTopEnabled;
}
    void setScrollOnTopEnabled(bool enabled) {
    scrollOnTopEnabled = enabled;

    QSettings settings("SpeedyNote", "App");
    settings.setValue("scrollOnTopEnabled", enabled);
}
    bool touchGesturesEnabled = true;
    bool areTouchGesturesEnabled() const {
    return touchGesturesEnabled;
}
    void setTouchGesturesEnabled(bool enabled) {
    touchGesturesEnabled = enabled;

    // Apply to all canvases
    for (int i = 0; i < canvasStack->count(); ++i) {
        InkCanvas *canvas = qobject_cast<InkCanvas*>(canvasStack->widget(i));
        if (canvas) {
            canvas->setTouchGesturesEnabled(enabled);
        }
    }

    QSettings settings("SpeedyNote", "App");
    settings.setValue("touchGesturesEnabled", enabled);
}
    QColor customAccentColor;
    bool useCustomAccentColor = false;
    QColor getAccentColor() const {
    if (useCustomAccentColor && customAccentColor.isValid()) {
        return customAccentColor;
    }

    // Return system accent color
    QPalette palette = QGuiApplication::palette();
    return palette.highlight().color();
}
    void setCustomAccentColor(const QColor &color) {
    if (customAccentColor != color) {
        customAccentColor = color;
        saveThemeSettings();
        // Always update theme if custom accent color is enabled
        if (useCustomAccentColor) {
            updateTheme();
        }
    }
}
    void setUseCustomAccentColor(bool use) {
    if (useCustomAccentColor != use) {
        useCustomAccentColor = use;
        updateTheme();
        saveThemeSettings();
    }
}
    void setUseBrighterPalette(bool use) {
    if (useBrighterPalette != use) {
        useBrighterPalette = use;

        // Update all colors - call updateColorPalette which handles null checks
        updateColorPalette();

        // Save preference
        QSettings settings("SpeedyNote", "App");
        settings.setValue("useBrighterPalette", useBrighterPalette);
    }
}
    QColor getDefaultPenColor() {
    return isDarkMode() ? Qt::white : Qt::black;
}
    SDLControllerManager *controllerManager = nullptr;
    QThread *controllerThread = nullptr;
    QString getHoldMapping(const QString &buttonName) {
    return buttonHoldMapping.value(buttonName, "None");
}
    QString getPressMapping(const QString &buttonName) {
    return buttonPressMapping.value(buttonName, "None");
}
    void saveButtonMappings() {
    QSettings settings("SpeedyNote", "App");

    settings.beginGroup("ButtonHoldMappings");
    for (auto it = buttonHoldMapping.begin(); it != buttonHoldMapping.end(); ++it) {
        settings.setValue(it.key(), it.value());
    }
    settings.endGroup();

    settings.beginGroup("ButtonPressMappings");
    for (auto it = buttonPressMapping.begin(); it != buttonPressMapping.end(); ++it) {
        settings.setValue(it.key(), it.value());
    }
    settings.endGroup();
}
    void loadButtonMappings() {
    QSettings settings("SpeedyNote", "App");

    // First, check if we need to migrate old settings
    migrateOldButtonMappings();

    settings.beginGroup("ButtonHoldMappings");
    QStringList holdKeys = settings.allKeys();
    for (const QString &key : holdKeys) {
        buttonHoldMapping[key] = settings.value(key, "none").toString();
    }
    settings.endGroup();

    settings.beginGroup("ButtonPressMappings");
    QStringList pressKeys = settings.allKeys();
    for (const QString &key : pressKeys) {
        QString value = settings.value(key, "none").toString();
        buttonPressMapping[key] = value;

        // ✅ Convert internal key to action enum
        buttonPressActionMapping[key] = stringToAction(value);
    }
    settings.endGroup();
}
    void setHoldMapping(const QString &buttonName, const QString &dialMode) {
    buttonHoldMapping[buttonName] = dialMode;
}
    void setPressMapping(const QString &buttonName, const QString &action) {
    buttonPressMapping[buttonName] = action;
    buttonPressActionMapping[buttonName] = stringToAction(action);  // ✅ THIS LINE WAS MISSING
}
    DialMode dialModeFromString(const QString &mode) {
    // Convert internal key to our existing DialMode enum
    InternalDialMode internalMode = ButtonMappingHelper::internalKeyToDialMode(mode);

    switch (internalMode) {
        case InternalDialMode::None: return PageSwitching; // Default fallback
        case InternalDialMode::PageSwitching: return PageSwitching;
        case InternalDialMode::ZoomControl: return ZoomControl;
        case InternalDialMode::ThicknessControl: return ThicknessControl;

        case InternalDialMode::ToolSwitching: return ToolSwitching;
        case InternalDialMode::PresetSelection: return PresetSelection;
        case InternalDialMode::PanAndPageScroll: return PanAndPageScroll;
    }
    return PanAndPageScroll;  // Default fallback
}
    void importNotebookFromFile(const QString &packageFile) {

    QString destDir = QFileDialog::getExistingDirectory(this, tr("Select Working Directory for Notebook"));

    if (destDir.isEmpty()) {
        QMessageBox::warning(this, tr("Import Cancelled"), tr("No directory selected. Notebook will not be opened."));
        return;
    }

    InkCanvas *canvas = currentCanvas();
    if (!canvas) return;

    canvas->importNotebookTo(packageFile, destDir);

    // Change saveFolder in InkCanvas
    canvas->setSaveFolder(destDir);
    canvas->loadPage(0);
    updateZoom(); // ✅ Update zoom and pan range after importing notebook
}
    void openPdfFile(const QString &pdfPath) {
    // Check if the PDF file exists
    if (!QFile::exists(pdfPath)) {
        QMessageBox::warning(this, tr("File Not Found"), tr("The PDF file could not be found:\n%1").arg(pdfPath));
        return;
    }

    // First, check if there's already a valid notebook folder for this PDF
    QString existingFolderPath;
    if (PdfOpenDialog::hasValidNotebookFolder(pdfPath, existingFolderPath)) {
        // Found a valid notebook folder, open it directly without showing dialog
        InkCanvas *canvas = currentCanvas();
        if (!canvas) return;

        // Save current work if edited
        if (canvas->isEdited()) {
            saveCurrentPage();
        }

        // Set the existing folder as save folder
        canvas->setSaveFolder(existingFolderPath);

        // Load the PDF
        canvas->loadPdf(pdfPath);

        // Update tab label and recent notebooks
        updateTabLabel();
        if (recentNotebooksManager) {
            recentNotebooksManager->addRecentNotebook(existingFolderPath, canvas);
        }

        // Switch to page 1 and update UI
        switchPageWithDirection(1, 1);
        pageInput->setValue(1);
        updateZoom();
        updatePanRange();

        return; // Exit early, no need to show dialog
    }

    // No valid notebook folder found, show the dialog with options
    PdfOpenDialog dialog(pdfPath, this);
    dialog.exec();

    PdfOpenDialog::Result result = dialog.getResult();
    QString selectedFolder = dialog.getSelectedFolder();

    if (result == PdfOpenDialog::Cancel) {
        return; // User cancelled, do nothing
    }

    InkCanvas *canvas = currentCanvas();
    if (!canvas) return;

    // Save current work if edited
    if (canvas->isEdited()) {
        saveCurrentPage();
    }

    if (result == PdfOpenDialog::CreateNewFolder) {
        // Set the new folder as save folder
        canvas->setSaveFolder(selectedFolder);

        // Load the PDF
        canvas->loadPdf(pdfPath);

        // Update tab label and recent notebooks
        updateTabLabel();
        if (recentNotebooksManager) {
            recentNotebooksManager->addRecentNotebook(selectedFolder, canvas);
        }

        // Switch to page 1 and update UI
        switchPageWithDirection(1, 1);
        pageInput->setValue(1);
        updateZoom();
        updatePanRange();

    } else if (result == PdfOpenDialog::UseExistingFolder) {
        // Check if the existing folder is linked to the same PDF
        QString metadataFile = selectedFolder + "/.pdf_path.txt";
        bool isLinkedToSamePdf = false;

        if (QFile::exists(metadataFile)) {
            QFile file(metadataFile);
            if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
                QTextStream in(&file);
                QString existingPdfPath = in.readLine().trimmed();
                file.close();

                // Compare absolute paths
                QFileInfo existingInfo(existingPdfPath);
                QFileInfo newInfo(pdfPath);
                isLinkedToSamePdf = (existingInfo.absoluteFilePath() == newInfo.absoluteFilePath());
            }
        }

        if (!isLinkedToSamePdf && QFile::exists(metadataFile)) {
            // Folder is linked to a different PDF, ask user what to do
            QMessageBox::StandardButton reply = QMessageBox::question(
                this,
                tr("Different PDF Linked"),
                tr("This notebook folder is already linked to a different PDF file.\n\nDo you want to replace the link with the new PDF?"),
                QMessageBox::Yes | QMessageBox::No
            );

            if (reply == QMessageBox::No) {
                return; // User chose not to replace
            }
        }

        // Set the existing folder as save folder
        canvas->setSaveFolder(selectedFolder);

        // Load the PDF
        canvas->loadPdf(pdfPath);

        // Update tab label and recent notebooks
        updateTabLabel();
        if (recentNotebooksManager) {
            recentNotebooksManager->addRecentNotebook(selectedFolder, canvas);
        }

        // Switch to page 1 and update UI
        switchPageWithDirection(1, 1);
        pageInput->setValue(1);
        updateZoom();
        updatePanRange();
    }
}
    void setPdfDPI(int dpi) {
    if (dpi != pdfRenderDPI) {
        pdfRenderDPI = dpi;
        savePdfDPI(dpi);

        // Apply immediately to current canvas if needed
        if (currentCanvas()) {
            currentCanvas()->setPDFRenderDPI(dpi);
            currentCanvas()->clearPdfCache();
            currentCanvas()->loadPdfPage(getCurrentPageForCanvas(currentCanvas()));  // Optional: add this if needed
            updateZoom();
            updatePanRange();
        }
    }
}
    void savePdfDPI(int dpi) {
    QSettings settings("SpeedyNote", "App");
    settings.setValue("pdfRenderDPI", dpi);
}
    void saveDefaultBackgroundSettings(BackgroundStyle style, QColor color, int density) {
    QSettings settings("SpeedyNote", "App");
    settings.setValue("defaultBackgroundStyle", static_cast<int>(style));
    settings.setValue("defaultBackgroundColor", color.name());
    settings.setValue("defaultBackgroundDensity", density);
}
    void loadDefaultBackgroundSettings(BackgroundStyle &style, QColor &color, int &density) {
    QSettings settings("SpeedyNote", "App");
    style = static_cast<BackgroundStyle>(settings.value("defaultBackgroundStyle", static_cast<int>(BackgroundStyle::Grid)).toInt());
    color = QColor(settings.value("defaultBackgroundColor", "#FFFFFF").toString());
    density = settings.value("defaultBackgroundDensity", 30).toInt();

    // Ensure valid values
    if (!color.isValid()) color = Qt::white;
    if (density < 10) density = 10;
    if (density > 200) density = 200;
}
    void saveThemeSettings() {
    QSettings settings("SpeedyNote", "App");
    settings.setValue("useCustomAccentColor", useCustomAccentColor);
    if (customAccentColor.isValid()) {
        settings.setValue("customAccentColor", customAccentColor.name());
    }
    settings.setValue("useBrighterPalette", useBrighterPalette);
}
    void loadThemeSettings() {
    QSettings settings("SpeedyNote", "App");
    useCustomAccentColor = settings.value("useCustomAccentColor", false).toBool();
    QString colorName = settings.value("customAccentColor", "#0078D4").toString();
    customAccentColor = QColor(colorName);
    useBrighterPalette = settings.value("useBrighterPalette", false).toBool();

    // Ensure valid values
    if (!customAccentColor.isValid()) {
        customAccentColor = QColor("#0078D4"); // Default blue
    }

    // Apply theme immediately after loading
    updateTheme();
}
    void updateTheme() {
    // Update control bar background color
    QColor accentColor = getAccentColor();
    if (controlBar) {
        controlBar->setStyleSheet(QString(R"(
        QWidget#controlBar {
            background-color: %1;
            }
        )").arg(accentColor.name()));
    }

    // Update dial background color
    if (pageDial) {
        pageDial->setStyleSheet(QString(R"(
        QDial {
            background-color: %1;
            }
        )").arg(accentColor.name()));
    }

    // Update add tab button styling
    if (addTabButton) {
        bool darkMode = isDarkMode();
        QString buttonBgColor = darkMode ? "rgba(80, 80, 80, 255)" : "rgba(220, 220, 220, 255)";
        QString buttonHoverColor = darkMode ? "rgba(90, 90, 90, 255)" : "rgba(200, 200, 200, 255)";
        QString buttonPressColor = darkMode ? "rgba(70, 70, 70, 255)" : "rgba(180, 180, 180, 255)";
        QString borderColor = darkMode ? "rgba(100, 100, 100, 255)" : "rgba(180, 180, 180, 255)";

        addTabButton->setStyleSheet(QString(R"(
            QPushButton {
                background-color: %1;
                border: 1px solid %2;
                border-radius: 12px;
                margin: 2px;
            }
            QPushButton:hover {
                background-color: %3;
            }
            QPushButton:pressed {
                background-color: %4;
            }
        )").arg(buttonBgColor).arg(borderColor).arg(buttonHoverColor).arg(buttonPressColor));
    }

    // Update PDF outline sidebar styling
    if (outlineSidebar && outlineTree) {
        bool darkMode = isDarkMode();
        QString bgColor = darkMode ? "rgba(45, 45, 45, 255)" : "rgba(250, 250, 250, 255)";
        QString borderColor = darkMode ? "rgba(80, 80, 80, 255)" : "rgba(200, 200, 200, 255)";
        QString textColor = darkMode ? "#E0E0E0" : "#333";
        QString hoverColor = darkMode ? "rgba(60, 60, 60, 255)" : "rgba(240, 240, 240, 255)";
        QString selectedColor = QString("rgba(%1, %2, %3, 100)").arg(accentColor.red()).arg(accentColor.green()).arg(accentColor.blue());

        outlineSidebar->setStyleSheet(QString(R"(
            QWidget {
                background-color: %1;
                border-right: 1px solid %2;
            }
            QLabel {
                color: %3;
                background: transparent;
            }
        )").arg(bgColor).arg(borderColor).arg(textColor));

        outlineTree->setStyleSheet(QString(R"(
            QTreeWidget {
                background-color: %1;
                border: none;
                color: %2;
                outline: none;
            }
            QTreeWidget::item {
                padding: 4px;
                border: none;
            }
            QTreeWidget::item:hover {
                background-color: %3;
            }
            QTreeWidget::item:selected {
                background-color: %4;
                color: %2;
            }
            QTreeWidget::branch {
                background: transparent;
            }
            QTreeWidget::branch:has-children:!has-siblings:closed,
            QTreeWidget::branch:closed:has-children:has-siblings {
                border-image: none;
                image: url(:/resources/icons/down_arrow.png);
            }
            QTreeWidget::branch:open:has-children:!has-siblings,
            QTreeWidget::branch:open:has-children:has-siblings {
                border-image: none;
                image: url(:/resources/icons/up_arrow.png);
            }
            QScrollBar:vertical {
                background: rgba(200, 200, 200, 80);
                border: none;
                margin: 0px;
                width: 16px !important;
                max-width: 16px !important;
            }
            QScrollBar:vertical:hover {
                background: rgba(200, 200, 200, 120);
            }
            QScrollBar::handle:vertical {
                background: rgba(100, 100, 100, 150);
                border-radius: 2px;
                min-height: 120px;
            }
            QScrollBar::handle:vertical:hover {
                background: rgba(80, 80, 80, 210);
            }
            QScrollBar::add-line:vertical,
            QScrollBar::sub-line:vertical {
                width: 0px;
                height: 0px;
                background: none;
                border: none;
            }
            QScrollBar::add-page:vertical,
            QScrollBar::sub-page:vertical {
                background: transparent;
            }
        )").arg(bgColor).arg(textColor).arg(hoverColor).arg(selectedColor));

        // Apply same styling to bookmarks tree
        bookmarksTree->setStyleSheet(QString(R"(
            QTreeWidget {
                background-color: %1;
                border: none;
                color: %2;
                outline: none;
            }
            QTreeWidget::item {
                padding: 2px;
                border: none;
                min-height: 26px;
            }
            QTreeWidget::item:hover {
                background-color: %3;
            }
            QTreeWidget::item:selected {
                background-color: %4;
                color: %2;
            }
            QScrollBar:vertical {
                background: rgba(200, 200, 200, 80);
                border: none;
                margin: 0px;
                width: 16px !important;
                max-width: 16px !important;
            }
            QScrollBar:vertical:hover {
                background: rgba(200, 200, 200, 120);
            }
            QScrollBar::handle:vertical {
                background: rgba(100, 100, 100, 150);
                border-radius: 2px;
                min-height: 120px;
            }
            QScrollBar::handle:vertical:hover {
                background: rgba(80, 80, 80, 210);
            }
            QScrollBar::add-line:vertical,
            QScrollBar::sub-line:vertical {
                width: 0px;
                height: 0px;
                background: none;
                border: none;
            }
            QScrollBar::add-page:vertical,
            QScrollBar::sub-page:vertical {
                background: transparent;
            }
        )").arg(bgColor).arg(textColor).arg(hoverColor).arg(selectedColor));
    }

    // Update horizontal tab bar styling with accent color
    if (tabList) {
        bool darkMode = isDarkMode();
        QString bgColor = darkMode ? "rgba(60, 60, 60, 255)" : "rgba(240, 240, 240, 255)";
        QString itemBgColor = darkMode ? "rgba(80, 80, 80, 255)" : "rgba(220, 220, 220, 255)";
        QString selectedBgColor = darkMode ? "rgba(45, 45, 45, 255)" : "white";
        QString borderColor = darkMode ? "rgba(100, 100, 100, 255)" : "rgba(180, 180, 180, 255)";
        QString hoverBgColor = darkMode ? "rgba(90, 90, 90, 255)" : "rgba(230, 230, 230, 255)";

        tabList->setStyleSheet(QString(R"(
        QListWidget {
            background-color: %1;
            border: none;
            border-bottom: 2px solid %2;
            outline: none;
        }
        QListWidget::item {
            background-color: %3;
            border: 1px solid %4;
            border-bottom: none;
            margin-right: 1px;
            margin-top: 2px;
            padding: 0px;
            min-width: 80px;
            max-width: 120px;
        }
        QListWidget::item:selected {
            background-color: %5;
            border: 1px solid %4;
            border-bottom: 2px solid %2;
            margin-top: 1px;
        }
        QListWidget::item:hover:!selected {
            background-color: %6;
        }
        QScrollBar:horizontal {
            background: %1;
            height: 8px;
            border: none;
            margin: 0px;
            border-top: 1px solid %4;
        }
        QScrollBar::handle:horizontal {
            background: rgba(150, 150, 150, 120);
            border-radius: 4px;
            min-width: 20px;
            margin: 1px;
        }
        QScrollBar::handle:horizontal:hover {
            background: rgba(120, 120, 120, 200);
        }
        QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
            width: 0px;
            height: 0px;
            background: none;
            border: none;
        }
        QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {
            background: transparent;
        }
        )").arg(bgColor)
           .arg(accentColor.name())
           .arg(itemBgColor)
           .arg(borderColor)
           .arg(selectedBgColor)
           .arg(hoverBgColor));
    }

    // Force icon reload for all buttons that use themed icons
    if (loadPdfButton) loadPdfButton->setIcon(loadThemedIcon("pdf"));
    if (clearPdfButton) clearPdfButton->setIcon(loadThemedIcon("pdfdelete"));
    if (pdfTextSelectButton) pdfTextSelectButton->setIcon(loadThemedIcon("ibeam"));
    if (exportNotebookButton) exportNotebookButton->setIcon(loadThemedIcon("export"));
    if (importNotebookButton) importNotebookButton->setIcon(loadThemedIcon("import"));
    if (benchmarkButton) benchmarkButton->setIcon(loadThemedIcon("benchmark"));
    if (toggleTabBarButton) toggleTabBarButton->setIcon(loadThemedIcon("tabs"));
    if (toggleOutlineButton) toggleOutlineButton->setIcon(loadThemedIcon("outline"));
    if (toggleBookmarksButton) toggleBookmarksButton->setIcon(loadThemedIcon("bookmark"));
    if (toggleBookmarkButton) toggleBookmarkButton->setIcon(loadThemedIcon("star"));
    if (selectFolderButton) selectFolderButton->setIcon(loadThemedIcon("folder"));
    if (saveButton) saveButton->setIcon(loadThemedIcon("save"));
    if (saveAnnotatedButton) saveAnnotatedButton->setIcon(loadThemedIcon("saveannotated"));
    if (fullscreenButton) fullscreenButton->setIcon(loadThemedIcon("fullscreen"));
    if (backgroundButton) backgroundButton->setIcon(loadThemedIcon("background"));
    if (straightLineToggleButton) straightLineToggleButton->setIcon(loadThemedIcon("straightLine"));
    if (ropeToolButton) ropeToolButton->setIcon(loadThemedIcon("rope"));
    if (markdownButton) markdownButton->setIcon(loadThemedIcon("markdown"));
    if (deletePageButton) deletePageButton->setIcon(loadThemedIcon("trash"));
    if (zoomButton) zoomButton->setIcon(loadThemedIcon("zoom"));
    if (dialToggleButton) dialToggleButton->setIcon(loadThemedIcon("dial"));
    if (fastForwardButton) fastForwardButton->setIcon(loadThemedIcon("fastforward"));
    if (jumpToPageButton) jumpToPageButton->setIcon(loadThemedIcon("bookpage"));
    if (thicknessButton) thicknessButton->setIcon(loadThemedIcon("thickness"));
    if (btnPageSwitch) btnPageSwitch->setIcon(loadThemedIcon("bookpage"));
    if (btnZoom) btnZoom->setIcon(loadThemedIcon("zoom"));
    if (btnThickness) btnThickness->setIcon(loadThemedIcon("thickness"));
    if (btnTool) btnTool->setIcon(loadThemedIcon("pen"));
    if (btnPresets) btnPresets->setIcon(loadThemedIcon("preset"));
    if (btnPannScroll) btnPannScroll->setIcon(loadThemedIcon("scroll"));
    if (addPresetButton) addPresetButton->setIcon(loadThemedIcon("savepreset"));
    if (openControlPanelButton) openControlPanelButton->setIcon(loadThemedIcon("settings"));
    if (openRecentNotebooksButton) openRecentNotebooksButton->setIcon(loadThemedIcon("recent"));
    if (penToolButton) penToolButton->setIcon(loadThemedIcon("pen"));
    if (markerToolButton) markerToolButton->setIcon(loadThemedIcon("marker"));
    if (eraserToolButton) eraserToolButton->setIcon(loadThemedIcon("eraser"));

    // Update button styles with new theme
    bool darkMode = isDarkMode();
    QString newButtonStyle = createButtonStyle(darkMode);

    // Update all buttons that use the buttonStyle
    if (loadPdfButton) loadPdfButton->setStyleSheet(newButtonStyle);
    if (clearPdfButton) clearPdfButton->setStyleSheet(newButtonStyle);
    if (pdfTextSelectButton) pdfTextSelectButton->setStyleSheet(newButtonStyle);
    if (exportNotebookButton) exportNotebookButton->setStyleSheet(newButtonStyle);
    if (importNotebookButton) importNotebookButton->setStyleSheet(newButtonStyle);
    if (benchmarkButton) benchmarkButton->setStyleSheet(newButtonStyle);
    if (toggleTabBarButton) toggleTabBarButton->setStyleSheet(newButtonStyle);
    if (toggleOutlineButton) toggleOutlineButton->setStyleSheet(newButtonStyle);
    if (toggleBookmarksButton) toggleBookmarksButton->setStyleSheet(newButtonStyle);
    if (toggleBookmarkButton) toggleBookmarkButton->setStyleSheet(newButtonStyle);
    if (selectFolderButton) selectFolderButton->setStyleSheet(newButtonStyle);
    if (saveButton) saveButton->setStyleSheet(newButtonStyle);
    if (saveAnnotatedButton) saveAnnotatedButton->setStyleSheet(newButtonStyle);
    if (fullscreenButton) fullscreenButton->setStyleSheet(newButtonStyle);
    if (redButton) redButton->setStyleSheet(newButtonStyle);
    if (blueButton) blueButton->setStyleSheet(newButtonStyle);
    if (yellowButton) yellowButton->setStyleSheet(newButtonStyle);
    if (greenButton) greenButton->setStyleSheet(newButtonStyle);
    if (blackButton) blackButton->setStyleSheet(newButtonStyle);
    if (whiteButton) whiteButton->setStyleSheet(newButtonStyle);
    if (thicknessButton) thicknessButton->setStyleSheet(newButtonStyle);
    if (penToolButton) penToolButton->setStyleSheet(newButtonStyle);
    if (markerToolButton) markerToolButton->setStyleSheet(newButtonStyle);
    if (eraserToolButton) eraserToolButton->setStyleSheet(newButtonStyle);
    if (backgroundButton) backgroundButton->setStyleSheet(newButtonStyle);
    if (straightLineToggleButton) straightLineToggleButton->setStyleSheet(newButtonStyle);
    if (ropeToolButton) ropeToolButton->setStyleSheet(newButtonStyle);
    if (markdownButton) markdownButton->setStyleSheet(newButtonStyle);
    if (deletePageButton) deletePageButton->setStyleSheet(newButtonStyle);
    if (zoomButton) zoomButton->setStyleSheet(newButtonStyle);
    if (dialToggleButton) dialToggleButton->setStyleSheet(newButtonStyle);
    if (fastForwardButton) fastForwardButton->setStyleSheet(newButtonStyle);
    if (jumpToPageButton) jumpToPageButton->setStyleSheet(newButtonStyle);
    if (btnPageSwitch) btnPageSwitch->setStyleSheet(newButtonStyle);
    if (btnZoom) btnZoom->setStyleSheet(newButtonStyle);
    if (btnThickness) btnThickness->setStyleSheet(newButtonStyle);
    if (btnTool) btnTool->setStyleSheet(newButtonStyle);
    if (btnPresets) btnPresets->setStyleSheet(newButtonStyle);
    if (btnPannScroll) btnPannScroll->setStyleSheet(newButtonStyle);
    if (addPresetButton) addPresetButton->setStyleSheet(newButtonStyle);
    if (openControlPanelButton) openControlPanelButton->setStyleSheet(newButtonStyle);
    if (openRecentNotebooksButton) openRecentNotebooksButton->setStyleSheet(newButtonStyle);
    if (zoom50Button) zoom50Button->setStyleSheet(newButtonStyle);
    if (dezoomButton) dezoomButton->setStyleSheet(newButtonStyle);
    if (zoom200Button) zoom200Button->setStyleSheet(newButtonStyle);
    if (prevPageButton) prevPageButton->setStyleSheet(newButtonStyle);
    if (nextPageButton) nextPageButton->setStyleSheet(newButtonStyle);

    // Update color buttons with palette-based icons
    if (redButton) {
        QString redIconPath = useBrighterPalette ? ":/resources/icons/pen_light_red.png" : ":/resources/icons/pen_dark_red.png";
        redButton->setIcon(QIcon(redIconPath));
    }
    if (blueButton) {
        QString blueIconPath = useBrighterPalette ? ":/resources/icons/pen_light_blue.png" : ":/resources/icons/pen_dark_blue.png";
        blueButton->setIcon(QIcon(blueIconPath));
    }
    if (yellowButton) {
        QString yellowIconPath = useBrighterPalette ? ":/resources/icons/pen_light_yellow.png" : ":/resources/icons/pen_dark_yellow.png";
        yellowButton->setIcon(QIcon(yellowIconPath));
    }
    if (greenButton) {
        QString greenIconPath = useBrighterPalette ? ":/resources/icons/pen_light_green.png" : ":/resources/icons/pen_dark_green.png";
        greenButton->setIcon(QIcon(greenIconPath));
    }
    if (blackButton) {
        QString blackIconPath = darkMode ? ":/resources/icons/pen_light_black.png" : ":/resources/icons/pen_dark_black.png";
        blackButton->setIcon(QIcon(blackIconPath));
    }
    if (whiteButton) {
        QString whiteIconPath = darkMode ? ":/resources/icons/pen_light_white.png" : ":/resources/icons/pen_dark_white.png";
        whiteButton->setIcon(QIcon(whiteIconPath));
    }

    // Update tab close button icons and label styling
    if (tabList) {
        bool darkMode = isDarkMode();
        QString labelColor = darkMode ? "#E0E0E0" : "#333";

        for (int i = 0; i < tabList->count(); ++i) {
            QListWidgetItem *item = tabList->item(i);
            if (item) {
                QWidget *tabWidget = tabList->itemWidget(item);
                if (tabWidget) {
                    QPushButton *closeButton = tabWidget->findChild<QPushButton*>();
                    if (closeButton) {
                        closeButton->setIcon(loadThemedIcon("cross"));
                    }

                    QLabel *tabLabel = tabWidget->findChild<QLabel*>("tabLabel");
                    if (tabLabel) {
                        tabLabel->setStyleSheet(QString("color: %1; font-weight: 500; padding: 2px; text-align: left;").arg(labelColor));
                    }
                }
            }
        }
    }

    // Update dial display
    updateDialDisplay();
}
    void migrateOldButtonMappings() {
    QSettings settings("SpeedyNote", "App");

    // Check if migration is needed by looking for old format strings
    settings.beginGroup("ButtonHoldMappings");
    QStringList holdKeys = settings.allKeys();
    bool needsMigration = false;

    for (const QString &key : holdKeys) {
        QString value = settings.value(key).toString();
        // If we find old English strings, we need to migrate
        if (value == "PageSwitching" || value == "ZoomControl" || value == "ThicknessControl" ||
            value == "ToolSwitching" || value == "PresetSelection" ||
            value == "PanAndPageScroll") {
            needsMigration = true;
            break;
        }
    }
    settings.endGroup();

    if (!needsMigration) {
        settings.beginGroup("ButtonPressMappings");
        QStringList pressKeys = settings.allKeys();
        for (const QString &key : pressKeys) {
            QString value = settings.value(key).toString();
            // Check for old English action strings
            if (value == "Toggle Fullscreen" || value == "Toggle Dial" || value == "Zoom 50%" ||
                value == "Add Preset" || value == "Delete Page" || value == "Fast Forward" ||
                value == "Open Control Panel" || value == "Custom Color") {
                needsMigration = true;
                break;
            }
        }
        settings.endGroup();
    }

    if (!needsMigration) return;

    // Perform migration
    // qDebug() << "Migrating old button mappings to new format...";

    // Migrate hold mappings
    settings.beginGroup("ButtonHoldMappings");
    holdKeys = settings.allKeys();
    for (const QString &key : holdKeys) {
        QString oldValue = settings.value(key).toString();
        QString newValue = migrateOldDialModeString(oldValue);
        if (newValue != oldValue) {
            settings.setValue(key, newValue);
        }
    }
    settings.endGroup();

    // Migrate press mappings
    settings.beginGroup("ButtonPressMappings");
    QStringList pressKeys = settings.allKeys();
    for (const QString &key : pressKeys) {
        QString oldValue = settings.value(key).toString();
        QString newValue = migrateOldActionString(oldValue);
        if (newValue != oldValue) {
            settings.setValue(key, newValue);
        }
    }
    settings.endGroup();

   // qDebug() << "Button mapping migration completed.";
}
    QString migrateOldDialModeString(const QString &oldString) {
    // Convert old English strings to new internal keys
    if (oldString == "None") return "none";
    if (oldString == "PageSwitching") return "page_switching";
    if (oldString == "ZoomControl") return "zoom_control";
    if (oldString == "ThicknessControl") return "thickness_control";

    if (oldString == "ToolSwitching") return "tool_switching";
    if (oldString == "PresetSelection") return "preset_selection";
    if (oldString == "PanAndPageScroll") return "pan_and_page_scroll";
    return oldString; // Return as-is if not found (might already be new format)
}
    QString migrateOldActionString(const QString &oldString) {
    // Convert old English strings to new internal keys
    if (oldString == "None") return "none";
    if (oldString == "Toggle Fullscreen") return "toggle_fullscreen";
    if (oldString == "Toggle Dial") return "toggle_dial";
    if (oldString == "Zoom 50%") return "zoom_50";
    if (oldString == "Zoom Out") return "zoom_out";
    if (oldString == "Zoom 200%") return "zoom_200";
    if (oldString == "Add Preset") return "add_preset";
    if (oldString == "Delete Page") return "delete_page";
    if (oldString == "Fast Forward") return "fast_forward";
    if (oldString == "Open Control Panel") return "open_control_panel";
    if (oldString == "Red") return "red_color";
    if (oldString == "Blue") return "blue_color";
    if (oldString == "Yellow") return "yellow_color";
    if (oldString == "Green") return "green_color";
    if (oldString == "Black") return "black_color";
    if (oldString == "White") return "white_color";
    if (oldString == "Custom Color") return "custom_color";
    if (oldString == "Toggle Sidebar") return "toggle_sidebar";
    if (oldString == "Save") return "save";
    if (oldString == "Straight Line Tool") return "straight_line_tool";
    if (oldString == "Rope Tool") return "rope_tool";
    if (oldString == "Set Pen Tool") return "set_pen_tool";
    if (oldString == "Set Marker Tool") return "set_marker_tool";
    if (oldString == "Set Eraser Tool") return "set_eraser_tool";
    if (oldString == "Toggle PDF Text Selection") return "toggle_pdf_text_selection";
    return oldString; // Return as-is if not found (might already be new format)
}
    InkCanvas* currentCanvas();
    void saveCurrentPage() {
    currentCanvas()->saveToFile(getCurrentPageForCanvas(currentCanvas()));
}
    void saveCurrentPageConcurrent() {
    InkCanvas *canvas = currentCanvas();
    if (!canvas || !canvas->isEdited()) return;

    int pageNumber = getCurrentPageForCanvas(canvas);
    QString saveFolder = canvas->getSaveFolder();

    if (saveFolder.isEmpty()) return;

    // Create a copy of the buffer for concurrent saving
    QPixmap bufferCopy = canvas->getBuffer();

    // Save markdown windows for this page (this must be done on the main thread)
    if (canvas->getMarkdownManager()) {
        canvas->getMarkdownManager()->saveWindowsForPage(pageNumber);
    }

    // Run the save operation concurrently
    QFuture<void> future = QtConcurrent::run([saveFolder, pageNumber, bufferCopy]() {
        // Get notebook ID from the save folder (similar to how InkCanvas does it)
        QString notebookId = "notebook"; // Default fallback
        QString idFile = saveFolder + "/.notebook_id.txt";
        if (QFile::exists(idFile)) {
            QFile file(idFile);
            if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
                QTextStream in(&file);
                notebookId = in.readLine().trimmed();
                file.close();
            }
        }

        QString filePath = saveFolder + QString("/%1_%2.png").arg(notebookId).arg(pageNumber, 5, 10, QChar('0'));

        QImage image(bufferCopy.size(), QImage::Format_ARGB32);
        image.fill(Qt::transparent);
        QPainter painter(&image);
        painter.drawPixmap(0, 0, bufferCopy);
        image.save(filePath, "PNG");
    });

    // Mark as not edited since we're saving
    canvas->setEdited(false);
}
    void switchPage(int pageNumber) {
    InkCanvas *canvas = currentCanvas();
    if (!canvas) return;

    if (currentCanvas()->isEdited()){
        saveCurrentPageConcurrent(); // Use concurrent saving for smoother page flipping
    }

    int oldPage = getCurrentPageForCanvas(currentCanvas()) + 1; // Convert to 1-based for comparison
    int newPage = pageNumber - 1;
    pageMap[canvas] = newPage;  // ✅ Save the page for this tab

    if (canvas->isPdfLoadedFunc() && pageNumber - 1 < canvas->getTotalPdfPages()) {
        canvas->loadPdfPage(newPage);
    } else {
        canvas->loadPage(newPage);
    }

    canvas->setLastActivePage(newPage);
    updateZoom();
    // It seems panXSlider and panYSlider can be null here during startup.
    if(panXSlider && panYSlider){
    canvas->setLastPanX(panXSlider->maximum());
    canvas->setLastPanY(panYSlider->maximum());

        // ✅ Enhanced scroll-on-top functionality
        if (scrollOnTopEnabled && panYSlider->maximum() > 0) {
            if (pageNumber > oldPage) {
                // Forward page switching → scroll to top
                panYSlider->setValue(0);
            } else if (pageNumber < oldPage) {
                // Backward page switching → scroll to bottom
                panYSlider->setValue(panYSlider->maximum());
            }
        }
    }
    updateDialDisplay();
    updateBookmarkButtonState(); // Update bookmark button state when switching pages
}
    void switchPageWithDirection(int pageNumber, int direction) {
    InkCanvas *canvas = currentCanvas();
    if (!canvas) return;

    if (currentCanvas()->isEdited()){
        saveCurrentPageConcurrent(); // Use concurrent saving for smoother page flipping
    }

    int newPage = pageNumber - 1;
    pageMap[canvas] = newPage;  // ✅ Save the page for this tab

    if (canvas->isPdfLoadedFunc() && pageNumber - 1 < canvas->getTotalPdfPages()) {
        canvas->loadPdfPage(newPage);
    } else {
        canvas->loadPage(newPage);
    }

    canvas->setLastActivePage(newPage);
    updateZoom();
    // It seems panXSlider and panYSlider can be null here during startup.
    if(panXSlider && panYSlider){
        canvas->setLastPanX(panXSlider->maximum());
        canvas->setLastPanY(panYSlider->maximum());

        // ✅ Enhanced scroll-on-top functionality with explicit direction
        if (scrollOnTopEnabled && panYSlider->maximum() > 0) {
            if (direction > 0) {
                // Forward page switching → scroll to top
                panYSlider->setValue(0);
            } else if (direction < 0) {
                // Backward page switching → scroll to bottom
                panYSlider->setValue(panYSlider->maximum());
            }
        }
    }
    updateDialDisplay();
    updateBookmarkButtonState(); // Update bookmark button state when switching pages
}
    void updateTabLabel() {
    int index = tabList->currentRow();
    if (index < 0) return;

    InkCanvas *canvas = currentCanvas();
    if (!canvas) return;

    QString folderPath = canvas->getSaveFolder(); // ✅ Get save folder
    if (folderPath.isEmpty()) return;

    QString tabName;

    // ✅ Check if there is an assigned PDF
    QString metadataFile = folderPath + "/.pdf_path.txt";
    if (QFile::exists(metadataFile)) {
        QFile file(metadataFile);
        if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
            QTextStream in(&file);
            QString pdfPath = in.readLine().trimmed();
            file.close();

            // ✅ Extract just the PDF filename (not full path)
            QFileInfo pdfInfo(pdfPath);
            if (pdfInfo.exists()) {
                tabName = elideTabText(pdfInfo.fileName(), 90); // e.g., "mydocument.pdf" (elided)
            }
        }
    }

    // ✅ If no PDF, use the folder name
    if (tabName.isEmpty()) {
        QFileInfo folderInfo(folderPath);
        tabName = elideTabText(folderInfo.fileName(), 90); // e.g., "MyNotebook" (elided)
    }

    QListWidgetItem *tabItem = tabList->item(index);
    if (tabItem) {
        QWidget *tabWidget = tabList->itemWidget(tabItem); // Get the tab's custom widget
        if (tabWidget) {
            QLabel *tabLabel = tabWidget->findChild<QLabel *>(); // Get the QLabel inside
            if (tabLabel) {
                tabLabel->setText(tabName); // ✅ Update tab label
                tabLabel->setWordWrap(false); // No wrapping for horizontal tabs
            }
        }
    }
}
    QSpinBox *pageInput;
    QPushButton *prevPageButton;
    QPushButton *nextPageButton;
    void addKeyboardMapping(const QString &keySequence, const QString &action) {
    // List of IME-related shortcuts that should not be intercepted
    QStringList imeShortcuts = {
        "Ctrl+Space",      // Primary IME toggle
        "Ctrl+Shift",      // Language switching
        "Ctrl+Alt",        // IME functions
        "Shift+Alt",       // Alternative language switching
        "Alt+Shift"        // Alternative language switching (reversed)
    };

    // Don't allow mapping of IME-related shortcuts
    if (imeShortcuts.contains(keySequence)) {
        qWarning() << "Cannot map IME-related shortcut:" << keySequence;
        return;
    }

    keyboardMappings[keySequence] = action;
    keyboardActionMapping[keySequence] = stringToAction(action);
    saveKeyboardMappings();
}
    void removeKeyboardMapping(const QString &keySequence) {
    keyboardMappings.remove(keySequence);
    keyboardActionMapping.remove(keySequence);
    saveKeyboardMappings();
}
    QMap<QString, QString> getKeyboardMappings() const {
    return keyboardMappings;
}
    void reconnectControllerSignals() {
    if (!controllerManager || !pageDial) {
        return;
    }

    // Reset internal dial state
    tracking = false;
    accumulatedRotation = 0;
    grossTotalClicks = 0;
    tempClicks = 0;
    lastAngle = 0;
    startAngle = 0;
    pendingPageFlip = 0;
    accumulatedRotationAfterLimit = 0;

    // Disconnect all existing connections to avoid duplicates
    disconnect(controllerManager, nullptr, this, nullptr);
    disconnect(controllerManager, nullptr, pageDial, nullptr);

    // Reconnect all controller signals
    connect(controllerManager, &SDLControllerManager::buttonHeld, this, &MainWindow::handleButtonHeld);
    connect(controllerManager, &SDLControllerManager::buttonReleased, this, &MainWindow::handleButtonReleased);
    connect(controllerManager, &SDLControllerManager::leftStickAngleChanged, pageDial, &QDial::setValue);
    connect(controllerManager, &SDLControllerManager::leftStickReleased, pageDial, &QDial::sliderReleased);
    connect(controllerManager, &SDLControllerManager::buttonSinglePress, this, &MainWindow::handleControllerButton);

    // Re-establish dial mode connections by changing to current mode
    DialMode currentMode = currentDialMode;
    changeDialMode(currentMode);

    // Update dial display to reflect current state
    updateDialDisplay();

    // qDebug() << "Controller signals reconnected successfully";
}
    void updateDialButtonState() {
    // Check if dial is visible
    bool isDialVisible = dialContainer && dialContainer->isVisible();

    if (dialToggleButton) {
        dialToggleButton->setProperty("selected", isDialVisible);

        // Force style update
        dialToggleButton->style()->unpolish(dialToggleButton);
        dialToggleButton->style()->polish(dialToggleButton);
    }
}
    void updateFastForwardButtonState() {
    if (fastForwardButton) {
        fastForwardButton->setProperty("selected", fastForwardMode);

        // Force style update
        fastForwardButton->style()->unpolish(fastForwardButton);
        fastForwardButton->style()->polish(fastForwardButton);
    }
}
    void updateToolButtonStates() {
    if (!currentCanvas()) return;

    // Reset all tool buttons
    penToolButton->setProperty("selected", false);
    markerToolButton->setProperty("selected", false);
    eraserToolButton->setProperty("selected", false);

    // Set the selected property for the current tool
    ToolType currentTool = currentCanvas()->getCurrentTool();
    switch (currentTool) {
        case ToolType::Pen:
            penToolButton->setProperty("selected", true);
            break;
        case ToolType::Marker:
            markerToolButton->setProperty("selected", true);
            break;
        case ToolType::Eraser:
            eraserToolButton->setProperty("selected", true);
            break;
    }

    // Force style update
    penToolButton->style()->unpolish(penToolButton);
    penToolButton->style()->polish(penToolButton);
    markerToolButton->style()->unpolish(markerToolButton);
    markerToolButton->style()->polish(markerToolButton);
    eraserToolButton->style()->unpolish(eraserToolButton);
    eraserToolButton->style()->polish(eraserToolButton);
}
    void handleColorButtonClick() {
    if (!currentCanvas()) return;

    ToolType currentTool = currentCanvas()->getCurrentTool();

    // If in eraser mode, switch back to pen mode
    if (currentTool == ToolType::Eraser) {
        currentCanvas()->setTool(ToolType::Pen);
        updateToolButtonStates();
        updateThicknessSliderForCurrentTool();
    }

    // If rope tool is enabled, turn it off
    if (currentCanvas()->isRopeToolMode()) {
        currentCanvas()->setRopeToolMode(false);
        updateRopeToolButtonState();
    }

    // For marker and straight line mode, leave them as they are
    // No special handling needed - they can work with color changes
}
    void updateThicknessSliderForCurrentTool() {
    if (!currentCanvas() || !thicknessSlider) return;

    // Block signals to prevent recursive calls
    thicknessSlider->blockSignals(true);

    // Update slider to reflect current tool's thickness
    qreal currentThickness = currentCanvas()->getPenThickness();

    // Convert thickness back to slider value (reverse of updateThickness calculation)
    qreal visualThickness = currentThickness * (currentCanvas()->getZoom() / 100.0);
    int sliderValue = qBound(1, static_cast<int>(qRound(visualThickness)), 50);

    thicknessSlider->setValue(sliderValue);
    thicknessSlider->blockSignals(false);
}
    void updatePdfTextSelectButtonState() {
    // Check if PDF text selection is enabled
    bool isEnabled = currentCanvas() && currentCanvas()->isPdfTextSelectionEnabled();

    if (pdfTextSelectButton) {
        pdfTextSelectButton->setProperty("selected", isEnabled);

        // Force style update (uses the same buttonStyle as other toggle buttons)
        pdfTextSelectButton->style()->unpolish(pdfTextSelectButton);
        pdfTextSelectButton->style()->polish(pdfTextSelectButton);
    }
}
  private:
    void toggleBenchmark() {
    benchmarking = !benchmarking;
    if (benchmarking) {
        currentCanvas()->startBenchmark();
        benchmarkTimer->start(1000); // Update every second
    } else {
        currentCanvas()->stopBenchmark();
        benchmarkTimer->stop();
        benchmarkLabel->setText(tr("PR:N/A"));
    }
}
    void updateBenchmarkDisplay() {
    int sampleRate = currentCanvas()->getProcessedRate();
    benchmarkLabel->setText(QString(tr("PR:%1 Hz")).arg(sampleRate));
}
    void applyCustomColor() {
    QString colorCode = customColorInput->text();
    if (!colorCode.startsWith("#")) {
        colorCode.prepend("#");
    }
    currentCanvas()->setPenColor(QColor(colorCode));
    updateDialDisplay();
}
    void updateThickness(int value) {
    // Calculate thickness based on the slider value at 100% zoom
    // The slider value represents the desired visual thickness
    qreal visualThickness = value; // Scale slider value to reasonable thickness

    // Apply zoom scaling to maintain visual consistency
    qreal actualThickness = visualThickness * (100.0 / currentCanvas()->getZoom());

    currentCanvas()->setPenThickness(actualThickness);
}
    void adjustThicknessForZoom(int oldZoom, int newZoom) {
    // Adjust all tool thicknesses to maintain visual consistency when zoom changes
    if (oldZoom == newZoom || oldZoom <= 0 || newZoom <= 0) return;

    InkCanvas* canvas = currentCanvas();
    if (!canvas) return;

    qreal zoomRatio = qreal(oldZoom) / qreal(newZoom);
    ToolType currentTool = canvas->getCurrentTool();

    // Adjust thickness for all tools, not just the current one
    canvas->adjustAllToolThicknesses(zoomRatio);

    // Update the thickness slider to reflect the current tool's new thickness
    updateThicknessSliderForCurrentTool();

    // ✅ FIXED: Update dial display to show new thickness immediately during zoom changes
    updateDialDisplay();
}
    void changeTool(int index) {
    if (index == 0) {
        currentCanvas()->setTool(ToolType::Pen);
    } else if (index == 1) {
        currentCanvas()->setTool(ToolType::Marker);
    } else if (index == 2) {
        currentCanvas()->setTool(ToolType::Eraser);
    }
    updateToolButtonStates();
    updateThicknessSliderForCurrentTool();
    updateDialDisplay();
}
    void selectFolder() {
    QString folder = QFileDialog::getExistingDirectory(this, tr("Select Save Folder"));
    if (!folder.isEmpty()) {
        InkCanvas *canvas = currentCanvas();
        if (canvas) {
            if (canvas->isEdited()){
                saveCurrentPage();
            }
            canvas->setSaveFolder(folder);
        switchPageWithDirection(1, 1); // Going to page 1 is forward direction
        pageInput->setValue(1);
        updateTabLabel();
            recentNotebooksManager->addRecentNotebook(folder, canvas); // Track when folder is selected
        }
    }
}
    void saveCanvas() {
    currentCanvas()->saveToFile(getCurrentPageForCanvas(currentCanvas()));
}
    void saveAnnotated() {
    currentCanvas()->saveAnnotated(getCurrentPageForCanvas(currentCanvas()));
}
    void deleteCurrentPage() {
    currentCanvas()->deletePage(getCurrentPageForCanvas(currentCanvas()));
}
    void loadPdf() {
    InkCanvas *canvas = currentCanvas();
    if (!canvas) return;

    QString saveFolder = canvas->getSaveFolder();
    QString tempDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/temp_session";

    // Check if no save folder is set or if it's the temporary directory
    if (saveFolder.isEmpty() || saveFolder == tempDir) {
        QMessageBox::warning(this, tr("Cannot Load PDF"),
            tr("Please select a permanent save folder before loading a PDF.\n\nClick the folder icon to choose a location for your notebook."));
        return;
    }

    QString filePath = QFileDialog::getOpenFileName(this, tr("Select PDF"), "", "PDF Files (*.pdf)");
    if (!filePath.isEmpty()) {
        currentCanvas()->loadPdf(filePath);
        updateTabLabel(); // ✅ Update the tab name after assigning a PDF
        updateZoom(); // ✅ Update zoom and pan range after PDF is loaded

        // Refresh PDF outline if sidebar is visible
        if (outlineSidebarVisible) {
            loadPdfOutline();
        }
    }
}
    void clearPdf() {
    InkCanvas *canvas = currentCanvas();
    if (!canvas) return;

    canvas->clearPdf();
    updateZoom(); // ✅ Update zoom and pan range after PDF is cleared
}
    void updateZoom() {
    InkCanvas *canvas = currentCanvas();
    if (canvas) {
        canvas->setZoom(zoomSlider->value());
        canvas->setLastZoomLevel(zoomSlider->value());  // ✅ Store zoom level per tab
        updatePanRange();
        // updateThickness(thicknessSlider->value()); // ✅ REMOVED: This was resetting thickness on page switch
        // updateDialDisplay();
    }
}
    void onZoomSliderChanged(int value) {
    // This handles manual zoom slider changes and preserves thickness
    int oldZoom = currentCanvas() ? currentCanvas()->getZoom() : 100;
    int newZoom = value;

    updateZoom();
    adjustThicknessForZoom(oldZoom, newZoom); // Maintain visual thickness consistency
}
    void applyZoom() {
    bool ok;
    int zoomValue = zoomInput->text().toInt(&ok);
    if (ok && zoomValue > 0) {
        currentCanvas()->setZoom(zoomValue);
        updatePanRange(); // Update slider range after zoom change
    }
}
    void updatePanRange() {
    int zoom = currentCanvas()->getZoom();

    QSize canvasSize = currentCanvas()->getCanvasSize();
    QSize viewportSize = QGuiApplication::primaryScreen()->size() * QGuiApplication::primaryScreen()->devicePixelRatio();
    qreal dpr = initialDpr;

    // Get the actual widget size instead of screen size for more accurate calculation
    QSize actualViewportSize = size();

    // Adjust viewport size for toolbar and tab bar layout
    QSize effectiveViewportSize = actualViewportSize;
    int toolbarHeight = isToolbarTwoRows ? 80 : 50; // Toolbar height
    int tabBarHeight = (tabBarContainer && tabBarContainer->isVisible()) ? 38 : 0; // Tab bar height
    effectiveViewportSize.setHeight(actualViewportSize.height() - toolbarHeight - tabBarHeight);

    // Calculate scaled canvas size using proper DPR scaling
    int scaledCanvasWidth = canvasSize.width() * zoom / 100;
    int scaledCanvasHeight = canvasSize.height() * zoom / 100;

    // Calculate max pan values - if canvas is smaller than viewport, pan should be 0
    int maxPanX = qMax(0, scaledCanvasWidth - effectiveViewportSize.width());
    int maxPanY = qMax(0, scaledCanvasHeight - effectiveViewportSize.height());

    // Scale the pan range properly
    int maxPanX_scaled = maxPanX * 100 / zoom;
    int maxPanY_scaled = maxPanY * 100 / zoom;

    // Set range to 0 when canvas is smaller than viewport (centered)
    if (scaledCanvasWidth <= effectiveViewportSize.width()) {
        panXSlider->setRange(0, 0);
        panXSlider->setValue(0);
        // No need for horizontal scrollbar
        panXSlider->setVisible(false);
    } else {
    panXSlider->setRange(0, maxPanX_scaled);
        // Show scrollbar only if mouse is near and timeout hasn't occurred
        if (scrollbarsVisible && !scrollbarHideTimer->isActive()) {
            scrollbarHideTimer->start();
        }
    }

    if (scaledCanvasHeight <= effectiveViewportSize.height()) {
        panYSlider->setRange(0, 0);
        panYSlider->setValue(0);
        // No need for vertical scrollbar
        panYSlider->setVisible(false);
    } else {
        panYSlider->setRange(0, maxPanY_scaled);
        // Show scrollbar only if mouse is near and timeout hasn't occurred
        if (scrollbarsVisible && !scrollbarHideTimer->isActive()) {
            scrollbarHideTimer->start();
        }
    }
}
    void updatePanX(int value) {
    InkCanvas *canvas = currentCanvas();
    if (canvas) {
        canvas->setPanX(value);
        canvas->setLastPanX(value);  // ✅ Store panX per tab

        // Show horizontal scrollbar temporarily
        if (panXSlider->maximum() > 0) {
            panXSlider->setVisible(true);
            scrollbarsVisible = true;

            // Make sure scrollbar position matches the canvas position
            if (panXSlider->value() != value) {
                panXSlider->blockSignals(true);
                panXSlider->setValue(value);
                panXSlider->blockSignals(false);
            }

            if (scrollbarHideTimer->isActive()) {
                scrollbarHideTimer->stop();
            }
            scrollbarHideTimer->start();
        }
    }
}
    void updatePanY(int value) {
    InkCanvas *canvas = currentCanvas();
    if (canvas) {
        canvas->setPanY(value);
        canvas->setLastPanY(value);  // ✅ Store panY per tab

        // Show vertical scrollbar temporarily
        if (panYSlider->maximum() > 0) {
            panYSlider->setVisible(true);
            scrollbarsVisible = true;

            // Make sure scrollbar position matches the canvas position
            if (panYSlider->value() != value) {
                panYSlider->blockSignals(true);
                panYSlider->setValue(value);
                panYSlider->blockSignals(false);
            }

            if (scrollbarHideTimer->isActive()) {
                scrollbarHideTimer->stop();
            }
            scrollbarHideTimer->start();
        }
    }
}
    void selectBackground() {
    QString filePath = QFileDialog::getOpenFileName(this, tr("Select Background Image"), "", "Images (*.png *.jpg *.jpeg)");
    if (!filePath.isEmpty()) {
        currentCanvas()->setBackground(filePath, getCurrentPageForCanvas(currentCanvas()));
        updateZoom(); // ✅ Update zoom and pan range after background is set
    }
}
    void forceUIRefresh() {
    setWindowState(Qt::WindowNoState);  // Restore first
    setWindowState(Qt::WindowMaximized);  // Maximize again
}
    void switchTab(int index) {
    if (!canvasStack || !tabList || !pageInput || !zoomSlider || !panXSlider || !panYSlider) {
        // qDebug() << "Error: switchTab() called before UI was fully initialized!";
        return;
    }

    if (index >= 0 && index < canvasStack->count()) {
        canvasStack->setCurrentIndex(index);

        // Ensure the tab list selection is properly set and styled
        if (tabList->currentRow() != index) {
            tabList->setCurrentRow(index);
        }

        InkCanvas *canvas = currentCanvas();
        if (canvas) {
            int savedPage = canvas->getLastActivePage();

            // ✅ Only call blockSignals if pageInput is valid
            if (pageInput) {
                pageInput->blockSignals(true);
                pageInput->setValue(savedPage + 1);
                pageInput->blockSignals(false);
            }

            // ✅ Ensure zoomSlider exists before calling methods
            if (zoomSlider) {
                zoomSlider->blockSignals(true);
                zoomSlider->setValue(canvas->getLastZoomLevel());
                zoomSlider->blockSignals(false);
                canvas->setZoom(canvas->getLastZoomLevel());
            }

            // ✅ Ensure pan sliders exist before modifying values
            if (panXSlider && panYSlider) {
                panXSlider->blockSignals(true);
                panYSlider->blockSignals(true);
                panXSlider->setValue(canvas->getLastPanX());
                panYSlider->setValue(canvas->getLastPanY());
                panXSlider->blockSignals(false);
                panYSlider->blockSignals(false);
                updatePanRange();
            }
            updateDialDisplay();
            updateColorButtonStates();  // Update button states when switching tabs
            updateStraightLineButtonState();  // Update straight line button state when switching tabs
            updateRopeToolButtonState(); // Update rope tool button state when switching tabs
            updateMarkdownButtonState(); // Update markdown button state when switching tabs
            updatePdfTextSelectButtonState(); // Update PDF text selection button state when switching tabs
            updateBookmarkButtonState(); // Update bookmark button state when switching tabs
            updateDialButtonState();     // Update dial button state when switching tabs
            updateFastForwardButtonState(); // Update fast forward button state when switching tabs
            updateToolButtonStates();   // Update tool button states when switching tabs
            updateThicknessSliderForCurrentTool(); // Update thickness slider for current tool when switching tabs

            // Refresh PDF outline if sidebar is visible
            if (outlineSidebarVisible) {
                loadPdfOutline();
            }
        }
    }
}
    void addNewTab() {
    if (!tabList || !canvasStack) return;  // Ensure tabList and canvasStack exist

    int newTabIndex = tabList->count();  // New tab index
    QWidget *tabWidget = new QWidget();  // Custom tab container
    tabWidget->setObjectName("tabWidget"); // Name the widget for easy retrieval later
    QHBoxLayout *tabLayout = new QHBoxLayout(tabWidget);
    tabLayout->setContentsMargins(5, 2, 5, 2);

    // ✅ Create the label (Tab Name) - optimized for horizontal layout
    QLabel *tabLabel = new QLabel(QString("Tab %1").arg(newTabIndex + 1), tabWidget);
    tabLabel->setObjectName("tabLabel"); // ✅ Name the label for easy retrieval later
    tabLabel->setWordWrap(false); // ✅ No wrapping for horizontal tabs
    tabLabel->setFixedWidth(115); // ✅ Narrower width for compact layout
    tabLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
    tabLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); // Left-align to show filename start
    tabLabel->setTextFormat(Qt::PlainText); // Ensure plain text for proper eliding
    // Tab label styling will be updated by theme

    // ✅ Create the close button (❌) - styled for browser-like tabs
    QPushButton *closeButton = new QPushButton(tabWidget);
    closeButton->setFixedSize(14, 14); // Smaller to fit narrower tabs
    closeButton->setIcon(loadThemedIcon("cross")); // Set themed icon
    closeButton->setStyleSheet(R"(
        QPushButton {
            border: none;
            background: transparent;
            border-radius: 6px;
            padding: 1px;
        }
        QPushButton:hover {
            background: rgba(255, 100, 100, 150);
            border-radius: 6px;
        }
        QPushButton:pressed {
            background: rgba(255, 50, 50, 200);
            border-radius: 6px;
        }
    )"); // Themed styling with hover and press effects

    // ✅ Create new InkCanvas instance EARLIER so it can be captured by the lambda
    InkCanvas *newCanvas = new InkCanvas(this);

    // ✅ Handle tab closing when the button is clicked
    connect(closeButton, &QPushButton::clicked, this, [=]() { // newCanvas is now captured

        // Prevent closing if it's the last remaining tab
        if (tabList->count() <= 1) {
            // Optional: show a message or do nothing silently
            QMessageBox::information(this, tr("Notice"), tr("At least one tab must remain open."));

            return;
        }

        // Find the index of the tab associated with this button's parent (tabWidget)
        int indexToRemove = -1;
        // newCanvas is captured by the lambda, representing the canvas of the tab being closed.
        // tabWidget is also captured.
        for (int i = 0; i < tabList->count(); ++i) {
            if (tabList->itemWidget(tabList->item(i)) == tabWidget) {
                indexToRemove = i;
                break;
            }
        }

        if (indexToRemove == -1) {
            qWarning() << "Could not find tab to remove based on tabWidget.";
            // Fallback or error handling if needed, though this shouldn't happen if tabWidget is valid.
            // As a fallback, try to find the index based on newCanvas if lists are in sync.
            for (int i = 0; i < canvasStack->count(); ++i) {
                if (canvasStack->widget(i) == newCanvas) {
                    indexToRemove = i;
                    break;
                }
            }
            if (indexToRemove == -1) {
                 qWarning() << "Could not find tab to remove based on newCanvas either.";
                 return; // Critical error, cannot proceed.
            }
        }

        // At this point, newCanvas is the InkCanvas instance for the tab being closed.
        // And indexToRemove is its index in tabList and canvasStack.

        // 1. Ensure the notebook has a unique save folder if it's temporary/edited
        ensureTabHasUniqueSaveFolder(newCanvas); // Pass the specific canvas

        // 2. Get the final save folder path
        QString folderPath = newCanvas->getSaveFolder();
        QString tempDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/temp_session";

        // 3. Update cover preview and recent list if it's a permanent notebook
        if (!folderPath.isEmpty() && folderPath != tempDir && recentNotebooksManager) {
            recentNotebooksManager->generateAndSaveCoverPreview(folderPath, newCanvas);
            // Add/update in recent list. This also moves it to the top.
            recentNotebooksManager->addRecentNotebook(folderPath, newCanvas);
        }

        // 4. Update the tab's label directly as folderPath might have changed
        QLabel *label = tabWidget->findChild<QLabel*>("tabLabel");
        if (label) {
            QString tabNameText;
            if (!folderPath.isEmpty() && folderPath != tempDir) { // Only for permanent notebooks
                QString metadataFile = folderPath + "/.pdf_path.txt";
                if (QFile::exists(metadataFile)) {
                    QFile file(metadataFile);
                    if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
                        QTextStream in(&file);
                        QString pdfPath = in.readLine().trimmed();
                        file.close();
                        if (QFile::exists(pdfPath)) { // Check if PDF file actually exists
                            tabNameText = elideTabText(QFileInfo(pdfPath).fileName(), 90); // Elide to fit tab width
                        }
                    }
                }
                // Fallback to folder name if no PDF or PDF path invalid
                if (tabNameText.isEmpty()) {
                    tabNameText = elideTabText(QFileInfo(folderPath).fileName(), 90); // Elide to fit tab width
                }
            }
            // Only update the label if a new valid name was determined.
            // If it's still a temp folder, the original "Tab X" label remains appropriate.
            if (!tabNameText.isEmpty()) {
                label->setText(tabNameText);
            }
        }

        // 5. Remove the tab
        removeTabAt(indexToRemove);
    });

    // ✅ Add widgets to the tab layout
    tabLayout->addWidget(tabLabel);
    tabLayout->addWidget(closeButton);
    tabLayout->setStretch(0, 1);
    tabLayout->setStretch(1, 0);

    // ✅ Create the tab item and set widget (horizontal layout)
    QListWidgetItem *tabItem = new QListWidgetItem();
    tabItem->setSizeHint(QSize(135, 22)); // ✅ Narrower and thinner for compact layout
    tabList->addItem(tabItem);
    tabList->setItemWidget(tabItem, tabWidget);  // Attach tab layout

    canvasStack->addWidget(newCanvas);

    // ✅ Connect touch gesture signals
    connect(newCanvas, &InkCanvas::zoomChanged, this, &MainWindow::handleTouchZoomChange);
    connect(newCanvas, &InkCanvas::panChanged, this, &MainWindow::handleTouchPanChange);
    connect(newCanvas, &InkCanvas::touchGestureEnded, this, &MainWindow::handleTouchGestureEnd);
    connect(newCanvas, &InkCanvas::ropeSelectionCompleted, this, &MainWindow::showRopeSelectionMenu);
    connect(newCanvas, &InkCanvas::pdfLinkClicked, this, [this](int targetPage) {
        // Navigate to the target page when a PDF link is clicked
        if (targetPage >= 0 && targetPage < 9999) {
            switchPageWithDirection(targetPage + 1, (targetPage + 1 > getCurrentPageForCanvas(currentCanvas()) + 1) ? 1 : -1);
            pageInput->setValue(targetPage + 1);
        }
    });
    connect(newCanvas, &InkCanvas::pdfLoaded, this, [this]() {
        // Refresh PDF outline if sidebar is visible
        if (outlineSidebarVisible) {
            loadPdfOutline();
        }
    });
    connect(newCanvas, &InkCanvas::markdownSelectionModeChanged, this, &MainWindow::updateMarkdownButtonState);

    // Install event filter to detect mouse movement for scrollbar visibility
    newCanvas->setMouseTracking(true);
    newCanvas->installEventFilter(this);

    // Disable tablet tracking for canvases for now to prevent crashes
    // TODO: Find a safer way to implement hover tooltips without tablet tracking
    // QTimer::singleShot(50, this, [newCanvas]() {
    //     try {
    //         if (newCanvas && newCanvas->window() && newCanvas->window()->windowHandle()) {
    //             newCanvas->setAttribute(Qt::WA_TabletTracking, true);
    //         }
    //     } catch (...) {
    //         // Silently ignore tablet tracking errors
    //     }
    // });

    // ✅ Apply touch gesture setting
    newCanvas->setTouchGesturesEnabled(touchGesturesEnabled);

    pageMap[newCanvas] = 0;

    // ✅ Select the new tab
    tabList->setCurrentItem(tabItem);
    canvasStack->setCurrentWidget(newCanvas);

    zoomSlider->setValue(100 / initialDpr); // Set initial zoom level based on DPR
    updateDialDisplay();
    updateStraightLineButtonState();  // Initialize straight line button state for the new tab
    updateRopeToolButtonState(); // Initialize rope tool button state for the new tab
    updatePdfTextSelectButtonState(); // Initialize PDF text selection button state for the new tab
    updateBookmarkButtonState(); // Initialize bookmark button state for the new tab
    updateMarkdownButtonState(); // Initialize markdown button state for the new tab
    updateDialButtonState();     // Initialize dial button state for the new tab
    updateFastForwardButtonState(); // Initialize fast forward button state for the new tab
    updateToolButtonStates();   // Initialize tool button states for the new tab

    QString tempDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/temp_session";
    newCanvas->setSaveFolder(tempDir);

    // Load persistent background settings
    BackgroundStyle defaultStyle;
    QColor defaultColor;
    int defaultDensity;
    loadDefaultBackgroundSettings(defaultStyle, defaultColor, defaultDensity);

    newCanvas->setBackgroundStyle(defaultStyle);
    newCanvas->setBackgroundColor(defaultColor);
    newCanvas->setBackgroundDensity(defaultDensity);
    newCanvas->setPDFRenderDPI(getPdfDPI());

    // Update color button states for the new tab
    updateColorButtonStates();
}
    void removeTabAt(int index) {
    if (!tabList || !canvasStack) return; // Ensure UI elements exist
    if (index < 0 || index >= canvasStack->count()) return;

    // ✅ Remove tab entry
    QListWidgetItem *item = tabList->takeItem(index);
    delete item;

    // ✅ Remove and delete the canvas safely
    QWidget *canvasWidget = canvasStack->widget(index); // Get widget before removal
    // ensureTabHasUniqueSaveFolder(currentCanvas()); // Moved to the close button lambda

    if (canvasWidget) {
        canvasStack->removeWidget(canvasWidget); // Remove from stack
        // InkCanvas *canvasInstance = qobject_cast<InkCanvas*>(canvasWidget);
        // if (canvasInstance) {
        //     QString folderPath = canvasInstance->getSaveFolder();
        //     QString tempDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/temp_session";
        //     if (!folderPath.isEmpty() && folderPath != tempDir && recentNotebooksManager) {
        //         recentNotebooksManager->addRecentNotebook(folderPath, canvasInstance); // Moved to close button lambda
        //     }
        // }
        delete canvasWidget; // Now delete the widget (and its InkCanvas)
    }

    // ✅ Select the previous tab (or first tab if none left)
    if (tabList->count() > 0) {
        int newIndex = qMax(0, index - 1);
        tabList->setCurrentRow(newIndex);
        canvasStack->setCurrentWidget(canvasStack->widget(newIndex));
    }

    // QWidget *canvasWidget = canvasStack->widget(index); // Redeclaration - remove this block
    // InkCanvas *canvasInstance = qobject_cast<InkCanvas*>(canvasWidget);
    //
    // if (canvasInstance) {
    //     QString folderPath = canvasInstance->getSaveFolder();
    //     if (!folderPath.isEmpty() && folderPath != QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/temp_session") {
    //         // recentNotebooksManager->addRecentNotebook(folderPath, canvasInstance); // Moved to close button lambda
    //     }
    // }
}
    void toggleZoomSlider() {
    if (zoomFrame->isVisible()) {
        zoomFrame->hide();
        return;
    }

    // ✅ Set as a standalone pop-up window so it can receive events
    zoomFrame->setWindowFlags(Qt::Popup);

    // ✅ Position it right below the button
    QPoint buttonPos = zoomButton->mapToGlobal(QPoint(0, zoomButton->height()));
    zoomFrame->move(buttonPos.x(), buttonPos.y() + 5);
    zoomFrame->show();
}
    void toggleThicknessSlider() {
    if (thicknessFrame->isVisible()) {
        thicknessFrame->hide();
        return;
    }

    // ✅ Set as a standalone pop-up window so it can receive events
    thicknessFrame->setWindowFlags(Qt::Popup);

    // ✅ Position it right below the button
    QPoint buttonPos = thicknessButton->mapToGlobal(QPoint(0, thicknessButton->height()));
    thicknessFrame->move(buttonPos.x(), buttonPos.y() + 5);

    thicknessFrame->show();
}
    void toggleFullscreen() {
    if (isFullScreen()) {
        showNormal();  // Exit fullscreen mode
    } else {
        showFullScreen();  // Enter fullscreen mode
    }
}
    void showJumpToPageDialog() {
    bool ok;
    int currentPage = getCurrentPageForCanvas(currentCanvas()) + 1;  // ✅ Convert zero-based to one-based
    int newPage = QInputDialog::getInt(this, "Jump to Page", "Enter Page Number:",
                                       currentPage, 1, 9999, 1, &ok);
    if (ok) {
        // ✅ Use direction-aware page switching for jump-to-page
        int direction = (newPage > currentPage) ? 1 : (newPage < currentPage) ? -1 : 0;
        if (direction != 0) {
            switchPageWithDirection(newPage, direction);
        } else {
            switchPage(newPage); // Same page, no direction needed
        }
        pageInput->setValue(newPage);
    }
}
    void goToPreviousPage() {
    int currentPage = getCurrentPageForCanvas(currentCanvas()) + 1;  // Convert to 1-based
    if (currentPage > 1) {
        int newPage = currentPage - 1;
        switchPageWithDirection(newPage, -1); // -1 indicates backward
        pageInput->setValue(newPage);
    }
}
    void goToNextPage() {
    int currentPage = getCurrentPageForCanvas(currentCanvas()) + 1;  // Convert to 1-based
    int newPage = currentPage + 1;
    switchPageWithDirection(newPage, 1); // 1 indicates forward
    pageInput->setValue(newPage);
}
    void onPageInputChanged(int newPage) {
    int currentPage = getCurrentPageForCanvas(currentCanvas()) + 1;  // Convert to 1-based

    // ✅ Use direction-aware page switching for spinbox
    int direction = (newPage > currentPage) ? 1 : (newPage < currentPage) ? -1 : 0;
    if (direction != 0) {
        switchPageWithDirection(newPage, direction);
    } else {
        switchPage(newPage); // Same page, no direction needed
    }
}
    void toggleDial() {
    if (!dialContainer) {
        // ✅ Create floating container for the dial
        dialContainer = new QWidget(this);
        dialContainer->setObjectName("dialContainer");
        dialContainer->setFixedSize(140, 140);
        dialContainer->setAttribute(Qt::WA_TranslucentBackground);
        dialContainer->setAttribute(Qt::WA_NoSystemBackground);
        dialContainer->setAttribute(Qt::WA_OpaquePaintEvent);
        dialContainer->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
        dialContainer->setStyleSheet("background: transparent; border-radius: 100px;");  // ✅ More transparent

        // ✅ Create dial
        pageDial = new QDial(dialContainer);
        pageDial->setFixedSize(140, 140);
        pageDial->setMinimum(0);
        pageDial->setMaximum(360);
        pageDial->setWrapping(true);  // ✅ Allow full-circle rotation

        // Apply theme color immediately when dial is created
        QColor accentColor = getAccentColor();
        pageDial->setStyleSheet(QString(R"(
        QDial {
            background-color: %1;
            }
        )").arg(accentColor.name()));

        /*

        modeDial = new QDial(dialContainer);
        modeDial->setFixedSize(150, 150);
        modeDial->setMinimum(0);
        modeDial->setMaximum(300);  // 6 modes, 60° each
        modeDial->setSingleStep(60);
        modeDial->setWrapping(true);
        modeDial->setStyleSheet("background:rgb(0, 76, 147);");
        modeDial->move(25, 25);

        */

        dialColorPreview = new QFrame(dialContainer);
        dialColorPreview->setFixedSize(30, 30);
        dialColorPreview->setStyleSheet("border-radius: 15px; border: 1px solid black;");
        dialColorPreview->move(55, 35); // Center of dial

        dialIconView = new QLabel(dialContainer);
        dialIconView->setFixedSize(30, 30);
        dialIconView->setStyleSheet("border-radius: 1px; border: 1px solid black;");
        dialIconView->move(55, 35); // Center of dial

        // ✅ Position dial near top-right corner initially
        positionDialContainer();

        dialDisplay = new QLabel(dialContainer);
        dialDisplay->setAlignment(Qt::AlignCenter);
        dialDisplay->setFixedSize(80, 80);
        dialDisplay->move(30, 30);  // Center position inside the dial

        int fontId = QFontDatabase::addApplicationFont(":/resources/fonts/Jersey20-Regular.ttf");
        // int chnFontId = QFontDatabase::addApplicationFont(":/resources/fonts/NotoSansSC-Medium.ttf");
        QStringList fontFamilies = QFontDatabase::applicationFontFamilies(fontId);

        if (!fontFamilies.isEmpty()) {
            QFont pixelFont(fontFamilies.at(0), 11);
            dialDisplay->setFont(pixelFont);
        }

        dialDisplay->setStyleSheet("background-color: black; color: white; font-size: 14px; border-radius: 4px;");

        dialHiddenButton = new QPushButton(dialContainer);
        dialHiddenButton->setFixedSize(80, 80);
        dialHiddenButton->move(30, 30); // Same position as dialDisplay
        dialHiddenButton->setStyleSheet("background: transparent; border: none;"); // ✅ Fully invisible
        dialHiddenButton->setFocusPolicy(Qt::NoFocus); // ✅ Prevents accidental focus issues
        dialHiddenButton->setEnabled(false);  // ✅ Disabled by default

        // ✅ Connection will be set in changeDialMode() based on current mode

        dialColorPreview->raise();  // ✅ Ensure it's on top
        dialIconView->raise();
        // ✅ Connect dial input and release
        // connect(pageDial, &QDial::valueChanged, this, &MainWindow::handleDialInput);
        // connect(pageDial, &QDial::sliderReleased, this, &MainWindow::onDialReleased);

        // connect(modeDial, &QDial::valueChanged, this, &MainWindow::handleModeSelection);
        changeDialMode(currentDialMode);  // ✅ Set initial mode

        // ✅ Enable drag detection
        dialContainer->installEventFilter(this);
    }

    // ✅ Ensure that `dialContainer` is always initialized before setting visibility
    if (dialContainer) {
        dialContainer->setVisible(!dialContainer->isVisible());
    }

    initializeDialSound();  // ✅ Ensure sound is loaded

    // Inside toggleDial():

    if (!dialDisplay) {
        dialDisplay = new QLabel(dialContainer);
    }
    updateDialDisplay(); // ✅ Ensure it's updated before showing

    if (controllerManager) {
        connect(controllerManager, &SDLControllerManager::buttonHeld, this, &MainWindow::handleButtonHeld);
        connect(controllerManager, &SDLControllerManager::buttonReleased, this, &MainWindow::handleButtonReleased);
        connect(controllerManager, &SDLControllerManager::leftStickAngleChanged, pageDial, &QDial::setValue);
        connect(controllerManager, &SDLControllerManager::leftStickReleased, pageDial, &QDial::sliderReleased);
        connect(controllerManager, &SDLControllerManager::buttonSinglePress, this, &MainWindow::handleControllerButton);
    }

    loadButtonMappings();  // ✅ Load button mappings for the controller

    // Update button state to reflect dial visibility
    updateDialButtonState();
}
    void positionDialContainer() {
    if (!dialContainer) return;

    // Get window dimensions
    int windowWidth = width();
    int windowHeight = height();

    // Get dial dimensions
    int dialWidth = dialContainer->width();   // 140px
    int dialHeight = dialContainer->height(); // 140px

    // Calculate toolbar height based on current layout
    int toolbarHeight = isToolbarTwoRows ? 80 : 50; // Approximate heights

    // Add tab bar height if visible
    int tabBarHeight = (tabBarContainer && tabBarContainer->isVisible()) ? 38 : 0;

    // Define margins from edges
    int rightMargin = 20;  // Distance from right edge
    int topMargin = 20;    // Distance from top edge (below toolbar and tabs)

    // Calculate ideal position (top-right corner with margins)
    int idealX = windowWidth - dialWidth - rightMargin;
    int idealY = toolbarHeight + tabBarHeight + topMargin;

    // Ensure dial stays within window bounds with minimum margins
    int minMargin = 10;
    int maxX = windowWidth - dialWidth - minMargin;
    int maxY = windowHeight - dialHeight - minMargin;

    // Clamp position to stay within bounds
    int finalX = qBound(minMargin, idealX, maxX);
    int finalY = qBound(toolbarHeight + tabBarHeight + minMargin, idealY, maxY);

    // Move the dial to the calculated position
    dialContainer->move(finalX, finalY);
}
    void handleDialInput(int angle) {
    if (!tracking) {
        startAngle = angle;  // ✅ Set initial position
        accumulatedRotation = 0;  // ✅ Reset tracking
        tracking = true;
        lastAngle = angle;
        return;
    }

    int delta = angle - lastAngle;

    // ✅ Handle 360-degree wrapping
    if (delta > 180) delta -= 360;  // Example: 350° → 10° should be -20° instead of +340°
    if (delta < -180) delta += 360; // Example: 10° → 350° should be +20° instead of -340°

    accumulatedRotation += delta;  // ✅ Accumulate movement

    // ✅ Detect crossing a 45-degree boundary
    int currentClicks = accumulatedRotation / 45; // Total number of "clicks" crossed
    int previousClicks = (accumulatedRotation - delta) / 45; // Previous click count

    if (currentClicks != previousClicks) {  // ✅ Play sound if a new boundary is crossed

        if (dialClickSound) {
            dialClickSound->play();

            // ✅ Vibrate controller
            SDL_Joystick *joystick = controllerManager->getJoystick();
            if (joystick) {
                // Note: SDL_JoystickRumble requires SDL 2.0.9+
                // For older versions, this will be a no-op
                #if SDL_VERSION_ATLEAST(2, 0, 9)
                SDL_JoystickRumble(joystick, 0xA000, 0xF000, 10);  // Vibrate shortly
                #endif
            }

            grossTotalClicks += 1;
            tempClicks = currentClicks;
            updateDialDisplay();

            if (isLowResPreviewEnabled()) {
                int previewPage = qBound(1, getCurrentPageForCanvas(currentCanvas()) + currentClicks, 99999);
                currentCanvas()->loadPdfPreviewAsync(previewPage);
            }
        }
    }

    lastAngle = angle;  // ✅ Store last position
}
    void onDialReleased() {
    if (!tracking) return;  // ✅ Ignore if no tracking

    int pagesToAdvance = fastForwardMode ? 8 : 1;
    int totalClicks = accumulatedRotation / 45;  // ✅ Convert degrees to pages

    /*
    int leftOver = accumulatedRotation % 45;  // ✅ Track remaining rotation
    if (leftOver > 22 && totalClicks >= 0) {
        totalClicks += 1;  // ✅ Round up if more than halfway
    }
    else if (leftOver <= -22 && totalClicks >= 0) {
        totalClicks -= 1;  // ✅ Round down if more than halfway
    }
    */

    if (totalClicks != 0 || grossTotalClicks != 0) {  // ✅ Only switch pages if movement happened
        // Use concurrent autosave for smoother dial operation
        if (currentCanvas()->isEdited()) {
            saveCurrentPageConcurrent();
        }

        int currentPage = getCurrentPageForCanvas(currentCanvas()) + 1;
        int newPage = qBound(1, currentPage + totalClicks * pagesToAdvance, 99999);

        // ✅ Use direction-aware page switching for dial
        int direction = (totalClicks * pagesToAdvance > 0) ? 1 : -1;
        switchPageWithDirection(newPage, direction);
        pageInput->setValue(newPage);
        tempClicks = 0;
        updateDialDisplay();
        /*
        if (dialClickSound) {
            dialClickSound->play();
        }
        */
    }

    accumulatedRotation = 0;  // ✅ Reset tracking
    grossTotalClicks = 0;
    tracking = false;
}
    void initializeDialSound() {
    if (!dialClickSound) {
        dialClickSound = new QSoundEffect(this);
        dialClickSound->setSource(QUrl::fromLocalFile(":/resources/sounds/dial_click.wav")); // ✅ Path to the sound file
        dialClickSound->setVolume(0.8);  // ✅ Set volume (0.0 - 1.0)
    }
}
    void changeDialMode(DialMode mode) {

    if (!dialContainer) return;  // ✅ Ensure dial container exists
    currentDialMode = mode; // ✅ Set new mode
    updateDialDisplay();

    // ✅ Enable dialHiddenButton for PanAndPageScroll and ZoomControl modes
    dialHiddenButton->setEnabled(currentDialMode == PanAndPageScroll || currentDialMode == ZoomControl);

    // ✅ Disconnect previous slots
    disconnect(pageDial, &QDial::valueChanged, nullptr, nullptr);
    disconnect(pageDial, &QDial::sliderReleased, nullptr, nullptr);

    // ✅ Disconnect dialHiddenButton to reconnect with appropriate function
    disconnect(dialHiddenButton, &QPushButton::clicked, nullptr, nullptr);

    // ✅ Connect dialHiddenButton to appropriate function based on mode
    if (currentDialMode == PanAndPageScroll) {
        connect(dialHiddenButton, &QPushButton::clicked, this, &MainWindow::toggleControlBar);
    } else if (currentDialMode == ZoomControl) {
        connect(dialHiddenButton, &QPushButton::clicked, this, &MainWindow::cycleZoomLevels);
    }

    dialColorPreview->hide();
    dialDisplay->setStyleSheet("background-color: black; color: white; font-size: 14px; border-radius: 40px;");

    // ✅ Connect the correct function set for the current mode
    switch (currentDialMode) {
        case PageSwitching:
            connect(pageDial, &QDial::valueChanged, this, &MainWindow::handleDialInput);
            connect(pageDial, &QDial::sliderReleased, this, &MainWindow::onDialReleased);
            break;
        case ZoomControl:
            connect(pageDial, &QDial::valueChanged, this, &MainWindow::handleDialZoom);
            connect(pageDial, &QDial::sliderReleased, this, &MainWindow::onZoomReleased);
            break;
        case ThicknessControl:
            connect(pageDial, &QDial::valueChanged, this, &MainWindow::handleDialThickness);
            connect(pageDial, &QDial::sliderReleased, this, &MainWindow::onThicknessReleased);
            break;

        case ToolSwitching:
            connect(pageDial, &QDial::valueChanged, this, &MainWindow::handleToolSelection);
            connect(pageDial, &QDial::sliderReleased, this, &MainWindow::onToolReleased);
            break;
        case PresetSelection:
            connect(pageDial, &QDial::valueChanged, this, &MainWindow::handlePresetSelection);
            connect(pageDial, &QDial::sliderReleased, this, &MainWindow::onPresetReleased);
            break;
        case PanAndPageScroll:
            connect(pageDial, &QDial::valueChanged, this, &MainWindow::handleDialPanScroll);
            connect(pageDial, &QDial::sliderReleased, this, &MainWindow::onPanScrollReleased);
            break;

    }
}
    void handleDialZoom(int angle) {
    if (!tracking) {
        startAngle = angle;
        accumulatedRotation = 0;
        tracking = true;
        lastAngle = angle;
        return;
    }

    int delta = angle - lastAngle;

    // ✅ Handle 360-degree wrapping
    if (delta > 180) delta -= 360;
    if (delta < -180) delta += 360;

    accumulatedRotation += delta;

    if (abs(delta) < 4) {
        return;
    }

    // ✅ Apply zoom dynamically (instead of waiting for release)
    int oldZoom = zoomSlider->value();
    int newZoom = qBound(10, oldZoom + (delta / 4), 400);
    zoomSlider->setValue(newZoom);
    updateZoom();  // ✅ Ensure zoom updates immediately
    updateDialDisplay();

    lastAngle = angle;
}
    void handleDialThickness(int angle) {
    if (!tracking) {
        startAngle = angle;
        tracking = true;
        lastAngle = angle;
        return;
    }

    int delta = angle - lastAngle;
    if (delta > 180) delta -= 360;
    if (delta < -180) delta += 360;

    int thicknessStep = fastForwardMode ? 5 : 1;
    currentCanvas()->setPenThickness(qBound<qreal>(1.0, currentCanvas()->getPenThickness() + (delta / 10.0) * thicknessStep, 50.0));

    updateDialDisplay();
    lastAngle = angle;
}
    void onZoomReleased() {
    accumulatedRotation = 0;
    tracking = false;
}
    void onThicknessReleased() {
    accumulatedRotation = 0;
    tracking = false;
}
    void updateDialDisplay() {
    if (!dialDisplay) return;
    if (!dialColorPreview) return;
    if (!dialIconView) return;
    dialIconView->show();
    qreal dpr = initialDpr;
    QColor currentColor = currentCanvas()->getPenColor();
    switch (currentDialMode) {
        case DialMode::PageSwitching:
            if (fastForwardMode){
                dialDisplay->setText(QString(tr("\n\nPage\n%1").arg(getCurrentPageForCanvas(currentCanvas()) + 1 + tempClicks * 8)));
            }
            else {
                dialDisplay->setText(QString(tr("\n\nPage\n%1").arg(getCurrentPageForCanvas(currentCanvas()) + 1 + tempClicks)));
            }
            dialIconView->setPixmap(QPixmap(":/resources/reversed_icons/bookpage_reversed.png").scaled(30, 30, Qt::KeepAspectRatio, Qt::SmoothTransformation));
            break;
        case DialMode::ThicknessControl:
            {
                QString toolName;
                switch (currentCanvas()->getCurrentTool()) {
                    case ToolType::Pen:
                        toolName = tr("Pen");
                        break;
                    case ToolType::Marker:
                        toolName = tr("Marker");
                        break;
                    case ToolType::Eraser:
                        toolName = tr("Eraser");
                        break;
                }
                dialDisplay->setText(QString(tr("\n\n%1\n%2").arg(toolName).arg(QString::number(currentCanvas()->getPenThickness(), 'f', 1))));
            dialIconView->setPixmap(QPixmap(":/resources/reversed_icons/thickness_reversed.png").scaled(30, 30, Qt::KeepAspectRatio, Qt::SmoothTransformation));
            }
            break;
        case DialMode::ZoomControl:
            dialDisplay->setText(QString(tr("\n\nZoom\n%1%").arg(currentCanvas() ? currentCanvas()->getZoom() * initialDpr : zoomSlider->value() * initialDpr)));
            dialIconView->setPixmap(QPixmap(":/resources/reversed_icons/zoom_reversed.png").scaled(30, 30, Qt::KeepAspectRatio, Qt::SmoothTransformation));
            break;

        case DialMode::ToolSwitching:
            // ✅ Convert ToolType to QString for display
            switch (currentCanvas()->getCurrentTool()) {
                case ToolType::Pen:
                    dialDisplay->setText(tr("\n\n\nPen"));
                    dialIconView->setPixmap(QPixmap(":/resources/reversed_icons/pen_reversed.png").scaled(30, 30, Qt::KeepAspectRatio, Qt::SmoothTransformation));
                    break;
                case ToolType::Marker:
                    dialDisplay->setText(tr("\n\n\nMarker"));
                    dialIconView->setPixmap(QPixmap(":/resources/reversed_icons/marker_reversed.png").scaled(30, 30, Qt::KeepAspectRatio, Qt::SmoothTransformation));
                    break;
                case ToolType::Eraser:
                    dialDisplay->setText(tr("\n\n\nEraser"));
                    dialIconView->setPixmap(QPixmap(":/resources/reversed_icons/eraser_reversed.png").scaled(30, 30, Qt::KeepAspectRatio, Qt::SmoothTransformation));
                    break;
            }
            break;
        case PresetSelection:
            dialColorPreview->show();
            dialIconView->hide();
            dialColorPreview->setStyleSheet(QString("background-color: %1; border-radius: 15px; border: 1px solid black;")
                                            .arg(colorPresets[currentPresetIndex].name()));
            dialDisplay->setText(QString(tr("\n\nPreset %1\n#%2"))
                                            .arg(currentPresetIndex + 1)  // ✅ Display preset index (1-based)
                                            .arg(colorPresets[currentPresetIndex].name().remove("#"))); // ✅ Display hex color
            // dialIconView->setPixmap(QPixmap(":/resources/reversed_icons/preset_reversed.png").scaled(30, 30, Qt::KeepAspectRatio, Qt::SmoothTransformation));
            break;
        case DialMode::PanAndPageScroll:
            dialIconView->setPixmap(QPixmap(":/resources/icons/scroll_reversed.png").scaled(30, 30, Qt::KeepAspectRatio, Qt::SmoothTransformation));
            QString fullscreenStatus = controlBarVisible ? tr("Etr") : tr("Exit");
            dialDisplay->setText(QString(tr("\n\nPage %1\n%2 FulScr")).arg(getCurrentPageForCanvas(currentCanvas()) + 1).arg(fullscreenStatus));
            break;
    }
}
    void handleToolSelection(int angle) {
    static int lastToolIndex = -1;  // ✅ Track last tool index

    // ✅ Snap to closest fixed 120° step
    int snappedAngle = (angle + 60) / 120 * 120;  // Round to nearest 120°
    int toolIndex = snappedAngle / 120;  // Convert to tool index (0, 1, 2)

    if (toolIndex >= 3) toolIndex = 0;  // ✅ Wrap around at 360° → Back to Pen (0)

    if (toolIndex != lastToolIndex) {  // ✅ Only switch if tool actually changes
        toolSelector->setCurrentIndex(toolIndex);  // ✅ Change tool
        lastToolIndex = toolIndex;  // ✅ Update last selected tool

        // ✅ Play click sound when tool changes
        if (dialClickSound) {
            dialClickSound->play();
        }

        SDL_Joystick *joystick = controllerManager->getJoystick();

        if (joystick) {
            #if SDL_VERSION_ATLEAST(2, 0, 9)
            SDL_JoystickRumble(joystick, 0xA000, 0xF000, 20);  // ✅ Vibrate controller
            #endif
        }

        updateToolButtonStates();  // ✅ Update tool button states
        updateDialDisplay();  // ✅ Update dial display]
    }
}
    void onToolReleased() {

}
    void handlePresetSelection(int angle) {
    static int lastAngle = angle;
    int delta = angle - lastAngle;

    // ✅ Handle 360-degree wrapping
    if (delta > 180) delta -= 360;
    if (delta < -180) delta += 360;

    if (abs(delta) >= 60) {  // ✅ Change preset every 60° (6 presets)
        lastAngle = angle;
        currentPresetIndex = (currentPresetIndex + (delta > 0 ? 1 : -1) + colorPresets.size()) % colorPresets.size();

        QColor selectedColor = colorPresets[currentPresetIndex];
        currentCanvas()->setPenColor(selectedColor);
        updateCustomColorButtonStyle(selectedColor);
        updateDialDisplay();
        updateColorButtonStates();  // Update button states when preset is selected

        if (dialClickSound) dialClickSound->play();  // ✅ Provide feedback
        SDL_Joystick *joystick = controllerManager->getJoystick();
            if (joystick) {
                #if SDL_VERSION_ATLEAST(2, 0, 9)
                SDL_JoystickRumble(joystick, 0xA000, 0xF000, 25);  // Vibrate shortly
                #endif
            }
    }
}
    void onPresetReleased() {
    accumulatedRotation = 0;
    tracking = false;
}
    void addColorPreset() {
    QColor currentColor = currentCanvas()->getPenColor();

    // ✅ Prevent duplicates
    if (!colorPresets.contains(currentColor)) {
        if (colorPresets.size() >= 6) {
            colorPresets.dequeue();  // ✅ Remove oldest color
        }
        colorPresets.enqueue(currentColor);
    }
}
    void updateColorPalette() {
    // Clear existing presets
    colorPresets.clear();
    currentPresetIndex = 0;

    // Add default pen color (theme-aware)
    colorPresets.enqueue(getDefaultPenColor());

    // Add palette colors
    colorPresets.enqueue(getPaletteColor("red"));
    colorPresets.enqueue(getPaletteColor("yellow"));
    colorPresets.enqueue(getPaletteColor("blue"));
    colorPresets.enqueue(getPaletteColor("green"));
    colorPresets.enqueue(QColor("#000000")); // Black (always same)
    colorPresets.enqueue(QColor("#FFFFFF")); // White (always same)

    // Only update UI elements if they exist
    if (redButton && blueButton && yellowButton && greenButton) {
        bool darkMode = isDarkMode();

        // Update color button icons based on current palette (not theme)
        QString redIconPath = useBrighterPalette ? ":/resources/icons/pen_light_red.png" : ":/resources/icons/pen_dark_red.png";
        QString blueIconPath = useBrighterPalette ? ":/resources/icons/pen_light_blue.png" : ":/resources/icons/pen_dark_blue.png";
        QString yellowIconPath = useBrighterPalette ? ":/resources/icons/pen_light_yellow.png" : ":/resources/icons/pen_dark_yellow.png";
        QString greenIconPath = useBrighterPalette ? ":/resources/icons/pen_light_green.png" : ":/resources/icons/pen_dark_green.png";

        redButton->setIcon(QIcon(redIconPath));
        blueButton->setIcon(QIcon(blueIconPath));
        yellowButton->setIcon(QIcon(yellowIconPath));
        greenButton->setIcon(QIcon(greenIconPath));

        // Update color button states
        updateColorButtonStates();
    }
}
    QColor getPaletteColor(const QString &colorName) {
    if (useBrighterPalette) {
        // Brighter colors (good for dark backgrounds)
        if (colorName == "red") return QColor("#FF7755");
        if (colorName == "yellow") return QColor("#EECC00");
        if (colorName == "blue") return QColor("#66CCFF");
        if (colorName == "green") return QColor("#55FF77");
    } else {
        // Darker colors (good for light backgrounds)
        if (colorName == "red") return QColor("#AA0000");
        if (colorName == "yellow") return QColor("#997700");
        if (colorName == "blue") return QColor("#0000AA");
        if (colorName == "green") return QColor("#007700");
    }

    // Fallback colors
    if (colorName == "black") return QColor("#000000");
    if (colorName == "white") return QColor("#FFFFFF");

    return QColor("#000000"); // Default fallback
}
    qreal getDevicePixelRatio() {
    QScreen *screen = QGuiApplication::primaryScreen();
    qreal devicePixelRatio = screen ? screen->devicePixelRatio() : 1.0; // Default to 1.0 if null
    return devicePixelRatio;
}
    bool isDarkMode() {
    QColor bg = palette().color(QPalette::Window);
    return bg.lightness() < 128;  // Lightness scale: 0 (black) - 255 (white)
}
    QIcon loadThemedIcon(const QString& baseName) {
    QString path = isDarkMode()
        ? QString(":/resources/icons/%1_reversed.png").arg(baseName)
        : QString(":/resources/icons/%1.png").arg(baseName);
    return QIcon(path);
}
    QString createButtonStyle(bool darkMode) {
    if (darkMode) {
        // Dark mode: Keep current white highlights (good contrast)
        return R"(
            QPushButton {
                background: transparent;
                border: none;
                padding: 6px;
            }
            QPushButton:hover {
                background: rgba(255, 255, 255, 50);
            }
            QPushButton:pressed {
                background: rgba(0, 0, 0, 50);
            }
            QPushButton[selected="true"] {
                background: rgba(255, 255, 255, 100);
                border: 2px solid rgba(255, 255, 255, 150);
                padding: 4px;
                border-radius: 4px;
            }
            QPushButton[selected="true"]:hover {
                background: rgba(255, 255, 255, 120);
            }
            QPushButton[selected="true"]:pressed {
                background: rgba(0, 0, 0, 50);
            }
        )";
    } else {
        // Light mode: Use darker colors for better visibility
        return R"(
            QPushButton {
                background: transparent;
                border: none;
                padding: 6px;
            }
            QPushButton:hover {
                background: rgba(0, 0, 0, 30);
            }
            QPushButton:pressed {
                background: rgba(0, 0, 0, 60);
            }
            QPushButton[selected="true"] {
                background: rgba(0, 0, 0, 80);
                border: 2px solid rgba(0, 0, 0, 120);
                padding: 4px;
                border-radius: 4px;
            }
            QPushButton[selected="true"]:hover {
                background: rgba(0, 0, 0, 100);
            }
            QPushButton[selected="true"]:pressed {
                background: rgba(0, 0, 0, 140);
            }
        )";
    }
}
    void handleDialPanScroll(int angle) {
    if (!tracking) {
        startAngle = angle;
        accumulatedRotation = 0;
        accumulatedRotationAfterLimit = 0;
        tracking = true;
        lastAngle = angle;
        pendingPageFlip = 0;
        return;
    }

    int delta = angle - lastAngle;

    // Handle 360 wrap
    if (delta > 180) delta -= 360;
    if (delta < -180) delta += 360;

    accumulatedRotation += delta;

    // Pan scroll
    int panDelta = delta * 4;  // Adjust scroll sensitivity here
    int currentPan = panYSlider->value();
    int newPan = currentPan + panDelta;

    // Clamp pan slider
    newPan = qBound(panYSlider->minimum(), newPan, panYSlider->maximum());
    panYSlider->setValue(newPan);

    // ✅ NEW → if slider reached top/bottom, accumulate AFTER LIMIT
    if (newPan == panYSlider->maximum()) {
        accumulatedRotationAfterLimit += delta;

        if (accumulatedRotationAfterLimit >= 120) {
            pendingPageFlip = +1;  // Flip next when released
        }
    }
    else if (newPan == panYSlider->minimum()) {
        accumulatedRotationAfterLimit += delta;

        if (accumulatedRotationAfterLimit <= -120) {
            pendingPageFlip = -1;  // Flip previous when released
        }
    }
    else {
        // Reset after limit accumulator when not at limit
        accumulatedRotationAfterLimit = 0;
        pendingPageFlip = 0;
    }

    lastAngle = angle;
}
    void onPanScrollReleased() {
    // ✅ Perform page flip only when dial released and flip is pending
    if (pendingPageFlip != 0) {
        // Use concurrent saving for smooth dial operation
        if (currentCanvas()->isEdited()) {
            saveCurrentPageConcurrent();
        }

        int currentPage = getCurrentPageForCanvas(currentCanvas());
        int newPage = qBound(1, currentPage + pendingPageFlip + 1, 99999);

        // ✅ Use direction-aware page switching for pan-and-scroll dial
        switchPageWithDirection(newPage, pendingPageFlip);
        pageInput->setValue(newPage);
        updateDialDisplay();

        SDL_Joystick *joystick = controllerManager->getJoystick();
        if (joystick) {
            #if SDL_VERSION_ATLEAST(2, 0, 9)
            SDL_JoystickRumble(joystick, 0xA000, 0xF000, 25);  // Vibrate shortly
            #endif
        }
    }

    // Reset states
    pendingPageFlip = 0;
    accumulatedRotation = 0;
    accumulatedRotationAfterLimit = 0;
    tracking = false;
}
    void handleTouchZoomChange(int newZoom) {
    // Update zoom slider without triggering updateZoom again
    zoomSlider->blockSignals(true);
    int oldZoom = zoomSlider->value();  // Get the old zoom value before updating the slider
    zoomSlider->setValue(newZoom);
    zoomSlider->blockSignals(false);

    // Show both scrollbars during gesture
    if (panXSlider->maximum() > 0) {
        panXSlider->setVisible(true);
    }
    if (panYSlider->maximum() > 0) {
        panYSlider->setVisible(true);
    }
    scrollbarsVisible = true;

    // Update canvas zoom directly
    InkCanvas *canvas = currentCanvas();
    if (canvas) {
        // The canvas zoom has already been set by the gesture processing in InkCanvas::event()
        // So we don't need to set it again, just update the last zoom level
        canvas->setLastZoomLevel(newZoom);
        updatePanRange();

        // ✅ FIXED: Add thickness adjustment for pinch-to-zoom gestures to maintain visual consistency
        adjustThicknessForZoom(oldZoom, newZoom);

        updateDialDisplay();
    }
}
    void handleTouchPanChange(int panX, int panY) {
    // Clamp values to valid ranges
    panX = qBound(panXSlider->minimum(), panX, panXSlider->maximum());
    panY = qBound(panYSlider->minimum(), panY, panYSlider->maximum());

    // Show both scrollbars during gesture
    if (panXSlider->maximum() > 0) {
        panXSlider->setVisible(true);
    }
    if (panYSlider->maximum() > 0) {
        panYSlider->setVisible(true);
    }
    scrollbarsVisible = true;

    // Update sliders without triggering their valueChanged signals
    panXSlider->blockSignals(true);
    panYSlider->blockSignals(true);
    panXSlider->setValue(panX);
    panYSlider->setValue(panY);
    panXSlider->blockSignals(false);
    panYSlider->blockSignals(false);

    // Update canvas pan directly
    InkCanvas *canvas = currentCanvas();
    if (canvas) {
        canvas->setPanX(panX);
        canvas->setPanY(panY);
        canvas->setLastPanX(panX);
        canvas->setLastPanY(panY);
    }
}
    void handleTouchGestureEnd() {
    // Hide scrollbars immediately when touch gesture ends
    panXSlider->setVisible(false);
    panYSlider->setVisible(false);
    scrollbarsVisible = false;
}
    void updateColorButtonStates() {
    // Check if there's a current canvas
    if (!currentCanvas()) return;

    // Get current pen color
    QColor currentColor = currentCanvas()->getPenColor();

    // Determine if we're in dark mode to match the correct colors
    bool darkMode = isDarkMode();

    // Reset all color buttons to original style
    redButton->setProperty("selected", false);
    blueButton->setProperty("selected", false);
    yellowButton->setProperty("selected", false);
    greenButton->setProperty("selected", false);
    blackButton->setProperty("selected", false);
    whiteButton->setProperty("selected", false);

    // Set the selected property for the matching color button based on current palette
    QColor redColor = getPaletteColor("red");
    QColor blueColor = getPaletteColor("blue");
    QColor yellowColor = getPaletteColor("yellow");
    QColor greenColor = getPaletteColor("green");

    if (currentColor == redColor) {
        redButton->setProperty("selected", true);
    } else if (currentColor == blueColor) {
        blueButton->setProperty("selected", true);
    } else if (currentColor == yellowColor) {
        yellowButton->setProperty("selected", true);
    } else if (currentColor == greenColor) {
        greenButton->setProperty("selected", true);
    } else if (currentColor == QColor("#000000")) {
        blackButton->setProperty("selected", true);
    } else if (currentColor == QColor("#FFFFFF")) {
        whiteButton->setProperty("selected", true);
    }

    // Force style update
    redButton->style()->unpolish(redButton);
    redButton->style()->polish(redButton);
    blueButton->style()->unpolish(blueButton);
    blueButton->style()->polish(blueButton);
    yellowButton->style()->unpolish(yellowButton);
    yellowButton->style()->polish(yellowButton);
    greenButton->style()->unpolish(greenButton);
    greenButton->style()->polish(greenButton);
    blackButton->style()->unpolish(blackButton);
    blackButton->style()->polish(blackButton);
    whiteButton->style()->unpolish(whiteButton);
    whiteButton->style()->polish(whiteButton);
}
    void selectColorButton(QPushButton* selectedButton) {
    updateColorButtonStates();
}
    void updateStraightLineButtonState() {
    // Check if there's a current canvas
    if (!currentCanvas()) return;

    // Update the button state to match the canvas straight line mode
    bool isEnabled = currentCanvas()->isStraightLineMode();

    // Set visual indicator that the button is active/inactive
    if (straightLineToggleButton) {
        straightLineToggleButton->setProperty("selected", isEnabled);

        // Force style update
        straightLineToggleButton->style()->unpolish(straightLineToggleButton);
        straightLineToggleButton->style()->polish(straightLineToggleButton);
    }
}
    void updateRopeToolButtonState() {
    // Check if there's a current canvas
    if (!currentCanvas()) return;

    // Update the button state to match the canvas rope tool mode
    bool isEnabled = currentCanvas()->isRopeToolMode();

    // Set visual indicator that the button is active/inactive
    if (ropeToolButton) {
        ropeToolButton->setProperty("selected", isEnabled);

        // Force style update
        ropeToolButton->style()->unpolish(ropeToolButton);
        ropeToolButton->style()->polish(ropeToolButton);
    }
}
    void updateMarkdownButtonState() {
    // Check if there's a current canvas
    if (!currentCanvas()) return;

    // Update the button state to match the canvas markdown selection mode
    bool isEnabled = currentCanvas()->isMarkdownSelectionMode();

    // Set visual indicator that the button is active/inactive
    if (markdownButton) {
        markdownButton->setProperty("selected", isEnabled);

        // Force style update
        markdownButton->style()->unpolish(markdownButton);
        markdownButton->style()->polish(markdownButton);
    }
}
    void setPenTool() {
    if (!currentCanvas()) return;
    currentCanvas()->setTool(ToolType::Pen);
    updateToolButtonStates();
    updateThicknessSliderForCurrentTool();
    updateDialDisplay();
}
    void setMarkerTool() {
    if (!currentCanvas()) return;
    currentCanvas()->setTool(ToolType::Marker);
    updateToolButtonStates();
    updateThicknessSliderForCurrentTool();
    updateDialDisplay();
}
    void setEraserTool() {
    if (!currentCanvas()) return;
    currentCanvas()->setTool(ToolType::Eraser);
    updateToolButtonStates();
    updateThicknessSliderForCurrentTool();
    updateDialDisplay();
}
    QColor getContrastingTextColor(const QColor &backgroundColor) {
    // Calculate relative luminance using the formula from WCAG 2.0
    double r = backgroundColor.redF();
    double g = backgroundColor.greenF();
    double b = backgroundColor.blueF();

    // Gamma correction
    r = (r <= 0.03928) ? r/12.92 : pow((r + 0.055)/1.055, 2.4);
    g = (g <= 0.03928) ? g/12.92 : pow((g + 0.055)/1.055, 2.4);
    b = (b <= 0.03928) ? b/12.92 : pow((b + 0.055)/1.055, 2.4);

    // Calculate luminance
    double luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;

    // Use white text for darker backgrounds
    return (luminance < 0.5) ? Qt::white : Qt::black;
}
    void updateCustomColorButtonStyle(const QColor &color) {
    QColor textColor = getContrastingTextColor(color);
    customColorButton->setStyleSheet(QString("background-color: %1; color: %2")
        .arg(color.name())
        .arg(textColor.name()));
    customColorButton->setText(QString("%1").arg(color.name()).toUpper());
}
    void openRecentNotebooksDialog() {
    RecentNotebooksDialog dialog(this, recentNotebooksManager, this);
    dialog.exec();
}
    void showPendingTooltip() {
    // This function is now unused since we disabled tablet tracking
    // Tooltips will work through normal mouse hover events instead
    // Keeping the function for potential future use
}
    void showRopeSelectionMenu(const QPoint &position) {
    // Create context menu for rope tool selection
    QMenu *contextMenu = new QMenu(this);
    contextMenu->setAttribute(Qt::WA_DeleteOnClose);

    // Add Copy action
    QAction *copyAction = contextMenu->addAction(tr("Copy"));
    copyAction->setIcon(loadThemedIcon("copy"));
    connect(copyAction, &QAction::triggered, this, [this]() {
        if (currentCanvas()) {
            currentCanvas()->copyRopeSelection();
        }
    });

    // Add Delete action
    QAction *deleteAction = contextMenu->addAction(tr("Delete"));
    deleteAction->setIcon(loadThemedIcon("trash"));
    connect(deleteAction, &QAction::triggered, this, [this]() {
        if (currentCanvas()) {
            currentCanvas()->deleteRopeSelection();
        }
    });

    // Add Cancel action
    QAction *cancelAction = contextMenu->addAction(tr("Cancel"));
    cancelAction->setIcon(loadThemedIcon("cross"));
    connect(cancelAction, &QAction::triggered, this, [this]() {
        if (currentCanvas()) {
            currentCanvas()->cancelRopeSelection();
        }
    });

    // Convert position from canvas coordinates to global coordinates
    QPoint globalPos = currentCanvas()->mapToGlobal(position);

    // Show the menu at the specified position
    contextMenu->popup(globalPos);
}
    void toggleOutlineSidebar() {
    outlineSidebarVisible = !outlineSidebarVisible;

    // Hide bookmarks sidebar if it's visible when opening outline
    if (outlineSidebarVisible && bookmarksSidebar && bookmarksSidebar->isVisible()) {
        bookmarksSidebar->setVisible(false);
        bookmarksSidebarVisible = false;
        // Update bookmarks button state
        if (toggleBookmarksButton) {
            toggleBookmarksButton->setProperty("selected", false);
            toggleBookmarksButton->style()->unpolish(toggleBookmarksButton);
            toggleBookmarksButton->style()->polish(toggleBookmarksButton);
        }
    }

    outlineSidebar->setVisible(outlineSidebarVisible);

    // Update button toggle state
    if (toggleOutlineButton) {
        toggleOutlineButton->setProperty("selected", outlineSidebarVisible);
        toggleOutlineButton->style()->unpolish(toggleOutlineButton);
        toggleOutlineButton->style()->polish(toggleOutlineButton);
    }

    // Load PDF outline when showing sidebar for the first time
    if (outlineSidebarVisible) {
        loadPdfOutline();
    }
}
    void onOutlineItemClicked(QTreeWidgetItem *item, int column) {
    Q_UNUSED(column);

    if (!item) return;

    // Get the page number stored in the item data
    QVariant pageData = item->data(0, Qt::UserRole);
    if (pageData.isValid()) {
        int pageNumber = pageData.toInt();
        if (pageNumber >= 0) {
            // Switch to the selected page (convert from 0-based to 1-based)
            switchPage(pageNumber + 1);
            pageInput->setValue(pageNumber + 1);
        }
    }
}
    void loadPdfOutline() {
    if (!outlineTree) return;

    outlineTree->clear();

    // Get current PDF document
    Poppler::Document* pdfDoc = getPdfDocument();
    if (!pdfDoc) return;

    // Get the outline from the PDF document
    QVector<Poppler::OutlineItem> outlineItems = pdfDoc->outline();

    if (outlineItems.isEmpty()) {
        // If no outline exists, show page numbers as fallback
        int pageCount = pdfDoc->numPages();
        for (int i = 0; i < pageCount; ++i) {
            QTreeWidgetItem* item = new QTreeWidgetItem(outlineTree);
            item->setText(0, QString(tr("Page %1")).arg(i + 1));
            item->setData(0, Qt::UserRole, i); // Store 0-based page index
        }
    } else {
        // Process the actual PDF outline
        for (const Poppler::OutlineItem& outlineItem : outlineItems) {
            addOutlineItem(outlineItem, nullptr);
        }
    }

    // Expand the first level by default
    outlineTree->expandToDepth(0);
}
    void addOutlineItem(const Poppler::OutlineItem& outlineItem, QTreeWidgetItem* parentItem) {
    if (outlineItem.isNull()) return;

    QTreeWidgetItem* item;
    if (parentItem) {
        item = new QTreeWidgetItem(parentItem);
    } else {
        item = new QTreeWidgetItem(outlineTree);
    }

    // Set the title
    item->setText(0, outlineItem.name());

    // Try to get the page number from the destination
    int pageNumber = -1;
    auto destination = outlineItem.destination();
    if (destination) {
        pageNumber = destination->pageNumber();
    }

    // Store the page number (1-based) in the item data
    if (pageNumber >= 0) {
        item->setData(0, Qt::UserRole, pageNumber + 1); // Convert to 1-based
    }

    // Add child items recursively
    if (outlineItem.hasChildren()) {
        QVector<Poppler::OutlineItem> children = outlineItem.children();
        for (const Poppler::OutlineItem& child : children) {
            addOutlineItem(child, item);
        }
    }
}
    Poppler::Document* getPdfDocument();
    void toggleBookmarksSidebar() {
    if (!bookmarksSidebar) return;

    bool isVisible = bookmarksSidebar->isVisible();

    // Hide outline sidebar if it's visible
    if (!isVisible && outlineSidebar && outlineSidebar->isVisible()) {
        outlineSidebar->setVisible(false);
        outlineSidebarVisible = false;
        // Update outline button state
        if (toggleOutlineButton) {
            toggleOutlineButton->setProperty("selected", false);
            toggleOutlineButton->style()->unpolish(toggleOutlineButton);
            toggleOutlineButton->style()->polish(toggleOutlineButton);
        }
    }

    bookmarksSidebar->setVisible(!isVisible);
    bookmarksSidebarVisible = !isVisible;

    // Update button toggle state
    if (toggleBookmarksButton) {
        toggleBookmarksButton->setProperty("selected", bookmarksSidebarVisible);
        toggleBookmarksButton->style()->unpolish(toggleBookmarksButton);
        toggleBookmarksButton->style()->polish(toggleBookmarksButton);
    }

    if (bookmarksSidebarVisible) {
        loadBookmarks(); // Refresh bookmarks when opening
    }
}
    void onBookmarkItemClicked(QTreeWidgetItem *item, int column) {
    Q_UNUSED(column);
    if (!item) return;

    // Get the page number from the item data
    bool ok;
    int pageNumber = item->data(0, Qt::UserRole).toInt(&ok);
    if (ok && pageNumber > 0) {
        // Navigate to the bookmarked page
        switchPageWithDirection(pageNumber, (pageNumber > getCurrentPageForCanvas(currentCanvas()) + 1) ? 1 : -1);
        pageInput->setValue(pageNumber);
    }
}
    void loadBookmarks() {
    if (!bookmarksTree || !currentCanvas()) return;

    bookmarksTree->clear();

    // Get the current notebook's save folder
    QString saveFolder = currentCanvas()->getSaveFolder();
    if (saveFolder.isEmpty()) return;

    QString bookmarksFile = saveFolder + "/.bookmarks.txt";
    QFile file(bookmarksFile);

    bookmarks.clear();

    if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        QTextStream in(&file);
        while (!in.atEnd()) {
            QString line = in.readLine().trimmed();
            if (line.isEmpty()) continue;

            QStringList parts = line.split('\t', Qt::KeepEmptyParts);
            if (parts.size() >= 2) {
                bool ok;
                int pageNum = parts[0].toInt(&ok);
                if (ok) {
                    QString title = parts[1];
                    bookmarks[pageNum] = title;
                }
            }
        }
        file.close();
    }

    // Populate the tree widget
    for (auto it = bookmarks.begin(); it != bookmarks.end(); ++it) {
        QTreeWidgetItem *item = new QTreeWidgetItem(bookmarksTree);

        // Create a custom widget for each bookmark item
        QWidget *itemWidget = new QWidget();
        QHBoxLayout *itemLayout = new QHBoxLayout(itemWidget);
        itemLayout->setContentsMargins(5, 2, 5, 2);
        itemLayout->setSpacing(5);

        // Page number label (fixed width)
        QLabel *pageLabel = new QLabel(QString(tr("Page %1")).arg(it.key()));
        pageLabel->setFixedWidth(60);
        pageLabel->setStyleSheet("font-weight: bold; color: #666;");
        itemLayout->addWidget(pageLabel);

        // Editable title (supports Windows handwriting if available)
        QLineEdit *titleEdit = new QLineEdit(it.value());
        titleEdit->setPlaceholderText("Enter bookmark title...");
        titleEdit->setProperty("pageNumber", it.key()); // Store page number for saving

        // Enable IME support for multi-language input
        titleEdit->setAttribute(Qt::WA_InputMethodEnabled, true);
        titleEdit->setInputMethodHints(Qt::ImhNone); // Allow all input methods
        titleEdit->installEventFilter(this); // Install event filter for IME handling

        // Connect to save when editing is finished
        connect(titleEdit, &QLineEdit::editingFinished, this, [this, titleEdit]() {
            int pageNum = titleEdit->property("pageNumber").toInt();
            QString newTitle = titleEdit->text().trimmed();

            if (newTitle.isEmpty()) {
                // Remove bookmark if title is empty
                bookmarks.remove(pageNum);
            } else {
                // Update bookmark title
                bookmarks[pageNum] = newTitle;
            }
            saveBookmarks();
            updateBookmarkButtonState(); // Update button state
        });

        itemLayout->addWidget(titleEdit, 1);

        // Store page number in item data for navigation
        item->setData(0, Qt::UserRole, it.key());

        // Set the custom widget
        bookmarksTree->setItemWidget(item, 0, itemWidget);

        // Set item height
        item->setSizeHint(0, QSize(0, 30));
    }

    updateBookmarkButtonState(); // Update button state after loading
}
    void saveBookmarks() {
    if (!currentCanvas()) return;

    QString saveFolder = currentCanvas()->getSaveFolder();
    if (saveFolder.isEmpty()) return;

    QString bookmarksFile = saveFolder + "/.bookmarks.txt";
    QFile file(bookmarksFile);

    if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
        QTextStream out(&file);

        // Sort bookmarks by page number
        QList<int> sortedPages = bookmarks.keys();
        std::sort(sortedPages.begin(), sortedPages.end());

        for (int pageNum : sortedPages) {
            out << pageNum << '\t' << bookmarks[pageNum] << '\n';
        }

        file.close();
    }
}
    void toggleCurrentPageBookmark() {
    if (!currentCanvas()) return;

    int currentPage = getCurrentPageForCanvas(currentCanvas()) + 1; // Convert to 1-based

    if (bookmarks.contains(currentPage)) {
        // Remove bookmark
        bookmarks.remove(currentPage);
    } else {
        // Add bookmark with default title
        QString defaultTitle = QString(tr("Bookmark %1")).arg(currentPage);
        bookmarks[currentPage] = defaultTitle;
    }

    saveBookmarks();
    updateBookmarkButtonState();

    // Refresh bookmarks view if visible
    if (bookmarksSidebarVisible) {
        loadBookmarks();
    }
}
    void updateBookmarkButtonState() {
    if (!toggleBookmarkButton || !currentCanvas()) return;

    int currentPage = getCurrentPageForCanvas(currentCanvas()) + 1; // Convert to 1-based
    bool isBookmarked = bookmarks.contains(currentPage);

    toggleBookmarkButton->setProperty("selected", isBookmarked);

    // Update tooltip
    if (isBookmarked) {
        toggleBookmarkButton->setToolTip(tr("Remove Bookmark"));
    } else {
        toggleBookmarkButton->setToolTip(tr("Add Bookmark"));
    }

    // Force style update
    toggleBookmarkButton->style()->unpolish(toggleBookmarkButton);
    toggleBookmarkButton->style()->polish(toggleBookmarkButton);
}
  private:
    InkCanvas *canvas;
    QPushButton *benchmarkButton;
    QLabel *benchmarkLabel;
    QTimer *benchmarkTimer;
    bool benchmarking;
    QPushButton *exportNotebookButton;
    QPushButton *importNotebookButton;
    QPushButton *redButton;
    QPushButton *blueButton;
    QPushButton *yellowButton;
    QPushButton *greenButton;
    QPushButton *blackButton;
    QPushButton *whiteButton;
    QLineEdit *customColorInput;
    QPushButton *customColorButton;
    QPushButton *thicknessButton;
    QSlider *thicknessSlider;
    QFrame *thicknessFrame;
    QComboBox *toolSelector;
    QPushButton *penToolButton;
    QPushButton *markerToolButton;
    QPushButton *eraserToolButton;
    QPushButton *deletePageButton;
    QPushButton *selectFolderButton;
    QPushButton *saveButton;
    QPushButton *saveAnnotatedButton;
    QPushButton *fullscreenButton;
    QPushButton *openControlPanelButton;
    QPushButton *openRecentNotebooksButton;
    QPushButton *loadPdfButton;
    QPushButton *clearPdfButton;
    QPushButton *pdfTextSelectButton;
    QPushButton *toggleTabBarButton;
    QMap<InkCanvas*, int> pageMap;
    QPushButton *backgroundButton;
    QPushButton *straightLineToggleButton;
    QPushButton *ropeToolButton;
    QPushButton *markdownButton;
    QSlider *zoomSlider;
    QPushButton *zoomButton;
    QFrame *zoomFrame;
    QPushButton *dezoomButton;
    QPushButton *zoom50Button;
    QPushButton *zoom200Button;
    QWidget *zoomContainer;
    QLineEdit *zoomInput;
    QScrollBar *panXSlider;
    QScrollBar *panYSlider;
    QListWidget *tabList;
    QStackedWidget *canvasStack;
    QPushButton *addTabButton;
    QWidget *tabBarContainer;
    QWidget *outlineSidebar;
    QTreeWidget *outlineTree;
    QPushButton *toggleOutlineButton;
    bool outlineSidebarVisible = false;
    QWidget *bookmarksSidebar;
    QTreeWidget *bookmarksTree;
    QPushButton *toggleBookmarksButton;
    QPushButton *toggleBookmarkButton;
    QPushButton *touchGesturesButton;
    bool bookmarksSidebarVisible = false;
    QMap<int, QString> bookmarks;
    QPushButton *jumpToPageButton;
    QWidget *dialContainer = nullptr;
    QDial *pageDial = nullptr;
    QDial *modeDial = nullptr;
    QPushButton *dialToggleButton;
    bool fastForwardMode = false;
    QPushButton *fastForwardButton;
    int lastAngle = 0;
    int startAngle = 0;
    bool tracking = false;
    int accumulatedRotation = 0;
    QSoundEffect *dialClickSound = nullptr;
    int grossTotalClicks = 0;
    DialMode currentDialMode = PanAndPageScroll;
    DialMode temporaryDialMode = None;
    QComboBox *dialModeSelector;
    QPushButton *colorPreview;
    QLabel *dialDisplay = nullptr;
    QFrame *dialColorPreview;
    QLabel *dialIconView;
    QFont pixelFont;
    QPushButton *btnPageSwitch;
    QPushButton *btnZoom;
    QPushButton *btnThickness;
    QPushButton *btnTool;
    QPushButton *btnPresets;
    QPushButton *btnPannScroll;
    int tempClicks = 0;
    QPushButton *dialHiddenButton;
    QQueue<QColor> colorPresets;
    QPushButton *addPresetButton;
    int currentPresetIndex = 0;
    bool useBrighterPalette = false;
    qreal initialDpr = 1.0;
    QWidget *sidebarContainer;
    QWidget *controlBar;
    void setTemporaryDialMode(DialMode mode) {
    if (temporaryDialMode == None) {
        temporaryDialMode = currentDialMode;
    }
    changeDialMode(mode);
}
    void clearTemporaryDialMode() {
    if (temporaryDialMode != None) {
        changeDialMode(temporaryDialMode);
        temporaryDialMode = None;
    }
}
    bool controlBarVisible = true;
    void toggleControlBar() {
    // Proper fullscreen toggle: handle both sidebar and control bar

    if (controlBarVisible) {
        // Going into fullscreen mode

        // First, remember current tab bar state
        sidebarWasVisibleBeforeFullscreen = tabBarContainer->isVisible();

        // Hide tab bar if it's visible
        if (tabBarContainer->isVisible()) {
            tabBarContainer->setVisible(false);
        }

        // Hide control bar
        controlBarVisible = false;
        controlBar->setVisible(false);

        // Hide floating popup widgets when control bar is hidden to prevent stacking
        if (zoomFrame && zoomFrame->isVisible()) zoomFrame->hide();
        if (thicknessFrame && thicknessFrame->isVisible()) thicknessFrame->hide();

        // Hide orphaned widgets that are not added to any layout
        if (colorPreview) colorPreview->hide();
        if (thicknessButton) thicknessButton->hide();
        if (jumpToPageButton) jumpToPageButton->hide();

        if (toolSelector) toolSelector->hide();
        if (zoomButton) zoomButton->hide();
        if (customColorInput) customColorInput->hide();

        // Find and hide local widgets that might be orphaned
        QList<QComboBox*> comboBoxes = findChildren<QComboBox*>();
        for (QComboBox* combo : comboBoxes) {
            if (combo->parent() == this && !combo->isVisible()) {
                // Already hidden, keep it hidden
            } else if (combo->parent() == this) {
                // This might be the orphaned dialModeSelector or similar
                combo->hide();
            }
        }
    } else {
        // Coming out of fullscreen mode

        // Restore control bar
        controlBarVisible = true;
        controlBar->setVisible(true);

        // Restore tab bar to its previous state
        tabBarContainer->setVisible(sidebarWasVisibleBeforeFullscreen);

        // Show orphaned widgets when control bar is visible
        // Note: These are kept hidden normally since they're not in the layout
        // Only show them if they were specifically intended to be visible
    }

    // Update dial display to reflect new status
    updateDialDisplay();

    // Force layout update to recalculate space
    if (auto *canvas = currentCanvas()) {
        QTimer::singleShot(0, this, [this, canvas]() {
            canvas->setMaximumSize(canvas->getCanvasSize());
        });
    }
}
    void cycleZoomLevels() {
    if (!zoomSlider) return;

    int currentZoom = zoomSlider->value();
    int targetZoom;

    // Calculate the scaled zoom levels based on initial DPR
    int zoom50 = 50 / initialDpr;
    int zoom100 = 100 / initialDpr;
    int zoom200 = 200 / initialDpr;

    // Cycle through 0.5x -> 1x -> 2x -> 0.5x...
    if (currentZoom <= zoom50 + 5) { // Close to 0.5x (with small tolerance)
        targetZoom = zoom100; // Go to 1x
    } else if (currentZoom <= zoom100 + 5) { // Close to 1x
        targetZoom = zoom200; // Go to 2x
    } else { // Any other zoom level or close to 2x
        targetZoom = zoom50; // Go to 0.5x
    }

    zoomSlider->setValue(targetZoom);
    updateZoom();
    updateDialDisplay();
}
    bool sidebarWasVisibleBeforeFullscreen = true;
    int accumulatedRotationAfterLimit = 0;
    int pendingPageFlip = 0;
    QMap<QString, QString> buttonHoldMapping;
    QMap<QString, QString> buttonPressMapping;
    QMap<QString, ControllerAction> buttonPressActionMapping;
    QMap<QString, QString> keyboardMappings;
    QMap<QString, ControllerAction> keyboardActionMapping;
    QTimer *tooltipTimer;
    QWidget *lastHoveredWidget;
    QPoint pendingTooltipPos;
    void handleButtonHeld(const QString &buttonName) {
    QString mode = buttonHoldMapping.value(buttonName, "None");
    if (mode != "None") {
        setTemporaryDialMode(dialModeFromString(mode));
        return;
    }
}
    void handleButtonReleased(const QString &buttonName) {
    QString mode = buttonHoldMapping.value(buttonName, "None");
    if (mode != "None") {
        clearTemporaryDialMode();
    }
}
    void handleControllerButton(const QString &buttonName) {  // This is for single press functions
    ControllerAction action = buttonPressActionMapping.value(buttonName, ControllerAction::None);

    switch (action) {
        case ControllerAction::ToggleFullscreen:
            fullscreenButton->click();
            break;
        case ControllerAction::ToggleDial:
            toggleDial();
            break;
        case ControllerAction::Zoom50:
            zoom50Button->click();
            break;
        case ControllerAction::ZoomOut:
            dezoomButton->click();
            break;
        case ControllerAction::Zoom200:
            zoom200Button->click();
            break;
        case ControllerAction::AddPreset:
            addPresetButton->click();
            break;
        case ControllerAction::DeletePage:
            deletePageButton->click();  // assuming you have this
            break;
        case ControllerAction::FastForward:
            fastForwardButton->click();  // assuming you have this
            break;
        case ControllerAction::OpenControlPanel:
            openControlPanelButton->click();
            break;
        case ControllerAction::RedColor:
            redButton->click();
            break;
        case ControllerAction::BlueColor:
            blueButton->click();
            break;
        case ControllerAction::YellowColor:
            yellowButton->click();
            break;
        case ControllerAction::GreenColor:
            greenButton->click();
            break;
        case ControllerAction::BlackColor:
            blackButton->click();
            break;
        case ControllerAction::WhiteColor:
            whiteButton->click();
            break;
        case ControllerAction::CustomColor:
            customColorButton->click();
            break;
        case ControllerAction::ToggleSidebar:
            toggleTabBarButton->click();
            break;
        case ControllerAction::Save:
            saveButton->click();
            break;
        case ControllerAction::StraightLineTool:
            straightLineToggleButton->click();
            break;
        case ControllerAction::RopeTool:
            ropeToolButton->click();
            break;
        case ControllerAction::SetPenTool:
            setPenTool();
            break;
        case ControllerAction::SetMarkerTool:
            setMarkerTool();
            break;
        case ControllerAction::SetEraserTool:
            setEraserTool();
            break;
        case ControllerAction::TogglePdfTextSelection:
            pdfTextSelectButton->click();
            break;
        case ControllerAction::ToggleOutline:
            toggleOutlineButton->click();
            break;
        case ControllerAction::ToggleBookmarks:
            toggleBookmarksButton->click();
            break;
        case ControllerAction::AddBookmark:
            toggleBookmarkButton->click();
            break;
        case ControllerAction::ToggleTouchGestures:
            touchGesturesButton->click();
            break;
        default:
            break;
    }
}
    void handleKeyboardShortcut(const QString &keySequence) {
    ControllerAction action = keyboardActionMapping.value(keySequence, ControllerAction::None);

    // Use the same handler as Joy-Con buttons
    switch (action) {
        case ControllerAction::ToggleFullscreen:
            fullscreenButton->click();
            break;
        case ControllerAction::ToggleDial:
            toggleDial();
            break;
        case ControllerAction::Zoom50:
            zoom50Button->click();
            break;
        case ControllerAction::ZoomOut:
            dezoomButton->click();
            break;
        case ControllerAction::Zoom200:
            zoom200Button->click();
            break;
        case ControllerAction::AddPreset:
            addPresetButton->click();
            break;
        case ControllerAction::DeletePage:
            deletePageButton->click();
            break;
        case ControllerAction::FastForward:
            fastForwardButton->click();
            break;
        case ControllerAction::OpenControlPanel:
            openControlPanelButton->click();
            break;
        case ControllerAction::RedColor:
            redButton->click();
            break;
        case ControllerAction::BlueColor:
            blueButton->click();
            break;
        case ControllerAction::YellowColor:
            yellowButton->click();
            break;
        case ControllerAction::GreenColor:
            greenButton->click();
            break;
        case ControllerAction::BlackColor:
            blackButton->click();
            break;
        case ControllerAction::WhiteColor:
            whiteButton->click();
            break;
        case ControllerAction::CustomColor:
            customColorButton->click();
            break;
        case ControllerAction::ToggleSidebar:
            toggleTabBarButton->click();
            break;
        case ControllerAction::Save:
            saveButton->click();
            break;
        case ControllerAction::StraightLineTool:
            straightLineToggleButton->click();
            break;
        case ControllerAction::RopeTool:
            ropeToolButton->click();
            break;
        case ControllerAction::SetPenTool:
            setPenTool();
            break;
        case ControllerAction::SetMarkerTool:
            setMarkerTool();
            break;
        case ControllerAction::SetEraserTool:
            setEraserTool();
            break;
        case ControllerAction::TogglePdfTextSelection:
            pdfTextSelectButton->click();
            break;
        case ControllerAction::ToggleOutline:
            toggleOutlineButton->click();
            break;
        case ControllerAction::ToggleBookmarks:
            toggleBookmarksButton->click();
            break;
        case ControllerAction::AddBookmark:
            toggleBookmarkButton->click();
            break;
        case ControllerAction::ToggleTouchGestures:
            touchGesturesButton->click();
            break;
        default:
            break;
    }
}
    void saveKeyboardMappings() {
    QSettings settings("SpeedyNote", "App");
    settings.beginGroup("KeyboardMappings");
    for (auto it = keyboardMappings.begin(); it != keyboardMappings.end(); ++it) {
        settings.setValue(it.key(), it.value());
    }
    settings.endGroup();
}
    void loadKeyboardMappings() {
    QSettings settings("SpeedyNote", "App");
    settings.beginGroup("KeyboardMappings");
    QStringList keys = settings.allKeys();

    // List of IME-related shortcuts that should not be intercepted
    QStringList imeShortcuts = {
        "Ctrl+Space",      // Primary IME toggle
        "Ctrl+Shift",      // Language switching
        "Ctrl+Alt",        // IME functions
        "Shift+Alt",       // Alternative language switching
        "Alt+Shift"        // Alternative language switching (reversed)
    };

    for (const QString &key : keys) {
        // Skip IME-related shortcuts
        if (imeShortcuts.contains(key)) {
            // Remove from settings if it exists
            settings.remove(key);
            continue;
        }

        QString value = settings.value(key).toString();
        keyboardMappings[key] = value;
        keyboardActionMapping[key] = stringToAction(value);
    }
    settings.endGroup();

    // Save settings to persist the removal of IME shortcuts
    settings.sync();
}
    void ensureTabHasUniqueSaveFolder(InkCanvas* canvas) {
    if (!canvas) return;

    if (canvasStack->count() == 0) return;

    QString currentFolder = canvas->getSaveFolder();
    QString tempFolder = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/temp_session";

    if (currentFolder.isEmpty() || currentFolder == tempFolder) {

        QDir sourceDir(tempFolder);
        QStringList pageFiles = sourceDir.entryList(QStringList() << "*.png", QDir::Files);

        // No pages to save → skip prompting
        if (pageFiles.isEmpty()) {
            return;
        }

        QMessageBox::warning(this, tr("Unsaved Notebook"),
                             tr("This notebook is still using a temporary session folder.\nPlease select a permanent folder to avoid data loss."));

        QString selectedFolder = QFileDialog::getExistingDirectory(this, tr("Select Save Folder"));
        if (selectedFolder.isEmpty()) return;

        QDir destDir(selectedFolder);
        if (!destDir.exists()) {
            QDir().mkpath(selectedFolder);
        }

        // Copy contents from temp to selected folder
        for (const QString &file : sourceDir.entryList(QDir::Files)) {
            QString srcFilePath = tempFolder + "/" + file;
            QString dstFilePath = selectedFolder + "/" + file;

            // If file already exists at destination, remove it to avoid rename failure
            if (QFile::exists(dstFilePath)) {
                QFile::remove(dstFilePath);
            }

            QFile::rename(srcFilePath, dstFilePath);  // This moves the file
        }

        canvas->setSaveFolder(selectedFolder);
        // updateTabLabel(); // No longer needed here, handled by the close button lambda
    }

    return;
}
    RecentNotebooksManager *recentNotebooksManager;
    int pdfRenderDPI = 192;
    void setupUi() {

    // Ensure IME is properly enabled for the application
    QInputMethod *inputMethod = QGuiApplication::inputMethod();
    if (inputMethod) {
        inputMethod->show();
        inputMethod->reset();
    }

    // Create theme-aware button style
    bool darkMode = isDarkMode();
    QString buttonStyle = createButtonStyle(darkMode);

    loadPdfButton = new QPushButton(this);
    clearPdfButton = new QPushButton(this);
    loadPdfButton->setFixedSize(26, 30);
    clearPdfButton->setFixedSize(26, 30);
    QIcon pdfIcon(loadThemedIcon("pdf"));  // Path to your icon in resources
    QIcon pdfDeleteIcon(loadThemedIcon("pdfdelete"));  // Path to your icon in resources
    loadPdfButton->setIcon(pdfIcon);
    clearPdfButton->setIcon(pdfDeleteIcon);
    loadPdfButton->setStyleSheet(buttonStyle);
    clearPdfButton->setStyleSheet(buttonStyle);
    loadPdfButton->setToolTip(tr("Load PDF"));
    clearPdfButton->setToolTip(tr("Clear PDF"));
    connect(loadPdfButton, &QPushButton::clicked, this, &MainWindow::loadPdf);
    connect(clearPdfButton, &QPushButton::clicked, this, &MainWindow::clearPdf);

    pdfTextSelectButton = new QPushButton(this);
    pdfTextSelectButton->setFixedSize(26, 30);
    QIcon pdfTextIcon(loadThemedIcon("ibeam"));
    pdfTextSelectButton->setIcon(pdfTextIcon);
    pdfTextSelectButton->setStyleSheet(buttonStyle);
    pdfTextSelectButton->setToolTip(tr("Toggle PDF Text Selection"));
    connect(pdfTextSelectButton, &QPushButton::clicked, this, [this]() {
        if (!currentCanvas()) return;

        bool newMode = !currentCanvas()->isPdfTextSelectionEnabled();
        currentCanvas()->setPdfTextSelectionEnabled(newMode);
        updatePdfTextSelectButtonState();
        updateBookmarkButtonState();

        // Clear any existing selection when toggling
        if (!newMode) {
            currentCanvas()->clearPdfTextSelection();
        }
    });

    exportNotebookButton = new QPushButton(this);
    exportNotebookButton->setFixedSize(26, 30);
    QIcon exportIcon(loadThemedIcon("export"));  // Path to your icon in resources
    exportNotebookButton->setIcon(exportIcon);
    exportNotebookButton->setStyleSheet(buttonStyle);
    exportNotebookButton->setToolTip(tr("Export Notebook Into .SNPKG File"));
    importNotebookButton = new QPushButton(this);
    importNotebookButton->setFixedSize(26, 30);
    QIcon importIcon(loadThemedIcon("import"));  // Path to your icon in resources
    importNotebookButton->setIcon(importIcon);
    importNotebookButton->setStyleSheet(buttonStyle);
    importNotebookButton->setToolTip(tr("Import Notebook From .SNPKG File"));

    connect(exportNotebookButton, &QPushButton::clicked, this, [=]() {
        QString filename = QFileDialog::getSaveFileName(this, tr("Export Notebook"), "", "SpeedyNote Package (*.snpkg)");
        if (!filename.isEmpty()) {
            if (!filename.endsWith(".snpkg")) filename += ".snpkg";
            currentCanvas()->exportNotebook(filename);
        }
    });

    connect(importNotebookButton, &QPushButton::clicked, this, [=]() {
        QString filename = QFileDialog::getOpenFileName(this, tr("Import Notebook"), "", "SpeedyNote Package (*.snpkg)");
        if (!filename.isEmpty()) {
            currentCanvas()->importNotebook(filename);
        }
    });

    benchmarkButton = new QPushButton(this);
    QIcon benchmarkIcon(loadThemedIcon("benchmark"));  // Path to your icon in resources
    benchmarkButton->setIcon(benchmarkIcon);
    benchmarkButton->setFixedSize(26, 30); // Make the benchmark button smaller
    benchmarkButton->setStyleSheet(buttonStyle);
    benchmarkButton->setToolTip(tr("Toggle Benchmark"));
    benchmarkLabel = new QLabel("PR:N/A", this);
    benchmarkLabel->setFixedHeight(30);  // Make the benchmark bar smaller

    toggleTabBarButton = new QPushButton(this);
    toggleTabBarButton->setIcon(loadThemedIcon("tabs"));  // You can design separate icons for "show" and "hide"
    toggleTabBarButton->setToolTip(tr("Show/Hide Tab Bar"));
    toggleTabBarButton->setFixedSize(26, 30);
    toggleTabBarButton->setStyleSheet(buttonStyle);
    toggleTabBarButton->setProperty("selected", true); // Initially visible

    // PDF Outline Toggle Button
    toggleOutlineButton = new QPushButton(this);
    toggleOutlineButton->setIcon(loadThemedIcon("outline"));  // Icon to be added later
    toggleOutlineButton->setToolTip(tr("Show/Hide PDF Outline"));
    toggleOutlineButton->setFixedSize(26, 30);
    toggleOutlineButton->setStyleSheet(buttonStyle);
    toggleOutlineButton->setProperty("selected", false); // Initially hidden

    // Bookmarks Toggle Button
    toggleBookmarksButton = new QPushButton(this);
    toggleBookmarksButton->setIcon(loadThemedIcon("bookmark"));  // Using bookpage icon for bookmarks
    toggleBookmarksButton->setToolTip(tr("Show/Hide Bookmarks"));
    toggleBookmarksButton->setFixedSize(26, 30);
    toggleBookmarksButton->setStyleSheet(buttonStyle);
    toggleBookmarksButton->setProperty("selected", false); // Initially hidden

    // Add/Remove Bookmark Toggle Button
    toggleBookmarkButton = new QPushButton(this);
    toggleBookmarkButton->setIcon(loadThemedIcon("star"));
    toggleBookmarkButton->setToolTip(tr("Add/Remove Bookmark"));
    toggleBookmarkButton->setFixedSize(26, 30);
    toggleBookmarkButton->setStyleSheet(buttonStyle);
    toggleBookmarkButton->setProperty("selected", false); // For toggle state styling

    // Touch Gestures Toggle Button
    touchGesturesButton = new QPushButton(this);
    touchGesturesButton->setIcon(loadThemedIcon("hand"));  // Using hand icon for touch/gesture
    touchGesturesButton->setToolTip(tr("Toggle Touch Gestures"));
    touchGesturesButton->setFixedSize(26, 30);
    touchGesturesButton->setStyleSheet(buttonStyle);
    touchGesturesButton->setProperty("selected", touchGesturesEnabled); // For toggle state styling

    selectFolderButton = new QPushButton(this);
    selectFolderButton->setFixedSize(26, 30);
    QIcon folderIcon(loadThemedIcon("folder"));  // Path to your icon in resources
    selectFolderButton->setIcon(folderIcon);
    selectFolderButton->setStyleSheet(buttonStyle);
    selectFolderButton->setToolTip(tr("Select Save Folder"));
    connect(selectFolderButton, &QPushButton::clicked, this, &MainWindow::selectFolder);

    saveButton = new QPushButton(this);
    saveButton->setFixedSize(26, 30);
    QIcon saveIcon(loadThemedIcon("save"));  // Path to your icon in resources
    saveButton->setIcon(saveIcon);
    saveButton->setStyleSheet(buttonStyle);
    saveButton->setToolTip(tr("Save Current Page"));
    connect(saveButton, &QPushButton::clicked, this, &MainWindow::saveCurrentPage);

    saveAnnotatedButton = new QPushButton(this);
    saveAnnotatedButton->setFixedSize(26, 30);
    QIcon saveAnnotatedIcon(loadThemedIcon("saveannotated"));  // Path to your icon in resources
    saveAnnotatedButton->setIcon(saveAnnotatedIcon);
    saveAnnotatedButton->setStyleSheet(buttonStyle);
    saveAnnotatedButton->setToolTip(tr("Save Page with Background"));
    connect(saveAnnotatedButton, &QPushButton::clicked, this, &MainWindow::saveAnnotated);

    fullscreenButton = new QPushButton(this);
    fullscreenButton->setIcon(loadThemedIcon("fullscreen"));  // Load from resources
    fullscreenButton->setFixedSize(26, 30);
    fullscreenButton->setToolTip(tr("Toggle Fullscreen"));
    fullscreenButton->setStyleSheet(buttonStyle);

    // ✅ Connect button click to toggleFullscreen() function
    connect(fullscreenButton, &QPushButton::clicked, this, &MainWindow::toggleFullscreen);

    // Use the darkMode variable already declared at the beginning of setupUi()

    redButton = new QPushButton(this);
    redButton->setFixedSize(18, 30);  // Half width
    QString redIconPath = darkMode ? ":/resources/icons/pen_light_red.png" : ":/resources/icons/pen_dark_red.png";
    QIcon redIcon(redIconPath);
    redButton->setIcon(redIcon);
    redButton->setStyleSheet(buttonStyle);
    connect(redButton, &QPushButton::clicked, [this]() {
        handleColorButtonClick();
        currentCanvas()->setPenColor(getPaletteColor("red"));
        updateDialDisplay();
        updateColorButtonStates();
    });

    blueButton = new QPushButton(this);
    blueButton->setFixedSize(18, 30);  // Half width
    QString blueIconPath = darkMode ? ":/resources/icons/pen_light_blue.png" : ":/resources/icons/pen_dark_blue.png";
    QIcon blueIcon(blueIconPath);
    blueButton->setIcon(blueIcon);
    blueButton->setStyleSheet(buttonStyle);
    connect(blueButton, &QPushButton::clicked, [this]() {
        handleColorButtonClick();
        currentCanvas()->setPenColor(getPaletteColor("blue"));
        updateDialDisplay();
        updateColorButtonStates();
    });

    yellowButton = new QPushButton(this);
    yellowButton->setFixedSize(18, 30);  // Half width
    QString yellowIconPath = darkMode ? ":/resources/icons/pen_light_yellow.png" : ":/resources/icons/pen_dark_yellow.png";
    QIcon yellowIcon(yellowIconPath);
    yellowButton->setIcon(yellowIcon);
    yellowButton->setStyleSheet(buttonStyle);
    connect(yellowButton, &QPushButton::clicked, [this]() {
        handleColorButtonClick();
        currentCanvas()->setPenColor(getPaletteColor("yellow"));
        updateDialDisplay();
        updateColorButtonStates();
    });

    greenButton = new QPushButton(this);
    greenButton->setFixedSize(18, 30);  // Half width
    QString greenIconPath = darkMode ? ":/resources/icons/pen_light_green.png" : ":/resources/icons/pen_dark_green.png";
    QIcon greenIcon(greenIconPath);
    greenButton->setIcon(greenIcon);
    greenButton->setStyleSheet(buttonStyle);
    connect(greenButton, &QPushButton::clicked, [this]() {
        handleColorButtonClick();
        currentCanvas()->setPenColor(getPaletteColor("green"));
        updateDialDisplay();
        updateColorButtonStates();
    });

    blackButton = new QPushButton(this);
    blackButton->setFixedSize(18, 30);  // Half width
    QString blackIconPath = darkMode ? ":/resources/icons/pen_light_black.png" : ":/resources/icons/pen_dark_black.png";
    QIcon blackIcon(blackIconPath);
    blackButton->setIcon(blackIcon);
    blackButton->setStyleSheet(buttonStyle);
    connect(blackButton, &QPushButton::clicked, [this]() {
        handleColorButtonClick();
        currentCanvas()->setPenColor(QColor("#000000"));
        updateDialDisplay();
        updateColorButtonStates();
    });

    whiteButton = new QPushButton(this);
    whiteButton->setFixedSize(18, 30);  // Half width
    QString whiteIconPath = darkMode ? ":/resources/icons/pen_light_white.png" : ":/resources/icons/pen_dark_white.png";
    QIcon whiteIcon(whiteIconPath);
    whiteButton->setIcon(whiteIcon);
    whiteButton->setStyleSheet(buttonStyle);
    connect(whiteButton, &QPushButton::clicked, [this]() {
        handleColorButtonClick();
        currentCanvas()->setPenColor(QColor("#FFFFFF"));
        updateDialDisplay();
        updateColorButtonStates();
    });

    customColorInput = new QLineEdit(this);
    customColorInput->setPlaceholderText("Custom HEX");
    customColorInput->setFixedSize(85, 30);

    // Enable IME support for multi-language input
    customColorInput->setAttribute(Qt::WA_InputMethodEnabled, true);
    customColorInput->setInputMethodHints(Qt::ImhNone); // Allow all input methods
    customColorInput->installEventFilter(this); // Install event filter for IME handling

    connect(customColorInput, &QLineEdit::returnPressed, this, &MainWindow::applyCustomColor);

    thicknessButton = new QPushButton(this);
    thicknessButton->setIcon(loadThemedIcon("thickness"));
    thicknessButton->setFixedSize(26, 30);
    thicknessButton->setStyleSheet(buttonStyle);
    connect(thicknessButton, &QPushButton::clicked, this, &MainWindow::toggleThicknessSlider);

    thicknessFrame = new QFrame(this);
    thicknessFrame->setFrameShape(QFrame::StyledPanel);
    thicknessFrame->setStyleSheet(R"(
        background-color: black;
        border: 1px solid black;
        padding: 5px;
    )");
    thicknessFrame->setVisible(false);
    thicknessFrame->setFixedSize(220, 40); // Adjust width/height as needed

    thicknessSlider = new QSlider(Qt::Horizontal, this);
    thicknessSlider->setRange(1, 50);
    thicknessSlider->setValue(5);
    thicknessSlider->setMaximumWidth(200);

    connect(thicknessSlider, &QSlider::valueChanged, this, &MainWindow::updateThickness);

    QVBoxLayout *popupLayoutThickness = new QVBoxLayout();
    popupLayoutThickness->setContentsMargins(10, 5, 10, 5);
    popupLayoutThickness->addWidget(thicknessSlider);
    thicknessFrame->setLayout(popupLayoutThickness);

    toolSelector = new QComboBox(this);
    toolSelector->addItem(loadThemedIcon("pen"), "");
    toolSelector->addItem(loadThemedIcon("marker"), "");
    toolSelector->addItem(loadThemedIcon("eraser"), "");
    toolSelector->setFixedWidth(43);
    toolSelector->setFixedHeight(30);
    connect(toolSelector, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &MainWindow::changeTool);

    // ✅ Individual tool buttons
    penToolButton = new QPushButton(this);
    penToolButton->setFixedSize(26, 30);
    penToolButton->setIcon(loadThemedIcon("pen"));
    penToolButton->setStyleSheet(buttonStyle);
    penToolButton->setToolTip(tr("Pen Tool"));
    connect(penToolButton, &QPushButton::clicked, this, &MainWindow::setPenTool);

    markerToolButton = new QPushButton(this);
    markerToolButton->setFixedSize(26, 30);
    markerToolButton->setIcon(loadThemedIcon("marker"));
    markerToolButton->setStyleSheet(buttonStyle);
    markerToolButton->setToolTip(tr("Marker Tool"));
    connect(markerToolButton, &QPushButton::clicked, this, &MainWindow::setMarkerTool);

    eraserToolButton = new QPushButton(this);
    eraserToolButton->setFixedSize(26, 30);
    eraserToolButton->setIcon(loadThemedIcon("eraser"));
    eraserToolButton->setStyleSheet(buttonStyle);
    eraserToolButton->setToolTip(tr("Eraser Tool"));
    connect(eraserToolButton, &QPushButton::clicked, this, &MainWindow::setEraserTool);

    backgroundButton = new QPushButton(this);
    backgroundButton->setFixedSize(26, 30);
    QIcon bgIcon(loadThemedIcon("background"));  // Path to your icon in resources
    backgroundButton->setIcon(bgIcon);
    backgroundButton->setStyleSheet(buttonStyle);
    backgroundButton->setToolTip(tr("Set Background Pic"));
    connect(backgroundButton, &QPushButton::clicked, this, &MainWindow::selectBackground);

    // Initialize straight line toggle button
    straightLineToggleButton = new QPushButton(this);
    straightLineToggleButton->setFixedSize(26, 30);
    QIcon straightLineIcon(loadThemedIcon("straightLine"));  // Make sure this icon exists or use a different one
    straightLineToggleButton->setIcon(straightLineIcon);
    straightLineToggleButton->setStyleSheet(buttonStyle);
    straightLineToggleButton->setToolTip(tr("Toggle Straight Line Mode"));
    connect(straightLineToggleButton, &QPushButton::clicked, this, [this]() {
        if (!currentCanvas()) return;

        // If we're turning on straight line mode, first disable rope tool
        if (!currentCanvas()->isStraightLineMode()) {
            currentCanvas()->setRopeToolMode(false);
            updateRopeToolButtonState();
        }

        bool newMode = !currentCanvas()->isStraightLineMode();
        currentCanvas()->setStraightLineMode(newMode);
        updateStraightLineButtonState();
    });

    ropeToolButton = new QPushButton(this);
    ropeToolButton->setFixedSize(26, 30);
    QIcon ropeToolIcon(loadThemedIcon("rope")); // Make sure this icon exists
    ropeToolButton->setIcon(ropeToolIcon);
    ropeToolButton->setStyleSheet(buttonStyle);
    ropeToolButton->setToolTip(tr("Toggle Rope Tool Mode"));
    connect(ropeToolButton, &QPushButton::clicked, this, [this]() {
        if (!currentCanvas()) return;

        // If we're turning on rope tool mode, first disable straight line
        if (!currentCanvas()->isRopeToolMode()) {
            currentCanvas()->setStraightLineMode(false);
            updateStraightLineButtonState();
        }

        bool newMode = !currentCanvas()->isRopeToolMode();
        currentCanvas()->setRopeToolMode(newMode);
        updateRopeToolButtonState();
    });

    markdownButton = new QPushButton(this);
    markdownButton->setFixedSize(26, 30);
    QIcon markdownIcon(loadThemedIcon("markdown")); // Using text icon for markdown
    markdownButton->setIcon(markdownIcon);
    markdownButton->setStyleSheet(buttonStyle);
    markdownButton->setToolTip(tr("Add Markdown Window"));
    connect(markdownButton, &QPushButton::clicked, this, [this]() {
        if (!currentCanvas()) return;

        // Toggle markdown selection mode
        bool newMode = !currentCanvas()->isMarkdownSelectionMode();
        currentCanvas()->setMarkdownSelectionMode(newMode);
        updateMarkdownButtonState();
    });

    deletePageButton = new QPushButton(this);
    deletePageButton->setFixedSize(26, 30);
    QIcon trashIcon(loadThemedIcon("trash"));  // Path to your icon in resources
    deletePageButton->setIcon(trashIcon);
    deletePageButton->setStyleSheet(buttonStyle);
    deletePageButton->setToolTip(tr("Delete Current Page"));
    connect(deletePageButton, &QPushButton::clicked, this, &MainWindow::deleteCurrentPage);

    zoomButton = new QPushButton(this);
    zoomButton->setIcon(loadThemedIcon("zoom"));
    zoomButton->setFixedSize(26, 30);
    zoomButton->setStyleSheet(buttonStyle);
    connect(zoomButton, &QPushButton::clicked, this, &MainWindow::toggleZoomSlider);

    // ✅ Create the floating frame (Initially Hidden)
    zoomFrame = new QFrame(this);
    zoomFrame->setFrameShape(QFrame::StyledPanel);
    zoomFrame->setStyleSheet(R"(
        background-color: black;
        border: 1px solid black;
        padding: 5px;
    )");
    zoomFrame->setVisible(false);
    zoomFrame->setFixedSize(440, 40); // Adjust width/height as needed

    zoomSlider = new QSlider(Qt::Horizontal, this);
    zoomSlider->setRange(10, 400);
    zoomSlider->setValue(100);
    zoomSlider->setMaximumWidth(405);

    connect(zoomSlider, &QSlider::valueChanged, this, &MainWindow::onZoomSliderChanged);

    QVBoxLayout *popupLayout = new QVBoxLayout();
    popupLayout->setContentsMargins(10, 5, 10, 5);
    popupLayout->addWidget(zoomSlider);
    zoomFrame->setLayout(popupLayout);

    zoom50Button = new QPushButton("0.5x", this);
    zoom50Button->setFixedSize(35, 30);
    zoom50Button->setStyleSheet(buttonStyle);
    zoom50Button->setToolTip(tr("Set Zoom to 50%"));
    connect(zoom50Button, &QPushButton::clicked, [this]() { zoomSlider->setValue(50 / initialDpr); updateDialDisplay(); });

    dezoomButton = new QPushButton("1x", this);
    dezoomButton->setFixedSize(26, 30);
    dezoomButton->setStyleSheet(buttonStyle);
    dezoomButton->setToolTip(tr("Set Zoom to 100%"));
    connect(dezoomButton, &QPushButton::clicked, [this]() { zoomSlider->setValue(100 / initialDpr); updateDialDisplay(); });

    zoom200Button = new QPushButton("2x", this);
    zoom200Button->setFixedSize(31, 30);
    zoom200Button->setStyleSheet(buttonStyle);
    zoom200Button->setToolTip(tr("Set Zoom to 200%"));
    connect(zoom200Button, &QPushButton::clicked, [this]() { zoomSlider->setValue(200 / initialDpr); updateDialDisplay(); });

    panXSlider = new QScrollBar(Qt::Horizontal, this);
    panYSlider = new QScrollBar(Qt::Vertical, this);
    panYSlider->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);

    // Set scrollbar styling
    QString scrollBarStyle = R"(
        QScrollBar {
            background: rgba(200, 200, 200, 80);
            border: none;
            margin: 0px;
        }
        QScrollBar:hover {
            background: rgba(200, 200, 200, 120);
        }
        QScrollBar:horizontal {
            height: 16px !important;  /* Force narrow height */
            max-height: 16px !important;
        }
        QScrollBar:vertical {
            width: 16px !important;   /* Force narrow width */
            max-width: 16px !important;
        }
        QScrollBar::handle {
            background: rgba(100, 100, 100, 150);
            border-radius: 2px;
            min-height: 120px;  /* Longer handle for vertical scrollbar */
            min-width: 120px;   /* Longer handle for horizontal scrollbar */
        }
        QScrollBar::handle:hover {
            background: rgba(80, 80, 80, 210);
        }
        /* Hide scroll buttons */
        QScrollBar::add-line,
        QScrollBar::sub-line {
            width: 0px;
            height: 0px;
            background: none;
            border: none;
        }
        /* Disable scroll page buttons */
        QScrollBar::add-page,
        QScrollBar::sub-page {
            background: transparent;
        }
    )";

    panXSlider->setStyleSheet(scrollBarStyle);
    panYSlider->setStyleSheet(scrollBarStyle);

    // Force fixed dimensions programmatically
    panXSlider->setFixedHeight(16);
    panYSlider->setFixedWidth(16);

    // Set up auto-hiding
    panXSlider->setMouseTracking(true);
    panYSlider->setMouseTracking(true);
    panXSlider->installEventFilter(this);
    panYSlider->installEventFilter(this);
    panXSlider->setVisible(false);
    panYSlider->setVisible(false);

    // Create timer for auto-hiding
    scrollbarHideTimer = new QTimer(this);
    scrollbarHideTimer->setSingleShot(true);
    scrollbarHideTimer->setInterval(200); // Hide after 0.2 seconds
    connect(scrollbarHideTimer, &QTimer::timeout, this, [this]() {
        panXSlider->setVisible(false);
        panYSlider->setVisible(false);
        scrollbarsVisible = false;
    });

    // panXSlider->setFixedHeight(30);
    // panYSlider->setFixedWidth(30);

    connect(panXSlider, &QScrollBar::valueChanged, this, &MainWindow::updatePanX);

    connect(panYSlider, &QScrollBar::valueChanged, this, &MainWindow::updatePanY);

    // 🌟 PDF Outline Sidebar
    outlineSidebar = new QWidget(this);
    outlineSidebar->setFixedWidth(250);
    outlineSidebar->setVisible(false); // Hidden by default

    QVBoxLayout *outlineLayout = new QVBoxLayout(outlineSidebar);
    outlineLayout->setContentsMargins(5, 5, 5, 5);

    QLabel *outlineLabel = new QLabel(tr("PDF Outline"), outlineSidebar);
    outlineLabel->setStyleSheet("font-weight: bold; padding: 5px;");
    outlineLayout->addWidget(outlineLabel);

    outlineTree = new QTreeWidget(outlineSidebar);
    outlineTree->setHeaderHidden(true);
    outlineTree->setRootIsDecorated(true);
    outlineTree->setIndentation(15);
    outlineLayout->addWidget(outlineTree);

    // Connect outline tree item clicks
    connect(outlineTree, &QTreeWidget::itemClicked, this, &MainWindow::onOutlineItemClicked);

    // 🌟 Bookmarks Sidebar
    bookmarksSidebar = new QWidget(this);
    bookmarksSidebar->setFixedWidth(250);
    bookmarksSidebar->setVisible(false); // Hidden by default

    QVBoxLayout *bookmarksLayout = new QVBoxLayout(bookmarksSidebar);
    bookmarksLayout->setContentsMargins(5, 5, 5, 5);

    QLabel *bookmarksLabel = new QLabel(tr("Bookmarks"), bookmarksSidebar);
    bookmarksLabel->setStyleSheet("font-weight: bold; padding: 5px;");
    bookmarksLayout->addWidget(bookmarksLabel);

    bookmarksTree = new QTreeWidget(bookmarksSidebar);
    bookmarksTree->setHeaderHidden(true);
    bookmarksTree->setRootIsDecorated(false);
    bookmarksTree->setIndentation(0);
    bookmarksLayout->addWidget(bookmarksTree);

    // Connect bookmarks tree item clicks
    connect(bookmarksTree, &QTreeWidget::itemClicked, this, &MainWindow::onBookmarkItemClicked);

    // 🌟 Horizontal Tab Bar (like modern web browsers)
    tabList = new QListWidget(this);
    tabList->setFlow(QListView::LeftToRight);  // Make it horizontal
    tabList->setFixedHeight(32);  // Increased to accommodate scrollbar below tabs
    tabList->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
    tabList->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
    tabList->setSelectionMode(QAbstractItemView::SingleSelection);

    // Style the tab bar like a modern browser with transparent scrollbar
    tabList->setStyleSheet(R"(
        QListWidget {
            background-color: rgba(240, 240, 240, 255);
            border: none;
            border-bottom: 1px solid rgba(200, 200, 200, 255);
            outline: none;
        }
        QListWidget::item {
            background-color: rgba(220, 220, 220, 255);
            border: 1px solid rgba(180, 180, 180, 255);
            border-bottom: none;
            margin-right: 1px;
            margin-top: 2px;
            padding: 0px;
            min-width: 80px;
            max-width: 120px;
        }
        QListWidget::item:selected {
            background-color: white;
            border: 1px solid rgba(180, 180, 180, 255);
            border-bottom: 1px solid white;
            margin-top: 1px;
        }
        QListWidget::item:hover:!selected {
            background-color: rgba(230, 230, 230, 255);
        }
        QScrollBar:horizontal {
            background: rgba(240, 240, 240, 255);
            height: 8px;
            border: none;
            margin: 0px;
            border-top: 1px solid rgba(200, 200, 200, 255);
        }
        QScrollBar::handle:horizontal {
            background: rgba(150, 150, 150, 120);
            border-radius: 4px;
            min-width: 20px;
            margin: 1px;
        }
        QScrollBar::handle:horizontal:hover {
            background: rgba(120, 120, 120, 200);
        }
        QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
            width: 0px;
            height: 0px;
            background: none;
            border: none;
        }
        QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {
            background: transparent;
        }
    )");

    // 🌟 Add Button for New Tab (styled like browser + button)
    addTabButton = new QPushButton(this);
    QIcon addTab(loadThemedIcon("addtab"));  // Path to your icon in resources
    addTabButton->setIcon(addTab);
    addTabButton->setFixedSize(30, 30);  // Even smaller to match thinner layout
    addTabButton->setStyleSheet(R"(
        QPushButton {
            background-color: rgba(220, 220, 220, 255);
            border: 1px solid rgba(180, 180, 180, 255);
            border-radius: 15px;
            margin: 2px;
        }
        QPushButton:hover {
            background-color: rgba(200, 200, 200, 255);
        }
        QPushButton:pressed {
            background-color: rgba(180, 180, 180, 255);
        }
    )");
    addTabButton->setToolTip(tr("Add New Tab"));
    connect(addTabButton, &QPushButton::clicked, this, &MainWindow::addNewTab);

    if (!canvasStack) {
        canvasStack = new QStackedWidget(this);
    }

    connect(tabList, &QListWidget::currentRowChanged, this, &MainWindow::switchTab);

    // Create horizontal tab container
    tabBarContainer = new QWidget(this);
    tabBarContainer->setObjectName("tabBarContainer");
    tabBarContainer->setFixedHeight(38);  // Increased to accommodate scrollbar below tabs

    QHBoxLayout *tabBarLayout = new QHBoxLayout(tabBarContainer);
    tabBarLayout->setContentsMargins(5, 5, 5, 5);
    tabBarLayout->setSpacing(5);
    tabBarLayout->addWidget(tabList, 1);  // Tab list takes most space
    tabBarLayout->addWidget(addTabButton, 0);  // Add button stays at the end

    connect(toggleTabBarButton, &QPushButton::clicked, this, [=]() {
        bool isVisible = tabBarContainer->isVisible();
        tabBarContainer->setVisible(!isVisible);

        // Update button toggle state
        toggleTabBarButton->setProperty("selected", !isVisible);
        toggleTabBarButton->style()->unpolish(toggleTabBarButton);
        toggleTabBarButton->style()->polish(toggleTabBarButton);

        QTimer::singleShot(0, this, [this]() {
            if (auto *canvas = currentCanvas()) {
                canvas->setMaximumSize(canvas->getCanvasSize());
            }
        });
    });

    connect(toggleOutlineButton, &QPushButton::clicked, this, &MainWindow::toggleOutlineSidebar);
    connect(toggleBookmarksButton, &QPushButton::clicked, this, &MainWindow::toggleBookmarksSidebar);
    connect(toggleBookmarkButton, &QPushButton::clicked, this, &MainWindow::toggleCurrentPageBookmark);
    connect(touchGesturesButton, &QPushButton::clicked, this, [this]() {
        setTouchGesturesEnabled(!touchGesturesEnabled);
        touchGesturesButton->setProperty("selected", touchGesturesEnabled);
        touchGesturesButton->style()->unpolish(touchGesturesButton);
        touchGesturesButton->style()->polish(touchGesturesButton);
    });

    // Previous page button
    prevPageButton = new QPushButton(this);
    prevPageButton->setFixedSize(26, 30);
    prevPageButton->setText("◀");
    prevPageButton->setStyleSheet(buttonStyle);
    prevPageButton->setToolTip(tr("Previous Page"));
    connect(prevPageButton, &QPushButton::clicked, this, &MainWindow::goToPreviousPage);

    pageInput = new QSpinBox(this);
    pageInput->setFixedSize(42, 30);
    pageInput->setMinimum(1);
    pageInput->setMaximum(9999);
    pageInput->setValue(1);
    pageInput->setMaximumWidth(100);
    connect(pageInput, QOverload<int>::of(&QSpinBox::valueChanged), this, &MainWindow::onPageInputChanged);

    // Next page button
    nextPageButton = new QPushButton(this);
    nextPageButton->setFixedSize(26, 30);
    nextPageButton->setText("▶");
    nextPageButton->setStyleSheet(buttonStyle);
    nextPageButton->setToolTip(tr("Next Page"));
    connect(nextPageButton, &QPushButton::clicked, this, &MainWindow::goToNextPage);

    jumpToPageButton = new QPushButton(this);
    // QIcon jumpIcon(":/resources/icons/bookpage.png");  // Path to your icon in resources
    jumpToPageButton->setFixedSize(26, 30);
    jumpToPageButton->setStyleSheet(buttonStyle);
    jumpToPageButton->setIcon(loadThemedIcon("bookpage"));
    connect(jumpToPageButton, &QPushButton::clicked, this, &MainWindow::showJumpToPageDialog);

    // ✅ Dial Toggle Button
    dialToggleButton = new QPushButton(this);
    dialToggleButton->setIcon(loadThemedIcon("dial"));  // Icon for dial
    dialToggleButton->setFixedSize(26, 30);
    dialToggleButton->setToolTip(tr("Toggle Magic Dial"));
    dialToggleButton->setStyleSheet(buttonStyle);

    // ✅ Connect to toggle function
    connect(dialToggleButton, &QPushButton::clicked, this, &MainWindow::toggleDial);

    // toggleDial();

    fastForwardButton = new QPushButton(this);
    fastForwardButton->setFixedSize(26, 30);
    // QIcon ffIcon(":/resources/icons/fastforward.png");  // Path to your icon in resources
    fastForwardButton->setIcon(loadThemedIcon("fastforward"));
    fastForwardButton->setToolTip(tr("Toggle Fast Forward 8x"));
    fastForwardButton->setStyleSheet(buttonStyle);

    // ✅ Toggle fast-forward mode
    connect(fastForwardButton, &QPushButton::clicked, [this]() {
        fastForwardMode = !fastForwardMode;
        updateFastForwardButtonState();
    });

    QComboBox *dialModeSelector = new QComboBox(this);
    dialModeSelector->addItem("Page Switch", PageSwitching);
    dialModeSelector->addItem("Zoom", ZoomControl);
    dialModeSelector->addItem("Thickness", ThicknessControl);

    dialModeSelector->addItem("Tool Switch", ToolSwitching);
    dialModeSelector->setFixedWidth(120);

    connect(dialModeSelector, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
        [this](int index) { changeDialMode(static_cast<DialMode>(index)); });

    colorPreview = new QPushButton(this);
    colorPreview->setFixedSize(26, 30);
    colorPreview->setStyleSheet("border-radius: 15px; border: 1px solid gray;");
    colorPreview->setEnabled(false);  // ✅ Prevents it from being clicked

    btnPageSwitch = new QPushButton(loadThemedIcon("bookpage"), "", this);
    btnPageSwitch->setStyleSheet(buttonStyle);
    btnPageSwitch->setFixedSize(26, 30);
    btnPageSwitch->setToolTip(tr("Set Dial Mode to Page Switching"));
    btnZoom = new QPushButton(loadThemedIcon("zoom"), "", this);
    btnZoom->setStyleSheet(buttonStyle);
    btnZoom->setFixedSize(26, 30);
    btnZoom->setToolTip(tr("Set Dial Mode to Zoom Ctrl"));
    btnThickness = new QPushButton(loadThemedIcon("thickness"), "", this);
    btnThickness->setStyleSheet(buttonStyle);
    btnThickness->setFixedSize(26, 30);
    btnThickness->setToolTip(tr("Set Dial Mode to Pen Tip Thickness Ctrl"));

    btnTool = new QPushButton(loadThemedIcon("pen"), "", this);
    btnTool->setStyleSheet(buttonStyle);
    btnTool->setFixedSize(26, 30);
    btnTool->setToolTip(tr("Set Dial Mode to Tool Switching"));
    btnPresets = new QPushButton(loadThemedIcon("preset"), "", this);
    btnPresets->setStyleSheet(buttonStyle);
    btnPresets->setFixedSize(26, 30);
    btnPresets->setToolTip(tr("Set Dial Mode to Color Preset Selection"));
    btnPannScroll = new QPushButton(loadThemedIcon("scroll"), "", this);
    btnPannScroll->setStyleSheet(buttonStyle);
    btnPannScroll->setFixedSize(26, 30);
    btnPannScroll->setToolTip(tr("Slide and turn pages with the dial"));

    connect(btnPageSwitch, &QPushButton::clicked, this, [this]() { changeDialMode(PageSwitching); });
    connect(btnZoom, &QPushButton::clicked, this, [this]() { changeDialMode(ZoomControl); });
    connect(btnThickness, &QPushButton::clicked, this, [this]() { changeDialMode(ThicknessControl); });

    connect(btnTool, &QPushButton::clicked, this, [this]() { changeDialMode(ToolSwitching); });
    connect(btnPresets, &QPushButton::clicked, this, [this]() { changeDialMode(PresetSelection); });
    connect(btnPannScroll, &QPushButton::clicked, this, [this]() { changeDialMode(PanAndPageScroll); });

    // ✅ Initialize color presets based on palette mode (will be updated after UI setup)
    colorPresets.enqueue(getDefaultPenColor());
    colorPresets.enqueue(QColor("#AA0000"));  // Temporary - will be updated later
    colorPresets.enqueue(QColor("#997700"));
    colorPresets.enqueue(QColor("#0000AA"));
    colorPresets.enqueue(QColor("#007700"));
    colorPresets.enqueue(QColor("#000000"));
    colorPresets.enqueue(QColor("#FFFFFF"));

    // ✅ Button to add current color to presets
    addPresetButton = new QPushButton(loadThemedIcon("savepreset"), "", this);
    addPresetButton->setStyleSheet(buttonStyle);
    addPresetButton->setToolTip(tr("Add Current Color to Presets"));
    addPresetButton->setFixedSize(26, 30);
    connect(addPresetButton, &QPushButton::clicked, this, &MainWindow::addColorPreset);

    openControlPanelButton = new QPushButton(this);
    openControlPanelButton->setIcon(loadThemedIcon("settings"));  // Replace with your actual settings icon
    openControlPanelButton->setStyleSheet(buttonStyle);
    openControlPanelButton->setToolTip(tr("Open Control Panel"));
    openControlPanelButton->setFixedSize(26, 30);  // Adjust to match your other buttons

    connect(openControlPanelButton, &QPushButton::clicked, this, [=]() {
        InkCanvas *canvas = currentCanvas();
        if (canvas) {
            ControlPanelDialog dialog(this, canvas, this);
            dialog.exec();  // Modal
        }
    });

    openRecentNotebooksButton = new QPushButton(this); // Create button
    openRecentNotebooksButton->setIcon(loadThemedIcon("recent")); // Replace with actual icon if available
    openRecentNotebooksButton->setStyleSheet(buttonStyle);
    openRecentNotebooksButton->setToolTip(tr("Open Recent Notebooks"));
    openRecentNotebooksButton->setFixedSize(26, 30);
    connect(openRecentNotebooksButton, &QPushButton::clicked, this, &MainWindow::openRecentNotebooksDialog);

    customColorButton = new QPushButton(this);
    customColorButton->setFixedSize(62, 30);
    QColor initialColor = getDefaultPenColor();  // Theme-aware default color
    customColorButton->setText(initialColor.name().toUpper());

    if (currentCanvas()) {
        initialColor = currentCanvas()->getPenColor();
    }

    updateCustomColorButtonStyle(initialColor);

    QTimer::singleShot(0, this, [=]() {
        connect(customColorButton, &QPushButton::clicked, this, [=]() {
            if (!currentCanvas()) return;

            handleColorButtonClick();

            // Get the current custom color from the button text
            QString buttonText = customColorButton->text();
            QColor customColor(buttonText);

            // Check if the custom color is already the current pen color
            if (currentCanvas()->getPenColor() == customColor) {
                // Second click - show color picker dialog
            QColor chosen = QColorDialog::getColor(currentCanvas()->getPenColor(), this, "Select Pen Color");
            if (chosen.isValid()) {
                currentCanvas()->setPenColor(chosen);
                    updateCustomColorButtonStyle(chosen);
                updateDialDisplay();
                    updateColorButtonStates();
                }
            } else {
                // First click - apply the custom color
                currentCanvas()->setPenColor(customColor);
                updateDialDisplay();
                updateColorButtonStates();
            }
        });
    });

    QHBoxLayout *controlLayout = new QHBoxLayout;

    controlLayout->addWidget(toggleOutlineButton);
    controlLayout->addWidget(toggleBookmarksButton);
    controlLayout->addWidget(toggleBookmarkButton);
    controlLayout->addWidget(touchGesturesButton);
    controlLayout->addWidget(toggleTabBarButton);
    controlLayout->addWidget(selectFolderButton);

    controlLayout->addWidget(exportNotebookButton);
    controlLayout->addWidget(importNotebookButton);
    controlLayout->addWidget(loadPdfButton);
    controlLayout->addWidget(clearPdfButton);
    controlLayout->addWidget(pdfTextSelectButton);
    controlLayout->addWidget(backgroundButton);
    controlLayout->addWidget(saveButton);
    controlLayout->addWidget(saveAnnotatedButton);
    controlLayout->addWidget(openControlPanelButton);
    controlLayout->addWidget(openRecentNotebooksButton); // Add button to layout
    controlLayout->addWidget(redButton);
    controlLayout->addWidget(blueButton);
    controlLayout->addWidget(yellowButton);
    controlLayout->addWidget(greenButton);
    controlLayout->addWidget(blackButton);
    controlLayout->addWidget(whiteButton);
    controlLayout->addWidget(customColorButton);
    controlLayout->addWidget(straightLineToggleButton);
    controlLayout->addWidget(ropeToolButton); // Add rope tool button to layout
    controlLayout->addWidget(markdownButton); // Add markdown button to layout
    // controlLayout->addWidget(colorPreview);
    // controlLayout->addWidget(thicknessButton);
    // controlLayout->addWidget(jumpToPageButton);
    controlLayout->addWidget(dialToggleButton);
    controlLayout->addWidget(fastForwardButton);
    // controlLayout->addWidget(channelSelector);
    controlLayout->addWidget(btnPageSwitch);
    controlLayout->addWidget(btnPannScroll);
    controlLayout->addWidget(btnZoom);
    controlLayout->addWidget(btnThickness);

    controlLayout->addWidget(btnTool);
    controlLayout->addWidget(btnPresets);
    controlLayout->addWidget(addPresetButton);
    // controlLayout->addWidget(dialModeSelector);
    // controlLayout->addStretch();

    // controlLayout->addWidget(toolSelector);
    controlLayout->addWidget(fullscreenButton);
    // controlLayout->addWidget(zoomButton);
    controlLayout->addWidget(zoom50Button);
    controlLayout->addWidget(dezoomButton);
    controlLayout->addWidget(zoom200Button);
    controlLayout->addStretch();

    controlLayout->addWidget(prevPageButton);
    controlLayout->addWidget(pageInput);
    controlLayout->addWidget(nextPageButton);
    controlLayout->addWidget(benchmarkButton);
    controlLayout->addWidget(benchmarkLabel);
    controlLayout->addWidget(deletePageButton);

    controlBar = new QWidget;  // Use member variable instead of local
    controlBar->setObjectName("controlBar");
    // controlBar->setLayout(controlLayout);  // Commented out - responsive layout will handle this
    controlBar->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);

    // Theme will be applied later in loadUserSettings -> updateTheme()
    controlBar->setStyleSheet("");

    canvasStack = new QStackedWidget();
    canvasStack->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);

    // Create a container for the canvas and scrollbars with relative positioning
    QWidget *canvasContainer = new QWidget;
    QVBoxLayout *canvasLayout = new QVBoxLayout(canvasContainer);
    canvasLayout->setContentsMargins(0, 0, 0, 0);
    canvasLayout->addWidget(canvasStack);

    // Enable context menu for the workaround
    canvasContainer->setContextMenuPolicy(Qt::CustomContextMenu);

    // Set up the scrollbars to overlay the canvas
    panXSlider->setParent(canvasContainer);
    panYSlider->setParent(canvasContainer);

    // Raise scrollbars to ensure they're visible above the canvas
    panXSlider->raise();
    panYSlider->raise();

    // Handle scrollbar intersection
    connect(canvasContainer, &QWidget::customContextMenuRequested, this, [this]() {
        // This connection is just to make sure the container exists
        // and can receive signals - a workaround for some Qt versions
    });

    // Position the scrollbars at the bottom and right edges
    canvasContainer->installEventFilter(this);

    // Update scrollbar positions initially
    QTimer::singleShot(0, this, [this, canvasContainer]() {
        updateScrollbarPositions();
    });

    // Main layout: toolbar -> tab bar -> canvas (vertical stack)
    QWidget *container = new QWidget;
    container->setObjectName("container");
    QVBoxLayout *mainLayout = new QVBoxLayout(container);
    mainLayout->setContentsMargins(0, 0, 0, 0);  // ✅ Remove extra margins
    mainLayout->setSpacing(0); // ✅ Remove spacing between components

    // Add components in vertical order
    mainLayout->addWidget(controlBar);        // Toolbar at top
    mainLayout->addWidget(tabBarContainer);   // Tab bar below toolbar

    // Content area with sidebars and canvas
    QHBoxLayout *contentLayout = new QHBoxLayout;
    contentLayout->setContentsMargins(0, 0, 0, 0);
    contentLayout->setSpacing(0);
    contentLayout->addWidget(outlineSidebar, 0); // Fixed width outline sidebar
    contentLayout->addWidget(bookmarksSidebar, 0); // Fixed width bookmarks sidebar
    contentLayout->addWidget(canvasContainer, 1); // Canvas takes remaining space

    QWidget *contentWidget = new QWidget;
    contentWidget->setLayout(contentLayout);
    mainLayout->addWidget(contentWidget, 1);

    setCentralWidget(container);

    benchmarkTimer = new QTimer(this);
    connect(benchmarkButton, &QPushButton::clicked, this, &MainWindow::toggleBenchmark);
    connect(benchmarkTimer, &QTimer::timeout, this, &MainWindow::updateBenchmarkDisplay);

    QString tempDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/temp_session";
    QDir dir(tempDir);

    // Remove all contents (but keep the directory itself)
    if (dir.exists()) {
        dir.removeRecursively();  // Careful: this wipes everything inside
    }
    QDir().mkpath(tempDir);  // Recreate clean directory

    addNewTab();

    // Initialize responsive toolbar layout
    createSingleRowLayout();  // Start with single row layout

    // Now that all UI components are created, update the color palette
    updateColorPalette();

}
    void loadUserSettings() {
    QSettings settings("SpeedyNote", "App");

    // Load low-res toggle
    lowResPreviewEnabled = settings.value("lowResPreviewEnabled", true).toBool();
    setLowResPreviewEnabled(lowResPreviewEnabled);

    zoomButtonsVisible = settings.value("zoomButtonsVisible", true).toBool();
    setZoomButtonsVisible(zoomButtonsVisible);

    scrollOnTopEnabled = settings.value("scrollOnTopEnabled", true).toBool();
    setScrollOnTopEnabled(scrollOnTopEnabled);

    touchGesturesEnabled = settings.value("touchGesturesEnabled", true).toBool();
    setTouchGesturesEnabled(touchGesturesEnabled);

    // Update button visual state to match loaded setting
    touchGesturesButton->setProperty("selected", touchGesturesEnabled);
    touchGesturesButton->style()->unpolish(touchGesturesButton);
    touchGesturesButton->style()->polish(touchGesturesButton);

    // Initialize default background settings if they don't exist
    if (!settings.contains("defaultBackgroundStyle")) {
        saveDefaultBackgroundSettings(BackgroundStyle::Grid, Qt::white, 30);
    }

    // Load keyboard mappings
    loadKeyboardMappings();

    // Load theme settings
    loadThemeSettings();
}
    bool scrollbarsVisible = false;
    QTimer *scrollbarHideTimer = nullptr;
    bool eventFilter(QObject *obj, QEvent *event) override {
    static bool dragging = false;
    static QPoint lastMousePos;
    static QTimer *longPressTimer = nullptr;

    // Handle IME focus events for text input widgets
    QLineEdit *lineEdit = qobject_cast<QLineEdit*>(obj);
    if (lineEdit) {
        if (event->type() == QEvent::FocusIn) {
            // Ensure IME is enabled when text field gets focus
            lineEdit->setAttribute(Qt::WA_InputMethodEnabled, true);
            QInputMethod *inputMethod = QGuiApplication::inputMethod();
            if (inputMethod) {
                inputMethod->show();
            }
        }
        else if (event->type() == QEvent::FocusOut) {
            // Keep IME available but reset state
            QInputMethod *inputMethod = QGuiApplication::inputMethod();
            if (inputMethod) {
                inputMethod->reset();
            }
        }
    }

    // Handle resize events for canvas container
    QWidget *container = canvasStack ? canvasStack->parentWidget() : nullptr;
    if (obj == container && event->type() == QEvent::Resize) {
        updateScrollbarPositions();
        return false; // Let the event propagate
    }

    // Handle scrollbar visibility
    if (obj == panXSlider || obj == panYSlider) {
        if (event->type() == QEvent::Enter) {
            // Mouse entered scrollbar area
            if (scrollbarHideTimer->isActive()) {
                scrollbarHideTimer->stop();
            }
            return false;
        }
        else if (event->type() == QEvent::Leave) {
            // Mouse left scrollbar area - start timer to hide
            if (!scrollbarHideTimer->isActive()) {
                scrollbarHideTimer->start();
            }
            return false;
        }
    }

    // Check if this is a canvas event for scrollbar handling
    InkCanvas* canvas = qobject_cast<InkCanvas*>(obj);
    if (canvas) {
        // Handle mouse movement for scrollbar visibility
        if (event->type() == QEvent::MouseMove) {
            QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
            handleEdgeProximity(canvas, mouseEvent->pos());
        }
        // Handle tablet events for stylus hover (safely)
        else if (event->type() == QEvent::TabletMove) {
            try {
                QTabletEvent* tabletEvent = static_cast<QTabletEvent*>(event);
                handleEdgeProximity(canvas, tabletEvent->position().toPoint());
            } catch (...) {
                // Ignore tablet event errors to prevent crashes
            }
        }
        // Handle mouse button press events for forward/backward navigation
        else if (event->type() == QEvent::MouseButtonPress) {
            QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);

            // Mouse button 4 (Back button) - Previous page
            if (mouseEvent->button() == Qt::BackButton) {
                if (prevPageButton) {
                    prevPageButton->click();
                }
                return true; // Consume the event
            }
            // Mouse button 5 (Forward button) - Next page
            else if (mouseEvent->button() == Qt::ForwardButton) {
                if (nextPageButton) {
                    nextPageButton->click();
                }
                return true; // Consume the event
            }
        }
        // Handle wheel events for scrolling
        else if (event->type() == QEvent::Wheel) {
            QWheelEvent* wheelEvent = static_cast<QWheelEvent*>(event);

            // Check if we need scrolling
            bool needHorizontalScroll = panXSlider->maximum() > 0;
            bool needVerticalScroll = panYSlider->maximum() > 0;

            // Handle vertical scrolling (Y axis)
            if (wheelEvent->angleDelta().y() != 0 && needVerticalScroll) {
                // Calculate scroll amount (negative because wheel up should scroll up)
                int scrollDelta = -wheelEvent->angleDelta().y() / 8; // Convert from 1/8 degree units
                scrollDelta = scrollDelta / 15; // Convert to steps (typical wheel step is 15 degrees)

                // Much faster base scroll speed - aim for ~3-5 scrolls to reach bottom
                int baseScrollAmount = panYSlider->maximum() / 8; // 1/8 of total range per scroll
                scrollDelta = scrollDelta * qMax(baseScrollAmount, 50); // Minimum 50 units per scroll

                // Apply the scroll
                int currentPan = panYSlider->value();
                int newPan = qBound(panYSlider->minimum(), currentPan + scrollDelta, panYSlider->maximum());
                panYSlider->setValue(newPan);

                // Show scrollbar temporarily
                panYSlider->setVisible(true);
                scrollbarsVisible = true;
                if (scrollbarHideTimer->isActive()) {
                    scrollbarHideTimer->stop();
                }
                scrollbarHideTimer->start();

                // Consume the event
                return true;
            }

            // Handle horizontal scrolling (X axis) - for completeness
            if (wheelEvent->angleDelta().x() != 0 && needHorizontalScroll) {
                // Calculate scroll amount
                int scrollDelta = wheelEvent->angleDelta().x() / 8; // Convert from 1/8 degree units
                scrollDelta = scrollDelta / 15; // Convert to steps

                // Much faster base scroll speed - same logic as vertical
                int baseScrollAmount = panXSlider->maximum() / 8; // 1/8 of total range per scroll
                scrollDelta = scrollDelta * qMax(baseScrollAmount, 50); // Minimum 50 units per scroll

                // Apply the scroll
                int currentPan = panXSlider->value();
                int newPan = qBound(panXSlider->minimum(), currentPan + scrollDelta, panXSlider->maximum());
                panXSlider->setValue(newPan);

                // Show scrollbar temporarily
                panXSlider->setVisible(true);
                scrollbarsVisible = true;
                if (scrollbarHideTimer->isActive()) {
                    scrollbarHideTimer->stop();
                }
                scrollbarHideTimer->start();

                // Consume the event
                return true;
            }

            // If no scrolling was needed, let the event propagate
            return false;
        }
    }

    // Handle dial container drag events
    if (obj == dialContainer) {
        if (event->type() == QEvent::MouseButtonPress) {
            QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
            lastMousePos = mouseEvent->globalPos();
            dragging = false;

            if (!longPressTimer) {
                longPressTimer = new QTimer(this);
                longPressTimer->setSingleShot(true);
                connect(longPressTimer, &QTimer::timeout, [this]() {
                    dragging = true;  // ✅ Allow movement after long press
                });
            }
            longPressTimer->start(1500);  // ✅ Start long press timer (500ms)
            return true;
        }

        if (event->type() == QEvent::MouseMove && dragging) {
            QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
            QPoint delta = mouseEvent->globalPos() - lastMousePos;
            dialContainer->move(dialContainer->pos() + delta);
            lastMousePos = mouseEvent->globalPos();
            return true;
        }

        if (event->type() == QEvent::MouseButtonRelease) {
            if (longPressTimer) longPressTimer->stop();
            dragging = false;  // ✅ Stop dragging on release
            return true;
        }
    }

    return QObject::eventFilter(obj, event);
}
    void updateScrollbarPositions() {
    QWidget *container = canvasStack->parentWidget();
    if (!container || !panXSlider || !panYSlider) return;

    // Add small margins for better visibility
    const int margin = 3;

    // Get scrollbar dimensions
    const int scrollbarWidth = panYSlider->width();
    const int scrollbarHeight = panXSlider->height();

    // Calculate sizes based on container
    int containerWidth = container->width();
    int containerHeight = container->height();

    // Leave a bit of space for the corner
    int cornerOffset = 15;

    // Position horizontal scrollbar at top
    panXSlider->setGeometry(
        cornerOffset + margin,  // Leave space at left corner
        margin,
        containerWidth - cornerOffset - margin*2,  // Full width minus corner and right margin
        scrollbarHeight
    );

    // Position vertical scrollbar at left
    panYSlider->setGeometry(
        margin,
        cornerOffset + margin,  // Leave space at top corner
        scrollbarWidth,
        containerHeight - cornerOffset - margin*2  // Full height minus corner and bottom margin
    );
}
    void handleEdgeProximity(InkCanvas* canvas, const QPoint& pos) {
    if (!canvas) return;

    // Get canvas dimensions
    int canvasWidth = canvas->width();
    int canvasHeight = canvas->height();

    // Edge detection zones - show scrollbars when pointer is within 50px of edges
    bool nearLeftEdge = pos.x() < 25;  // For vertical scrollbar
    bool nearTopEdge = pos.y() < 25;   // For horizontal scrollbar - entire top edge

    // Only show scrollbars if canvas is larger than viewport
    bool needHorizontalScroll = panXSlider->maximum() > 0;
    bool needVerticalScroll = panYSlider->maximum() > 0;

    // Show/hide scrollbars based on pointer position
    if (nearLeftEdge && needVerticalScroll) {
        panYSlider->setVisible(true);
        scrollbarsVisible = true;
        if (scrollbarHideTimer->isActive()) {
            scrollbarHideTimer->stop();
        }
        scrollbarHideTimer->start();
    }

    if (nearTopEdge && needHorizontalScroll) {
        panXSlider->setVisible(true);
        scrollbarsVisible = true;
        if (scrollbarHideTimer->isActive()) {
            scrollbarHideTimer->stop();
        }
        scrollbarHideTimer->start();
    }
}
    bool isToolbarTwoRows = false;
    QVBoxLayout *controlLayoutVertical = nullptr;
    QHBoxLayout *controlLayoutSingle = nullptr;
    QHBoxLayout *controlLayoutFirstRow = nullptr;
    QHBoxLayout *controlLayoutSecondRow = nullptr;
    void updateToolbarLayout() {
    // Calculate scaled width using device pixel ratio
    QScreen *screen = QGuiApplication::primaryScreen();
    // qreal dpr = screen ? screen->devicePixelRatio() : 1.0;
    int scaledWidth = width();

    // Dynamic threshold based on zoom button visibility
    int threshold = areZoomButtonsVisible() ? 1548 : 1438;

    // Debug output to understand what's happening
    // qDebug() << "Window width:" << scaledWidth << "Threshold:" << threshold << "Zoom buttons visible:" << areZoomButtonsVisible();

    bool shouldBeTwoRows = scaledWidth <= threshold;

    // qDebug() << "Should be two rows:" << shouldBeTwoRows << "Currently is two rows:" << isToolbarTwoRows;

    if (shouldBeTwoRows != isToolbarTwoRows) {
        isToolbarTwoRows = shouldBeTwoRows;

        // qDebug() << "Switching to" << (isToolbarTwoRows ? "two rows" : "single row");

        if (isToolbarTwoRows) {
            createTwoRowLayout();
        } else {
            createSingleRowLayout();
        }
    }
}
    void createSingleRowLayout() {
    // Delete separator line if it exists (from previous 2-row layout)
    if (separatorLine) {
        delete separatorLine;
        separatorLine = nullptr;
    }

    // Create new single row layout first
    QHBoxLayout *newLayout = new QHBoxLayout;

    // Add all widgets to single row (same order as before)
    newLayout->addWidget(toggleTabBarButton);
            newLayout->addWidget(toggleOutlineButton);
        newLayout->addWidget(toggleBookmarksButton);
        newLayout->addWidget(toggleBookmarkButton);
        newLayout->addWidget(touchGesturesButton);
        newLayout->addWidget(selectFolderButton);
    newLayout->addWidget(exportNotebookButton);
    newLayout->addWidget(importNotebookButton);
    newLayout->addWidget(loadPdfButton);
    newLayout->addWidget(clearPdfButton);
    newLayout->addWidget(pdfTextSelectButton);
    newLayout->addWidget(backgroundButton);
    newLayout->addWidget(saveButton);
    newLayout->addWidget(saveAnnotatedButton);
    newLayout->addWidget(openControlPanelButton);
    newLayout->addWidget(openRecentNotebooksButton);
    newLayout->addWidget(redButton);
    newLayout->addWidget(blueButton);
    newLayout->addWidget(yellowButton);
    newLayout->addWidget(greenButton);
    newLayout->addWidget(blackButton);
    newLayout->addWidget(whiteButton);
    newLayout->addWidget(customColorButton);
    newLayout->addWidget(penToolButton);
    newLayout->addWidget(markerToolButton);
    newLayout->addWidget(eraserToolButton);
    newLayout->addWidget(straightLineToggleButton);
    newLayout->addWidget(ropeToolButton);
    newLayout->addWidget(markdownButton);
    newLayout->addWidget(dialToggleButton);
    newLayout->addWidget(fastForwardButton);
    newLayout->addWidget(btnPageSwitch);
    newLayout->addWidget(btnPannScroll);
    newLayout->addWidget(btnZoom);
    newLayout->addWidget(btnThickness);

    newLayout->addWidget(btnTool);
    newLayout->addWidget(btnPresets);
    newLayout->addWidget(addPresetButton);
    newLayout->addWidget(fullscreenButton);

    // Only add zoom buttons if they're visible
    if (areZoomButtonsVisible()) {
        newLayout->addWidget(zoom50Button);
        newLayout->addWidget(dezoomButton);
        newLayout->addWidget(zoom200Button);
    }

    newLayout->addStretch();
    newLayout->addWidget(prevPageButton);
    newLayout->addWidget(pageInput);
    newLayout->addWidget(nextPageButton);
    newLayout->addWidget(benchmarkButton);
    newLayout->addWidget(benchmarkLabel);
    newLayout->addWidget(deletePageButton);

    // Safely replace the layout
    QLayout* oldLayout = controlBar->layout();
    if (oldLayout) {
        // Remove all items from old layout (but don't delete widgets)
        QLayoutItem* item;
        while ((item = oldLayout->takeAt(0)) != nullptr) {
            // Just removing, not deleting widgets
        }
        delete oldLayout;
    }

    // Set the new layout
    controlBar->setLayout(newLayout);
    controlLayoutSingle = newLayout;

    // Clean up other layout pointers
    controlLayoutVertical = nullptr;
    controlLayoutFirstRow = nullptr;
    controlLayoutSecondRow = nullptr;

    // Update pan range after layout change
    updatePanRange();
}
    void createTwoRowLayout() {
    // Create new layouts first
    QVBoxLayout *newVerticalLayout = new QVBoxLayout;
    QHBoxLayout *newFirstRowLayout = new QHBoxLayout;
    QHBoxLayout *newSecondRowLayout = new QHBoxLayout;

    // Add comfortable spacing and margins for 2-row layout
    newFirstRowLayout->setContentsMargins(8, 8, 8, 6);  // More generous margins
    newFirstRowLayout->setSpacing(3);  // Add spacing between buttons for less cramped feel
    newSecondRowLayout->setContentsMargins(8, 6, 8, 8);  // More generous margins
    newSecondRowLayout->setSpacing(3);  // Add spacing between buttons for less cramped feel

    // First row: up to customColorButton
    newFirstRowLayout->addWidget(toggleTabBarButton);
            newFirstRowLayout->addWidget(toggleOutlineButton);
        newFirstRowLayout->addWidget(toggleBookmarksButton);
        newFirstRowLayout->addWidget(toggleBookmarkButton);
        newFirstRowLayout->addWidget(touchGesturesButton);
        newFirstRowLayout->addWidget(selectFolderButton);
    newFirstRowLayout->addWidget(exportNotebookButton);
    newFirstRowLayout->addWidget(importNotebookButton);
    newFirstRowLayout->addWidget(loadPdfButton);
    newFirstRowLayout->addWidget(clearPdfButton);
    newFirstRowLayout->addWidget(pdfTextSelectButton);
    newFirstRowLayout->addWidget(backgroundButton);
    newFirstRowLayout->addWidget(saveButton);
    newFirstRowLayout->addWidget(saveAnnotatedButton);
    newFirstRowLayout->addWidget(openControlPanelButton);
    newFirstRowLayout->addWidget(openRecentNotebooksButton);
    newFirstRowLayout->addWidget(redButton);
    newFirstRowLayout->addWidget(blueButton);
    newFirstRowLayout->addWidget(yellowButton);
    newFirstRowLayout->addWidget(greenButton);
    newFirstRowLayout->addWidget(blackButton);
    newFirstRowLayout->addWidget(whiteButton);
    newFirstRowLayout->addWidget(customColorButton);
    newFirstRowLayout->addWidget(penToolButton);
    newFirstRowLayout->addWidget(markerToolButton);
    newFirstRowLayout->addWidget(eraserToolButton);
    newFirstRowLayout->addStretch();

    // Create a separator line
    if (!separatorLine) {
        separatorLine = new QFrame();
        separatorLine->setFrameShape(QFrame::HLine);
        separatorLine->setFrameShadow(QFrame::Sunken);
        separatorLine->setLineWidth(1);
        separatorLine->setStyleSheet("QFrame { color: rgba(255, 255, 255, 255); }");
    }

    // Second row: everything after customColorButton
    newSecondRowLayout->addWidget(straightLineToggleButton);
    newSecondRowLayout->addWidget(ropeToolButton);
    newSecondRowLayout->addWidget(markdownButton);
    newSecondRowLayout->addWidget(dialToggleButton);
    newSecondRowLayout->addWidget(fastForwardButton);
    newSecondRowLayout->addWidget(btnPageSwitch);
    newSecondRowLayout->addWidget(btnPannScroll);
    newSecondRowLayout->addWidget(btnZoom);
    newSecondRowLayout->addWidget(btnThickness);

    newSecondRowLayout->addWidget(btnTool);
    newSecondRowLayout->addWidget(btnPresets);
    newSecondRowLayout->addWidget(addPresetButton);
    newSecondRowLayout->addWidget(fullscreenButton);

    // Only add zoom buttons if they're visible
    if (areZoomButtonsVisible()) {
        newSecondRowLayout->addWidget(zoom50Button);
        newSecondRowLayout->addWidget(dezoomButton);
        newSecondRowLayout->addWidget(zoom200Button);
    }

    newSecondRowLayout->addStretch();
    newSecondRowLayout->addWidget(prevPageButton);
    newSecondRowLayout->addWidget(pageInput);
    newSecondRowLayout->addWidget(nextPageButton);
    newSecondRowLayout->addWidget(benchmarkButton);
    newSecondRowLayout->addWidget(benchmarkLabel);
    newSecondRowLayout->addWidget(deletePageButton);

    // Add layouts to vertical layout with separator
    newVerticalLayout->addLayout(newFirstRowLayout);
    newVerticalLayout->addWidget(separatorLine);
    newVerticalLayout->addLayout(newSecondRowLayout);
    newVerticalLayout->setContentsMargins(0, 0, 0, 0);
    newVerticalLayout->setSpacing(0); // No spacing since we have our own separator

    // Safely replace the layout
    QLayout* oldLayout = controlBar->layout();
    if (oldLayout) {
        // Remove all items from old layout (but don't delete widgets)
        QLayoutItem* item;
        while ((item = oldLayout->takeAt(0)) != nullptr) {
            // Just removing, not deleting widgets
        }
        delete oldLayout;
    }

    // Set the new layout
    controlBar->setLayout(newVerticalLayout);
    controlLayoutVertical = newVerticalLayout;
    controlLayoutFirstRow = newFirstRowLayout;
    controlLayoutSecondRow = newSecondRowLayout;

    // Clean up other layout pointer
    controlLayoutSingle = nullptr;

    // Update pan range after layout change
    updatePanRange();
}
    QString elideTabText(const QString &text, int maxWidth) {
    // Create a font metrics object using the default font
    QFontMetrics fontMetrics(QApplication::font());

    // Elide the text from the right (showing the beginning)
    return fontMetrics.elidedText(text, Qt::ElideRight, maxWidth);
}
    QTimer *layoutUpdateTimer = nullptr;
    QFrame *separatorLine = nullptr;
  protected:
    void resizeEvent(QResizeEvent *event) override {
    QMainWindow::resizeEvent(event);

    // Use a timer to delay layout updates during resize to prevent excessive switching
    if (!layoutUpdateTimer) {
        layoutUpdateTimer = new QTimer(this);
        layoutUpdateTimer->setSingleShot(true);
        connect(layoutUpdateTimer, &QTimer::timeout, this, [this]() {
            updateToolbarLayout();
            // Also reposition dial after resize finishes
            if (dialContainer && dialContainer->isVisible()) {
                positionDialContainer();
            }
        });
    }

    layoutUpdateTimer->stop();
    layoutUpdateTimer->start(100); // Wait 100ms after resize stops
}
    void keyPressEvent(QKeyEvent *event) override {
    // Don't intercept keyboard events when text input widgets have focus
    // This prevents conflicts with Windows TextInputFramework
    QWidget *focusWidget = QApplication::focusWidget();
    if (focusWidget) {
        bool isTextInputWidget = qobject_cast<QLineEdit*>(focusWidget) ||
                               qobject_cast<QSpinBox*>(focusWidget) ||
                               qobject_cast<QTextEdit*>(focusWidget) ||
                               qobject_cast<QPlainTextEdit*>(focusWidget) ||
                               qobject_cast<QComboBox*>(focusWidget);

        if (isTextInputWidget) {
            // Let text input widgets handle their own keyboard events
            QMainWindow::keyPressEvent(event);
            return;
        }
    }

    // Don't intercept IME-related keyboard shortcuts
    // These are reserved for Windows Input Method Editor
    if (event->modifiers() & Qt::ControlModifier) {
        if (event->key() == Qt::Key_Space ||           // Ctrl+Space (IME toggle)
            event->key() == Qt::Key_Shift ||           // Ctrl+Shift (language switch)
            event->key() == Qt::Key_Alt) {             // Ctrl+Alt (IME functions)
            // Let Windows handle IME shortcuts
            QMainWindow::keyPressEvent(event);
            return;
        }
    }

    // Don't intercept Shift+Alt (another common IME shortcut)
    if ((event->modifiers() & Qt::ShiftModifier) && (event->modifiers() & Qt::AltModifier)) {
        QMainWindow::keyPressEvent(event);
        return;
    }

    // Build key sequence string
    QStringList modifiers;

    if (event->modifiers() & Qt::ControlModifier) modifiers << "Ctrl";
    if (event->modifiers() & Qt::ShiftModifier) modifiers << "Shift";
    if (event->modifiers() & Qt::AltModifier) modifiers << "Alt";
    if (event->modifiers() & Qt::MetaModifier) modifiers << "Meta";

    QString keyString = QKeySequence(event->key()).toString();

    QString fullSequence;
    if (!modifiers.isEmpty()) {
        fullSequence = modifiers.join("+") + "+" + keyString;
    } else {
        fullSequence = keyString;
    }

    // Check if this sequence is mapped
    if (keyboardMappings.contains(fullSequence)) {
        handleKeyboardShortcut(fullSequence);
        event->accept();
        return;
    }

    // If not handled, pass to parent
    QMainWindow::keyPressEvent(event);
}
    void tabletEvent(QTabletEvent *event) override {
    // Since tablet tracking is disabled to prevent crashes, we now only handle
    // basic tablet events that come through when stylus is touching the surface
    if (!event) {
        return;
    }

    // Just pass tablet events to parent safely without custom hover handling
    // (hover tooltips will work through normal mouse events instead)
    try {
        QMainWindow::tabletEvent(event);
    } catch (...) {
        // Catch any exceptions and just accept the event
        event->accept();
    }
}
    void inputMethodEvent(QInputMethodEvent *event) override {
    // Forward IME events to the focused widget
    QWidget *focusWidget = QApplication::focusWidget();
    if (focusWidget && focusWidget != this) {
        QApplication::sendEvent(focusWidget, event);
        event->accept();
        return;
    }

    // Default handling
    QMainWindow::inputMethodEvent(event);
}
    QVariant inputMethodQuery(Qt::InputMethodQuery query) const override {
    // Forward IME queries to the focused widget
    QWidget *focusWidget = QApplication::focusWidget();
    if (focusWidget && focusWidget != this) {
        return focusWidget->inputMethodQuery(query);
    }

    // Default handling
    return QMainWindow::inputMethodQuery(query);
}
};

class MarkdownWindowManager {
    Q_OBJECT

signals:
    void zoomChanged(int newZoom);
    void panChanged(int panX, int panY);
    void touchGestureEnded();
    void ropeSelectionCompleted(const QPoint &position);
    void pdfLinkClicked(int targetPage);
    void pdfTextSelected(const QString &text);
    void pdfLoaded();
    void markdownSelectionModeChanged(bool enabled);
  public:
    explicit InkCanvas(QWidget *parent = nullptr) {

    // Set theme-aware default pen color
    MainWindow *mainWindow = qobject_cast<MainWindow*>(parent);
    if (mainWindow) {
        penColor = mainWindow->getDefaultPenColor();
    }
    setAttribute(Qt::WA_StaticContents);
    setTabletTracking(true);
    setAttribute(Qt::WA_AcceptTouchEvents);  // Enable touch events

    // Enable immediate updates for smoother animation
    setAttribute(Qt::WA_OpaquePaintEvent);
    setAttribute(Qt::WA_NoSystemBackground);

    // Detect screen resolution and set canvas size
    QScreen *screen = QGuiApplication::primaryScreen();
    if (screen) {
        QSize logicalSize = screen->availableGeometry().size() * 0.89;
        setMaximumSize(logicalSize); // Optional
        setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
    } else {
        setFixedSize(1920, 1080); // Fallback size
    }

    initializeBuffer();
    QCache<int, QPixmap> pdfCache(10);
    pdfCache.setMaxCost(10);  // ✅ Ensures the cache holds at most 5 pages
    // No need to set auto-delete, QCache will handle deletion automatically

    // Initialize PDF text selection throttling timer (60 FPS = ~16.67ms)
    pdfTextSelectionTimer = new QTimer(this);
    pdfTextSelectionTimer->setSingleShot(true);
    pdfTextSelectionTimer->setInterval(16); // ~60 FPS
    connect(pdfTextSelectionTimer, &QTimer::timeout, this, &InkCanvas::processPendingTextSelection);

    // Initialize PDF cache timer (will be created on-demand)
    pdfCacheTimer = nullptr;
    currentCachedPage = -1;

    // Initialize note page cache system
    noteCache.setMaxCost(15); // Cache up to 15 note pages (more than PDF since they're smaller)
    noteCacheTimer = nullptr;
    currentCachedNotePage = -1;

    // Initialize markdown manager
    markdownManager = new MarkdownWindowManager(this, this);

    // Connect pan/zoom signals to update markdown window positions
    connect(this, &InkCanvas::panChanged, markdownManager, &MarkdownWindowManager::updateAllWindowPositions);
    connect(this, &InkCanvas::zoomChanged, markdownManager, &MarkdownWindowManager::updateAllWindowPositions);
}
    virtual ~InkCanvas() {
    // Cleanup if needed
}
    void startBenchmark() {
    benchmarking = true;
    processedTimestamps.clear();
    benchmarkTimer.start();
}
    void stopBenchmark() {
    benchmarking = false;
}
    int getProcessedRate() {
    qint64 now = benchmarkTimer.elapsed();
    while (!processedTimestamps.empty() && now - processedTimestamps.front() > 1000) {
        processedTimestamps.pop_front();
    }
    return static_cast<int>(processedTimestamps.size());
}
    void setPenColor(const QColor &color) {
    penColor = color;
}
    void setPenThickness(qreal thickness) {
    // Set thickness for the current tool
    switch (currentTool) {
        case ToolType::Pen:
            penToolThickness = thickness;
            break;
        case ToolType::Marker:
            markerToolThickness = thickness;
            break;
        case ToolType::Eraser:
            eraserToolThickness = thickness;
            break;
    }

    // Update the current thickness for efficient drawing
    penThickness = thickness;
}
    void adjustAllToolThicknesses(qreal zoomRatio) {
    // Adjust thickness for all tools to maintain visual consistency
    penToolThickness *= zoomRatio;
    markerToolThickness *= zoomRatio;
    eraserToolThickness *= zoomRatio;

    // Update the current thickness based on the current tool
    switch (currentTool) {
        case ToolType::Pen:
            penThickness = penToolThickness;
            break;
        case ToolType::Marker:
            penThickness = markerToolThickness;
            break;
        case ToolType::Eraser:
            penThickness = eraserToolThickness;
            break;
    }
}
    void setTool(ToolType tool) {
    currentTool = tool;

    // Switch to the thickness for the new tool
    switch (currentTool) {
        case ToolType::Pen:
            penThickness = penToolThickness;
            break;
        case ToolType::Marker:
            penThickness = markerToolThickness;
            break;
        case ToolType::Eraser:
            penThickness = eraserToolThickness;
            break;
    }
}
    void setSaveFolder(const QString &folderPath) {
    saveFolder = folderPath;
    clearPdfNoDelete();

    if (!saveFolder.isEmpty()) {
        QDir().mkpath(saveFolder);
        loadNotebookId();  // ✅ Load notebook ID when save folder is set
    }

    QString bgMetaFile = saveFolder + "/.background_config.txt";  // This metadata is for background styles, not to be confused with pdf directories.
    if (QFile::exists(bgMetaFile)) {
        QFile file(bgMetaFile);
        if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
            QTextStream in(&file);
            while (!in.atEnd()) {
                QString line = in.readLine().trimmed();
                if (line.startsWith("style=")) {
                    QString val = line.mid(6);
                    if (val == "Grid") backgroundStyle = BackgroundStyle::Grid;
                    else if (val == "Lines") backgroundStyle = BackgroundStyle::Lines;
                    else backgroundStyle = BackgroundStyle::None;
                } else if (line.startsWith("color=")) {
                    backgroundColor = QColor(line.mid(6));
                } else if (line.startsWith("density=")) {
                    backgroundDensity = line.mid(8).toInt();
                }
            }
            file.close();
        }
    }

    // ✅ Check if the folder has a saved PDF path
    QString metadataFile = saveFolder + "/.pdf_path.txt";
    if (!QFile::exists(metadataFile)) {
        return;
    }

    QFile file(metadataFile);
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        return;
    }

    QTextStream in(&file);
    QString storedPdfPath = in.readLine().trimmed();
    file.close();

    if (storedPdfPath.isEmpty()) {
        return;
    }

    // ✅ Ensure the stored PDF file still exists before loading
    if (!QFile::exists(storedPdfPath)) {
        return;
    }
    loadPdf(storedPdfPath);
}
    void saveToFile(int pageNumber) {
    if (saveFolder.isEmpty()) {
        return;
    }
    QString filePath = saveFolder + QString("/%1_%2.png").arg(notebookId).arg(pageNumber, 5, 10, QChar('0'));

    // ✅ If no file exists and the buffer is empty, do nothing

    if (!edited) {
        return;
    }

    QImage image(buffer.size(), QImage::Format_ARGB32);
    image.fill(Qt::transparent);
    QPainter painter(&image);
    painter.drawPixmap(0, 0, buffer);
    image.save(filePath, "PNG");
    edited = false;

    // Save markdown windows for this page
    if (markdownManager) {
        markdownManager->saveWindowsForPage(pageNumber);
    }

    // Update note cache with the saved buffer
    noteCache.insert(pageNumber, new QPixmap(buffer));
}
    void saveCurrentPage() {
    MainWindow *mainWin = qobject_cast<MainWindow*>(parentWidget());  // ✅ Get main window
    if (!mainWin) return;

    int currentPage = mainWin->getCurrentPageForCanvas(this);  // ✅ Get correct page
    saveToFile(currentPage);
}
    void loadPage(int pageNumber) {
    if (saveFolder.isEmpty()) return;

    // Hide any markdown windows from the previous page BEFORE loading the new page content.
    // This ensures the correct repaint area and stops the transparency timer.
    if (markdownManager) {
        markdownManager->hideAllWindows();
    }

    // Update current note page tracker
    currentCachedNotePage = pageNumber;

    // Check if note page is already cached
    bool loadedFromCache = false;
    if (noteCache.contains(pageNumber)) {
        // Use cached note page immediately
        buffer = *noteCache.object(pageNumber);
        loadedFromCache = true;
    } else {
        // Load note page from disk and cache it
        loadNotePageToCache(pageNumber);

        // Use the newly cached page or initialize buffer if loading failed
        if (noteCache.contains(pageNumber)) {
            buffer = *noteCache.object(pageNumber);
            loadedFromCache = true;
    } else {
        initializeBuffer(); // Clear the canvas if no file exists
        loadedFromCache = false;
    }
    }

    // Reset edited state when loading a new page
    edited = false;

    // Handle background image loading (PDF or custom background)
    if (isPdfLoaded && pdfDocument && pageNumber >= 0 && pageNumber < pdfDocument->numPages()) {
        // Use PDF as background (should already be cached by loadPdfPage)
        if (pdfCache.contains(pageNumber)) {
        backgroundImage = *pdfCache.object(pageNumber);

            // Resize canvas buffer to match PDF page size if needed
        if (backgroundImage.size() != buffer.size()) {
            QPixmap newBuffer(backgroundImage.size());
            newBuffer.fill(Qt::transparent);

            // Copy existing drawings
            QPainter painter(&newBuffer);
            painter.drawPixmap(0, 0, buffer);

            buffer = newBuffer;
            // Don't constrain widget size - let it expand to fill available space
            // The paintEvent will center the PDF content within the widget

                // Update cache with resized buffer
                noteCache.insert(pageNumber, new QPixmap(buffer));
            }
        }
    } else {
        // Handle custom background images
        QString bgFileName = saveFolder + QString("/bg_%1_%2.png").arg(notebookId).arg(pageNumber, 5, 10, QChar('0'));
        QString metadataFile = saveFolder + QString("/.%1_bgsize_%2.txt").arg(notebookId).arg(pageNumber, 5, 10, QChar('0'));

        int bgWidth = 0, bgHeight = 0;
        if (QFile::exists(metadataFile)) {
            QFile file(metadataFile);
            if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
                QTextStream in(&file);
                in >> bgWidth >> bgHeight;
                file.close();
            }
        }

        if (QFile::exists(bgFileName)) {
            QImage bgImage(bgFileName);
            backgroundImage = QPixmap::fromImage(bgImage);

            // Resize canvas **only if background resolution is different**
            if (bgWidth > 0 && bgHeight > 0 && (bgWidth != width() || bgHeight != height())) {
                // Create a new buffer
                QPixmap newBuffer(bgWidth, bgHeight);
                newBuffer.fill(Qt::transparent);

                // Copy existing drawings to the new buffer
                QPainter painter(&newBuffer);
                painter.drawPixmap(0, 0, buffer);

                // Assign the new buffer
                buffer = newBuffer;
                setMaximumSize(bgWidth, bgHeight);

                // Update cache with resized buffer
                noteCache.insert(pageNumber, new QPixmap(buffer));
            }
        } else {
            backgroundImage = QPixmap(); // No background for this page
            // Only apply device pixel ratio fix if buffer was NOT loaded from cache
            // This prevents resizing cached buffers that might already be correctly sized
            if (!loadedFromCache) {
                QScreen *screen = QGuiApplication::primaryScreen();
                qreal dpr = screen ? screen->devicePixelRatio() : 1.0;
                QSize logicalSize = screen ? screen->size() : QSize(1440, 900);
                QSize expectedPixelSize = logicalSize * dpr;

                if (buffer.size() != expectedPixelSize) {
                    // Buffer is wrong size, need to resize it properly
                    QPixmap newBuffer(expectedPixelSize);
                    newBuffer.fill(Qt::transparent);

                    // Copy existing drawings if buffer was smaller
                    if (!buffer.isNull()) {
                        QPainter painter(&newBuffer);
                        painter.drawPixmap(0, 0, buffer);
                    }

                    buffer = newBuffer;
                    setMaximumSize(expectedPixelSize);
                }
            }
        }
    }

    // Cache adjacent note pages after delay for faster navigation
    checkAndCacheAdjacentNotePages(pageNumber);

    update();
    adjustSize();            // Force the widget to update its size
    parentWidget()->update();

    // Load markdown windows AFTER canvas is fully rendered and sized
    // Use a single-shot timer to ensure the canvas is fully updated
    QTimer::singleShot(0, this, [this, pageNumber]() {
        if (markdownManager) {
            markdownManager->loadWindowsForPage(pageNumber);
        }
    });
}
    void deletePage(int pageNumber) {
    if (saveFolder.isEmpty()) {
        return;
    }
    QString fileName = saveFolder + QString("/%1_%2.png").arg(notebookId).arg(pageNumber, 5, 10, QChar('0'));
    QString bgFileName = saveFolder + QString("/bg_%1_%2.png").arg(notebookId).arg(pageNumber, 5, 10, QChar('0'));
    QString metadataFileName = saveFolder + QString("/.%1_bgsize_%2.txt").arg(notebookId).arg(pageNumber, 5, 10, QChar('0'));

    #ifdef Q_OS_WIN
    // Remove hidden attribute before deleting in Windows
    SetFileAttributesW(reinterpret_cast<const wchar_t *>(metadataFileName.utf16()), FILE_ATTRIBUTE_NORMAL);
    #endif

    QFile::remove(fileName);
    QFile::remove(bgFileName);
    QFile::remove(metadataFileName);

    // Remove deleted page from note cache
    noteCache.remove(pageNumber);

    // Delete markdown windows for this page
    if (markdownManager) {
        markdownManager->deleteWindowsForPage(pageNumber);
    }

    if (pdfDocument){
        loadPdfPage(pageNumber);
    }
    else{
        loadPage(pageNumber);
    }

}
    void setBackground(const QString &filePath, int pageNumber) {
    if (saveFolder.isEmpty()) {
        return; // No save folder set
    }

    // Construct full path inside save folder
    QString bgFileName = saveFolder + QString("/bg_%1_%2.png").arg(notebookId).arg(pageNumber, 5, 10, QChar('0'));

    // Copy the file to the save folder
    QFile::copy(filePath, bgFileName);

    // Load the background image
    QImage bgImage(bgFileName);

    if (!bgImage.isNull()) {
        // Save background resolution in metadata file
        QString metadataFile = saveFolder + QString("/.%1_bgsize_%2.txt")
                                            .arg(notebookId)
                                            .arg(pageNumber, 5, 10, QChar('0'));
        QFile file(metadataFile);
        if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
            QTextStream out(&file);
            out << bgImage.width() << " " << bgImage.height();
            file.close();
        }

        #ifdef Q_OS_WIN
        // Set hidden attribute for metadata on Windows
        SetFileAttributesW(reinterpret_cast<const wchar_t *>(metadataFile.utf16()), FILE_ATTRIBUTE_HIDDEN);
        #endif

        // Only resize if the background size is different
        if (bgImage.width() != width() || bgImage.height() != height()) {
            // Create a new buffer with the new size
            QPixmap newBuffer(bgImage.width(), bgImage.height());
            newBuffer.fill(Qt::transparent);

            // Copy existing drawings
            QPainter painter(&newBuffer);
            painter.drawPixmap(0, 0, buffer);

            // Assign new buffer and update canvas size
            buffer = newBuffer;
            setMaximumSize(bgImage.width(), bgImage.height());
        }

        backgroundImage = QPixmap::fromImage(bgImage);

        update();
        adjustSize();
        parentWidget()->update();
    }

    update();  // Refresh canvas
}
    void saveAnnotated(int pageNumber) {
    if (saveFolder.isEmpty()) return;

    QString filePath = saveFolder + QString("/annotated_%1_%2.png").arg(notebookId).arg(pageNumber, 5, 10, QChar('0'));

    // Use the buffer size to ensure correct resolution
    QImage image(buffer.size(), QImage::Format_ARGB32);
    image.fill(Qt::transparent);
    QPainter painter(&image);

    if (!backgroundImage.isNull()) {
        painter.drawPixmap(0, 0, backgroundImage.scaled(buffer.size(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation));
    }

    painter.drawPixmap(0, 0, buffer);
    image.save(filePath, "PNG");
}
    void setZoom(int zoomLevel) {
    int newZoom = qMax(10, qMin(zoomLevel, 400)); // Limit zoom to 10%-400%
    if (zoomFactor != newZoom) {
        zoomFactor = newZoom;
        internalZoomFactor = zoomFactor; // Sync internal zoom
    update();
        emit zoomChanged(zoomFactor);
    }
}
    int getZoom() const {
    return zoomFactor;
}
    void updatePanOffsets(int xOffset, int yOffset) {
    panOffsetX = xOffset;
    panOffsetY = yOffset;
    update();
}
    int getPanOffsetX() const {
    return panOffsetX;
}
    int getPanOffsetY() const {
    return panOffsetY;
}
    void setPanX(int xOffset) {
    if (panOffsetX != value) {
    panOffsetX = value;
    update();
        emit panChanged(panOffsetX, panOffsetY);
    }
}
    void setPanY(int yOffset) {
    if (panOffsetY != value) {
    panOffsetY = value;
    update();
        emit panChanged(panOffsetX, panOffsetY);
    }
}
    void loadPdf(const QString &pdfPath) {
    pdfDocument = Poppler::Document::load(pdfPath);
    if (pdfDocument && !pdfDocument->isLocked()) {
        // Enable anti-aliasing rendering hints for better text quality
        pdfDocument->setRenderHint(Poppler::Document::Antialiasing, true);
        pdfDocument->setRenderHint(Poppler::Document::TextAntialiasing, true);
        pdfDocument->setRenderHint(Poppler::Document::TextHinting, true);
        pdfDocument->setRenderHint(Poppler::Document::TextSlightHinting, true);

        totalPdfPages = pdfDocument->numPages();
        isPdfLoaded = true;
        totalPdfPages = pdfDocument->numPages();
        loadPdfPage(0); // Load first page
        // ✅ Save the PDF path in the metadata file
        if (!saveFolder.isEmpty()) {
            QString metadataFile = saveFolder + "/.pdf_path.txt";
            QFile file(metadataFile);
            if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
                QTextStream out(&file);
                out << pdfPath;  // Store the absolute path of the PDF
                file.close();
            }
        }
        // Emit signal that PDF was loaded
        emit pdfLoaded();
    }
}
    void loadPdfPage(int pageNumber) {
    if (!pdfDocument) return;

    // Update current page tracker
    currentCachedPage = pageNumber;

    // Check if target page is already cached
    if (pdfCache.contains(pageNumber)) {
        // Display the cached page immediately
        backgroundImage = *pdfCache.object(pageNumber);
        loadPage(pageNumber);  // Load annotations
        loadPdfTextBoxes(pageNumber); // Load text boxes for PDF text selection
        update();

        // Check and cache adjacent pages after delay
        checkAndCacheAdjacentPages(pageNumber);
        return;
    }

    // Target page not in cache - render it immediately
    renderPdfPageToCache(pageNumber);

    // Display the newly rendered page
    if (pdfCache.contains(pageNumber)) {
        backgroundImage = *pdfCache.object(pageNumber);
    } else {
        backgroundImage = QPixmap();  // Clear background if rendering failed
    }

    loadPage(pageNumber);  // Load existing canvas annotations
    loadPdfTextBoxes(pageNumber); // Load text boxes for PDF text selection
    update();

    // Cache adjacent pages after delay
    checkAndCacheAdjacentPages(pageNumber);
}
    bool isPdfLoadedFunc() const {
    return isPdfLoaded;
}
    int getTotalPdfPages() const {
    return totalPdfPages;
}
    Poppler::Document* getPdfDocument() const;
    void clearPdf() {
    pdfDocument.reset();
    pdfDocument = nullptr;
    isPdfLoaded = false;
    totalPdfPages = 0;
    pdfCache.clear();

    // Reset cache system state
    currentCachedPage = -1;
    if (pdfCacheTimer && pdfCacheTimer->isActive()) {
        pdfCacheTimer->stop();
    }

    // Cancel and clean up any active PDF watchers
    for (QFutureWatcher<void>* watcher : activePdfWatchers) {
        if (watcher && !watcher->isFinished()) {
            watcher->cancel();
        }
        watcher->deleteLater();
    }
    activePdfWatchers.clear();

    // ✅ Remove the PDF path file when clearing the PDF
    if (!saveFolder.isEmpty()) {
        QString metadataFile = saveFolder + "/.pdf_path.txt";
        QFile::remove(metadataFile);
    }
}
    void clearPdfNoDelete() {
    pdfDocument.reset();
    pdfDocument = nullptr;
    isPdfLoaded = false;
    totalPdfPages = 0;
    pdfCache.clear();

    // Reset cache system state
    currentCachedPage = -1;
    if (pdfCacheTimer && pdfCacheTimer->isActive()) {
        pdfCacheTimer->stop();
    }

    // Cancel and clean up any active PDF watchers
    for (QFutureWatcher<void>* watcher : activePdfWatchers) {
        if (watcher && !watcher->isFinished()) {
            watcher->cancel();
        }
        watcher->deleteLater();
    }
    activePdfWatchers.clear();
}
    QString getSaveFolder() const {
    return saveFolder;
}
    QColor getPenColor() {
    return penColor;
}
    qreal getPenThickness() {
    return penThickness;
}
    ToolType getCurrentTool() {
    return currentTool;
}
    void loadPdfPreviewAsync(int pageNumber) {
    if (!pdfDocument || pageNumber < 0 || pageNumber >= pdfDocument->numPages()) return;

    QFutureWatcher<QPixmap> *watcher = new QFutureWatcher<QPixmap>(this);

    connect(watcher, &QFutureWatcher<QPixmap>::finished, this, [this, watcher]() {
        QPixmap result = watcher->result();
        if (!result.isNull()) {
            backgroundImage = result;
            update();  // trigger repaint
        }
        watcher->deleteLater();  // Clean up
    });

    QFuture<QPixmap> future = QtConcurrent::run([=]() -> QPixmap {
        std::unique_ptr<Poppler::Page> page(pdfDocument->page(pageNumber));
        if (!page) return QPixmap();

        // Render with document-level anti-aliasing settings
        QImage pdfImage = page->renderToImage(96, 96);
        if (pdfImage.isNull()) return QPixmap();

        QImage upscaled = pdfImage.scaled(pdfImage.width() * (pdfRenderDPI / 96),
                                  pdfImage.height() * (pdfRenderDPI / 96),
                                  Qt::KeepAspectRatio,
                                  Qt::SmoothTransformation);

        return QPixmap::fromImage(upscaled);
    });

    watcher->setFuture(future);
}
    void setBackgroundStyle(BackgroundStyle style) {
    backgroundStyle = style;
    update();  // Trigger repaint
}
    void setBackgroundColor(const QColor &color) {
    backgroundColor = color;
    update();
}
    void setBackgroundDensity(int density) {
    backgroundDensity = density;
    update();
}
    void saveBackgroundMetadata() {
    if (saveFolder.isEmpty()) return;

    QString bgMetaFile = saveFolder + "/.background_config.txt";
    QFile file(bgMetaFile);
    if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
        QTextStream out(&file);
        QString styleStr = "None";
        if (backgroundStyle == BackgroundStyle::Grid) styleStr = "Grid";
        else if (backgroundStyle == BackgroundStyle::Lines) styleStr = "Lines";

        out << "style=" << styleStr << "\n";
        out << "color=" << backgroundColor.name().toUpper() << "\n";
        out << "density=" << backgroundDensity << "\n";
        file.close();
    }
}
    void exportNotebook(const QString &destinationFile) {
    if (destinationFile.isEmpty()) {
        QMessageBox::warning(nullptr, tr("Export Error"), tr("No export file specified."));
        return;
    }

    if (saveFolder.isEmpty()) {
        QMessageBox::warning(nullptr, tr("Export Error"), tr("No notebook loaded (saveFolder is empty)"));
        return;
    }

    QStringList files;

    // Collect all files from saveFolder
    QDir dir(saveFolder);
    QDirIterator it(saveFolder, QDir::Files, QDirIterator::Subdirectories);
    while (it.hasNext()) {
        QString filePath = it.next();
        QString relativePath = dir.relativeFilePath(filePath);
        files << relativePath;
    }

    if (files.isEmpty()) {
        QMessageBox::warning(nullptr, tr("Export Error"), tr("No files found to export."));
        return;
    }

    // Generate temporary file list
    QString tempFileList = saveFolder + "/filelist.txt";
    QFile listFile(tempFileList);
    if (listFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
        QTextStream out(&listFile);
        for (const QString &file : files) {
            out << file << "\n";
        }
        listFile.close();
    } else {
        QMessageBox::warning(nullptr, tr("Export Error"), tr("Failed to create temporary file list."));
        return;
    }

#ifdef _WIN32
    QString tarExe = QCoreApplication::applicationDirPath() + "/bsdtar.exe";
#else
    QString tarExe = "tar";
#endif

    QStringList args;
    args << "-cf" << QDir::toNativeSeparators(destinationFile);

    for (const QString &file : files) {
        args << QDir::toNativeSeparators(file);
    }

    QProcess process;
    process.setWorkingDirectory(saveFolder);

    process.start(tarExe, args);
    if (!process.waitForFinished()) {
        QMessageBox::warning(nullptr, tr("Export Error"), tr("Tar process failed to finish."));
        QFile::remove(tempFileList);
        return;
    }

    QFile::remove(tempFileList);

    if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) {
        QMessageBox::warning(nullptr, tr("Export Error"), tr("Tar process failed."));
        return;
    }

    QMessageBox::information(nullptr, tr("Export"), tr("Notebook exported successfully."));
}
    void importNotebook(const QString &packageFile) {

    // Ask user for destination working folder
    QString destFolder = QFileDialog::getExistingDirectory(nullptr, tr("Select Destination Folder for Imported Notebook"));

    if (destFolder.isEmpty()) {
        QMessageBox::warning(nullptr, tr("Import Canceled"), tr("No destination folder selected."));
        return;
    }

    // Check if destination folder is empty (optional, good practice)
    QDir destDir(destFolder);
    if (!destDir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot).isEmpty()) {
        QMessageBox::StandardButton reply = QMessageBox::question(nullptr, tr("Destination Not Empty"),
            tr("The selected folder is not empty. Files may be overwritten. Continue?"),
            QMessageBox::Yes | QMessageBox::No);
        if (reply != QMessageBox::Yes) {
            return;
        }
    }

#ifdef _WIN32
    QString tarExe = QCoreApplication::applicationDirPath() + "/bsdtar.exe";
#else
    QString tarExe = "tar";
#endif

    // Extract package
    QStringList args;
    args << "-xf" << packageFile;

    QProcess process;
    process.setWorkingDirectory(destFolder);
    process.start(tarExe, args);
    process.waitForFinished();

    // Switch notebook folder
    setSaveFolder(destFolder);
    loadPage(0);

    QMessageBox::information(nullptr, tr("Import Complete"), tr("Notebook imported successfully."));
}
    void importNotebookTo(const QString &packageFile, const QString &destFolder) {

    #ifdef _WIN32
        QString tarExe = QCoreApplication::applicationDirPath() + "/bsdtar.exe";
    #else
        QString tarExe = "tar";
    #endif

        QStringList args;
        args << "-xf" << packageFile;

        QProcess process;
        process.setWorkingDirectory(destFolder);
        process.start(tarExe, args);
        process.waitForFinished();

        setSaveFolder(destFolder);
        loadNotebookId();
        loadPage(0);

        QMessageBox::information(nullptr, tr("Import"), tr("Notebook imported successfully."));
    }
    void deleteRopeSelection() {
    if (!selectionBuffer.isNull() && !selectionRect.isEmpty()) {
        // If the selection area hasn't been cleared from the buffer yet, clear it now for deletion
        if (!selectionAreaCleared && !selectionMaskPath.isEmpty()) {
            QPainter painter(&buffer);
            painter.setCompositionMode(QPainter::CompositionMode_Clear);
            painter.fillPath(selectionMaskPath, Qt::transparent);
            painter.end();
        }

        // Clear the selection state
        selectionBuffer = QPixmap();
        selectionRect = QRect();
        exactSelectionRectF = QRectF();
        movingSelection = false;
        selectingWithRope = false;
        selectionJustCopied = false;
        selectionAreaCleared = false;
        selectionMaskPath = QPainterPath();
        selectionBufferRect = QRectF();

        // Mark as edited since we deleted content
        if (!edited) {
            edited = true;
            // Invalidate cache for current page since it's been modified
            invalidateCurrentPageCache();
        }

        // Update the entire canvas to remove selection visuals
        update();
    }
}
    void cancelRopeSelection() {
    if (!selectionBuffer.isNull() && !selectionRect.isEmpty()) {
        // Paste the selection back to its current location (where user moved it)
        QPainter painter(&buffer);
        // Explicitly set composition mode to draw on top of existing content
        painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
        // Use the current selection position
        QPointF currentTopLeft = exactSelectionRectF.isEmpty() ? selectionRect.topLeft() : exactSelectionRectF.topLeft();
        QPointF bufferDest = mapLogicalWidgetToPhysicalBuffer(currentTopLeft);
        painter.drawPixmap(bufferDest.toPoint(), selectionBuffer);
        painter.end();

        // Store selection buffer size for update calculation before clearing it
        QSize selectionSize = selectionBuffer.size();
        QRect updateRect = QRect(currentTopLeft.toPoint(), selectionSize);

        // Clear the selection state
        selectionBuffer = QPixmap();
        selectionRect = QRect();
        exactSelectionRectF = QRectF();
        movingSelection = false;
        selectingWithRope = false;
        selectionJustCopied = false;
        selectionAreaCleared = false;
        selectionMaskPath = QPainterPath();
        selectionBufferRect = QRectF();

        // Update the restored area
        update(updateRect.adjusted(-5, -5, 5, 5));
    }
}
    void copyRopeSelection() {
    if (!selectionBuffer.isNull() && !selectionRect.isEmpty()) {
        // Get current selection position
        QPointF currentTopLeft = exactSelectionRectF.isEmpty() ? selectionRect.topLeft() : exactSelectionRectF.topLeft();

        // Calculate new position (right next to the original), but ensure it stays within canvas bounds
        QPointF newTopLeft = currentTopLeft + QPointF(selectionRect.width() + 5, 0); // 5 pixels gap

        // Check if the new position would extend beyond buffer boundaries and adjust if needed
        QPointF currentBufferDest = mapLogicalWidgetToPhysicalBuffer(currentTopLeft);
        QPointF newBufferDest = mapLogicalWidgetToPhysicalBuffer(newTopLeft);

        // Ensure the copy stays within buffer bounds
        if (newBufferDest.x() + selectionBuffer.width() > buffer.width()) {
            // If it would extend beyond right edge, place it to the left of the original
            newTopLeft = currentTopLeft - QPointF(selectionRect.width() + 5, 0);
            newBufferDest = mapLogicalWidgetToPhysicalBuffer(newTopLeft);

            // If it still doesn't fit on the left, place it below the original
            if (newBufferDest.x() < 0) {
                newTopLeft = currentTopLeft + QPointF(0, selectionRect.height() + 5);
                newBufferDest = mapLogicalWidgetToPhysicalBuffer(newTopLeft);

                // If it doesn't fit below either, place it above
                if (newBufferDest.y() + selectionBuffer.height() > buffer.height()) {
                    newTopLeft = currentTopLeft - QPointF(0, selectionRect.height() + 5);
                    newBufferDest = mapLogicalWidgetToPhysicalBuffer(newTopLeft);

                    // If none of the positions work, just offset slightly and let it extend
                    if (newBufferDest.y() < 0) {
                        newTopLeft = currentTopLeft + QPointF(10, 10);
                        newBufferDest = mapLogicalWidgetToPhysicalBuffer(newTopLeft);
                    }
                }
            }
        }
        if (newBufferDest.y() + selectionBuffer.height() > buffer.height()) {
            // If it would extend beyond bottom edge, place it above the original
            newTopLeft = currentTopLeft - QPointF(0, selectionRect.height() + 5);
            newBufferDest = mapLogicalWidgetToPhysicalBuffer(newTopLeft);
        }

        // First, paste the selection back to its current location to restore the original
        QPainter painter(&buffer);
        // Explicitly set composition mode to draw on top of existing content
        painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
        painter.drawPixmap(currentBufferDest.toPoint(), selectionBuffer);

        // Then, paste the copy to the new location
        // We need to handle the case where the copy extends beyond buffer boundaries
        QRect targetRect(newBufferDest.toPoint(), selectionBuffer.size());
        QRect bufferBounds(0, 0, buffer.width(), buffer.height());
        QRect clippedRect = targetRect.intersected(bufferBounds);

        if (!clippedRect.isEmpty()) {
            // Calculate which part of the selectionBuffer to draw
            QRect sourceRect = QRect(
                clippedRect.x() - targetRect.x(),
                clippedRect.y() - targetRect.y(),
                clippedRect.width(),
                clippedRect.height()
            );

            painter.drawPixmap(clippedRect, selectionBuffer, sourceRect);
        }

        // For the selection buffer, we keep the FULL content, even if parts extend beyond canvas
        // This way when the user drags it back, the full content is preserved
        QPixmap newSelectionBuffer = selectionBuffer; // Keep the full original content

        // DON'T clear the copied area immediately - leave both original and copy on the canvas
        // The copy will only be cleared when the user actually starts moving the selection
        // This way, if the user cancels without moving, both original and copy remain permanently
        painter.end();

        // Update the selection buffer and position
        selectionBuffer = newSelectionBuffer;
        selectionRect = QRect(newTopLeft.toPoint(), selectionRect.size());
        exactSelectionRectF = QRectF(newTopLeft, selectionRect.size());
        selectionJustCopied = true; // Mark that this selection was just copied

        // Mark as edited since we added content
        if (!edited) {
            edited = true;
            // Invalidate cache for current page since it's been modified
            invalidateCurrentPageCache();
        }

        // Update the entire affected area (original + copy + gap)
        QRect updateArea = QRect(currentTopLeft.toPoint(), selectionRect.size())
                          .united(selectionRect)
                          .adjusted(-10, -10, 10, 10);
        update(updateArea);
    }
}
    void setMarkdownSelectionMode(bool enabled) {
    markdownSelectionMode = enabled;

    if (markdownManager) {
        markdownManager->setSelectionMode(enabled);
    }

    if (!enabled) {
        markdownSelecting = false;
    }

    // Update cursor
    setCursor(enabled ? Qt::CrossCursor : Qt::ArrowCursor);

    // Notify signal
    emit markdownSelectionModeChanged(enabled);
}
    bool isMarkdownSelectionMode() const {
    return markdownSelectionMode;
}
    void clearPdfTextSelection() {
    // Clear selection state
    selectedTextBoxes.clear();
    pdfTextSelecting = false;

    // Cancel any pending throttled updates
    if (pdfTextSelectionTimer && pdfTextSelectionTimer->isActive()) {
        pdfTextSelectionTimer->stop();
    }
    hasPendingSelection = false;

    // Refresh display
    update();
}
    QString getSelectedPdfText() const {
    if (selectedTextBoxes.isEmpty()) {
        return QString();
    }

    // Pre-allocate string with estimated size for efficiency
    QString selectedText;
    selectedText.reserve(selectedTextBoxes.size() * 20); // Estimate ~20 chars per text box

    // Build text with space separators
    for (const Poppler::TextBox* textBox : selectedTextBoxes) {
        if (textBox && !textBox->text().isEmpty()) {
            if (!selectedText.isEmpty()) {
                selectedText += " ";
            }
            selectedText += textBox->text();
        }
    }

    return selectedText;
}
    QPointF mapWidgetToCanvas(const QPointF &widgetPoint) const {
    QPointF topLeft = mapWidgetToCanvas(widgetRect.topLeft());
    QPointF bottomRight = mapWidgetToCanvas(widgetRect.bottomRight());

    return QRect(topLeft.toPoint(), bottomRight.toPoint());
}
    QPointF mapCanvasToWidget(const QPointF &canvasPoint) const {
    QPointF topLeft = mapCanvasToWidget(canvasRect.topLeft());
    QPointF bottomRight = mapCanvasToWidget(canvasRect.bottomRight());

    return QRect(topLeft.toPoint(), bottomRight.toPoint());
}
    QRect mapWidgetToCanvas(const QRect &widgetRect) const {
    QPointF topLeft = mapWidgetToCanvas(widgetRect.topLeft());
    QPointF bottomRight = mapWidgetToCanvas(widgetRect.bottomRight());

    return QRect(topLeft.toPoint(), bottomRight.toPoint());
}
    QRect mapCanvasToWidget(const QRect &canvasRect) const {
    QPointF topLeft = mapCanvasToWidget(canvasRect.topLeft());
    QPointF bottomRight = mapCanvasToWidget(canvasRect.bottomRight());

    return QRect(topLeft.toPoint(), bottomRight.toPoint());
}
  protected:
    void paintEvent(QPaintEvent *event) override {
    QPainter painter(this);

    // Save the painter state before transformations
    painter.save();

    // Calculate the scaled canvas size
    qreal scaledCanvasWidth = buffer.width() * (internalZoomFactor / 100.0);
    qreal scaledCanvasHeight = buffer.height() * (internalZoomFactor / 100.0);

    // Calculate centering offsets
    qreal centerOffsetX = 0;
    qreal centerOffsetY = 0;

    // Center horizontally if canvas is smaller than widget
    if (scaledCanvasWidth < width()) {
        centerOffsetX = (width() - scaledCanvasWidth) / 2.0;
    }

    // Center vertically if canvas is smaller than widget
    if (scaledCanvasHeight < height()) {
        centerOffsetY = (height() - scaledCanvasHeight) / 2.0;
    }

    // Apply centering offset first
    painter.translate(centerOffsetX, centerOffsetY);

    // Use internal zoom factor for smoother animation
    painter.scale(internalZoomFactor / 100.0, internalZoomFactor / 100.0);

    // Pan offset needs to be reversed because painter works in transformed coordinates
    painter.translate(-panOffsetX, -panOffsetY);

    // Set clipping rectangle to canvas bounds to prevent painting outside
    painter.setClipRect(0, 0, buffer.width(), buffer.height());

    // 🟨 Notebook-style background rendering
    if (backgroundImage.isNull()) {
        painter.save();

        // Always apply background color regardless of style
        painter.fillRect(QRectF(0, 0, buffer.width(), buffer.height()), backgroundColor);

        // Only draw grid/lines if not "None" style
        if (backgroundStyle != BackgroundStyle::None) {
        QPen linePen(QColor(100, 100, 100, 100));  // Subtle gray lines
        linePen.setWidthF(1.0);
        painter.setPen(linePen);

        qreal scaledDensity = backgroundDensity;

        if (devicePixelRatioF() > 1.0)
            scaledDensity *= devicePixelRatioF();  // Optional DPI handling

        if (backgroundStyle == BackgroundStyle::Lines || backgroundStyle == BackgroundStyle::Grid) {
            for (int y = 0; y < buffer.height(); y += scaledDensity)
                painter.drawLine(0, y, buffer.width(), y);
        }
        if (backgroundStyle == BackgroundStyle::Grid) {
            for (int x = 0; x < buffer.width(); x += scaledDensity)
            painter.drawLine(x, 0, x, buffer.height());
            }
        }

        painter.restore();
    }

    // ✅ Draw loaded image or PDF background if available
    if (!backgroundImage.isNull()) {
        painter.drawPixmap(0, 0, backgroundImage);
    }

    // ✅ Draw user's strokes from the buffer (transparent overlay)
    painter.drawPixmap(0, 0, buffer);

    // Draw straight line preview if in straight line mode and drawing
    // Skip preview for eraser tool
    if (straightLineMode && drawing && currentTool != ToolType::Eraser) {
        // Save the current state before applying the eraser mode
        painter.save();

        // Store current pressure - ensure minimum is 0.5 for consistent preview
        qreal pressure = qMax(0.5, painter.device()->devicePixelRatioF() > 1.0 ? 0.8 : 1.0);

        // Set up the pen based on tool type
        if (currentTool == ToolType::Marker) {
            qreal thickness = penThickness * 8.0;
            QColor markerColor = penColor;
            // Increase alpha for better visibility in straight line mode
            // Using a higher value (80) instead of the regular 4 to compensate for single-pass drawing
            markerColor.setAlpha(80);
            QPen pen(markerColor, thickness, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin);
            painter.setPen(pen);
        } else { // Default Pen
            // Match the exact same thickness calculation as in drawStroke
            qreal scaledThickness = penThickness * pressure;
            QPen pen(penColor, scaledThickness, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin);
            painter.setPen(pen);
        }

        // Use the same coordinate transformation logic as in drawStroke
        // to ensure the preview line appears at the exact same position
        QPointF bufferStart, bufferEnd;

        // Convert screen coordinates to buffer coordinates using the same calculations as drawStroke
        qreal scaledCanvasWidth = buffer.width() * (zoomFactor / 100.0);
        qreal scaledCanvasHeight = buffer.height() * (zoomFactor / 100.0);
        qreal centerOffsetX = (scaledCanvasWidth < width()) ? (width() - scaledCanvasWidth) / 2.0 : 0;
        qreal centerOffsetY = (scaledCanvasHeight < height()) ? (height() - scaledCanvasHeight) / 2.0 : 0;

        QPointF adjustedStart = straightLineStartPoint - QPointF(centerOffsetX, centerOffsetY);
        QPointF adjustedEnd = lastPoint - QPointF(centerOffsetX, centerOffsetY);

        bufferStart = (adjustedStart / (zoomFactor / 100.0)) + QPointF(panOffsetX, panOffsetY);
        bufferEnd = (adjustedEnd / (zoomFactor / 100.0)) + QPointF(panOffsetX, panOffsetY);

        // Draw the preview line using the same coordinates that will be used for the final line
        painter.drawLine(bufferStart, bufferEnd);

        // Restore the original painter state
        painter.restore();
    }

    // Draw selection rectangle if in rope tool mode and selecting, moving, or has a selection
    if (ropeToolMode && (selectingWithRope || movingSelection || (!selectionBuffer.isNull() && !selectionRect.isEmpty()))) {
        painter.save(); // Save painter state for overlays
        painter.resetTransform(); // Reset transform to draw directly in logical widget coordinates

        if (selectingWithRope && !lassoPathPoints.isEmpty()) {
            QPen selectionPen(Qt::DashLine);
            selectionPen.setColor(Qt::blue);
            selectionPen.setWidthF(1.5); // Width in logical pixels
            painter.setPen(selectionPen);
            painter.drawPolygon(lassoPathPoints); // lassoPathPoints are logical widget coordinates
        } else if (!selectionBuffer.isNull() && !selectionRect.isEmpty()) {
            // selectionRect is in logical widget coordinates.
            // selectionBuffer is in buffer coordinates, we need to handle scaling correctly
            QPixmap scaledBuffer = selectionBuffer;

            // Calculate the current zoom factor
            qreal currentZoom = internalZoomFactor / 100.0;

            // Scale the selection buffer to match the current zoom level
            if (currentZoom != 1.0) {
                QSize scaledSize = QSize(
                    qRound(scaledBuffer.width() * currentZoom),
                    qRound(scaledBuffer.height() * currentZoom)
                );
                scaledBuffer = scaledBuffer.scaled(scaledSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
            }

            // Draw it at the logical position
            // Use exactSelectionRectF for smoother movement if available
            QPointF topLeft = exactSelectionRectF.isEmpty() ? selectionRect.topLeft() : exactSelectionRectF.topLeft();
            painter.drawPixmap(topLeft, scaledBuffer);

            QPen selectionBorderPen(Qt::DashLine);
            selectionBorderPen.setColor(Qt::darkCyan);
            selectionBorderPen.setWidthF(1.5); // Width in logical pixels
            painter.setPen(selectionBorderPen);

            // Use exactSelectionRectF for drawing the selection border if available
            if (!exactSelectionRectF.isEmpty()) {
                painter.drawRect(exactSelectionRectF);
            } else {
                painter.drawRect(selectionRect);
            }
        }
        painter.restore(); // Restore painter state to what it was for drawing the main buffer
    }

    // Restore the painter state
    painter.restore();

    // Fill the area outside the canvas with the widget's background color
    QRect widgetRect = rect();
    QRectF canvasRect(
        centerOffsetX - panOffsetX * (internalZoomFactor / 100.0),
        centerOffsetY - panOffsetY * (internalZoomFactor / 100.0),
        buffer.width() * (internalZoomFactor / 100.0),
        buffer.height() * (internalZoomFactor / 100.0)
    );

    // Create regions for areas outside the canvas
    QRegion outsideRegion(widgetRect);
    outsideRegion -= QRegion(canvasRect.toRect());

    // Fill the outside region with the background color
    painter.setClipRegion(outsideRegion);
    painter.fillRect(widgetRect, palette().window().color());

    // Reset clipping for overlay elements that should appear on top
    painter.setClipping(false);

    // Draw markdown selection overlay
    if (markdownSelectionMode && markdownSelecting) {
        painter.save();
        QPen selectionPen(Qt::DashLine);
        selectionPen.setColor(Qt::green);
        selectionPen.setWidthF(2.0);
        painter.setPen(selectionPen);

        QBrush selectionBrush(QColor(0, 255, 0, 30)); // Semi-transparent green
        painter.setBrush(selectionBrush);

        QRect selectionRect = QRect(markdownSelectionStart, markdownSelectionEnd).normalized();
        painter.drawRect(selectionRect);
        painter.restore();
    }

    // Draw PDF text selection overlay on top of everything
    if (pdfTextSelectionEnabled && isPdfLoaded) {
        painter.save(); // Save painter state for PDF text overlay
        painter.resetTransform(); // Reset transform to draw directly in logical widget coordinates

        // Draw selection rectangle if actively selecting
        if (pdfTextSelecting) {
            QPen selectionPen(Qt::DashLine);
            selectionPen.setColor(QColor(0, 120, 215)); // Blue selection rectangle
            selectionPen.setWidthF(2.0); // Make it more visible
            painter.setPen(selectionPen);

            QBrush selectionBrush(QColor(0, 120, 215, 30)); // Semi-transparent blue fill
            painter.setBrush(selectionBrush);

            QRectF selectionRect(pdfSelectionStart, pdfSelectionEnd);
            selectionRect = selectionRect.normalized();
            painter.drawRect(selectionRect);
        }

        // Draw highlights for selected text boxes
        if (!selectedTextBoxes.isEmpty()) {
            QColor highlightColor = QColor(255, 255, 0, 100); // Semi-transparent yellow
            painter.setBrush(highlightColor);
            painter.setPen(Qt::NoPen);

            // Draw highlight rectangles for selected text boxes
            for (const Poppler::TextBox* textBox : selectedTextBoxes) {
                if (textBox) {
                    // Convert PDF coordinates to widget coordinates
                    QRectF pdfRect = textBox->boundingBox();
                    QPointF topLeft = mapPdfToWidgetCoordinates(pdfRect.topLeft());
                    QPointF bottomRight = mapPdfToWidgetCoordinates(pdfRect.bottomRight());
                    QRectF widgetRect(topLeft, bottomRight);
                    widgetRect = widgetRect.normalized();

                    painter.drawRect(widgetRect);
                }
            }
        }

        painter.restore(); // Restore painter state
    }
}
    void tabletEvent(QTabletEvent *event) override {
    // ✅ PRIORITY: Handle PDF text selection with stylus when enabled
    // This redirects stylus input to text selection instead of drawing
    if (pdfTextSelectionEnabled && isPdfLoaded) {
        if (event->type() == QEvent::TabletPress) {
            pdfTextSelecting = true;
            pdfSelectionStart = event->position();
            pdfSelectionEnd = pdfSelectionStart;

            // Clear any existing selected text boxes without resetting pdfTextSelecting
            selectedTextBoxes.clear();

            setCursor(Qt::IBeamCursor); // Ensure cursor is correct
            update(); // Refresh display
            event->accept();
            return;
        } else if (event->type() == QEvent::TabletMove && pdfTextSelecting) {
            pdfSelectionEnd = event->position();

            // Store pending selection for throttled processing (60 FPS throttling)
            pendingSelectionStart = pdfSelectionStart;
            pendingSelectionEnd = pdfSelectionEnd;
            hasPendingSelection = true;

            // Start timer if not already running (throttled to 60 FPS)
            if (!pdfTextSelectionTimer->isActive()) {
                pdfTextSelectionTimer->start();
            }

            // NOTE: No direct update() call here - let the timer handle updates at 60 FPS
            event->accept();
            return;
        } else if (event->type() == QEvent::TabletRelease && pdfTextSelecting) {
            pdfSelectionEnd = event->position();

            // Process any pending selection immediately on release
            if (pdfTextSelectionTimer && pdfTextSelectionTimer->isActive()) {
                pdfTextSelectionTimer->stop();
                if (hasPendingSelection) {
                    updatePdfTextSelection(pendingSelectionStart, pendingSelectionEnd);
                    hasPendingSelection = false;
                }
            } else {
                // Update selection with final position
                updatePdfTextSelection(pdfSelectionStart, pdfSelectionEnd);
            }

            pdfTextSelecting = false;

            // Check for link clicks if no text was selected
            QString selectedText = getSelectedPdfText();
            if (selectedText.isEmpty()) {
                handlePdfLinkClick(event->position());
            } else {
                // Show context menu for text selection
                QPoint globalPos = mapToGlobal(event->position().toPoint());
                showPdfTextSelectionMenu(globalPos);
            }

            event->accept();
            return;
        }
    }

    // ✅ NORMAL STYLUS BEHAVIOR: Only reached when PDF text selection is OFF
    // Hardware eraser detection
    static bool hardwareEraserActive = false;
    bool wasUsingHardwareEraser = false;

    // Track hardware eraser state
    if (event->pointerType() == QPointingDevice::PointerType::Eraser) {
        // Hardware eraser is being used
        wasUsingHardwareEraser = true;

        if (event->type() == QEvent::TabletPress) {
            // Start of eraser stroke - save current tool and switch to eraser
            hardwareEraserActive = true;
            previousTool = currentTool;
            currentTool = ToolType::Eraser;
        }
    }

    // Maintain hardware eraser state across move events
    if (hardwareEraserActive && event->type() != QEvent::TabletRelease) {
        wasUsingHardwareEraser = true;
    }

    // Determine if we're in eraser mode (either hardware eraser or tool set to eraser)
    bool isErasing = (currentTool == ToolType::Eraser);

    if (event->type() == QEvent::TabletPress) {
        drawing = true;
        lastPoint = event->posF(); // Logical widget coordinates
        if (straightLineMode) {
            straightLineStartPoint = lastPoint;
        }
        if (ropeToolMode) {
            if (!selectionBuffer.isNull() && selectionRect.contains(lastPoint.toPoint())) {
                // Start moving an existing selection (or continue if already moving)
                movingSelection = true;
                selectingWithRope = false;
                lastMovePoint = lastPoint;
                // Initialize the exact floating-point rect if it's empty
                if (exactSelectionRectF.isEmpty()) {
                    exactSelectionRectF = QRectF(selectionRect);
                }

                // If this selection was just copied, clear the area now that user is moving it
                if (selectionJustCopied) {
                    QPainter painter(&buffer);
                    painter.setCompositionMode(QPainter::CompositionMode_Clear);
                    QPointF bufferDest = mapLogicalWidgetToPhysicalBuffer(selectionRect.topLeft());
                    QRect clearRect(bufferDest.toPoint(), selectionBuffer.size());
                    // Only clear the part that's within buffer bounds
                    QRect bufferBounds(0, 0, buffer.width(), buffer.height());
                    QRect clippedClearRect = clearRect.intersected(bufferBounds);
                    if (!clippedClearRect.isEmpty()) {
                        painter.fillRect(clippedClearRect, Qt::transparent);
                    }
                    painter.end();
                    selectionJustCopied = false; // Clear the flag
                }

                // If the selection area hasn't been cleared from the buffer yet, clear it now
                if (!selectionAreaCleared && !selectionMaskPath.isEmpty()) {
                    QPainter painter(&buffer);
                    painter.setCompositionMode(QPainter::CompositionMode_Clear);
                    painter.fillPath(selectionMaskPath, Qt::transparent);
                    painter.end();
                    selectionAreaCleared = true;
                }
                // selectionBuffer already has the content.
                // The original area in 'buffer' was already cleared when selection was made.
            } else {
                // Start a new selection or cancel existing one
                if (!selectionBuffer.isNull()) { // If there's an active selection, a tap outside cancels it
                    selectionBuffer = QPixmap();
                    selectionRect = QRect();
                    lassoPathPoints.clear();
                    movingSelection = false;
                    selectingWithRope = false;
                    selectionJustCopied = false;
                    selectionAreaCleared = false;
                    selectionMaskPath = QPainterPath();
                    selectionBufferRect = QRectF();
                    update(); // Full update to remove old selection visuals
                    drawing = false; // Consumed this press for cancel
                    return;
                }
                selectingWithRope = true;
                movingSelection = false;
                selectionJustCopied = false;
                selectionAreaCleared = false;
                selectionMaskPath = QPainterPath();
                selectionBufferRect = QRectF();
                lassoPathPoints.clear();
                lassoPathPoints << lastPoint; // Start the lasso path
                selectionRect = QRect();
                selectionBuffer = QPixmap();
            }
        }
    } else if (event->type() == QEvent::TabletMove && drawing) {
        if (ropeToolMode) {
            if (selectingWithRope) {
                QRectF oldPreviewBoundingRect = lassoPathPoints.boundingRect();
                lassoPathPoints << event->posF();
                lastPoint = event->posF();
                QRectF newPreviewBoundingRect = lassoPathPoints.boundingRect();
                // Update the area of the selection rectangle preview (logical widget coordinates)
                update(oldPreviewBoundingRect.united(newPreviewBoundingRect).toRect().adjusted(-5,-5,5,5));
            } else if (movingSelection) {
                QPointF delta = event->posF() - lastMovePoint; // Delta in logical widget coordinates
                QRect oldWidgetSelectionRect = selectionRect;

                // Update the exact floating-point rectangle first
                exactSelectionRectF.translate(delta);

                // Convert back to integer rect, but only when the position actually changes
                QRect newRect = exactSelectionRectF.toRect();
                if (newRect != selectionRect) {
                    selectionRect = newRect;
                    // Update the combined area of the old and new selection positions (logical widget coordinates)
                    update(oldWidgetSelectionRect.united(selectionRect).adjusted(-2,-2,2,2));
                } else {
                    // Even if the integer position didn't change, we still need to update
                    // to make movement feel smoother, especially with slow movements
                    update(selectionRect.adjusted(-2,-2,2,2));
                }

                lastMovePoint = event->posF();
                if (!edited){
                    edited = true;
                    // Invalidate cache for current page since it's been modified
                    invalidateCurrentPageCache();
                }
            }
        } else if (straightLineMode && !isErasing) {
            // For straight line mode with non-eraser tools, just update the last position
            // and trigger a repaint of only the affected area for preview
            static QElapsedTimer updateTimer;
            static bool timerInitialized = false;

            if (!timerInitialized) {
                updateTimer.start();
                timerInitialized = true;
            }

            // Throttle updates based on time for high CPU usage tools
            bool shouldUpdate = true;

            // Apply throttling for marker which can be CPU intensive
            if (currentTool == ToolType::Marker) {
                shouldUpdate = updateTimer.elapsed() > 16; // Only update every 16ms (approx 60fps)
            }

            if (shouldUpdate) {
                QPointF oldLastPoint = lastPoint;
                lastPoint = event->posF();

                // Calculate affected rectangle that needs updating
                QRectF updateRect = calculatePreviewRect(straightLineStartPoint, oldLastPoint, lastPoint);
                update(updateRect.toRect());

                // Reset timer
                updateTimer.restart();
            } else {
                // Just update the last point without redrawing
                lastPoint = event->posF();
            }
        } else if (straightLineMode && isErasing) {
            // For eraser in straight line mode, continuously erase from start to current point
            // This gives immediate visual feedback and smoother erasing experience

            // Store current point
            QPointF currentPoint = event->posF();

            // Clear previous stroke by redrawing with transparency
            QRectF updateRect = QRectF(straightLineStartPoint, lastPoint).normalized().adjusted(-20, -20, 20, 20);
            update(updateRect.toRect());

            // Erase from start point to current position
            eraseStroke(straightLineStartPoint, currentPoint, event->pressure());

            // Update last point
            lastPoint = currentPoint;

            // Only track benchmarking when enabled
            if (benchmarking) {
                processedTimestamps.push_back(benchmarkTimer.elapsed());
            }
        } else {
            // Normal drawing mode OR eraser regardless of straight line mode
            if (isErasing) {
            eraseStroke(lastPoint, event->posF(), event->pressure());
        } else {
            drawStroke(lastPoint, event->posF(), event->pressure());
        }
        lastPoint = event->posF();

            // Only track benchmarking when enabled
            if (benchmarking) {
        processedTimestamps.push_back(benchmarkTimer.elapsed());
            }
        }
    } else if (event->type() == QEvent::TabletRelease) {
        if (straightLineMode && !isErasing) {
            // Draw the final line on release with the current pressure
            qreal pressure = event->pressure();

            // Always use at least a minimum pressure
            pressure = qMax(pressure, 0.5);

            drawStroke(straightLineStartPoint, event->posF(), pressure);

            // Only track benchmarking when enabled
            if (benchmarking) {
                processedTimestamps.push_back(benchmarkTimer.elapsed());
            }

            // Force repaint to clear the preview line
            update();
            if (!edited){
                edited = true;
                // Invalidate cache for current page since it's been modified
                invalidateCurrentPageCache();
            }
        } else if (straightLineMode && isErasing) {
            // For erasing in straight line mode, most of the work is done during movement
            // Just ensure one final erasing pass from start to end point
            qreal pressure = qMax(event->pressure(), 0.5);

            // Final pass to ensure the entire line is erased
            eraseStroke(straightLineStartPoint, event->posF(), pressure);

            // Force update to clear any remaining artifacts
            update();
            if (!edited){
                edited = true;
                // Invalidate cache for current page since it's been modified
                invalidateCurrentPageCache();
            }
        }

        drawing = false;

        // Reset tool state if we were using the hardware eraser
        if (wasUsingHardwareEraser) {
            currentTool = previousTool;
            hardwareEraserActive = false;  // Reset hardware eraser tracking
        }

        if (ropeToolMode) {
            if (selectingWithRope) {
                if (lassoPathPoints.size() > 2) { // Need at least 3 points for a polygon
                    lassoPathPoints << lassoPathPoints.first(); // Close the polygon

                    if (!lassoPathPoints.boundingRect().isEmpty()) {
                        // 1. Create a QPolygonF in buffer coordinates using proper transformation
                        QPolygonF bufferLassoPath;
                        for (const QPointF& p_widget_logical : qAsConst(lassoPathPoints)) {
                            bufferLassoPath << mapLogicalWidgetToPhysicalBuffer(p_widget_logical);
                        }

                        // 2. Get the bounding box of this path on the buffer
                        QRectF bufferPathBoundingRect = bufferLassoPath.boundingRect();

                        // 3. Copy that part of the main buffer
                        QPixmap originalPiece = buffer.copy(bufferPathBoundingRect.toRect());

                        // 4. Create the selectionBuffer (same size as originalPiece) and fill transparent
                        selectionBuffer = QPixmap(originalPiece.size());
                        selectionBuffer.fill(Qt::transparent);

                        // 5. Create a mask from the lasso path
                        QPainterPath maskPath;
                        // The lasso path for the mask needs to be relative to originalPiece.topLeft()
                        maskPath.addPolygon(bufferLassoPath.translated(-bufferPathBoundingRect.topLeft()));

                        // 6. Paint the originalPiece onto selectionBuffer, using the mask
                        QPainter selectionPainter(&selectionBuffer);
                        selectionPainter.setClipPath(maskPath);
                        selectionPainter.drawPixmap(0,0, originalPiece);
                        selectionPainter.end();

                        // 7. DON'T clear the selected area from the main buffer yet
                        // The area will only be cleared when the user actually starts moving the selection
                        // This way, if the user cancels without moving, the original content remains
                        selectionAreaCleared = false;

                        // Store the mask path and buffer rect for later clearing when movement starts
                        selectionMaskPath = maskPath.translated(bufferPathBoundingRect.topLeft());
                        selectionBufferRect = bufferPathBoundingRect;

                        // 8. Calculate the correct selectionRect in logical widget coordinates
                        QRectF logicalSelectionRect = mapRectBufferToWidgetLogical(bufferPathBoundingRect);
                        selectionRect = logicalSelectionRect.toRect();
                        exactSelectionRectF = logicalSelectionRect;

                        // Update the area of the selection on screen
                        update(logicalSelectionRect.adjusted(-2,-2,2,2).toRect());

                        // Emit signal for context menu at the center of the selection with a delay
                        // This allows the user to immediately start moving the selection if desired
                        QPoint menuPosition = selectionRect.center();
                        QTimer::singleShot(500, this, [this, menuPosition]() {
                            // Only show menu if selection still exists and hasn't been moved
                            if (!selectionBuffer.isNull() && !selectionRect.isEmpty() && !movingSelection) {
                                emit ropeSelectionCompleted(menuPosition);
                            }
                        });
                    }
                }
                lassoPathPoints.clear(); // Ready for next selection, or move
                selectingWithRope = false;
                // Now, if the user presses inside selectionRect, movingSelection will become true.
            } else if (movingSelection) {
                if (!selectionBuffer.isNull() && !selectionRect.isEmpty()) {
                    QPainter painter(&buffer);
                    // Explicitly set composition mode to draw on top of existing content
                    painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
                    // Use exact floating-point position if available for more precise placement
                    QPointF topLeft = exactSelectionRectF.isEmpty() ? selectionRect.topLeft() : exactSelectionRectF.topLeft();
                    // Use proper coordinate transformation to get buffer coordinates
                    QPointF bufferDest = mapLogicalWidgetToPhysicalBuffer(topLeft);
                    painter.drawPixmap(bufferDest.toPoint(), selectionBuffer);
                    painter.end();

                    // Update the pasted area
                    QRectF bufferPasteRect(bufferDest, selectionBuffer.size());
                    update(mapRectBufferToWidgetLogical(bufferPasteRect).adjusted(-2,-2,2,2));

                    // Clear selection after pasting, making it permanent
                    selectionBuffer = QPixmap();
                    selectionRect = QRect();
                    exactSelectionRectF = QRectF();
                    movingSelection = false;
                    selectionJustCopied = false;
                    selectionAreaCleared = false;
                    selectionMaskPath = QPainterPath();
                    selectionBufferRect = QRectF();
                }
            }
        }

    }
    event->accept();
}
    void mousePressEvent(QMouseEvent *event) override {
    // Handle markdown selection when enabled
    if (markdownSelectionMode && event->button() == Qt::LeftButton) {
        markdownSelecting = true;
        markdownSelectionStart = event->pos();
        markdownSelectionEnd = markdownSelectionStart;
        event->accept();
        return;
    }

    // Handle PDF text selection when enabled (mouse/touch fallback - stylus handled in tabletEvent)
    if (pdfTextSelectionEnabled && isPdfLoaded) {
        if (event->button() == Qt::LeftButton) {
            pdfTextSelecting = true;
            pdfSelectionStart = event->position();
            pdfSelectionEnd = pdfSelectionStart;

            // Clear any existing selected text boxes without resetting pdfTextSelecting
            selectedTextBoxes.clear();

            setCursor(Qt::IBeamCursor); // Ensure cursor is correct
            update(); // Refresh display
            event->accept();
            return;
        }
    }

    // Ignore mouse/touch input on canvas for drawing (drawing only works with tablet/stylus)
    event->ignore();
}
    void mouseMoveEvent(QMouseEvent *event) override {
    // Handle markdown selection when enabled
    if (markdownSelectionMode && markdownSelecting) {
        markdownSelectionEnd = event->pos();
        update(); // Refresh to show selection rectangle
        event->accept();
        return;
    }

    // Handle PDF text selection when enabled (mouse/touch fallback - stylus handled in tabletEvent)
    if (pdfTextSelectionEnabled && isPdfLoaded && pdfTextSelecting) {
        pdfSelectionEnd = event->position();

        // Store pending selection for throttled processing
        pendingSelectionStart = pdfSelectionStart;
        pendingSelectionEnd = pdfSelectionEnd;
        hasPendingSelection = true;

        // Start timer if not already running (throttled to 60 FPS)
        if (!pdfTextSelectionTimer->isActive()) {
            pdfTextSelectionTimer->start();
        }

        event->accept();
        return;
    }

    // Update cursor based on mode when not actively selecting
    if (pdfTextSelectionEnabled && isPdfLoaded && !pdfTextSelecting) {
        setCursor(Qt::IBeamCursor);
    }

    event->ignore();
}
    void mouseReleaseEvent(QMouseEvent *event) override {
    // Handle markdown selection when enabled
    if (markdownSelectionMode && markdownSelecting && event->button() == Qt::LeftButton) {
        markdownSelecting = false;

        // Create markdown window if selection is valid
        QRect selectionRect = QRect(markdownSelectionStart, markdownSelectionEnd).normalized();
        if (selectionRect.width() > 50 && selectionRect.height() > 50 && markdownManager) {
            markdownManager->createMarkdownWindow(selectionRect);
        }

        // Exit selection mode
        setMarkdownSelectionMode(false);

        // Force screen update to clear the green selection overlay
        update();

        event->accept();
        return;
    }

    // Handle PDF text selection when enabled (mouse/touch fallback - stylus handled in tabletEvent)
    if (pdfTextSelectionEnabled && isPdfLoaded && pdfTextSelecting) {
        if (event->button() == Qt::LeftButton) {
            pdfSelectionEnd = event->position();

            // Process any pending selection immediately on release
            if (pdfTextSelectionTimer && pdfTextSelectionTimer->isActive()) {
                pdfTextSelectionTimer->stop();
                if (hasPendingSelection) {
                    updatePdfTextSelection(pendingSelectionStart, pendingSelectionEnd);
                    hasPendingSelection = false;
                }
            } else {
                // Update selection with final position
                updatePdfTextSelection(pdfSelectionStart, pdfSelectionEnd);
            }

            pdfTextSelecting = false;

            // Check for link clicks if no text was selected
            QString selectedText = getSelectedPdfText();
            if (selectedText.isEmpty()) {
                handlePdfLinkClick(event->position());
            } else {
                // Show context menu for text selection
                QPoint globalPos = mapToGlobal(event->position().toPoint());
                showPdfTextSelectionMenu(globalPos);
            }

            event->accept();
            return;
        }
    }

    event->ignore();
}
    void resizeEvent(QResizeEvent *event) override {
    // Don't resize the buffer when the widget resizes
    // The buffer size should be determined by the PDF/document content, not the widget size
    // The paintEvent will handle centering the buffer content within the widget

    QWidget::resizeEvent(event);
}
    bool event(QEvent *event) override {
    if (!touchGesturesEnabled) {
        return QWidget::event(event);
    }

    if (event->type() == QEvent::TouchBegin ||
        event->type() == QEvent::TouchUpdate ||
        event->type() == QEvent::TouchEnd) {

        QTouchEvent *touchEvent = static_cast<QTouchEvent*>(event);
        const QList<QTouchEvent::TouchPoint> touchPoints = touchEvent->points();

        activeTouchPoints = touchPoints.count();

        if (activeTouchPoints == 1) {
            // Single finger pan
            const QTouchEvent::TouchPoint &touchPoint = touchPoints.first();

            if (event->type() == QEvent::TouchBegin) {
                isPanning = true;
                lastTouchPos = touchPoint.position();
            } else if (event->type() == QEvent::TouchUpdate && isPanning) {
                QPointF delta = touchPoint.position() - lastTouchPos;

                // Make panning more responsive by using floating-point calculations
                qreal scaledDeltaX = delta.x() / (internalZoomFactor / 100.0);
                qreal scaledDeltaY = delta.y() / (internalZoomFactor / 100.0);

                // Calculate new pan positions with sub-pixel precision
                qreal newPanX = panOffsetX - scaledDeltaX;
                qreal newPanY = panOffsetY - scaledDeltaY;

                // Clamp pan values when canvas is smaller than viewport
                qreal scaledCanvasWidth = buffer.width() * (internalZoomFactor / 100.0);
                qreal scaledCanvasHeight = buffer.height() * (internalZoomFactor / 100.0);

                // If canvas is smaller than widget, lock pan to 0 (centered)
                if (scaledCanvasWidth < width()) {
                    newPanX = 0;
                }
                if (scaledCanvasHeight < height()) {
                    newPanY = 0;
                }

                // Emit signal with integer values for compatibility
                emit panChanged(qRound(newPanX), qRound(newPanY));

                lastTouchPos = touchPoint.position();
            }
        } else if (activeTouchPoints == 2) {
            // Two finger pinch zoom
            isPanning = false;

            const QTouchEvent::TouchPoint &touch1 = touchPoints[0];
            const QTouchEvent::TouchPoint &touch2 = touchPoints[1];

            // Calculate distance between touch points with higher precision
            qreal currentDist = QLineF(touch1.position(), touch2.position()).length();
            qreal startDist = QLineF(touch1.pressPosition(), touch2.pressPosition()).length();

            if (event->type() == QEvent::TouchBegin) {
                lastPinchScale = 1.0;
                // Store the starting internal zoom
                internalZoomFactor = zoomFactor;
            } else if (event->type() == QEvent::TouchUpdate && startDist > 0) {
                qreal scale = currentDist / startDist;

                // Use exponential scaling for more natural feel
                qreal scaleChange = scale / lastPinchScale;

                // Apply scale with higher sensitivity
                internalZoomFactor *= scaleChange;
                internalZoomFactor = qBound(10.0, internalZoomFactor, 400.0);

                // Calculate zoom center (midpoint between two fingers)
                QPointF center = (touch1.position() + touch2.position()) / 2.0;

                // Account for centering offset
                qreal scaledCanvasWidth = buffer.width() * (zoomFactor / 100.0);
                qreal scaledCanvasHeight = buffer.height() * (zoomFactor / 100.0);
                qreal centerOffsetX = (scaledCanvasWidth < width()) ? (width() - scaledCanvasWidth) / 2.0 : 0;
                qreal centerOffsetY = (scaledCanvasHeight < height()) ? (height() - scaledCanvasHeight) / 2.0 : 0;

                // Adjust center point for centering offset
                QPointF adjustedCenter = center - QPointF(centerOffsetX, centerOffsetY);

                // Always update zoom for smooth animation
                int newZoom = qRound(internalZoomFactor);

                // Calculate pan adjustment to keep the zoom centered at pinch point
                QPointF bufferCenter = adjustedCenter / (zoomFactor / 100.0) + QPointF(panOffsetX, panOffsetY);

                // Update zoom factor before emitting
                qreal oldZoomFactor = zoomFactor;
                zoomFactor = newZoom;

                // Emit zoom change even for small changes
                emit zoomChanged(newZoom);

                // Adjust pan to keep center point fixed with sub-pixel precision
                qreal newPanX = bufferCenter.x() - adjustedCenter.x() / (internalZoomFactor / 100.0);
                qreal newPanY = bufferCenter.y() - adjustedCenter.y() / (internalZoomFactor / 100.0);

                // After zoom, check if we need to center
                qreal newScaledCanvasWidth = buffer.width() * (internalZoomFactor / 100.0);
                qreal newScaledCanvasHeight = buffer.height() * (internalZoomFactor / 100.0);

                if (newScaledCanvasWidth < width()) {
                    newPanX = 0;
                }
                if (newScaledCanvasHeight < height()) {
                    newPanY = 0;
                }

                emit panChanged(qRound(newPanX), qRound(newPanY));

                lastPinchScale = scale;

                // Request update only for visible area
                update();
            }
        } else {
            // More than 2 fingers - ignore
            isPanning = false;
        }

        if (event->type() == QEvent::TouchEnd) {
            isPanning = false;
            lastPinchScale = 1.0;
            activeTouchPoints = 0;
            // Sync internal zoom with actual zoom
            internalZoomFactor = zoomFactor;

            // Emit signal that touch gesture has ended
            emit touchGestureEnded();
        }

        event->accept();
        return true;
    }

    return QWidget::event(event);
}
  private:
    QPointF mapLogicalWidgetToPhysicalBuffer(const QPointF& logicalWidgetPoint) {
    // Use the same coordinate transformation logic as drawStroke for consistency
    qreal scaledCanvasWidth = buffer.width() * (zoomFactor / 100.0);
    qreal scaledCanvasHeight = buffer.height() * (zoomFactor / 100.0);
    qreal centerOffsetX = (scaledCanvasWidth < width()) ? (width() - scaledCanvasWidth) / 2.0 : 0;
    qreal centerOffsetY = (scaledCanvasHeight < height()) ? (height() - scaledCanvasHeight) / 2.0 : 0;

    // Convert widget position to buffer position, accounting for centering
    QPointF adjustedPoint = logicalWidgetPoint - QPointF(centerOffsetX, centerOffsetY);
    QPointF bufferPoint = (adjustedPoint / (zoomFactor / 100.0)) + QPointF(panOffsetX, panOffsetY);

    return bufferPoint;
}
    QRect mapRectBufferToWidgetLogical(const QRectF& physicalBufferRect) {
    // Use the same coordinate transformation logic as drawStroke for consistency
    qreal scaledCanvasWidth = buffer.width() * (zoomFactor / 100.0);
    qreal scaledCanvasHeight = buffer.height() * (zoomFactor / 100.0);
    qreal centerOffsetX = (scaledCanvasWidth < width()) ? (width() - scaledCanvasWidth) / 2.0 : 0;
    qreal centerOffsetY = (scaledCanvasHeight < height()) ? (height() - scaledCanvasHeight) / 2.0 : 0;

    // Convert buffer coordinates back to widget coordinates
    QRectF widgetRect = QRectF(
        (physicalBufferRect.topLeft() - QPointF(panOffsetX, panOffsetY)) * (zoomFactor / 100.0) + QPointF(centerOffsetX, centerOffsetY),
        physicalBufferRect.size() * (zoomFactor / 100.0)
    );

    return widgetRect.toRect();
}
    QPixmap buffer;
    QImage background;
    QPointF lastPoint;
    QPointF straightLineStartPoint;
    bool drawing;
    QColor penColor;
    qreal penThickness;
    ToolType currentTool;
    ToolType previousTool;
    qreal penToolThickness = 5.0;
    qreal markerToolThickness = 5.0;
    qreal eraserToolThickness = 5.0;
    QString saveFolder;
    QPixmap backgroundImage;
    bool straightLineMode = false;
    bool ropeToolMode = false;
    QPixmap selectionBuffer;
    QRect selectionRect;
    QRectF exactSelectionRectF;
    QPolygonF lassoPathPoints;
    bool selectingWithRope = false;
    bool movingSelection = false;
    bool selectionJustCopied = false;
    bool selectionAreaCleared = false;
    QPainterPath selectionMaskPath;
    QRectF selectionBufferRect;
    QPointF lastMovePoint;
    int zoomFactor;
    int panOffsetX;
    int panOffsetY;
    qreal internalZoomFactor = 100.0;
    void initializeBuffer() {
    QScreen *screen = QGuiApplication::primaryScreen();
    qreal dpr = screen ? screen->devicePixelRatio() : 1.0;

    // Get logical screen size
    QSize logicalSize = screen ? screen->size() : QSize(1440, 900);
    QSize pixelSize = logicalSize * dpr;

    buffer = QPixmap(pixelSize);
    buffer.fill(Qt::transparent);

    setMaximumSize(pixelSize); // 🔥 KEY LINE to make full canvas drawable
}
    void drawStroke(const QPointF &start, const QPointF &end, qreal pressure) {
    if (buffer.isNull()) {
        initializeBuffer();
    }

    if (!edited){
        edited = true;
        // Invalidate cache for current page since it's been modified
        invalidateCurrentPageCache();
    }

    QPainter painter(&buffer);
    painter.setRenderHint(QPainter::Antialiasing);

    qreal thickness = penThickness;

    qreal updatePadding = (currentTool == ToolType::Marker) ? thickness * 4.0 : 10;

    if (currentTool == ToolType::Marker) {
        thickness *= 8.0;
        QColor markerColor = penColor;
        // Adjust alpha based on whether we're in straight line mode
        if (straightLineMode) {
            // For straight line mode, use higher alpha to make it more visible
            markerColor.setAlpha(40);
        } else {
            // For regular drawing, use lower alpha for the usual marker effect
            markerColor.setAlpha(4);
        }
        QPen pen(markerColor, thickness, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin);
        painter.setPen(pen);
    } else { // Default Pen
        qreal scaledThickness = thickness * pressure;  // **Linear pressure scaling**
        QPen pen(penColor, scaledThickness, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin);
        painter.setPen(pen);
    }

    // Calculate centering offsets
    qreal scaledCanvasWidth = buffer.width() * (zoomFactor / 100.0);
    qreal scaledCanvasHeight = buffer.height() * (zoomFactor / 100.0);
    qreal centerOffsetX = (scaledCanvasWidth < width()) ? (width() - scaledCanvasWidth) / 2.0 : 0;
    qreal centerOffsetY = (scaledCanvasHeight < height()) ? (height() - scaledCanvasHeight) / 2.0 : 0;

    // Convert screen position to buffer position, accounting for centering
    QPointF adjustedStart = start - QPointF(centerOffsetX, centerOffsetY);
    QPointF adjustedEnd = end - QPointF(centerOffsetX, centerOffsetY);

    QPointF bufferStart = (adjustedStart / (zoomFactor / 100.0)) + QPointF(panOffsetX, panOffsetY);
    QPointF bufferEnd = (adjustedEnd / (zoomFactor / 100.0)) + QPointF(panOffsetX, panOffsetY);

    painter.drawLine(bufferStart, bufferEnd);

    QRectF updateRect = QRectF(bufferStart, bufferEnd)
                        .normalized()
                        .adjusted(-updatePadding, -updatePadding, updatePadding, updatePadding);

    QRect scaledUpdateRect = QRect(
        ((updateRect.topLeft() - QPointF(panOffsetX, panOffsetY)) * (zoomFactor / 100.0) + QPointF(centerOffsetX, centerOffsetY)).toPoint(),
        ((updateRect.bottomRight() - QPointF(panOffsetX, panOffsetY)) * (zoomFactor / 100.0) + QPointF(centerOffsetX, centerOffsetY)).toPoint()
    );
    update(scaledUpdateRect);
}
    void eraseStroke(const QPointF &start, const QPointF &end, qreal pressure) {
    if (buffer.isNull()) {
        initializeBuffer();
    }

    if (!edited){
        edited = true;
        // Invalidate cache for current page since it's been modified
        invalidateCurrentPageCache();
    }

    QPainter painter(&buffer);
    painter.setCompositionMode(QPainter::CompositionMode_Clear);

    qreal eraserThickness = penThickness * 6.0;
    QPen eraserPen(Qt::transparent, eraserThickness, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin);
    painter.setPen(eraserPen);

    // Calculate centering offsets
    qreal scaledCanvasWidth = buffer.width() * (zoomFactor / 100.0);
    qreal scaledCanvasHeight = buffer.height() * (zoomFactor / 100.0);
    qreal centerOffsetX = (scaledCanvasWidth < width()) ? (width() - scaledCanvasWidth) / 2.0 : 0;
    qreal centerOffsetY = (scaledCanvasHeight < height()) ? (height() - scaledCanvasHeight) / 2.0 : 0;

    // Convert screen position to buffer position, accounting for centering
    QPointF adjustedStart = start - QPointF(centerOffsetX, centerOffsetY);
    QPointF adjustedEnd = end - QPointF(centerOffsetX, centerOffsetY);

    QPointF bufferStart = (adjustedStart / (zoomFactor / 100.0)) + QPointF(panOffsetX, panOffsetY);
    QPointF bufferEnd = (adjustedEnd / (zoomFactor / 100.0)) + QPointF(panOffsetX, panOffsetY);

    painter.drawLine(bufferStart, bufferEnd);

    qreal updatePadding = eraserThickness / 2.0 + 5.0; // Half the eraser thickness plus some extra padding
    QRectF updateRect = QRectF(bufferStart, bufferEnd)
                        .normalized()
                        .adjusted(-updatePadding, -updatePadding, updatePadding, updatePadding);

    QRect scaledUpdateRect = QRect(
        ((updateRect.topLeft() - QPointF(panOffsetX, panOffsetY)) * (zoomFactor / 100.0) + QPointF(centerOffsetX, centerOffsetY)).toPoint(),
        ((updateRect.bottomRight() - QPointF(panOffsetX, panOffsetY)) * (zoomFactor / 100.0) + QPointF(centerOffsetX, centerOffsetY)).toPoint()
    );
    update(scaledUpdateRect);
}
    QRectF calculatePreviewRect(const QPointF &start, const QPointF &oldEnd, const QPointF &newEnd) {
    // Calculate centering offsets - use the same calculation as in paintEvent
    qreal scaledCanvasWidth = buffer.width() * (zoomFactor / 100.0);
    qreal scaledCanvasHeight = buffer.height() * (zoomFactor / 100.0);
    qreal centerOffsetX = (scaledCanvasWidth < width()) ? (width() - scaledCanvasWidth) / 2.0 : 0;
    qreal centerOffsetY = (scaledCanvasHeight < height()) ? (height() - scaledCanvasHeight) / 2.0 : 0;

    // Calculate the buffer coordinates for our points (same as in drawStroke)
    QPointF adjustedStart = start - QPointF(centerOffsetX, centerOffsetY);
    QPointF adjustedOldEnd = oldEnd - QPointF(centerOffsetX, centerOffsetY);
    QPointF adjustedNewEnd = newEnd - QPointF(centerOffsetX, centerOffsetY);

    QPointF bufferStart = (adjustedStart / (zoomFactor / 100.0)) + QPointF(panOffsetX, panOffsetY);
    QPointF bufferOldEnd = (adjustedOldEnd / (zoomFactor / 100.0)) + QPointF(panOffsetX, panOffsetY);
    QPointF bufferNewEnd = (adjustedNewEnd / (zoomFactor / 100.0)) + QPointF(panOffsetX, panOffsetY);

    // Now convert from buffer coordinates to screen coordinates
    QPointF screenStart = (bufferStart - QPointF(panOffsetX, panOffsetY)) * (zoomFactor / 100.0) + QPointF(centerOffsetX, centerOffsetY);
    QPointF screenOldEnd = (bufferOldEnd - QPointF(panOffsetX, panOffsetY)) * (zoomFactor / 100.0) + QPointF(centerOffsetX, centerOffsetY);
    QPointF screenNewEnd = (bufferNewEnd - QPointF(panOffsetX, panOffsetY)) * (zoomFactor / 100.0) + QPointF(centerOffsetX, centerOffsetY);

    // Create rectangles for both the old and new lines
    QRectF oldLineRect = QRectF(screenStart, screenOldEnd).normalized();
    QRectF newLineRect = QRectF(screenStart, screenNewEnd).normalized();

    // Calculate padding based on pen thickness and device pixel ratio
    qreal dpr = devicePixelRatioF();
    qreal padding;

    if (currentTool == ToolType::Eraser) {
        padding = penThickness * 6.0 * dpr;
    } else if (currentTool == ToolType::Marker) {
        padding = penThickness * 8.0 * dpr;
    } else {
        padding = penThickness * dpr;
    }

    // Ensure minimum padding
    padding = qMax(padding, 15.0);

    // Combine rectangles with appropriate padding
    QRectF combinedRect = oldLineRect.united(newLineRect);
    return combinedRect.adjusted(-padding, -padding, padding, padding);
}
    QCache<int, QPixmap> pdfCache;
    std::unique_ptr<Poppler::Document> pdfDocument;
    int currentPdfPage;
    bool isPdfLoaded = false;
    int totalPdfPages = 0;
    int lastActivePage = 0;
    int lastZoomLevel = 100;
    int lastPanX = 0;
    int lastPanY = 0;
    bool edited = false;
    bool benchmarking;
    std::deque<qint64> processedTimestamps;
    QElapsedTimer benchmarkTimer;
    QString notebookId;
    void loadNotebookId() {
    QString idFile = saveFolder + "/.notebook_id.txt";
    if (QFile::exists(idFile)) {
        QFile file(idFile);
        if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
            QTextStream in(&file);
            notebookId = in.readLine().trimmed();
        }
    } else {
        // No ID file → create new random ID
        notebookId = QUuid::createUuid().toString(QUuid::WithoutBraces).replace("-", "");
        saveNotebookId();
    }
}
    void saveNotebookId() {
    QString idFile = saveFolder + "/.notebook_id.txt";
    QFile file(idFile);
    if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
        QTextStream out(&file);
        out << notebookId;
    }
}
    int pdfRenderDPI = 192;
    bool touchGesturesEnabled = false;
    QPointF lastTouchPos;
    qreal lastPinchScale = 1.0;
    bool isPanning = false;
    int activeTouchPoints = 0;
    BackgroundStyle backgroundStyle = BackgroundStyle::None;
    QColor backgroundColor = Qt::white;
    int backgroundDensity = 40;
    bool pdfTextSelectionEnabled = false;
    bool pdfTextSelecting = false;
    QPointF pdfSelectionStart;
    QPointF pdfSelectionEnd;
    QList<Poppler::TextBox*> currentPdfTextBoxes;
    QList<Poppler::TextBox*> selectedTextBoxes;
    std::unique_ptr<Poppler::Page> currentPdfPageForText;
    QTimer* pdfTextSelectionTimer = nullptr;
    QPointF pendingSelectionStart;
    QPointF pendingSelectionEnd;
    bool hasPendingSelection = false;
    QTimer* pdfCacheTimer = nullptr;
    int currentCachedPage = -1;
    int pendingCacheTargetPage = -1;
    QList<QFutureWatcher<void>*> activePdfWatchers;
    QCache<int, QPixmap> noteCache;
    QTimer* noteCacheTimer = nullptr;
    int currentCachedNotePage = -1;
    int pendingNoteCacheTargetPage = -1;
    QList<QFutureWatcher<void>*> activeNoteWatchers;
    MarkdownWindowManager* markdownManager = nullptr;
    bool markdownSelectionMode = false;
    QPoint markdownSelectionStart;
    QPoint markdownSelectionEnd;
    bool markdownSelecting = false;
    void loadPdfTextBoxes(int pageNumber) {
    // Clear existing text boxes
    qDeleteAll(currentPdfTextBoxes);
    currentPdfTextBoxes.clear();

    if (!pdfDocument || pageNumber < 0 || pageNumber >= pdfDocument->numPages()) {
        return;
    }

    // Get the page for text operations
    currentPdfPageForText = std::unique_ptr<Poppler::Page>(pdfDocument->page(pageNumber));
    if (!currentPdfPageForText) {
        return;
    }

    // Load text boxes for the page
    auto textBoxVector = currentPdfPageForText->textList();
    currentPdfTextBoxes.clear();
    for (auto& textBox : textBoxVector) {
        currentPdfTextBoxes.append(textBox.release()); // Transfer ownership to QList
    }
}
    QPointF mapWidgetToPdfCoordinates(const QPointF &widgetPoint) {
    if (!currentPdfPageForText || backgroundImage.isNull()) {
        return QPointF();
    }

    // Use the same zoom factor as in paintEvent (internalZoomFactor for smooth animations)
    qreal zoom = internalZoomFactor / 100.0;

    // Calculate the scaled canvas size (same as in paintEvent)
    qreal scaledCanvasWidth = buffer.width() * zoom;
    qreal scaledCanvasHeight = buffer.height() * zoom;

    // Calculate centering offsets (same as in paintEvent)
    qreal centerOffsetX = 0;
    qreal centerOffsetY = 0;

    // Center horizontally if canvas is smaller than widget
    if (scaledCanvasWidth < width()) {
        centerOffsetX = (width() - scaledCanvasWidth) / 2.0;
    }

    // Center vertically if canvas is smaller than widget
    if (scaledCanvasHeight < height()) {
        centerOffsetY = (height() - scaledCanvasHeight) / 2.0;
    }

    // Reverse the transformations applied in paintEvent
    QPointF adjustedPoint = widgetPoint;
    adjustedPoint -= QPointF(centerOffsetX, centerOffsetY);
    adjustedPoint /= zoom;
    adjustedPoint += QPointF(panOffsetX, panOffsetY);

    // Convert to PDF coordinates
    // Since Poppler renders the PDF in Qt coordinate system (top-left origin),
    // we don't need to flip the Y coordinate
    QSizeF pdfPageSize = currentPdfPageForText->pageSizeF();
    QSizeF imageSize = backgroundImage.size();

    // Scale from image coordinates to PDF coordinates
    qreal scaleX = pdfPageSize.width() / imageSize.width();
    qreal scaleY = pdfPageSize.height() / imageSize.height();

    QPointF pdfPoint;
    pdfPoint.setX(adjustedPoint.x() * scaleX);
    pdfPoint.setY(adjustedPoint.y() * scaleY); // No Y-axis flipping needed

    return pdfPoint;
}
    QPointF mapPdfToWidgetCoordinates(const QPointF &pdfPoint) {
    if (!currentPdfPageForText || backgroundImage.isNull()) {
        return QPointF();
    }

    // Convert from PDF coordinates to image coordinates
    QSizeF pdfPageSize = currentPdfPageForText->pageSizeF();
    QSizeF imageSize = backgroundImage.size();

    // Scale from PDF coordinates to image coordinates
    qreal scaleX = imageSize.width() / pdfPageSize.width();
    qreal scaleY = imageSize.height() / pdfPageSize.height();

    QPointF imagePoint;
    imagePoint.setX(pdfPoint.x() * scaleX);
    imagePoint.setY(pdfPoint.y() * scaleY); // No Y-axis flipping needed

    // Use the same zoom factor as in paintEvent (internalZoomFactor for smooth animations)
    qreal zoom = internalZoomFactor / 100.0;

    // Apply the same transformations as in paintEvent
    QPointF widgetPoint = imagePoint;
    widgetPoint -= QPointF(panOffsetX, panOffsetY);
    widgetPoint *= zoom;

    // Calculate the scaled canvas size (same as in paintEvent)
    qreal scaledCanvasWidth = buffer.width() * zoom;
    qreal scaledCanvasHeight = buffer.height() * zoom;

    // Calculate centering offsets (same as in paintEvent)
    qreal centerOffsetX = 0;
    qreal centerOffsetY = 0;

    // Center horizontally if canvas is smaller than widget
    if (scaledCanvasWidth < width()) {
        centerOffsetX = (width() - scaledCanvasWidth) / 2.0;
    }

    // Center vertically if canvas is smaller than widget
    if (scaledCanvasHeight < height()) {
        centerOffsetY = (height() - scaledCanvasHeight) / 2.0;
    }

    widgetPoint += QPointF(centerOffsetX, centerOffsetY);

    return widgetPoint;
}
    void updatePdfTextSelection(const QPointF &start, const QPointF &end) {
    // Early return if PDF is not loaded or no text boxes available
    if (!isPdfLoaded || currentPdfTextBoxes.isEmpty()) {
        return;
    }

    // Clear previous selection efficiently
    selectedTextBoxes.clear();

    // Create normalized selection rectangle in widget coordinates
    QRectF widgetSelectionRect(start, end);
    widgetSelectionRect = widgetSelectionRect.normalized();

    // Convert to PDF coordinate space
    QPointF pdfTopLeft = mapWidgetToPdfCoordinates(widgetSelectionRect.topLeft());
    QPointF pdfBottomRight = mapWidgetToPdfCoordinates(widgetSelectionRect.bottomRight());
    QRectF pdfSelectionRect(pdfTopLeft, pdfBottomRight);
    pdfSelectionRect = pdfSelectionRect.normalized();

    // Reserve space for efficiency if we expect many selections
    selectedTextBoxes.reserve(qMin(currentPdfTextBoxes.size(), 50));

    // Find intersecting text boxes with optimized loop
    for (const Poppler::TextBox* textBox : currentPdfTextBoxes) {
        if (textBox && textBox->boundingBox().intersects(pdfSelectionRect)) {
            selectedTextBoxes.append(const_cast<Poppler::TextBox*>(textBox));
        }
    }

    // Only emit signal and update if we have selected text
    if (!selectedTextBoxes.isEmpty()) {
        QString selectedText = getSelectedPdfText();
        if (!selectedText.isEmpty()) {
            emit pdfTextSelected(selectedText);
        }
    }

    // Trigger repaint to show selection
    update();
}
    void handlePdfLinkClick(const QPointF &clickPoint) {
    if (!isPdfLoaded || !currentPdfPageForText) {
        return;
    }

    // Convert widget coordinates to PDF coordinates
    QPointF pdfPoint = mapWidgetToPdfCoordinates(position);

    // Get PDF page size for reference
    QSizeF pdfPageSize = currentPdfPageForText->pageSizeF();

    // Convert to normalized coordinates (0.0 to 1.0) to match Poppler's link coordinate system
    QPointF normalizedPoint(pdfPoint.x() / pdfPageSize.width(), pdfPoint.y() / pdfPageSize.height());

    // Get links for the current page
    auto links = currentPdfPageForText->links();

    for (const auto& link : links) {
        QRectF linkArea = link->linkArea();

        // Normalize the rectangle to handle negative width/height
        QRectF normalizedLinkArea = linkArea.normalized();

        // Check if the normalized rectangle contains the normalized point
        if (normalizedLinkArea.contains(normalizedPoint)) {
            // Handle different types of links
            if (link->linkType() == Poppler::Link::Goto) {
                Poppler::LinkGoto* gotoLink = static_cast<Poppler::LinkGoto*>(link.get());
                if (gotoLink && gotoLink->destination().pageNumber() >= 0) {
                    int targetPage = gotoLink->destination().pageNumber() - 1; // Convert to 0-based
                    emit pdfLinkClicked(targetPage);
                    return;
                }
            }
            // Add other link types as needed (URI, etc.)
        }
    }
}
    void showPdfTextSelectionMenu(const QPoint &position) {
    QString selectedText = getSelectedPdfText();
    if (selectedText.isEmpty()) {
        return; // No text selected, don't show menu
    }

    // Create context menu
    QMenu *contextMenu = new QMenu(this);
    contextMenu->setAttribute(Qt::WA_DeleteOnClose);

    // Add Copy action
    QAction *copyAction = contextMenu->addAction(tr("Copy"));
    copyAction->setIcon(QIcon(":/resources/icons/copy.png")); // You may need to add this icon
    connect(copyAction, &QAction::triggered, this, [selectedText]() {
        QClipboard *clipboard = QGuiApplication::clipboard();
        clipboard->setText(selectedText);
    });

    // Add separator
    contextMenu->addSeparator();

    // Add Cancel action
    QAction *cancelAction = contextMenu->addAction(tr("Cancel"));
    cancelAction->setIcon(QIcon(":/resources/icons/cross.png"));
    connect(cancelAction, &QAction::triggered, this, [this]() {
        clearPdfTextSelection();
    });

    // Show the menu at the specified position
    contextMenu->popup(position);
}
    QList<Poppler::TextBox*> getTextBoxesInSelection(const QPointF &start, const QPointF &end) {
    QList<Poppler::TextBox*> selectedBoxes;

    if (!currentPdfPageForText) {
        // qDebug() << "PDF text selection: No current page for text";
        return selectedBoxes;
    }

    // Convert widget coordinates to PDF coordinates
    QPointF pdfStart = mapWidgetToPdfCoordinates(start);
    QPointF pdfEnd = mapWidgetToPdfCoordinates(end);

    // qDebug() << "PDF text selection: Widget coords" << start << "to" << end;
    // qDebug() << "PDF text selection: PDF coords" << pdfStart << "to" << pdfEnd;

    // Create selection rectangle in PDF coordinates
    QRectF selectionRect(pdfStart, pdfEnd);
    selectionRect = selectionRect.normalized();

    // qDebug() << "PDF text selection: Selection rect in PDF coords:" << selectionRect;

    // Find text boxes that intersect with the selection
    int intersectionCount = 0;
    for (Poppler::TextBox* textBox : currentPdfTextBoxes) {
        if (textBox) {
            QRectF textBoxRect = textBox->boundingBox();
            bool intersects = textBoxRect.intersects(selectionRect);

            if (intersects) {
                selectedBoxes.append(textBox);
                intersectionCount++;
                // qDebug() << "PDF text selection: Text box intersects:" << textBox->text()
                //          << "at" << textBoxRect;
            }
        }
    }

    // qDebug() << "PDF text selection: Found" << intersectionCount << "intersecting text boxes";

    return selectedBoxes;
}
    void renderPdfPageToCache(int pageNumber) {
    if (!pdfDocument || !isValidPageNumber(pageNumber)) {
        return;
    }

    // Check if already cached
    if (pdfCache.contains(pageNumber)) {
        return;
    }

    // Ensure the cache holds only 10 pages max
    if (pdfCache.count() >= 10) {
        auto oldestKey = pdfCache.keys().first();
        pdfCache.remove(oldestKey);
    }

    // Render the page and store it in the cache
    std::unique_ptr<Poppler::Page> page(pdfDocument->page(pageNumber));
    if (page) {
        // Render with document-level anti-aliasing settings
        QImage pdfImage = page->renderToImage(pdfRenderDPI, pdfRenderDPI);
        if (!pdfImage.isNull()) {
            QPixmap cachedPixmap = QPixmap::fromImage(pdfImage);
            pdfCache.insert(pageNumber, new QPixmap(cachedPixmap));
        }
    }
}
    void checkAndCacheAdjacentPages(int targetPage) {
    if (!pdfDocument || !isValidPageNumber(targetPage)) {
        return;
    }

    // Calculate adjacent pages
    int prevPage = targetPage - 1;
    int nextPage = targetPage + 1;

    // Check what needs to be cached
    bool needPrevPage = isValidPageNumber(prevPage) && !pdfCache.contains(prevPage);
    bool needCurrentPage = !pdfCache.contains(targetPage);
    bool needNextPage = isValidPageNumber(nextPage) && !pdfCache.contains(nextPage);

    // If all pages are cached, nothing to do
    if (!needPrevPage && !needCurrentPage && !needNextPage) {
        return;
    }

    // Stop any existing timer
    if (pdfCacheTimer && pdfCacheTimer->isActive()) {
        pdfCacheTimer->stop();
    }

    // Create timer if it doesn't exist
    if (!pdfCacheTimer) {
        pdfCacheTimer = new QTimer(this);
        pdfCacheTimer->setSingleShot(true);
        connect(pdfCacheTimer, &QTimer::timeout, this, &InkCanvas::cacheAdjacentPages);
    }

    // Store the target page for validation when timer fires
    pendingCacheTargetPage = targetPage;

    // Start 1-second delay timer
    pdfCacheTimer->start(1000);
}
    bool isValidPageNumber(int pageNumber) const {
    return (pageNumber >= 0 && pageNumber < totalPdfPages);
}
    void loadNotePageToCache(int pageNumber) {
    // Check if already cached
    if (noteCache.contains(pageNumber)) {
        return;
    }

    QString filePath = getNotePageFilePath(pageNumber);
    if (filePath.isEmpty()) {
        return;
    }

    // Ensure the cache doesn't exceed its limit
    if (noteCache.count() >= 15) {
        // QCache will automatically remove least recently used items
        // but we can be explicit about it
        auto keys = noteCache.keys();
        if (!keys.isEmpty()) {
            noteCache.remove(keys.first());
        }
    }

    // Load note page from disk if it exists
    if (QFile::exists(filePath)) {
        QPixmap notePixmap;
        if (notePixmap.load(filePath)) {
            noteCache.insert(pageNumber, new QPixmap(notePixmap));
        }
    }
    // If file doesn't exist, we don't cache anything - loadPage will handle initialization
}
    void checkAndCacheAdjacentNotePages(int targetPage) {
    if (saveFolder.isEmpty()) {
        return;
    }

    // Calculate adjacent pages
    int prevPage = targetPage - 1;
    int nextPage = targetPage + 1;

    // Check what needs to be cached (we don't have a max page limit for notes)
    bool needPrevPage = (prevPage >= 0) && !noteCache.contains(prevPage);
    bool needCurrentPage = !noteCache.contains(targetPage);
    bool needNextPage = !noteCache.contains(nextPage); // No upper limit check for notes

    // If all nearby pages are cached, nothing to do
    if (!needPrevPage && !needCurrentPage && !needNextPage) {
        return;
    }

    // Stop any existing timer
    if (noteCacheTimer && noteCacheTimer->isActive()) {
        noteCacheTimer->stop();
    }

    // Create timer if it doesn't exist
    if (!noteCacheTimer) {
        noteCacheTimer = new QTimer(this);
        noteCacheTimer->setSingleShot(true);
        connect(noteCacheTimer, &QTimer::timeout, this, &InkCanvas::cacheAdjacentNotePages);
    }

    // Store the target page for validation when timer fires
    pendingNoteCacheTargetPage = targetPage;

    // Start 1-second delay timer
    noteCacheTimer->start(1000);
}
    QString getNotePageFilePath(int pageNumber) const {
    if (saveFolder.isEmpty() || notebookId.isEmpty()) {
        return QString();
    }
    return saveFolder + QString("/%1_%2.png").arg(notebookId).arg(pageNumber, 5, 10, QChar('0'));
}
    void invalidateCurrentPageCache() {
    if (currentCachedNotePage >= 0) {
        noteCache.remove(currentCachedNotePage);
    }
}
  private:
    void processPendingTextSelection() {
    if (!hasPendingSelection) {
        return;
    }

    // Process the pending selection update
    updatePdfTextSelection(pendingSelectionStart, pendingSelectionEnd);
    hasPendingSelection = false;
}
    void cacheAdjacentPages() {
    if (!pdfDocument || currentCachedPage < 0) {
        return;
    }

    // Check if the user has moved to a different page since the timer was started
    // If so, skip caching adjacent pages as they're no longer relevant
    if (pendingCacheTargetPage != currentCachedPage) {
        return; // User switched pages, don't cache adjacent pages for old page
    }

    int targetPage = currentCachedPage;
    int prevPage = targetPage - 1;
    int nextPage = targetPage + 1;

    // Create list of pages to cache asynchronously
    QList<int> pagesToCache;

    // Add previous page if needed
    if (isValidPageNumber(prevPage) && !pdfCache.contains(prevPage)) {
        pagesToCache.append(prevPage);
    }

    // Add next page if needed
    if (isValidPageNumber(nextPage) && !pdfCache.contains(nextPage)) {
        pagesToCache.append(nextPage);
    }

    // Cache pages asynchronously
    for (int pageNum : pagesToCache) {
        QFutureWatcher<void> *watcher = new QFutureWatcher<void>(this);

        // Track the watcher for cleanup
        activePdfWatchers.append(watcher);

        connect(watcher, &QFutureWatcher<void>::finished, this, [this, watcher]() {
            // Remove from active list and delete
            activePdfWatchers.removeOne(watcher);
            watcher->deleteLater();
        });

        // Capture pageNum by value to avoid issues with lambda capture
        QFuture<void> future = QtConcurrent::run([this, pageNum]() {
            renderPdfPageToCache(pageNum);
        });

        watcher->setFuture(future);
    }
}
    void cacheAdjacentNotePages() {
    if (saveFolder.isEmpty() || currentCachedNotePage < 0) {
        return;
    }

    // Check if the user has moved to a different page since the timer was started
    // If so, skip caching adjacent pages as they're no longer relevant
    if (pendingNoteCacheTargetPage != currentCachedNotePage) {
        return; // User switched pages, don't cache adjacent pages for old page
    }

    int targetPage = currentCachedNotePage;
    int prevPage = targetPage - 1;
    int nextPage = targetPage + 1;

    // Create list of note pages to cache asynchronously
    QList<int> notePagesToCache;

    // Add previous page if needed (check for >= 0 since notes can start from page 0)
    if (prevPage >= 0 && !noteCache.contains(prevPage)) {
        notePagesToCache.append(prevPage);
    }

    // Add next page if needed (no upper limit check for notes)
    if (!noteCache.contains(nextPage)) {
        notePagesToCache.append(nextPage);
    }

    // Cache note pages asynchronously
    for (int pageNum : notePagesToCache) {
        QFutureWatcher<void> *watcher = new QFutureWatcher<void>(this);

        // Track the watcher for cleanup
        activeNoteWatchers.append(watcher);

        connect(watcher, &QFutureWatcher<void>::finished, this, [this, watcher]() {
            // Remove from active list and delete
            activeNoteWatchers.removeOne(watcher);
            watcher->deleteLater();
        });

        // Capture pageNum by value to avoid issues with lambda capture
        QFuture<void> future = QtConcurrent::run([this, pageNum]() {
            loadNotePageToCache(pageNum);
        });

        watcher->setFuture(future);
    }
}
};

class QMarkdownTextEdit {
    Q_OBJECT

public:
    explicit MarkdownWindow(const QRect &rect, QWidget *parent = nullptr);
    ~MarkdownWindow() = default;
    QString getMarkdownContent() const {
    return markdownEditor ? markdownEditor->toPlainText() : QString();
}
    void setMarkdownContent(const QString &content) {
    if (markdownEditor) {
        markdownEditor->setPlainText(content);
    }
}
    QRect getWindowRect() const {
    return geometry();
}
    void setWindowRect(const QRect &rect) {
    setGeometry(rect);
}
    QRect getCanvasRect() const {
    return canvasRect;
}
    void setCanvasRect(const QRect &canvasRect) {
    canvasRect = rect;
    updateScreenPosition();
}
    void updateScreenPosition() {
    // Prevent recursive updates
    if (isUpdatingPosition) return;
    isUpdatingPosition = true;

    // Debug: Log when this is called due to external changes (not during mouse movement)
    if (!dragging && !resizing) {
        // qDebug() << "MarkdownWindow::updateScreenPosition() called for window" << this << "Canvas rect:" << canvasRect;
    }

    // Get the canvas parent to access coordinate conversion methods
    if (QWidget *canvas = parentWidget()) {
        // Try to cast to InkCanvas to use the new coordinate methods
        if (InkCanvas *inkCanvas = qobject_cast<InkCanvas*>(canvas)) {
            // Use the new coordinate conversion methods
            QRect screenRect = inkCanvas->mapCanvasToWidget(canvasRect);
            // qDebug() << "MarkdownWindow::updateScreenPosition() - Canvas rect:" << canvasRect << "-> Screen rect:" << screenRect;
            // qDebug() << "  Canvas size:" << inkCanvas->getCanvasSize() << "Zoom:" << inkCanvas->getZoomFactor() << "Pan:" << inkCanvas->getPanOffset();
            setGeometry(screenRect);
        } else {
            // Fallback: use canvas coordinates directly
            // qDebug() << "MarkdownWindow::updateScreenPosition() - No InkCanvas parent, using canvas rect directly:" << canvasRect;
            setGeometry(canvasRect);
        }
    } else {
        // No parent, use canvas coordinates directly
        // qDebug() << "MarkdownWindow::updateScreenPosition() - No parent, using canvas rect directly:" << canvasRect;
        setGeometry(canvasRect);
    }

    isUpdatingPosition = false;
}
    QVariantMap serialize() const {
    QVariantMap data;
    data["canvas_x"] = canvasRect.x();
    data["canvas_y"] = canvasRect.y();
    data["canvas_width"] = canvasRect.width();
    data["canvas_height"] = canvasRect.height();
    data["content"] = getMarkdownContent();

    // Add canvas size information for debugging and consistency checks
    if (InkCanvas *inkCanvas = qobject_cast<InkCanvas*>(parentWidget())) {
        QSize canvasSize = inkCanvas->getCanvasSize();
        data["canvas_buffer_width"] = canvasSize.width();
        data["canvas_buffer_height"] = canvasSize.height();
        data["zoom_factor"] = inkCanvas->getZoomFactor();
        QPointF panOffset = inkCanvas->getPanOffset();
        data["pan_x"] = panOffset.x();
        data["pan_y"] = panOffset.y();
    }

    return data;
}
    void deserialize(const QVariantMap &data) {
    int x = data.value("canvas_x", 0).toInt();
    int y = data.value("canvas_y", 0).toInt();
    int width = data.value("canvas_width", 300).toInt();
    int height = data.value("canvas_height", 200).toInt();
    QString content = data.value("content", "# New Markdown Window").toString();

    QRect loadedRect = QRect(x, y, width, height);
    // qDebug() << "MarkdownWindow::deserialize() - Loaded canvas rect:" << loadedRect;
    // qDebug() << "  Original canvas size:" << data.value("canvas_buffer_width", -1).toInt() << "x" << data.value("canvas_buffer_height", -1).toInt();

    // Apply bounds checking to loaded coordinates
    if (InkCanvas *inkCanvas = qobject_cast<InkCanvas*>(parentWidget())) {
        QRect canvasBounds = inkCanvas->getCanvasRect();

        // Ensure window stays within canvas bounds
        int maxX = canvasBounds.width() - loadedRect.width();
        int maxY = canvasBounds.height() - loadedRect.height();

        loadedRect.setX(qMax(0, qMin(loadedRect.x(), maxX)));
        loadedRect.setY(qMax(0, qMin(loadedRect.y(), maxY)));

        if (loadedRect != QRect(x, y, width, height)) {
            // qDebug() << "  Bounds-corrected canvas rect:" << loadedRect << "Canvas bounds:" << canvasBounds;
        }
    }

    canvasRect = loadedRect;
    setMarkdownContent(content);

    // Ensure canvas connections are set up
    ensureCanvasConnections();

    updateScreenPosition();
}
    void focusEditor() {
    if (markdownEditor) {
        markdownEditor->setFocus();
    }
}
    bool isEditorFocused() const {
    return markdownEditor && markdownEditor->hasFocus();
}
    void setTransparent(bool transparent) {
    if (isTransparentState == transparent) return;

    // qDebug() << "MarkdownWindow::setTransparent(" << transparent << ") for window" << this;
    isTransparentState = transparent;

    // Apply transparency effect by changing the background style
    if (transparent) {
        // qDebug() << "Setting window background to semi-transparent";
        applyTransparentStyle();
    } else {
        // qDebug() << "Setting window background to opaque";
        applyStyle(); // Restore normal style
    }
}
    bool isTransparent() const {
    return isTransparentState;
}
    bool isValidForCanvas() const {
    if (InkCanvas *inkCanvas = qobject_cast<InkCanvas*>(parentWidget())) {
        QRect canvasBounds = inkCanvas->getCanvasRect();

        // Check if the window is at least partially within the canvas bounds
        return canvasBounds.intersects(canvasRect);
    }

    // If no canvas parent, assume valid
    return true;
}
    QString getCoordinateInfo() const {
    QString info;
    info += QString("Canvas Coordinates: (%1, %2) %3x%4\n")
            .arg(canvasRect.x()).arg(canvasRect.y())
            .arg(canvasRect.width()).arg(canvasRect.height());

    QRect screenRect = geometry();
    info += QString("Screen Coordinates: (%1, %2) %3x%4\n")
            .arg(screenRect.x()).arg(screenRect.y())
            .arg(screenRect.width()).arg(screenRect.height());

    if (InkCanvas *inkCanvas = qobject_cast<InkCanvas*>(parentWidget())) {
        QSize canvasSize = inkCanvas->getCanvasSize();
        info += QString("Canvas Size: %1x%2\n")
                .arg(canvasSize.width()).arg(canvasSize.height());

        info += QString("Zoom Factor: %1\n").arg(inkCanvas->getZoomFactor());

        QPointF panOffset = inkCanvas->getPanOffset();
        info += QString("Pan Offset: (%1, %2)\n")
                .arg(panOffset.x()).arg(panOffset.y());

        info += QString("Valid for Canvas: %1\n").arg(isValidForCanvas() ? "Yes" : "No");
    } else {
        info += "No InkCanvas parent found\n";
    }

    return info;
}
    void ensureCanvasConnections() {
    // Connect to canvas pan/zoom changes to update position
    if (InkCanvas *inkCanvas = qobject_cast<InkCanvas*>(parentWidget())) {
        // Disconnect any existing connections to avoid duplicates
        disconnect(inkCanvas, &InkCanvas::panChanged, this, &MarkdownWindow::updateScreenPosition);
        disconnect(inkCanvas, &InkCanvas::zoomChanged, this, &MarkdownWindow::updateScreenPosition);

        // Reconnect
        connect(inkCanvas, &InkCanvas::panChanged, this, &MarkdownWindow::updateScreenPosition);
        connect(inkCanvas, &InkCanvas::zoomChanged, this, &MarkdownWindow::updateScreenPosition);

        // Install event filter to handle canvas resize events (for layout changes)
        inkCanvas->installEventFilter(this);

        // qDebug() << "MarkdownWindow::ensureCanvasConnections() - Connected to canvas signals for window" << this;
    } else {
        // qDebug() << "MarkdownWindow::ensureCanvasConnections() - No InkCanvas parent found for window" << this;
    }
}
    void deleteRequested(MarkdownWindow *window);
    void contentChanged();
    void windowMoved(MarkdownWindow *window);
    void windowResized(MarkdownWindow *window);
    void focusChanged(MarkdownWindow *window, bool focused);
    void editorFocusChanged(MarkdownWindow *window, bool focused);
    void windowInteracted(MarkdownWindow *window);
  protected:
    void mousePressEvent(QMouseEvent *event) override {
    if (event->button() == Qt::LeftButton) {
        // Emit signal to indicate window interaction
        emit windowInteracted(this);

        ResizeHandle handle = getResizeHandle(event->pos());

        if (handle != None) {
            // Start resizing
            resizing = true;
            isUserInteracting = true;
            currentResizeHandle = handle;
            resizeStartPosition = event->globalPosition().toPoint();
            resizeStartRect = geometry();
        } else if (event->pos().y() < 24) { // Header area
            // Start dragging
            dragging = true;
            isUserInteracting = true;
            dragStartPosition = event->globalPosition().toPoint();
            windowStartPosition = pos();
        }
    }

    QWidget::mousePressEvent(event);
}
    void mouseMoveEvent(QMouseEvent *event) override {
    if (resizing) {
        QPoint delta = event->globalPosition().toPoint() - resizeStartPosition;
        QRect newRect = resizeStartRect;

        // Apply resize based on the stored handle
        switch (currentResizeHandle) {
            case TopLeft:
                newRect.setTopLeft(newRect.topLeft() + delta);
                break;
            case TopRight:
                newRect.setTopRight(newRect.topRight() + delta);
                break;
            case BottomLeft:
                newRect.setBottomLeft(newRect.bottomLeft() + delta);
                break;
            case BottomRight:
                newRect.setBottomRight(newRect.bottomRight() + delta);
                break;
            case Top:
                newRect.setTop(newRect.top() + delta.y());
                break;
            case Bottom:
                newRect.setBottom(newRect.bottom() + delta.y());
                break;
            case Left:
                newRect.setLeft(newRect.left() + delta.x());
                break;
            case Right:
                newRect.setRight(newRect.right() + delta.x());
                break;
            default:
                break;
        }

        // Enforce minimum size
        newRect.setSize(newRect.size().expandedTo(QSize(200, 150)));

        // Keep resized window within canvas bounds
        if (InkCanvas *inkCanvas = qobject_cast<InkCanvas*>(parentWidget())) {
            // Convert to canvas coordinates to check bounds
            QRect tempCanvasRect = inkCanvas->mapWidgetToCanvas(newRect);
            QRect canvasBounds = inkCanvas->getCanvasRect();

            // Apply canvas bounds constraints
            int maxCanvasX = canvasBounds.width() - tempCanvasRect.width();
            int maxCanvasY = canvasBounds.height() - tempCanvasRect.height();

            tempCanvasRect.setX(qMax(0, qMin(tempCanvasRect.x(), maxCanvasX)));
            tempCanvasRect.setY(qMax(0, qMin(tempCanvasRect.y(), maxCanvasY)));

            // Also ensure the window doesn't get resized beyond canvas bounds
            if (tempCanvasRect.right() > canvasBounds.width()) {
                tempCanvasRect.setWidth(canvasBounds.width() - tempCanvasRect.x());
            }
            if (tempCanvasRect.bottom() > canvasBounds.height()) {
                tempCanvasRect.setHeight(canvasBounds.height() - tempCanvasRect.y());
            }

            // Convert back to screen coordinates for the constrained geometry
            newRect = inkCanvas->mapCanvasToWidget(tempCanvasRect);
        }

        // Update the widget geometry directly during resizing
        setGeometry(newRect);

        // Convert screen coordinates back to canvas coordinates
        convertScreenToCanvasRect(newRect);
        emit windowResized(this);
    } else if (dragging) {
        QPoint delta = event->globalPosition().toPoint() - dragStartPosition;
        QPoint newPos = windowStartPosition + delta;

        // Keep window within canvas bounds (not just parent widget bounds)
        if (InkCanvas *inkCanvas = qobject_cast<InkCanvas*>(parentWidget())) {
            // Convert current position to canvas coordinates to check bounds
            QRect tempScreenRect(newPos, size());
            QRect tempCanvasRect = inkCanvas->mapWidgetToCanvas(tempScreenRect);
            QRect canvasBounds = inkCanvas->getCanvasRect();

            // Apply canvas bounds constraints
            int maxCanvasX = canvasBounds.width() - tempCanvasRect.width();
            int maxCanvasY = canvasBounds.height() - tempCanvasRect.height();

            tempCanvasRect.setX(qMax(0, qMin(tempCanvasRect.x(), maxCanvasX)));
            tempCanvasRect.setY(qMax(0, qMin(tempCanvasRect.y(), maxCanvasY)));

            // Convert back to screen coordinates for the constrained position
            QRect constrainedScreenRect = inkCanvas->mapCanvasToWidget(tempCanvasRect);
            newPos = constrainedScreenRect.topLeft();

            // Debug: Log when position is constrained
            if (tempCanvasRect != inkCanvas->mapWidgetToCanvas(QRect(windowStartPosition + delta, size()))) {
                // qDebug() << "MarkdownWindow: Constraining position to canvas bounds. Canvas rect:" << tempCanvasRect;
            }
        } else {
            // Fallback: Keep window within parent bounds
            if (parentWidget()) {
                QRect parentRect = parentWidget()->rect();
                newPos.setX(qMax(0, qMin(newPos.x(), parentRect.width() - width())));
                newPos.setY(qMax(0, qMin(newPos.y(), parentRect.height() - height())));
            }
        }

        // Update the widget position directly during dragging
        move(newPos);

        // Convert screen position back to canvas coordinates
        QRect newScreenRect(newPos, size());
        convertScreenToCanvasRect(newScreenRect);
        emit windowMoved(this);
    } else {
        // Update cursor for resize handles
        updateCursor(event->pos());
    }

    QWidget::mouseMoveEvent(event);
}
    void mouseReleaseEvent(QMouseEvent *event) override {
    if (event->button() == Qt::LeftButton) {
        resizing = false;
        dragging = false;
        isUserInteracting = false;
        currentResizeHandle = None;
    }

    QWidget::mouseReleaseEvent(event);
}
    void resizeEvent(QResizeEvent *event) override {
    QWidget::resizeEvent(event);

    // Emit windowResized if this is a user-initiated resize or if we're not updating position due to canvas changes
    if (isUserInteracting || !isUpdatingPosition) {
        emit windowResized(this);
    }
}
    void paintEvent(QPaintEvent *event) override {
    QWidget::paintEvent(event);

    // Draw resize handles
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);

    QColor handleColor = hasFocus() ? QColor(74, 144, 226) : QColor(180, 180, 180);
    painter.setPen(QPen(handleColor, 2));
    painter.setBrush(QBrush(handleColor));

    // Draw corner handles
    int handleSize = 6;
    painter.drawEllipse(0, 0, handleSize, handleSize); // Top-left
    painter.drawEllipse(width() - handleSize, 0, handleSize, handleSize); // Top-right
    painter.drawEllipse(0, height() - handleSize, handleSize, handleSize); // Bottom-left
    painter.drawEllipse(width() - handleSize, height() - handleSize, handleSize, handleSize); // Bottom-right
}
    void focusInEvent(QFocusEvent *event) override {
    // qDebug() << "MarkdownWindow::focusInEvent() - Window" << this << "gained focus";
    QWidget::focusInEvent(event);
    emit focusChanged(this, true);
}
    void focusOutEvent(QFocusEvent *event) override {
    // qDebug() << "MarkdownWindow::focusOutEvent() - Window" << this << "lost focus";
    QWidget::focusOutEvent(event);
    emit focusChanged(this, false);
}
    bool eventFilter(QObject *obj, QEvent *event) override {
    if (obj == markdownEditor) {
        if (event->type() == QEvent::FocusIn) {
            // qDebug() << "MarkdownWindow::eventFilter() - Editor gained focus in window" << this;
            emit editorFocusChanged(this, true);
        } else if (event->type() == QEvent::FocusOut) {
            // qDebug() << "MarkdownWindow::eventFilter() - Editor lost focus in window" << this;
            emit editorFocusChanged(this, false);
        }
    } else if (obj == parentWidget() && event->type() == QEvent::Resize) {
        // Canvas widget was resized (likely due to layout changes like showing/hiding sidebars)
        // Update our screen position to maintain canvas position
        QTimer::singleShot(0, this, [this]() {
            updateScreenPosition();
        });
    }
    return QWidget::eventFilter(obj, event);
}
  private:
    void onDeleteClicked() {
    emit deleteRequested(this);
}
    void onMarkdownTextChanged() {
    emit contentChanged();
}
  private:
    enum ResizeHandle {
        None,
        TopLeft,
        TopRight,
        BottomLeft,
        BottomRight,
        Top,
        Bottom,
        Left,
        Right
    };
    QMarkdownTextEdit *markdownEditor;
    QPushButton *deleteButton;
    QLabel *titleLabel;
    QVBoxLayout *mainLayout;
    QHBoxLayout *headerLayout;
    bool dragging = false;
    QPoint dragStartPosition;
    QPoint windowStartPosition;
    bool resizing = false;
    QPoint resizeStartPosition;
    QRect resizeStartRect;
    ResizeHandle currentResizeHandle = None;
    QRect canvasRect;
    bool isTransparentState = false;
    bool isUpdatingPosition = false;
    bool isUserInteracting = false;
    ResizeHandle getResizeHandle(const QPoint &pos) const {
    const int handleSize = 8;
    const QRect rect = this->rect();

    // Check corner handles first
    if (QRect(0, 0, handleSize, handleSize).contains(pos))
        return TopLeft;
    if (QRect(rect.width() - handleSize, 0, handleSize, handleSize).contains(pos))
        return TopRight;
    if (QRect(0, rect.height() - handleSize, handleSize, handleSize).contains(pos))
        return BottomLeft;
    if (QRect(rect.width() - handleSize, rect.height() - handleSize, handleSize, handleSize).contains(pos))
        return BottomRight;

    // Check edge handles
    if (QRect(0, 0, rect.width(), handleSize).contains(pos))
        return Top;
    if (QRect(0, rect.height() - handleSize, rect.width(), handleSize).contains(pos))
        return Bottom;
    if (QRect(0, 0, handleSize, rect.height()).contains(pos))
        return Left;
    if (QRect(rect.width() - handleSize, 0, handleSize, rect.height()).contains(pos))
        return Right;

    return None;
}
    void updateCursor(const QPoint &pos) {
    ResizeHandle handle = getResizeHandle(pos);

    switch (handle) {
        case TopLeft:
        case BottomRight:
            setCursor(Qt::SizeFDiagCursor);
            break;
        case TopRight:
        case BottomLeft:
            setCursor(Qt::SizeBDiagCursor);
            break;
        case Top:
        case Bottom:
            setCursor(Qt::SizeVerCursor);
            break;
        case Left:
        case Right:
            setCursor(Qt::SizeHorCursor);
            break;
        default:
            if (pos.y() < 24) { // Header area
                setCursor(Qt::SizeAllCursor);
            } else {
                setCursor(Qt::ArrowCursor);
            }
            break;
    }
}
    void setupUI() {
    mainLayout = new QVBoxLayout(this);
    mainLayout->setContentsMargins(2, 2, 2, 2);
    mainLayout->setSpacing(0);

    // Header with title and delete button
    headerLayout = new QHBoxLayout();
    headerLayout->setContentsMargins(4, 2, 4, 2);
    headerLayout->setSpacing(4);

    titleLabel = new QLabel("Markdown", this);
    titleLabel->setStyleSheet("font-weight: bold; color: #333;");

    deleteButton = new QPushButton("×", this);
    deleteButton->setFixedSize(16, 16);
    deleteButton->setStyleSheet(R"(
        QPushButton {
            background-color: #ff4444;
            color: white;
            border: none;
            border-radius: 8px;
            font-weight: bold;
            font-size: 10px;
        }
        QPushButton:hover {
            background-color: #ff6666;
        }
        QPushButton:pressed {
            background-color: #cc2222;
        }
    )");

    connect(deleteButton, &QPushButton::clicked, this, &MarkdownWindow::onDeleteClicked);

    headerLayout->addWidget(titleLabel);
    headerLayout->addStretch();
    headerLayout->addWidget(deleteButton);

    // Markdown editor
    markdownEditor = new QMarkdownTextEdit(this);
    markdownEditor->setPlainText(""); // Start with empty content

    connect(markdownEditor, &QMarkdownTextEdit::textChanged, this, &MarkdownWindow::onMarkdownTextChanged);

    // Connect editor focus events using event filter
    markdownEditor->installEventFilter(this);

    mainLayout->addLayout(headerLayout);
    mainLayout->addWidget(markdownEditor);
}
    void applyStyle() {
    // Detect dark mode using the same method as MainWindow
    bool isDarkMode = palette().color(QPalette::Window).lightness() < 128;

    QString backgroundColor = isDarkMode ? "#2b2b2b" : "white";
    QString borderColor = isDarkMode ? "#555555" : "#cccccc";
    QString headerBackgroundColor = isDarkMode ? "#3c3c3c" : "#f0f0f0";
    QString focusBorderColor = isDarkMode ? "#6ca9dc" : "#4a90e2";

    setStyleSheet(QString(R"(
        MarkdownWindow {
            background-color: %1;
            border: 2px solid %2;
            border-radius: 4px;
        }
        MarkdownWindow:focus {
            border-color: %3;
        }
    )").arg(backgroundColor, borderColor, focusBorderColor));

    // Style the header
    if (headerLayout && headerLayout->parentWidget()) {
        headerLayout->parentWidget()->setStyleSheet(QString(R"(
            background-color: %1;
            border-bottom: 1px solid %2;
        )").arg(headerBackgroundColor, borderColor));
    }

    // Reset the markdown editor style to default (opaque)
    if (markdownEditor) {
        markdownEditor->setStyleSheet(""); // Clear any custom styles
    }
}
    void applyTransparentStyle() {
    // Detect dark mode using the same method as MainWindow
    bool isDarkMode = palette().color(QPalette::Window).lightness() < 128;

    QString backgroundColor = isDarkMode ? "rgba(43, 43, 43, 0.3)" : "rgba(255, 255, 255, 0.3)"; // 30% opacity
    QString borderColor = isDarkMode ? "#555555" : "#cccccc";
    QString headerBackgroundColor = isDarkMode ? "rgba(60, 60, 60, 0.3)" : "rgba(240, 240, 240, 0.3)"; // 30% opacity
    QString focusBorderColor = isDarkMode ? "#6ca9dc" : "#4a90e2";

    setStyleSheet(QString(R"(
        MarkdownWindow {
            background-color: %1;
            border: 2px solid %2;
            border-radius: 4px;
        }
        MarkdownWindow:focus {
            border-color: %3;
        }
    )").arg(backgroundColor, borderColor, focusBorderColor));

    // Style the header with transparency
    if (headerLayout && headerLayout->parentWidget()) {
        headerLayout->parentWidget()->setStyleSheet(QString(R"(
            background-color: %1;
            border-bottom: 1px solid %2;
        )").arg(headerBackgroundColor, borderColor));
    }

    // Make the markdown editor background semi-transparent too
    if (markdownEditor) {
        QString editorBg = isDarkMode ? "rgba(43, 43, 43, 0.3)" : "rgba(255, 255, 255, 0.3)";
        markdownEditor->setStyleSheet(QString(R"(
            QMarkdownTextEdit {
                background-color: %1;
                border: none;
            }
        )").arg(editorBg));
    }
}
    void convertScreenToCanvasRect(const QRect &screenRect) {
    // Get the canvas parent to access coordinate conversion methods
    if (QWidget *canvas = parentWidget()) {
        // Try to cast to InkCanvas to use the new coordinate methods
        if (InkCanvas *inkCanvas = qobject_cast<InkCanvas*>(canvas)) {
            // Use the new coordinate conversion methods
            QRect oldCanvasRect = canvasRect;
            QRect newCanvasRect = inkCanvas->mapWidgetToCanvas(screenRect);

            // Store the converted canvas coordinates without bounds checking
            // Bounds checking should only happen during user interaction (dragging/resizing)
            canvasRect = newCanvasRect;
            // Only log if there was a significant change (and not during mouse movement)
            if ((canvasRect.topLeft() - oldCanvasRect.topLeft()).manhattanLength() > 10 && !dragging && !resizing) {
                // qDebug() << "MarkdownWindow::convertScreenToCanvasRect() - Screen rect:" << screenRect << "-> Canvas rect:" << canvasRect;
                // qDebug() << "  Old canvas rect:" << oldCanvasRect;
            }
        } else {
            // Fallback: use screen coordinates directly
            // qDebug() << "MarkdownWindow::convertScreenToCanvasRect() - No InkCanvas parent, using screen rect directly:" << screenRect;
            canvasRect = screenRect;
        }
    } else {
        // No parent, use screen coordinates directly
        // qDebug() << "MarkdownWindow::convertScreenToCanvasRect() - No parent, using screen rect directly:" << screenRect;
        canvasRect = screenRect;
    }
}
};

class ControlPanelDialog {
    Q_OBJECT

public:
    explicit ControlPanelDialog(MainWindow *mainWindow, InkCanvas *targetCanvas, QWidget *parent = nullptr);
  private:
    void applyChanges() {
    if (!canvas) return;

    BackgroundStyle style = static_cast<BackgroundStyle>(
        styleCombo->currentData().toInt()
    );

    canvas->setBackgroundStyle(style);
    canvas->setBackgroundColor(selectedColor);
    canvas->setBackgroundDensity(densitySpin->value());
    canvas->update();
    canvas->saveBackgroundMetadata();

    // ✅ Save these settings as defaults for new tabs
    if (mainWindowRef) {
        mainWindowRef->saveDefaultBackgroundSettings(style, selectedColor, densitySpin->value());
    }

    // ✅ Apply button mappings back to MainWindow with internal keys
    if (mainWindowRef) {
        for (const QString &buttonKey : holdMappingCombos.keys()) {
            QString displayString = holdMappingCombos[buttonKey]->currentText();
            QString internalKey = ButtonMappingHelper::displayToInternalKey(displayString, true);  // true = isDialMode
            mainWindowRef->setHoldMapping(buttonKey, internalKey);
        }
        for (const QString &buttonKey : pressMappingCombos.keys()) {
            QString displayString = pressMappingCombos[buttonKey]->currentText();
            QString internalKey = ButtonMappingHelper::displayToInternalKey(displayString, false);  // false = isAction
            mainWindowRef->setPressMapping(buttonKey, internalKey);
        }

        // ✅ Save to persistent settings
        mainWindowRef->saveButtonMappings();

        // ✅ Apply theme settings
        mainWindowRef->setUseCustomAccentColor(useCustomAccentCheckbox->isChecked());
        if (selectedAccentColor.isValid()) {
            mainWindowRef->setCustomAccentColor(selectedAccentColor);
        }

        // ✅ Apply color palette setting
        mainWindowRef->setUseBrighterPalette(useBrighterPaletteCheckbox->isChecked());
    }
}
    void chooseColor() {
    QColor chosen = QColorDialog::getColor(selectedColor, this, tr("Select Background Color"));
    if (chosen.isValid()) {
        selectedColor = chosen;
        colorButton->setStyleSheet(QString("background-color: %1").arg(selectedColor.name()));
    }
}
    void addKeyboardMapping() {
    // Step 1: Capture key sequence
    KeyCaptureDialog captureDialog(this);
    if (captureDialog.exec() != QDialog::Accepted) {
        return;
    }

    QString keySequence = captureDialog.getCapturedKeySequence();
    if (keySequence.isEmpty()) {
        return;
    }

    // Check if key sequence already exists
    if (mainWindowRef && mainWindowRef->getKeyboardMappings().contains(keySequence)) {
        QMessageBox::warning(this, tr("Key Already Mapped"),
            tr("The key sequence '%1' is already mapped. Please choose a different key combination.").arg(keySequence));
        return;
    }

    // Step 2: Choose action
    QStringList actions = ButtonMappingHelper::getTranslatedActions();
    bool ok;
    QString selectedAction = QInputDialog::getItem(this, tr("Select Action"),
        tr("Choose the action to perform when '%1' is pressed:").arg(keySequence),
        actions, 0, false, &ok);

    if (!ok || selectedAction.isEmpty()) {
        return;
    }

    // Convert display name to internal key
    QString internalKey = ButtonMappingHelper::displayToInternalKey(selectedAction, false);

    // Add the mapping
    if (mainWindowRef) {
        mainWindowRef->addKeyboardMapping(keySequence, internalKey);

        // Update table
        int row = keyboardTable->rowCount();
        keyboardTable->insertRow(row);
        keyboardTable->setItem(row, 0, new QTableWidgetItem(keySequence));
        keyboardTable->setItem(row, 1, new QTableWidgetItem(selectedAction));
    }
}
    void removeKeyboardMapping() {
    int currentRow = keyboardTable->currentRow();
    if (currentRow < 0) {
        QMessageBox::information(this, tr("No Selection"), tr("Please select a mapping to remove."));
        return;
    }

    QTableWidgetItem *keyItem = keyboardTable->item(currentRow, 0);
    if (!keyItem) return;

    QString keySequence = keyItem->text();

    // Confirm removal
    int ret = QMessageBox::question(this, tr("Remove Mapping"),
        tr("Are you sure you want to remove the keyboard shortcut '%1'?").arg(keySequence),
        QMessageBox::Yes | QMessageBox::No, QMessageBox::No);

    if (ret == QMessageBox::Yes) {
        // Remove from MainWindow
        if (mainWindowRef) {
            mainWindowRef->removeKeyboardMapping(keySequence);
        }

        // Remove from table
        keyboardTable->removeRow(currentRow);
    }
}
    void openControllerMapping() {
    if (!mainWindowRef) {
        QMessageBox::warning(this, tr("Error"), tr("MainWindow reference not available."));
        return;
    }

    SDLControllerManager *controllerManager = mainWindowRef->getControllerManager();
    if (!controllerManager) {
        QMessageBox::warning(this, tr("Controller Not Available"),
            tr("Controller manager is not available. Please ensure a controller is connected and restart the application."));
        return;
    }

    if (!controllerManager->getJoystick()) {
        QMessageBox::warning(this, tr("No Controller Detected"),
            tr("No controller is currently connected. Please connect your controller and restart the application."));
        return;
    }

    ControllerMappingDialog dialog(controllerManager, this);
    dialog.exec();
}
    void reconnectController() {
    if (!mainWindowRef) {
        QMessageBox::warning(this, tr("Error"), tr("MainWindow reference not available."));
        return;
    }

    SDLControllerManager *controllerManager = mainWindowRef->getControllerManager();
    if (!controllerManager) {
        QMessageBox::warning(this, tr("Controller Not Available"),
            tr("Controller manager is not available."));
        return;
    }

    // Show reconnecting message
    controllerStatusLabel->setText(tr("🔄 Reconnecting..."));
    controllerStatusLabel->setStyleSheet("color: orange;");

    // Force the UI to update immediately
    QApplication::processEvents();

    // Attempt to reconnect using thread-safe method
    QMetaObject::invokeMethod(controllerManager, "reconnect", Qt::BlockingQueuedConnection);

    // Update status after reconnection attempt
    updateControllerStatus();

    // Show result message
    if (controllerManager->getJoystick()) {
        // Reconnect the controller signals in MainWindow
        mainWindowRef->reconnectControllerSignals();

        QMessageBox::information(this, tr("Reconnection Successful"),
            tr("Controller has been successfully reconnected!"));
    } else {
        QMessageBox::warning(this, tr("Reconnection Failed"),
            tr("Failed to reconnect controller. Please ensure your controller is powered on and in pairing mode, then try again."));
    }
}
  private:
    InkCanvas *canvas;
    QTabWidget *tabWidget;
    QWidget *backgroundTab;
    QComboBox *styleCombo;
    QPushButton *colorButton;
    QSpinBox *densitySpin;
    QPushButton *applyButton;
    QPushButton *okButton;
    QPushButton *cancelButton;
    QColor selectedColor;
    void createBackgroundTab() {
    backgroundTab = new QWidget(this);

    QLabel *styleLabel = new QLabel(tr("Background Style:"));
    styleCombo = new QComboBox();
    styleCombo->addItem(tr("None"), static_cast<int>(BackgroundStyle::None));
    styleCombo->addItem(tr("Grid"), static_cast<int>(BackgroundStyle::Grid));
    styleCombo->addItem(tr("Lines"), static_cast<int>(BackgroundStyle::Lines));

    QLabel *colorLabel = new QLabel(tr("Background Color:"));
    colorButton = new QPushButton();
    connect(colorButton, &QPushButton::clicked, this, &ControlPanelDialog::chooseColor);

    QLabel *densityLabel = new QLabel(tr("Density:"));
    densitySpin = new QSpinBox();
    densitySpin->setRange(10, 200);
    densitySpin->setSuffix(" px");
    densitySpin->setSingleStep(5);

    QGridLayout *layout = new QGridLayout(backgroundTab);
    layout->addWidget(styleLabel, 0, 0);
    layout->addWidget(styleCombo, 0, 1);
    layout->addWidget(colorLabel, 1, 0);
    layout->addWidget(colorButton, 1, 1);
    layout->addWidget(densityLabel, 2, 0);
    layout->addWidget(densitySpin, 2, 1);
    // layout->setColumnStretch(1, 1); // Stretch the second column
    layout->setRowStretch(3, 1); // Stretch the last row
}
    void loadFromCanvas() {
    styleCombo->setCurrentIndex(static_cast<int>(canvas->getBackgroundStyle()));
    densitySpin->setValue(canvas->getBackgroundDensity());
    selectedColor = canvas->getBackgroundColor();

    colorButton->setStyleSheet(QString("background-color: %1").arg(selectedColor.name()));

    if (mainWindowRef) {
        for (const QString &buttonKey : holdMappingCombos.keys()) {
            QString internalKey = mainWindowRef->getHoldMapping(buttonKey);
            QString displayString = ButtonMappingHelper::internalKeyToDisplay(internalKey, true);  // true = isDialMode
            int index = holdMappingCombos[buttonKey]->findText(displayString);
            if (index >= 0) holdMappingCombos[buttonKey]->setCurrentIndex(index);
        }

        for (const QString &buttonKey : pressMappingCombos.keys()) {
            QString internalKey = mainWindowRef->getPressMapping(buttonKey);
            QString displayString = ButtonMappingHelper::internalKeyToDisplay(internalKey, false);  // false = isAction
            int index = pressMappingCombos[buttonKey]->findText(displayString);
            if (index >= 0) pressMappingCombos[buttonKey]->setCurrentIndex(index);
        }

        // Load theme settings
        useCustomAccentCheckbox->setChecked(mainWindowRef->isUsingCustomAccentColor());

        // Get the stored custom accent color
        selectedAccentColor = mainWindowRef->getCustomAccentColor();

        accentColorButton->setStyleSheet(QString("background-color: %1").arg(selectedAccentColor.name()));
        accentColorButton->setEnabled(useCustomAccentCheckbox->isChecked());

        // Load color palette setting
        useBrighterPaletteCheckbox->setChecked(mainWindowRef->isUsingBrighterPalette());
    }
}
    MainWindow *mainWindowRef;
    InkCanvas *canvasRef;
    QWidget *performanceTab;
    QWidget *toolbarTab;
    void createToolbarTab() {
    toolbarTab = new QWidget(this);
    QVBoxLayout *toolbarLayout = new QVBoxLayout(toolbarTab);

    // ✅ Checkbox to show/hide benchmark controls
    QCheckBox *benchmarkVisibilityCheckbox = new QCheckBox(tr("Show Benchmark Controls"), toolbarTab);
    benchmarkVisibilityCheckbox->setChecked(mainWindowRef->areBenchmarkControlsVisible());
    toolbarLayout->addWidget(benchmarkVisibilityCheckbox);
    QLabel *benchmarkNote = new QLabel(tr("This will show/hide the benchmark controls on the toolbar. Press the clock button to start/stop the benchmark."));
    benchmarkNote->setWordWrap(true);
    benchmarkNote->setStyleSheet("color: gray; font-size: 10px;");
    toolbarLayout->addWidget(benchmarkNote);

    // ✅ Checkbox to show/hide zoom buttons
    QCheckBox *zoomButtonsVisibilityCheckbox = new QCheckBox(tr("Show Zoom Buttons"), toolbarTab);
    zoomButtonsVisibilityCheckbox->setChecked(mainWindowRef->areZoomButtonsVisible());
    toolbarLayout->addWidget(zoomButtonsVisibilityCheckbox);
    QLabel *zoomButtonsNote = new QLabel(tr("This will show/hide the 0.5x, 1x, and 2x zoom buttons on the toolbar"));
    zoomButtonsNote->setWordWrap(true);
    zoomButtonsNote->setStyleSheet("color: gray; font-size: 10px;");
    toolbarLayout->addWidget(zoomButtonsNote);

    QCheckBox *scrollOnTopCheckBox = new QCheckBox(tr("Scroll on Top after Page Switching"), toolbarTab);
    scrollOnTopCheckBox->setChecked(mainWindowRef->isScrollOnTopEnabled());
    toolbarLayout->addWidget(scrollOnTopCheckBox);
    QLabel *scrollNote = new QLabel(tr("Enabling this will make the page scroll to the top after switching to a new page."));
    scrollNote->setWordWrap(true);
    scrollNote->setStyleSheet("color: gray; font-size: 10px;");
    toolbarLayout->addWidget(scrollNote);

    toolbarLayout->addStretch();
    toolbarTab->setLayout(toolbarLayout);
    tabWidget->addTab(toolbarTab, tr("Features"));

    // Connect the checkbox
    connect(benchmarkVisibilityCheckbox, &QCheckBox::toggled, mainWindowRef, &MainWindow::setBenchmarkControlsVisible);
    connect(zoomButtonsVisibilityCheckbox, &QCheckBox::toggled, mainWindowRef, &MainWindow::setZoomButtonsVisible);
    connect(scrollOnTopCheckBox, &QCheckBox::toggled, mainWindowRef, &MainWindow::setScrollOnTopEnabled);
}
    void createPerformanceTab() {
    performanceTab = new QWidget();
    QVBoxLayout *layout = new QVBoxLayout(performanceTab);

    QCheckBox *previewToggle = new QCheckBox(tr("Enable Low-Resolution PDF Previews"));
    previewToggle->setChecked(mainWindowRef->isLowResPreviewEnabled());

    connect(previewToggle, &QCheckBox::toggled, mainWindowRef, &MainWindow::setLowResPreviewEnabled);

    QLabel *note = new QLabel(tr("Disabling this may improve dial smoothness on low-end devices."));
    note->setWordWrap(true);
    note->setStyleSheet("color: gray; font-size: 10px;");

    QLabel *dpiLabel = new QLabel(tr("PDF Rendering DPI:"));
    QComboBox *dpiSelector = new QComboBox();
    dpiSelector->addItems({"96", "192", "288", "384", "480"});
    dpiSelector->setCurrentText(QString::number(mainWindowRef->getPdfDPI()));

    connect(dpiSelector, &QComboBox::currentTextChanged, this, [=](const QString &value) {
        mainWindowRef->setPdfDPI(value.toInt());
    });

    QLabel *notePDF = new QLabel(tr("Adjust how the PDF is rendered. Higher DPI means better quality but slower performance. DO NOT CHANGE THIS OPTION WHEN MULTIPLE TABS ARE OPEN. THIS MAY LEAD TO UNDEFINED BEHAVIOR!"));
    notePDF->setWordWrap(true);
    notePDF->setStyleSheet("color: gray; font-size: 10px;");

    layout->addWidget(previewToggle);
    layout->addWidget(note);
    layout->addWidget(dpiLabel);
    layout->addWidget(dpiSelector);
    layout->addWidget(notePDF);

    layout->addStretch();

    // return performanceTab;
}
    QWidget *controllerMappingTab;
    QPushButton *reconnectButton;
    QLabel *controllerStatusLabel;
    QMap<QString, QComboBox*> holdMappingCombos;
    QMap<QString, QComboBox*> pressMappingCombos;
    void createButtonMappingTab() {
    QWidget *buttonTab = new QWidget(this);
    QVBoxLayout *layout = new QVBoxLayout(buttonTab);

    QStringList buttonKeys = ButtonMappingHelper::getInternalButtonKeys();
    QStringList buttonDisplayNames = ButtonMappingHelper::getTranslatedButtons();
    QStringList dialModes = ButtonMappingHelper::getTranslatedDialModes();
    QStringList actions = ButtonMappingHelper::getTranslatedActions();

    for (int i = 0; i < buttonKeys.size(); ++i) {
        const QString &buttonKey = buttonKeys[i];
        const QString &buttonDisplayName = buttonDisplayNames[i];

        QHBoxLayout *h = new QHBoxLayout();
        h->addWidget(new QLabel(buttonDisplayName));  // Use translated button name

        QComboBox *holdCombo = new QComboBox();
        holdCombo->addItems(dialModes);  // Add translated dial mode names
        holdMappingCombos[buttonKey] = holdCombo;
        h->addWidget(new QLabel(tr("Hold:")));
        h->addWidget(holdCombo);

        QComboBox *pressCombo = new QComboBox();
        pressCombo->addItems(actions);  // Add translated action names
        pressMappingCombos[buttonKey] = pressCombo;
        h->addWidget(new QLabel(tr("Press:")));
        h->addWidget(pressCombo);

        layout->addLayout(h);
    }

    layout->addStretch();
    buttonTab->setLayout(layout);
    tabWidget->addTab(buttonTab, tr("Button Mapping"));
}
    void createControllerMappingTab() {
    controllerMappingTab = new QWidget(this);
    QVBoxLayout *layout = new QVBoxLayout(controllerMappingTab);

    // Instructions
    QLabel *instructionLabel = new QLabel(tr("Configure physical controller button mappings for your Joy-Con or other controller:"), controllerMappingTab);
    instructionLabel->setWordWrap(true);
    layout->addWidget(instructionLabel);

    QLabel *noteLabel = new QLabel(tr("Note: This maps your physical controller buttons to the logical Joy-Con functions used by the application. "
                                     "After setting up the physical mapping, you can configure what actions each logical button performs in the 'Button Mapping' tab."), controllerMappingTab);
    noteLabel->setWordWrap(true);
    noteLabel->setStyleSheet("color: gray; font-size: 10px; margin-bottom: 10px;");
    layout->addWidget(noteLabel);

    // Button to open controller mapping dialog
    QPushButton *openMappingButton = new QPushButton(tr("Configure Controller Mapping"), controllerMappingTab);
    openMappingButton->setMinimumHeight(40);
    connect(openMappingButton, &QPushButton::clicked, this, &ControlPanelDialog::openControllerMapping);
    layout->addWidget(openMappingButton);

    // Button to reconnect controller
    reconnectButton = new QPushButton(tr("Reconnect Controller"), controllerMappingTab);
    reconnectButton->setMinimumHeight(40);
    reconnectButton->setStyleSheet("QPushButton { background-color: #4CAF50; color: white; font-weight: bold; }");
    connect(reconnectButton, &QPushButton::clicked, this, &ControlPanelDialog::reconnectController);
    layout->addWidget(reconnectButton);

    // Status information
    QLabel *statusLabel = new QLabel(tr("Current controller status:"), controllerMappingTab);
    statusLabel->setStyleSheet("font-weight: bold; margin-top: 20px;");
    layout->addWidget(statusLabel);

    // Dynamic status label
    controllerStatusLabel = new QLabel(controllerMappingTab);
    updateControllerStatus();
    layout->addWidget(controllerStatusLabel);

    layout->addStretch();

    tabWidget->addTab(controllerMappingTab, tr("Controller Mapping"));
}
    void createKeyboardMappingTab() {
    keyboardTab = new QWidget(this);
    QVBoxLayout *layout = new QVBoxLayout(keyboardTab);

    // Instructions
    QLabel *instructionLabel = new QLabel(tr("Configure custom keyboard shortcuts for application actions:"), keyboardTab);
    instructionLabel->setWordWrap(true);
    layout->addWidget(instructionLabel);

    // Table to show current mappings
    keyboardTable = new QTableWidget(0, 2, keyboardTab);
    keyboardTable->setHorizontalHeaderLabels({tr("Key Sequence"), tr("Action")});
    keyboardTable->horizontalHeader()->setStretchLastSection(true);
    keyboardTable->setSelectionBehavior(QAbstractItemView::SelectRows);
    keyboardTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
    layout->addWidget(keyboardTable);

    // Buttons
    QHBoxLayout *buttonLayout = new QHBoxLayout();
    addKeyboardMappingButton = new QPushButton(tr("Add Mapping"), keyboardTab);
    removeKeyboardMappingButton = new QPushButton(tr("Remove Mapping"), keyboardTab);

    buttonLayout->addWidget(addKeyboardMappingButton);
    buttonLayout->addWidget(removeKeyboardMappingButton);
    buttonLayout->addStretch();

    layout->addLayout(buttonLayout);

    // Connections
    connect(addKeyboardMappingButton, &QPushButton::clicked, this, &ControlPanelDialog::addKeyboardMapping);
    connect(removeKeyboardMappingButton, &QPushButton::clicked, this, &ControlPanelDialog::removeKeyboardMapping);

    // Load current mappings
    if (mainWindowRef) {
        QMap<QString, QString> mappings = mainWindowRef->getKeyboardMappings();
        keyboardTable->setRowCount(mappings.size());
        int row = 0;
        for (auto it = mappings.begin(); it != mappings.end(); ++it) {
            keyboardTable->setItem(row, 0, new QTableWidgetItem(it.key()));
            QString displayAction = ButtonMappingHelper::internalKeyToDisplay(it.value(), false);
            keyboardTable->setItem(row, 1, new QTableWidgetItem(displayAction));
            row++;
        }
    }

    tabWidget->addTab(keyboardTab, tr("Keyboard Shortcuts"));
}
    QWidget *keyboardTab;
    QTableWidget *keyboardTable;
    QPushButton *addKeyboardMappingButton;
    QPushButton *removeKeyboardMappingButton;
    QWidget *themeTab;
    QCheckBox *useCustomAccentCheckbox;
    QPushButton *accentColorButton;
    QColor selectedAccentColor;
    QCheckBox *useBrighterPaletteCheckbox;
    void createThemeTab() {
    themeTab = new QWidget(this);
    QVBoxLayout *layout = new QVBoxLayout(themeTab);

    // Custom accent color
    useCustomAccentCheckbox = new QCheckBox(tr("Use Custom Accent Color"), themeTab);
    layout->addWidget(useCustomAccentCheckbox);

    QLabel *accentColorLabel = new QLabel(tr("Accent Color:"), themeTab);
    accentColorButton = new QPushButton(themeTab);
    accentColorButton->setFixedSize(100, 30);
    connect(accentColorButton, &QPushButton::clicked, this, &ControlPanelDialog::chooseAccentColor);

    QHBoxLayout *accentColorLayout = new QHBoxLayout();
    accentColorLayout->addWidget(accentColorLabel);
    accentColorLayout->addWidget(accentColorButton);
    accentColorLayout->addStretch();
    layout->addLayout(accentColorLayout);

    QLabel *accentColorNote = new QLabel(tr("When enabled, use a custom accent color instead of the system accent color for the toolbar, dial, and tab selection."));
    accentColorNote->setWordWrap(true);
    accentColorNote->setStyleSheet("color: gray; font-size: 10px;");
    layout->addWidget(accentColorNote);

    // Enable/disable accent color button based on checkbox
    connect(useCustomAccentCheckbox, &QCheckBox::toggled, accentColorButton, &QPushButton::setEnabled);
    connect(useCustomAccentCheckbox, &QCheckBox::toggled, accentColorLabel, &QLabel::setEnabled);

    // Color palette preference
    useBrighterPaletteCheckbox = new QCheckBox(tr("Use Brighter Color Palette"), themeTab);
    layout->addWidget(useBrighterPaletteCheckbox);

    QLabel *paletteNote = new QLabel(tr("When enabled, use brighter colors (good for dark PDF backgrounds). When disabled, use darker colors (good for light PDF backgrounds). This setting is independent of the UI theme."));
    paletteNote->setWordWrap(true);
    paletteNote->setStyleSheet("color: gray; font-size: 10px;");
    layout->addWidget(paletteNote);

    layout->addStretch();

    tabWidget->addTab(themeTab, tr("Theme"));
}
    void chooseAccentColor() {
    QColor chosen = QColorDialog::getColor(selectedAccentColor, this, tr("Select Accent Color"));
    if (chosen.isValid()) {
        selectedAccentColor = chosen;
        accentColorButton->setStyleSheet(QString("background-color: %1").arg(selectedAccentColor.name()));
    }
}
    void updateControllerStatus() {
    if (!mainWindowRef || !controllerStatusLabel) return;

    SDLControllerManager *controllerManager = mainWindowRef->getControllerManager();
    if (!controllerManager) {
        controllerStatusLabel->setText(tr("✗ Controller manager not available"));
        controllerStatusLabel->setStyleSheet("color: red;");
        return;
    }

    if (controllerManager->getJoystick()) {
        controllerStatusLabel->setText(tr("✓ Controller connected"));
        controllerStatusLabel->setStyleSheet("color: green; font-weight: bold;");
    } else {
        controllerStatusLabel->setText(tr("✗ No controller detected"));
        controllerStatusLabel->setStyleSheet("color: red; font-weight: bold;");
    }
}
};

class SDLControllerManager {
    Q_OBJECT

public:
    explicit ControllerMappingDialog(SDLControllerManager *controllerManager, QWidget *parent = nullptr);
  private:
    void startButtonMapping(const QString &logicalButton) {
    if (!controller) return;

    // Disable all mapping buttons during mapping
    for (auto button : mappingButtons) {
        button->setEnabled(false);
    }

    // Update the current button being mapped
    currentMappingButton = logicalButton;
    mappingButtons[logicalButton]->setText(tr("Press button..."));

    // Start button detection mode
    controller->startButtonDetection();

    // Start timeout timer
    mappingTimeoutTimer->start();

    // Show status message
    QApplication::setOverrideCursor(Qt::WaitCursor);
}
    void onRawButtonPressed(int sdlButton, const QString &buttonName) {
    if (currentMappingButton.isEmpty()) return;

    // Stop detection and timeout
    controller->stopButtonDetection();
    mappingTimeoutTimer->stop();
    QApplication::restoreOverrideCursor();

    // Check if this button is already mapped to another function
    QMap<QString, int> currentMappings = controller->getAllPhysicalMappings();
    QString conflictingButton;
    for (auto it = currentMappings.begin(); it != currentMappings.end(); ++it) {
        if (it.value() == sdlButton && it.key() != currentMappingButton) {
            conflictingButton = it.key();
            break;
        }
    }

    if (!conflictingButton.isEmpty()) {
        int ret = QMessageBox::question(this, tr("Button Conflict"),
            tr("The button '%1' is already mapped to '%2'.\n\nDo you want to reassign it to '%3'?")
            .arg(buttonName).arg(conflictingButton).arg(currentMappingButton),
            QMessageBox::Yes | QMessageBox::No);

        if (ret != QMessageBox::Yes) {
            // Reset UI and return
            mappingButtons[currentMappingButton]->setText(tr("Map"));
            for (auto button : mappingButtons) {
                button->setEnabled(true);
            }
            currentMappingButton.clear();
            return;
        }

        // Clear the conflicting mapping
        controller->setPhysicalButtonMapping(conflictingButton, -1);
        currentMappingLabels[conflictingButton]->setText("Not mapped");
        currentMappingLabels[conflictingButton]->setStyleSheet("color: gray;");
    }

    // Set the new mapping
    controller->setPhysicalButtonMapping(currentMappingButton, sdlButton);

    // Get appropriate text color for current theme
    QString textColor = isDarkMode() ? "white" : "black";

    // Update UI
    currentMappingLabels[currentMappingButton]->setText(buttonName);
    currentMappingLabels[currentMappingButton]->setStyleSheet(QString("color: %1; font-weight: bold;").arg(textColor));
    mappingButtons[currentMappingButton]->setText(tr("Map"));

    // Re-enable all buttons
    for (auto button : mappingButtons) {
        button->setEnabled(true);
    }

    currentMappingButton.clear();

    QMessageBox::information(this, tr("Mapping Complete"),
        tr("Button '%1' has been successfully mapped!").arg(buttonName));
}
    void resetToDefaults() {
    int ret = QMessageBox::question(this, tr("Reset to Defaults"),
        tr("Are you sure you want to reset all button mappings to their default values?\n\nThis will overwrite your current configuration."),
        QMessageBox::Yes | QMessageBox::No);

    if (ret == QMessageBox::Yes) {
        // Get default mappings and apply them
        QMap<QString, int> defaults = controller->getDefaultMappings();
        for (auto it = defaults.begin(); it != defaults.end(); ++it) {
            controller->setPhysicalButtonMapping(it.key(), it.value());
        }

        // Update display
        updateMappingDisplay();

        QMessageBox::information(this, tr("Reset Complete"),
            tr("All button mappings have been reset to their default values."));
    }
}
    void applyMappings() {
    // Mappings are already applied in real-time, just close the dialog
    accept();
}
  private:
    SDLControllerManager *controller;
    QGridLayout *mappingLayout;
    QMap<QString, QLabel*> buttonLabels;
    QMap<QString, QLabel*> currentMappingLabels;
    QMap<QString, QPushButton*> mappingButtons;
    QPushButton *resetButton;
    QPushButton *applyButton;
    QPushButton *cancelButton;
    QString currentMappingButton;
    QTimer *mappingTimeoutTimer;
    void setupUI() {
    QVBoxLayout *mainLayout = new QVBoxLayout(this);

    // Instructions
    QLabel *instructionLabel = new QLabel(tr("Map your physical controller buttons to Joy-Con functions.\n"
                                           "Click 'Map' next to each function, then press the corresponding button on your controller."), this);
    instructionLabel->setWordWrap(true);
    instructionLabel->setStyleSheet("font-weight: bold; margin-bottom: 10px;");
    mainLayout->addWidget(instructionLabel);

    // Mapping grid
    QWidget *mappingWidget = new QWidget(this);
    mappingLayout = new QGridLayout(mappingWidget);

    // Headers
    mappingLayout->addWidget(new QLabel(tr("Joy-Con Function"), this), 0, 0);
    mappingLayout->addWidget(new QLabel(tr("Description"), this), 0, 1);
    mappingLayout->addWidget(new QLabel(tr("Current Mapping"), this), 0, 2);
    mappingLayout->addWidget(new QLabel(tr("Action"), this), 0, 3);

    // Get logical button descriptions
    QMap<QString, QString> descriptions = getLogicalButtonDescriptions();

    // Create mapping rows for each logical button
    QStringList logicalButtons = {"LEFTSHOULDER", "RIGHTSHOULDER", "PADDLE2", "PADDLE4",
                                 "Y", "A", "B", "X", "LEFTSTICK", "START", "GUIDE"};

    int row = 1;
    for (const QString &logicalButton : logicalButtons) {
        // Function name
        QLabel *functionLabel = new QLabel(logicalButton, this);
        functionLabel->setStyleSheet("font-weight: bold;");
        buttonLabels[logicalButton] = functionLabel;
        mappingLayout->addWidget(functionLabel, row, 0);

        // Description
        QLabel *descLabel = new QLabel(descriptions.value(logicalButton, "Unknown"), this);
        descLabel->setWordWrap(true);
        mappingLayout->addWidget(descLabel, row, 1);

        // Current mapping display
        QLabel *currentLabel = new QLabel("Not mapped", this);
        currentLabel->setStyleSheet("color: gray;");
        currentMappingLabels[logicalButton] = currentLabel;
        mappingLayout->addWidget(currentLabel, row, 2);

        // Map button
        QPushButton *mapButton = new QPushButton(tr("Map"), this);
        mappingButtons[logicalButton] = mapButton;
        connect(mapButton, &QPushButton::clicked, this, [this, logicalButton]() {
            startButtonMapping(logicalButton);
        });
        mappingLayout->addWidget(mapButton, row, 3);

        row++;
    }

    mainLayout->addWidget(mappingWidget);

    // Buttons
    QHBoxLayout *buttonLayout = new QHBoxLayout();

    resetButton = new QPushButton(tr("Reset to Defaults"), this);
    connect(resetButton, &QPushButton::clicked, this, &ControllerMappingDialog::resetToDefaults);
    buttonLayout->addWidget(resetButton);

    buttonLayout->addStretch();

    applyButton = new QPushButton(tr("Apply"), this);
    connect(applyButton, &QPushButton::clicked, this, &ControllerMappingDialog::applyMappings);
    buttonLayout->addWidget(applyButton);

    cancelButton = new QPushButton(tr("Cancel"), this);
    connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject);
    buttonLayout->addWidget(cancelButton);

    mainLayout->addLayout(buttonLayout);
}
    QMap<QString, QString> getLogicalButtonDescriptions() const {
    QMap<QString, QString> descriptions;
    descriptions["LEFTSHOULDER"] = tr("L Button (Left Shoulder)");
    descriptions["RIGHTSHOULDER"] = tr("ZL Button (Left Trigger)");
    descriptions["PADDLE2"] = tr("SL Button (Side Left)");
    descriptions["PADDLE4"] = tr("SR Button (Side Right)");
    descriptions["Y"] = tr("Up Arrow (D-Pad Up)");
    descriptions["A"] = tr("Down Arrow (D-Pad Down)");
    descriptions["B"] = tr("Left Arrow (D-Pad Left)");
    descriptions["X"] = tr("Right Arrow (D-Pad Right)");
    descriptions["LEFTSTICK"] = tr("Analog Stick Press");
    descriptions["START"] = tr("Minus Button (-)");
    descriptions["GUIDE"] = tr("Screenshot Button");
    return descriptions;
}
    void loadCurrentMappings() {
    QMap<QString, int> currentMappings = controller->getAllPhysicalMappings();

    // Get appropriate text color for current theme
    QString textColor = isDarkMode() ? "white" : "black";

    for (auto it = currentMappings.begin(); it != currentMappings.end(); ++it) {
        const QString &logicalButton = it.key();
        int physicalButton = it.value();

        if (currentMappingLabels.contains(logicalButton)) {
            QString physicalName = controller->getPhysicalButtonName(physicalButton);
            currentMappingLabels[logicalButton]->setText(physicalName);
            currentMappingLabels[logicalButton]->setStyleSheet(QString("color: %1; font-weight: bold;").arg(textColor));
        }
    }
}
    void updateMappingDisplay() {
    loadCurrentMappings();
}
    bool isDarkMode() const {
    // Same logic as MainWindow::isDarkMode()
    QColor bg = palette().color(QPalette::Window);
    return bg.lightness() < 128;  // Lightness scale: 0 (black) - 255 (white)
}
};

class InkCanvas {
    Q_OBJECT

public:
    explicit RecentNotebooksManager(QObject *parent = nullptr);
    void addRecentNotebook(const QString& folderPath, InkCanvas* canvasForPreview = nullptr) {
    if (folderPath.isEmpty()) return;

    // Remove if already exists to move it to the front
    recentNotebookPaths.removeAll(folderPath);
    recentNotebookPaths.prepend(folderPath);

    // Trim the list if it exceeds the maximum size
    while (recentNotebookPaths.size() > MAX_RECENT_NOTEBOOKS) {
        recentNotebookPaths.removeLast();
    }

    generateAndSaveCoverPreview(folderPath, canvasForPreview);
    saveRecentNotebooks();
}
    QStringList getRecentNotebooks() const {
    return recentNotebookPaths;
}
    QString getCoverImagePathForNotebook(const QString& folderPath) const {
    QString coverDir = getCoverImageDir();
    QString coverFileName = sanitizeFolderName(folderPath) + "_cover.png";
    QString coverFilePath = coverDir + coverFileName;
    if (QFile::exists(coverFilePath)) {
        return coverFilePath;
    }
    return ""; // Return empty if no cover exists
}
    QString getNotebookDisplayName(const QString& folderPath) const {
    // Check for PDF metadata first
    QString metadataFile = folderPath + "/.pdf_path.txt";
    if (QFile::exists(metadataFile)) {
        QFile file(metadataFile);
        if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
            QTextStream in(&file);
            QString pdfPath = in.readLine().trimmed();
            file.close();
            if (!pdfPath.isEmpty()) {
                return QFileInfo(pdfPath).fileName();
            }
        }
    }
    // Fallback to folder name
    return QFileInfo(folderPath).fileName();
}
    void generateAndSaveCoverPreview(const QString& folderPath, InkCanvas* optionalCanvas = nullptr) {
    QString coverDir = getCoverImageDir();
    QString coverFileName = sanitizeFolderName(folderPath) + "_cover.png";
    QString coverFilePath = coverDir + coverFileName;

    QImage coverImage(400, 300, QImage::Format_ARGB32_Premultiplied);
    coverImage.fill(Qt::white); // Default background
    QPainter painter(&coverImage);
    painter.setRenderHint(QPainter::SmoothPixmapTransform);

    if (optionalCanvas && optionalCanvas->width() > 0 && optionalCanvas->height() > 0) {
        int logicalCanvasWidth = optionalCanvas->width();
        int logicalCanvasHeight = optionalCanvas->height();

        double targetAspectRatio = 4.0 / 3.0;
        double canvasAspectRatio = (double)logicalCanvasWidth / logicalCanvasHeight;

        int grabWidth, grabHeight;
        int xOffset = 0, yOffset = 0;

        if (canvasAspectRatio > targetAspectRatio) { // Canvas is wider than target 4:3
            grabHeight = logicalCanvasHeight;
            grabWidth = qRound(logicalCanvasHeight * targetAspectRatio);
            xOffset = (logicalCanvasWidth - grabWidth) / 2;
        } else { // Canvas is taller than or equal to target 4:3
            grabWidth = logicalCanvasWidth;
            grabHeight = qRound(logicalCanvasWidth / targetAspectRatio);
            yOffset = (logicalCanvasHeight - grabHeight) / 2;
        }

        if (grabWidth > 0 && grabHeight > 0) {
            QRect grabRect(xOffset, yOffset, grabWidth, grabHeight);
            QPixmap capturedView = optionalCanvas->grab(grabRect);
            painter.drawPixmap(coverImage.rect(), capturedView, capturedView.rect());
        } else {
            // Fallback if calculated grab dimensions are invalid, draw placeholder or fill
            // This case should ideally not be hit if canvas width/height > 0
            painter.fillRect(coverImage.rect(), Qt::lightGray);
            painter.setPen(Qt::black);
            painter.drawText(coverImage.rect(), Qt::AlignCenter, tr("Preview Error"));
        }
    } else {
        // Fallback: check for a saved "annotated_..._00000.png" or the first page
        // This part remains the same as it deals with pre-rendered images.
        QString notebookIdStr;
        InkCanvas tempCanvas(nullptr); // Temporary canvas to access potential notebookId property logic if it were there
                                     // However, notebookId is specific to an instance with a saveFolder.
                                     // For a generic fallback, we might need a different way or assume a common naming if notebookId is unknown.
                                     // For now, let's assume a simple naming or improve this fallback if needed.

        // Attempt to get notebookId if folderPath is a valid notebook with an ID file
        QFile idFile(folderPath + "/.notebook_id.txt");
        if (idFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
            QTextStream in(&idFile);
            notebookIdStr = in.readLine().trimmed();
            idFile.close();
        }

        QString firstPagePath, firstAnnotatedPagePath;
        if (!notebookIdStr.isEmpty()) {
            firstPagePath = folderPath + QString("/%1_00000.png").arg(notebookIdStr);
            firstAnnotatedPagePath = folderPath + QString("/annotated_%1_00000.png").arg(notebookIdStr);
        } else {
            // Fallback if no ID, try a generic name (less ideal)
            // This situation should be rare if notebooks are managed properly
            // For robustness, one might iterate files or use a known default page name
            // For this example, we'll stick to a pattern that might not always work without an ID
            // Consider that `InkCanvas(nullptr).property("notebookId")` was problematic as `notebookId` is instance specific.
        }

        QImage pageImage;
        if (!firstAnnotatedPagePath.isEmpty() && QFile::exists(firstAnnotatedPagePath)) {
            pageImage.load(firstAnnotatedPagePath);
        } else if (!firstPagePath.isEmpty() && QFile::exists(firstPagePath)) {
            pageImage.load(firstPagePath);
        }

        if (!pageImage.isNull()) {
            painter.drawImage(coverImage.rect(), pageImage, pageImage.rect());
        } else {
            // If no image found, draw a placeholder
            painter.fillRect(coverImage.rect(), Qt::darkGray);
            painter.setPen(Qt::white);
            painter.drawText(coverImage.rect(), Qt::AlignCenter, tr("No Page 0 Preview"));
        }
    }
    coverImage.save(coverFilePath, "PNG");
}
  private:
    void loadRecentNotebooks() {
    recentNotebookPaths = settings.value("recentNotebooks").toStringList();
}
    void saveRecentNotebooks() {
    settings.setValue("recentNotebooks", recentNotebookPaths);
}
    QString getCoverImageDir() const {
    return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/RecentCovers/";
}
    QString sanitizeFolderName(const QString& folderPath) const {
    return QFileInfo(folderPath).baseName().replace(QRegularExpression("[^a-zA-Z0-9_]"), "_");
}
    QStringList recentNotebookPaths;
    const int MAX_RECENT_NOTEBOOKS = 16;
    QSettings settings;
};

class QPushButton {
    Q_OBJECT

public:
    explicit RecentNotebooksDialog(MainWindow *mainWindow, RecentNotebooksManager *manager, QWidget *parent = nullptr);
  private:
    void onNotebookClicked() {
    QPushButton *button = qobject_cast<QPushButton*>(sender());
    if (button) {
        QString notebookPath = button->property("notebookPath").toString();
        if (!notebookPath.isEmpty() && mainWindowRef) {
            // This logic is similar to MainWindow::selectFolder but directly sets the folder.
            InkCanvas *canvas = mainWindowRef->currentCanvas(); // Get current canvas from MainWindow
            if (canvas) {
                if (canvas->isEdited()){
                     mainWindowRef->saveCurrentPage(); // Use MainWindow's save method
                }
                canvas->setSaveFolder(notebookPath);
                mainWindowRef->switchPageWithDirection(1, 1); // Use MainWindow's direction-aware switchPage method
                mainWindowRef->pageInput->setValue(1); // Update pageInput in MainWindow
                mainWindowRef->updateTabLabel(); // Update tab label in MainWindow

                // Update recent notebooks list and refresh cover page
                if (notebookManager) {
                    // Generate and save fresh cover preview
                    notebookManager->generateAndSaveCoverPreview(notebookPath, canvas);
                    // Add/update in recent list (this moves it to the top)
                    notebookManager->addRecentNotebook(notebookPath, canvas);
                }
            }
            accept(); // Close the dialog
        }
    }
}
  private:
    void setupUi() {
    QVBoxLayout *mainLayout = new QVBoxLayout(this);
    QScrollArea *scrollArea = new QScrollArea(this);
    scrollArea->setWidgetResizable(true);
    QWidget *gridContainer = new QWidget();
    gridLayout = new QGridLayout(gridContainer);
    gridLayout->setSpacing(15);

    scrollArea->setWidget(gridContainer);
    mainLayout->addWidget(scrollArea);
    setLayout(mainLayout);
}
    void populateGrid() {
    QStringList recentPaths = notebookManager->getRecentNotebooks();
    int row = 0;
    int col = 0;
    const int numCols = 4;

    for (const QString &path : recentPaths) {
        if (path.isEmpty()) continue;

        QPushButton *button = new QPushButton(this);
        button->setFixedSize(180, 180); // Larger buttons
        button->setProperty("notebookPath", path); // Store path for slot
        connect(button, &QPushButton::clicked, this, &RecentNotebooksDialog::onNotebookClicked);

        QVBoxLayout *buttonLayout = new QVBoxLayout(button);
        buttonLayout->setContentsMargins(5,5,5,5);

        QLabel *coverLabel = new QLabel(this);
        coverLabel->setFixedSize(170, 127); // 4:3 aspect ratio for cover
        coverLabel->setAlignment(Qt::AlignCenter);
        QString coverPath = notebookManager->getCoverImagePathForNotebook(path);
        if (!coverPath.isEmpty()) {
            QPixmap coverPixmap(coverPath);
            coverLabel->setPixmap(coverPixmap.scaled(coverLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
        } else {
            coverLabel->setText(tr("No Preview"));
        }
        coverLabel->setStyleSheet("border: 1px solid gray;");

        QLabel *nameLabel = new QLabel(notebookManager->getNotebookDisplayName(path), this);
        nameLabel->setAlignment(Qt::AlignCenter);
        nameLabel->setWordWrap(true);
        nameLabel->setFixedHeight(40); // Fixed height for name label

        buttonLayout->addWidget(coverLabel);
        buttonLayout->addWidget(nameLabel);
        button->setLayout(buttonLayout);

        gridLayout->addWidget(button, row, col);

        col++;
        if (col >= numCols) {
            col = 0;
            row++;
        }
    }
}
    RecentNotebooksManager *notebookManager;
    QGridLayout *gridLayout;
    MainWindow *mainWindowRef;
};

class LineNumArea {
    Q_OBJECT
    Q_PROPERTY(
        bool highlighting READ highlightingEnabled WRITE setHighlightingEnabled);
    friend class LineNumArea;
  public:
    enum AutoTextOption {
        None = 0x0000,

        // inserts closing characters for brackets and Markdown characters
        BracketClosing = 0x0001,

        // removes matching brackets and Markdown characters
        BracketRemoval = 0x0002
    };
    Q_DECLARE_FLAGS(AutoTextOptions, AutoTextOption);
    explicit QMarkdownTextEdit(QWidget *parent = nullptr,
                               bool initHighlighter = true) {
    installEventFilter(this);
    viewport()->installEventFilter(this);
    _autoTextOptions = AutoTextOption::BracketClosing;

    _lineNumArea = new LineNumArea(this);
    updateLineNumberAreaWidth(0);

    // Markdown highlighting is enabled by default
    _highlightingEnabled = initHighlighter;
    if (initHighlighter) {
        _highlighter = new MarkdownHighlighter(document());
    }

    QFont font = this->font();

    // set the tab stop to the width of 4 spaces in the editor
    constexpr int tabStop = 4;
    QFontMetrics metrics(font);

#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
    setTabStopWidth(tabStop * metrics.width(' '));
#else
    setTabStopDistance(tabStop * metrics.horizontalAdvance(QLatin1Char(' ')));
#endif

    // add shortcuts for duplicating text
    //    new QShortcut( QKeySequence( "Ctrl+D" ), this, SLOT( duplicateText() )
    //    ); new QShortcut( QKeySequence( "Ctrl+Alt+Down" ), this, SLOT(
    //    duplicateText() ) );

    // add a layout to the widget
    auto *layout = new QVBoxLayout(this);
    layout->setContentsMargins(0, 0, 0, 0);
    layout->addStretch();
    this->setLayout(layout);

    // add the hidden search widget
    _searchWidget = new QPlainTextEditSearchWidget(this);
    this->layout()->addWidget(_searchWidget);

    connect(this, &QPlainTextEdit::textChanged, this,
            &QMarkdownTextEdit::adjustRightMargin);
    connect(this, &QPlainTextEdit::cursorPositionChanged, this,
            &QMarkdownTextEdit::centerTheCursor);
    connect(verticalScrollBar(), &QScrollBar::valueChanged, this,
            [this](int) { _lineNumArea->update(); });
    connect(this, &QPlainTextEdit::cursorPositionChanged, this, [this]() {
        _lineNumArea->update();

        auto oldArea = blockBoundingGeometry(_textCursor.block())
                           .translated(contentOffset());
        _textCursor = textCursor();
        auto newArea = blockBoundingGeometry(_textCursor.block())
                           .translated(contentOffset());
        auto areaToUpdate = oldArea | newArea;
        viewport()->update(areaToUpdate.toRect());
    });
    connect(document(), &QTextDocument::blockCountChanged, this,
            &QMarkdownTextEdit::updateLineNumberAreaWidth);
    connect(this, &QPlainTextEdit::updateRequest, this,
            &QMarkdownTextEdit::updateLineNumberArea);

    updateSettings();

    // workaround for disabled signals up initialization
    QTimer::singleShot(300, this, &QMarkdownTextEdit::adjustRightMargin);
}
    MarkdownHighlighter *highlighter();
    QPlainTextEditSearchWidget *searchWidget();
    void setIgnoredClickUrlSchemata(QStringList ignoredUrlSchemata) {
    _ignoredClickUrlSchemata = std::move(ignoredUrlSchemata);
}
    virtual void openUrl(const QString &urlString) {
    qDebug() << "QMarkdownTextEdit " << __func__
             << " - 'urlString': " << urlString;

    QDesktopServices::openUrl(QUrl(urlString));
}
    QString getMarkdownUrlAtPosition(const QString &text, int position) {
    QString url;

    // get a map of parsed Markdown urls with their link texts as key
    const QMap<QString, QString> urlMap = parseMarkdownUrlsFromText(text);
    QMap<QString, QString>::const_iterator i = urlMap.constBegin();
    for (; i != urlMap.constEnd(); ++i) {
        const QString &linkText = i.key();
        const QString &urlString = i.value();

        const int foundPositionStart = text.indexOf(linkText);

        if (foundPositionStart >= 0) {
            // calculate end position of found linkText
            const int foundPositionEnd = foundPositionStart + linkText.size();

            // check if position is in found string range
            if ((position >= foundPositionStart) &&
                (position <= foundPositionEnd)) {
                url = urlString;
                break;
            }
        }
    }

    return url;
}
    void initSearchFrame(QWidget *searchFrame, bool darkMode = false) {
    _searchFrame = searchFrame;

    // remove the search widget from our layout
    layout()->removeWidget(_searchWidget);

    QLayout *layout = _searchFrame->layout();

    // create a grid layout for the frame and add the search widget to it
    if (layout == nullptr) {
        layout = new QVBoxLayout(_searchFrame);
        layout->setSpacing(0);
        layout->setContentsMargins(0, 0, 0, 0);
    }

    _searchWidget->setDarkMode(darkMode);
    layout->addWidget(_searchWidget);
    _searchFrame->setLayout(layout);
}
    void setAutoTextOptions(AutoTextOptions options) {
    _autoTextOptions = options;
}
    static bool isValidUrl(const QString &urlString) {
    static QRegularExpression regex(R"(^\w+:\/\/.+)");
    const QRegularExpressionMatch match = regex.match(urlString);
    return match.hasMatch();
}
    void resetMouseCursor() const {
    QWidget *viewPort = viewport();
    viewPort->setCursor(Qt::IBeamCursor);
}
    void setReadOnly(bool ro) {
    QPlainTextEdit::setReadOnly(ro);

    // attempted to fix a problem with Chinese and Japanese input methods
    // @see https://github.com/pbek/QOwnNotes/issues/976
    setAttribute(Qt::WA_InputMethodEnabled, !isReadOnly());
}
    void doSearch(QString &searchText,
                  QPlainTextEditSearchWidget::SearchMode searchMode =
                      QPlainTextEditSearchWidget::SearchMode::PlainTextMode) {
    _searchWidget->setSearchText(searchText);
    _searchWidget->setSearchMode(searchMode);
    _searchWidget->doSearchCount();
    _searchWidget->activate(false);
}
    void hideSearchWidget(bool reset) {
    _searchWidget->deactivate();

    if (reset) {
        _searchWidget->reset();
    }
}
    void updateSettings() {
    // if true: centers the screen if cursor reaches bottom (but not top)
    searchWidget()->setDebounceDelay(_debounceDelay);
    setCenterOnScroll(_centerCursor);
}
    void setLineNumbersCurrentLineColor(QColor color) {
    _lineNumArea->setCurrentLineColor(std::move(color));
}
    void setLineNumbersOtherLineColor(QColor color) {
    _lineNumArea->setOtherLineColor(std::move(color));
}
    void setSearchWidgetDebounceDelay(uint debounceDelay) {
    _debounceDelay = debounceDelay;
    searchWidget()->setDebounceDelay(_debounceDelay);
}
    void setHighlightingEnabled(bool enabled) {
    if (_highlightingEnabled == enabled || _highlighter == nullptr) {
        return;
    }

    _highlightingEnabled = enabled;
    _highlighter->setDocument(enabled ? document() : Q_NULLPTR);

    if (enabled) {
        _highlighter->rehighlight();
    }
}
    [[nodiscard]] bool highlightingEnabled() const {
    return _highlightingEnabled && _highlighter != nullptr;
}
    void setHighlightCurrentLine(bool set) {
    _highlightCurrentLine = set;
}
    bool highlightCurrentLine() { return _highlightCurrentLine; }
    void setCurrentLineHighlightColor(const QColor &c) {
    _currentLineHighlightColor = color;
}
    QColor currentLineHighlightColor() {
    return _currentLineHighlightColor;
}
  public:
    void duplicateText() {
    if (isReadOnly()) {
        return;
    }

    QTextCursor cursor = this->textCursor();
    QString selectedText = cursor.selectedText();

    // duplicate line if no text was selected
    if (selectedText.isEmpty()) {
        const int position = cursor.position();

        // select the whole line
        cursor.movePosition(QTextCursor::StartOfBlock);
        cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);

        const int positionDiff = cursor.position() - position;
        selectedText = "\n" + cursor.selectedText();

        // insert text with new line at end of the selected line
        cursor.setPosition(cursor.selectionEnd());
        cursor.insertText(selectedText);

        // set the position to same position it was in the duplicated line
        cursor.setPosition(cursor.position() - positionDiff);
    } else {
        // duplicate selected text
        cursor.setPosition(cursor.selectionEnd());
        const int selectionStart = cursor.position();

        // insert selected text
        cursor.insertText(selectedText);
        const int selectionEnd = cursor.position();

        // select the inserted text
        cursor.setPosition(selectionStart);
        cursor.setPosition(selectionEnd, QTextCursor::KeepAnchor);
    }

    this->setTextCursor(cursor);
}
    void setText(const QString &text) { setPlainText(text); }
    void setPlainText(const QString &text) {
    // clear the dirty blocks vector to increase performance and prevent
    // a possible crash in QSyntaxHighlighter::rehighlightBlock
    if (_highlighter) _highlighter->clearDirtyBlocks();

    QPlainTextEdit::setPlainText(text);
    adjustRightMargin();
}
    void adjustRightMargin() {
    QMargins margins = layout()->contentsMargins();
    const int rightMargin =
        document()->size().height() > viewport()->size().height() ? 24 : 0;
    margins.setRight(rightMargin);
    layout()->setContentsMargins(margins);
}
    void hide() {
    _searchWidget->hide();
    QWidget::hide();
}
    bool openLinkAtCursorPosition() {
    QTextCursor cursor = this->textCursor();
    const int clickedPosition = cursor.position();

    // select the text in the clicked block and find out on
    // which position we clicked
    cursor.movePosition(QTextCursor::StartOfBlock);
    const int positionFromStart = clickedPosition - cursor.position();
    cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);

    const QString selectedText = cursor.selectedText();

    // find out which url in the selected text was clicked
    const QString urlString =
        getMarkdownUrlAtPosition(selectedText, positionFromStart);
    const QUrl url = QUrl(urlString);
    const bool isRelativeFileUrl =
        urlString.startsWith(QLatin1String("file://.."));
    const bool isLegacyAttachmentUrl =
        urlString.startsWith(QLatin1String("file://attachments"));

    qDebug() << __func__ << " - 'emit urlClicked( urlString )': " << urlString;

    Q_EMIT urlClicked(urlString);

    if ((url.isValid() && isValidUrl(urlString)) || isRelativeFileUrl ||
        isLegacyAttachmentUrl) {
        // ignore some schemata
        if (!(_ignoredClickUrlSchemata.contains(url.scheme()) ||
              isRelativeFileUrl || isLegacyAttachmentUrl)) {
            // open the url
            openUrl(urlString);
        }

        return true;
    }

    return false;
}
    bool handleBackspaceEntered() {
    if (!(_autoTextOptions & AutoTextOption::BracketRemoval) || isReadOnly()) {
        return false;
    }

    QTextCursor cursor = textCursor();

    // return if some text was selected
    if (!cursor.selectedText().isEmpty()) {
        return false;
    }

    int position = cursor.position();
    const int positionInBlock = cursor.positionInBlock();
    int block = cursor.block().blockNumber();

    if (_highlighter)
        if (_highlighter->isPosInACodeSpan(block, positionInBlock - 1))
            return false;

    // return if backspace was pressed at the beginning of a block
    if (positionInBlock == 0) {
        return false;
    }

    // get the current text from the block
    const QString text = cursor.block().text();

    char charToRemove{};

    // current char
    const char charInFront = text.at(positionInBlock - 1).toLatin1();

    if (charInFront == '*')
        return handleCharRemoval(MarkdownHighlighter::RangeType::Emphasis,
                                 block, positionInBlock - 1);
    else if (charInFront == '`')
        return handleCharRemoval(MarkdownHighlighter::RangeType::CodeSpan,
                                 block, positionInBlock - 1);

    // handle removal of ", ', and brackets

    // is it opener?
    int pos = _openingCharacters.indexOf(charInFront);
    // for " and '
    bool isOpener = false;
    bool isCloser = false;
    if (pos == 5 || pos == 6) {
        isOpener = isQuotOpener(positionInBlock - 1, text);
    } else {
        isOpener = pos != -1;
    }
    if (isOpener) {
        charToRemove = _closingCharacters.at(pos);
    } else {
        // is it closer?
        pos = _closingCharacters.indexOf(charInFront);
        if (pos == 5 || pos == 6)
            isCloser = isQuotCloser(positionInBlock - 1, text);
        else
            isCloser = pos != -1;
        if (isCloser)
            charToRemove = _openingCharacters.at(pos);
        else
            return false;
    }

    int charToRemoveIndex = -1;
    if (isOpener) {
        bool closer = true;
        charToRemoveIndex = text.indexOf(charToRemove, positionInBlock);
        if (charToRemoveIndex == -1) return false;
        if (pos == 5 || pos == 6)
            closer = isQuotCloser(charToRemoveIndex, text);
        if (!closer) return false;
        cursor.setPosition(position + (charToRemoveIndex - positionInBlock));
        cursor.deleteChar();
    } else if (isCloser) {
        charToRemoveIndex = text.lastIndexOf(charToRemove, positionInBlock - 2);
        if (charToRemoveIndex == -1) return false;
        bool opener = true;
        if (pos == 5 || pos == 6)
            opener = isQuotOpener(charToRemoveIndex, text);
        if (!opener) return false;
        const int pos = position - (positionInBlock - charToRemoveIndex);
        cursor.setPosition(pos);
        cursor.deleteChar();
        position -= 1;
    } else {
        charToRemoveIndex = text.lastIndexOf(charToRemove, positionInBlock - 2);
        if (charToRemoveIndex == -1) return false;
        const int pos = position - (positionInBlock - charToRemoveIndex);
        cursor.setPosition(pos);
        cursor.deleteChar();
        position -= 1;
    }

    // moving the cursor back to the old position so the previous character
    // can be removed
    cursor.setPosition(position);
    setTextCursor(cursor);
    return false;
}
    void centerTheCursor() {
    if (_mouseButtonDown || !_centerCursor) {
        return;
    }

    // Centers the cursor every time, but not on the top and bottom,
    // bottom is done by setCenterOnScroll() in updateSettings()
    centerCursor();

    /*
        QRect cursor = cursorRect();
        QRect vp = viewport()->rect();

        qDebug() << __func__ << " - 'cursor.top': " << cursor.top();
        qDebug() << __func__ << " - 'cursor.bottom': " << cursor.bottom();
        qDebug() << __func__ << " - 'vp': " << vp.bottom();

        int bottom = 0;
        int top = 0;

        qDebug() << __func__ << " - 'viewportMargins().top()': "
                 << viewportMargins().top();

        qDebug() << __func__ << " - 'viewportMargins().bottom()': "
                 << viewportMargins().bottom();

        int vpBottom = viewportMargins().top() + viewportMargins().bottom() +
    vp.bottom(); int vpCenter = vpBottom / 2; int cBottom = cursor.bottom() +
    viewportMargins().top();

        qDebug() << __func__ << " - 'vpBottom': " << vpBottom;
        qDebug() << __func__ << " - 'vpCenter': " << vpCenter;
        qDebug() << __func__ << " - 'cBottom': " << cBottom;

        if (cBottom >= vpCenter) {
            bottom = cBottom + viewportMargins().top() / 2 +
    viewportMargins().bottom() / 2 - (vp.bottom() / 2);
    //        bottom = cBottom - (vp.bottom() / 2);
    //        bottom *= 1.5;
        }

    //    setStyleSheet(QString("QPlainTextEdit {padding-bottom:
    %1px;}").arg(QString::number(bottom)));

    //    if (cursor.top() < (vp.bottom() / 2)) {
    //        top = (vp.bottom() / 2) - cursor.top() + viewportMargins().top() /
    2 + viewportMargins().bottom() / 2;
    ////        top *= -1;
    ////        bottom *= 1.5;
    //    }
        qDebug() << __func__ << " - 'top': " << top;
        qDebug() << __func__ << " - 'bottom': " << bottom;
        setViewportMargins(0,top,0, bottom);

    //    QScrollBar* scrollbar = verticalScrollBar();
    //
    //    qDebug() << __func__ << " - 'scrollbar->value();': " <<
    scrollbar->value();;
    //    qDebug() << __func__ << " - 'scrollbar->maximum();': "
    //             << scrollbar->maximum();;

    //    scrollbar->setValue(scrollbar->value() - offset.y());
    //
    //    setViewportMargins

    //    setViewportMargins(0, 0, 0, bottom);
    */
}
    void undo() {
    if (isReadOnly()) {
        return;
    }

    QTextCursor cursor = textCursor();
    // if no text selected, call undo
    if (!cursor.hasSelection()) {
        QPlainTextEdit::undo();
        return;
    }

    // if text is selected and bracket closing was used
    // we retain our selection
    if (_handleBracketClosingUsed) {
        // get the selection
        int selectionEnd = cursor.selectionEnd();
        int selectionStart = cursor.selectionStart();
        // call undo
        QPlainTextEdit::undo();
        // select again
        cursor.setPosition(selectionStart - 1);
        cursor.setPosition(selectionEnd - 1, QTextCursor::KeepAnchor);
        this->setTextCursor(cursor);
        _handleBracketClosingUsed = false;
    } else {
        // if text was selected but bracket closing wasn't used
        // do normal undo
        QPlainTextEdit::undo();
        return;
    }
}
    void moveTextUpDown(bool up) {
    if (isReadOnly()) {
        return;
    }

    QTextCursor cursor = textCursor();
    QTextCursor move = cursor;

    move.setVisualNavigation(false);

    move.beginEditBlock();    // open an edit block to keep undo operations sane
    bool hasSelection = cursor.hasSelection();

    if (hasSelection) {
        // if there's a selection inside the block, we select the whole block
        move.setPosition(cursor.selectionStart());
        move.movePosition(QTextCursor::StartOfBlock);
        move.setPosition(cursor.selectionEnd(), QTextCursor::KeepAnchor);
        move.movePosition(
            move.atBlockStart() ? QTextCursor::Left : QTextCursor::EndOfBlock,
            QTextCursor::KeepAnchor);
    } else {
        move.movePosition(QTextCursor::StartOfBlock);
        move.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
    }

    // get the text of the current block
    QString text = move.selectedText();

    move.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor);
    move.removeSelectedText();

    if (up) {    // up key
        move.movePosition(QTextCursor::PreviousBlock);
        move.insertBlock();
        move.movePosition(QTextCursor::Left);
    } else {    // down key
        move.movePosition(QTextCursor::EndOfBlock);
        if (move.atBlockStart()) {    // empty block
            move.movePosition(QTextCursor::NextBlock);
            move.insertBlock();
            move.movePosition(QTextCursor::Left);
        } else {
            move.insertBlock();
        }
    }

    int start = move.position();
    move.clearSelection();
    move.insertText(text);
    int end = move.position();

    // reselect
    if (hasSelection) {
        move.setPosition(end);
        move.setPosition(start, QTextCursor::KeepAnchor);
    } else {
        move.setPosition(start);
    }

    move.endEditBlock();

    setTextCursor(move);
}
    void setLineNumberEnabled(bool enabled) {
    _lineNumArea->setLineNumAreaEnabled(enabled);
    updateLineNumberAreaWidth(0);
}
  protected:
    QTextCursor _textCursor;
    MarkdownHighlighter *_highlighter = nullptr;
    bool _highlightingEnabled;
    QStringList _ignoredClickUrlSchemata;
    QPlainTextEditSearchWidget *_searchWidget;
    QWidget *_searchFrame;
    AutoTextOptions _autoTextOptions;
    bool _mouseButtonDown = false;
    bool _centerCursor = false;
    bool _highlightCurrentLine = false;
    QColor _currentLineHighlightColor = QColor();
    uint _debounceDelay = 0;
    bool eventFilter(QObject *obj, QEvent *event) override {
    // qDebug() << event->type();
    if (event->type() == QEvent::HoverMove) {
        auto *mouseEvent = static_cast<QMouseEvent *>(event);

        QWidget *viewPort = this->viewport();
        // toggle cursor when control key has been pressed or released
        viewPort->setCursor(
            mouseEvent->modifiers().testFlag(Qt::ControlModifier)
                ? Qt::PointingHandCursor
                : Qt::IBeamCursor);
    } else if (event->type() == QEvent::KeyPress) {
        auto *keyEvent = static_cast<QKeyEvent *>(event);

        // set cursor to pointing hand if control key was pressed
        if (keyEvent->modifiers().testFlag(Qt::ControlModifier)) {
            QWidget *viewPort = this->viewport();
            viewPort->setCursor(Qt::PointingHandCursor);
        }

        // disallow keys if text edit hasn't focus
        if (!this->hasFocus()) {
            return true;
        }

        if ((keyEvent->key() == Qt::Key_Escape) && _searchWidget->isVisible()) {
            _searchWidget->deactivate();
            return true;
        } else if (keyEvent->key() == Qt::Key_Insert &&
                   keyEvent->modifiers().testFlag(Qt::NoModifier)) {
            setOverwriteMode(!overwriteMode());

            // This solves a UI glitch if the visual cursor was not properly
            // updated when characters have different widths
            QTextCursor cursor = this->textCursor();
            cursor.movePosition(QTextCursor::Right);
            setTextCursor(cursor);
            cursor.movePosition(QTextCursor::Left);
            setTextCursor(cursor);

            return false;
        } else if ((keyEvent->key() == Qt::Key_Tab) ||
                   (keyEvent->key() == Qt::Key_Backtab)) {
            // handle entered tab and reverse tab keys
            return handleTabEntered(keyEvent->key() == Qt::Key_Backtab);
        } else if ((keyEvent->key() == Qt::Key_F) &&
                   keyEvent->modifiers().testFlag(Qt::ControlModifier)) {
            _searchWidget->activate();
            return true;
        } else if ((keyEvent->key() == Qt::Key_R) &&
                   keyEvent->modifiers().testFlag(Qt::ControlModifier)) {
            _searchWidget->activateReplace();
            return true;
            //        } else if (keyEvent->key() == Qt::Key_Delete) {
        } else if (keyEvent->key() == Qt::Key_Backspace) {
            return handleBackspaceEntered();
        } else if (keyEvent->key() == Qt::Key_Asterisk) {
            return handleBracketClosing(QLatin1Char('*'));
        } else if (keyEvent->key() == Qt::Key_QuoteDbl) {
            return quotationMarkCheck(QLatin1Char('"'));
            // apostrophe bracket closing is temporary disabled because
            // apostrophes are used in different contexts
            //        } else if (keyEvent->key() == Qt::Key_Apostrophe) {
            //            return handleBracketClosing("'");
            // underline bracket closing is temporary disabled because
            // underlines are used in different contexts
            //        } else if (keyEvent->key() == Qt::Key_Underscore) {
            //            return handleBracketClosing("_");
        } else if (keyEvent->key() == Qt::Key_QuoteLeft) {
            return quotationMarkCheck(QLatin1Char('`'));
        } else if (keyEvent->key() == Qt::Key_AsciiTilde) {
            return handleBracketClosing(QLatin1Char('~'));
#ifdef Q_OS_MAC
        } else if (keyEvent->modifiers().testFlag(Qt::AltModifier) &&
                   keyEvent->key() == Qt::Key_ParenLeft) {
            // bracket closing for US keyboard on macOS
            return handleBracketClosing(QLatin1Char('{'), QLatin1Char('}'));
#endif
        } else if (keyEvent->key() == Qt::Key_ParenLeft) {
            return handleBracketClosing(QLatin1Char('('), QLatin1Char(')'));
        } else if (keyEvent->key() == Qt::Key_BraceLeft) {
            return handleBracketClosing(QLatin1Char('{'), QLatin1Char('}'));
        } else if (keyEvent->key() == Qt::Key_BracketLeft) {
            return handleBracketClosing(QLatin1Char('['), QLatin1Char(']'));
        } else if (keyEvent->key() == Qt::Key_Less) {
            return handleBracketClosing(QLatin1Char('<'), QLatin1Char('>'));
#ifdef Q_OS_MAC
        } else if (keyEvent->modifiers().testFlag(Qt::AltModifier) &&
                   keyEvent->key() == Qt::Key_ParenRight) {
            // bracket closing for US keyboard on macOS
            return bracketClosingCheck(QLatin1Char('{'), QLatin1Char('}'));
#endif
        } else if (keyEvent->key() == Qt::Key_ParenRight) {
            return bracketClosingCheck(QLatin1Char('('), QLatin1Char(')'));
        } else if (keyEvent->key() == Qt::Key_BraceRight) {
            return bracketClosingCheck(QLatin1Char('{'), QLatin1Char('}'));
        } else if (keyEvent->key() == Qt::Key_BracketRight) {
            return bracketClosingCheck(QLatin1Char('['), QLatin1Char(']'));
        } else if (keyEvent->key() == Qt::Key_Greater) {
            return bracketClosingCheck(QLatin1Char('<'), QLatin1Char('>'));
        } else if ((keyEvent->key() == Qt::Key_Return ||
                    keyEvent->key() == Qt::Key_Enter) &&
                   !isReadOnly() &&
                   keyEvent->modifiers().testFlag(Qt::ShiftModifier)) {
            QTextCursor cursor = this->textCursor();
            cursor.insertText("  \n");
            return true;
        } else if ((keyEvent->key() == Qt::Key_Return ||
                    keyEvent->key() == Qt::Key_Enter) &&
                   !isReadOnly() &&
                   keyEvent->modifiers().testFlag(Qt::ControlModifier)) {
            QTextCursor cursor = this->textCursor();
            cursor.movePosition(QTextCursor::EndOfBlock);
            cursor.insertText(QStringLiteral("\n"));
            setTextCursor(cursor);
            return true;
        } else if (keyEvent == QKeySequence::Copy ||
                   keyEvent == QKeySequence::Cut) {
            QTextCursor cursor = this->textCursor();
            if (!cursor.hasSelection()) {
                QString text;
                if (cursor.block().length() <= 1)    // no content
                    text = "\n";
                else {
                    // cursor.select(QTextCursor::BlockUnderCursor); //
                    // negative, it will include the previous paragraph
                    // separator
                    cursor.movePosition(QTextCursor::StartOfBlock);
                    cursor.movePosition(QTextCursor::EndOfBlock,
                                        QTextCursor::KeepAnchor);
                    text = cursor.selectedText();
                    if (!cursor.atEnd()) {
                        text += "\n";
                        // this is the paragraph separator
                        cursor.movePosition(QTextCursor::NextCharacter,
                                            QTextCursor::KeepAnchor, 1);
                    }
                }
                if (keyEvent == QKeySequence::Cut) {
                    if (!cursor.atEnd() && text == "\n")
                        cursor.deletePreviousChar();
                    else
                        cursor.removeSelectedText();
                    cursor.movePosition(QTextCursor::StartOfBlock);
                    setTextCursor(cursor);
                }
                qApp->clipboard()->setText(text);
                return true;
            }
        } else if ((keyEvent->key() == Qt::Key_Down) &&
                   keyEvent->modifiers().testFlag(Qt::ControlModifier) &&
                   keyEvent->modifiers().testFlag(Qt::AltModifier)) {
            // duplicate text with `Ctrl + Alt + Down`
            duplicateText();
            return true;
#ifndef Q_OS_MAC
        } else if ((keyEvent->key() == Qt::Key_Down) &&
                   keyEvent->modifiers().testFlag(Qt::ControlModifier) &&
                   !keyEvent->modifiers().testFlag(Qt::ShiftModifier)) {
            // scroll the page down
            auto *scrollBar = verticalScrollBar();
            scrollBar->setSliderPosition(scrollBar->sliderPosition() + 1);
            return true;
        } else if ((keyEvent->key() == Qt::Key_Up) &&
                   keyEvent->modifiers().testFlag(Qt::ControlModifier) &&
                   !keyEvent->modifiers().testFlag(Qt::ShiftModifier)) {
            // scroll the page up
            auto *scrollBar = verticalScrollBar();
            scrollBar->setSliderPosition(scrollBar->sliderPosition() - 1);
            return true;
#endif
        } else if ((keyEvent->key() == Qt::Key_Down) &&
                   keyEvent->modifiers().testFlag(Qt::NoModifier)) {
            // if you are in the last line and press cursor down the cursor will
            // jump to the end of the line
            QTextCursor cursor = textCursor();
            if (cursor.position() >= document()->lastBlock().position()) {
                cursor.movePosition(QTextCursor::EndOfLine);

                // check if we are really in the last line, not only in
                // the last block
                if (cursor.atBlockEnd()) {
                    setTextCursor(cursor);
                }
            }
            return QPlainTextEdit::eventFilter(obj, event);
        } else if ((keyEvent->key() == Qt::Key_Up) &&
                   keyEvent->modifiers().testFlag(Qt::NoModifier)) {
            // if you are in the first line and press cursor up the cursor will
            // jump to the start of the line
            QTextCursor cursor = textCursor();
            QTextBlock block = document()->firstBlock();
            int endOfFirstLinePos = block.position() + block.length();

            if (cursor.position() <= endOfFirstLinePos) {
                cursor.movePosition(QTextCursor::StartOfLine);

                // check if we are really in the first line, not only in
                // the first block
                if (cursor.atBlockStart()) {
                    setTextCursor(cursor);
                }
            }
            return QPlainTextEdit::eventFilter(obj, event);
        } else if (keyEvent->key() == Qt::Key_Return ||
                   keyEvent->key() == Qt::Key_Enter) {
            return handleReturnEntered();
        } else if ((keyEvent->key() == Qt::Key_F3)) {
            _searchWidget->doSearch(
                !keyEvent->modifiers().testFlag(Qt::ShiftModifier));
            return true;
        } else if ((keyEvent->key() == Qt::Key_Z) &&
                   (keyEvent->modifiers().testFlag(Qt::ControlModifier)) &&
                   !(keyEvent->modifiers().testFlag(Qt::ShiftModifier))) {
            undo();
            return true;
        } else if ((keyEvent->key() == Qt::Key_Down) &&
                   (keyEvent->modifiers().testFlag(Qt::ControlModifier)) &&
                   (keyEvent->modifiers().testFlag(Qt::ShiftModifier))) {
            moveTextUpDown(false);
            return true;
        } else if ((keyEvent->key() == Qt::Key_Up) &&
                   (keyEvent->modifiers().testFlag(Qt::ControlModifier)) &&
                   (keyEvent->modifiers().testFlag(Qt::ShiftModifier))) {
            moveTextUpDown(true);
            return true;
#ifdef Q_OS_MAC
            // https://github.com/pbek/QOwnNotes/issues/1593
            // https://github.com/pbek/QOwnNotes/issues/2643
        } else if (keyEvent->key() == Qt::Key_Home) {
            QTextCursor cursor = textCursor();
            // Meta is Control on macOS
            cursor.movePosition(
                keyEvent->modifiers().testFlag(Qt::MetaModifier)
                    ? QTextCursor::Start
                    : QTextCursor::StartOfLine,
                keyEvent->modifiers().testFlag(Qt::ShiftModifier)
                    ? QTextCursor::KeepAnchor
                    : QTextCursor::MoveAnchor);
            this->setTextCursor(cursor);
            return true;
        } else if (keyEvent->key() == Qt::Key_End) {
            QTextCursor cursor = textCursor();
            // Meta is Control on macOS
            cursor.movePosition(
                keyEvent->modifiers().testFlag(Qt::MetaModifier)
                    ? QTextCursor::End
                    : QTextCursor::EndOfLine,
                keyEvent->modifiers().testFlag(Qt::ShiftModifier)
                    ? QTextCursor::KeepAnchor
                    : QTextCursor::MoveAnchor);
            this->setTextCursor(cursor);
            return true;
#endif
        }

        return QPlainTextEdit::eventFilter(obj, event);
    } else if (event->type() == QEvent::KeyRelease) {
        auto *keyEvent = static_cast<QKeyEvent *>(event);

        // reset cursor if control key was released
        if (keyEvent->key() == Qt::Key_Control) {
            resetMouseCursor();
        }

        return QPlainTextEdit::eventFilter(obj, event);
    } else if (event->type() == QEvent::MouseButtonRelease) {
        _mouseButtonDown = false;
        auto *mouseEvent = static_cast<QMouseEvent *>(event);

        // track `Ctrl + Click` in the text edit
        if ((obj == this->viewport()) &&
            (mouseEvent->button() == Qt::LeftButton) &&
            (QGuiApplication::keyboardModifiers() == Qt::ExtraButton24)) {
            // open the link (if any) at the current position
            // in the noteTextEdit
            openLinkAtCursorPosition();
            return true;
        }
    } else if (event->type() == QEvent::MouseButtonPress) {
        _mouseButtonDown = true;
    } else if (event->type() == QEvent::MouseButtonDblClick) {
        _mouseButtonDown = true;
    } else if (event->type() == QEvent::Wheel) {
        auto *wheel = dynamic_cast<QWheelEvent *>(event);

        // emit zoom signals
        if (wheel->modifiers() == Qt::ControlModifier) {
            if (wheel->angleDelta().y() > 0) {
                Q_EMIT zoomIn();
            } else {
                Q_EMIT zoomOut();
            }

            return true;
        }
    }

    return QPlainTextEdit::eventFilter(obj, event);
}
    QMargins viewportMargins() {
    return QPlainTextEdit::viewportMargins();
}
    bool increaseSelectedTextIndention(
        bool reverse,
        const QString &indentCharacters = QChar::fromLatin1('\t')) {
    QTextCursor cursor = this->textCursor();
    QString selectedText = cursor.selectedText();

    if (!selectedText.isEmpty()) {
        // Start the selection at start of the first block of the selection
        int end = cursor.selectionEnd();
        cursor.setPosition(cursor.selectionStart());
        cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);
        cursor.setPosition(end, QTextCursor::KeepAnchor);
        this->setTextCursor(cursor);
        selectedText = cursor.selectedText();

        // we need this strange newline character we are getting in the
        // selected text for newlines
        const QString newLine =
            QString::fromUtf8(QByteArray::fromHex(QByteArrayLiteral("e280a9")));
        QString newText;

        if (reverse) {
            // un-indent text

            const int indentSize = indentCharacters == QStringLiteral("\t")
                                       ? 4
                                       : indentCharacters.length();

            // remove leading \t or spaces in following lines
            newText = selectedText.replace(
                QRegularExpression(newLine + QStringLiteral("(\\t| {1,") +
                                   QString::number(indentSize) +
                                   QStringLiteral("})")),
                QStringLiteral("\n"));

            // remove leading \t or spaces in first line
            newText.remove(QRegularExpression(QStringLiteral("^(\\t| {1,") +
                                              QString::number(indentSize) +
                                              QStringLiteral("})")));
        } else {
            // replace trailing new line to prevent an indent of the line after
            // the selection
            newText = selectedText.replace(
                QRegularExpression(QRegularExpression::escape(newLine) +
                                   QStringLiteral("$")),
                QStringLiteral("\n"));

            // indent text
            newText.replace(newLine, QStringLiteral("\n") + indentCharacters)
                .prepend(indentCharacters);

            // remove trailing \t
            static QRegularExpression regex1(QStringLiteral("\\t$"));
            newText.remove(regex1);
        }

        // insert the new text
        cursor.insertText(newText);

        // update the selection to the new text
        cursor.setPosition(cursor.position() - newText.size(),
                           QTextCursor::KeepAnchor);
        this->setTextCursor(cursor);

        return true;
    } else if (reverse) {
        const int indentSize = indentCharacters.length();

        // do the check as often as we have characters to un-indent
        for (int i = 1; i <= indentSize; i++) {
            // if nothing was selected but we want to reverse the indention
            // check if there is a \t in front or after the cursor and remove it
            // if so
            const int position = cursor.position();

            if (!cursor.atStart()) {
                // get character in front of cursor
                cursor.setPosition(position - 1, QTextCursor::KeepAnchor);
            }

            // check for \t or space in front of cursor
            static QRegularExpression regex1(QStringLiteral("[\\t ]"));
            QRegularExpressionMatch match = regex1.match(cursor.selectedText());

            if (!match.hasMatch()) {
                // (select to) check for \t or space after the cursor
                cursor.setPosition(position);

                if (!cursor.atEnd()) {
                    cursor.setPosition(position + 1, QTextCursor::KeepAnchor);
                }
            }

            match = regex1.match(cursor.selectedText());

            if (match.hasMatch()) {
                cursor.removeSelectedText();
            }

            cursor = this->textCursor();
        }

        return true;
    }

    // else just insert indentCharacters
    cursor.insertText(indentCharacters);

    return true;
}
    bool handleTabEntered(bool reverse, const QString &indentCharacters =
                                            QChar::fromLatin1('\t')) {
    if (isReadOnly()) {
        return true;
    }

    QTextCursor cursor = this->textCursor();

    // only check for lists if we haven't a text selected
    if (cursor.selectedText().isEmpty()) {
        cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor);
        const QString currentLineText = cursor.selectedText();

        // check if we want to indent or un-indent an ordered list
        // Valid listCharacters: '+ ', '-' , '* ', '+ [ ] ', '+ [x] ', '- [ ] ',
        // '- [x] ', '- [-] ', '* [ ] ', '* [x] '.
        static QRegularExpression regex1(
            R"(^(\s*)([+|\-|\*] \[(x|-| )\]|[+\-\*])(\s+)$)");
        QRegularExpressionMatchIterator i = regex1.globalMatch(currentLineText);

        if (i.hasNext()) {
            QRegularExpressionMatch match = i.next();
            QString whitespaces = match.captured(1);
            const QString listCharacter = match.captured(2);
            const QString whitespaceCharacter = match.captured(4);

            // add or remove one tabulator key
            if (reverse) {
                // remove one set of indentCharacters or a tabulator
                whitespaces.remove(QRegularExpression(
                    QStringLiteral("^(\\t|") +
                    QRegularExpression::escape(indentCharacters) +
                    QStringLiteral(")")));

            } else {
                whitespaces += indentCharacters;
            }

            cursor.insertText(whitespaces + listCharacter +
                              whitespaceCharacter);
            return true;
        }

        // check if we want to indent or un-indent an ordered list
        static QRegularExpression regex2(R"(^(\s*)(\d+)([\.|\)])(\s+)$)");
        i = regex2.globalMatch(currentLineText);

        if (i.hasNext()) {
            const QRegularExpressionMatch match = i.next();
            QString whitespaces = match.captured(1);
            const QString listCharacter = match.captured(2);
            const QString listMarker = match.captured(3);
            const QString whitespaceCharacter = match.captured(4);

            // add or remove one tabulator key
            if (reverse) {
                whitespaces.chop(1);
            } else {
                whitespaces += indentCharacters;
            }

            cursor.insertText(whitespaces + listCharacter + listMarker +
                              whitespaceCharacter);
            return true;
        }
    }

    // check if we want to indent the whole text
    return increaseSelectedTextIndention(reverse, indentCharacters);
}
    QMap<QString, QString> parseMarkdownUrlsFromText(const QString &text) {
    QMap<QString, QString> urlMap;
    QRegularExpressionMatchIterator iterator;

    // match urls like this: <http://mylink>
    //    re = QRegularExpression("(<(.+?:\\/\\/.+?)>)");
    static QRegularExpression regex1(QStringLiteral("(<(.+?)>)"));
    iterator = regex1.globalMatch(text);
    while (iterator.hasNext()) {
        QRegularExpressionMatch match = iterator.next();
        QString linkText = match.captured(1);
        QString url = match.captured(2);
        urlMap[linkText] = url;
    }

    // match urls like this: [this url](http://mylink)
    //    QRegularExpression re("(\\[.*?\\]\\((.+?:\\/\\/.+?)\\))");
    static QRegularExpression regex2(R"((\[.*?\]\((.+?)\)))");
    iterator = regex2.globalMatch(text);
    while (iterator.hasNext()) {
        QRegularExpressionMatch match = iterator.next();
        QString linkText = match.captured(1);
        QString url = match.captured(2);
        urlMap[linkText] = url;
    }

    // match urls like this: http://mylink
    static QRegularExpression regex3(R"(\b\w+?:\/\/[^\s]+[^\s>\)])");
    iterator = regex3.globalMatch(text);
    while (iterator.hasNext()) {
        QRegularExpressionMatch match = iterator.next();
        QString url = match.captured(0);
        urlMap[url] = url;
    }

    // match urls like this: www.github.com
    static QRegularExpression regex4(R"(\bwww\.[^\s]+\.[^\s]+\b)");
    iterator = regex4.globalMatch(text);
    while (iterator.hasNext()) {
        QRegularExpressionMatch match = iterator.next();
        QString url = match.captured(0);
        urlMap[url] = QStringLiteral("http://") + url;
    }

    // match reference urls like this: [this url][1] with this later:
    // [1]: http://domain
    static QRegularExpression regex5(R"((\[.*?\]\[(.+?)\]))");
    iterator = regex5.globalMatch(text);
    while (iterator.hasNext()) {
        QRegularExpressionMatch match = iterator.next();
        QString linkText = match.captured(1);
        QString referenceId = match.captured(2);

        // search for the referenced url in the whole text edit
        //        QRegularExpression refRegExp(
        //                "\\[" + QRegularExpression::escape(referenceId) +
        //                        "\\]: (.+?:\\/\\/.+)");
        QRegularExpression refRegExp(QStringLiteral("\\[") +
                                     QRegularExpression::escape(referenceId) +
                                     QStringLiteral("\\]: (.+)"));
        QRegularExpressionMatch urlMatch = refRegExp.match(toPlainText());

        if (urlMatch.hasMatch()) {
            QString url = urlMatch.captured(1);
            urlMap[linkText] = url;
        }
    }

    return urlMap;
}
    bool handleReturnEntered() {
    if (isReadOnly()) {
        return true;
    }

    // This will be the main cursor to add or remove text
    QTextCursor cursor = this->textCursor();

    // We need a 2nd cursor to get the text of the current block without moving
    // the main cursor that is used to remove the selected text
    QTextCursor cursor2 = this->textCursor();
    cursor2.select(QTextCursor::BlockUnderCursor);
    const QString currentLineText = cursor2.selectedText().trimmed();

    const int position = cursor.position();
    const bool cursorAtBlockStart = cursor.atBlockStart();
    cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor);
    const QString currentLinePartialText = cursor.selectedText();

    // if return is pressed and there is just an unordered list symbol then we
    // want to remove the list symbol Valid listCharacters: '+ ', '-' , '* ', '+
    // [ ] ', '+ [x] ', '- [ ] ', '- [-] ', '- [x] ', '* [ ] ', '* [x] '.
    static QRegularExpression regex1(
        R"(^(\s*)([+|\-|\*] \[(x|-| |)\]|[+\-\*])(\s+)$)");
    QRegularExpressionMatchIterator iterator =
        regex1.globalMatch(currentLinePartialText);
    if (iterator.hasNext()) {
        cursor.removeSelectedText();
        return true;
    }

    // if return is pressed and there is just an ordered list symbol then we
    // want to remove the list symbol
    static QRegularExpression regex2(R"(^(\s*)(\d+[\.|\)])(\s+)$)");
    iterator = regex2.globalMatch(currentLinePartialText);
    if (iterator.hasNext()) {
        qDebug() << cursor.selectedText();
        cursor.removeSelectedText();
        return true;
    }

    // Check if we are in an unordered list.
    // We are in a list when we have '* ', '- ' or '+ ', possibly with preceding
    // whitespace. If e.g. user has entered '**text**' and pressed enter - we
    // don't want to do more list-stuff.
    QString currentLine = currentLinePartialText.trimmed();
    QChar char0;
    QChar char1;
    if (currentLine.length() >= 1) char0 = currentLine.at(0);
    if (currentLine.length() >= 2) char1 = currentLine.at(1);
    const bool inList =
        ((char0 == QLatin1Char('*') || char0 == QLatin1Char('-') ||
          char0 == QLatin1Char('+')) &&
         char1 == QLatin1Char(' '));

    if (inList) {
        // if the current line starts with a list character (possibly after
        // whitespaces) add the whitespaces at the next line too
        // Valid listCharacters: '+ ', '-' , '* ', '+ [ ] ', '+ [x] ', '- [ ] ',
        // '- [x] ', '- [-] ', '* [ ] ', '* [x] '.
        static QRegularExpression regex3(
            R"(^(\s*)([+|\-|\*] \[(x|-| |)\]|[+\-\*])(\s+))");
        iterator = regex3.globalMatch(currentLinePartialText);
        if (iterator.hasNext()) {
            const QRegularExpressionMatch match = iterator.next();
            const QString whitespaces = match.captured(1);
            QString listCharacter = match.captured(2);
            const QString whitespaceCharacter = match.captured(4);

            static QRegularExpression regex4(R"(^([+|\-|\*]) \[(x| |\-|)\])");
            // start new checkbox list item with an unchecked checkbox
            iterator = regex4.globalMatch(listCharacter);
            if (iterator.hasNext()) {
                const QRegularExpressionMatch match1 = iterator.next();
                const QString realListCharacter = match1.captured(1);
                listCharacter = realListCharacter + QStringLiteral(" [ ]");
            }

            cursor.setPosition(position);
            cursor.insertText("\n" + whitespaces + listCharacter +
                              whitespaceCharacter);

            // scroll to the cursor if we are at the bottom of the document
            ensureCursorVisible();
            return true;
        }
    }

    // check for ordered lists and increment the list number in the next line
    static QRegularExpression regex5(R"(^(\s*)(\d+)([\.|\)])(\s+))");
    iterator = regex5.globalMatch(currentLinePartialText);
    if (iterator.hasNext()) {
        const QRegularExpressionMatch match = iterator.next();
        const QString whitespaces = match.captured(1);
        const uint listNumber = match.captured(2).toUInt();
        const QString listMarker = match.captured(3);
        const QString whitespaceCharacter = match.captured(4);

        cursor.setPosition(position);
        cursor.insertText("\n" + whitespaces + QString::number(listNumber + 1) +
                          listMarker + whitespaceCharacter);

        // scroll to the cursor if we are at the bottom of the document
        ensureCursorVisible();
        return true;
    }

    // intent next line with same whitespaces as in current line
    static QRegularExpression regex6(R"(^(\s+))");
    iterator = regex6.globalMatch(currentLinePartialText);
    if (iterator.hasNext()) {
        const QRegularExpressionMatch match = iterator.next();
        const QString whitespaces = match.captured(1);

        cursor.setPosition(position);
        cursor.insertText("\n" + whitespaces);

        // scroll to the cursor if we are at the bottom of the document
        ensureCursorVisible();
        return true;
    }

    // Add new list item above current line if we are at the start the line of a
    // list item
    if (cursorAtBlockStart) {
        static QRegularExpression regex7(
            R"(^([+|\-|\*] \[(x|-| |)\]|[+\-\*])(\s+))");
        iterator = regex7.globalMatch(currentLineText);
        if (iterator.hasNext()) {
            const QRegularExpressionMatch match = iterator.next();
            QString listCharacter = match.captured(1);
            const QString whitespaceCharacter = match.captured(3);

            static QRegularExpression regex8(R"(^([+|\-|\*]) \[(x| |\-|)\])");
            // start new checkbox list item with an unchecked checkbox
            iterator = regex8.globalMatch(listCharacter);
            if (iterator.hasNext()) {
                const QRegularExpressionMatch match1 = iterator.next();
                const QString realListCharacter = match1.captured(1);
                listCharacter = realListCharacter + QStringLiteral(" [ ]");
            }

            cursor.setPosition(position);
            // Enter new list item above current line
            cursor.insertText(listCharacter + whitespaceCharacter + "\n");
            // Move the cursor at the end of the new list item
            cursor.movePosition(QTextCursor::Left);
            setTextCursor(cursor);

            // scroll to the cursor if we are at the bottom of the document
            ensureCursorVisible();
            return true;
        }
    }

    return false;
}
    bool handleBracketClosing(const QChar openingCharacter,
                              QChar closingCharacter = QChar()) {
    // check if bracket closing or read-only are enabled
    if (!(_autoTextOptions & AutoTextOption::BracketClosing) || isReadOnly()) {
        return false;
    }

    QTextCursor cursor = textCursor();

    if (closingCharacter.isNull()) {
        closingCharacter = openingCharacter;
    }

    const QString selectedText = cursor.selectedText();

    // When user currently has text selected, we prepend the openingCharacter
    // and append the closingCharacter. E.g. 'text' -> '(text)'. We keep the
    // current selectedText selected.
    if (!selectedText.isEmpty()) {
        // Insert. The selectedText is overwritten.
        const QString newText =
            openingCharacter + selectedText + closingCharacter;
        cursor.insertText(newText);

        // Re-select the selectedText.
        const int selectionEnd = cursor.position() - 1;
        const int selectionStart = selectionEnd - selectedText.length();

        cursor.setPosition(selectionStart);
        cursor.setPosition(selectionEnd, QTextCursor::KeepAnchor);
        this->setTextCursor(cursor);
        _handleBracketClosingUsed = true;
        return true;
    }

    // get the current text from the block (inserted character not included)
    // Remove whitespace at start of string (e.g. in multilevel-lists).
    static QRegularExpression regex1("^\\s+");
    const QString text = cursor.block().text().remove(regex1);

    const int pib = cursor.positionInBlock();
    bool isPreviousAsterisk =
        pib > 0 && pib < text.length() && text.at(pib - 1) == '*';
    bool isNextAsterisk = pib < text.length() && text.at(pib) == '*';
    bool isMaybeBold = isPreviousAsterisk && isNextAsterisk;
    if (pib < text.length() && !isMaybeBold && !text.at(pib).isSpace()) {
        return false;
    }

    // Default positions to move the cursor back.
    int cursorSubtract = 1;
    // Special handling for `*` opening character, as this could be:
    // - start of a list (or sublist);
    // - start of a bold text;
    if (openingCharacter == QLatin1Char('*')) {
        // don't auto complete in code block
        bool isInCode =
            MarkdownHighlighter::isCodeBlock(cursor.block().userState());
        // we only do auto completion if there is a space before the cursor pos
        bool hasSpaceOrAsteriskBefore = !text.isEmpty() && pib > 0 &&
                                        (text.at(pib - 1).isSpace() ||
                                         text.at(pib - 1) == QLatin1Char('*'));
        // This could be the start of a list, don't autocomplete.
        bool isEmpty = text.isEmpty();

        if (isInCode || !hasSpaceOrAsteriskBefore || isEmpty) {
            return false;
        }

        // bold
        if (isPreviousAsterisk && isNextAsterisk) {
            cursorSubtract = 1;
        }

        // User wants: '**'.
        // Not the start of a list, probably bold text. We autocomplete with
        // extra closingCharacter and cursorSubtract to 'catchup'.
        if (text == QLatin1String("*")) {
            cursor.insertText(QStringLiteral("*"));
            cursorSubtract = 2;
        }
    }

    // Auto-completion for ``` pair
    if (openingCharacter == QLatin1Char('`')) {
#if QT_VERSION < QT_VERSION_CHECK(5, 12, 0)
        if (QRegExp(QStringLiteral("[^`]*``")).exactMatch(text)) {
#else
        if (QRegularExpression(
                QRegularExpression::anchoredPattern(QStringLiteral("[^`]*``")))
                .match(text)
                .hasMatch()) {
#endif
            cursor.insertText(QStringLiteral("``"));
            cursorSubtract = 3;
        }
    }

    // don't auto complete in code block
    if (openingCharacter == QLatin1Char('<') &&
        MarkdownHighlighter::isCodeBlock(cursor.block().userState())) {
        return false;
    }

    cursor.beginEditBlock();
    cursor.insertText(openingCharacter);
    cursor.insertText(closingCharacter);
    cursor.setPosition(cursor.position() - cursorSubtract);
    cursor.endEditBlock();

    setTextCursor(cursor);
    return true;
}

/**
 * Checks if the closing character should be output or not
 *
 * @param openingCharacter
 * @param closingCharacter
 * @return
 */
bool QMarkdownTextEdit::bracketClosingCheck(const QChar openingCharacter,
                                            QChar closingCharacter) {
    // check if bracket closing or read-only are enabled
    if (!(_autoTextOptions & AutoTextOption::BracketClosing) || isReadOnly()) {
        return false;
    }

    if (closingCharacter.isNull()) {
        closingCharacter = openingCharacter;
    }

    QTextCursor cursor = textCursor();
    const int positionInBlock = cursor.positionInBlock();

    // get the current text from the block
    const QString text = cursor.block().text();
    const int textLength = text.length();

    // if we are at the end of the line we just want to enter the character
    if (positionInBlock >= textLength) {
        return false;
    }

    const QChar currentChar = text.at(positionInBlock);

    //    if (closingCharacter == openingCharacter) {

    //    }

    qDebug() << __func__ << " - 'currentChar': " << currentChar;

    // if the current character is not the closing character we just want to
    // enter the character
    if (currentChar != closingCharacter) {
        return false;
    }

    const QString leftText = text.left(positionInBlock);
    const int openingCharacterCount = leftText.count(openingCharacter);
    const int closingCharacterCount = leftText.count(closingCharacter);

    // if there were enough opening characters just enter the character
    if (openingCharacterCount < (closingCharacterCount + 1)) {
        return false;
    }

    // move the cursor to the right and don't enter the character
    cursor.movePosition(QTextCursor::Right);
    setTextCursor(cursor);
    return true;
}

/**
 * Checks if the closing character should be output or not or if a closing
 * character after an opening character if needed
 *
 * @param quotationCharacter
 * @return
 */
bool QMarkdownTextEdit::quotationMarkCheck(const QChar quotationCharacter) {
    // check if bracket closing or read-only are enabled
    if (!(_autoTextOptions & AutoTextOption::BracketClosing) || isReadOnly()) {
        return false;
    }

    QTextCursor cursor = textCursor();
    const int positionInBlock = cursor.positionInBlock();

    // get the current text from the block
    const QString text = cursor.block().text();
    const int textLength = text.length();

    // if last char is not space, we are at word end, no autocompletion
    const bool isBacktick = quotationCharacter == '`';
    if (!isBacktick && positionInBlock != 0 &&
        !text.at(positionInBlock - 1).isSpace()) {
        return false;
    }

    // if we are at the end of the line we just want to enter the character
    if (positionInBlock >= textLength) {
        return handleBracketClosing(quotationCharacter);
    }

    const QChar currentChar = text.at(positionInBlock);

    // if the current character is not the quotation character we just want to
    // enter the character
    if (currentChar != quotationCharacter) {
        return handleBracketClosing(quotationCharacter);
    }

    // move the cursor to the right and don't enter the character
    cursor.movePosition(QTextCursor::Right);
    setTextCursor(cursor);
    return true;
}

/***********************************
 * helper methods for char removal
 * Rules for (') and ("):
 * if [sp]" -> opener (sp = space)
 * if "[sp] -> closer
 ***********************************/
bool isQuotOpener(int position, const QString &text) {
    if (position == 0) return true;
    const int prevCharPos = position - 1;
    return text.at(prevCharPos).isSpace();
}
bool isQuotCloser(int position, const QString &text) {
    const int nextCharPos = position + 1;
    if (nextCharPos >= text.length()) return true;
    return text.at(nextCharPos).isSpace();
}

/**
 * Handles removing of matching brackets and other Markdown characters
 * Only works with backspace to remove text
 *
 * @return
 */
bool QMarkdownTextEdit::handleBackspaceEntered() {
    if (!(_autoTextOptions & AutoTextOption::BracketRemoval) || isReadOnly()) {
        return false;
    }

    QTextCursor cursor = textCursor();

    // return if some text was selected
    if (!cursor.selectedText().isEmpty()) {
        return false;
    }

    int position = cursor.position();
    const int positionInBlock = cursor.positionInBlock();
    int block = cursor.block().blockNumber();

    if (_highlighter)
        if (_highlighter->isPosInACodeSpan(block, positionInBlock - 1))
            return false;

    // return if backspace was pressed at the beginning of a block
    if (positionInBlock == 0) {
        return false;
    }

    // get the current text from the block
    const QString text = cursor.block().text();

    char charToRemove{};

    // current char
    const char charInFront = text.at(positionInBlock - 1).toLatin1();

    if (charInFront == '*')
        return handleCharRemoval(MarkdownHighlighter::RangeType::Emphasis,
                                 block, positionInBlock - 1);
    else if (charInFront == '`')
        return handleCharRemoval(MarkdownHighlighter::RangeType::CodeSpan,
                                 block, positionInBlock - 1);

    // handle removal of ", ', and brackets

    // is it opener?
    int pos = _openingCharacters.indexOf(charInFront);
    // for " and '
    bool isOpener = false;
    bool isCloser = false;
    if (pos == 5 || pos == 6) {
        isOpener = isQuotOpener(positionInBlock - 1, text);
    } else {
        isOpener = pos != -1;
    }
    if (isOpener) {
        charToRemove = _closingCharacters.at(pos);
    } else {
        // is it closer?
        pos = _closingCharacters.indexOf(charInFront);
        if (pos == 5 || pos == 6)
            isCloser = isQuotCloser(positionInBlock - 1, text);
        else
            isCloser = pos != -1;
        if (isCloser)
            charToRemove = _openingCharacters.at(pos);
        else
            return false;
    }

    int charToRemoveIndex = -1;
    if (isOpener) {
        bool closer = true;
        charToRemoveIndex = text.indexOf(charToRemove, positionInBlock);
        if (charToRemoveIndex == -1) return false;
        if (pos == 5 || pos == 6)
            closer = isQuotCloser(charToRemoveIndex, text);
        if (!closer) return false;
        cursor.setPosition(position + (charToRemoveIndex - positionInBlock));
        cursor.deleteChar();
    } else if (isCloser) {
        charToRemoveIndex = text.lastIndexOf(charToRemove, positionInBlock - 2);
        if (charToRemoveIndex == -1) return false;
        bool opener = true;
        if (pos == 5 || pos == 6)
            opener = isQuotOpener(charToRemoveIndex, text);
        if (!opener) return false;
        const int pos = position - (positionInBlock - charToRemoveIndex);
        cursor.setPosition(pos);
        cursor.deleteChar();
        position -= 1;
    } else {
        charToRemoveIndex = text.lastIndexOf(charToRemove, positionInBlock - 2);
        if (charToRemoveIndex == -1) return false;
        const int pos = position - (positionInBlock - charToRemoveIndex);
        cursor.setPosition(pos);
        cursor.deleteChar();
        position -= 1;
    }

    // moving the cursor back to the old position so the previous character
    // can be removed
    cursor.setPosition(position);
    setTextCursor(cursor);
    return false;
}

bool QMarkdownTextEdit::handleCharRemoval(MarkdownHighlighter::RangeType type,
                                          int block, int position) {
    if (!_highlighter) return false;

    auto range = _highlighter->findPositionInRanges(type, block, position);
    if (range == QPair<int, int>{-1, -1}) return false;

    int charToRemovePos = range.first;
    if (position == range.first) charToRemovePos = range.second;

    QTextCursor cursor = textCursor();
    auto gpos = cursor.position();

    if (charToRemovePos > position) {
        cursor.setPosition(gpos + (charToRemovePos - (position + 1)));
    } else {
        cursor.setPosition(gpos - (position - charToRemovePos + 1));
        gpos--;
    }

    cursor.deleteChar();
    cursor.setPosition(gpos);
    setTextCursor(cursor);
    return false;
}

void QMarkdownTextEdit::updateLineNumAreaGeometry() {
    const auto contentsRect = this->contentsRect();
    const QRect newGeometry = {contentsRect.left(), contentsRect.top(),
                               _lineNumArea->sizeHint().width(),
                               contentsRect.height()};
    auto oldGeometry = _lineNumArea->geometry();
    if (newGeometry != oldGeometry) {
        _lineNumArea->setGeometry(newGeometry);
    }
}

void QMarkdownTextEdit::resizeEvent(QResizeEvent *event) {
    QPlainTextEdit::resizeEvent(event);
    updateLineNumAreaGeometry();
}

/**
 * Increases (or decreases) the indention of the selected text
 * (if there is a text selected) in the noteTextEdit
 * @return
 */
bool QMarkdownTextEdit::increaseSelectedTextIndention(
    bool reverse, const QString &indentCharacters) {
    QTextCursor cursor = this->textCursor();
    QString selectedText = cursor.selectedText();

    if (!selectedText.isEmpty()) {
        // Start the selection at start of the first block of the selection
        int end = cursor.selectionEnd();
        cursor.setPosition(cursor.selectionStart());
        cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);
        cursor.setPosition(end, QTextCursor::KeepAnchor);
        this->setTextCursor(cursor);
        selectedText = cursor.selectedText();

        // we need this strange newline character we are getting in the
        // selected text for newlines
        const QString newLine =
            QString::fromUtf8(QByteArray::fromHex(QByteArrayLiteral("e280a9")));
        QString newText;

        if (reverse) {
            // un-indent text

            const int indentSize = indentCharacters == QStringLiteral("\t")
                                       ? 4
                                       : indentCharacters.length();

            // remove leading \t or spaces in following lines
            newText = selectedText.replace(
                QRegularExpression(newLine + QStringLiteral("(\\t| {1,") +
                                   QString::number(indentSize) +
                                   QStringLiteral("})")),
                QStringLiteral("\n"));

            // remove leading \t or spaces in first line
            newText.remove(QRegularExpression(QStringLiteral("^(\\t| {1,") +
                                              QString::number(indentSize) +
                                              QStringLiteral("})")));
        } else {
            // replace trailing new line to prevent an indent of the line after
            // the selection
            newText = selectedText.replace(
                QRegularExpression(QRegularExpression::escape(newLine) +
                                   QStringLiteral("$")),
                QStringLiteral("\n"));

            // indent text
            newText.replace(newLine, QStringLiteral("\n") + indentCharacters)
                .prepend(indentCharacters);

            // remove trailing \t
            static QRegularExpression regex1(QStringLiteral("\\t$"));
            newText.remove(regex1);
        }

        // insert the new text
        cursor.insertText(newText);

        // update the selection to the new text
        cursor.setPosition(cursor.position() - newText.size(),
                           QTextCursor::KeepAnchor);
        this->setTextCursor(cursor);

        return true;
    } else if (reverse) {
        const int indentSize = indentCharacters.length();

        // do the check as often as we have characters to un-indent
        for (int i = 1; i <= indentSize; i++) {
            // if nothing was selected but we want to reverse the indention
            // check if there is a \t in front or after the cursor and remove it
            // if so
            const int position = cursor.position();

            if (!cursor.atStart()) {
                // get character in front of cursor
                cursor.setPosition(position - 1, QTextCursor::KeepAnchor);
            }

            // check for \t or space in front of cursor
            static QRegularExpression regex1(QStringLiteral("[\\t ]"));
            QRegularExpressionMatch match = regex1.match(cursor.selectedText());

            if (!match.hasMatch()) {
                // (select to) check for \t or space after the cursor
                cursor.setPosition(position);

                if (!cursor.atEnd()) {
                    cursor.setPosition(position + 1, QTextCursor::KeepAnchor);
                }
            }

            match = regex1.match(cursor.selectedText());

            if (match.hasMatch()) {
                cursor.removeSelectedText();
            }

            cursor = this->textCursor();
        }

        return true;
    }

    // else just insert indentCharacters
    cursor.insertText(indentCharacters);

    return true;
}

/**
 * @brief Opens the link (if any) at the current cursor position
 */
bool QMarkdownTextEdit::openLinkAtCursorPosition() {
    QTextCursor cursor = this->textCursor();
    const int clickedPosition = cursor.position();

    // select the text in the clicked block and find out on
    // which position we clicked
    cursor.movePosition(QTextCursor::StartOfBlock);
    const int positionFromStart = clickedPosition - cursor.position();
    cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);

    const QString selectedText = cursor.selectedText();

    // find out which url in the selected text was clicked
    const QString urlString =
        getMarkdownUrlAtPosition(selectedText, positionFromStart);
    const QUrl url = QUrl(urlString);
    const bool isRelativeFileUrl =
        urlString.startsWith(QLatin1String("file://.."));
    const bool isLegacyAttachmentUrl =
        urlString.startsWith(QLatin1String("file://attachments"));

    qDebug() << __func__ << " - 'emit urlClicked( urlString )': " << urlString;

    Q_EMIT urlClicked(urlString);

    if ((url.isValid() && isValidUrl(urlString)) || isRelativeFileUrl ||
        isLegacyAttachmentUrl) {
        // ignore some schemata
        if (!(_ignoredClickUrlSchemata.contains(url.scheme()) ||
              isRelativeFileUrl || isLegacyAttachmentUrl)) {
            // open the url
            openUrl(urlString);
        }

        return true;
    }

    return false;
}

/**
 * Checks if urlString is a valid url
 *
 * @param urlString
 * @return
 */
bool QMarkdownTextEdit::isValidUrl(const QString &urlString) {
    static QRegularExpression regex(R"(^\w+:\/\/.+)");
    const QRegularExpressionMatch match = regex.match(urlString);
    return match.hasMatch();
}

/**
 * Handles clicked urls
 *
 * examples:
 * - <https://www.qownnotes.org> opens the webpage
 * - <file:///path/to/my/file/QOwnNotes.pdf> opens the file
 *   "/path/to/my/file/QOwnNotes.pdf" if the operating system supports that
 *  handler
 */
void QMarkdownTextEdit::openUrl(const QString &urlString) {
    qDebug() << "QMarkdownTextEdit " << __func__
             << " - 'urlString': " << urlString;

    QDesktopServices::openUrl(QUrl(urlString));
}

/**
 * @brief Returns the highlighter instance
 * @return
 */
MarkdownHighlighter *QMarkdownTextEdit::highlighter() { return _highlighter; }

/**
 * @brief Returns the searchWidget instance
 * @return
 */
QPlainTextEditSearchWidget *QMarkdownTextEdit::searchWidget() {
    return _searchWidget;
}

/**
 * @brief Sets url schemata that will be ignored when clicked on
 * @param urlSchemes
 */
void QMarkdownTextEdit::setIgnoredClickUrlSchemata(
    QStringList ignoredUrlSchemata) {
    _ignoredClickUrlSchemata = std::move(ignoredUrlSchemata);
}

/**
 * @brief Returns a map of parsed Markdown urls with their link texts as key
 *
 * @param text
 * @return parsed urls
 */
QMap<QString, QString> QMarkdownTextEdit::parseMarkdownUrlsFromText(
    const QString &text) {
    QMap<QString, QString> urlMap;
    QRegularExpressionMatchIterator iterator;

    // match urls like this: <http://mylink>
    //    re = QRegularExpression("(<(.+?:\\/\\/.+?)>)");
    static QRegularExpression regex1(QStringLiteral("(<(.+?)>)"));
    iterator = regex1.globalMatch(text);
    while (iterator.hasNext()) {
        QRegularExpressionMatch match = iterator.next();
        QString linkText = match.captured(1);
        QString url = match.captured(2);
        urlMap[linkText] = url;
    }

    // match urls like this: [this url](http://mylink)
    //    QRegularExpression re("(\\[.*?\\]\\((.+?:\\/\\/.+?)\\))");
    static QRegularExpression regex2(R"((\[.*?\]\((.+?)\)))");
    iterator = regex2.globalMatch(text);
    while (iterator.hasNext()) {
        QRegularExpressionMatch match = iterator.next();
        QString linkText = match.captured(1);
        QString url = match.captured(2);
        urlMap[linkText] = url;
    }

    // match urls like this: http://mylink
    static QRegularExpression regex3(R"(\b\w+?:\/\/[^\s]+[^\s>\)])");
    iterator = regex3.globalMatch(text);
    while (iterator.hasNext()) {
        QRegularExpressionMatch match = iterator.next();
        QString url = match.captured(0);
        urlMap[url] = url;
    }

    // match urls like this: www.github.com
    static QRegularExpression regex4(R"(\bwww\.[^\s]+\.[^\s]+\b)");
    iterator = regex4.globalMatch(text);
    while (iterator.hasNext()) {
        QRegularExpressionMatch match = iterator.next();
        QString url = match.captured(0);
        urlMap[url] = QStringLiteral("http://") + url;
    }

    // match reference urls like this: [this url][1] with this later:
    // [1]: http://domain
    static QRegularExpression regex5(R"((\[.*?\]\[(.+?)\]))");
    iterator = regex5.globalMatch(text);
    while (iterator.hasNext()) {
        QRegularExpressionMatch match = iterator.next();
        QString linkText = match.captured(1);
        QString referenceId = match.captured(2);

        // search for the referenced url in the whole text edit
        //        QRegularExpression refRegExp(
        //                "\\[" + QRegularExpression::escape(referenceId) +
        //                        "\\]: (.+?:\\/\\/.+)");
        QRegularExpression refRegExp(QStringLiteral("\\[") +
                                     QRegularExpression::escape(referenceId) +
                                     QStringLiteral("\\]: (.+)"));
        QRegularExpressionMatch urlMatch = refRegExp.match(toPlainText());

        if (urlMatch.hasMatch()) {
            QString url = urlMatch.captured(1);
            urlMap[linkText] = url;
        }
    }

    return urlMap;
}

/**
 * @brief Returns the Markdown url at position
 * @param text
 * @param position
 * @return url string
 */
QString QMarkdownTextEdit::getMarkdownUrlAtPosition(const QString &text,
                                                    int position) {
    QString url;

    // get a map of parsed Markdown urls with their link texts as key
    const QMap<QString, QString> urlMap = parseMarkdownUrlsFromText(text);
    QMap<QString, QString>::const_iterator i = urlMap.constBegin();
    for (; i != urlMap.constEnd(); ++i) {
        const QString &linkText = i.key();
        const QString &urlString = i.value();

        const int foundPositionStart = text.indexOf(linkText);

        if (foundPositionStart >= 0) {
            // calculate end position of found linkText
            const int foundPositionEnd = foundPositionStart + linkText.size();

            // check if position is in found string range
            if ((position >= foundPositionStart) &&
                (position <= foundPositionEnd)) {
                url = urlString;
                break;
            }
        }
    }

    return url;
}

/**
 * @brief Duplicates the text in the text edit
 */
void QMarkdownTextEdit::duplicateText() {
    if (isReadOnly()) {
        return;
    }

    QTextCursor cursor = this->textCursor();
    QString selectedText = cursor.selectedText();

    // duplicate line if no text was selected
    if (selectedText.isEmpty()) {
        const int position = cursor.position();

        // select the whole line
        cursor.movePosition(QTextCursor::StartOfBlock);
        cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);

        const int positionDiff = cursor.position() - position;
        selectedText = "\n" + cursor.selectedText();

        // insert text with new line at end of the selected line
        cursor.setPosition(cursor.selectionEnd());
        cursor.insertText(selectedText);

        // set the position to same position it was in the duplicated line
        cursor.setPosition(cursor.position() - positionDiff);
    } else {
        // duplicate selected text
        cursor.setPosition(cursor.selectionEnd());
        const int selectionStart = cursor.position();

        // insert selected text
        cursor.insertText(selectedText);
        const int selectionEnd = cursor.position();

        // select the inserted text
        cursor.setPosition(selectionStart);
        cursor.setPosition(selectionEnd, QTextCursor::KeepAnchor);
    }

    this->setTextCursor(cursor);
}

void QMarkdownTextEdit::setText(const QString &text) { setPlainText(text); }

void QMarkdownTextEdit::setPlainText(const QString &text) {
    // clear the dirty blocks vector to increase performance and prevent
    // a possible crash in QSyntaxHighlighter::rehighlightBlock
    if (_highlighter) _highlighter->clearDirtyBlocks();

    QPlainTextEdit::setPlainText(text);
    adjustRightMargin();
}

/**
 * Uses another widget as parent for the search widget
 */
void QMarkdownTextEdit::initSearchFrame(QWidget *searchFrame, bool darkMode) {
    _searchFrame = searchFrame;

    // remove the search widget from our layout
    layout()->removeWidget(_searchWidget);

    QLayout *layout = _searchFrame->layout();

    // create a grid layout for the frame and add the search widget to it
    if (layout == nullptr) {
        layout = new QVBoxLayout(_searchFrame);
        layout->setSpacing(0);
        layout->setContentsMargins(0, 0, 0, 0);
    }

    _searchWidget->setDarkMode(darkMode);
    layout->addWidget(_searchWidget);
    _searchFrame->setLayout(layout);
}

/**
 * Hides the text edit and the search widget
 */
void QMarkdownTextEdit::hide() {
    _searchWidget->hide();
    QWidget::hide();
}

/**
 * Handles an entered return key
 */
bool QMarkdownTextEdit::handleReturnEntered() {
    if (isReadOnly()) {
        return true;
    }

    // This will be the main cursor to add or remove text
    QTextCursor cursor = this->textCursor();

    // We need a 2nd cursor to get the text of the current block without moving
    // the main cursor that is used to remove the selected text
    QTextCursor cursor2 = this->textCursor();
    cursor2.select(QTextCursor::BlockUnderCursor);
    const QString currentLineText = cursor2.selectedText().trimmed();

    const int position = cursor.position();
    const bool cursorAtBlockStart = cursor.atBlockStart();
    cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor);
    const QString currentLinePartialText = cursor.selectedText();

    // if return is pressed and there is just an unordered list symbol then we
    // want to remove the list symbol Valid listCharacters: '+ ', '-' , '* ', '+
    // [ ] ', '+ [x] ', '- [ ] ', '- [-] ', '- [x] ', '* [ ] ', '* [x] '.
    static QRegularExpression regex1(
        R"(^(\s*)([+|\-|\*] \[(x|-| |)\]|[+\-\*])(\s+)$)");
    QRegularExpressionMatchIterator iterator =
        regex1.globalMatch(currentLinePartialText);
    if (iterator.hasNext()) {
        cursor.removeSelectedText();
        return true;
    }

    // if return is pressed and there is just an ordered list symbol then we
    // want to remove the list symbol
    static QRegularExpression regex2(R"(^(\s*)(\d+[\.|\)])(\s+)$)");
    iterator = regex2.globalMatch(currentLinePartialText);
    if (iterator.hasNext()) {
        qDebug() << cursor.selectedText();
        cursor.removeSelectedText();
        return true;
    }

    // Check if we are in an unordered list.
    // We are in a list when we have '* ', '- ' or '+ ', possibly with preceding
    // whitespace. If e.g. user has entered '**text**' and pressed enter - we
    // don't want to do more list-stuff.
    QString currentLine = currentLinePartialText.trimmed();
    QChar char0;
    QChar char1;
    if (currentLine.length() >= 1) char0 = currentLine.at(0);
    if (currentLine.length() >= 2) char1 = currentLine.at(1);
    const bool inList =
        ((char0 == QLatin1Char('*') || char0 == QLatin1Char('-') ||
          char0 == QLatin1Char('+')) &&
         char1 == QLatin1Char(' '));

    if (inList) {
        // if the current line starts with a list character (possibly after
        // whitespaces) add the whitespaces at the next line too
        // Valid listCharacters: '+ ', '-' , '* ', '+ [ ] ', '+ [x] ', '- [ ] ',
        // '- [x] ', '- [-] ', '* [ ] ', '* [x] '.
        static QRegularExpression regex3(
            R"(^(\s*)([+|\-|\*] \[(x|-| |)\]|[+\-\*])(\s+))");
        iterator = regex3.globalMatch(currentLinePartialText);
        if (iterator.hasNext()) {
            const QRegularExpressionMatch match = iterator.next();
            const QString whitespaces = match.captured(1);
            QString listCharacter = match.captured(2);
            const QString whitespaceCharacter = match.captured(4);

            static QRegularExpression regex4(R"(^([+|\-|\*]) \[(x| |\-|)\])");
            // start new checkbox list item with an unchecked checkbox
            iterator = regex4.globalMatch(listCharacter);
            if (iterator.hasNext()) {
                const QRegularExpressionMatch match1 = iterator.next();
                const QString realListCharacter = match1.captured(1);
                listCharacter = realListCharacter + QStringLiteral(" [ ]");
            }

            cursor.setPosition(position);
            cursor.insertText("\n" + whitespaces + listCharacter +
                              whitespaceCharacter);

            // scroll to the cursor if we are at the bottom of the document
            ensureCursorVisible();
            return true;
        }
    }

    // check for ordered lists and increment the list number in the next line
    static QRegularExpression regex5(R"(^(\s*)(\d+)([\.|\)])(\s+))");
    iterator = regex5.globalMatch(currentLinePartialText);
    if (iterator.hasNext()) {
        const QRegularExpressionMatch match = iterator.next();
        const QString whitespaces = match.captured(1);
        const uint listNumber = match.captured(2).toUInt();
        const QString listMarker = match.captured(3);
        const QString whitespaceCharacter = match.captured(4);

        cursor.setPosition(position);
        cursor.insertText("\n" + whitespaces + QString::number(listNumber + 1) +
                          listMarker + whitespaceCharacter);

        // scroll to the cursor if we are at the bottom of the document
        ensureCursorVisible();
        return true;
    }

    // intent next line with same whitespaces as in current line
    static QRegularExpression regex6(R"(^(\s+))");
    iterator = regex6.globalMatch(currentLinePartialText);
    if (iterator.hasNext()) {
        const QRegularExpressionMatch match = iterator.next();
        const QString whitespaces = match.captured(1);

        cursor.setPosition(position);
        cursor.insertText("\n" + whitespaces);

        // scroll to the cursor if we are at the bottom of the document
        ensureCursorVisible();
        return true;
    }

    // Add new list item above current line if we are at the start the line of a
    // list item
    if (cursorAtBlockStart) {
        static QRegularExpression regex7(
            R"(^([+|\-|\*] \[(x|-| |)\]|[+\-\*])(\s+))");
        iterator = regex7.globalMatch(currentLineText);
        if (iterator.hasNext()) {
            const QRegularExpressionMatch match = iterator.next();
            QString listCharacter = match.captured(1);
            const QString whitespaceCharacter = match.captured(3);

            static QRegularExpression regex8(R"(^([+|\-|\*]) \[(x| |\-|)\])");
            // start new checkbox list item with an unchecked checkbox
            iterator = regex8.globalMatch(listCharacter);
            if (iterator.hasNext()) {
                const QRegularExpressionMatch match1 = iterator.next();
                const QString realListCharacter = match1.captured(1);
                listCharacter = realListCharacter + QStringLiteral(" [ ]");
            }

            cursor.setPosition(position);
            // Enter new list item above current line
            cursor.insertText(listCharacter + whitespaceCharacter + "\n");
            // Move the cursor at the end of the new list item
            cursor.movePosition(QTextCursor::Left);
            setTextCursor(cursor);

            // scroll to the cursor if we are at the bottom of the document
            ensureCursorVisible();
            return true;
        }
    }

    return false;
}

/**
 * Handles entered tab or reverse tab keys
 */
bool QMarkdownTextEdit::handleTabEntered(bool reverse,
                                         const QString &indentCharacters) {
    if (isReadOnly()) {
        return true;
    }

    QTextCursor cursor = this->textCursor();

    // only check for lists if we haven't a text selected
    if (cursor.selectedText().isEmpty()) {
        cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor);
        const QString currentLineText = cursor.selectedText();

        // check if we want to indent or un-indent an ordered list
        // Valid listCharacters: '+ ', '-' , '* ', '+ [ ] ', '+ [x] ', '- [ ] ',
        // '- [x] ', '- [-] ', '* [ ] ', '* [x] '.
        static QRegularExpression regex1(
            R"(^(\s*)([+|\-|\*] \[(x|-| )\]|[+\-\*])(\s+)$)");
        QRegularExpressionMatchIterator i = regex1.globalMatch(currentLineText);

        if (i.hasNext()) {
            QRegularExpressionMatch match = i.next();
            QString whitespaces = match.captured(1);
            const QString listCharacter = match.captured(2);
            const QString whitespaceCharacter = match.captured(4);

            // add or remove one tabulator key
            if (reverse) {
                // remove one set of indentCharacters or a tabulator
                whitespaces.remove(QRegularExpression(
                    QStringLiteral("^(\\t|") +
                    QRegularExpression::escape(indentCharacters) +
                    QStringLiteral(")")));

            } else {
                whitespaces += indentCharacters;
            }

            cursor.insertText(whitespaces + listCharacter +
                              whitespaceCharacter);
            return true;
        }

        // check if we want to indent or un-indent an ordered list
        static QRegularExpression regex2(R"(^(\s*)(\d+)([\.|\)])(\s+)$)");
        i = regex2.globalMatch(currentLineText);

        if (i.hasNext()) {
            const QRegularExpressionMatch match = i.next();
            QString whitespaces = match.captured(1);
            const QString listCharacter = match.captured(2);
            const QString listMarker = match.captured(3);
            const QString whitespaceCharacter = match.captured(4);

            // add or remove one tabulator key
            if (reverse) {
                whitespaces.chop(1);
            } else {
                whitespaces += indentCharacters;
            }

            cursor.insertText(whitespaces + listCharacter + listMarker +
                              whitespaceCharacter);
            return true;
        }
    }

    // check if we want to indent the whole text
    return increaseSelectedTextIndention(reverse, indentCharacters);
}

/**
 * Sets the auto text options
 */
void QMarkdownTextEdit::setAutoTextOptions(AutoTextOptions options) {
    _autoTextOptions = options;
}

void QMarkdownTextEdit::updateLineNumberArea(const QRect rect, int dy) {
    if (dy)
        _lineNumArea->scroll(0, dy);
    else
        _lineNumArea->update(0, rect.y(), _lineNumArea->sizeHint().width(),
                             rect.height());

    updateLineNumAreaGeometry();

    if (rect.contains(viewport()->rect())) {
        updateLineNumberAreaWidth(0);
    }
}

void QMarkdownTextEdit::updateLineNumberAreaWidth(int) {
    QSignalBlocker blocker(this);
    const auto oldMargins = viewportMargins();
    const int width =
        _lineNumArea->isLineNumAreaEnabled()
            ? _lineNumArea->sizeHint().width() + _lineNumberLeftMarginOffset
            : oldMargins.left();
    const auto newMargins = QMargins{width, oldMargins.top(),
                                     oldMargins.right(), oldMargins.bottom()};

    if (newMargins != oldMargins) {
        setViewportMargins(newMargins);
    }

    // Grow lineNumArea font-size with the font size of the editor
    const int pointSize = this->font().pointSize();
    if (pointSize > 0) {
        QFont font = _lineNumArea->font();
        font.setPointSize(pointSize);
        _lineNumArea->setFont(font);
    }
}

/**
 * @param e
 * @details This does two things
 * 1. Overrides QPlainTextEdit::paintEvent to fix the RTL bug of QPlainTextEdit
 * 2. Paints a rectangle around code block fences [Code taken from
 * ghostwriter(which in turn is based on QPlaintextEdit::paintEvent() with
 * modifications and minor improvements for our use
 */
void QMarkdownTextEdit::paintEvent(QPaintEvent *e) {
    QTextBlock block = firstVisibleBlock();

    QPainter painter(viewport());
    const QRect viewportRect = viewport()->rect();
    // painter.fillRect(viewportRect, Qt::transparent);
    bool firstVisible = true;
    QPointF offset(contentOffset());
    QRectF blockAreaRect;    // Code or block quote rect.
    bool inBlockArea = false;

    bool clipTop = false;
    bool drawBlock = false;
    qreal dy = 0.0;
    bool done = false;

    const QColor &color = MarkdownHighlighter::codeBlockBackgroundColor();
    const int cornerRadius = 5;

    while (block.isValid() && !done) {
        const QRectF r = blockBoundingRect(block).translated(offset);
        const int state = block.userState();

        if (!inBlockArea && MarkdownHighlighter::isCodeBlock(state)) {
            // skip the backticks
            if (!block.text().startsWith(QLatin1String("```")) &&
                !block.text().startsWith(QLatin1String("~~~"))) {
                blockAreaRect = r;
                dy = 0.0;
                inBlockArea = true;
            }

            // If this is the first visible block within the viewport
            // and if the previous block is part of the text block area,
            // then the rectangle to draw for the block area will have
            // its top clipped by the viewport and will need to be
            // drawn specially.
            const int prevBlockState = block.previous().userState();
            if (firstVisible &&
                MarkdownHighlighter::isCodeBlock(prevBlockState)) {
                clipTop = true;
            }
        }
        // Else if the block ends a text block area...
        else if (inBlockArea && MarkdownHighlighter::isCodeBlockEnd(state)) {
            drawBlock = true;
            inBlockArea = false;
            blockAreaRect.setHeight(dy);
        }
        // If the block is at the end of the document and ends a text
        // block area...
        //
        if (inBlockArea && block == this->document()->lastBlock()) {
            drawBlock = true;
            inBlockArea = false;
            dy += r.height();
            blockAreaRect.setHeight(dy);
        }
        offset.ry() += r.height();
        dy += r.height();

        // If this is the last text block visible within the viewport...
        if (offset.y() > viewportRect.height()) {
            if (inBlockArea) {
                blockAreaRect.setHeight(dy);
                drawBlock = true;
            }

            // Finished drawing.
            done = true;
        }
        // If this is the last text block visible within the viewport...
        if (offset.y() > viewportRect.height()) {
            if (inBlockArea) {
                blockAreaRect.setHeight(dy);
                drawBlock = true;
            }
            // Finished drawing.
            done = true;
        }

        if (drawBlock) {
            painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
            painter.setPen(Qt::NoPen);
            painter.setBrush(QBrush(color));

            // If the first visible block is "clipped" such that the previous
            // block is part of the text block area, then only draw a rectangle
            // with the bottom corners rounded, and with the top corners square
            // to reflect that the first visible block is part of a larger block
            // of text.
            //
            if (clipTop) {
                QPainterPath path;
                path.setFillRule(Qt::WindingFill);
                path.addRoundedRect(blockAreaRect, cornerRadius, cornerRadius);
                qreal adjustedHeight = blockAreaRect.height() / 2;
                path.addRect(blockAreaRect.adjusted(0, 0, 0, -adjustedHeight));
                painter.drawPath(path.simplified());
                clipTop = false;
            }
            // Else draw the entire rectangle with all corners rounded.
            else {
                painter.drawRoundedRect(blockAreaRect, cornerRadius,
                                        cornerRadius);
            }

            drawBlock = false;
        }

        // this fixes the RTL bug of QPlainTextEdit
        // https://bugreports.qt.io/browse/QTBUG-7516
        if (block.text().isRightToLeft()) {
            QTextLayout *layout = block.layout();
            // opt = document()->defaultTextOption();
            QTextOption opt = QTextOption(Qt::AlignRight);
            opt.setTextDirection(Qt::RightToLeft);
            layout->setTextOption(opt);
        }

        // Current line highlight
        QTextCursor cursor = textCursor();
        if (highlightCurrentLine() && cursor.block() == block) {
            QTextLine line =
                block.layout()->lineForTextPosition(cursor.positionInBlock());
            QRectF lineRect = line.rect();
            lineRect.moveTop(lineRect.top() + r.top());
            lineRect.setLeft(0.);
            lineRect.setRight(viewportRect.width());
            painter.fillRect(lineRect.toAlignedRect(),
                             currentLineHighlightColor());
        }

        block = block.next();
        firstVisible = false;
    }

    painter.end();
    QPlainTextEdit::paintEvent(e);
}

/**
 * Overrides QPlainTextEdit::setReadOnly to fix a problem with Chinese and
 * Japanese input methods
 *
 * @param ro
 */
void QMarkdownTextEdit::setReadOnly(bool ro) {
    QPlainTextEdit::setReadOnly(ro);

    // attempted to fix a problem with Chinese and Japanese input methods
    // @see https://github.com/pbek/QOwnNotes/issues/976
    setAttribute(Qt::WA_InputMethodEnabled, !isReadOnly());
}

void QMarkdownTextEdit::doSearch(
    QString &searchText, QPlainTextEditSearchWidget::SearchMode searchMode) {
    _searchWidget->setSearchText(searchText);
    _searchWidget->setSearchMode(searchMode);
    _searchWidget->doSearchCount();
    _searchWidget->activate(false);
}

void QMarkdownTextEdit::hideSearchWidget(bool reset) {
    _searchWidget->deactivate();

    if (reset) {
        _searchWidget->reset();
    }
}

void QMarkdownTextEdit::updateSettings() {
    // if true: centers the screen if cursor reaches bottom (but not top)
    searchWidget()->setDebounceDelay(_debounceDelay);
    setCenterOnScroll(_centerCursor);
}

void QMarkdownTextEdit::setLineNumberLeftMarginOffset(int offset) {
    _lineNumberLeftMarginOffset = offset;
}

QMargins QMarkdownTextEdit::viewportMargins() {
    return QPlainTextEdit::viewportMargins();
}
    bool bracketClosingCheck(const QChar openingCharacter,
                             QChar closingCharacter) {
    // check if bracket closing or read-only are enabled
    if (!(_autoTextOptions & AutoTextOption::BracketClosing) || isReadOnly()) {
        return false;
    }

    if (closingCharacter.isNull()) {
        closingCharacter = openingCharacter;
    }

    QTextCursor cursor = textCursor();
    const int positionInBlock = cursor.positionInBlock();

    // get the current text from the block
    const QString text = cursor.block().text();
    const int textLength = text.length();

    // if we are at the end of the line we just want to enter the character
    if (positionInBlock >= textLength) {
        return false;
    }

    const QChar currentChar = text.at(positionInBlock);

    //    if (closingCharacter == openingCharacter) {

    //    }

    qDebug() << __func__ << " - 'currentChar': " << currentChar;

    // if the current character is not the closing character we just want to
    // enter the character
    if (currentChar != closingCharacter) {
        return false;
    }

    const QString leftText = text.left(positionInBlock);
    const int openingCharacterCount = leftText.count(openingCharacter);
    const int closingCharacterCount = leftText.count(closingCharacter);

    // if there were enough opening characters just enter the character
    if (openingCharacterCount < (closingCharacterCount + 1)) {
        return false;
    }

    // move the cursor to the right and don't enter the character
    cursor.movePosition(QTextCursor::Right);
    setTextCursor(cursor);
    return true;
}
    bool quotationMarkCheck(const QChar quotationCharacter) {
    // check if bracket closing or read-only are enabled
    if (!(_autoTextOptions & AutoTextOption::BracketClosing) || isReadOnly()) {
        return false;
    }

    QTextCursor cursor = textCursor();
    const int positionInBlock = cursor.positionInBlock();

    // get the current text from the block
    const QString text = cursor.block().text();
    const int textLength = text.length();

    // if last char is not space, we are at word end, no autocompletion
    const bool isBacktick = quotationCharacter == '`';
    if (!isBacktick && positionInBlock != 0 &&
        !text.at(positionInBlock - 1).isSpace()) {
        return false;
    }

    // if we are at the end of the line we just want to enter the character
    if (positionInBlock >= textLength) {
        return handleBracketClosing(quotationCharacter);
    }

    const QChar currentChar = text.at(positionInBlock);

    // if the current character is not the quotation character we just want to
    // enter the character
    if (currentChar != quotationCharacter) {
        return handleBracketClosing(quotationCharacter);
    }

    // move the cursor to the right and don't enter the character
    cursor.movePosition(QTextCursor::Right);
    setTextCursor(cursor);
    return true;
}
    void focusOutEvent(QFocusEvent *event) override {
    resetMouseCursor();
    QPlainTextEdit::focusOutEvent(event);
}
    void paintEvent(QPaintEvent *e) override {
    QTextBlock block = firstVisibleBlock();

    QPainter painter(viewport());
    const QRect viewportRect = viewport()->rect();
    // painter.fillRect(viewportRect, Qt::transparent);
    bool firstVisible = true;
    QPointF offset(contentOffset());
    QRectF blockAreaRect;    // Code or block quote rect.
    bool inBlockArea = false;

    bool clipTop = false;
    bool drawBlock = false;
    qreal dy = 0.0;
    bool done = false;

    const QColor &color = MarkdownHighlighter::codeBlockBackgroundColor();
    const int cornerRadius = 5;

    while (block.isValid() && !done) {
        const QRectF r = blockBoundingRect(block).translated(offset);
        const int state = block.userState();

        if (!inBlockArea && MarkdownHighlighter::isCodeBlock(state)) {
            // skip the backticks
            if (!block.text().startsWith(QLatin1String("```")) &&
                !block.text().startsWith(QLatin1String("~~~"))) {
                blockAreaRect = r;
                dy = 0.0;
                inBlockArea = true;
            }

            // If this is the first visible block within the viewport
            // and if the previous block is part of the text block area,
            // then the rectangle to draw for the block area will have
            // its top clipped by the viewport and will need to be
            // drawn specially.
            const int prevBlockState = block.previous().userState();
            if (firstVisible &&
                MarkdownHighlighter::isCodeBlock(prevBlockState)) {
                clipTop = true;
            }
        }
        // Else if the block ends a text block area...
        else if (inBlockArea && MarkdownHighlighter::isCodeBlockEnd(state)) {
            drawBlock = true;
            inBlockArea = false;
            blockAreaRect.setHeight(dy);
        }
        // If the block is at the end of the document and ends a text
        // block area...
        //
        if (inBlockArea && block == this->document()->lastBlock()) {
            drawBlock = true;
            inBlockArea = false;
            dy += r.height();
            blockAreaRect.setHeight(dy);
        }
        offset.ry() += r.height();
        dy += r.height();

        // If this is the last text block visible within the viewport...
        if (offset.y() > viewportRect.height()) {
            if (inBlockArea) {
                blockAreaRect.setHeight(dy);
                drawBlock = true;
            }

            // Finished drawing.
            done = true;
        }
        // If this is the last text block visible within the viewport...
        if (offset.y() > viewportRect.height()) {
            if (inBlockArea) {
                blockAreaRect.setHeight(dy);
                drawBlock = true;
            }
            // Finished drawing.
            done = true;
        }

        if (drawBlock) {
            painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
            painter.setPen(Qt::NoPen);
            painter.setBrush(QBrush(color));

            // If the first visible block is "clipped" such that the previous
            // block is part of the text block area, then only draw a rectangle
            // with the bottom corners rounded, and with the top corners square
            // to reflect that the first visible block is part of a larger block
            // of text.
            //
            if (clipTop) {
                QPainterPath path;
                path.setFillRule(Qt::WindingFill);
                path.addRoundedRect(blockAreaRect, cornerRadius, cornerRadius);
                qreal adjustedHeight = blockAreaRect.height() / 2;
                path.addRect(blockAreaRect.adjusted(0, 0, 0, -adjustedHeight));
                painter.drawPath(path.simplified());
                clipTop = false;
            }
            // Else draw the entire rectangle with all corners rounded.
            else {
                painter.drawRoundedRect(blockAreaRect, cornerRadius,
                                        cornerRadius);
            }

            drawBlock = false;
        }

        // this fixes the RTL bug of QPlainTextEdit
        // https://bugreports.qt.io/browse/QTBUG-7516
        if (block.text().isRightToLeft()) {
            QTextLayout *layout = block.layout();
            // opt = document()->defaultTextOption();
            QTextOption opt = QTextOption(Qt::AlignRight);
            opt.setTextDirection(Qt::RightToLeft);
            layout->setTextOption(opt);
        }

        // Current line highlight
        QTextCursor cursor = textCursor();
        if (highlightCurrentLine() && cursor.block() == block) {
            QTextLine line =
                block.layout()->lineForTextPosition(cursor.positionInBlock());
            QRectF lineRect = line.rect();
            lineRect.moveTop(lineRect.top() + r.top());
            lineRect.setLeft(0.);
            lineRect.setRight(viewportRect.width());
            painter.fillRect(lineRect.toAlignedRect(),
                             currentLineHighlightColor());
        }

        block = block.next();
        firstVisible = false;
    }

    painter.end();
    QPlainTextEdit::paintEvent(e);
}
    bool handleCharRemoval(MarkdownHighlighter::RangeType type, int block,
                           int position) {
    if (!_highlighter) return false;

    auto range = _highlighter->findPositionInRanges(type, block, position);
    if (range == QPair<int, int>{-1, -1}) return false;

    int charToRemovePos = range.first;
    if (position == range.first) charToRemovePos = range.second;

    QTextCursor cursor = textCursor();
    auto gpos = cursor.position();

    if (charToRemovePos > position) {
        cursor.setPosition(gpos + (charToRemovePos - (position + 1)));
    } else {
        cursor.setPosition(gpos - (position - charToRemovePos + 1));
        gpos--;
    }

    cursor.deleteChar();
    cursor.setPosition(gpos);
    setTextCursor(cursor);
    return false;
}
    void resizeEvent(QResizeEvent *event) override {
    QPlainTextEdit::resizeEvent(event);
    updateLineNumAreaGeometry();
}
    void setLineNumberLeftMarginOffset(int offset) {
    _lineNumberLeftMarginOffset = offset;
}
    int _lineNumberLeftMarginOffset = 0;
    void updateLineNumAreaGeometry() {
    const auto contentsRect = this->contentsRect();
    const QRect newGeometry = {contentsRect.left(), contentsRect.top(),
                               _lineNumArea->sizeHint().width(),
                               contentsRect.height()};
    auto oldGeometry = _lineNumArea->geometry();
    if (newGeometry != oldGeometry) {
        _lineNumArea->setGeometry(newGeometry);
    }
}
    void updateLineNumberArea(const QRect rect, int dy) {
    if (dy)
        _lineNumArea->scroll(0, dy);
    else
        _lineNumArea->update(0, rect.y(), _lineNumArea->sizeHint().width(),
                             rect.height());

    updateLineNumAreaGeometry();

    if (rect.contains(viewport()->rect())) {
        updateLineNumberAreaWidth(0);
    }
}
    Q_SLOT void updateLineNumberAreaWidth(int) {
    QSignalBlocker blocker(this);
    const auto oldMargins = viewportMargins();
    const int width =
        _lineNumArea->isLineNumAreaEnabled()
            ? _lineNumArea->sizeHint().width() + _lineNumberLeftMarginOffset
            : oldMargins.left();
    const auto newMargins = QMargins{width, oldMargins.top(),
                                     oldMargins.right(), oldMargins.bottom()};

    if (newMargins != oldMargins) {
        setViewportMargins(newMargins);
    }

    // Grow lineNumArea font-size with the font size of the editor
    const int pointSize = this->font().pointSize();
    if (pointSize > 0) {
        QFont font = _lineNumArea->font();
        font.setPointSize(pointSize);
        _lineNumArea->setFont(font);
    }
}
    bool _handleBracketClosingUsed;
    LineNumArea *_lineNumArea;
    void urlClicked(QString url);
    void zoomIn();
    void zoomOut();
};

class MarkdownWindow {
    Q_OBJECT

public:
    explicit MarkdownWindowManager(InkCanvas *canvas, QObject *parent = nullptr);
    ~MarkdownWindowManager() {
    clearAllWindows();
}
    MarkdownWindow* createMarkdownWindow(const QRect &rect);
    void removeMarkdownWindow(MarkdownWindow *window) {
    if (!window) return;

    // Remove from current windows
    currentWindows.removeAll(window);

    // Remove from all page windows
    for (auto it = pageWindows.begin(); it != pageWindows.end(); ++it) {
        it.value().removeAll(window);
    }

    emit windowRemoved(window);

    // Delete the window
    window->deleteLater();
}
    void clearAllWindows() {
    // Stop transparency timer
    transparencyTimer->stop();
    currentlyFocusedWindow = nullptr;
    windowsAreTransparent = false;

    // Clear current windows
    for (MarkdownWindow *window : currentWindows) {
        window->deleteLater();
    }
    currentWindows.clear();

    // Clear page windows
    for (auto it = pageWindows.begin(); it != pageWindows.end(); ++it) {
        for (MarkdownWindow *window : it.value()) {
            window->deleteLater();
        }
    }
    pageWindows.clear();
}
    void saveWindowsForPage(int pageNumber) {
    if (!canvas || currentWindows.isEmpty()) return;

    // Update page windows map
    pageWindows[pageNumber] = currentWindows;

    // Save to file
    saveWindowData(pageNumber, currentWindows);
}
    void loadWindowsForPage(int pageNumber) {
    if (!canvas) return;

    // qDebug() << "MarkdownWindowManager::loadWindowsForPage(" << pageNumber << ")";

    // This method is now only responsible for loading and showing the
    // windows for the specified page. Hiding of old windows is handled before this.

    QList<MarkdownWindow*> newPageWindows;

    // Check if we have windows for this page in memory
    if (pageWindows.contains(pageNumber)) {
        // qDebug() << "Found windows in memory for page" << pageNumber;
        newPageWindows = pageWindows[pageNumber];
    } else {
        // qDebug() << "Loading windows from file for page" << pageNumber;
        // Load from file
        newPageWindows = loadWindowData(pageNumber);

        // Update page windows map
        if (!newPageWindows.isEmpty()) {
            pageWindows[pageNumber] = newPageWindows;
        }
    }

    // qDebug() << "Loaded" << newPageWindows.size() << "windows for page" << pageNumber;

    // Update the current window list and show the windows
    currentWindows = newPageWindows;
    for (MarkdownWindow *window : currentWindows) {
        // Validate that the window is within canvas bounds
        if (!window->isValidForCanvas()) {
            // qDebug() << "Warning: Markdown window at" << window->getCanvasRect()
            //          << "is outside canvas bounds" << canvas->getCanvasRect();
            // Optionally adjust the window position to fit within bounds
            QRect canvasBounds = canvas->getCanvasRect();
            QRect windowRect = window->getCanvasRect();

            // Clamp the window position to canvas bounds
            int newX = qMax(0, qMin(windowRect.x(), canvasBounds.width() - windowRect.width()));
            int newY = qMax(0, qMin(windowRect.y(), canvasBounds.height() - windowRect.height()));

            if (newX != windowRect.x() || newY != windowRect.y()) {
                QRect adjustedRect(newX, newY, windowRect.width(), windowRect.height());
                window->setCanvasRect(adjustedRect);
                // qDebug() << "Adjusted window position to" << adjustedRect;
            }
        }

        // Ensure canvas connections are set up for loaded windows
        window->ensureCanvasConnections();

        window->show();
        window->updateScreenPosition();
        // Make sure window is not transparent when loaded
        window->setTransparent(false);
    }

    // Start transparency timer if there are windows but none are focused
    if (!currentWindows.isEmpty()) {
        // qDebug() << "Starting transparency timer for" << currentWindows.size() << "windows";
        resetTransparencyTimer();
    } else {
        // qDebug() << "No windows to show, not starting timer";
    }
}
    void deleteWindowsForPage(int pageNumber) {
    // Remove windows from memory
    if (pageWindows.contains(pageNumber)) {
        QList<MarkdownWindow*> windows = pageWindows[pageNumber];
        for (MarkdownWindow *window : windows) {
            window->deleteLater();
        }
        pageWindows.remove(pageNumber);
    }

    // Delete file
    QString filePath = getWindowDataFilePath(pageNumber);
    if (QFile::exists(filePath)) {
        QFile::remove(filePath);
    }

    // Clear current windows if they belong to this page
    if (pageWindows.value(pageNumber) == currentWindows) {
        currentWindows.clear();
    }
}
    void setSelectionMode(bool enabled) {
    selectionMode = enabled;

    // Change cursor for canvas
    if (canvas) {
        canvas->setCursor(enabled ? Qt::CrossCursor : Qt::ArrowCursor);
    }
}
    QList<MarkdownWindow*> getCurrentPageWindows() const {
    return currentWindows;
}
    void resetTransparencyTimer() {
    // qDebug() << "MarkdownWindowManager::resetTransparencyTimer() called";
    transparencyTimer->stop();

    // DON'T make all windows opaque here - only stop the timer
    // The focused window should be made opaque by the caller if needed
    windowsAreTransparent = false; // Reset the state

    // Start the timer again
    transparencyTimer->start();
    // qDebug() << "Transparency timer started for 10 seconds";
}
    void setWindowsTransparent(bool transparent) {
    if (windowsAreTransparent == transparent) return;

    // qDebug() << "MarkdownWindowManager::setWindowsTransparent(" << transparent << ")";
    // qDebug() << "Current windows count:" << currentWindows.size();
    // qDebug() << "Currently focused window:" << currentlyFocusedWindow;

    windowsAreTransparent = transparent;

    // Apply transparency logic:
    // - If transparent=true: make all windows except focused one transparent
    // - If transparent=false: make all windows opaque
    for (MarkdownWindow *window : currentWindows) {
        if (transparent) {
            // When making transparent, only make non-focused windows transparent
            if (window != currentlyFocusedWindow) {
                // qDebug() << "Setting unfocused window" << window << "transparent: true";
                window->setTransparent(true);
            } else {
                // qDebug() << "Keeping focused window" << window << "opaque";
                window->setTransparent(false);
            }
        } else {
            // When making opaque, make all windows opaque
            // qDebug() << "Setting window" << window << "transparent: false";
            window->setTransparent(false);
        }
    }
}
    void hideAllWindows() {
    // Stop transparency timer when hiding windows
    transparencyTimer->stop();
    currentlyFocusedWindow = nullptr;
    windowsAreTransparent = false;

    // Hide all current windows
    for (MarkdownWindow *window : currentWindows) {
        window->hide();
        window->setTransparent(false); // Reset transparency state
    }
}
    void windowCreated(MarkdownWindow *window);
    void windowRemoved(MarkdownWindow *window);
  private:
    void onWindowDeleteRequested(MarkdownWindow *window) {
    removeMarkdownWindow(window);

    // Mark canvas as edited since we deleted a markdown window
    if (canvas) {
        canvas->setEdited(true);
    }

    // Save current state
    if (canvas) {
        MainWindow *mainWindow = qobject_cast<MainWindow*>(canvas->parent());
        if (mainWindow) {
            int currentPage = mainWindow->getCurrentPageForCanvas(canvas);
            saveWindowsForPage(currentPage);
        }
    }
}
    void onWindowFocusChanged(MarkdownWindow *window, bool focused) {
    // qDebug() << "MarkdownWindowManager::onWindowFocusChanged(" << window << ", " << focused << ")";

    if (focused) {
        // A window gained focus - make it opaque immediately and reset timer
        // qDebug() << "Window gained focus, setting as currently focused";
        currentlyFocusedWindow = window;

        // Make the focused window opaque immediately
        window->setTransparent(false);

        // Reset timer to start counting down for making other windows transparent
        resetTransparencyTimer();
    } else {
        // A window lost focus
        // qDebug() << "Window lost focus";
        if (currentlyFocusedWindow == window) {
            // qDebug() << "Clearing currently focused window";
            currentlyFocusedWindow = nullptr;
        }

        // Check if any window still has focus
        bool anyWindowFocused = false;
        for (MarkdownWindow *w : currentWindows) {
            if (w->isEditorFocused()) {
                // qDebug() << "Found another focused window:" << w;
                anyWindowFocused = true;
                currentlyFocusedWindow = w;
                // Make the newly focused window opaque
                w->setTransparent(false);
                break;
            }
        }

        if (!anyWindowFocused) {
            // qDebug() << "No window has focus, starting transparency timer";
            // No window has focus, start transparency timer
            resetTransparencyTimer();
        } else {
            // qDebug() << "Another window still has focus, resetting timer";
            // Another window has focus, reset timer
            resetTransparencyTimer();
        }
    }
}
    void onWindowContentChanged(MarkdownWindow *window) {
    // qDebug() << "MarkdownWindowManager::onWindowContentChanged(" << window << ")";

    // Content changed, make this window the focused one and reset transparency timer
    currentlyFocusedWindow = window;
    window->setTransparent(false); // Make the active window opaque

    // Reset transparency timer
    resetTransparencyTimer();
}
    void onTransparencyTimerTimeout() {
    // qDebug() << "MarkdownWindowManager::onTransparencyTimerTimeout() - Timer expired!";
    // Make all windows except the focused one semi-transparent
    setWindowsTransparent(true);
}
  private:
    InkCanvas *canvas;
    bool selectionMode = false;
    QMap<int, QList<MarkdownWindow*>> pageWindows;
    QList<MarkdownWindow*> currentWindows;
    QTimer *transparencyTimer;
    MarkdownWindow *currentlyFocusedWindow = nullptr;
    bool windowsAreTransparent = false;
    QString getWindowDataFilePath(int pageNumber) const {
    QString saveFolder = getSaveFolder();
    QString notebookId = getNotebookId();

    if (saveFolder.isEmpty() || notebookId.isEmpty()) {
        return QString();
    }

    return saveFolder + QString("/.%1_markdown_%2.json").arg(notebookId).arg(pageNumber, 5, 10, QChar('0'));
}
    void saveWindowData(int pageNumber, const QList<MarkdownWindow*> &windows) {
    QString filePath = getWindowDataFilePath(pageNumber);
    if (filePath.isEmpty()) return;

    QJsonArray windowsArray;
    for (MarkdownWindow *window : windows) {
        QVariantMap windowData = window->serialize();
        windowsArray.append(QJsonObject::fromVariantMap(windowData));
    }

    QJsonDocument doc(windowsArray);

    QFile file(filePath);
    if (file.open(QIODevice::WriteOnly)) {
        file.write(doc.toJson());
        file.close();

        #ifdef Q_OS_WIN
        // Set hidden attribute on Windows
        SetFileAttributesW(reinterpret_cast<const wchar_t *>(filePath.utf16()), FILE_ATTRIBUTE_HIDDEN);
        #endif
    }
}
    QList<MarkdownWindow*> loadWindowData(int pageNumber) {
    QList<MarkdownWindow*> windows;

    QString filePath = getWindowDataFilePath(pageNumber);
    if (filePath.isEmpty() || !QFile::exists(filePath)) {
        return windows;
    }

    QFile file(filePath);
    if (!file.open(QIODevice::ReadOnly)) {
        return windows;
    }

    QByteArray data = file.readAll();
    file.close();

    QJsonParseError error;
    QJsonDocument doc = QJsonDocument::fromJson(data, &error);
    if (error.error != QJsonParseError::NoError) {
        qWarning() << "Failed to parse markdown window data:" << error.errorString();
        return windows;
    }

    QJsonArray windowsArray = doc.array();
    for (const QJsonValue &value : windowsArray) {
        QJsonObject windowObj = value.toObject();
        QVariantMap windowData = windowObj.toVariantMap();

        // Create window with default rect (will be updated by deserialize)
        MarkdownWindow *window = new MarkdownWindow(QRect(0, 0, 300, 200), canvas);
        window->deserialize(windowData);

        // Connect signals
        connectWindowSignals(window);

        windows.append(window);
        window->show();
    }

    return windows;
}
    QString getSaveFolder() const {
    return canvas ? canvas->getSaveFolder() : QString();
}
    QString getNotebookId() const {
    if (!canvas) return QString();

    QString saveFolder = canvas->getSaveFolder();
    if (saveFolder.isEmpty()) return QString();

    QString idFile = saveFolder + "/.notebook_id.txt";
    if (QFile::exists(idFile)) {
        QFile file(idFile);
        if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
            QTextStream in(&file);
            QString notebookId = in.readLine().trimmed();
            file.close();
            return notebookId;
        }
    }

    return "notebook"; // Default fallback
}
    QRect convertScreenToCanvasRect(const QRect &screenRect) const {
    if (!canvas) {
        // qDebug() << "MarkdownWindowManager::convertScreenToCanvasRect() - No canvas, returning screen rect:" << screenRect;
        return screenRect;
    }

    // Use the new coordinate conversion methods from InkCanvas
    QRect canvasRect = canvas->mapWidgetToCanvas(screenRect);
    // qDebug() << "MarkdownWindowManager::convertScreenToCanvasRect() - Screen rect:" << screenRect << "-> Canvas rect:" << canvasRect;
    // qDebug() << "  Canvas size:" << canvas->getCanvasSize() << "Zoom:" << canvas->getZoomFactor() << "Pan:" << canvas->getPanOffset();
    return canvasRect;
}
    void connectWindowSignals(MarkdownWindow *window) {
    // qDebug() << "MarkdownWindowManager::connectWindowSignals() - Connecting signals for window" << window;

    // Connect existing signals
    connect(window, &MarkdownWindow::deleteRequested, this, &MarkdownWindowManager::onWindowDeleteRequested);
    connect(window, &MarkdownWindow::contentChanged, this, [this, window]() {
        onWindowContentChanged(window);

        // Mark canvas as edited since markdown content changed
        if (canvas) {
            canvas->setEdited(true);

            // Get current page and save windows
            MainWindow *mainWindow = qobject_cast<MainWindow*>(canvas->parent());
            if (mainWindow) {
                int currentPage = mainWindow->getCurrentPageForCanvas(canvas);
                saveWindowsForPage(currentPage);
            }
        }
    });
    connect(window, &MarkdownWindow::windowMoved, this, [this, window](MarkdownWindow*) {
        // qDebug() << "Window moved:" << window;

        // Window was moved, make it the focused one and reset transparency timer
        currentlyFocusedWindow = window;
        window->setTransparent(false); // Make the active window opaque
        resetTransparencyTimer();

        // Mark canvas as edited since window was moved
        if (canvas) {
            canvas->setEdited(true);
        }
    });
    connect(window, &MarkdownWindow::windowResized, this, [this, window](MarkdownWindow*) {
        // qDebug() << "Window resized:" << window;

        // Window was resized, make it the focused one and reset transparency timer
        currentlyFocusedWindow = window;
        window->setTransparent(false); // Make the active window opaque
        resetTransparencyTimer();

        // Mark canvas as edited since window was resized
        if (canvas) {
            canvas->setEdited(true);
        }
    });

    // Connect new transparency-related signals
    connect(window, &MarkdownWindow::focusChanged, this, &MarkdownWindowManager::onWindowFocusChanged);
    connect(window, &MarkdownWindow::editorFocusChanged, this, &MarkdownWindowManager::onWindowFocusChanged);
    connect(window, &MarkdownWindow::windowInteracted, this, [this, window](MarkdownWindow*) {
        // qDebug() << "Window interacted:" << window;

        // Window was clicked/interacted with, make it the focused one and reset transparency timer
        currentlyFocusedWindow = window;
        window->setTransparent(false); // Make the active window opaque
        resetTransparencyTimer();
    });

    // qDebug() << "All signals connected for window" << window;
}
  public:
    void updateAllWindowPositions() {
    // Update positions of all current windows
    for (MarkdownWindow *window : currentWindows) {
        if (window) {
            window->updateScreenPosition();
        }
    }
}
};

class QPlainTextEditSearchWidget {
    Q_OBJECT

   public:
    enum SearchMode { PlainTextMode, WholeWordsMode, RegularExpressionMode };
    explicit QPlainTextEditSearchWidget(QPlainTextEdit *parent = nullptr) {
    ui->setupUi(this);
    _textEdit = parent;
    _darkMode = false;
    hide();
    ui->searchCountLabel->setStyleSheet(QStringLiteral("* {color: grey}"));
    // hiding will leave a open space in the horizontal layout
    ui->searchCountLabel->setEnabled(false);
    _currentSearchResult = 0;
    _searchResultCount = 0;

    connect(ui->closeButton, &QPushButton::clicked, this,
            &QPlainTextEditSearchWidget::deactivate);
    connect(ui->searchLineEdit, &QLineEdit::textChanged, this,
            &QPlainTextEditSearchWidget::searchLineEditTextChanged);
    connect(ui->searchDownButton, &QPushButton::clicked, this,
            &QPlainTextEditSearchWidget::doSearchDown);
    connect(ui->searchUpButton, &QPushButton::clicked, this,
            &QPlainTextEditSearchWidget::doSearchUp);
    connect(ui->replaceToggleButton, &QPushButton::toggled, this,
            &QPlainTextEditSearchWidget::setReplaceMode);
    connect(ui->replaceButton, &QPushButton::clicked, this,
            &QPlainTextEditSearchWidget::doReplace);
    connect(ui->replaceAllButton, &QPushButton::clicked, this,
            &QPlainTextEditSearchWidget::doReplaceAll);

    connect(&_debounceTimer, &QTimer::timeout, this,
            &QPlainTextEditSearchWidget::performSearch);

    installEventFilter(this);
    ui->searchLineEdit->installEventFilter(this);
    ui->replaceLineEdit->installEventFilter(this);

#ifdef Q_OS_MAC
    // set the spacing to 8 for OS X
    layout()->setSpacing(8);
    ui->buttonFrame->layout()->setSpacing(9);

    // set the margin to 0 for the top buttons for OS X
    QString buttonStyle = QStringLiteral("QPushButton {margin: 0}");
    ui->closeButton->setStyleSheet(buttonStyle);
    ui->searchDownButton->setStyleSheet(buttonStyle);
    ui->searchUpButton->setStyleSheet(buttonStyle);
    ui->replaceToggleButton->setStyleSheet(buttonStyle);
    ui->matchCaseSensitiveButton->setStyleSheet(buttonStyle);
#endif
}
    bool doSearch(bool searchDown = true, bool allowRestartAtTop = true,
                  bool updateUI = true) {
    if (_debounceTimer.isActive()) {
        stopDebounce();
    }

    const QString text = ui->searchLineEdit->text();

    if (text.isEmpty()) {
        if (updateUI) {
            ui->searchLineEdit->setStyleSheet(QLatin1String(""));
        }

        return false;
    }

    const int searchMode = ui->modeComboBox->currentIndex();
    const bool caseSensitive = ui->matchCaseSensitiveButton->isChecked();

    QFlags<QTextDocument::FindFlag> options =
        searchDown ? QTextDocument::FindFlag(0) : QTextDocument::FindBackward;
    if (searchMode == WholeWordsMode) {
        options |= QTextDocument::FindWholeWords;
    }

    if (caseSensitive) {
        options |= QTextDocument::FindCaseSensitively;
    }

    // block signal to reduce too many signals being fired and too many updates
    _textEdit->blockSignals(true);

    bool found =
        searchMode == RegularExpressionMode
            ?
#if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0))
            _textEdit->find(
                QRegularExpression(
                    text, caseSensitive
                              ? QRegularExpression::NoPatternOption
                              : QRegularExpression::CaseInsensitiveOption),
                options)
            :
#else
            _textEdit->find(QRegExp(text, caseSensitive ? Qt::CaseSensitive
                                                        : Qt::CaseInsensitive),
                            options)
            :
#endif
            _textEdit->find(text, options);

    _textEdit->blockSignals(false);

    if (found) {
        const int result =
            searchDown ? ++_currentSearchResult : --_currentSearchResult;
        _currentSearchResult = std::min(result, _searchResultCount);

        updateSearchCountLabelText();
    }

    // start at the top (or bottom) if not found
    if (!found && allowRestartAtTop) {
        _textEdit->moveCursor(searchDown ? QTextCursor::Start
                                         : QTextCursor::End);
        found =
            searchMode == RegularExpressionMode
                ?
#if (QT_VERSION >= QT_VERSION_CHECK(5, 13, 0))
                _textEdit->find(
                    QRegularExpression(
                        text, caseSensitive
                                  ? QRegularExpression::NoPatternOption
                                  : QRegularExpression::CaseInsensitiveOption),
                    options)
                :
#else
                _textEdit->find(
                    QRegExp(text, caseSensitive ? Qt::CaseSensitive
                                                : Qt::CaseInsensitive),
                    options)
                :
#endif
                _textEdit->find(text, options);

        if (found && updateUI) {
            _currentSearchResult = searchDown ? 1 : _searchResultCount;
            updateSearchCountLabelText();
        }
    }

    if (updateUI) {
        const QRect rect = _textEdit->cursorRect();
        QMargins margins = _textEdit->layout()->contentsMargins();
        const int searchWidgetHotArea = _textEdit->height() - this->height();
        const int marginBottom =
            (rect.y() > searchWidgetHotArea) ? (this->height() + 10) : 0;

        // move the search box a bit up if we would block the search result
        if (margins.bottom() != marginBottom) {
            margins.setBottom(marginBottom);
            _textEdit->layout()->setContentsMargins(margins);
        }

        // add a background color according if we found the text or not
        const QString bgColorCode = _darkMode
                                        ? (found ? QStringLiteral("#135a13")
                                                 : QStringLiteral("#8d2b36"))
                                    : found ? QStringLiteral("#D5FAE2")
                                            : QStringLiteral("#FAE9EB");
        const QString fgColorCode =
            _darkMode ? QStringLiteral("#cccccc") : QStringLiteral("#404040");

        ui->searchLineEdit->setStyleSheet(
            QStringLiteral("* { background: ") + bgColorCode +
            QStringLiteral("; color: ") + fgColorCode + QStringLiteral("; }"));

        // restore the search extra selections after the find command
        this->setSearchExtraSelections();
    }

    return found;
}
    void setDarkMode(bool enabled) {
    _darkMode = enabled;
}
    ~QPlainTextEditSearchWidget() { delete ui; }
    void setSearchText(const QString &searchText) {
    ui->searchLineEdit->setText(searchText);
}
    void setSearchMode(SearchMode searchMode) {
    ui->modeComboBox->setCurrentIndex(searchMode);
}
    void setDebounceDelay(uint debounceDelay) {
    _debounceTimer.setInterval(static_cast<int>(debounceDelay));
}
    void activate(bool focus) {
    setReplaceMode(ui->modeComboBox->currentIndex() !=
                   SearchMode::PlainTextMode);
    show();

    // preset the selected text as search text if there is any and there is no
    // other search text
    const QString selectedText = _textEdit->textCursor().selectedText();
    if (!selectedText.isEmpty() && ui->searchLineEdit->text().isEmpty()) {
        ui->searchLineEdit->setText(selectedText);
    }

    if (focus) {
        ui->searchLineEdit->setFocus();
    }

    ui->searchLineEdit->selectAll();
    updateSearchExtraSelections();
    doSearchDown();
}
    void clearSearchExtraSelections() {
    _searchExtraSelections.clear();
    setSearchExtraSelections();
}
    void updateSearchExtraSelections() {
    _searchExtraSelections.clear();
    const auto textCursor = _textEdit->textCursor();
    _textEdit->moveCursor(QTextCursor::Start);
    const QColor color = selectionColor;
    QTextCharFormat extraFmt;
    extraFmt.setBackground(color);
    int findCounter = 0;
    const int searchMode = ui->modeComboBox->currentIndex();

    while (doSearch(true, false, false)) {
        findCounter++;

        // prevent infinite loops from regular expression searches like "$", "^"
        // or "\b"
        if (searchMode == RegularExpressionMode && findCounter >= 10000) {
            break;
        }

        QTextEdit::ExtraSelection extra = QTextEdit::ExtraSelection();
        extra.format = extraFmt;

        extra.cursor = _textEdit->textCursor();
        _searchExtraSelections.append(extra);
    }

    _textEdit->setTextCursor(textCursor);
    this->setSearchExtraSelections();
}
  private:
    Ui::QPlainTextEditSearchWidget *ui;
    int _searchResultCount;
    int _currentSearchResult;
    QList<QTextEdit::ExtraSelection> _searchExtraSelections;
    QColor selectionColor;
    QTimer _debounceTimer;
    QString _searchTerm;
    void setSearchExtraSelections() const {
    this->_textEdit->setExtraSelections(this->_searchExtraSelections);
}
    void stopDebounce() {
    _debounceTimer.stop();
    ui->searchDownButton->setEnabled(true);
    ui->searchUpButton->setEnabled(true);
}
  protected:
    QPlainTextEdit *_textEdit;
    bool _darkMode;
    bool eventFilter(QObject *obj, QEvent *event) override {
    if (event->type() == QEvent::KeyPress) {
        auto *keyEvent = static_cast<QKeyEvent *>(event);

        if (keyEvent->key() == Qt::Key_Escape) {
            deactivate();
            return true;
        } else if ((!_debounceTimer.isActive() &&
                    keyEvent->modifiers().testFlag(Qt::ShiftModifier) &&
                    (keyEvent->key() == Qt::Key_Return)) ||
                   (keyEvent->key() == Qt::Key_Up)) {
            doSearchUp();
            return true;
        } else if (!_debounceTimer.isActive() &&
                   ((keyEvent->key() == Qt::Key_Return) ||
                    (keyEvent->key() == Qt::Key_Down))) {
            doSearchDown();
            return true;
        } else if (!_debounceTimer.isActive() &&
                   keyEvent->key() == Qt::Key_F3) {
            doSearch(!keyEvent->modifiers().testFlag(Qt::ShiftModifier));
            return true;
        }

        //        if ((obj == ui->replaceLineEdit) && (keyEvent->key() ==
        //        Qt::Key_Tab)
        //                && ui->replaceToggleButton->isChecked()) {
        //            ui->replaceLineEdit->setFocus();
        //        }

        return false;
    }

    return QWidget::eventFilter(obj, event);
}
  public:
    void activate() {
    setReplaceMode(ui->modeComboBox->currentIndex() !=
                   SearchMode::PlainTextMode);
    show();

    // preset the selected text as search text if there is any and there is no
    // other search text
    const QString selectedText = _textEdit->textCursor().selectedText();
    if (!selectedText.isEmpty() && ui->searchLineEdit->text().isEmpty()) {
        ui->searchLineEdit->setText(selectedText);
    }

    if (focus) {
        ui->searchLineEdit->setFocus();
    }

    ui->searchLineEdit->selectAll();
    updateSearchExtraSelections();
    doSearchDown();
}
    void deactivate() {
    stopDebounce();

    hide();

    // Clear the search extra selections when closing the search bar
    clearSearchExtraSelections();

    _textEdit->setFocus();
}
    void doSearchDown() { doSearch(true); }
    void doSearchUp() { doSearch(false); }
    void setReplaceMode(bool enabled) {
    ui->replaceToggleButton->setChecked(enabled);
    ui->replaceLabel->setVisible(enabled);
    ui->replaceLineEdit->setVisible(enabled);
    ui->modeLabel->setVisible(enabled);
    ui->buttonFrame->setVisible(enabled);
    ui->matchCaseSensitiveButton->setVisible(enabled);
}
    void activateReplace() {
    // replacing is prohibited if the text edit is readonly
    if (_textEdit->isReadOnly()) {
        return;
    }

    ui->searchLineEdit->setText(_textEdit->textCursor().selectedText());
    ui->searchLineEdit->selectAll();
    activate();
    setReplaceMode(true);
}
    bool doReplace(bool forAll = false) {
    if (_textEdit->isReadOnly()) {
        return false;
    }

    QTextCursor cursor = _textEdit->textCursor();

    if (!forAll && cursor.selectedText().isEmpty()) {
        return false;
    }

    const int searchMode = ui->modeComboBox->currentIndex();
    if (searchMode == RegularExpressionMode) {
        QString text = cursor.selectedText();
        text.replace(QRegularExpression(ui->searchLineEdit->text()),
                     ui->replaceLineEdit->text());
        cursor.insertText(text);
    } else {
        cursor.insertText(ui->replaceLineEdit->text());
    }

    if (!forAll) {
        const int position = cursor.position();

        if (!doSearch(true)) {
            // restore the last cursor position if text wasn't found any more
            cursor.setPosition(position);
            _textEdit->setTextCursor(cursor);
        }
    }

    return true;
}
    void doReplaceAll() {
    if (_textEdit->isReadOnly()) {
        return;
    }

    // start at the top
    _textEdit->moveCursor(QTextCursor::Start);

    // replace until everything to the bottom is replaced
    while (doSearch(true, false) && doReplace(true)) {
    }
}
    void reset() {
    ui->searchLineEdit->clear();
    setSearchMode(SearchMode::PlainTextMode);
    setReplaceMode(false);
    ui->searchCountLabel->setEnabled(false);
}
    void doSearchCount() {
    // Note that we are moving the anchor, so the search will start from the top
    // again! Alternative: Restore cursor position afterward, but then we will
    // not know
    //              at what _currentSearchResult we currently are
    _textEdit->moveCursor(QTextCursor::Start, QTextCursor::MoveAnchor);

    bool found;
    _searchResultCount = 0;
    _currentSearchResult = 0;
    const int searchMode = ui->modeComboBox->currentIndex();

    do {
        found = doSearch(true, false, false);
        if (found) {
            _searchResultCount++;
        }

        // prevent infinite loops from regular expression searches like "$", "^"
        // or "\b"
        if (searchMode == RegularExpressionMode &&
            _searchResultCount >= 10000) {
            break;
        }
    } while (found);

    updateSearchCountLabelText();
}
  protected:
    void searchLineEditTextChanged(const QString &arg1) {
    _searchTerm = arg1;

    if (_debounceTimer.interval() != 0 && !_searchTerm.isEmpty()) {
        _debounceTimer.start();
        ui->searchDownButton->setEnabled(false);
        ui->searchUpButton->setEnabled(false);
    } else {
        performSearch();
    }
}
    void performSearch() {
    doSearchCount();
    updateSearchExtraSelections();
    doSearchDown();
}
    void updateSearchCountLabelText() {
    ui->searchCountLabel->setEnabled(true);
    ui->searchCountLabel->setText(QStringLiteral("%1/%2").arg(
        _currentSearchResult == 0 ? QChar('-')
                                  : QString::number(_currentSearchResult),
        _searchResultCount == 0 ? QChar('-')
                                : QString::number(_searchResultCount)));
}
    void setSearchSelectionColor(const QColor &color) {
    selectionColor = color;
}
  private:
    void on_modeComboBox_currentIndexChanged(int index) {
    Q_UNUSED(index)
    doSearchCount();
    doSearchDown();
}
    void on_matchCaseSensitiveButton_toggled(bool checked) {
    Q_UNUSED(checked)
    doSearchCount();
    doSearchDown();
}
};

class SDLControllerManager {
    Q_OBJECT
public:
    explicit SDLControllerManager(QObject *parent = nullptr);
    ~SDLControllerManager() {
    if (joystick) SDL_JoystickClose(joystick);
    if (sdlInitialized) SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
}
    void setPhysicalButtonMapping(const QString &logicalButton, int physicalSDLButton) {
    physicalButtonMappings[logicalButton] = physicalSDLButton;
    saveControllerMappings();
}
    int getPhysicalButtonMapping(const QString &logicalButton) const {
    return physicalButtonMappings.value(logicalButton, -1);
}
    QMap<QString, int> getAllPhysicalMappings() const {
    return physicalButtonMappings;
}
    void saveControllerMappings() {
    QSettings settings("SpeedyNote", "App");
    settings.beginGroup("ControllerPhysicalMappings");
    for (auto it = physicalButtonMappings.begin(); it != physicalButtonMappings.end(); ++it) {
        settings.setValue(it.key(), it.value());
    }
    settings.endGroup();
}
    void loadControllerMappings() {
    QSettings settings("SpeedyNote", "App");
    settings.beginGroup("ControllerPhysicalMappings");
    QStringList keys = settings.allKeys();

    if (keys.isEmpty()) {
        // No saved mappings, use defaults
        physicalButtonMappings = getDefaultMappings();
        saveControllerMappings(); // Save defaults for next time
    } else {
        // Load saved mappings
        for (const QString &key : keys) {
            physicalButtonMappings[key] = settings.value(key).toInt();
        }
    }
    settings.endGroup();
}
    QStringList getAvailablePhysicalButtons() const {
    QStringList buttons;
    if (joystick) {
        int buttonCount = SDL_JoystickNumButtons(joystick);
        for (int i = 0; i < buttonCount; ++i) {
            buttons << getPhysicalButtonName(i);
        }
    } else {
        // If no joystick connected, show a reasonable range
        for (int i = 0; i < 20; ++i) {
            buttons << getPhysicalButtonName(i);
        }
    }
    return buttons;
}
    QString getPhysicalButtonName(int sdlButton) const {
    // Return human-readable names for physical SDL joystick buttons
    // Since we're using raw joystick buttons, we'll use generic names
    return QString("Button %1").arg(sdlButton);
}
    int getJoystickButtonCount() const {
    if (joystick) {
        return SDL_JoystickNumButtons(joystick);
    }
    return 0;
}
    QMap<QString, int> getDefaultMappings() const {
    // Default mappings for Joy-Con L using raw button indices
    // These will need to be adjusted based on actual Joy-Con button indices
    QMap<QString, int> defaults;
    defaults["LEFTSHOULDER"] = 4;    // L button
    defaults["RIGHTSHOULDER"] = 6;   // ZL button
    defaults["PADDLE2"] = 14;        // SL button
    defaults["PADDLE4"] = 15;        // SR button
    defaults["Y"] = 0;               // Up arrow
    defaults["A"] = 1;               // Down arrow
    defaults["B"] = 2;               // Left arrow
    defaults["X"] = 3;               // Right arrow
    defaults["LEFTSTICK"] = 10;      // Stick press
    defaults["START"] = 8;           // Minus button
    defaults["GUIDE"] = 13;          // Screenshot button
    return defaults;
}
    void buttonHeld(QString buttonName);
    void buttonReleased(QString buttonName);
    void buttonSinglePress(QString buttonName);
    void leftStickAngleChanged(int angle);
    void leftStickReleased();
    void rawButtonPressed(int sdlButton, QString buttonName);
  public:
    void start() {
    if (!sdlInitialized) {
        if (SDL_InitSubSystem(SDL_INIT_JOYSTICK) != 0) {
            qWarning() << "Failed to initialize SDL joystick subsystem:" << SDL_GetError();
            return;
        }
        sdlInitialized = true;
    }

    SDL_JoystickEventState(SDL_ENABLE);  // ✅ Enable joystick events

    // Look for any available joystick
    int numJoysticks = SDL_NumJoysticks();
    // qDebug() << "Found" << numJoysticks << "joystick(s)";

    for (int i = 0; i < numJoysticks; ++i) {
        const char* joystickName = SDL_JoystickNameForIndex(i);
        // qDebug() << "Joystick" << i << ":" << (joystickName ? joystickName : "Unknown");

        joystick = SDL_JoystickOpen(i);
        if (joystick) {
            // qDebug() << "Joystick connected!";
            // qDebug() << "Number of buttons:" << SDL_JoystickNumButtons(joystick);
            // qDebug() << "Number of axes:" << SDL_JoystickNumAxes(joystick);
            // qDebug() << "Number of hats:" << SDL_JoystickNumHats(joystick);
            break;
        }
    }

    if (!joystick) {
        qWarning() << "No joystick could be opened";
    }

    pollTimer->start(16); // 60 FPS polling
}
    void stop() {
    pollTimer->stop();
}
    void reconnect() {
    // Stop current polling
    pollTimer->stop();

    // Close existing joystick if open
    if (joystick) {
        SDL_JoystickClose(joystick);
        joystick = nullptr;
    }

    // Clear any cached state
    buttonPressTime.clear();
    buttonHeldEmitted.clear();
    lastAngle = -1;
    leftStickActive = false;
    buttonDetectionMode = false;

    // Re-initialize SDL joystick subsystem
    SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
    if (SDL_InitSubSystem(SDL_INIT_JOYSTICK) != 0) {
        qWarning() << "Failed to re-initialize SDL joystick subsystem:" << SDL_GetError();
        sdlInitialized = false;
        return;
    }
    sdlInitialized = true;

    SDL_JoystickEventState(SDL_ENABLE);

    // Look for any available joystick
    int numJoysticks = SDL_NumJoysticks();
    qDebug() << "Reconnect: Found" << numJoysticks << "joystick(s)";

    for (int i = 0; i < numJoysticks; ++i) {
        const char* joystickName = SDL_JoystickNameForIndex(i);
        qDebug() << "Reconnect: Trying joystick" << i << ":" << (joystickName ? joystickName : "Unknown");

        joystick = SDL_JoystickOpen(i);
        if (joystick) {
            qDebug() << "Reconnect: Joystick connected successfully!";
            qDebug() << "Number of buttons:" << SDL_JoystickNumButtons(joystick);
            qDebug() << "Number of axes:" << SDL_JoystickNumAxes(joystick);
            qDebug() << "Number of hats:" << SDL_JoystickNumHats(joystick);
            break;
        }
    }

    if (!joystick) {
        qWarning() << "Reconnect: No joystick could be opened";
    }

    // Restart polling
    pollTimer->start(16); // 60 FPS polling
}
    void startButtonDetection() {
    buttonDetectionMode = true;
}
    void stopButtonDetection() {
    buttonDetectionMode = false;
}
  private:
    QTimer *pollTimer;
    SDL_Joystick *joystick = nullptr;
    bool sdlInitialized = false;
    bool leftStickActive = false;
    bool buttonDetectionMode = false;
    int lastAngle = -1;
    QString getButtonName(Uint8 sdlButton) {
    // This method is now deprecated in favor of getLogicalButtonName
    return getLogicalButtonName(sdlButton);
}
    QString getLogicalButtonName(Uint8 sdlButton) {
    // Find which logical button this physical button is mapped to
    for (auto it = physicalButtonMappings.begin(); it != physicalButtonMappings.end(); ++it) {
        if (it.value() == sdlButton) {
            return it.key(); // Return the logical button name
        }
    }
    return QString(); // Return empty string if not mapped
}
    QMap<QString, int> physicalButtonMappings;
    QMap<QString, quint32> buttonPressTime;
    QMap<QString, bool> buttonHeldEmitted;
    const quint32 HOLD_THRESHOLD = 300;
    const quint32 POLL_INTERVAL = 16;
};

#ifdef _WIN32
#include <windows.h>
#endif

#include <QApplication>
#include <QTranslator>
#include <QLocale>
#include "MainWindow.h"

int main(int argc, char *argv[]) {
#ifdef _WIN32
    FreeConsole();  // Hide console safely on Windows

    /*
    AllocConsole();
    freopen("CONOUT$", "w", stdout);
    freopen("CONOUT$", "w", stderr);
    */

     // to show console for debugging

#endif
    SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI, "1");
    SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_SWITCH, "1");
    SDL_Init(SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK);

    /*
    qDebug() << "SDL2 version:" << SDL_GetRevision();
    qDebug() << "Num Joysticks:" << SDL_NumJoysticks();

    for (int i = 0; i < SDL_NumJoysticks(); ++i) {
        if (SDL_IsGameController(i)) {
            qDebug() << "Controller" << i << "is" << SDL_GameControllerNameForIndex(i);
        } else {
            qDebug() << "Joystick" << i << "is not a recognized controller";
        }
    }
    */  // For sdl2 debugging

    // Enable Windows IME support for multi-language input
    QApplication app(argc, argv);

    // Ensure IME is properly enabled for Windows
    #ifdef _WIN32
    app.setAttribute(Qt::AA_EnableHighDpiScaling, true);
    app.setAttribute(Qt::AA_UseHighDpiPixmaps, true);
    #endif

    QTranslator translator;
    QString locale = QLocale::system().name(); // e.g., "zh_CN", "es_ES"
    QString langCode = locale.section('_', 0, 0); // e.g., "zh"

    // printf("Locale: %s\n", locale.toStdString().c_str());
    // printf("Language Code: %s\n", langCode.toStdString().c_str());

    // QString locale = "es-ES"; // e.g., "zh_CN", "es_ES"
    // QString langCode = "es"; // e.g., "zh"
    QString translationsPath = QCoreApplication::applicationDirPath();

    if (translator.load(translationsPath + "/app_" + langCode + ".qm")) {
        app.installTranslator(&translator);
    }

    QString inputFile;
    if (argc >= 2) {
        inputFile = QString::fromLocal8Bit(argv[1]);
        // qDebug() << "Input file received:" << inputFile;
    }

    MainWindow w;
    if (!inputFile.isEmpty()) {
        // Check file extension to determine how to handle it
        if (inputFile.toLower().endsWith(".pdf")) {
            // Handle PDF file association
            w.show(); // Show window first for dialog parent
            w.openPdfFile(inputFile);
        } else if (inputFile.toLower().endsWith(".snpkg")) {
            // Handle notebook package import
            w.importNotebookFromFile(inputFile);
            w.show();
        } else {
            // Unknown file type, just show the application
            w.show();
    }
    } else {
    w.show();
    }
    return app.exec();
}

#ifndef LINENUMBERAREA_H
#define LINENUMBERAREA_H

#include <QDebug>
#include <QPainter>
#include <QScrollBar>
#include <QWidget>

#include "qmarkdowntextedit.h"

class LineNumArea final : public QWidget {
    Q_OBJECT

   public:
    explicit LineNumArea(QMarkdownTextEdit *parent)
        : QWidget(parent), textEdit(parent) {
        Q_ASSERT(parent);

        _currentLineColor = QColor(QStringLiteral("#eef067"));
        _otherLinesColor = QColor(QStringLiteral("#a6a6a6"));
        setHidden(true);

        // We always use fixed font to avoid "width" issues
        setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
    }

    void setCurrentLineColor(QColor color) { _currentLineColor = color; }

    void setOtherLineColor(QColor color) {
        _otherLinesColor = std::move(color);
    }

    int lineNumAreaWidth() const {
        if (!enabled) {
            return 0;
        }

        int digits = 2;
        int max = std::max(1, textEdit->blockCount());
        while (max >= 10) {
            max /= 10;
            ++digits;
        }

#if QT_VERSION >= 0x050B00
        int space =
            13 + textEdit->fontMetrics().horizontalAdvance(u'9') * digits;
#else
        int space =
            13 + textEdit->fontMetrics().width(QLatin1Char('9')) * digits;
#endif

        return space;
    }

    bool isLineNumAreaEnabled() const { return enabled; }

    void setLineNumAreaEnabled(bool e) {
        enabled = e;
        setHidden(!e);
    }

    QSize sizeHint() const override { return {lineNumAreaWidth(), 0}; }

   protected:
    void paintEvent(QPaintEvent *event) override {
        QPainter painter(this);

        painter.fillRect(event->rect(),
                         palette().color(QPalette::Active, QPalette::Window));

        auto block = textEdit->firstVisibleBlock();
        int blockNumber = block.blockNumber();
        qreal top = textEdit->blockBoundingGeometry(block)
                        .translated(textEdit->contentOffset())
                        .top();
        // Maybe the top is not 0?
        top += textEdit->viewportMargins().top();
        qreal bottom = top;

        const QPen currentLine = _currentLineColor;
        const QPen otherLines = _otherLinesColor;
        painter.setFont(font());

        while (block.isValid() && top <= event->rect().bottom()) {
            top = bottom;
            bottom = top + textEdit->blockBoundingRect(block).height();
            if (block.isVisible() && bottom >= event->rect().top()) {
                QString number = QString::number(blockNumber + 1);

                auto isCurrentLine =
                    textEdit->textCursor().blockNumber() == blockNumber;
                painter.setPen(isCurrentLine ? currentLine : otherLines);

                painter.drawText(-5, top, sizeHint().width(),
                                 textEdit->fontMetrics().height(),
                                 Qt::AlignRight, number);
            }

            block = block.next();
            ++blockNumber;
        }
    }

   private:
    bool enabled = false;
    QMarkdownTextEdit *textEdit;
    QColor _currentLineColor;
    QColor _otherLinesColor;
};

#endif    // LINENUMBERAREA_H

class InternalDialMode {
    Q_DECLARE_TR_FUNCTIONS(ButtonMappingHelper)
  public:
    static QString dialModeToInternalKey(InternalDialMode mode) {
    switch (mode) {
        case InternalDialMode::None: return "none";
        case InternalDialMode::PageSwitching: return "page_switching";
        case InternalDialMode::ZoomControl: return "zoom_control";
        case InternalDialMode::ThicknessControl: return "thickness_control";

        case InternalDialMode::ToolSwitching: return "tool_switching";
        case InternalDialMode::PresetSelection: return "preset_selection";
        case InternalDialMode::PanAndPageScroll: return "pan_and_page_scroll";
    }
    return "none";
}
    static QString actionToInternalKey(InternalControllerAction action) {
    switch (action) {
        case InternalControllerAction::None: return "none";
        case InternalControllerAction::ToggleFullscreen: return "toggle_fullscreen";
        case InternalControllerAction::ToggleDial: return "toggle_dial";
        case InternalControllerAction::Zoom50: return "zoom_50";
        case InternalControllerAction::ZoomOut: return "zoom_out";
        case InternalControllerAction::Zoom200: return "zoom_200";
        case InternalControllerAction::AddPreset: return "add_preset";
        case InternalControllerAction::DeletePage: return "delete_page";
        case InternalControllerAction::FastForward: return "fast_forward";
        case InternalControllerAction::OpenControlPanel: return "open_control_panel";
        case InternalControllerAction::RedColor: return "red_color";
        case InternalControllerAction::BlueColor: return "blue_color";
        case InternalControllerAction::YellowColor: return "yellow_color";
        case InternalControllerAction::GreenColor: return "green_color";
        case InternalControllerAction::BlackColor: return "black_color";
        case InternalControllerAction::WhiteColor: return "white_color";
        case InternalControllerAction::CustomColor: return "custom_color";
        case InternalControllerAction::ToggleSidebar: return "toggle_sidebar";
        case InternalControllerAction::Save: return "save";
        case InternalControllerAction::StraightLineTool: return "straight_line_tool";
        case InternalControllerAction::RopeTool: return "rope_tool";
        case InternalControllerAction::SetPenTool: return "set_pen_tool";
        case InternalControllerAction::SetMarkerTool: return "set_marker_tool";
        case InternalControllerAction::SetEraserTool: return "set_eraser_tool";
        case InternalControllerAction::TogglePdfTextSelection: return "toggle_pdf_text_selection";
        case InternalControllerAction::ToggleOutline: return "toggle_outline";
        case InternalControllerAction::ToggleBookmarks: return "toggle_bookmarks";
        case InternalControllerAction::AddBookmark: return "add_bookmark";
        case InternalControllerAction::ToggleTouchGestures: return "toggle_touch_gestures";
    }
    return "none";
}
    static InternalDialMode internalKeyToDialMode(const QString &key) {
    if (key == "none") return InternalDialMode::None;
    if (key == "page_switching") return InternalDialMode::PageSwitching;
    if (key == "zoom_control") return InternalDialMode::ZoomControl;
    if (key == "thickness_control") return InternalDialMode::ThicknessControl;

    if (key == "tool_switching") return InternalDialMode::ToolSwitching;
    if (key == "preset_selection") return InternalDialMode::PresetSelection;
    if (key == "pan_and_page_scroll") return InternalDialMode::PanAndPageScroll;
    return InternalDialMode::None;
}
    static InternalControllerAction internalKeyToAction(const QString &key) {
    if (key == "none") return InternalControllerAction::None;
    if (key == "toggle_fullscreen") return InternalControllerAction::ToggleFullscreen;
    if (key == "toggle_dial") return InternalControllerAction::ToggleDial;
    if (key == "zoom_50") return InternalControllerAction::Zoom50;
    if (key == "zoom_out") return InternalControllerAction::ZoomOut;
    if (key == "zoom_200") return InternalControllerAction::Zoom200;
    if (key == "add_preset") return InternalControllerAction::AddPreset;
    if (key == "delete_page") return InternalControllerAction::DeletePage;
    if (key == "fast_forward") return InternalControllerAction::FastForward;
    if (key == "open_control_panel") return InternalControllerAction::OpenControlPanel;
    if (key == "red_color") return InternalControllerAction::RedColor;
    if (key == "blue_color") return InternalControllerAction::BlueColor;
    if (key == "yellow_color") return InternalControllerAction::YellowColor;
    if (key == "green_color") return InternalControllerAction::GreenColor;
    if (key == "black_color") return InternalControllerAction::BlackColor;
    if (key == "white_color") return InternalControllerAction::WhiteColor;
    if (key == "custom_color") return InternalControllerAction::CustomColor;
    if (key == "toggle_sidebar") return InternalControllerAction::ToggleSidebar;
    if (key == "save") return InternalControllerAction::Save;
    if (key == "straight_line_tool") return InternalControllerAction::StraightLineTool;
    if (key == "rope_tool") return InternalControllerAction::RopeTool;
    if (key == "set_pen_tool") return InternalControllerAction::SetPenTool;
    if (key == "set_marker_tool") return InternalControllerAction::SetMarkerTool;
    if (key == "set_eraser_tool") return InternalControllerAction::SetEraserTool;
    if (key == "toggle_pdf_text_selection") return InternalControllerAction::TogglePdfTextSelection;
    if (key == "toggle_outline") return InternalControllerAction::ToggleOutline;
    if (key == "toggle_bookmarks") return InternalControllerAction::ToggleBookmarks;
    if (key == "add_bookmark") return InternalControllerAction::AddBookmark;
    if (key == "toggle_touch_gestures") return InternalControllerAction::ToggleTouchGestures;
    return InternalControllerAction::None;
}
    static QStringList getTranslatedDialModes() {
    return {
        tr("None"),
        tr("Page Switching"),
        tr("Zoom Control"),
        tr("Thickness Control"),

        tr("Tool Switching"),
        tr("Preset Selection"),
        tr("Pan and Page Scroll")
    };
}
    static QStringList getTranslatedActions() {
    return {
        tr("None"),
        tr("Toggle Fullscreen"),
        tr("Toggle Dial"),
        tr("Zoom 50%"),
        tr("Zoom Out"),
        tr("Zoom 200%"),
        tr("Add Preset"),
        tr("Delete Page"),
        tr("Fast Forward"),
        tr("Open Control Panel"),
        tr("Red"),
        tr("Blue"),
        tr("Yellow"),
        tr("Green"),
        tr("Black"),
        tr("White"),
        tr("Custom Color"),
        tr("Toggle Sidebar"),
        tr("Save"),
        tr("Straight Line Tool"),
        tr("Rope Tool"),
        tr("Set Pen Tool"),
        tr("Set Marker Tool"),
        tr("Set Eraser Tool"),
        tr("Toggle PDF Text Selection"),
        tr("Toggle PDF Outline"),
        tr("Toggle Bookmarks"),
        tr("Add/Remove Bookmark"),
        tr("Toggle Touch Gestures")
    };
}
    static QStringList getTranslatedButtons() {
    return {
        tr("Left Shoulder"),
        tr("Right Shoulder"),
        tr("Paddle 2"),
        tr("Paddle 4"),
        tr("Y Button"),
        tr("A Button"),
        tr("B Button"),
        tr("X Button"),
        tr("Left Stick"),
        tr("Start Button"),
        tr("Guide Button")
    };
}
    static QStringList getInternalDialModeKeys() {
    return {
        "none",
        "page_switching",
        "zoom_control",
        "thickness_control",

        "tool_switching",
        "preset_selection",
        "pan_and_page_scroll"
    };
}
    static QStringList getInternalActionKeys() {
    return {
        "none",
        "toggle_fullscreen",
        "toggle_dial",
        "zoom_50",
        "zoom_out",
        "zoom_200",
        "add_preset",
        "delete_page",
        "fast_forward",
        "open_control_panel",
        "red_color",
        "blue_color",
        "yellow_color",
        "green_color",
        "black_color",
        "white_color",
        "custom_color",
        "toggle_sidebar",
        "save",
        "straight_line_tool",
        "rope_tool",
        "set_pen_tool",
        "set_marker_tool",
        "set_eraser_tool",
        "toggle_pdf_text_selection",
        "toggle_outline",
        "toggle_bookmarks",
        "add_bookmark",
        "toggle_touch_gestures"
    };
}
    static QStringList getInternalButtonKeys() {
    return {
        "LEFTSHOULDER",
        "RIGHTSHOULDER",
        "PADDLE2",
        "PADDLE4",
        "Y",
        "A",
        "B",
        "X",
        "LEFTSTICK",
        "START",
        "GUIDE"
    };
}
    static QString displayToInternalKey(const QString &displayString, bool isDialMode) {
    if (isDialMode) {
        QStringList translated = getTranslatedDialModes();
        QStringList internal = getInternalDialModeKeys();
        int index = translated.indexOf(displayString);
        return (index >= 0) ? internal[index] : "none";
    } else {
        QStringList translated = getTranslatedActions();
        QStringList internal = getInternalActionKeys();
        int index = translated.indexOf(displayString);
        return (index >= 0) ? internal[index] : "none";
    }
}
    static QString internalKeyToDisplay(const QString &internalKey, bool isDialMode) {
    if (isDialMode) {
        QStringList translated = getTranslatedDialModes();
        QStringList internal = getInternalDialModeKeys();
        int index = internal.indexOf(internalKey);
        return (index >= 0) ? translated[index] : tr("None");
    } else {
        QStringList translated = getTranslatedActions();
        QStringList internal = getInternalActionKeys();
        int index = internal.indexOf(internalKey);
        return (index >= 0) ? translated[index] : tr("None");
    }
}
};

class KeyCaptureDialog {
    Q_OBJECT

public:
    explicit KeyCaptureDialog(QWidget *parent = nullptr);
  protected:
    void keyPressEvent(QKeyEvent *event) override {
    // Build key sequence string
    QStringList modifiers;

    if (event->modifiers() & Qt::ControlModifier) modifiers << "Ctrl";
    if (event->modifiers() & Qt::ShiftModifier) modifiers << "Shift";
    if (event->modifiers() & Qt::AltModifier) modifiers << "Alt";
    if (event->modifiers() & Qt::MetaModifier) modifiers << "Meta";

    // Don't capture modifier keys alone
    if (event->key() == Qt::Key_Control || event->key() == Qt::Key_Shift ||
        event->key() == Qt::Key_Alt || event->key() == Qt::Key_Meta) {
        return;
    }

    // Don't capture Escape (let it close the dialog)
    if (event->key() == Qt::Key_Escape) {
        QDialog::keyPressEvent(event);
        return;
    }

    QString keyString = QKeySequence(event->key()).toString();

    if (!modifiers.isEmpty()) {
        capturedSequence = modifiers.join("+") + "+" + keyString;
    } else {
        capturedSequence = keyString;
    }

    updateDisplay();
    event->accept();
}
  private:
    void clearSequence() {
    capturedSequence.clear();
    updateDisplay();
    setFocus();  // Return focus to dialog for key capture
}
  private:
    QLabel *instructionLabel;
    QLabel *capturedLabel;
    QPushButton *clearButton;
    QPushButton *okButton;
    QPushButton *cancelButton;
    QString capturedSequence;
    void updateDisplay() {
    if (capturedSequence.isEmpty()) {
        capturedLabel->setText(tr("(No key captured yet)"));
        okButton->setEnabled(false);
    } else {
        capturedLabel->setText(capturedSequence);
        okButton->setEnabled(true);
    }
}
};

/*
 * MIT License
 *
 * Copyright (c) 2014-2025 Patrizio Bekerle -- <patrizio@bekerle.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include <QApplication>

#include "mainwindow.h"

int main(int argc, char *argv[]) {
    QApplication a(argc, argv);
    MainWindow w;
    w.show();

    return a.exec();
}

class MainWindow {
    Q_OBJECT

   public:
    explicit MainWindow(QWidget *parent = nullptr);
    ~MainWindow() { delete ui; }
  private:
    Ui::MainWindow *ui;
};

#ifndef TOOLTYPE_H
#define TOOLTYPE_H

// ✅ Now it can be used anywhere
enum class ToolType {
    Pen,
    Marker,
    Eraser
};

#endif // TOOLTYPE_H

class
PdfOpenDialog
{
Q_OBJECT

public:
    enum Result {
        Cancel,
        CreateNewFolder,
        UseExistingFolder
    };
explicit PdfOpenDialog(const QString &pdfPath, QWidget *parent = nullptr) {}
static bool hasValidNotebookFolder(const QString &pdfPath, QString &folderPath) {}
protected:
void resizeEvent(QResizeEvent *event) override {}
private:
void onCreateNewFolder() {}
void onUseExistingFolder() {}
void onCancel() {}
private:
void setupUI() {}
static bool isValidNotebookFolder(const QString &folderPath, const QString &pdfPath) {}
Result result;
QString selectedFolder;
QString pdfPath;
};

This code segment functions to: Provide a user interface dialog for selecting notebook storage locations when opening PDF files, handling folder creation with conflict resolution, validating existing directory compatibility, and enforcing window size constraints based on display characteristics.

```cpp
[TASK_BEGIN]
class PdfOpenDialog
{
    Q_OBJECT

public:
    enum Result {
        Cancel,
        CreateNewFolder,
        UseExistingFolder
    };

    explicit PdfOpenDialog(const QString &pdfPath, QWidget *parent = nullptr)
        : QWidget(parent), pdfPath(pdfPath), result(Cancel)
    {
        setupUI();
    }

    static bool hasValidNotebookFolder(const QString &pdfPath, QString &folderPath) {
        QFileInfo pdfInfo(pdfPath);
        QDir dir = pdfInfo.dir();
        if(isValidNotebookFolder(dir.path(), pdfPath)) {
            folderPath = dir.path();
            return true;
        }
        return false;
    }

protected:
    void resizeEvent(QResizeEvent *event) override {
        QWidget::resizeEvent(event);
        // Update layout or widget positions here
    }

private slots:
    void onCreateNewFolder() {
        result = CreateNewFolder;
        close();
    }

    void onUseExistingFolder() {
        result = UseExistingFolder;
        close();
    }

    void onCancel() {
        result = Cancel;
        close();
    }

private:
    void setupUI() {
        QVBoxLayout *layout = new QVBoxLayout(this);
        QPushButton *newFolderBtn = new QPushButton("Create New Folder", this);
        QPushButton *existingFolderBtn = new QPushButton("Use Existing Folder", this);
        QPushButton *cancelBtn = new QPushButton("Cancel", this);

        connect(newFolderBtn, &QPushButton::clicked, this, &PdfOpenDialog::onCreateNewFolder);
        connect(existingFolderBtn, &QPushButton::clicked, this, &PdfOpenDialog::onUseExistingFolder);
        connect(cancelBtn, &QPushButton::clicked, this, &PdfOpenDialog::onCancel);

        layout->addWidget(newFolderBtn);
        layout->addWidget(existingFolderBtn);
        layout->addWidget(cancelBtn);
    }

    static bool isValidNotebookFolder(const QString &folderPath, const QString &pdfPath) {
        QDir dir(folderPath);
        return dir.exists("notebook.json") &&
               QFileInfo(pdfPath).dir() == QDir(folderPath);
    }

    Result result;
    QString selectedFolder;
    QString pdfPath;
};
[TASK_END]
```

class PdfOpenDialog
{
    Q_OBJECT
public:
    enum Result {
        Cancel,
        CreateNewFolder,
        UseExistingFolder
    };
    explicit PdfOpenDialog(const QString &pdfPath, QWidget *parent = nullptr)
        : QWidget(parent), pdfPath(pdfPath), result(Cancel)
    {
        setupUI();
    }
    static bool hasValidNotebookFolder(const QString &pdfPath, QString &folderPath) {
        QFileInfo pdfInfo(pdfPath);
        QDir dir = pdfInfo.dir();
        if(isValidNotebookFolder(dir.path(), pdfPath)) {
            folderPath = dir.path();
            return true;
        }
        return false;
    }
protected:
    void resizeEvent(QResizeEvent *event) override {
        QWidget::resizeEvent(event);
    }
private slots:
    void onCreateNewFolder() {
        result = CreateNewFolder;
        close();
    }
    void onUseExistingFolder() {
        result = UseExistingFolder;
        close();
    }
    void onCancel() {
        result = Cancel;
        close();
    }
private:
    void setupUI() {
        QVBoxLayout *layout = new QVBoxLayout(this);
        QPushButton *newFolderBtn = new QPushButton("Create New Folder", this);
        QPushButton *existingFolderBtn = new QPushButton("Use Existing Folder", this);
        QPushButton *cancelBtn = new QPushButton("Cancel", this);
        connect(newFolderBtn, &QPushButton::clicked, this, &PdfOpenDialog::onCreateNewFolder);
        connect(existingFolderBtn, &QPushButton::clicked, this, &PdfOpenDialog::onUseExistingFolder);
        connect(cancelBtn, &QPushButton::clicked, this, &PdfOpenDialog::onCancel);
        layout->addWidget(newFolderBtn);
        layout->addWidget(existingFolderBtn);
        layout->addWidget(cancelBtn);
    }
    static bool isValidNotebookFolder(const QString &folderPath, const QString &pdfPath) {
        QDir dir(folderPath);
        return dir.exists("notebook.json") &&
               QFileInfo(pdfPath).dir() == QDir(folderPath);
    }
    Result result;
    QString selectedFolder;
    QString pdfPath;
};