diff --git a/CMakeLists.txt b/CMakeLists.txt index 845d14c..6c3d3cb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,104 +1,103 @@ cmake_minimum_required(VERSION 3.5) set(KF5_VERSION "5.64.0") # handled by release scripts set(KF5_DEP_VERSION "5.63.0") # handled by release scripts project(KTextWidgets VERSION ${KF5_VERSION}) # ECM setup include(FeatureSummary) find_package(ECM 5.63.0 NO_MODULE) set_package_properties(ECM PROPERTIES TYPE REQUIRED DESCRIPTION "Extra CMake Modules." URL "https://projects.kde.org/projects/kdesupport/extra-cmake-modules") feature_summary(WHAT REQUIRED_PACKAGES_NOT_FOUND FATAL_ON_MISSING_REQUIRED_PACKAGES) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) include(KDEInstallDirs) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(KDECMakeSettings) include(ECMGenerateExportHeader) include(ECMSetupVersion) include(ECMGenerateHeaders) include(CMakePackageConfigHelpers) include(ECMAddQch) ecm_setup_version(PROJECT VARIABLE_PREFIX KTEXTWIDGETS VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/ktextwidgets_version.h" PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KF5TextWidgetsConfigVersion.cmake" SOVERSION 5) # Dependencies set(REQUIRED_QT_VERSION 5.11.0) find_package(Qt5 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Widgets) find_package(Qt5 OPTIONAL_COMPONENTS TextToSpeech) if (NOT Qt5TextToSpeech_FOUND) message(STATUS "Qt5TextToSpeech not found, speech feature will be disabled") else() add_definitions(-DHAVE_SPEECH) endif() find_package(KF5Completion ${KF5_DEP_VERSION} REQUIRED) find_package(KF5Config ${KF5_DEP_VERSION} REQUIRED) find_package(KF5ConfigWidgets ${KF5_DEP_VERSION} REQUIRED) find_package(KF5I18n ${KF5_DEP_VERSION} REQUIRED) -find_package(KF5IconThemes ${KF5_DEP_VERSION} REQUIRED) find_package(KF5Service ${KF5_DEP_VERSION} REQUIRED) find_package(KF5WidgetsAddons ${KF5_DEP_VERSION} REQUIRED) find_package(KF5WindowSystem ${KF5_DEP_VERSION} REQUIRED) find_package(KF5Sonnet ${KF5_DEP_VERSION} REQUIRED) set(EXCLUDE_DEPRECATED_BEFORE_AND_AT 0 CACHE STRING "Control the range of deprecated API excluded from the build [default=0].") option(BUILD_QCH "Build API documentation in QCH format (for e.g. Qt Assistant, Qt Creator & KDevelop)" OFF) add_feature_info(QCH ${BUILD_QCH} "API documentation in QCH format (for e.g. Qt Assistant, Qt Creator & KDevelop)") option(BUILD_DESIGNERPLUGIN "Build plugin for Qt Designer" ON) add_feature_info(DESIGNERPLUGIN ${BUILD_DESIGNERPLUGIN} "Build plugin for Qt Designer") add_definitions(-DTRANSLATION_DOMAIN=\"ktextwidgets5\") add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x050d00) add_definitions(-DQT_DEPRECATED_WARNINGS_SINCE=0x060000) if (IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/po") ki18n_install(po) endif() add_definitions(-DQT_NO_FOREACH) add_subdirectory(src) if (BUILD_TESTING) add_subdirectory(tests) add_subdirectory(autotests) endif() # create a Config.cmake and a ConfigVersion.cmake file and install them set(CMAKECONFIG_INSTALL_DIR "${KDE_INSTALL_CMAKEPACKAGEDIR}/KF5TextWidgets") if (BUILD_QCH) ecm_install_qch_export( TARGETS KF5TextWidgets_QCH FILE KF5TextWidgetsQchTargets.cmake DESTINATION "${CMAKECONFIG_INSTALL_DIR}" COMPONENT Devel ) set(PACKAGE_INCLUDE_QCHTARGETS "include(\"\${CMAKE_CURRENT_LIST_DIR}/KF5TextWidgetsQchTargets.cmake\")") endif() configure_package_config_file("${CMAKE_CURRENT_SOURCE_DIR}/KF5TextWidgetsConfig.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/KF5TextWidgetsConfig.cmake" INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR} ) install(EXPORT KF5TextWidgetsTargets DESTINATION "${CMAKECONFIG_INSTALL_DIR}" FILE KF5TextWidgetsTargets.cmake NAMESPACE KF5:: ) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/KF5TextWidgetsConfig.cmake" "${CMAKE_CURRENT_BINARY_DIR}/KF5TextWidgetsConfigVersion.cmake" DESTINATION "${CMAKECONFIG_INSTALL_DIR}" COMPONENT Devel ) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/ktextwidgets_version.h DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5} COMPONENT Devel ) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index effb411..8d6dcb8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,135 +1,134 @@ set(ktextwidgets_LIB_SRCS dialogs/klinkdialog.cpp findreplace/kfind.cpp findreplace/kfinddialog.cpp findreplace/kreplace.cpp findreplace/kreplacedialog.cpp widgets/krichtextedit.cpp widgets/krichtextwidget.cpp widgets/ktextedit.cpp widgets/nestedlisthelper.cpp widgets/kpluralhandlingspinbox.cpp ) add_library(KF5TextWidgets ${ktextwidgets_LIB_SRCS}) add_library(KF5::TextWidgets ALIAS KF5TextWidgets) ecm_generate_export_header(KF5TextWidgets BASE_NAME KTextWidgets GROUP_BASE_NAME KF VERSION ${KF5_VERSION} DEPRECATED_BASE_VERSION 0 DEPRECATION_VERSIONS 5.0 EXCLUDE_DEPRECATED_BEFORE_AND_AT ${EXCLUDE_DEPRECATED_BEFORE_AND_AT} ) set(ktextwidgets_INCLUDES ${CMAKE_CURRENT_SOURCE_DIR}/dialogs ${CMAKE_CURRENT_SOURCE_DIR}/findreplace ${CMAKE_CURRENT_SOURCE_DIR}/kregexpeditor ${CMAKE_CURRENT_SOURCE_DIR}/widgets ) target_include_directories(KF5TextWidgets PUBLIC "$" ) target_include_directories(KF5TextWidgets INTERFACE "$") target_link_libraries(KF5TextWidgets PUBLIC Qt5::Widgets KF5::SonnetUi KF5::I18n PRIVATE KF5::SonnetCore KF5::Service KF5::ConfigWidgets KF5::WindowSystem - KF5::IconThemes KF5::Completion ) if (Qt5TextToSpeech_FOUND) target_link_libraries(KF5TextWidgets PRIVATE Qt5::TextToSpeech) endif() set_target_properties(KF5TextWidgets PROPERTIES VERSION ${KTEXTWIDGETS_VERSION_STRING} SOVERSION ${KTEXTWIDGETS_SOVERSION} EXPORT_NAME TextWidgets ) ecm_generate_headers(KTextWidgets_HEADERS HEADER_NAMES KRichTextEdit KRichTextWidget KTextEdit KPluralHandlingSpinBox RELATIVE widgets REQUIRED_HEADERS KTextWidgets_HEADERS ) ecm_generate_headers(KTextWidgets_HEADERS HEADER_NAMES KFind KFindDialog KReplace KReplaceDialog RELATIVE findreplace REQUIRED_HEADERS KTextWidgets_HEADERS ) ecm_generate_headers(KTextWidgets_HEADERS HEADER_NAMES KRegExpEditorInterface RELATIVE kregexpeditor REQUIRED_HEADERS KTextWidgets_HEADERS ) install(TARGETS KF5TextWidgets EXPORT KF5TextWidgetsTargets ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/ktextwidgets_export.h ${KTextWidgets_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KTextWidgets COMPONENT Devel ) install( FILES kregexpeditor/kregexpeditor.desktop DESTINATION ${KDE_INSTALL_KSERVICETYPES5DIR} ) if (BUILD_DESIGNERPLUGIN) add_subdirectory(designer) endif() if (BUILD_QCH) ecm_add_qch( KF5TextWidgets_QCH NAME KTextWidgets BASE_NAME KF5TextWidgets VERSION ${KF5_VERSION} ORG_DOMAIN org.kde SOURCES # using only public headers, to cover only public API ${KTextWidgets_HEADERS} MD_MAINPAGE "${CMAKE_SOURCE_DIR}/README.md" IMAGE_DIRS "${CMAKE_SOURCE_DIR}/docs/pics" LINK_QCHS Qt5Widgets_QCH KF5SonnetUi_QCH KF5I18n_QCH INCLUDE_DIRS ${CMAKE_CURRENT_BINARY_DIR} ${ktextwidgets_INCLUDES} BLANK_MACROS KTEXTWIDGETS_EXPORT KTEXTWIDGETS_DEPRECATED_EXPORT KTEXTWIDGETS_DEPRECATED "KTEXTWIDGETS_DEPRECATED_VERSION(x, y, t)" TAGFILE_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} QCH_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} COMPONENT Devel ) endif() include(ECMGeneratePriFile) ecm_generate_pri_file(BASE_NAME KTextWidgets LIB_NAME KF5TextWidgets DEPS "widgets SonnetUi KI18n" FILENAME_VAR PRI_FILENAME INCLUDE_INSTALL_DIR ${KDE_INSTALL_INCLUDEDIR_KF5}/KTextWidgets) install(FILES ${PRI_FILENAME} DESTINATION ${ECM_MKSPECS_INSTALL_DIR}) diff --git a/src/widgets/ktextedit.cpp b/src/widgets/ktextedit.cpp index 4ba6719..20d44e4 100644 --- a/src/widgets/ktextedit.cpp +++ b/src/widgets/ktextedit.cpp @@ -1,1067 +1,1063 @@ /* This file is part of the KDE libraries Copyright (C) 2002 Carsten Pfeiffer 2005 Michael Brade 2012 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ktextedit.h" #include #include #include #include #include #include #include #include #include #ifdef HAVE_SPEECH #include #endif #include #include #include #include #include #include #include -#include #include #include #include #include #include "kreplacedialog.h" #include "kfinddialog.h" #include "kfind.h" #include "kreplace.h" class KTextDecorator : public Sonnet::SpellCheckDecorator { public: explicit KTextDecorator(KTextEdit *textEdit); bool isSpellCheckingEnabledForBlock(const QString &textBlock) const override; private: KTextEdit *m_textEdit; }; class Q_DECL_HIDDEN KTextEdit::Private { public: Private(KTextEdit *_parent) : parent(_parent), languagesMenu(nullptr), customPalette(false), spellCheckingEnabled(false), findReplaceEnabled(true), showTabAction(true), showAutoCorrectionButton(false), decorator(nullptr), speller(nullptr), findDlg(nullptr), find(nullptr), repDlg(nullptr), replace(nullptr), #ifdef HAVE_SPEECH textToSpeech(nullptr), #endif findIndex(0), repIndex(0), lastReplacedPosition(-1) { //Check the default sonnet settings to see if spellchecking should be enabled. QSettings settings(QStringLiteral("KDE"), QStringLiteral("Sonnet")); spellCheckingEnabled = settings.value(QStringLiteral("checkerEnabledByDefault"), false).toBool(); } ~Private() { delete decorator; delete findDlg; delete find; delete replace; delete repDlg; delete speller; #ifdef HAVE_SPEECH delete textToSpeech; #endif } /** * Checks whether we should/should not consume a key used as a shortcut. * This makes it possible to handle shortcuts in the focused widget before any * window-global QAction is triggered. */ bool overrideShortcut(const QKeyEvent *e); /** * Actually handle a shortcut event. */ bool handleShortcut(const QKeyEvent *e); void spellCheckerMisspelling(const QString &text, int pos); void spellCheckerCorrected(const QString &, int, const QString &); void spellCheckerAutoCorrect(const QString &, const QString &); void spellCheckerCanceled(); void spellCheckerFinished(); void toggleAutoSpellCheck(); void slotFindHighlight(const QString &text, int matchingIndex, int matchingLength); void slotReplaceText(const QString &text, int replacementIndex, int /*replacedLength*/, int matchedLength); /** * Similar to QTextEdit::clear(), only that it is possible to undo this * action. */ void undoableClear(); void slotAllowTab(); void menuActivated(QAction *action); void init(); void checkSpelling(bool force); KTextEdit *parent; QAction *autoSpellCheckAction; QAction *allowTab; QAction *spellCheckAction; QMenu *languagesMenu; bool customPalette : 1; bool spellCheckingEnabled : 1; bool findReplaceEnabled: 1; bool showTabAction: 1; bool showAutoCorrectionButton: 1; QTextDocumentFragment originalDoc; QString spellCheckingLanguage; Sonnet::SpellCheckDecorator *decorator; Sonnet::Speller *speller; KFindDialog *findDlg; KFind *find; KReplaceDialog *repDlg; KReplace *replace; #ifdef HAVE_SPEECH QTextToSpeech *textToSpeech; #endif int findIndex, repIndex; int lastReplacedPosition; }; void KTextEdit::Private::checkSpelling(bool force) { if (parent->document()->isEmpty()) { KMessageBox::information(parent, i18n("Nothing to spell check.")); if (force) { emit parent->spellCheckingFinished(); } return; } Sonnet::BackgroundChecker *backgroundSpellCheck = new Sonnet::BackgroundChecker; if (!spellCheckingLanguage.isEmpty()) { backgroundSpellCheck->changeLanguage(spellCheckingLanguage); } Sonnet::Dialog *spellDialog = new Sonnet::Dialog( backgroundSpellCheck, force ? parent : nullptr); backgroundSpellCheck->setParent(spellDialog); spellDialog->setAttribute(Qt::WA_DeleteOnClose, true); spellDialog->activeAutoCorrect(showAutoCorrectionButton); connect(spellDialog, SIGNAL(replace(QString,int,QString)), parent, SLOT(spellCheckerCorrected(QString,int,QString))); connect(spellDialog, SIGNAL(misspelling(QString,int)), parent, SLOT(spellCheckerMisspelling(QString,int))); connect(spellDialog, &Sonnet::Dialog::autoCorrect, parent, &KTextEdit::spellCheckerAutoCorrect); connect(spellDialog, SIGNAL(done(QString)), parent, SLOT(spellCheckerFinished())); connect(spellDialog, SIGNAL(cancel()), parent, SLOT(spellCheckerCanceled())); //Laurent in sonnet/dialog.cpp we emit done(QString) too => it calls here twice spellCheckerFinished not necessary /* connect(spellDialog, SIGNAL(stop()), parent, SLOT(spellCheckerFinished())); */ connect(spellDialog, &Sonnet::Dialog::spellCheckStatus, parent, &KTextEdit::spellCheckStatus); connect(spellDialog, &Sonnet::Dialog::languageChanged, parent, &KTextEdit::languageChanged); if (force) { connect(spellDialog, SIGNAL(done(QString)), parent, SIGNAL(spellCheckingFinished())); connect(spellDialog, &Sonnet::Dialog::cancel, parent, &KTextEdit::spellCheckingCanceled); //Laurent in sonnet/dialog.cpp we emit done(QString) too => it calls here twice spellCheckerFinished not necessary //connect(spellDialog, SIGNAL(stop()), parent, SIGNAL(spellCheckingFinished())); } originalDoc = QTextDocumentFragment(parent->document()); spellDialog->setBuffer(parent->toPlainText()); spellDialog->show(); } void KTextEdit::Private::spellCheckerCanceled() { QTextDocument *doc = parent->document(); doc->clear(); QTextCursor cursor(doc); cursor.insertFragment(originalDoc); spellCheckerFinished(); } void KTextEdit::Private::spellCheckerAutoCorrect(const QString ¤tWord, const QString &autoCorrectWord) { emit parent->spellCheckerAutoCorrect(currentWord, autoCorrectWord); } void KTextEdit::Private::spellCheckerMisspelling(const QString &text, int pos) { //qDebug()<<"TextEdit::Private::spellCheckerMisspelling :"<highlightWord(text.length(), pos); } void KTextEdit::Private::spellCheckerCorrected(const QString &oldWord, int pos, const QString &newWord) { //qDebug()<<" oldWord :"<document()); cursor.setPosition(pos); cursor.setPosition(pos + oldWord.length(), QTextCursor::KeepAnchor); cursor.insertText(newWord); } } void KTextEdit::Private::spellCheckerFinished() { QTextCursor cursor(parent->document()); cursor.clearSelection(); parent->setTextCursor(cursor); if (parent->highlighter()) { parent->highlighter()->rehighlight(); } } void KTextEdit::Private::toggleAutoSpellCheck() { parent->setCheckSpellingEnabled(!parent->checkSpellingEnabled()); } void KTextEdit::Private::undoableClear() { QTextCursor cursor = parent->textCursor(); cursor.beginEditBlock(); cursor.movePosition(QTextCursor::Start); cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); cursor.removeSelectedText(); cursor.endEditBlock(); } void KTextEdit::Private::slotAllowTab() { parent->setTabChangesFocus(!parent->tabChangesFocus()); } void KTextEdit::Private::menuActivated(QAction *action) { if (action == spellCheckAction) { parent->checkSpelling(); } else if (action == autoSpellCheckAction) { toggleAutoSpellCheck(); } else if (action == allowTab) { slotAllowTab(); } } void KTextEdit::Private::slotFindHighlight(const QString &text, int matchingIndex, int matchingLength) { Q_UNUSED(text) //qDebug() << "Highlight: [" << text << "] mi:" << matchingIndex << " ml:" << matchingLength; QTextCursor tc = parent->textCursor(); tc.setPosition(matchingIndex); tc.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, matchingLength); parent->setTextCursor(tc); parent->ensureCursorVisible(); } void KTextEdit::Private::slotReplaceText(const QString &text, int replacementIndex, int replacedLength, int matchedLength) { //qDebug() << "Replace: [" << text << "] ri:" << replacementIndex << " rl:" << replacedLength << " ml:" << matchedLength; QTextCursor tc = parent->textCursor(); tc.setPosition(replacementIndex); tc.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, matchedLength); tc.removeSelectedText(); tc.insertText(text.mid(replacementIndex, replacedLength)); if (replace->options() & KReplaceDialog::PromptOnReplace) { parent->setTextCursor(tc); parent->ensureCursorVisible(); } lastReplacedPosition = replacementIndex; } void KTextEdit::Private::init() { KCursor::setAutoHideCursor(parent, true, false); parent->connect(parent, &KTextEdit::languageChanged, parent, &KTextEdit::setSpellCheckingLanguage); } KTextDecorator::KTextDecorator(KTextEdit *textEdit): SpellCheckDecorator(textEdit), m_textEdit(textEdit) { } bool KTextDecorator::isSpellCheckingEnabledForBlock(const QString &textBlock) const { return m_textEdit->shouldBlockBeSpellChecked(textBlock); } KTextEdit::KTextEdit(const QString &text, QWidget *parent) : QTextEdit(text, parent), d(new Private(this)) { d->init(); } KTextEdit::KTextEdit(QWidget *parent) : QTextEdit(parent), d(new Private(this)) { d->init(); } KTextEdit::~KTextEdit() { delete d; } const QString &KTextEdit::spellCheckingLanguage() const { return d->spellCheckingLanguage; } void KTextEdit::setSpellCheckingLanguage(const QString &_language) { if (highlighter()) { highlighter()->setCurrentLanguage(_language); highlighter()->rehighlight(); } if (_language != d->spellCheckingLanguage) { d->spellCheckingLanguage = _language; emit languageChanged(_language); } } void KTextEdit::showSpellConfigDialog(const QString &windowIcon) { Sonnet::ConfigDialog dialog(this); if (!d->spellCheckingLanguage.isEmpty()) { dialog.setLanguage(d->spellCheckingLanguage); } if (!windowIcon.isEmpty()) { dialog.setWindowIcon(QIcon::fromTheme(windowIcon, dialog.windowIcon())); } if (dialog.exec()) { setSpellCheckingLanguage(dialog.language()); } } bool KTextEdit::event(QEvent *ev) { if (ev->type() == QEvent::ShortcutOverride) { QKeyEvent *e = static_cast(ev); if (d->overrideShortcut(e)) { e->accept(); return true; } } return QTextEdit::event(ev); } bool KTextEdit::Private::handleShortcut(const QKeyEvent *event) { const int key = event->key() | event->modifiers(); if (KStandardShortcut::copy().contains(key)) { parent->copy(); return true; } else if (KStandardShortcut::paste().contains(key)) { parent->paste(); return true; } else if (KStandardShortcut::cut().contains(key)) { parent->cut(); return true; } else if (KStandardShortcut::undo().contains(key)) { if (!parent->isReadOnly()) { parent->undo(); } return true; } else if (KStandardShortcut::redo().contains(key)) { if (!parent->isReadOnly()) { parent->redo(); } return true; } else if (KStandardShortcut::deleteWordBack().contains(key)) { if (!parent->isReadOnly()) { parent->deleteWordBack(); } return true; } else if (KStandardShortcut::deleteWordForward().contains(key)) { if (!parent->isReadOnly()) { parent->deleteWordForward(); } return true; } else if (KStandardShortcut::backwardWord().contains(key)) { QTextCursor cursor = parent->textCursor(); cursor.movePosition(QTextCursor::PreviousWord); parent->setTextCursor(cursor); return true; } else if (KStandardShortcut::forwardWord().contains(key)) { QTextCursor cursor = parent->textCursor(); cursor.movePosition(QTextCursor::NextWord); parent->setTextCursor(cursor); return true; } else if (KStandardShortcut::next().contains(key)) { QTextCursor cursor = parent->textCursor(); bool moved = false; qreal lastY = parent->cursorRect(cursor).bottom(); qreal distance = 0; do { qreal y = parent->cursorRect(cursor).bottom(); distance += qAbs(y - lastY); lastY = y; moved = cursor.movePosition(QTextCursor::Down); } while (moved && distance < parent->viewport()->height()); if (moved) { cursor.movePosition(QTextCursor::Up); parent->verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepAdd); } parent->setTextCursor(cursor); return true; } else if (KStandardShortcut::prior().contains(key)) { QTextCursor cursor = parent->textCursor(); bool moved = false; qreal lastY = parent->cursorRect(cursor).bottom(); qreal distance = 0; do { qreal y = parent->cursorRect(cursor).bottom(); distance += qAbs(y - lastY); lastY = y; moved = cursor.movePosition(QTextCursor::Up); } while (moved && distance < parent->viewport()->height()); if (moved) { cursor.movePosition(QTextCursor::Down); parent->verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepSub); } parent->setTextCursor(cursor); return true; } else if (KStandardShortcut::begin().contains(key)) { QTextCursor cursor = parent->textCursor(); cursor.movePosition(QTextCursor::Start); parent->setTextCursor(cursor); return true; } else if (KStandardShortcut::end().contains(key)) { QTextCursor cursor = parent->textCursor(); cursor.movePosition(QTextCursor::End); parent->setTextCursor(cursor); return true; } else if (KStandardShortcut::beginningOfLine().contains(key)) { QTextCursor cursor = parent->textCursor(); cursor.movePosition(QTextCursor::StartOfLine); parent->setTextCursor(cursor); return true; } else if (KStandardShortcut::endOfLine().contains(key)) { QTextCursor cursor = parent->textCursor(); cursor.movePosition(QTextCursor::EndOfLine); parent->setTextCursor(cursor); return true; } else if (findReplaceEnabled && KStandardShortcut::find().contains(key)) { parent->slotFind(); return true; } else if (findReplaceEnabled && KStandardShortcut::findNext().contains(key)) { parent->slotFindNext(); return true; } else if (findReplaceEnabled && KStandardShortcut::findPrev().contains(key)) { parent->slotFindPrevious(); return true; } else if (findReplaceEnabled && KStandardShortcut::replace().contains(key)) { if (!parent->isReadOnly()) { parent->slotReplace(); } return true; } else if (KStandardShortcut::pasteSelection().contains(key)) { QString text = QApplication::clipboard()->text(QClipboard::Selection); if (!text.isEmpty()) { parent->insertPlainText(text); // TODO: check if this is html? (MiB) } return true; } return false; } static void deleteWord(QTextCursor cursor, QTextCursor::MoveOperation op) { cursor.clearSelection(); cursor.movePosition(op, QTextCursor::KeepAnchor); cursor.removeSelectedText(); } void KTextEdit::deleteWordBack() { deleteWord(textCursor(), QTextCursor::PreviousWord); } void KTextEdit::deleteWordForward() { deleteWord(textCursor(), QTextCursor::WordRight); } QMenu *KTextEdit::mousePopupMenu() { QMenu *popup = createStandardContextMenu(); if (!popup) { return nullptr; } connect(popup, SIGNAL(triggered(QAction*)), this, SLOT(menuActivated(QAction*))); const bool emptyDocument = document()->isEmpty(); if (!isReadOnly()) { QList actionList = popup->actions(); enum { UndoAct, RedoAct, CutAct, CopyAct, PasteAct, ClearAct, SelectAllAct, NCountActs }; QAction *separatorAction = nullptr; int idx = actionList.indexOf(actionList[SelectAllAct]) + 1; if (idx < actionList.count()) { separatorAction = actionList.at(idx); } if (separatorAction) { QAction *clearAllAction = KStandardAction::clear(this, SLOT(undoableClear()), popup); if (emptyDocument) { clearAllAction->setEnabled(false); } popup->insertAction(separatorAction, clearAllAction); } } - KIconTheme::assignIconsToContextMenu(isReadOnly() ? KIconTheme::ReadOnlyText - : KIconTheme::TextEditor, - popup->actions()); if (!isReadOnly()) { popup->addSeparator(); if (!d->speller) { d->speller = new Sonnet::Speller(); } if (!d->speller->availableBackends().isEmpty()) { d->spellCheckAction = popup->addAction(QIcon::fromTheme(QStringLiteral("tools-check-spelling")), i18n("Check Spelling...")); if (emptyDocument) { d->spellCheckAction->setEnabled(false); } if (checkSpellingEnabled()) { d->languagesMenu = new QMenu(i18n("Spell Checking Language"), popup); QActionGroup *languagesGroup = new QActionGroup(d->languagesMenu); languagesGroup->setExclusive(true); QMapIterator i(d->speller->availableDictionaries()); const QString language = spellCheckingLanguage(); while (i.hasNext()) { i.next(); QAction *languageAction = d->languagesMenu->addAction(i.key()); languageAction->setCheckable(true); languageAction->setChecked(language == i.value() || (language.isEmpty() && d->speller->defaultLanguage() == i.value())); languageAction->setData(i.value()); languageAction->setActionGroup(languagesGroup); connect(languageAction, &QAction::triggered, [this, languageAction]() { setSpellCheckingLanguage(languageAction->data().toString()); }); } popup->addMenu(d->languagesMenu); } d->autoSpellCheckAction = popup->addAction(i18n("Auto Spell Check")); d->autoSpellCheckAction->setCheckable(true); d->autoSpellCheckAction->setChecked(checkSpellingEnabled()); popup->addSeparator(); } if (d->showTabAction) { d->allowTab = popup->addAction(i18n("Allow Tabulations")); d->allowTab->setCheckable(true); d->allowTab->setChecked(!tabChangesFocus()); } } if (d->findReplaceEnabled) { QAction *findAction = KStandardAction::find(this, SLOT(slotFind()), popup); QAction *findNextAction = KStandardAction::findNext(this, SLOT(slotFindNext()), popup); QAction *findPrevAction = KStandardAction::findPrev(this, SLOT(slotFindPrevious()), popup); if (emptyDocument) { findAction->setEnabled(false); findNextAction->setEnabled(false); findPrevAction->setEnabled(false); } else { findNextAction->setEnabled(d->find != nullptr); findPrevAction->setEnabled(d->find != nullptr); } popup->addSeparator(); popup->addAction(findAction); popup->addAction(findNextAction); popup->addAction(findPrevAction); if (!isReadOnly()) { QAction *replaceAction = KStandardAction::replace(this, SLOT(slotReplace()), popup); if (emptyDocument) { replaceAction->setEnabled(false); } popup->addAction(replaceAction); } } #ifdef HAVE_SPEECH popup->addSeparator(); QAction *speakAction = popup->addAction(i18n("Speak Text")); speakAction->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-text-to-speech"))); speakAction->setEnabled(!emptyDocument); connect(speakAction, &QAction::triggered, this, &KTextEdit::slotSpeakText); #endif return popup; } void KTextEdit::slotSpeakText() { #ifdef HAVE_SPEECH QString text; if (textCursor().hasSelection()) { text = textCursor().selectedText(); } else { text = toPlainText(); } if (!d->textToSpeech) { d->textToSpeech = new QTextToSpeech(this); } d->textToSpeech->say(text); #endif } void KTextEdit::contextMenuEvent(QContextMenuEvent *event) { QMenu *popup = mousePopupMenu(); if (popup) { aboutToShowContextMenu(popup); popup->exec(event->globalPos()); delete popup; } } void KTextEdit::createHighlighter() { setHighlighter(new Sonnet::Highlighter(this)); } Sonnet::Highlighter *KTextEdit::highlighter() const { if (d->decorator) { return d->decorator->highlighter(); } else { return nullptr; } } void KTextEdit::clearDecorator() { delete d->decorator; d->decorator = nullptr; } void KTextEdit::addTextDecorator(Sonnet::SpellCheckDecorator *decorator) { d->decorator = decorator; } void KTextEdit::setHighlighter(Sonnet::Highlighter *_highLighter) { KTextDecorator *decorator = new KTextDecorator(this); // The old default highlighter must be manually deleted. delete decorator->highlighter(); decorator->setHighlighter(_highLighter); //KTextEdit used to take ownership of the highlighter, Sonnet::SpellCheckDecorator does not. //so we reparent the highlighter so it will be deleted when the decorator is destroyed _highLighter->setParent(decorator); addTextDecorator(decorator); } void KTextEdit::setCheckSpellingEnabled(bool check) { emit checkSpellingChanged(check); if (check == d->spellCheckingEnabled) { return; } // From the above statment we know know that if we're turning checking // on that we need to create a new highlighter and if we're turning it // off we should remove the old one. d->spellCheckingEnabled = check; if (check) { if (hasFocus()) { createHighlighter(); if (!spellCheckingLanguage().isEmpty()) { setSpellCheckingLanguage(spellCheckingLanguage()); } } } else { clearDecorator(); } } void KTextEdit::focusInEvent(QFocusEvent *event) { if (d->spellCheckingEnabled && !isReadOnly() && !d->decorator) { createHighlighter(); } QTextEdit::focusInEvent(event); } bool KTextEdit::checkSpellingEnabled() const { return d->spellCheckingEnabled; } bool KTextEdit::shouldBlockBeSpellChecked(const QString &) const { return true; } void KTextEdit::setReadOnly(bool readOnly) { if (!readOnly && hasFocus() && d->spellCheckingEnabled && !d->decorator) { createHighlighter(); } if (readOnly == isReadOnly()) { return; } if (readOnly) { delete d->decorator; d->decorator = nullptr; d->customPalette = testAttribute(Qt::WA_SetPalette); QPalette p = palette(); QColor color = p.color(QPalette::Disabled, QPalette::Window); p.setColor(QPalette::Base, color); p.setColor(QPalette::Window, color); setPalette(p); } else { if (d->customPalette && testAttribute(Qt::WA_SetPalette)) { QPalette p = palette(); QColor color = p.color(QPalette::Normal, QPalette::Base); p.setColor(QPalette::Base, color); p.setColor(QPalette::Window, color); setPalette(p); } else { setPalette(QPalette()); } } QTextEdit::setReadOnly(readOnly); } void KTextEdit::checkSpelling() { d->checkSpelling(false); } void KTextEdit::forceSpellChecking() { d->checkSpelling(true); } void KTextEdit::highlightWord(int length, int pos) { QTextCursor cursor(document()); cursor.setPosition(pos); cursor.setPosition(pos + length, QTextCursor::KeepAnchor); setTextCursor(cursor); ensureCursorVisible(); } void KTextEdit::replace() { if (document()->isEmpty()) { // saves having to track the text changes return; } if (d->repDlg) { KWindowSystem::activateWindow(d->repDlg->winId()); } else { d->repDlg = new KReplaceDialog(this, 0, QStringList(), QStringList(), false); connect(d->repDlg, &KFindDialog::okClicked, this, &KTextEdit::slotDoReplace); } d->repDlg->show(); } void KTextEdit::slotDoReplace() { if (!d->repDlg) { // Should really assert() return; } if (d->repDlg->pattern().isEmpty()) { delete d->replace; d->replace = nullptr; ensureCursorVisible(); return; } delete d->replace; d->replace = new KReplace(d->repDlg->pattern(), d->repDlg->replacement(), d->repDlg->options(), this); d->repIndex = 0; if (d->replace->options() & KFind::FromCursor || d->replace->options() & KFind::FindBackwards) { d->repIndex = textCursor().anchor(); } // Connect highlight signal to code which handles highlighting // of found text. connect(d->replace, SIGNAL(highlight(QString,int,int)), this, SLOT(slotFindHighlight(QString,int,int))); connect(d->replace, &KFind::findNext, this, &KTextEdit::slotReplaceNext); connect(d->replace, SIGNAL(replace(QString,int,int,int)), this, SLOT(slotReplaceText(QString,int,int,int))); d->repDlg->close(); slotReplaceNext(); } void KTextEdit::slotReplaceNext() { if (!d->replace) { return; } d->lastReplacedPosition = -1; if (!(d->replace->options() & KReplaceDialog::PromptOnReplace)) { textCursor().beginEditBlock(); // #48541 viewport()->setUpdatesEnabled(false); } KFind::Result res = KFind::NoMatch; if (d->replace->needData()) { d->replace->setData(toPlainText(), d->repIndex); } res = d->replace->replace(); if (!(d->replace->options() & KReplaceDialog::PromptOnReplace)) { textCursor().endEditBlock(); // #48541 if (d->lastReplacedPosition >= 0) { QTextCursor tc = textCursor(); tc.setPosition(d->lastReplacedPosition); setTextCursor(tc); ensureCursorVisible(); } viewport()->setUpdatesEnabled(true); viewport()->update(); } if (res == KFind::NoMatch) { d->replace->displayFinalDialog(); d->replace->disconnect(this); d->replace->deleteLater(); // we are in a slot connected to m_replace, don't delete it right away d->replace = nullptr; ensureCursorVisible(); //or if ( m_replace->shouldRestart() ) { reinit (w/o FromCursor) and call slotReplaceNext(); } } else { //m_replace->closeReplaceNextDialog(); } } void KTextEdit::slotDoFind() { if (!d->findDlg) { // Should really assert() return; } if (d->findDlg->pattern().isEmpty()) { delete d->find; d->find = nullptr; return; } delete d->find; d->find = new KFind(d->findDlg->pattern(), d->findDlg->options(), this); d->findIndex = 0; if (d->find->options() & KFind::FromCursor || d->find->options() & KFind::FindBackwards) { d->findIndex = textCursor().anchor(); } // Connect highlight signal to code which handles highlighting // of found text. connect(d->find, SIGNAL(highlight(QString,int,int)), this, SLOT(slotFindHighlight(QString,int,int))); connect(d->find, &KFind::findNext, this, &KTextEdit::slotFindNext); d->findDlg->close(); d->find->closeFindNextDialog(); slotFindNext(); } void KTextEdit::slotFindNext() { if (!d->find) { return; } if (document()->isEmpty()) { d->find->disconnect(this); d->find->deleteLater(); // we are in a slot connected to m_find, don't delete right away d->find = nullptr; return; } KFind::Result res = KFind::NoMatch; if (d->find->needData()) { d->find->setData(toPlainText(), d->findIndex); } res = d->find->find(); if (res == KFind::NoMatch) { d->find->displayFinalDialog(); d->find->disconnect(this); d->find->deleteLater(); // we are in a slot connected to m_find, don't delete right away d->find = nullptr; //or if ( m_find->shouldRestart() ) { reinit (w/o FromCursor) and call slotFindNext(); } } else { //m_find->closeFindNextDialog(); } } void KTextEdit::slotFindPrevious() { if (!d->find) { return; } const long oldOptions = d->find->options(); d->find->setOptions(oldOptions ^ KFind::FindBackwards); slotFindNext(); if (d->find) { d->find->setOptions(oldOptions); } } void KTextEdit::slotFind() { if (document()->isEmpty()) { // saves having to track the text changes return; } if (d->findDlg) { KWindowSystem::activateWindow(d->findDlg->winId()); } else { d->findDlg = new KFindDialog(this); connect(d->findDlg, &KFindDialog::okClicked, this, &KTextEdit::slotDoFind); } d->findDlg->show(); } void KTextEdit::slotReplace() { if (document()->isEmpty()) { // saves having to track the text changes return; } if (d->repDlg) { KWindowSystem::activateWindow(d->repDlg->winId()); } else { d->repDlg = new KReplaceDialog(this, 0, QStringList(), QStringList(), false); connect(d->repDlg, &KFindDialog::okClicked, this, &KTextEdit::slotDoReplace); } d->repDlg->show(); } void KTextEdit::enableFindReplace(bool enabled) { d->findReplaceEnabled = enabled; } void KTextEdit::showTabAction(bool show) { d->showTabAction = show; } bool KTextEdit::Private::overrideShortcut(const QKeyEvent *event) { const int key = event->key() | event->modifiers(); if (KStandardShortcut::copy().contains(key)) { return true; } else if (KStandardShortcut::paste().contains(key)) { return true; } else if (KStandardShortcut::cut().contains(key)) { return true; } else if (KStandardShortcut::undo().contains(key)) { return true; } else if (KStandardShortcut::redo().contains(key)) { return true; } else if (KStandardShortcut::deleteWordBack().contains(key)) { return true; } else if (KStandardShortcut::deleteWordForward().contains(key)) { return true; } else if (KStandardShortcut::backwardWord().contains(key)) { return true; } else if (KStandardShortcut::forwardWord().contains(key)) { return true; } else if (KStandardShortcut::next().contains(key)) { return true; } else if (KStandardShortcut::prior().contains(key)) { return true; } else if (KStandardShortcut::begin().contains(key)) { return true; } else if (KStandardShortcut::end().contains(key)) { return true; } else if (KStandardShortcut::beginningOfLine().contains(key)) { return true; } else if (KStandardShortcut::endOfLine().contains(key)) { return true; } else if (KStandardShortcut::pasteSelection().contains(key)) { return true; } else if (findReplaceEnabled && KStandardShortcut::find().contains(key)) { return true; } else if (findReplaceEnabled && KStandardShortcut::findNext().contains(key)) { return true; } else if (findReplaceEnabled && KStandardShortcut::findPrev().contains(key)) { return true; } else if (findReplaceEnabled && KStandardShortcut::replace().contains(key)) { return true; } else if (event->matches(QKeySequence::SelectAll)) { // currently missing in QTextEdit return true; } return false; } void KTextEdit::keyPressEvent(QKeyEvent *event) { if (d->handleShortcut(event)) { event->accept(); } else { QTextEdit::keyPressEvent(event); } } void KTextEdit::showAutoCorrectButton(bool show) { d->showAutoCorrectionButton = show; } #include "moc_ktextedit.cpp"