#include "mainwindow.h" #include "sciedit.h" #include "tabwidget.h" #include "documentmanager.h" #include "appsettings.h" #include "editorviewhelper.h" #include "findreplacedialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include MainWindow::MainWindow(QWidget *parent) : KXmlGuiWindow(parent) , m_documentManager(new DocumentManager(this)) , m_tabWidget(new TabWidget(m_documentManager, this)) , m_cursorPosStatusLabel(new QLabel(QString("Ln: ? Col: ?"), this)) , m_encodingStatusLabel(new QLabel(QString("UTF-8"), this)) , m_languageStatusLabel(new QLabel(QString("Plain Text"), this)) { setCentralWidget(m_tabWidget); // 设置状态栏 statusBar()->addPermanentWidget(m_cursorPosStatusLabel); statusBar()->addPermanentWidget(m_encodingStatusLabel); statusBar()->addPermanentWidget(m_languageStatusLabel); // 连接标签页信号 connect(m_tabWidget, &TabWidget::currentEditorChanged, this, &MainWindow::onCurrentEditorChanged); // 连接文档管理器信号 connect(m_documentManager, &DocumentManager::documentModified, this, &MainWindow::onDocumentModified); setupActions(); // 创建第一个标签页 newFile(); } MainWindow::~MainWindow() { } void MainWindow::setupActions() { using namespace Qt::Literals::StringLiterals; // "File" menu KStandardAction::openNew(this, &MainWindow::newFile, actionCollection()); KStandardAction::open(this, &MainWindow::openFile, actionCollection()); KStandardAction::save(this, &MainWindow::saveFile, actionCollection()); KStandardAction::close(this, &MainWindow::closeFile, actionCollection()); KStandardAction::quit(qApp, &QApplication::quit, actionCollection()); // 添加另存为动作 QAction *saveAsAction = KStandardAction::saveAs(this, &MainWindow::saveAsFile, actionCollection()); // "Edit" menu KStandardAction::undo(this, [this](){ if (SciEdit *editor = m_tabWidget->currentEditor()) { editor->undo(); } }, actionCollection()); KStandardAction::redo(this, [this](){ if (SciEdit *editor = m_tabWidget->currentEditor()) { editor->redo(); } }, actionCollection()); KStandardAction::cut(this, [this](){ if (SciEdit *editor = m_tabWidget->currentEditor()) { editor->cut(); } }, actionCollection()); KStandardAction::copy(this, [this](){ if (SciEdit *editor = m_tabWidget->currentEditor()) { editor->copy(); } }, actionCollection()); KStandardAction::paste(this, [this](){ if (SciEdit *editor = m_tabWidget->currentEditor()) { editor->paste(); } }, actionCollection()); // "Search" menu KStandardAction::find(this, [this](){ showFindReplaceDialog(false); }, actionCollection()); KStandardAction::findNext(this, [this](){ findNextInCurrentEditor(true, true); }, actionCollection()); KStandardAction::findPrev(this, [this](){ findNextInCurrentEditor(false, true); }, actionCollection()); KStandardAction::replace(this, [this](){ showFindReplaceDialog(true); }, actionCollection()); // "Language" menu QAction *lexerNoneAction = new QAction(this); lexerNoneAction->setText("None (Normal Text)"); actionCollection()->addAction(u"lexer_none"_s, lexerNoneAction); connect(lexerNoneAction, &QAction::triggered, this, [this](){ if (SciEdit *editor = m_tabWidget->currentEditor()) { editor->setLexer(nullptr); const int docId = m_tabWidget->currentDocumentId(); if (docId >= 0) { m_documentManager->setDocumentLanguage(docId, QString()); } updateStatusBar(); } }); QMetaObject::invokeMethod(this, [this](){ auto lexerGroupListActions = QList(); for (const QChar &group : LexerGroupActionMenu::groups()) { auto *lexerGroupMenu = new LexerGroupActionMenu(group.toUpper(), group, this); connect(lexerGroupMenu, &LexerGroupActionMenu::lexerSelected, this, &MainWindow::applyLexer); lexerGroupListActions.append(lexerGroupMenu); } unplugActionList("lexer_group_list"); plugActionList(QStringLiteral("lexer_group_list"), lexerGroupListActions); }, Qt::QueuedConnection); // Toolbar actions KStandardAction::zoomIn(this, [this](){ if (SciEdit *editor = m_tabWidget->currentEditor()) { editor->zoomIn(); } }, actionCollection()); KStandardAction::zoomOut(this, [this](){ if (SciEdit *editor = m_tabWidget->currentEditor()) { editor->zoomOut(); } }, actionCollection()); KStandardAction::preferences(this, &MainWindow::showSettings, actionCollection()); QAction *toggleWrapModeAction = new QAction(this); toggleWrapModeAction->setText("Toggle Wrap Mode"); toggleWrapModeAction->setIcon(QIcon::fromTheme(u"text-wrap"_s)); toggleWrapModeAction->setCheckable(true); actionCollection()->addAction(u"toggle_wrap_mode"_s, toggleWrapModeAction); connect(toggleWrapModeAction, &QAction::triggered, this, [this, toggleWrapModeAction](){ if (SciEdit *editor = m_tabWidget->currentEditor()) { bool switchToWrapNone = editor->wrapMode() == SC_WRAP_WORD; editor->setWrapMode(switchToWrapNone ? SC_WRAP_NONE : SC_WRAP_WORD); toggleWrapModeAction->setChecked(!switchToWrapNone); } }); QAction *toggleWhitespaceVisibilityAction = new QAction(this); toggleWhitespaceVisibilityAction->setText("Show All Characters"); toggleWhitespaceVisibilityAction->setCheckable(true); // toggleWhitespaceVisibilityAction->setIcon(QIcon::fromTheme(u"text-wrap"_s)); actionCollection()->addAction(u"toggle_show_all_characters"_s, toggleWhitespaceVisibilityAction); connect(toggleWhitespaceVisibilityAction, &QAction::triggered, this, [this, toggleWhitespaceVisibilityAction](){ if (SciEdit *editor = m_tabWidget->currentEditor()) { bool switchToVisible = editor->viewWS() == SCWS_INVISIBLE; editor->setViewWS(switchToVisible ? SCWS_VISIBLEALWAYS : SCWS_INVISIBLE); editor->setViewEOL(switchToVisible); toggleWhitespaceVisibilityAction->setChecked(switchToVisible); } }); QAction *toggleIndentGuideAction = new QAction(this); toggleIndentGuideAction->setText("Toggle Indent Guide"); toggleIndentGuideAction->setIcon(QIcon::fromTheme(u"show-guides"_s)); toggleIndentGuideAction->setCheckable(true); toggleIndentGuideAction->setChecked(m_indentGuidesEnabled); actionCollection()->addAction(u"toggle_indent_guide"_s, toggleIndentGuideAction); connect(toggleIndentGuideAction, &QAction::toggled, this, [this](bool checked){ m_indentGuidesEnabled = checked; applySettingsToAllEditors(); }); QAction *toggleEditorDarkThemeAction = new QAction(this); toggleEditorDarkThemeAction->setText("Editor Dark Theme"); toggleEditorDarkThemeAction->setCheckable(true); toggleEditorDarkThemeAction->setChecked(AppSettings::editorDarkTheme()); actionCollection()->addAction(u"toggle_editor_dark_theme"_s, toggleEditorDarkThemeAction); connect(toggleEditorDarkThemeAction, &QAction::toggled, this, [this](bool checked){ AppSettings::setEditorDarkTheme(checked); AppSettings::self()->save(); applySettingsToAllEditors(); }); // Load themes KColorSchemeManager *manager = KColorSchemeManager::instance(); auto *colorSelectionMenu = KColorSchemeMenu::createMenu(manager, this); colorSelectionMenu->menu()->setTitle("&Window Color Scheme"); actionCollection()->addAction(QStringLiteral("colorscheme_menu"), colorSelectionMenu); setupGUI(); } void MainWindow::showSettings() { if (KConfigDialog::showDialog(QStringLiteral("settings"))) { return; } KConfigDialog *dialog = new KConfigDialog(this, QStringLiteral("settings"), AppSettings::self()); dialog->setFaceType(KPageDialog::FlatList); dialog->addPage(new SettingsPage(dialog), "Appearance", "preferences-desktop-theme-global"); connect(dialog, &KConfigDialog::settingsChanged, this, [this](const QString &dialogName){ Q_UNUSED(dialogName) applySettingsToAllEditors(); if (QAction *action = actionCollection()->action(QStringLiteral("toggle_editor_dark_theme"))) { action->setChecked(AppSettings::editorDarkTheme()); } }); dialog->show(); } void MainWindow::applyLexer(const QString &lexerName) { if (SciEdit *editor = m_tabWidget->currentEditor()) { Scintilla::ILexer5 * lexer = CreateLexer(lexerName.toStdString().c_str()); editor->setLexer(lexer); const int docId = m_tabWidget->currentDocumentId(); if (docId >= 0) { m_documentManager->setDocumentLanguage(docId, lexerName); } updateStatusBar(); } } void MainWindow::newFile() { m_tabWidget->newDocument(); updateActions(); } void MainWindow::openFile() { QString fileName = QFileDialog::getOpenFileName(this, tr("Open File"), QString(), tr("All Files (*.*)")); if (!fileName.isEmpty()) { m_tabWidget->openDocument(fileName); } } void MainWindow::saveFile() { m_tabWidget->saveCurrentDocument(); } void MainWindow::saveAsFile() { m_tabWidget->saveCurrentDocumentAs(); } void MainWindow::closeFile() { m_tabWidget->closeCurrentTab(); } void MainWindow::onCurrentTabChanged() { updateWindowTitle(); updateStatusBar(); updateActions(); } void MainWindow::onCurrentEditorChanged(SciEdit *editor) { Q_UNUSED(editor) updateWindowTitle(); updateStatusBar(); updateActions(); // 连接当前编辑器的光标位置变化信号 if (editor) { applySettingsToEditor(editor); connect(editor, &SciEdit::cursorPosChanged, this, [this](int line, int column) { m_cursorPosStatusLabel->setText(QString("Ln: %1 Col: %2").arg(line).arg(column)); }); } } void MainWindow::applySettingsToEditor(SciEdit *editor) { if (!editor) { return; } editor->setTabWidth(AppSettings::tabWidth()); editor->setEditorFont(AppSettings::editorFont()); editor->applyTheme(AppSettings::editorDarkTheme()); editor->setIndentationGuides(m_indentGuidesEnabled ? SC_IV_LOOKBOTH : SC_IV_NONE); } void MainWindow::applySettingsToAllEditors() { for (int i = 0; i < m_tabWidget->count(); ++i) { applySettingsToEditor(m_tabWidget->editorAt(i)); } } void MainWindow::onDocumentModified(int docIndex, bool modified) { Q_UNUSED(docIndex) Q_UNUSED(modified) updateWindowTitle(); } void MainWindow::updateWindowTitle() { QString title = "Pineapple Notepad"; if (SciEdit *editor = m_tabWidget->currentEditor()) { int docId = m_tabWidget->currentDocumentId(); if (docId >= 0) { QString fileName = m_documentManager->getDocumentTitle(docId); bool isModified = m_documentManager->isDocumentModified(docId); title = QString("%1%2") .arg(fileName) .arg(isModified ? "*" : ""); } } setWindowTitle(title); } void MainWindow::updateStatusBar() { if (SciEdit *editor = m_tabWidget->currentEditor()) { int docId = m_tabWidget->currentDocumentId(); if (docId >= 0) { QString encoding = m_documentManager->getDocumentEncoding(docId); const QString lexerName = m_documentManager->getDocumentLanguage(docId); const QString language = lexerName.isEmpty() ? QStringLiteral("Plain Text") : LexerGroupActionMenu::displayName(lexerName); m_encodingStatusLabel->setText(encoding); m_languageStatusLabel->setText(language); } } else { m_encodingStatusLabel->setText("UTF-8"); m_languageStatusLabel->setText("Plain Text"); } } void MainWindow::updateActions() { bool hasEditor = (m_tabWidget->currentEditor() != nullptr); // 更新编辑相关的动作状态 if (QAction *undoAction = actionCollection()->action(KStandardAction::name(KStandardAction::Undo))) { undoAction->setEnabled(hasEditor); } if (QAction *redoAction = actionCollection()->action(KStandardAction::name(KStandardAction::Redo))) { redoAction->setEnabled(hasEditor); } if (QAction *cutAction = actionCollection()->action(KStandardAction::name(KStandardAction::Cut))) { cutAction->setEnabled(hasEditor); } if (QAction *copyAction = actionCollection()->action(KStandardAction::name(KStandardAction::Copy))) { copyAction->setEnabled(hasEditor); } if (QAction *pasteAction = actionCollection()->action(KStandardAction::name(KStandardAction::Paste))) { pasteAction->setEnabled(hasEditor && m_tabWidget->currentEditor()->canPaste()); } if (QAction *indentGuideAction = actionCollection()->action(QStringLiteral("toggle_indent_guide"))) { indentGuideAction->setEnabled(hasEditor); indentGuideAction->setChecked(m_indentGuidesEnabled); } } void MainWindow::showFindReplaceDialog(bool showReplace) { if (!m_findReplaceDialog) { m_findReplaceDialog = new FindReplaceDialog(this); connect(m_findReplaceDialog, &FindReplaceDialog::findRequested, this, [this](bool forward) { findNextInCurrentEditor(forward, true); }); connect(m_findReplaceDialog, &FindReplaceDialog::replaceRequested, this, [this]() { replaceOneInCurrentEditor(true); }); connect(m_findReplaceDialog, &FindReplaceDialog::replaceAllRequested, this, [this]() { replaceAllInCurrentEditor(true); }); } if (SciEdit *editor = m_tabWidget->currentEditor()) { const int selStart = static_cast(editor->selectionStart()); const int selEnd = static_cast(editor->selectionEnd()); if (selStart != selEnd) { const int start = qMin(selStart, selEnd); const int end = qMax(selStart, selEnd); const QByteArray selected = editor->textRange(start, end); m_findReplaceDialog->setFindTextIfNotEmpty(QString::fromUtf8(selected)); } } if (showReplace) { m_findReplaceDialog->openReplace(); } else { m_findReplaceDialog->openFind(); } } static sptr_t buildSearchFlags(const FindReplaceDialog *dialog) { sptr_t flags = 0; if (dialog->matchCase()) { flags |= SCFIND_MATCHCASE; } if (dialog->wholeWord()) { flags |= SCFIND_WHOLEWORD; } if (dialog->searchMode() == SearchMode::Regex) { flags |= SCFIND_REGEXP; } return flags; } static QString buildFindPattern(const FindReplaceDialog *dialog) { return dialog->findTextForSearch(); } static QString buildReplacementText(const FindReplaceDialog *dialog) { return dialog->replaceTextForReplace(); } bool MainWindow::findNextInCurrentEditor(bool forward, bool showNotFoundMessage) { SciEdit *editor = m_tabWidget->currentEditor(); if (!editor) { return false; } if (!m_findReplaceDialog) { m_findReplaceDialog = new FindReplaceDialog(this); connect(m_findReplaceDialog, &FindReplaceDialog::findRequested, this, [this](bool forward) { findNextInCurrentEditor(forward, true); }); connect(m_findReplaceDialog, &FindReplaceDialog::replaceRequested, this, [this]() { replaceOneInCurrentEditor(true); }); connect(m_findReplaceDialog, &FindReplaceDialog::replaceAllRequested, this, [this]() { replaceAllInCurrentEditor(true); }); } const QString needle = buildFindPattern(m_findReplaceDialog); if (needle.isEmpty()) { return false; } const QByteArray needleBytes = needle.toUtf8(); const sptr_t flags = buildSearchFlags(m_findReplaceDialog); const int docLen = static_cast(editor->textLength()); int rangeStart = 0; int rangeEnd = docLen; if (m_findReplaceDialog->inSelection()) { if (!m_findInSelectionEnabled) { const int selStart = static_cast(editor->selectionStart()); const int selEnd = static_cast(editor->selectionEnd()); if (selStart != selEnd) { m_findSelectionStart = qMin(selStart, selEnd); m_findSelectionEnd = qMax(selStart, selEnd); m_findInSelectionEnabled = true; } } } else { m_findInSelectionEnabled = false; } if (m_findInSelectionEnabled) { rangeStart = qBound(0, m_findSelectionStart, docLen); rangeEnd = qBound(0, m_findSelectionEnd, docLen); if (rangeStart >= rangeEnd) { m_findInSelectionEnabled = false; rangeStart = 0; rangeEnd = docLen; } } const int selStartNow = static_cast(editor->selectionStart()); const int selEndNow = static_cast(editor->selectionEnd()); int caretBase = static_cast(editor->currentPos()); if (forward && selStartNow != selEndNow) { caretBase = qMax(selStartNow, selEndNow); } caretBase = qBound(rangeStart, caretBase, rangeEnd); auto selectMatch = [&](int start, int end) { editor->setSel(start, end); editor->scrollCaret(); }; auto showNotFound = [&]() { if (!showNotFoundMessage) { return; } QMessageBox::information(this, QStringLiteral("Find"), QStringLiteral("Text not found.")); }; if (forward) { const QPair found = editor->findText(static_cast(flags), needleBytes.constData(), caretBase, rangeEnd); if (found.first >= 0) { selectMatch(found.first, found.second); return true; } if (m_findReplaceDialog->wrapAround()) { const QPair wrapped = editor->findText(static_cast(flags), needleBytes.constData(), rangeStart, rangeEnd); if (wrapped.first >= 0) { selectMatch(wrapped.first, wrapped.second); return true; } } showNotFound(); return false; } auto findLastBefore = [&](int endExclusive) -> QPair { int bestStart = -1; int bestEnd = -1; int scanPos = rangeStart; while (scanPos < endExclusive) { const QPair found = editor->findText(static_cast(flags), needleBytes.constData(), scanPos, endExclusive); if (found.first < 0 || found.first >= endExclusive) { break; } bestStart = found.first; bestEnd = found.second; scanPos = found.first + 1; } return QPair(bestStart, bestEnd); }; QPair found = findLastBefore(caretBase); if (found.first >= 0) { selectMatch(found.first, found.second); return true; } if (m_findReplaceDialog->wrapAround()) { found = findLastBefore(rangeEnd); if (found.first >= 0) { selectMatch(found.first, found.second); return true; } } showNotFound(); return false; } bool MainWindow::replaceOneInCurrentEditor(bool showNotFoundMessage) { SciEdit *editor = m_tabWidget->currentEditor(); if (!editor) { return false; } if (!m_findReplaceDialog) { m_findReplaceDialog = new FindReplaceDialog(this); connect(m_findReplaceDialog, &FindReplaceDialog::findRequested, this, [this](bool forward) { findNextInCurrentEditor(forward, true); }); connect(m_findReplaceDialog, &FindReplaceDialog::replaceRequested, this, [this]() { replaceOneInCurrentEditor(true); }); connect(m_findReplaceDialog, &FindReplaceDialog::replaceAllRequested, this, [this]() { replaceAllInCurrentEditor(true); }); } const QString needle = buildFindPattern(m_findReplaceDialog); if (needle.isEmpty()) { return false; } if (!findNextInCurrentEditor(true, showNotFoundMessage)) { return false; } const QString replacement = buildReplacementText(m_findReplaceDialog); const QByteArray replacementBytes = replacement.toUtf8(); editor->targetFromSelection(); if (m_findReplaceDialog->searchMode() == SearchMode::Regex) { editor->replaceTargetRE(replacementBytes.size(), replacementBytes.constData()); } else { editor->replaceTarget(replacementBytes.size(), replacementBytes.constData()); } findNextInCurrentEditor(true, false); return true; } int MainWindow::replaceAllInCurrentEditor(bool showNotFoundMessage) { SciEdit *editor = m_tabWidget->currentEditor(); if (!editor) { return 0; } if (!m_findReplaceDialog) { m_findReplaceDialog = new FindReplaceDialog(this); connect(m_findReplaceDialog, &FindReplaceDialog::findRequested, this, [this](bool forward) { findNextInCurrentEditor(forward, true); }); connect(m_findReplaceDialog, &FindReplaceDialog::replaceRequested, this, [this]() { replaceOneInCurrentEditor(true); }); connect(m_findReplaceDialog, &FindReplaceDialog::replaceAllRequested, this, [this]() { replaceAllInCurrentEditor(true); }); } const QString needle = buildFindPattern(m_findReplaceDialog); if (needle.isEmpty()) { return 0; } const QByteArray needleBytes = needle.toUtf8(); const QString replacement = buildReplacementText(m_findReplaceDialog); const QByteArray replacementBytes = replacement.toUtf8(); const sptr_t flags = buildSearchFlags(m_findReplaceDialog); const int docLen = static_cast(editor->textLength()); int rangeStart = 0; int rangeEnd = docLen; if (m_findReplaceDialog->inSelection()) { if (!m_findInSelectionEnabled) { const int selStart = static_cast(editor->selectionStart()); const int selEnd = static_cast(editor->selectionEnd()); if (selStart != selEnd) { m_findSelectionStart = qMin(selStart, selEnd); m_findSelectionEnd = qMax(selStart, selEnd); m_findInSelectionEnabled = true; } } } else { m_findInSelectionEnabled = false; } if (m_findInSelectionEnabled) { rangeStart = qBound(0, m_findSelectionStart, docLen); rangeEnd = qBound(0, m_findSelectionEnd, docLen); if (rangeStart >= rangeEnd) { m_findInSelectionEnabled = false; rangeStart = 0; rangeEnd = docLen; } } editor->setSearchFlags(flags); int replacedCount = 0; int start = rangeStart; int end = rangeEnd; editor->beginUndoAction(); while (start <= end) { editor->setTargetRange(start, end); const sptr_t pos = editor->searchInTarget(needleBytes.size(), needleBytes.constData()); if (pos < 0) { break; } const int matchStart = static_cast(editor->targetStart()); const int matchEnd = static_cast(editor->targetEnd()); const int matchLen = matchEnd - matchStart; sptr_t replacedLen = 0; if (m_findReplaceDialog->searchMode() == SearchMode::Regex) { replacedLen = editor->replaceTargetRE(replacementBytes.size(), replacementBytes.constData()); } else { replacedLen = editor->replaceTarget(replacementBytes.size(), replacementBytes.constData()); } ++replacedCount; const int delta = static_cast(replacedLen) - matchLen; end += delta; start = matchStart + static_cast(replacedLen); } editor->endUndoAction(); if (replacedCount == 0 && showNotFoundMessage) { QMessageBox::information(this, QStringLiteral("Replace"), QStringLiteral("Text not found.")); } return replacedCount; }