Advance Wayland and KDE package bring-up

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-04-14 10:51:06 +01:00
parent 51f3c21121
commit cf12defd28
15214 changed files with 20594243 additions and 269 deletions
@@ -0,0 +1,139 @@
add_library(KF6TextWidgets)
add_library(KF6::TextWidgets ALIAS KF6TextWidgets)
set_target_properties(KF6TextWidgets PROPERTIES
VERSION ${KTEXTWIDGETS_VERSION}
SOVERSION ${KTEXTWIDGETS_SOVERSION}
EXPORT_NAME TextWidgets
)
target_sources(KF6TextWidgets PRIVATE
dialogs/klinkdialog.cpp
dialogs/klinkdialog_p.h
findreplace/kfind.cpp
findreplace/kfinddialog.cpp
findreplace/kfinddialog.h
findreplace/kfinddialog_p.h
findreplace/kfind.h
findreplace/kfind_p.h
findreplace/kreplace.cpp
findreplace/kreplacedialog.cpp
findreplace/kreplacedialog.h
findreplace/kreplace.h
widgets/kpluralhandlingspinbox.cpp
widgets/kpluralhandlingspinbox.h
widgets/krichtextedit.cpp
widgets/krichtextedit.h
widgets/krichtextedit_p.h
widgets/krichtextwidget.cpp
widgets/krichtextwidget.h
widgets/ktextedit.cpp
widgets/ktextedit.h
widgets/ktextedit_p.h
widgets/nestedlisthelper.cpp
widgets/nestedlisthelper_p.h
)
ecm_generate_export_header(KF6TextWidgets
BASE_NAME KTextWidgets
GROUP_BASE_NAME KF
VERSION ${KF_VERSION}
USE_VERSION_HEADER
DEPRECATED_BASE_VERSION 0
DEPRECATION_VERSIONS 6.6
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}/widgets
)
target_include_directories(KF6TextWidgets PUBLIC "$<BUILD_INTERFACE:${ktextwidgets_INCLUDES}>"
)
target_include_directories(KF6TextWidgets INTERFACE "$<INSTALL_INTERFACE:${KDE_INSTALL_INCLUDEDIR_KF}/KTextWidgets>")
target_link_libraries(KF6TextWidgets
PUBLIC
Qt6::Widgets
KF6::SonnetUi
KF6::I18n
PRIVATE
KF6::SonnetCore
KF6::WidgetsAddons
KF6::ColorScheme
KF6::Completion
KF6::ConfigGui
)
if (Qt6TextToSpeech_FOUND)
target_link_libraries(KF6TextWidgets
PRIVATE
Qt6::TextToSpeech)
endif()
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
)
install(TARGETS KF6TextWidgets EXPORT KF6TextWidgetsTargets ${KF_INSTALL_TARGETS_DEFAULT_ARGS})
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/ktextwidgets_export.h
${KTextWidgets_HEADERS}
DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF}/KTextWidgets COMPONENT Devel
)
if (BUILD_DESIGNERPLUGIN)
add_subdirectory(designer)
endif()
if (BUILD_QCH)
ecm_add_qch(
KF6TextWidgets_QCH
NAME KTextWidgets
BASE_NAME KF6TextWidgets
VERSION ${KF_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
Qt6Widgets_QCH
KF6SonnetUi_QCH
KF6I18n_QCH
INCLUDE_DIRS
${CMAKE_CURRENT_BINARY_DIR}
${ktextwidgets_INCLUDES}
BLANK_MACROS
KTEXTWIDGETS_EXPORT
KTEXTWIDGETS_DEPRECATED_EXPORT
KTEXTWIDGETS_DEPRECATED
"KTEXTWIDGETS_DEPRECATED_VERSION(x, y, t)"
"KTEXTWIDGETS_DEPRECATED_VERSION_BELATED(x, y, xt, yt, t)"
TAGFILE_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR}
QCH_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR}
COMPONENT Devel
)
endif()
+13
View File
@@ -0,0 +1,13 @@
#!/bin/sh
# Invoke the extractrc script on all .ui, .rc, and .kcfg files in the sources.
# The results are stored in a pseudo .cpp file to be picked up by xgettext.
lst=`find . -name \*.rc -o -name \*.ui -o -name \*.kcfg`
if [ -n "$lst" ] ; then
$EXTRACTRC $lst >> rc.cpp
fi
# Extract strings from all source files.
# If your framework depends on KI18n, use $XGETTEXT. If it uses Qt translation
# system, use $EXTRACT_TR_STRINGS.
$XGETTEXT `find . -name \*.cpp -o -name \*.h -o -name \*.qml` -o $podir/ktextwidgets6.pot
@@ -0,0 +1,34 @@
include(ECMAddQtDesignerPlugin)
ecm_qtdesignerplugin_widget(KRichTextEdit
TOOLTIP "Rich Text Editor (KF6)"
GROUP "Input (KF6)"
)
ecm_qtdesignerplugin_widget(KRichTextWidget
TOOLTIP "Rich Text Widget (KF6)"
GROUP "Input (KF6)"
)
ecm_qtdesignerplugin_widget(KTextEdit
TOOLTIP "Improved QTextEdit (KF6)"
WHATSTHIS "An improved version of the QTextEdit with mail or system browser invocation support"
GROUP "Input (KF6)"
)
ecm_qtdesignerplugin_widget(KPluralHandlingSpinBox
TOOLTIP "A QSpinBox with plural handling for the suffix (KF6)"
WHATSTHIS "A QSpinBox with plural handling for the suffix"
GROUP "Input (KF6)"
)
ecm_add_qtdesignerplugin(ktextwidgetswidgets
NAME KTextWidgetsWidgets
OUTPUT_NAME ktextwidgets6widgets
WIDGETS
KRichTextEdit
KRichTextWidget
KTextEdit
KPluralHandlingSpinBox
LINK_LIBRARIES
KF6::TextWidgets
INSTALL_DESTINATION "${KDE_INSTALL_QTPLUGINDIR}/designer"
COMPONENT Devel
)
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,104 @@
/*
klinkdialog
SPDX-FileCopyrightText: 2008 Stephen Kelly <steveire@gmailcom>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "klinkdialog_p.h"
#include <KLocalizedString>
#include <QDialogButtonBox>
#include <QGridLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QVBoxLayout>
/**
Private class that helps to provide binary compatibility between releases.
@internal
*/
//@cond PRIVATE
class KLinkDialogPrivate
{
public:
QLabel *textLabel = nullptr;
QLineEdit *textLineEdit = nullptr;
QLabel *linkUrlLabel = nullptr;
QLineEdit *linkUrlLineEdit = nullptr;
QDialogButtonBox *buttonBox = nullptr;
};
//@endcond
KLinkDialog::KLinkDialog(QWidget *parent)
: QDialog(parent)
, d(new KLinkDialogPrivate)
{
setWindowTitle(i18n("Manage Link"));
setModal(true);
QVBoxLayout *layout = new QVBoxLayout(this);
QGridLayout *grid = new QGridLayout;
d->textLabel = new QLabel(i18n("Link Text:"), this);
d->textLineEdit = new QLineEdit(this);
d->textLineEdit->setClearButtonEnabled(true);
d->linkUrlLabel = new QLabel(i18n("Link URL:"), this);
d->linkUrlLineEdit = new QLineEdit(this);
d->linkUrlLineEdit->setClearButtonEnabled(true);
grid->addWidget(d->textLabel, 0, 0);
grid->addWidget(d->textLineEdit, 0, 1);
grid->addWidget(d->linkUrlLabel, 1, 0);
grid->addWidget(d->linkUrlLineEdit, 1, 1);
layout->addLayout(grid);
d->buttonBox = new QDialogButtonBox(this);
d->buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(d->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(d->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
layout->addWidget(d->buttonBox);
d->textLineEdit->setFocus();
d->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
connect(d->textLineEdit, &QLineEdit::textChanged, this, &KLinkDialog::slotTextChanged);
}
KLinkDialog::~KLinkDialog()
{
delete d;
}
void KLinkDialog::slotTextChanged(const QString &text)
{
d->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!text.trimmed().isEmpty());
}
void KLinkDialog::setLinkText(const QString &linkText)
{
d->textLineEdit->setText(linkText);
if (!linkText.trimmed().isEmpty()) {
d->linkUrlLineEdit->setFocus();
}
}
void KLinkDialog::setLinkUrl(const QString &linkUrl)
{
d->linkUrlLineEdit->setText(linkUrl);
}
QString KLinkDialog::linkText() const
{
return d->textLineEdit->text().trimmed();
}
QString KLinkDialog::linkUrl() const
{
return d->linkUrlLineEdit->text();
}
#include "moc_klinkdialog_p.cpp"
@@ -0,0 +1,80 @@
/*
klinkdialog
SPDX-FileCopyrightText: 2008 Stephen Kelly <steveire@gmail.com>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef KLINKDIALOG_H
#define KLINKDIALOG_H
//@cond PRIVATE
#include <QDialog>
class KLinkDialogPrivate;
class QString;
/**
@short Dialog to allow user to configure a hyperlink.
@author Stephen Kelly
@since 4.1
@internal
This class provides a dialog to ask the user for a link target url and
text.
The size of the dialog is automatically saved to and restored from the
global KDE config file.
*/
class KLinkDialog : public QDialog
{
Q_OBJECT
public:
/**
* Create a link dialog.
* @param parent Parent widget.
*/
explicit KLinkDialog(QWidget *parent = nullptr);
/**
* Destructor
*/
~KLinkDialog() override;
/**
* Returns the link text shown in the dialog
* @param linkText The initial text
*/
void setLinkText(const QString &linkText);
/**
* Sets the target link url shown in the dialog
* @param linkUrl The initial link target url
*/
void setLinkUrl(const QString &linkUrl);
/**
* Returns the link text entered by the user.
* @return The link text
*/
QString linkText() const;
/**
* Returns the target link url entered by the user.
* @return The link url
*/
QString linkUrl() const;
private Q_SLOTS:
void slotTextChanged(const QString &);
private:
//@cond PRIVATE
KLinkDialogPrivate *const d;
//@endcond
};
//@endcond
#endif
@@ -0,0 +1,712 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2001 S .R.Haque <srhaque@iee.org>.
SPDX-FileCopyrightText: 2002 David Faure <david@mandrakesoft.com>
SPDX-FileCopyrightText: 2004 Arend van Beelen jr. <arend@auton.nl>
SPDX-License-Identifier: LGPL-2.0-only
*/
#include "kfind.h"
#include "kfind_p.h"
#include "kfinddialog.h"
#include <KGuiItem>
#include <KLocalizedString>
#include <KMessageBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QHash>
#include <QLabel>
#include <QPushButton>
#include <QRegularExpression>
#include <QVBoxLayout>
// #define DEBUG_FIND
static const int INDEX_NOMATCH = -1;
class KFindNextDialog : public QDialog
{
Q_OBJECT
public:
explicit KFindNextDialog(const QString &pattern, QWidget *parent);
QPushButton *findButton() const;
private:
QPushButton *m_findButton = nullptr;
};
// Create the dialog.
KFindNextDialog::KFindNextDialog(const QString &pattern, QWidget *parent)
: QDialog(parent)
{
setModal(false);
setWindowTitle(i18n("Find Next"));
QVBoxLayout *layout = new QVBoxLayout(this);
layout->addWidget(new QLabel(i18n("<qt>Find next occurrence of '<b>%1</b>'?</qt>", pattern), this));
m_findButton = new QPushButton;
KGuiItem::assign(m_findButton, KStandardGuiItem::find());
m_findButton->setDefault(true);
QDialogButtonBox *buttonBox = new QDialogButtonBox(this);
buttonBox->addButton(m_findButton, QDialogButtonBox::ActionRole);
buttonBox->setStandardButtons(QDialogButtonBox::Close);
layout->addWidget(buttonBox);
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
}
QPushButton *KFindNextDialog::findButton() const
{
return m_findButton;
}
////
KFind::KFind(const QString &pattern, long options, QWidget *parent)
: KFind(*new KFindPrivate(this), pattern, options, parent)
{
}
KFind::KFind(KFindPrivate &dd, const QString &pattern, long options, QWidget *parent)
: QObject(parent)
, d_ptr(&dd)
{
Q_D(KFind);
d->options = options;
d->init(pattern);
}
KFind::KFind(const QString &pattern, long options, QWidget *parent, QWidget *findDialog)
: KFind(*new KFindPrivate(this), pattern, options, parent, findDialog)
{
}
KFind::KFind(KFindPrivate &dd, const QString &pattern, long options, QWidget *parent, QWidget *findDialog)
: QObject(parent)
, d_ptr(&dd)
{
Q_D(KFind);
d->findDialog = findDialog;
d->options = options;
d->init(pattern);
}
void KFindPrivate::init(const QString &_pattern)
{
Q_Q(KFind);
matches = 0;
pattern = _pattern;
dialog = nullptr;
dialogClosed = false;
index = INDEX_NOMATCH;
lastResult = KFind::NoMatch;
// TODO: KF6 change this comment once d->regExp is removed
// set options and create d->regExp with the right options
q->setOptions(options);
}
KFind::~KFind() = default;
bool KFind::needData() const
{
Q_D(const KFind);
// always true when d->text is empty.
if (d->options & KFind::FindBackwards)
// d->index==-1 and d->lastResult==Match means we haven't answered nomatch yet
// This is important in the "replace with a prompt" case.
{
return (d->index < 0 && d->lastResult != Match);
} else
// "index over length" test removed: we want to get a nomatch before we set data again
// This is important in the "replace with a prompt" case.
{
return d->index == INDEX_NOMATCH;
}
}
void KFind::setData(const QString &data, int startPos)
{
setData(-1, data, startPos);
}
void KFind::setData(int id, const QString &data, int startPos)
{
Q_D(KFind);
// cache the data for incremental find
if (d->options & KFind::FindIncremental) {
if (id != -1) {
d->customIds = true;
} else {
id = d->currentId + 1;
}
Q_ASSERT(id <= d->data.size());
if (id == d->data.size()) {
d->data.append(KFindPrivate::Data(id, data, true));
} else {
d->data.replace(id, KFindPrivate::Data(id, data, true));
}
Q_ASSERT(d->data.at(id).text == data);
}
if (!(d->options & KFind::FindIncremental) || needData()) {
d->text = data;
if (startPos != -1) {
d->index = startPos;
} else if (d->options & KFind::FindBackwards) {
d->index = d->text.length();
} else {
d->index = 0;
}
#ifdef DEBUG_FIND
// qDebug() << "setData: '" << d->text << "' d->index=" << d->index;
#endif
Q_ASSERT(d->index != INDEX_NOMATCH);
d->lastResult = NoMatch;
d->currentId = id;
}
}
QDialog *KFind::findNextDialog(bool create)
{
Q_D(KFind);
if (!d->dialog && create) {
KFindNextDialog *dialog = new KFindNextDialog(d->pattern, parentWidget());
connect(dialog->findButton(), &QPushButton::clicked, this, [d]() {
d->slotFindNext();
});
connect(dialog, &QDialog::finished, this, [d]() {
d->slotDialogClosed();
});
d->dialog = dialog;
}
return d->dialog;
}
KFind::Result KFind::find()
{
Q_D(KFind);
Q_ASSERT(d->index != INDEX_NOMATCH || d->patternChanged);
if (d->lastResult == Match && !d->patternChanged) {
// Move on before looking for the next match, _if_ we just found a match
if (d->options & KFind::FindBackwards) {
d->index--;
if (d->index == -1) { // don't call KFind::find with -1, it has a special meaning
d->lastResult = NoMatch;
return NoMatch;
}
} else {
d->index++;
}
}
d->patternChanged = false;
if (d->options & KFind::FindIncremental) {
// if the current pattern is shorter than the matchedPattern we can
// probably look up the match in the incrementalPath
if (d->pattern.length() < d->matchedPattern.length()) {
KFindPrivate::Match match;
if (!d->pattern.isEmpty()) {
match = d->incrementalPath.value(d->pattern);
} else if (d->emptyMatch) {
match = *d->emptyMatch;
}
QString previousPattern(d->matchedPattern);
d->matchedPattern = d->pattern;
if (!match.isNull()) {
bool clean = true;
// find the first result backwards on the path that isn't dirty
while (d->data.at(match.dataId).dirty == true && !d->pattern.isEmpty()) {
d->pattern.truncate(d->pattern.length() - 1);
match = d->incrementalPath.value(d->pattern);
clean = false;
}
// remove all matches that lie after the current match
while (d->pattern.length() < previousPattern.length()) {
d->incrementalPath.remove(previousPattern);
previousPattern.truncate(previousPattern.length() - 1);
}
// set the current text, index, etc. to the found match
d->text = d->data.at(match.dataId).text;
d->index = match.index;
d->matchedLength = match.matchedLength;
d->currentId = match.dataId;
// if the result is clean we can return it now
if (clean) {
if (d->customIds) {
Q_EMIT textFoundAtId(d->currentId, d->index, d->matchedLength);
} else {
Q_EMIT textFound(d->text, d->index, d->matchedLength);
}
d->lastResult = Match;
d->matchedPattern = d->pattern;
return Match;
}
}
// if we couldn't look up the match, the new pattern isn't a
// substring of the matchedPattern, so we start a new search
else {
d->startNewIncrementalSearch();
}
}
// if the new pattern is longer than the matchedPattern we might be
// able to proceed from the last search
else if (d->pattern.length() > d->matchedPattern.length()) {
// continue from the previous pattern
if (d->pattern.startsWith(d->matchedPattern)) {
// we can't proceed from the previous position if the previous
// position already failed
if (d->index == INDEX_NOMATCH) {
return NoMatch;
}
QString temp(d->pattern);
d->pattern.truncate(d->matchedPattern.length() + 1);
d->matchedPattern = temp;
}
// start a new search
else {
d->startNewIncrementalSearch();
}
}
// if the new pattern is as long as the matchedPattern, we reset if
// they are not equal
else if (d->pattern != d->matchedPattern) {
d->startNewIncrementalSearch();
}
}
#ifdef DEBUG_FIND
// qDebug() << "d->index=" << d->index;
#endif
do {
// if we have multiple data blocks in our cache, walk through these
// blocks till we either searched all blocks or we find a match
do {
// Find the next candidate match.
d->index = KFind::find(d->text, d->pattern, d->index, d->options, &d->matchedLength, nullptr);
if (d->options & KFind::FindIncremental) {
d->data[d->currentId].dirty = false;
}
if (d->index == -1 && d->currentId < d->data.count() - 1) {
d->text = d->data.at(++d->currentId).text;
if (d->options & KFind::FindBackwards) {
d->index = d->text.length();
} else {
d->index = 0;
}
} else {
break;
}
} while (!(d->options & KFind::RegularExpression));
if (d->index != -1) {
// Flexibility: the app can add more rules to validate a possible match
if (validateMatch(d->text, d->index, d->matchedLength)) {
bool done = true;
if (d->options & KFind::FindIncremental) {
if (d->pattern.isEmpty()) {
delete d->emptyMatch;
d->emptyMatch = new KFindPrivate::Match(d->currentId, d->index, d->matchedLength);
} else {
d->incrementalPath.insert(d->pattern, KFindPrivate::Match(d->currentId, d->index, d->matchedLength));
}
if (d->pattern.length() < d->matchedPattern.length()) {
d->pattern += QStringView(d->matchedPattern).mid(d->pattern.length(), 1);
done = false;
}
}
if (done) {
d->matches++;
// Tell the world about the match we found, in case someone wants to
// highlight it.
if (d->customIds) {
Q_EMIT textFoundAtId(d->currentId, d->index, d->matchedLength);
} else {
Q_EMIT textFound(d->text, d->index, d->matchedLength);
}
if (!d->dialogClosed) {
findNextDialog(true)->show();
}
#ifdef DEBUG_FIND
// qDebug() << "Match. Next d->index=" << d->index;
#endif
d->lastResult = Match;
return Match;
}
} else { // Skip match
if (d->options & KFind::FindBackwards) {
d->index--;
} else {
d->index++;
}
}
} else {
if (d->options & KFind::FindIncremental) {
QString temp(d->pattern);
temp.truncate(temp.length() - 1);
d->pattern = d->matchedPattern;
d->matchedPattern = temp;
}
d->index = INDEX_NOMATCH;
}
} while (d->index != INDEX_NOMATCH);
#ifdef DEBUG_FIND
// qDebug() << "NoMatch. d->index=" << d->index;
#endif
d->lastResult = NoMatch;
return NoMatch;
}
void KFindPrivate::startNewIncrementalSearch()
{
KFindPrivate::Match *match = emptyMatch;
if (match == nullptr) {
text.clear();
index = 0;
currentId = 0;
} else {
text = data.at(match->dataId).text;
index = match->index;
currentId = match->dataId;
}
matchedLength = 0;
incrementalPath.clear();
delete emptyMatch;
emptyMatch = nullptr;
matchedPattern = pattern;
pattern.clear();
}
static bool isInWord(QChar ch)
{
return ch.isLetter() || ch.isDigit() || ch == QLatin1Char('_');
}
static bool isWholeWords(const QString &text, int starts, int matchedLength)
{
if (starts == 0 || !isInWord(text.at(starts - 1))) {
const int ends = starts + matchedLength;
if (ends == text.length() || !isInWord(text.at(ends))) {
return true;
}
}
return false;
}
static bool matchOk(const QString &text, int index, int matchedLength, long options)
{
if (options & KFind::WholeWordsOnly) {
// Is the match delimited correctly?
if (isWholeWords(text, index, matchedLength)) {
return true;
}
} else {
// Non-whole-word search: this match is good
return true;
}
return false;
}
static int findRegex(const QString &text, const QString &pattern, int index, long options, int *matchedLength, QRegularExpressionMatch *rmatch)
{
QString _pattern = pattern;
// Always enable Unicode support in QRegularExpression
QRegularExpression::PatternOptions opts = QRegularExpression::UseUnicodePropertiesOption;
// instead of this rudimentary test, add a checkbox to toggle MultilineOption ?
if (pattern.startsWith(QLatin1Char('^')) || pattern.endsWith(QLatin1Char('$'))) {
opts |= QRegularExpression::MultilineOption;
} else if (options & KFind::WholeWordsOnly) { // WholeWordsOnly makes no sense with multiline
_pattern = QLatin1String("\\b") + pattern + QLatin1String("\\b");
}
if (!(options & KFind::CaseSensitive)) {
opts |= QRegularExpression::CaseInsensitiveOption;
}
QRegularExpression re(_pattern, opts);
QRegularExpressionMatch match;
if (options & KFind::FindBackwards) {
// Backward search, until the beginning of the line...
(void)text.lastIndexOf(re, index, &match);
} else {
// Forward search, until the end of the line...
match = re.match(text, index);
}
// index is -1 if no match is found
index = match.capturedStart(0);
// matchedLength is 0 if no match is found
*matchedLength = match.capturedLength(0);
if (rmatch) {
*rmatch = match;
}
return index;
}
// static
int KFind::find(const QString &text, const QString &pattern, int index, long options, int *matchedLength, QRegularExpressionMatch *rmatch)
{
// Handle regular expressions in the appropriate way.
if (options & KFind::RegularExpression) {
return findRegex(text, pattern, index, options, matchedLength, rmatch);
}
// In Qt4 QString("aaaaaa").lastIndexOf("a",6) returns -1; we need
// to start at text.length() - pattern.length() to give a valid index to QString.
if (options & KFind::FindBackwards) {
index = qMin(qMax(0, text.length() - pattern.length()), index);
}
Qt::CaseSensitivity caseSensitive = (options & KFind::CaseSensitive) ? Qt::CaseSensitive : Qt::CaseInsensitive;
if (options & KFind::FindBackwards) {
// Backward search, until the beginning of the line...
while (index >= 0) {
// ...find the next match.
index = text.lastIndexOf(pattern, index, caseSensitive);
if (index == -1) {
break;
}
if (matchOk(text, index, pattern.length(), options)) {
break;
}
index--;
// qDebug() << "decrementing:" << index;
}
} else {
// Forward search, until the end of the line...
while (index <= text.length()) {
// ...find the next match.
index = text.indexOf(pattern, index, caseSensitive);
if (index == -1) {
break;
}
if (matchOk(text, index, pattern.length(), options)) {
break;
}
index++;
}
if (index > text.length()) { // end of line
// qDebug() << "at" << index << "-> not found";
index = -1; // not found
}
}
if (index <= -1) {
*matchedLength = 0;
} else {
*matchedLength = pattern.length();
}
return index;
}
void KFindPrivate::slotFindNext()
{
Q_Q(KFind);
Q_EMIT q->findNext();
}
void KFindPrivate::slotDialogClosed()
{
Q_Q(KFind);
#ifdef DEBUG_FIND
// qDebug() << " Begin";
#endif
Q_EMIT q->dialogClosed();
dialogClosed = true;
#ifdef DEBUG_FIND
// qDebug() << " End";
#endif
}
void KFind::displayFinalDialog() const
{
Q_D(const KFind);
QString message;
if (numMatches()) {
message = i18np("1 match found.", "%1 matches found.", numMatches());
} else {
message = i18n("<qt>No matches found for '<b>%1</b>'.</qt>", d->pattern.toHtmlEscaped());
}
KMessageBox::information(dialogsParent(), message);
}
bool KFind::shouldRestart(bool forceAsking, bool showNumMatches) const
{
Q_D(const KFind);
// Only ask if we did a "find from cursor", otherwise it's pointless.
// Well, unless the user can modify the document during a search operation,
// hence the force boolean.
if (!forceAsking && (d->options & KFind::FromCursor) == 0) {
displayFinalDialog();
return false;
}
QString message;
if (showNumMatches) {
if (numMatches()) {
message = i18np("1 match found.", "%1 matches found.", numMatches());
} else {
message = i18n("No matches found for '<b>%1</b>'.", d->pattern.toHtmlEscaped());
}
} else {
if (d->options & KFind::FindBackwards) {
message = i18n("Beginning of document reached.");
} else {
message = i18n("End of document reached.");
}
}
message += QLatin1String("<br><br>"); // can't be in the i18n() of the first if() because of the plural form.
// Hope this word puzzle is ok, it's a different sentence
message += (d->options & KFind::FindBackwards) ? i18n("Continue from the end?") : i18n("Continue from the beginning?");
int ret = KMessageBox::questionTwoActions(dialogsParent(),
QStringLiteral("<qt>%1</qt>").arg(message),
QString(),
KStandardGuiItem::cont(),
KStandardGuiItem::stop());
bool yes = (ret == KMessageBox::PrimaryAction);
if (yes) {
const_cast<KFindPrivate *>(d)->options &= ~KFind::FromCursor; // clear FromCursor option
}
return yes;
}
long KFind::options() const
{
Q_D(const KFind);
return d->options;
}
void KFind::setOptions(long options)
{
Q_D(KFind);
d->options = options;
}
void KFind::closeFindNextDialog()
{
Q_D(KFind);
if (d->dialog) {
d->dialog->deleteLater();
d->dialog = nullptr;
}
d->dialogClosed = true;
}
int KFind::index() const
{
Q_D(const KFind);
return d->index;
}
QString KFind::pattern() const
{
Q_D(const KFind);
return d->pattern;
}
void KFind::setPattern(const QString &pattern)
{
Q_D(KFind);
if (d->pattern != pattern) {
d->patternChanged = true;
d->matches = 0;
}
d->pattern = pattern;
// TODO: KF6 change this comment once d->regExp is removed
// set the options and rebuild d->regeExp if necessary
setOptions(options());
}
int KFind::numMatches() const
{
Q_D(const KFind);
return d->matches;
}
void KFind::resetCounts()
{
Q_D(KFind);
d->matches = 0;
}
bool KFind::validateMatch(const QString &, int, int)
{
return true;
}
QWidget *KFind::parentWidget() const
{
return static_cast<QWidget *>(parent());
}
QWidget *KFind::dialogsParent() const
{
Q_D(const KFind);
// If the find dialog is still up, it should get the focus when closing a message box
// Otherwise, maybe the "find next?" dialog is up
// Otherwise, the "view" is the parent.
return d->findDialog ? static_cast<QWidget *>(d->findDialog) : (d->dialog ? d->dialog : parentWidget());
}
#include "kfind.moc"
#include "moc_kfind.cpp"
@@ -0,0 +1,378 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2001 S.R. Haque <srhaque@iee.org>.
SPDX-FileCopyrightText: 2002 David Faure <david@mandrakesoft.com>
SPDX-License-Identifier: LGPL-2.0-only
*/
#ifndef KFIND_H
#define KFIND_H
#include "ktextwidgets_export.h"
#include <QObject>
#include <memory>
class QDialog;
class KFindPrivate;
/**
* @class KFind kfind.h <KFind>
*
* @brief A generic implementation of the "find" function.
*
* @author S.R.Haque <srhaque@iee.org>, David Faure <faure@kde.org>,
* Arend van Beelen jr. <arend@auton.nl>
*
* \b Detail:
*
* This class includes prompt handling etc. Also provides some
* static functions which can be used to create custom behavior
* instead of using the class directly.
*
* \b Example:
*
* To use the class to implement a complete find feature:
*
* In the slot connected to the find action, after using KFindDialog:
* \code
*
* // This creates a find-next-prompt dialog if needed.
* m_find = new KFind(pattern, options, this);
*
* // Connect textFound() signal to code which handles highlighting of found text.
* connect(m_find, &KFind::textFound, this, [this](const QString &text, int matchingIndex, int matchedLength) {
* slotHighlight(text, matchingIndex, matchedLength);
* }));
* // Connect findNext signal - called when pressing the button in the dialog
* connect(m_find, SIGNAL(findNext()),
* this, SLOT(slotFindNext()));
* \endcode
*
* Then initialize the variables determining the "current position"
* (to the cursor, if the option FromCursor is set,
* to the beginning of the selection if the option SelectedText is set,
* and to the beginning of the document otherwise).
* Initialize the "end of search" variables as well (end of doc or end of selection).
* Swap begin and end if FindBackwards.
* Finally, call slotFindNext();
*
* \code
* void slotFindNext()
* {
* KFind::Result res = KFind::NoMatch;
* while (res == KFind::NoMatch && <position not at end>) {
* if (m_find->needData())
* m_find->setData(<current text fragment>);
*
* // Let KFind inspect the text fragment, and display a dialog if a match is found
* res = m_find->find();
*
* if (res == KFind::NoMatch) {
* <Move to the next text fragment, honoring the FindBackwards setting for the direction>
* }
* }
*
* if (res == KFind::NoMatch) // i.e. at end
* <Call either m_find->displayFinalDialog(); m_find->deleteLater(); m_find = nullptr;
* or if (m_find->shouldRestart()) { reinit (w/o FromCursor); m_find->resetCounts(); slotFindNext(); }
* else { m_find->closeFindNextDialog(); }>
* }
* \endcode
*
* Don't forget to delete m_find in the destructor of your class,
* unless you gave it a parent widget on construction.
*
* This implementation allows to have a "Find Next" action, which resumes the
* search, even if the user closed the "Find Next" dialog.
*
* A "Find Previous" action can simply switch temporarily the value of
* FindBackwards and call slotFindNext() - and reset the value afterwards.
*/
class KTEXTWIDGETS_EXPORT KFind : public QObject
{
Q_OBJECT
public:
/**
* @see SearchOptions
*/
enum Options {
WholeWordsOnly = 1, ///< Match whole words only.
FromCursor = 2, ///< Start from current cursor position.
SelectedText = 4, ///< Only search selected area.
CaseSensitive = 8, ///< Consider case when matching.
FindBackwards = 16, ///< Go backwards.
RegularExpression = 32, ///< Interpret the pattern as a regular expression.
FindIncremental = 64, ///< Find incremental.
// Note that KReplaceDialog uses 256 and 512
// User extensions can use boolean options above this value.
MinimumUserOption = 65536, ///< user options start with this bit
};
/**
* Stores a combination of #Options values.
*/
Q_DECLARE_FLAGS(SearchOptions, Options)
/**
* Only use this constructor if you don't use KFindDialog, or if
* you use it as a modal dialog.
*/
KFind(const QString &pattern, long options, QWidget *parent);
/**
* This is the recommended constructor if you also use KFindDialog (non-modal).
* You should pass the pointer to it here, so that when a message box
* appears it has the right parent. Don't worry about deletion, KFind
* will notice if the find dialog is closed.
*/
KFind(const QString &pattern, long options, QWidget *parent, QWidget *findDialog);
~KFind() override;
enum Result {
NoMatch,
Match,
};
/**
* @return true if the application must supply a new text fragment
* It also means the last call returned "NoMatch". But by storing this here
* the application doesn't have to store it in a member variable (between
* calls to slotFindNext()).
*/
bool needData() const;
/**
* Call this when needData returns true, before calling find().
* @param data the text fragment (line)
* @param startPos if set, the index at which the search should start.
* This is only necessary for the very first call to setData usually,
* for the 'find in selection' feature. A value of -1 (the default value)
* means "process all the data", i.e. either 0 or data.length()-1 depending
* on FindBackwards.
*/
void setData(const QString &data, int startPos = -1);
/**
* Call this when needData returns true, before calling find(). The use of
* ID's is especially useful if you're using the FindIncremental option.
* @param id the id of the text fragment
* @param data the text fragment (line)
* @param startPos if set, the index at which the search should start.
* This is only necessary for the very first call to setData usually,
* for the 'find in selection' feature. A value of -1 (the default value)
* means "process all the data", i.e. either 0 or data.length()-1 depending
* on FindBackwards.
*/
void setData(int id, const QString &data, int startPos = -1);
/**
* Walk the text fragment (e.g. in a text-processor line or spreadsheet
* cell ...etc) looking for matches.
* For each match, emits the textFound() signal and displays the find-again
* dialog to ask if the user wants to find the same text again.
*/
Result find();
/**
* Return the current options.
*
* Warning: this is usually the same value as the one passed to the constructor,
* but options might change _during_ the replace operation:
* e.g. the "All" button resets the PromptOnReplace flag.
*
* @see KFind::Options
*/
long options() const;
/**
* Set new options. Usually this is used for setting or clearing the
* FindBackwards options.
*
* @see KFind::Options
*/
virtual void setOptions(long options);
/**
* @return the pattern we're currently looking for
*/
QString pattern() const;
/**
* Change the pattern we're looking for
*/
void setPattern(const QString &pattern);
/**
* Returns the number of matches found (i.e. the number of times the textFound()
* signal was emitted).
* If 0, can be used in a dialog box to tell the user "no match was found".
* The final dialog does so already, unless you used setDisplayFinalDialog(false).
*/
int numMatches() const;
/**
* Call this to reset the numMatches count
* (and the numReplacements count for a KReplace).
* Can be useful if reusing the same KReplace for different operations,
* or when restarting from the beginning of the document.
*/
virtual void resetCounts();
/**
* Virtual method, which allows applications to add extra checks for
* validating a candidate match. It's only necessary to reimplement this
* if the find dialog extension has been used to provide additional
* criteria.
*
* @param text The current text fragment
* @param index The starting index where the candidate match was found
* @param matchedlength The length of the candidate match
*/
virtual bool validateMatch(const QString &text, int index, int matchedlength);
/**
* Returns true if we should restart the search from scratch.
* Can ask the user, or return false (if we already searched the whole document).
*
* @param forceAsking set to true if the user modified the document during the
* search. In that case it makes sense to restart the search again.
*
* @param showNumMatches set to true if the dialog should show the number of
* matches. Set to false if the application provides a "find previous" action,
* in which case the match count will be erroneous when hitting the end,
* and we could even be hitting the beginning of the document (so not all
* matches have even been seen).
*/
virtual bool shouldRestart(bool forceAsking = false, bool showNumMatches = true) const;
/**
* Search @p text for @p pattern. If a match is found, the length of the matched
* string will be stored in @p matchedLength and the index of the matched string
* will be returned. If no match is found -1 is returned.
*
* If the KFind::RegularExpression flag is set, the @p pattern will be iterpreted
* as a regular expression (using QRegularExpression).
*
* @note Unicode support is always enabled (by setting the QRegularExpression::UseUnicodePropertiesOption flag).
*
* @param text The string to search in
* @param pattern The pattern to search for
* @param index The index in @p text from which to start the search
* @param options The options to use
* @param matchedlength If there is a match, its length will be stored in this parameter
* @param rmatch If there is a regular expression match (implies that the KFind::RegularExpression
* flag is set) and @p rmatch is not a nullptr the match result will be stored
* in this QRegularExpressionMatch object
* @return The index at which a match was found otherwise -1
*
* @since 5.70
*/
static int find(const QString &text, const QString &pattern, int index, long options, int *matchedLength, QRegularExpressionMatch *rmatch);
/**
* Displays the final dialog saying "no match was found", if that was the case.
* Call either this or shouldRestart().
*/
virtual void displayFinalDialog() const;
/**
* Return (or create) the dialog that shows the "find next?" prompt.
* Usually you don't need to call this.
* One case where it can be useful, is when the user selects the "Find"
* menu item while a find operation is under way. In that case, the
* program may want to call setActiveWindow() on that dialog.
*/
QDialog *findNextDialog(bool create = false);
/**
* Close the "find next?" dialog. The application should do this when
* the last match was hit. If the application deletes the KFind, then
* "find previous" won't be possible anymore.
*
* IMPORTANT: you should also call this if you are using a non-modal
* find dialog, to tell KFind not to pop up its own dialog.
*/
void closeFindNextDialog();
/**
* @return the current matching index (or -1).
* Same as the matchingIndex parameter passed to the textFound() signal.
* You usually don't need to use this, except maybe when updating the current data,
* so you need to call setData(newData, index()).
*/
int index() const;
Q_SIGNALS:
/**
* Connect to this signal to implement highlighting of found text during the find
* operation.
*
* If you've set data with setData(id, text), use the textFoundAtId(int, int, int) signal.
*
* WARNING: If you're using the FindIncremental option, the text argument
* passed by this signal is not necessarily the data last set through
* setData(), but can also be an earlier set data block.
*
* @see setData()
*
* @since 5.81
*/
void textFound(const QString &text, int matchingIndex, int matchedLength);
/**
* Connect to this signal to implement highlighting of found text during
* the find operation.
*
* Use this signal if you've set your data with setData(id, text),
* otherwise use the textFound(text, matchingIndex, matchedLength) signal.
*
* WARNING: If you're using the FindIncremental option, the id argument
* passed by this signal is not necessarily the id of the data last set
* through setData(), but can also be of an earlier set data block.
*
* @see setData()
*
* @since 5.81
*/
void textFoundAtId(int id, int matchingIndex, int matchedLength);
// ## TODO docu
// findprevious will also emit findNext, after temporarily switching the value
// of FindBackwards
void findNext();
/**
* Emitted when the options have changed.
* This can happen e.g. with "Replace All", or if our 'find next' dialog
* gets a "find previous" one day.
*/
void optionsChanged();
/**
* Emitted when the 'find next' dialog is being closed.
* Some apps might want to remove the highlighted text when this happens.
* Apps without support for "Find Next" can also do m_find->deleteLater()
* to terminate the find operation.
*/
void dialogClosed();
protected:
QWidget *parentWidget() const;
QWidget *dialogsParent() const;
protected:
KTEXTWIDGETS_NO_EXPORT KFind(KFindPrivate &dd, const QString &pattern, long options, QWidget *parent);
KTEXTWIDGETS_NO_EXPORT KFind(KFindPrivate &dd, const QString &pattern, long options, QWidget *parent, QWidget *findDialog);
protected:
std::unique_ptr<class KFindPrivate> const d_ptr;
private:
Q_DECLARE_PRIVATE(KFind)
};
Q_DECLARE_OPERATORS_FOR_FLAGS(KFind::SearchOptions)
#endif
@@ -0,0 +1,116 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2001 S.R. Haque <srhaque@iee.org>.
SPDX-FileCopyrightText: 2002 David Faure <david@mandrakesoft.com>
SPDX-FileCopyrightText: 2004 Arend van Beelen jr. <arend@auton.nl>
SPDX-License-Identifier: LGPL-2.0-only
*/
#ifndef KFIND_P_H
#define KFIND_P_H
#include "kfind.h"
#include <QDialog>
#include <QHash>
#include <QList>
#include <QPointer>
#include <QString>
class KFindPrivate
{
Q_DECLARE_PUBLIC(KFind)
public:
KFindPrivate(KFind *qq)
: q_ptr(qq)
, findDialog(nullptr)
, currentId(0)
, customIds(false)
, patternChanged(false)
, matchedPattern(QLatin1String(""))
, emptyMatch(nullptr)
{
}
virtual ~KFindPrivate()
{
if (dialog) {
dialog->deleteLater();
}
dialog = nullptr;
data.clear();
delete emptyMatch;
emptyMatch = nullptr;
}
struct Match {
Match()
: dataId(-1)
, index(-1)
, matchedLength(-1)
{
}
bool isNull() const
{
return index == -1;
}
Match(int _dataId, int _index, int _matchedLength)
: dataId(_dataId)
, index(_index)
, matchedLength(_matchedLength)
{
Q_ASSERT(index != -1);
}
int dataId;
int index;
int matchedLength;
};
struct Data {
Data()
{
}
Data(int id, const QString &text, bool dirty = false)
: text(text)
, id(id)
, dirty(dirty)
{
}
QString text;
int id = -1;
bool dirty = false;
};
void init(const QString &pattern);
void startNewIncrementalSearch();
void slotFindNext();
void slotDialogClosed();
KFind *const q_ptr;
QPointer<QWidget> findDialog;
int currentId;
bool customIds : 1;
bool patternChanged : 1;
QString matchedPattern;
QHash<QString, Match> incrementalPath;
Match *emptyMatch;
QList<Data> data; // used like a vector, not like a linked-list
QString pattern;
QDialog *dialog;
long options;
unsigned matches;
QString text; // the text set by setData
int index;
int matchedLength;
bool dialogClosed : 1;
bool lastResult : 1;
};
#endif // KFIND_P_H
@@ -0,0 +1,621 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2001 S.R. Haque <srhaque@iee.org>.
SPDX-FileCopyrightText: 2002 David Faure <david@mandrakesoft.com>
SPDX-License-Identifier: LGPL-2.0-only
*/
#include "kfinddialog.h"
#include "kfinddialog_p.h"
#include "kfind.h"
#include <QCheckBox>
#include <QDialogButtonBox>
#include <QGridLayout>
#include <QGroupBox>
#include <QLabel>
#include <QLineEdit>
#include <QMenu>
#include <QPushButton>
#include <QRegularExpression>
#include <KGuiItem>
#include <KHistoryComboBox>
#include <KLazyLocalizedString>
#include <KLocalizedString>
#include <KMessageBox>
#include <assert.h>
KFindDialog::KFindDialog(QWidget *parent, long options, const QStringList &findStrings, bool hasSelection, bool replaceDialog)
: KFindDialog(*new KFindDialogPrivate(this), parent, options, findStrings, hasSelection, replaceDialog)
{
setWindowTitle(i18n("Find Text"));
}
KFindDialog::KFindDialog(KFindDialogPrivate &dd, QWidget *parent, long options, const QStringList &findStrings, bool hasSelection, bool replaceDialog)
: QDialog(parent)
, d_ptr(&dd)
{
Q_D(KFindDialog);
d->init(replaceDialog, findStrings, hasSelection);
setOptions(options);
}
KFindDialog::~KFindDialog() = default;
QWidget *KFindDialog::findExtension() const
{
Q_D(const KFindDialog);
if (!d->findExtension) {
d->findExtension = new QWidget(d->findGrp);
d->findLayout->addWidget(d->findExtension, 3, 0, 1, 2);
}
return d->findExtension;
}
QStringList KFindDialog::findHistory() const
{
Q_D(const KFindDialog);
return d->find->historyItems();
}
void KFindDialogPrivate::init(bool forReplace, const QStringList &_findStrings, bool hasSelection)
{
Q_Q(KFindDialog);
// Create common parts of dialog.
QVBoxLayout *topLayout = new QVBoxLayout(q);
findGrp = new QGroupBox(i18nc("@title:group", "Find"), q);
findLayout = new QGridLayout(findGrp);
QLabel *findLabel = new QLabel(i18n("&Text to find:"), findGrp);
find = new KHistoryComboBox(findGrp);
find->setMaxCount(10);
find->setDuplicatesEnabled(false);
regExp = new QCheckBox(i18n("Regular e&xpression"), findGrp);
regExpItem = new QPushButton(i18nc("@action:button", "&Edit…"), findGrp);
regExpItem->setEnabled(false);
findLayout->addWidget(findLabel, 0, 0);
findLayout->addWidget(find, 1, 0, 1, 2);
findLayout->addWidget(regExp, 2, 0);
findLayout->addWidget(regExpItem, 2, 1);
topLayout->addWidget(findGrp);
replaceGrp = new QGroupBox(i18n("Replace With"), q);
replaceLayout = new QGridLayout(replaceGrp);
QLabel *replaceLabel = new QLabel(i18n("Replace&ment text:"), replaceGrp);
replace = new KHistoryComboBox(replaceGrp);
replace->setMaxCount(10);
replace->setDuplicatesEnabled(false);
backRef = new QCheckBox(i18n("Use p&laceholders"), replaceGrp);
backRefItem = new QPushButton(i18n("Insert Place&holder"), replaceGrp);
backRefItem->setEnabled(false);
replaceLayout->addWidget(replaceLabel, 0, 0);
replaceLayout->addWidget(replace, 1, 0, 1, 2);
replaceLayout->addWidget(backRef, 2, 0);
replaceLayout->addWidget(backRefItem, 2, 1);
topLayout->addWidget(replaceGrp);
QGroupBox *optionGrp = new QGroupBox(i18n("Options"), q);
QGridLayout *optionsLayout = new QGridLayout(optionGrp);
caseSensitive = new QCheckBox(i18n("C&ase sensitive"), optionGrp);
wholeWordsOnly = new QCheckBox(i18n("&Whole words only"), optionGrp);
fromCursor = new QCheckBox(i18n("From c&ursor"), optionGrp);
findBackwards = new QCheckBox(i18n("Find &backwards"), optionGrp);
selectedText = new QCheckBox(i18n("&Selected text"), optionGrp);
q->setHasSelection(hasSelection);
// If we have a selection, we make 'find in selection' default
// and if we don't, then the option has to be unchecked, obviously.
selectedText->setChecked(hasSelection);
slotSelectedTextToggled(hasSelection);
promptOnReplace = new QCheckBox(i18n("&Prompt on replace"), optionGrp);
promptOnReplace->setChecked(true);
optionsLayout->addWidget(caseSensitive, 0, 0);
optionsLayout->addWidget(wholeWordsOnly, 1, 0);
optionsLayout->addWidget(fromCursor, 2, 0);
optionsLayout->addWidget(findBackwards, 0, 1);
optionsLayout->addWidget(selectedText, 1, 1);
optionsLayout->addWidget(promptOnReplace, 2, 1);
topLayout->addWidget(optionGrp);
buttonBox = new QDialogButtonBox(q);
buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Close);
q->connect(buttonBox, &QDialogButtonBox::accepted, q, [this]() {
slotOk();
});
q->connect(buttonBox, &QDialogButtonBox::rejected, q, [this]() {
slotReject();
});
topLayout->addWidget(buttonBox);
// We delay creation of these until needed.
patterns = nullptr;
placeholders = nullptr;
// signals and slots connections
q->connect(selectedText, &QCheckBox::toggled, q, [this](bool checked) {
slotSelectedTextToggled(checked);
});
q->connect(regExp, &QCheckBox::toggled, regExpItem, &QWidget::setEnabled);
q->connect(backRef, &QCheckBox::toggled, backRefItem, &QWidget::setEnabled);
q->connect(regExpItem, &QPushButton::clicked, q, [this]() {
showPatterns();
});
q->connect(backRefItem, &QPushButton::clicked, q, [this]() {
showPlaceholders();
});
q->connect(find, &KHistoryComboBox::editTextChanged, q, [this](const QString &text) {
textSearchChanged(text);
});
q->connect(regExp, &QCheckBox::toggled, q, &KFindDialog::optionsChanged);
q->connect(backRef, &QCheckBox::toggled, q, &KFindDialog::optionsChanged);
q->connect(caseSensitive, &QCheckBox::toggled, q, &KFindDialog::optionsChanged);
q->connect(wholeWordsOnly, &QCheckBox::toggled, q, &KFindDialog::optionsChanged);
q->connect(fromCursor, &QCheckBox::toggled, q, &KFindDialog::optionsChanged);
q->connect(findBackwards, &QCheckBox::toggled, q, &KFindDialog::optionsChanged);
q->connect(selectedText, &QCheckBox::toggled, q, &KFindDialog::optionsChanged);
q->connect(promptOnReplace, &QCheckBox::toggled, q, &KFindDialog::optionsChanged);
// tab order
q->setTabOrder(find, regExp);
q->setTabOrder(regExp, regExpItem);
q->setTabOrder(regExpItem, replace); // findExtension widgets are inserted in showEvent()
q->setTabOrder(replace, backRef);
q->setTabOrder(backRef, backRefItem);
q->setTabOrder(backRefItem, caseSensitive);
q->setTabOrder(caseSensitive, wholeWordsOnly);
q->setTabOrder(wholeWordsOnly, fromCursor);
q->setTabOrder(fromCursor, findBackwards);
q->setTabOrder(findBackwards, selectedText);
q->setTabOrder(selectedText, promptOnReplace);
// buddies
findLabel->setBuddy(find);
replaceLabel->setBuddy(replace);
if (!forReplace) {
promptOnReplace->hide();
replaceGrp->hide();
}
findStrings = _findStrings;
find->setFocus();
QPushButton *buttonOk = buttonBox->button(QDialogButtonBox::Ok);
buttonOk->setEnabled(!q->pattern().isEmpty());
if (forReplace) {
KGuiItem::assign(buttonOk,
KGuiItem(i18n("&Replace"),
QString(),
i18n("Start replace"),
i18n("<qt>If you press the <b>Replace</b> button, the text you entered "
"above is searched for within the document and any occurrence is "
"replaced with the replacement text.</qt>")));
} else {
KGuiItem::assign(buttonOk,
KGuiItem(i18n("&Find"),
QStringLiteral("edit-find"),
i18n("Start searching"),
i18n("<qt>If you press the <b>Find</b> button, the text you entered "
"above is searched for within the document.</qt>")));
}
// QWhatsthis texts
find->setWhatsThis(i18n("Enter a pattern to search for, or select a previous pattern from the list."));
regExp->setWhatsThis(i18n("If enabled, search for a regular expression."));
regExpItem->setWhatsThis(i18n("Click here to edit your regular expression using a graphical editor."));
replace->setWhatsThis(i18n("Enter a replacement string, or select a previous one from the list."));
backRef->setWhatsThis(
i18n("<qt>If enabled, any occurrence of <code><b>\\N</b></code>, where "
"<code><b>N</b></code> is an integer number, will be replaced with "
"the corresponding capture (\"parenthesized substring\") from the "
"pattern.<p>To include (a literal <code><b>\\N</b></code> in your "
"replacement, put an extra backslash in front of it, like "
"<code><b>\\\\N</b></code>.</p></qt>"));
backRefItem->setWhatsThis(i18n("Click for a menu of available captures."));
wholeWordsOnly->setWhatsThis(i18n("Require word boundaries in both ends of a match to succeed."));
fromCursor->setWhatsThis(i18n("Start searching at the current cursor location rather than at the top."));
selectedText->setWhatsThis(i18n("Only search within the current selection."));
caseSensitive->setWhatsThis(i18n("Perform a case sensitive search: entering the pattern 'Joe' will not match 'joe' or 'JOE', only 'Joe'."));
findBackwards->setWhatsThis(i18n("Search backwards."));
promptOnReplace->setWhatsThis(i18n("Ask before replacing each match found."));
textSearchChanged(find->lineEdit()->text());
}
void KFindDialogPrivate::textSearchChanged(const QString &text)
{
buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!text.isEmpty());
}
void KFindDialog::showEvent(QShowEvent *e)
{
Q_D(KFindDialog);
if (!d->initialShowDone) {
d->initialShowDone = true; // only once
// qDebug() << "showEvent\n";
if (!d->findStrings.isEmpty()) {
setFindHistory(d->findStrings);
}
d->findStrings = QStringList();
if (!d->pattern.isEmpty()) {
d->find->lineEdit()->setText(d->pattern);
d->find->lineEdit()->selectAll();
d->pattern.clear();
}
// maintain a user-friendly tab order
if (d->findExtension) {
QWidget *prev = d->regExpItem;
const auto children = d->findExtension->findChildren<QWidget *>();
for (QWidget *child : children) {
setTabOrder(prev, child);
prev = child;
}
setTabOrder(prev, d->replace);
}
}
d->find->setFocus();
QDialog::showEvent(e);
}
long KFindDialog::options() const
{
Q_D(const KFindDialog);
long options = 0;
if (d->caseSensitive->isChecked()) {
options |= KFind::CaseSensitive;
}
if (d->wholeWordsOnly->isChecked()) {
options |= KFind::WholeWordsOnly;
}
if (d->fromCursor->isChecked()) {
options |= KFind::FromCursor;
}
if (d->findBackwards->isChecked()) {
options |= KFind::FindBackwards;
}
if (d->selectedText->isChecked()) {
options |= KFind::SelectedText;
}
if (d->regExp->isChecked()) {
options |= KFind::RegularExpression;
}
return options;
}
QString KFindDialog::pattern() const
{
Q_D(const KFindDialog);
return d->find->currentText();
}
void KFindDialog::setPattern(const QString &pattern)
{
Q_D(KFindDialog);
d->find->lineEdit()->setText(pattern);
d->find->lineEdit()->selectAll();
d->pattern = pattern;
// qDebug() << "setPattern " << pattern;
}
void KFindDialog::setFindHistory(const QStringList &strings)
{
Q_D(KFindDialog);
if (!strings.isEmpty()) {
d->find->setHistoryItems(strings, true);
d->find->lineEdit()->setText(strings.first());
d->find->lineEdit()->selectAll();
} else {
d->find->clearHistory();
}
}
void KFindDialog::setHasSelection(bool hasSelection)
{
Q_D(KFindDialog);
if (hasSelection) {
d->enabled |= KFind::SelectedText;
} else {
d->enabled &= ~KFind::SelectedText;
}
d->selectedText->setEnabled(hasSelection);
if (!hasSelection) {
d->selectedText->setChecked(false);
d->slotSelectedTextToggled(hasSelection);
}
}
void KFindDialogPrivate::slotSelectedTextToggled(bool selec)
{
// From cursor doesn't make sense if we have a selection
fromCursor->setEnabled(!selec && (enabled & KFind::FromCursor));
if (selec) { // uncheck if disabled
fromCursor->setChecked(false);
}
}
void KFindDialog::setHasCursor(bool hasCursor)
{
Q_D(KFindDialog);
if (hasCursor) {
d->enabled |= KFind::FromCursor;
} else {
d->enabled &= ~KFind::FromCursor;
}
d->fromCursor->setEnabled(hasCursor);
d->fromCursor->setChecked(hasCursor && (options() & KFind::FromCursor));
}
void KFindDialog::setSupportsBackwardsFind(bool supports)
{
Q_D(KFindDialog);
// ########## Shouldn't this hide the checkbox instead?
if (supports) {
d->enabled |= KFind::FindBackwards;
} else {
d->enabled &= ~KFind::FindBackwards;
}
d->findBackwards->setEnabled(supports);
d->findBackwards->setChecked(supports && (options() & KFind::FindBackwards));
}
void KFindDialog::setSupportsCaseSensitiveFind(bool supports)
{
Q_D(KFindDialog);
// ########## This should hide the checkbox instead
if (supports) {
d->enabled |= KFind::CaseSensitive;
} else {
d->enabled &= ~KFind::CaseSensitive;
}
d->caseSensitive->setEnabled(supports);
d->caseSensitive->setChecked(supports && (options() & KFind::CaseSensitive));
}
void KFindDialog::setSupportsWholeWordsFind(bool supports)
{
Q_D(KFindDialog);
// ########## This should hide the checkbox instead
if (supports) {
d->enabled |= KFind::WholeWordsOnly;
} else {
d->enabled &= ~KFind::WholeWordsOnly;
}
d->wholeWordsOnly->setEnabled(supports);
d->wholeWordsOnly->setChecked(supports && (options() & KFind::WholeWordsOnly));
}
void KFindDialog::setSupportsRegularExpressionFind(bool supports)
{
Q_D(KFindDialog);
if (supports) {
d->enabled |= KFind::RegularExpression;
} else {
d->enabled &= ~KFind::RegularExpression;
}
d->regExp->setEnabled(supports);
d->regExp->setChecked(supports && (options() & KFind::RegularExpression));
if (!supports) {
d->regExpItem->hide();
d->regExp->hide();
} else {
d->regExpItem->show();
d->regExp->show();
}
}
void KFindDialog::setOptions(long options)
{
Q_D(KFindDialog);
d->caseSensitive->setChecked((d->enabled & KFind::CaseSensitive) && (options & KFind::CaseSensitive));
d->wholeWordsOnly->setChecked((d->enabled & KFind::WholeWordsOnly) && (options & KFind::WholeWordsOnly));
d->fromCursor->setChecked((d->enabled & KFind::FromCursor) && (options & KFind::FromCursor));
d->findBackwards->setChecked((d->enabled & KFind::FindBackwards) && (options & KFind::FindBackwards));
d->selectedText->setChecked((d->enabled & KFind::SelectedText) && (options & KFind::SelectedText));
d->regExp->setChecked((d->enabled & KFind::RegularExpression) && (options & KFind::RegularExpression));
}
// Create a popup menu with a list of regular expression terms, to help the user
// compose a regular expression search pattern.
void KFindDialogPrivate::showPatterns()
{
Q_Q(KFindDialog);
typedef struct {
const KLazyLocalizedString description;
const char *regExp;
int cursorAdjustment;
} Term;
static const Term items[] = {
{kli18n("Any Character"), ".", 0},
{kli18n("Start of Line"), "^", 0},
{kli18n("End of Line"), "$", 0},
{kli18n("Set of Characters"), "[]", -1},
{kli18n("Repeats, Zero or More Times"), "*", 0},
{kli18n("Repeats, One or More Times"), "+", 0},
{kli18n("Optional"), "?", 0},
{kli18n("Escape"), "\\", 0},
{kli18n("TAB"), "\\t", 0},
{kli18n("Newline"), "\\n", 0},
{kli18n("Carriage Return"), "\\r", 0},
{kli18n("White Space"), "\\s", 0},
{kli18n("Digit"), "\\d", 0},
};
class RegExpAction : public QAction
{
public:
RegExpAction(QObject *parent, const QString &text, const QString &regExp, int cursor)
: QAction(text, parent)
, mText(text)
, mRegExp(regExp)
, mCursor(cursor)
{
}
QString text() const
{
return mText;
}
QString regExp() const
{
return mRegExp;
}
int cursor() const
{
return mCursor;
}
private:
QString mText;
QString mRegExp;
int mCursor;
};
// Populate the popup menu.
if (!patterns) {
patterns = new QMenu(q);
for (const Term &item : items) {
patterns->addAction(new RegExpAction(patterns, item.description.toString(), QLatin1String(item.regExp), item.cursorAdjustment));
}
}
// Insert the selection into the edit control.
QAction *action = patterns->exec(regExpItem->mapToGlobal(regExpItem->rect().bottomLeft()));
if (action) {
RegExpAction *regExpAction = static_cast<RegExpAction *>(action);
if (regExpAction) {
QLineEdit *editor = find->lineEdit();
editor->insert(regExpAction->regExp());
editor->setCursorPosition(editor->cursorPosition() + regExpAction->cursor());
}
}
}
class PlaceHolderAction : public QAction
{
public:
PlaceHolderAction(QObject *parent, const QString &text, int id)
: QAction(text, parent)
, mText(text)
, mId(id)
{
}
QString text() const
{
return mText;
}
int id() const
{
return mId;
}
private:
QString mText;
int mId;
};
// Create a popup menu with a list of backreference terms, to help the user
// compose a regular expression replacement pattern.
void KFindDialogPrivate::showPlaceholders()
{
Q_Q(KFindDialog);
// Populate the popup menu.
if (!placeholders) {
placeholders = new QMenu(q);
q->connect(placeholders, &QMenu::aboutToShow, q, [this]() {
slotPlaceholdersAboutToShow();
});
}
// Insert the selection into the edit control.
QAction *action = placeholders->exec(backRefItem->mapToGlobal(backRefItem->rect().bottomLeft()));
if (action) {
PlaceHolderAction *placeHolderAction = static_cast<PlaceHolderAction *>(action);
if (placeHolderAction) {
QLineEdit *editor = replace->lineEdit();
editor->insert(QStringLiteral("\\%1").arg(placeHolderAction->id()));
}
}
}
void KFindDialogPrivate::slotPlaceholdersAboutToShow()
{
Q_Q(KFindDialog);
placeholders->clear();
placeholders->addAction(new PlaceHolderAction(placeholders, i18n("Complete Match"), 0));
const int n = QRegularExpression(q->pattern(), QRegularExpression::UseUnicodePropertiesOption).captureCount();
for (int i = 1; i <= n; ++i) {
placeholders->addAction(new PlaceHolderAction(placeholders, i18n("Captured Text (%1)", i), i));
}
}
void KFindDialogPrivate::slotOk()
{
Q_Q(KFindDialog);
// Nothing to find?
if (q->pattern().isEmpty()) {
KMessageBox::error(q, i18n("You must enter some text to search for."));
return;
}
if (regExp->isChecked()) {
// Check for a valid regular expression.
if (!QRegularExpression(q->pattern(), QRegularExpression::UseUnicodePropertiesOption).isValid()) {
KMessageBox::error(q, i18n("Invalid PCRE pattern syntax."));
return;
}
}
find->addToHistory(q->pattern());
if (q->windowModality() != Qt::NonModal) {
q->accept();
}
Q_EMIT q->okClicked();
}
void KFindDialogPrivate::slotReject()
{
Q_Q(KFindDialog);
Q_EMIT q->cancelClicked();
q->reject();
}
#include "moc_kfinddialog.cpp"
@@ -0,0 +1,231 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2001 S.R. Haque <srhaque@iee.org>.
SPDX-FileCopyrightText: 2002 David Faure <david@mandrakesoft.com>
SPDX-License-Identifier: LGPL-2.0-only
*/
#ifndef KFINDDIALOG_H
#define KFINDDIALOG_H
#include "ktextwidgets_export.h"
#include <QDialog>
#include <memory>
class KFindDialogPrivate;
/**
* @class KFindDialog kfinddialog.h <KFindDialog>
*
* @brief A generic "find" dialog.
*
* @author S.R.Haque <srhaque@iee.org>
*
* \b Detail:
*
* This widget inherits from KDialog and implements
* the following additional functionalities: a find string
* object and an area for a user-defined widget to extend the dialog.
*
* \b Example:
*
* To use the basic modal find dialog, and then run the search:
*
* \code
* KFindDialog dlg(....)
* if (dlg.exec() != QDialog::Accepted)
* return;
*
* // proceed with KFind from here
* \endcode
*
* To create a non-modal find dialog:
* \code
* if (m_findDialog) {
* m_findDialog->activateWindow();
* } else {
* m_findDialog = new KFindDialog(...);
* connect(m_findDialog, &KFindDialog::okClicked, this, [this] {
* m_findDialog->close();
* delete m_find;
* m_find = new KFind(m_findDialog->pattern(), m_findDialog->options(), this);
* // ... see KFind documentation for what else should be done here
* });
* }
* \endcode
* Don't forget to delete and reset m_findDialog when closed.
* (But do NOT delete your KFind object at that point, it's needed for "Find Next".)
*
* To use your own extensions: see findExtension().
*
* \image html kfinddialog.png "KFindDialog Widget"
*/
class KTEXTWIDGETS_EXPORT KFindDialog : public QDialog
{
Q_OBJECT
public:
/**
* Construct a modal find dialog
*
* @param parent The parent object of this widget.
* @param options A bitfield of the Options to be checked.
* @param findStrings The find history, see findHistory()
* @param hasSelection Whether a selection exists
*/
explicit KFindDialog(QWidget *parent = nullptr,
long options = 0,
const QStringList &findStrings = QStringList(),
bool hasSelection = false,
bool replaceDialog = false);
/**
* Destructor.
*/
~KFindDialog() override;
/**
* Provide the list of @p strings to be displayed as the history
* of find strings. @p strings might get truncated if it is
* too long.
*
* @param history The find history.
* @see findHistory
*/
void setFindHistory(const QStringList &history);
/**
* Returns the list of history items.
*
* @see setFindHistory
*/
QStringList findHistory() const;
/**
* Enable/disable the 'search in selection' option, depending
* on whether there actually is a selection.
*
* @param hasSelection true if a selection exists
*/
void setHasSelection(bool hasSelection);
/**
* Hide/show the 'from cursor' option, depending
* on whether the application implements a cursor.
*
* @param hasCursor true if the application features a cursor
* This is assumed to be the case by default.
*/
void setHasCursor(bool hasCursor);
/**
* Enable/disable the 'Find backwards' option, depending
* on whether the application supports it.
*
* @param supports true if the application supports backwards find
* This is assumed to be the case by default.
*/
void setSupportsBackwardsFind(bool supports);
/**
* Enable/disable the 'Case sensitive' option, depending
* on whether the application supports it.
*
* @param supports true if the application supports case sensitive find
* This is assumed to be the case by default.
*/
void setSupportsCaseSensitiveFind(bool supports);
/**
* Enable/disable the 'Whole words only' option, depending
* on whether the application supports it.
*
* @param supports true if the application supports whole words only find
* This is assumed to be the case by default.
*/
void setSupportsWholeWordsFind(bool supports);
/**
* Enable/disable the 'Regular expression' option, depending
* on whether the application supports it.
*
* @param supports true if the application supports regular expression find
* This is assumed to be the case by default.
*/
void setSupportsRegularExpressionFind(bool supports);
/**
* Set the options which are checked.
*
* @param options The setting of the Options.
*
* @see options()
* @see KFind::Options
*/
void setOptions(long options);
/**
* Returns the state of the options. Disabled options may be returned in
* an indeterminate state.
*
* @see setOptions()
* @see KFind::Options
*/
long options() const;
/**
* Returns the pattern to find.
*/
QString pattern() const;
/**
* Sets the pattern to find
*/
void setPattern(const QString &pattern);
/**
* Returns an empty widget which the user may fill with additional UI
* elements as required. The widget occupies the width of the dialog,
* and is positioned immediately below the regular expression support
* widgets for the pattern string.
*/
QWidget *findExtension() const;
Q_SIGNALS:
/**
* This signal is sent whenever one of the option checkboxes is toggled.
* Call options() to get the new state of the checkboxes.
*/
void optionsChanged();
/**
* This signal is sent when the user clicks on Ok button.
*/
void okClicked();
/**
* This signal is sent when the user clicks on Cancel button.
*/
void cancelClicked();
protected:
void showEvent(QShowEvent *) override;
protected:
KTEXTWIDGETS_NO_EXPORT explicit KFindDialog(KFindDialogPrivate &dd,
QWidget *parent = nullptr,
long options = 0,
const QStringList &findStrings = QStringList(),
bool hasSelection = false,
bool replaceDialog = false);
protected:
std::unique_ptr<class KFindDialogPrivate> const d_ptr;
private:
Q_DECLARE_PRIVATE(KFindDialog)
};
#endif // KFINDDIALOG_H
@@ -0,0 +1,90 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2001 S.R. Haque <srhaque@iee.org>.
SPDX-FileCopyrightText: 2002 David Faure <david@mandrakesoft.com>
SPDX-License-Identifier: LGPL-2.0-only
*/
#ifndef KFINDDIALOG_P_H
#define KFINDDIALOG_P_H
#include "kfind.h"
#include "kfinddialog.h"
#include <QStringList>
class KHistoryComboBox;
class QCheckBox;
class QDialogButtonBox;
class QGridLayout;
class QGroupBox;
class QMenu;
class QPushButton;
class KFindDialogPrivate
{
Q_DECLARE_PUBLIC(KFindDialog)
public:
KFindDialogPrivate(KFindDialog *qq)
: q_ptr(qq)
, regexpDialog(nullptr)
, regexpDialogQueryDone(false)
, initialShowDone(false)
, enabled(KFind::WholeWordsOnly | KFind::FromCursor | KFind::SelectedText | KFind::CaseSensitive | KFind::FindBackwards | KFind::RegularExpression)
, findExtension(nullptr)
, buttonBox(nullptr)
{
}
virtual ~KFindDialogPrivate() = default;
void init(bool forReplace, const QStringList &findStrings, bool hasSelection);
void slotPlaceholdersAboutToShow();
void slotOk();
void slotReject();
void slotSelectedTextToggled(bool);
void showPatterns();
void showPlaceholders();
void textSearchChanged(const QString &);
KFindDialog *const q_ptr = nullptr;
QDialog *regexpDialog = nullptr;
bool regexpDialogQueryDone : 1;
bool initialShowDone : 1;
long enabled; // uses Options to define which search options are enabled
QStringList findStrings;
QString pattern;
mutable QWidget *findExtension = nullptr;
QDialogButtonBox *buttonBox = nullptr;
QGroupBox *findGrp = nullptr;
KHistoryComboBox *find = nullptr;
QCheckBox *regExp = nullptr;
QPushButton *regExpItem = nullptr;
QGridLayout *findLayout = nullptr;
QCheckBox *wholeWordsOnly = nullptr;
QCheckBox *fromCursor = nullptr;
QCheckBox *selectedText = nullptr;
QCheckBox *caseSensitive = nullptr;
QCheckBox *findBackwards = nullptr;
QMenu *patterns = nullptr;
// for the replace dialog
QGroupBox *replaceGrp = nullptr;
KHistoryComboBox *replace = nullptr;
QCheckBox *backRef = nullptr;
QPushButton *backRefItem = nullptr;
QGridLayout *replaceLayout = nullptr;
QCheckBox *promptOnReplace = nullptr;
QMenu *placeholders = nullptr;
};
#endif // KFINDDIALOG_P_H
@@ -0,0 +1,409 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2001 S.R. Haque <srhaque@iee.org>.
SPDX-FileCopyrightText: 2002 David Faure <david@mandrakesoft.com>
SPDX-License-Identifier: LGPL-2.0-only
*/
#include "kreplace.h"
#include "kfind_p.h"
#include "kreplacedialog.h"
#include <QDialogButtonBox>
#include <QLabel>
#include <QPushButton>
#include <QRegularExpression>
#include <QVBoxLayout>
#include <KLocalizedString>
#include <KMessageBox>
//#define DEBUG_REPLACE
#define INDEX_NOMATCH -1
class KReplaceNextDialog : public QDialog
{
Q_OBJECT
public:
explicit KReplaceNextDialog(QWidget *parent);
void setLabel(const QString &pattern, const QString &replacement);
QPushButton *replaceAllButton() const;
QPushButton *skipButton() const;
QPushButton *replaceButton() const;
private:
QLabel *m_mainLabel = nullptr;
QPushButton *m_allButton = nullptr;
QPushButton *m_skipButton = nullptr;
QPushButton *m_replaceButton = nullptr;
};
KReplaceNextDialog::KReplaceNextDialog(QWidget *parent)
: QDialog(parent)
{
setModal(false);
setWindowTitle(i18n("Replace"));
QVBoxLayout *layout = new QVBoxLayout(this);
m_mainLabel = new QLabel(this);
layout->addWidget(m_mainLabel);
m_allButton = new QPushButton(i18nc("@action:button Replace all occurrences", "&All"));
m_allButton->setObjectName(QStringLiteral("allButton"));
m_skipButton = new QPushButton(i18n("&Skip"));
m_skipButton->setObjectName(QStringLiteral("skipButton"));
m_replaceButton = new QPushButton(i18n("Replace"));
m_replaceButton->setObjectName(QStringLiteral("replaceButton"));
m_replaceButton->setDefault(true);
QDialogButtonBox *buttonBox = new QDialogButtonBox(this);
buttonBox->addButton(m_allButton, QDialogButtonBox::ActionRole);
buttonBox->addButton(m_skipButton, QDialogButtonBox::ActionRole);
buttonBox->addButton(m_replaceButton, QDialogButtonBox::ActionRole);
buttonBox->setStandardButtons(QDialogButtonBox::Close);
layout->addWidget(buttonBox);
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
}
void KReplaceNextDialog::setLabel(const QString &pattern, const QString &replacement)
{
m_mainLabel->setText(i18n("Replace '%1' with '%2'?", pattern, replacement));
}
QPushButton *KReplaceNextDialog::replaceAllButton() const
{
return m_allButton;
}
QPushButton *KReplaceNextDialog::skipButton() const
{
return m_skipButton;
}
QPushButton *KReplaceNextDialog::replaceButton() const
{
return m_replaceButton;
}
////
class KReplacePrivate : public KFindPrivate
{
Q_DECLARE_PUBLIC(KReplace)
public:
KReplacePrivate(KReplace *q, const QString &replacement)
: KFindPrivate(q)
, m_replacement(replacement)
{
}
KReplaceNextDialog *nextDialog();
void doReplace();
void slotSkip();
void slotReplace();
void slotReplaceAll();
QString m_replacement;
int m_replacements = 0;
QRegularExpressionMatch m_match;
};
////
KReplace::KReplace(const QString &pattern, const QString &replacement, long options, QWidget *parent)
: KFind(*new KReplacePrivate(this, replacement), pattern, options, parent)
{
}
KReplace::KReplace(const QString &pattern, const QString &replacement, long options, QWidget *parent, QWidget *dlg)
: KFind(*new KReplacePrivate(this, replacement), pattern, options, parent, dlg)
{
}
KReplace::~KReplace() = default;
int KReplace::numReplacements() const
{
Q_D(const KReplace);
return d->m_replacements;
}
QDialog *KReplace::replaceNextDialog(bool create)
{
Q_D(KReplace);
if (d->dialog || create) {
return d->nextDialog();
}
return nullptr;
}
KReplaceNextDialog *KReplacePrivate::nextDialog()
{
Q_Q(KReplace);
if (!dialog) {
auto *nextDialog = new KReplaceNextDialog(q->parentWidget());
q->connect(nextDialog->replaceAllButton(), &QPushButton::clicked, q, [this]() {
slotReplaceAll();
});
q->connect(nextDialog->skipButton(), &QPushButton::clicked, q, [this]() {
slotSkip();
});
q->connect(nextDialog->replaceButton(), &QPushButton::clicked, q, [this]() {
slotReplace();
});
q->connect(nextDialog, &QDialog::finished, q, [this]() {
slotDialogClosed();
});
dialog = nextDialog;
}
return static_cast<KReplaceNextDialog *>(dialog);
}
void KReplace::displayFinalDialog() const
{
Q_D(const KReplace);
if (!d->m_replacements) {
KMessageBox::information(parentWidget(), i18n("No text was replaced."));
} else {
KMessageBox::information(parentWidget(), i18np("1 replacement done.", "%1 replacements done.", d->m_replacements));
}
}
static int replaceHelper(QString &text, const QString &replacement, int index, long options, const QRegularExpressionMatch *match, int length)
{
QString rep(replacement);
if (options & KReplaceDialog::BackReference) {
// Handle backreferences
if (options & KFind::RegularExpression) { // regex search
Q_ASSERT(match);
const int capNum = match->regularExpression().captureCount();
for (int i = 0; i <= capNum; ++i) {
rep.replace(QLatin1String("\\") + QString::number(i), match->captured(i));
}
} else { // with non-regex search only \0 is supported, replace it with the
// right portion of 'text'
rep.replace(QLatin1String("\\0"), text.mid(index, length));
}
}
// Then replace rep into the text
text.replace(index, length, rep);
return rep.length();
}
KFind::Result KReplace::replace()
{
Q_D(KReplace);
#ifdef DEBUG_REPLACE
// qDebug() << "d->index=" << d->index;
#endif
if (d->index == INDEX_NOMATCH && d->lastResult == Match) {
d->lastResult = NoMatch;
return NoMatch;
}
do { // this loop is only because validateMatch can fail
#ifdef DEBUG_REPLACE
// qDebug() << "beginning of loop: d->index=" << d->index;
#endif
// Find the next match.
d->index = KFind::find(d->text, d->pattern, d->index, d->options, &d->matchedLength, d->options & KFind::RegularExpression ? &d->m_match : nullptr);
#ifdef DEBUG_REPLACE
// qDebug() << "KFind::find returned d->index=" << d->index;
#endif
if (d->index != -1) {
// Flexibility: the app can add more rules to validate a possible match
if (validateMatch(d->text, d->index, d->matchedLength)) {
if (d->options & KReplaceDialog::PromptOnReplace) {
#ifdef DEBUG_REPLACE
// qDebug() << "PromptOnReplace";
#endif
// Display accurate initial string and replacement string, they can vary
QString matchedText(d->text.mid(d->index, d->matchedLength));
QString rep(matchedText);
replaceHelper(rep, d->m_replacement, 0, d->options, d->options & KFind::RegularExpression ? &d->m_match : nullptr, d->matchedLength);
d->nextDialog()->setLabel(matchedText, rep);
d->nextDialog()->show(); // TODO kde5: virtual void showReplaceNextDialog(QString,QString), so that kreplacetest can skip the show()
// Tell the world about the match we found, in case someone wants to
// highlight it.
Q_EMIT textFound(d->text, d->index, d->matchedLength);
d->lastResult = Match;
return Match;
} else {
d->doReplace(); // this moves on too
}
} else {
// not validated -> move on
if (d->options & KFind::FindBackwards) {
d->index--;
} else {
d->index++;
}
}
} else {
d->index = INDEX_NOMATCH; // will exit the loop
}
} while (d->index != INDEX_NOMATCH);
d->lastResult = NoMatch;
return NoMatch;
}
int KReplace::replace(QString &text, const QString &pattern, const QString &replacement, int index, long options, int *replacedLength)
{
int matchedLength;
QRegularExpressionMatch match;
index = KFind::find(text, pattern, index, options, &matchedLength, options & KFind::RegularExpression ? &match : nullptr);
if (index != -1) {
*replacedLength = replaceHelper(text, replacement, index, options, options & KFind::RegularExpression ? &match : nullptr, matchedLength);
if (options & KFind::FindBackwards) {
index--;
} else {
index += *replacedLength;
}
}
return index;
}
void KReplacePrivate::slotReplaceAll()
{
Q_Q(KReplace);
doReplace();
options &= ~KReplaceDialog::PromptOnReplace;
Q_EMIT q->optionsChanged();
Q_EMIT q->findNext();
}
void KReplacePrivate::slotSkip()
{
Q_Q(KReplace);
if (options & KFind::FindBackwards) {
index--;
} else {
index++;
}
if (dialogClosed) {
dialog->deleteLater();
dialog = nullptr; // hide it again
} else {
Q_EMIT q->findNext();
}
}
void KReplacePrivate::slotReplace()
{
Q_Q(KReplace);
doReplace();
if (dialogClosed) {
dialog->deleteLater();
dialog = nullptr; // hide it again
} else {
Q_EMIT q->findNext();
}
}
void KReplacePrivate::doReplace()
{
Q_Q(KReplace);
Q_ASSERT(index >= 0);
const int replacedLength = replaceHelper(text, m_replacement, index, options, &m_match, matchedLength);
// Tell the world about the replacement we made, in case someone wants to
// highlight it.
Q_EMIT q->textReplaced(text, index, replacedLength, matchedLength);
#ifdef DEBUG_REPLACE
// qDebug() << "after replace() signal: d->index=" << d->index << " replacedLength=" << replacedLength;
#endif
m_replacements++;
if (options & KFind::FindBackwards) {
Q_ASSERT(index >= 0);
index--;
} else {
index += replacedLength;
// when replacing the empty pattern, move on. See also kjs/regexp.cpp for how this should be done for regexps.
if (pattern.isEmpty()) {
++index;
}
}
#ifdef DEBUG_REPLACE
// qDebug() << "after adjustment: d->index=" << d->index;
#endif
}
void KReplace::resetCounts()
{
Q_D(KReplace);
KFind::resetCounts();
d->m_replacements = 0;
}
bool KReplace::shouldRestart(bool forceAsking, bool showNumMatches) const
{
Q_D(const KReplace);
// Only ask if we did a "find from cursor", otherwise it's pointless.
// ... Or if the prompt-on-replace option was set.
// Well, unless the user can modify the document during a search operation,
// hence the force boolean.
if (!forceAsking && (d->options & KFind::FromCursor) == 0 && (d->options & KReplaceDialog::PromptOnReplace) == 0) {
displayFinalDialog();
return false;
}
QString message;
if (showNumMatches) {
if (!d->m_replacements) {
message = i18n("No text was replaced.");
} else {
message = i18np("1 replacement done.", "%1 replacements done.", d->m_replacements);
}
} else {
if (d->options & KFind::FindBackwards) {
message = i18n("Beginning of document reached.");
} else {
message = i18n("End of document reached.");
}
}
message += QLatin1Char('\n');
// Hope this word puzzle is ok, it's a different sentence
message +=
(d->options & KFind::FindBackwards) ? i18n("Do you want to restart search from the end?") : i18n("Do you want to restart search at the beginning?");
int ret = KMessageBox::questionTwoActions(parentWidget(),
message,
QString(),
KGuiItem(i18nc("@action:button Restart find & replace", "Restart")),
KGuiItem(i18nc("@action:button Stop find & replace", "Stop")));
return (ret == KMessageBox::PrimaryAction);
}
void KReplace::closeReplaceNextDialog()
{
closeFindNextDialog();
}
#include "kreplace.moc"
#include "moc_kreplace.cpp"
@@ -0,0 +1,207 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2001 S.R. Haque <srhaque@iee.org>.
SPDX-FileCopyrightText: 2002 David Faure <david@mandrakesoft.com>
SPDX-License-Identifier: LGPL-2.0-only
*/
#ifndef KREPLACE_H
#define KREPLACE_H
#include "kfind.h"
#include "ktextwidgets_export.h"
class KReplacePrivate;
/**
* @class KReplace kreplace.h <KReplace>
*
* @brief A generic implementation of the "replace" function.
*
* @author S.R.Haque <srhaque@iee.org>, David Faure <faure@kde.org>
*
* \b Detail:
*
* This class includes prompt handling etc. Also provides some
* static functions which can be used to create custom behavior
* instead of using the class directly.
*
* \b Example:
*
* To use the class to implement a complete replace feature:
*
* In the slot connect to the replace action, after using KReplaceDialog:
* \code
*
* // This creates a replace-on-prompt dialog if needed.
* m_replace = new KReplace(pattern, replacement, options, this);
*
* // Connect signals to code which handles highlighting of found text, and
* // on-the-fly replacement.
* connect(m_replace, &KFind::textFound, this, [this](const QString &text, int matchingIndex, int matchedLength) {
* slotHighlight(text, matchingIndex, matchedLength);
* });
* // Connect findNext signal - called when pressing the button in the dialog
* connect( m_replace, SIGNAL( findNext() ),
* this, SLOT( slotReplaceNext() ) );
* // Connect to the textReplaced() signal - emitted when a replacement is done
* connect( m_replace, &KReplace::textReplaced, this, [this](const QString &text, int replacementIndex, int replacedLength, int matchedLength) {
* slotReplace(text, replacementIndex, replacedLength, matchedLength);
* });
* \endcode
* Then initialize the variables determining the "current position"
* (to the cursor, if the option FromCursor is set,
* to the beginning of the selection if the option SelectedText is set,
* and to the beginning of the document otherwise).
* Initialize the "end of search" variables as well (end of doc or end of selection).
* Swap begin and end if FindBackwards.
* Finally, call slotReplaceNext();
*
* \code
* void slotReplaceNext()
* {
* KFind::Result res = KFind::NoMatch;
* while ( res == KFind::NoMatch && <position not at end> ) {
* if ( m_replace->needData() )
* m_replace->setData( <current text fragment> );
*
* // Let KReplace inspect the text fragment, and display a dialog if a match is found
* res = m_replace->replace();
*
* if ( res == KFind::NoMatch ) {
* <Move to the next text fragment, honoring the FindBackwards setting for the direction>
* }
* }
*
* if ( res == KFind::NoMatch ) // i.e. at end
* <Call either m_replace->displayFinalDialog(); delete m_replace; m_replace = nullptr;
* or if ( m_replace->shouldRestart() ) { reinit (w/o FromCursor) and call slotReplaceNext(); }
* else { m_replace->closeReplaceNextDialog(); }>
* }
* \endcode
*
* Don't forget delete m_find in the destructor of your class,
* unless you gave it a parent widget on construction.
*
*/
class KTEXTWIDGETS_EXPORT KReplace : public KFind
{
Q_OBJECT
public:
/**
* Only use this constructor if you don't use KFindDialog, or if
* you use it as a modal dialog.
*/
KReplace(const QString &pattern, const QString &replacement, long options, QWidget *parent = nullptr);
/**
* This is the recommended constructor if you also use KReplaceDialog (non-modal).
* You should pass the pointer to it here, so that when a message box
* appears it has the right parent. Don't worry about deletion, KReplace
* will notice if the find dialog is closed.
*/
KReplace(const QString &pattern, const QString &replacement, long options, QWidget *parent, QWidget *replaceDialog);
~KReplace() override;
/**
* Return the number of replacements made (i.e. the number of times
* the textReplaced() signal was emitted).
*
* Can be used in a dialog box to tell the user how many replacements were made.
* The final dialog does so already, unless you used setDisplayFinalDialog(false).
*/
int numReplacements() const;
/**
* Call this to reset the numMatches & numReplacements counts.
* Can be useful if reusing the same KReplace for different operations,
* or when restarting from the beginning of the document.
*/
void resetCounts() override;
/**
* Walk the text fragment (e.g. kwrite line, kspread cell) looking for matches.
* For each match, if prompt-on-replace is specified, emits the textFound() signal
* and displays the prompt-for-replace dialog before doing the replace.
*/
Result replace();
/**
* Return (or create) the dialog that shows the "find next?" prompt.
* Usually you don't need to call this.
* One case where it can be useful, is when the user selects the "Find"
* menu item while a find operation is under way. In that case, the
* program may want to call setActiveWindow() on that dialog.
*/
QDialog *replaceNextDialog(bool create = false);
/**
* Close the "replace next?" dialog. The application should do this when
* the last match was hit. If the application deletes the KReplace, then
* "find previous" won't be possible anymore.
*/
void closeReplaceNextDialog();
/**
* Searches the given @p text for @p pattern; if a match is found it is replaced
* with @p replacement and the index of the replacement string is returned.
*
* @param text The string to search
* @param pattern The pattern to search for
* @param replacement The replacement string to insert into the text
* @param index The starting index into the string
* @param options The options to use
* @param replacedLength Output parameter, contains the length of the replaced string
* Not always the same as replacement.length(), when backreferences are used
* @return The index at which a match was found, or -1 otherwise
*/
static int replace(QString &text, const QString &pattern, const QString &replacement, int index, long options, int *replacedLength);
/**
* Returns true if we should restart the search from scratch.
* Can ask the user, or return false (if we already searched/replaced the
* whole document without the PromptOnReplace option).
*
* @param forceAsking set to true if the user modified the document during the
* search. In that case it makes sense to restart the search again.
*
* @param showNumMatches set to true if the dialog should show the number of
* matches. Set to false if the application provides a "find previous" action,
* in which case the match count will be erroneous when hitting the end,
* and we could even be hitting the beginning of the document (so not all
* matches have even been seen).
*/
bool shouldRestart(bool forceAsking = false, bool showNumMatches = true) const override;
/**
* Displays the final dialog telling the user how many replacements were made.
* Call either this or shouldRestart().
*/
void displayFinalDialog() const override;
Q_SIGNALS:
/**
* Connect to this signal to implement updating of replaced text during the replace
* operation.
*
* Extra care must be taken to properly implement the "no prompt-on-replace" case.
* For instance, the textFound() signal isn't emitted in that case (some code
* might rely on it), and for performance reasons one should repaint after
* replace() ONLY if prompt-on-replace was selected.
*
* @param text The text, in which the replacement has already been done
* @param replacementIndex Starting index of the matched substring
* @param replacedLength Length of the replacement string
* @param matchedLength Length of the matched string
*
* @since 5.83
*/
void textReplaced(const QString &text, int replacementIndex, int replacedLength, int matchedLength);
private:
Q_DECLARE_PRIVATE(KReplace)
};
#endif
@@ -0,0 +1,166 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2001 S.R. Haque <srhaque@iee.org>.
SPDX-FileCopyrightText: 2002 David Faure <david@mandrakesoft.com>
SPDX-License-Identifier: LGPL-2.0-only
*/
#include "kreplacedialog.h"
#include "kfinddialog_p.h"
#include <QCheckBox>
#include <QGridLayout>
#include <QGroupBox>
#include <QLineEdit>
#include <QRegularExpression>
#include <KHistoryComboBox>
#include <KLocalizedString>
#include <KMessageBox>
/**
* we need to insert the strings after the dialog is set
* up, otherwise QComboBox will deliver an awful big sizeHint
* for long replacement texts.
*/
class KReplaceDialogPrivate : public KFindDialogPrivate
{
Q_DECLARE_PUBLIC(KReplaceDialog)
public:
explicit KReplaceDialogPrivate(KReplaceDialog *qq)
: KFindDialogPrivate(qq)
{
}
void slotOk();
QStringList replaceStrings;
mutable QWidget *replaceExtension = nullptr;
bool initialShowDone = false;
};
KReplaceDialog::KReplaceDialog(QWidget *parent, long options, const QStringList &findStrings, const QStringList &replaceStrings, bool hasSelection)
: KFindDialog(*new KReplaceDialogPrivate(this), parent, options, findStrings, hasSelection, true /*create replace dialog*/)
{
Q_D(KReplaceDialog);
d->replaceStrings = replaceStrings;
}
KReplaceDialog::~KReplaceDialog() = default;
void KReplaceDialog::showEvent(QShowEvent *e)
{
Q_D(KReplaceDialog);
if (!d->initialShowDone) {
d->initialShowDone = true; // only once
if (!d->replaceStrings.isEmpty()) {
setReplacementHistory(d->replaceStrings);
d->replace->lineEdit()->setText(d->replaceStrings[0]);
}
}
KFindDialog::showEvent(e);
}
long KReplaceDialog::options() const
{
Q_D(const KReplaceDialog);
long options = 0;
options = KFindDialog::options();
if (d->promptOnReplace->isChecked()) {
options |= PromptOnReplace;
}
if (d->backRef->isChecked()) {
options |= BackReference;
}
return options;
}
QWidget *KReplaceDialog::replaceExtension() const
{
Q_D(const KReplaceDialog);
if (!d->replaceExtension) {
d->replaceExtension = new QWidget(d->replaceGrp);
d->replaceLayout->addWidget(d->replaceExtension, 3, 0, 1, 2);
}
return d->replaceExtension;
}
QString KReplaceDialog::replacement() const
{
Q_D(const KReplaceDialog);
return d->replace->currentText();
}
QStringList KReplaceDialog::replacementHistory() const
{
Q_D(const KReplaceDialog);
QStringList lst = d->replace->historyItems();
// historyItems() doesn't tell us about the case of replacing with an empty string
if (d->replace->lineEdit()->text().isEmpty()) {
lst.prepend(QString());
}
return lst;
}
void KReplaceDialog::setOptions(long options)
{
Q_D(KReplaceDialog);
KFindDialog::setOptions(options);
d->promptOnReplace->setChecked(options & PromptOnReplace);
d->backRef->setChecked(options & BackReference);
}
void KReplaceDialog::setReplacementHistory(const QStringList &strings)
{
Q_D(KReplaceDialog);
if (!strings.isEmpty()) {
d->replace->setHistoryItems(strings, true);
} else {
d->replace->clearHistory();
}
}
void KReplaceDialogPrivate::slotOk()
{
Q_Q(KReplaceDialog);
// If regex and backrefs are enabled, do a sanity check.
if (regExp->isChecked() && backRef->isChecked()) {
const QRegularExpression re(q->pattern(), QRegularExpression::UseUnicodePropertiesOption);
const int caps = re.captureCount();
const QString rep = q->replacement();
const static QRegularExpression check(QStringLiteral("((?:\\\\)+)(\\d+)"));
auto iter = check.globalMatch(rep);
while (iter.hasNext()) {
const QRegularExpressionMatch match = iter.next();
if ((match.captured(1).size() % 2) && match.captured(2).toInt() > caps) {
KMessageBox::information(q,
i18n("Your replacement string is referencing a capture greater than '\\%1', ", caps)
+ (caps ? i18np("but your pattern only defines 1 capture.", "but your pattern only defines %1 captures.", caps)
: i18n("but your pattern defines no captures."))
+ i18n("\nPlease correct."));
return; // abort OKing
}
}
}
slotOk();
replace->addToHistory(q->replacement());
}
#include "moc_kreplacedialog.cpp"
@@ -0,0 +1,132 @@
/*
This file is part of the KDE project
SPDX-FileCopyrightText: 2001 S.R. Haque <srhaque@iee.org>.
SPDX-FileCopyrightText: 2002 David Faure <david@mandrakesoft.com>
SPDX-License-Identifier: LGPL-2.0-only
*/
#ifndef KREPLACEDIALOG_H
#define KREPLACEDIALOG_H
#include "ktextwidgets_export.h"
#include "kfinddialog.h"
class KReplaceDialogPrivate;
/**
* @class KReplaceDialog kreplace.h <KReplaceDialog>
*
* @short A generic "replace" dialog.
*
* @author S.R.Haque <srhaque@iee.org>
*
* \b Detail:
*
* This widget inherits from KFindDialog and implements
* the following additional functionalities: a replacement string
* object and an area for a user-defined widget to extend the dialog.
*
* \b Example:
*
* To use the basic replace dialog:
*
* \code
* \endcode
*
* To use your own extensions:
*
* \code
* \endcode
*
* \image html kreplacedialog.png "KReplaceDialog Widget"
*/
class KTEXTWIDGETS_EXPORT KReplaceDialog : public KFindDialog
{
Q_OBJECT
public:
/// Options.
enum Options {
/// Should the user be prompted before the replace operation?
PromptOnReplace = 256,
BackReference = 512,
};
/**
* Construct a replace dialog.read-only or rather select-only combo box with a
* parent object and a name.
*
* @param parent The parent object of this widget
* @param options A bitfield of the Options to be enabled.
* @param findStrings A QStringList to insert in the combo box of text to find
* @param replaceStrings A QStringList to insert in the combo box of text to
* replace with
* @param hasSelection Whether a selection exists
*/
explicit KReplaceDialog(QWidget *parent = nullptr,
long options = 0,
const QStringList &findStrings = QStringList(),
const QStringList &replaceStrings = QStringList(),
bool hasSelection = true);
/**
* Destructor.
*/
~KReplaceDialog() override;
/**
* Provide the list of @p strings to be displayed as the history
* of replacement strings. @p strings might get truncated if it is
* too long.
*
* @param history The replacement history.
* @see replacementHistory
*/
void setReplacementHistory(const QStringList &history);
/**
* Returns the list of history items.
*
* @see setReplacementHistory
*/
QStringList replacementHistory() const;
/**
* Set the options which are enabled.
*
* @param options The setting of the Options.
*/
void setOptions(long options);
/**
* Returns the state of the options. Disabled options may be returned in
* an indeterminate state.
*
* @see setOptions
*/
long options() const;
/**
* Returns the replacement string.
*/
QString replacement() const;
/**
* Returns an empty widget which the user may fill with additional UI
* elements as required. The widget occupies the width of the dialog,
* and is positioned immediately the regular expression support widgets
* for the replacement string.
*/
QWidget *replaceExtension() const;
protected:
void showEvent(QShowEvent *) override;
private:
Q_DECLARE_PRIVATE(KReplaceDialog)
};
#endif // KREPLACEDIALOG_H
@@ -0,0 +1,53 @@
/*
SPDX-FileCopyrightText: 2014 Laurent Montel <montel@kde.org>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "kpluralhandlingspinbox.h"
#if KTEXTWIDGETS_BUILD_DEPRECATED_SINCE(6, 6)
class KPluralHandlingSpinBoxPrivate
{
public:
KPluralHandlingSpinBoxPrivate(QSpinBox *qq)
: q(qq)
{
QObject::connect(q, qOverload<int>(&QSpinBox::valueChanged), q, [this](int value) {
updateSuffix(value);
});
}
void updateSuffix(int value)
{
if (!pluralSuffix.isEmpty()) {
KLocalizedString s = pluralSuffix;
q->setSuffix(s.subs(value).toString());
}
}
QSpinBox *const q;
KLocalizedString pluralSuffix;
};
KPluralHandlingSpinBox::KPluralHandlingSpinBox(QWidget *parent)
: QSpinBox(parent)
, d(new KPluralHandlingSpinBoxPrivate(this))
{
}
KPluralHandlingSpinBox::~KPluralHandlingSpinBox() = default;
void KPluralHandlingSpinBox::setSuffix(const KLocalizedString &suffix)
{
d->pluralSuffix = suffix;
if (suffix.isEmpty()) {
QSpinBox::setSuffix(QString());
} else {
d->updateSuffix(value());
}
}
#include "moc_kpluralhandlingspinbox.cpp"
#endif
@@ -0,0 +1,56 @@
/*
SPDX-FileCopyrightText: 2014 Laurent Montel <montel@kde.org>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef KPLURALHANDLINGSPINBOX_H
#define KPLURALHANDLINGSPINBOX_H
#include <ktextwidgets_export.h>
#if KTEXTWIDGETS_ENABLE_DEPRECATED_SINCE(6, 6)
#include <KLocalizedString>
#include <QSpinBox>
#include <memory>
/**
* @class KPluralHandlingSpinBox kpluralhandlingspinbox.h <KPluralHandlingSpinBox>
*
* @brief A QSpinBox with plural handling for the suffix.
*
* @author Laurent Montel <montel@kde.org>
*
* @since 5.0
* @deprecated since 6.6, use KLocalization::setupSpinBoxFormatString() from KF6::I18n instead,
* which is more powerful and does not require inheriting from a specific QSpinBox subclass.
*/
KTEXTWIDGETS_DEPRECATED_VERSION(6, 6, "use KLocalization::setupSpinBoxFormatString() from KF6::I18n instead")
class KTEXTWIDGETS_EXPORT KPluralHandlingSpinBox : public QSpinBox
{
Q_OBJECT
public:
/**
* Default constructor
*/
explicit KPluralHandlingSpinBox(QWidget *parent = nullptr);
~KPluralHandlingSpinBox() override;
/**
* Sets the suffix to @p suffix.
* Use this to add a plural-aware suffix, e.g. by using ki18np("singular", "plural").
*/
void setSuffix(const KLocalizedString &suffix);
private:
friend class KPluralHandlingSpinBoxPrivate;
std::unique_ptr<class KPluralHandlingSpinBoxPrivate> const d;
Q_DISABLE_COPY(KPluralHandlingSpinBox)
};
#endif
#endif // KPLURALHANDLINGSPINBOX_H
@@ -0,0 +1,661 @@
/*
krichtextedit
SPDX-FileCopyrightText: 2007 Laurent Montel <montel@kde.org>
SPDX-FileCopyrightText: 2008 Thomas McGuire <thomas.mcguire@gmx.net>
SPDX-FileCopyrightText: 2008 Stephen Kelly <steveire@gmail.com>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "krichtextedit.h"
#include "krichtextedit_p.h"
// Own includes
#include "klinkdialog_p.h"
// kdelibs includes
#include <KColorScheme>
#include <KCursor>
// Qt includes
#include <QRegularExpression>
void KRichTextEditPrivate::activateRichText()
{
Q_Q(KRichTextEdit);
if (mMode == KRichTextEdit::Plain) {
q->setAcceptRichText(true);
mMode = KRichTextEdit::Rich;
Q_EMIT q->textModeChanged(mMode);
}
}
void KRichTextEditPrivate::setTextCursor(QTextCursor &cursor)
{
Q_Q(KRichTextEdit);
q->setTextCursor(cursor);
}
void KRichTextEditPrivate::mergeFormatOnWordOrSelection(const QTextCharFormat &format)
{
Q_Q(KRichTextEdit);
QTextCursor cursor = q->textCursor();
QTextCursor wordStart(cursor);
QTextCursor wordEnd(cursor);
wordStart.movePosition(QTextCursor::StartOfWord);
wordEnd.movePosition(QTextCursor::EndOfWord);
cursor.beginEditBlock();
if (!cursor.hasSelection() && cursor.position() != wordStart.position() && cursor.position() != wordEnd.position()) {
cursor.select(QTextCursor::WordUnderCursor);
}
cursor.mergeCharFormat(format);
q->mergeCurrentCharFormat(format);
cursor.endEditBlock();
}
KRichTextEdit::KRichTextEdit(const QString &text, QWidget *parent)
: KRichTextEdit(*new KRichTextEditPrivate(this), text, parent)
{
}
KRichTextEdit::KRichTextEdit(KRichTextEditPrivate &dd, const QString &text, QWidget *parent)
: KTextEdit(dd, text, parent)
{
Q_D(KRichTextEdit);
d->init();
}
KRichTextEdit::KRichTextEdit(QWidget *parent)
: KRichTextEdit(*new KRichTextEditPrivate(this), parent)
{
}
KRichTextEdit::KRichTextEdit(KRichTextEditPrivate &dd, QWidget *parent)
: KTextEdit(dd, parent)
{
Q_D(KRichTextEdit);
d->init();
}
KRichTextEdit::~KRichTextEdit() = default;
//@cond PRIVATE
void KRichTextEditPrivate::init()
{
Q_Q(KRichTextEdit);
q->setAcceptRichText(false);
KCursor::setAutoHideCursor(q, true, true);
}
//@endcond
void KRichTextEdit::setListStyle(int _styleIndex)
{
Q_D(KRichTextEdit);
d->nestedListHelper->handleOnBulletType(-_styleIndex);
setFocus();
d->activateRichText();
}
void KRichTextEdit::indentListMore()
{
Q_D(KRichTextEdit);
d->nestedListHelper->changeIndent(+1);
d->activateRichText();
}
void KRichTextEdit::indentListLess()
{
Q_D(KRichTextEdit);
d->nestedListHelper->changeIndent(-1);
}
void KRichTextEdit::insertHorizontalRule()
{
Q_D(KRichTextEdit);
QTextCursor cursor = textCursor();
QTextBlockFormat bf = cursor.blockFormat();
QTextCharFormat cf = cursor.charFormat();
cursor.beginEditBlock();
cursor.insertHtml(QStringLiteral("<hr>"));
cursor.insertBlock(bf, cf);
cursor.endEditBlock();
setTextCursor(cursor);
d->activateRichText();
}
void KRichTextEdit::alignLeft()
{
Q_D(KRichTextEdit);
setAlignment(Qt::AlignLeft);
setFocus();
d->activateRichText();
}
void KRichTextEdit::alignCenter()
{
Q_D(KRichTextEdit);
setAlignment(Qt::AlignHCenter);
setFocus();
d->activateRichText();
}
void KRichTextEdit::alignRight()
{
Q_D(KRichTextEdit);
setAlignment(Qt::AlignRight);
setFocus();
d->activateRichText();
}
void KRichTextEdit::alignJustify()
{
Q_D(KRichTextEdit);
setAlignment(Qt::AlignJustify);
setFocus();
d->activateRichText();
}
void KRichTextEdit::makeRightToLeft()
{
Q_D(KRichTextEdit);
QTextBlockFormat format;
format.setLayoutDirection(Qt::RightToLeft);
QTextCursor cursor = textCursor();
cursor.mergeBlockFormat(format);
setTextCursor(cursor);
setFocus();
d->activateRichText();
}
void KRichTextEdit::makeLeftToRight()
{
Q_D(KRichTextEdit);
QTextBlockFormat format;
format.setLayoutDirection(Qt::LeftToRight);
QTextCursor cursor = textCursor();
cursor.mergeBlockFormat(format);
setTextCursor(cursor);
setFocus();
d->activateRichText();
}
void KRichTextEdit::setTextBold(bool bold)
{
Q_D(KRichTextEdit);
QTextCharFormat fmt;
fmt.setFontWeight(bold ? QFont::Bold : QFont::Normal);
d->mergeFormatOnWordOrSelection(fmt);
setFocus();
d->activateRichText();
}
void KRichTextEdit::setTextItalic(bool italic)
{
Q_D(KRichTextEdit);
QTextCharFormat fmt;
fmt.setFontItalic(italic);
d->mergeFormatOnWordOrSelection(fmt);
setFocus();
d->activateRichText();
}
void KRichTextEdit::setTextUnderline(bool underline)
{
Q_D(KRichTextEdit);
QTextCharFormat fmt;
fmt.setFontUnderline(underline);
d->mergeFormatOnWordOrSelection(fmt);
setFocus();
d->activateRichText();
}
void KRichTextEdit::setTextStrikeOut(bool strikeOut)
{
Q_D(KRichTextEdit);
QTextCharFormat fmt;
fmt.setFontStrikeOut(strikeOut);
d->mergeFormatOnWordOrSelection(fmt);
setFocus();
d->activateRichText();
}
void KRichTextEdit::setTextForegroundColor(const QColor &color)
{
Q_D(KRichTextEdit);
QTextCharFormat fmt;
fmt.setForeground(color);
d->mergeFormatOnWordOrSelection(fmt);
setFocus();
d->activateRichText();
}
void KRichTextEdit::setTextBackgroundColor(const QColor &color)
{
Q_D(KRichTextEdit);
QTextCharFormat fmt;
fmt.setBackground(color);
d->mergeFormatOnWordOrSelection(fmt);
setFocus();
d->activateRichText();
}
void KRichTextEdit::setFontFamily(const QString &fontFamily)
{
Q_D(KRichTextEdit);
QTextCharFormat fmt;
fmt.setFontFamilies({fontFamily});
d->mergeFormatOnWordOrSelection(fmt);
setFocus();
d->activateRichText();
}
void KRichTextEdit::setFontSize(int size)
{
Q_D(KRichTextEdit);
QTextCharFormat fmt;
fmt.setFontPointSize(size);
d->mergeFormatOnWordOrSelection(fmt);
setFocus();
d->activateRichText();
}
void KRichTextEdit::setFont(const QFont &font)
{
Q_D(KRichTextEdit);
QTextCharFormat fmt;
fmt.setFont(font);
d->mergeFormatOnWordOrSelection(fmt);
setFocus();
d->activateRichText();
}
void KRichTextEdit::switchToPlainText()
{
Q_D(KRichTextEdit);
if (d->mMode == Rich) {
d->mMode = Plain;
// TODO: Warn the user about this?
auto insertPlainFunc = [this]() {
insertPlainTextImplementation();
};
QMetaObject::invokeMethod(this, insertPlainFunc);
setAcceptRichText(false);
Q_EMIT textModeChanged(d->mMode);
}
}
void KRichTextEdit::insertPlainTextImplementation()
{
document()->setPlainText(document()->toPlainText());
}
void KRichTextEdit::setTextSuperScript(bool superscript)
{
Q_D(KRichTextEdit);
QTextCharFormat fmt;
fmt.setVerticalAlignment(superscript ? QTextCharFormat::AlignSuperScript : QTextCharFormat::AlignNormal);
d->mergeFormatOnWordOrSelection(fmt);
setFocus();
d->activateRichText();
}
void KRichTextEdit::setTextSubScript(bool subscript)
{
Q_D(KRichTextEdit);
QTextCharFormat fmt;
fmt.setVerticalAlignment(subscript ? QTextCharFormat::AlignSubScript : QTextCharFormat::AlignNormal);
d->mergeFormatOnWordOrSelection(fmt);
setFocus();
d->activateRichText();
}
void KRichTextEdit::setHeadingLevel(int level)
{
Q_D(KRichTextEdit);
const int boundedLevel = qBound(0, 6, level);
// Apparently, 5 is maximum for FontSizeAdjustment; otherwise level=1 and
// level=2 look the same
const int sizeAdjustment = boundedLevel > 0 ? 5 - boundedLevel : 0;
QTextCursor cursor = textCursor();
cursor.beginEditBlock();
QTextBlockFormat blkfmt;
blkfmt.setHeadingLevel(boundedLevel);
cursor.mergeBlockFormat(blkfmt);
QTextCharFormat chrfmt;
chrfmt.setFontWeight(boundedLevel > 0 ? QFont::Bold : QFont::Normal);
chrfmt.setProperty(QTextFormat::FontSizeAdjustment, sizeAdjustment);
// Applying style to the current line or selection
QTextCursor selectCursor = cursor;
if (selectCursor.hasSelection()) {
QTextCursor top = selectCursor;
top.setPosition(qMin(top.anchor(), top.position()));
top.movePosition(QTextCursor::StartOfBlock);
QTextCursor bottom = selectCursor;
bottom.setPosition(qMax(bottom.anchor(), bottom.position()));
bottom.movePosition(QTextCursor::EndOfBlock);
selectCursor.setPosition(top.position(), QTextCursor::MoveAnchor);
selectCursor.setPosition(bottom.position(), QTextCursor::KeepAnchor);
} else {
selectCursor.select(QTextCursor::BlockUnderCursor);
}
selectCursor.mergeCharFormat(chrfmt);
cursor.mergeBlockCharFormat(chrfmt);
cursor.endEditBlock();
setTextCursor(cursor);
setFocus();
d->activateRichText();
}
void KRichTextEdit::enableRichTextMode()
{
Q_D(KRichTextEdit);
d->activateRichText();
}
KRichTextEdit::Mode KRichTextEdit::textMode() const
{
Q_D(const KRichTextEdit);
return d->mMode;
}
QString KRichTextEdit::textOrHtml() const
{
if (textMode() == Rich) {
return toCleanHtml();
} else {
return toPlainText();
}
}
void KRichTextEdit::setTextOrHtml(const QString &text)
{
Q_D(KRichTextEdit);
// might be rich text
if (Qt::mightBeRichText(text)) {
if (d->mMode == KRichTextEdit::Plain) {
d->activateRichText();
}
setHtml(text);
} else {
setPlainText(text);
}
}
// KF6 TODO: remove constness
QString KRichTextEdit::currentLinkText() const
{
QTextCursor cursor = textCursor();
selectLinkText(&cursor);
return cursor.selectedText();
}
// KF6 TODO: remove constness
void KRichTextEdit::selectLinkText() const
{
Q_D(const KRichTextEdit);
QTextCursor cursor = textCursor();
selectLinkText(&cursor);
// KF6 TODO: remove const_cast
const_cast<KRichTextEditPrivate *>(d)->setTextCursor(cursor);
}
void KRichTextEdit::selectLinkText(QTextCursor *cursor) const
{
// If the cursor is on a link, select the text of the link.
if (cursor->charFormat().isAnchor()) {
QString aHref = cursor->charFormat().anchorHref();
// Move cursor to start of link
while (cursor->charFormat().anchorHref() == aHref) {
if (cursor->atStart()) {
break;
}
cursor->setPosition(cursor->position() - 1);
}
if (cursor->charFormat().anchorHref() != aHref) {
cursor->setPosition(cursor->position() + 1, QTextCursor::KeepAnchor);
}
// Move selection to the end of the link
while (cursor->charFormat().anchorHref() == aHref) {
if (cursor->atEnd()) {
break;
}
cursor->setPosition(cursor->position() + 1, QTextCursor::KeepAnchor);
}
if (cursor->charFormat().anchorHref() != aHref) {
cursor->setPosition(cursor->position() - 1, QTextCursor::KeepAnchor);
}
} else if (cursor->hasSelection()) {
// Nothing to to. Using the currently selected text as the link text.
} else {
// Select current word
cursor->movePosition(QTextCursor::StartOfWord);
cursor->movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
}
}
QString KRichTextEdit::currentLinkUrl() const
{
return textCursor().charFormat().anchorHref();
}
void KRichTextEdit::updateLink(const QString &linkUrl, const QString &linkText)
{
Q_D(KRichTextEdit);
selectLinkText();
QTextCursor cursor = textCursor();
cursor.beginEditBlock();
if (!cursor.hasSelection()) {
cursor.select(QTextCursor::WordUnderCursor);
}
QTextCharFormat format = cursor.charFormat();
// Save original format to create an extra space with the existing char
// format for the block
const QTextCharFormat originalFormat = format;
if (!linkUrl.isEmpty()) {
// Add link details
format.setAnchor(true);
format.setAnchorHref(linkUrl);
// Workaround for QTBUG-1814:
// Link formatting does not get applied immediately when setAnchor(true)
// is called. So the formatting needs to be applied manually.
format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
format.setUnderlineColor(KColorScheme(QPalette::Active, KColorScheme::View).foreground(KColorScheme::LinkText).color());
format.setForeground(KColorScheme(QPalette::Active, KColorScheme::View).foreground(KColorScheme::LinkText).color());
d->activateRichText();
} else {
// Remove link details
format.setAnchor(false);
format.setAnchorHref(QString());
// Workaround for QTBUG-1814:
// Link formatting does not get removed immediately when setAnchor(false)
// is called. So the formatting needs to be applied manually.
QTextDocument defaultTextDocument;
QTextCharFormat defaultCharFormat = defaultTextDocument.begin().charFormat();
format.setUnderlineStyle(defaultCharFormat.underlineStyle());
format.setUnderlineColor(defaultCharFormat.underlineColor());
format.setForeground(defaultCharFormat.foreground());
}
// Insert link text specified in dialog, otherwise write out url.
QString _linkText;
if (!linkText.isEmpty()) {
_linkText = linkText;
} else {
_linkText = linkUrl;
}
cursor.insertText(_linkText, format);
// Insert a space after the link if at the end of the block so that
// typing some text after the link does not carry link formatting
if (!linkUrl.isEmpty() && cursor.atBlockEnd()) {
cursor.setPosition(cursor.selectionEnd());
cursor.setCharFormat(originalFormat);
cursor.insertText(QStringLiteral(" "));
}
cursor.endEditBlock();
}
void KRichTextEdit::keyPressEvent(QKeyEvent *event)
{
Q_D(KRichTextEdit);
bool handled = false;
if (textCursor().currentList()) {
handled = d->nestedListHelper->handleKeyPressEvent(event);
}
// If a line was merged with previous (next) one, with different heading level,
// the style should also be adjusted accordingly (i.e. merged)
if ((event->key() == Qt::Key_Backspace && textCursor().atBlockStart()
&& (textCursor().blockFormat().headingLevel() != textCursor().block().previous().blockFormat().headingLevel()))
|| (event->key() == Qt::Key_Delete && textCursor().atBlockEnd()
&& (textCursor().blockFormat().headingLevel() != textCursor().block().next().blockFormat().headingLevel()))) {
QTextCursor cursor = textCursor();
cursor.beginEditBlock();
if (event->key() == Qt::Key_Delete) {
cursor.deleteChar();
} else {
cursor.deletePreviousChar();
}
setHeadingLevel(cursor.blockFormat().headingLevel());
cursor.endEditBlock();
handled = true;
}
const auto prevHeadingLevel = textCursor().blockFormat().headingLevel();
if (!handled) {
KTextEdit::keyPressEvent(event);
}
// Match the behavior of office suites: newline after header switches to normal text
if (event->key() == Qt::Key_Return //
&& prevHeadingLevel > 0) {
// it should be undoable together with actual "return" keypress
textCursor().joinPreviousEditBlock();
if (textCursor().atBlockEnd()) {
setHeadingLevel(0);
} else {
setHeadingLevel(prevHeadingLevel);
}
textCursor().endEditBlock();
}
Q_EMIT cursorPositionChanged();
}
// void KRichTextEdit::dropEvent(QDropEvent *event)
// {
// int dropSize = event->mimeData()->text().size();
//
// dropEvent( event );
// QTextCursor cursor = textCursor();
// int cursorPosition = cursor.position();
// cursor.setPosition( cursorPosition - dropSize );
// cursor.setPosition( cursorPosition, QTextCursor::KeepAnchor );
// setTextCursor( cursor );
// d->nestedListHelper->handleAfterDropEvent( event );
// }
bool KRichTextEdit::canIndentList() const
{
Q_D(const KRichTextEdit);
return d->nestedListHelper->canIndent();
}
bool KRichTextEdit::canDedentList() const
{
Q_D(const KRichTextEdit);
return d->nestedListHelper->canDedent();
}
QString KRichTextEdit::toCleanHtml() const
{
QString result = toHtml();
static const QString EMPTYLINEHTML = QLatin1String(
"<p style=\"-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; "
"margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; \">&nbsp;</p>");
// Qt inserts various style properties based on the current mode of the editor (underline,
// bold, etc), but only empty paragraphs *also* have qt-paragraph-type set to 'empty'.
static const QRegularExpression EMPTYLINEREGEX(QStringLiteral("<p style=\"-qt-paragraph-type:empty;(.*?)</p>"));
static const QString OLLISTPATTERNQT = QStringLiteral("<ol style=\"margin-top: 0px; margin-bottom: 0px; margin-left: 0px;");
static const QString ULLISTPATTERNQT = QStringLiteral("<ul style=\"margin-top: 0px; margin-bottom: 0px; margin-left: 0px;");
static const QString ORDEREDLISTHTML = QStringLiteral("<ol style=\"margin-top: 0px; margin-bottom: 0px;");
static const QString UNORDEREDLISTHTML = QStringLiteral("<ul style=\"margin-top: 0px; margin-bottom: 0px;");
// fix 1 - empty lines should show as empty lines - MS Outlook treats margin-top:0px; as
// a non-existing line.
// Although we can simply remove the margin-top style property, we still get unwanted results
// if you have three or more empty lines. It's best to replace empty <p> elements with <p>&nbsp;</p>.
// replace all occurrences with the new line text
result.replace(EMPTYLINEREGEX, EMPTYLINEHTML);
// fix 2a - ordered lists - MS Outlook treats margin-left:0px; as
// a non-existing number; e.g: "1. First item" turns into "First Item"
result.replace(OLLISTPATTERNQT, ORDEREDLISTHTML);
// fix 2b - unordered lists - MS Outlook treats margin-left:0px; as
// a non-existing bullet; e.g: "* First bullet" turns into "First Bullet"
result.replace(ULLISTPATTERNQT, UNORDEREDLISTHTML);
return result;
}
#include "moc_krichtextedit.cpp"
@@ -0,0 +1,374 @@
/*
krichtextedit.h
SPDX-FileCopyrightText: 2007 Laurent Montel <montel@kde.org>
SPDX-FileCopyrightText: 2008 Thomas McGuire <thomas.mcguire@gmx.net>
SPDX-FileCopyrightText: 2008 Stephen Kelly <steveire@gmail.com>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef KRICHTEXTEDIT_H
#define KRICHTEXTEDIT_H
#include <ktextedit.h>
class QKeyEvent;
class KRichTextEditPrivate;
#include <ktextwidgets_export.h>
/**
* @class KRichTextEdit krichtextedit.h <KRichTextEdit>
*
* The KRichTextEdit class provides a widget to edit and display rich text.
*
* It offers several additional rich text editing functions to KTextEdit and makes
* them easier to access including:
*
* @li Changing fonts, sizes.
* @li Font formatting, such as bold, underline, italic, foreground and
* background color.
* @li Paragraph alignment
* @li Ability to edit and remove hyperlinks
* @li Nested list handling
* @li Simple actions to insert tables. TODO
*
* The KRichTextEdit can be in two modes: Rich text mode and plain text mode.
* Calling functions which modify the format/style of the text will automatically
* enable the rich text mode. Rich text mode is sometimes also referred to as
* HTML mode.
*
* Do not call setAcceptRichText() or acceptRichText() yourself. Instead simply
* connect to the slots which insert the rich text, use switchToPlainText() or
* enableRichTextMode().
*
* \image html krichtextedit.png "KRichTextEdit Widget"
*
* @since 4.1
*/
class KTEXTWIDGETS_EXPORT KRichTextEdit : public KTextEdit
{
Q_OBJECT
public:
/**
* The mode the edit widget is in.
*/
enum Mode {
Plain, ///< Plain text mode
Rich, ///< Rich text mode
};
/**
* Constructs a KRichTextEdit object
*
* @param text The initial text of the text edit, which is interpreted as
* HTML.
* @param parent The parent widget
*/
explicit KRichTextEdit(const QString &text, QWidget *parent = nullptr);
/**
* Constructs a KRichTextEdit object.
*
* @param parent The parent widget
*/
explicit KRichTextEdit(QWidget *parent = nullptr);
/**
* Destructor.
*/
~KRichTextEdit() override;
/**
* This enables rich text mode. Nothing is done except changing the internal
* mode and allowing rich text pastes.
*/
void enableRichTextMode();
/**
* @return The current text mode
*/
Mode textMode() const;
/**
* @return The plain text string if in plain text mode or the HTML code
* if in rich text mode. The text is not word-wrapped.
*/
QString textOrHtml() const;
/**
* Replaces all the content of the text edit with the given string.
* If the string is in rich text format, the text is inserted as rich text,
* otherwise it is inserted as plain text.
*
* @param text The text to insert
*/
void setTextOrHtml(const QString &text);
/**
* Returns the text of the link at the current position or an empty string
* if the cursor is not on a link.
*
* @sa currentLinkUrl
* @return The link text
*/
QString currentLinkText() const;
/**
* Returns the URL target (href) of the link at the current position or an
* empty string if the cursor is not on a link.
*
* @sa currentLinkText
* @return The link target URL
*/
QString currentLinkUrl() const;
/**
* If the cursor is on a link, sets the @a cursor to a selection of the
* text of the link. If the @a cursor is not on a link, selects the current word
* or existing selection.
*
* @param cursor The cursor to use to select the text.
* @sa updateLink
*/
void selectLinkText(QTextCursor *cursor) const;
/**
* Convenience function to select the link text using the active cursor.
*
* @sa selectLinkText
*/
void selectLinkText() const;
/**
* Replaces the current selection with a hyperlink with the link URL @a linkUrl
* and the link text @a linkText.
*
* @sa selectLinkText
* @sa currentLinkUrl
* @sa currentLinkText
* @param linkUrl The link will get this URL as href (target)
* @param linkText The link will get this alternative text, which is the
* text displayed in the text edit.
*/
void updateLink(const QString &linkUrl, const QString &linkText);
/**
* Returns true if the list item at the current position can be indented.
*
* @sa canDedentList
*/
bool canIndentList() const;
/**
* Returns true if the list item at the current position can be dedented.
*
* @sa canIndentList
*/
bool canDedentList() const;
public Q_SLOTS:
/**
* Sets the alignment of the current block to Left Aligned
*/
void alignLeft();
/**
* Sets the alignment of the current block to Centered
*/
void alignCenter();
/**
* Sets the alignment of the current block to Right Aligned
*/
void alignRight();
/**
* Sets the alignment of the current block to Justified
*/
void alignJustify();
/**
* Sets the direction of the current block to Right-To-Left
*
* @since 4.6
*/
void makeRightToLeft();
/**
* Sets the direction of the current block to Left-To-Right
*
* @since 4.6
*/
void makeLeftToRight();
/**
* Sets the list style of the current list, or creates a new list using the
* current block. The @a _styleindex corresponds to the QTextListFormat::Style
*
* @param _styleIndex The list will get this style
*/
void setListStyle(int _styleIndex);
/**
* Increases the nesting level of the current block or selected blocks.
*
* @sa canIndentList
*/
void indentListMore();
/**
* Decreases the nesting level of the current block or selected blocks.
*
* @sa canDedentList
*/
void indentListLess();
/**
* Sets the current word or selection to the font family @a fontFamily
*
* @param fontFamily The text's font family will be changed to this one
*/
void setFontFamily(const QString &fontFamily);
/**
* Sets the current word or selection to the font size @a size
*
* @param size The text's font will get this size
*/
void setFontSize(int size);
/**
* Sets the current word or selection to the font @a font
*
* @param font the font of the text will be set to this font
*/
void setFont(const QFont &font);
/**
* Toggles the bold formatting of the current word or selection at the current
* cursor position.
*
* @param bold If true, the text will be set to bold
*/
void setTextBold(bool bold);
/**
* Toggles the italic formatting of the current word or selection at the current
* cursor position.
*
* @param italic If true, the text will be set to italic
*/
void setTextItalic(bool italic);
/**
* Toggles the underline formatting of the current word or selection at the current
* cursor position.
*
* @param underline If true, the text will be underlined
*/
void setTextUnderline(bool underline);
/**
* Toggles the strikeout formatting of the current word or selection at the current
* cursor position.
*
* @param strikeOut If true, the text will be struck out
*/
void setTextStrikeOut(bool strikeOut);
/**
* Sets the foreground color of the current word or selection to @a color.
*
* @param color The text will get this background color
*/
void setTextForegroundColor(const QColor &color);
/**
* Sets the background color of the current word or selection to @a color.
*
* @param color The text will get this foreground color
*/
void setTextBackgroundColor(const QColor &color);
/**
* Inserts a horizontal rule below the current block.
*/
void insertHorizontalRule();
/**
* This will switch the editor to plain text mode.
* All rich text formatting will be destroyed.
*/
void switchToPlainText();
/**
* This will clean some of the bad html produced by the underlying QTextEdit
* It walks over all lines and cleans up a bit. Should be improved to produce
* our own Html.
*/
QString toCleanHtml() const;
/**
* Toggles the superscript formatting of the current word or selection at the current
* cursor position.
*
* @param superscript If true, the text will be set to superscript
*/
void setTextSuperScript(bool superscript);
/**
* Toggles the subscript formatting of the current word or selection at the current
* cursor position.
*
* @param subscript If true, the text will be set to subscript
*/
void setTextSubScript(bool subscript);
/**
* Sets the heading level of a current block or selection
*
* @param level Heading level (value should be between 0 and 6)
* (0 is "normal text", 1 is the largest heading, 6 is the smallest one)
*
* @since 5.70
*/
void setHeadingLevel(int level);
/**
* @since 4.10
* Because of binary compatibility constraints, insertPlainText
* is not virtual. Therefore it must dynamically detect and call this slot.
*/
void insertPlainTextImplementation();
Q_SIGNALS:
/**
* Emitted whenever the text mode is changed.
*
* @param mode The new text mode
*/
void textModeChanged(KRichTextEdit::Mode mode);
protected:
/**
* Reimplemented.
* Catches key press events. Used to handle some key presses on lists.
*/
void keyPressEvent(QKeyEvent *event) override;
protected:
KTEXTWIDGETS_NO_EXPORT KRichTextEdit(KRichTextEditPrivate &dd, const QString &text, QWidget *parent);
KTEXTWIDGETS_NO_EXPORT KRichTextEdit(KRichTextEditPrivate &dd, QWidget *parent);
private:
//@cond PRIVATE
Q_DECLARE_PRIVATE(KRichTextEdit)
//@endcond
};
#endif
@@ -0,0 +1,58 @@
/*
krichtextedit
SPDX-FileCopyrightText: 2007 Laurent Montel <montel@kde.org>
SPDX-FileCopyrightText: 2008 Thomas McGuire <thomas.mcguire@gmx.net>
SPDX-FileCopyrightText: 2008 Stephen Kelly <steveire@gmail.com>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef KRICHTEXTEDIT_P_H
#define KRICHTEXTEDIT_P_H
#include "ktextedit_p.h"
#include "nestedlisthelper_p.h"
class KRichTextEditPrivate : public KTextEditPrivate
{
Q_DECLARE_PUBLIC(KRichTextEdit)
public:
explicit KRichTextEditPrivate(KRichTextEdit *qq)
: KTextEditPrivate(qq)
, nestedListHelper(new NestedListHelper(qq))
{
}
~KRichTextEditPrivate() override
{
delete nestedListHelper;
}
//
// Normal functions
//
// If the text under the cursor is a link, the cursor's selection is set to
// the complete link text. Otherwise selects the current word if there is no
// selection.
void selectLinkText() const;
void init();
// Switches to rich text mode and emits the mode changed signal if the
// mode really changed.
void activateRichText();
// Applies formatting to the current word if there is no selection.
void mergeFormatOnWordOrSelection(const QTextCharFormat &format);
void setTextCursor(QTextCursor &cursor);
// Data members
KRichTextEdit::Mode mMode = KRichTextEdit::Plain;
NestedListHelper *nestedListHelper;
};
#endif
@@ -0,0 +1,722 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2008 Stephen Kelly <steveire@gmail.com>
SPDX-FileCopyrightText: 2008 Thomas McGuire <thomas.mcguire@gmx.net>
SPDX-License-Identifier: LGPL-2.0-only
*/
#include "krichtextwidget.h"
#include "krichtextedit_p.h"
// KDE includes
#include <KColorScheme>
#include <KFontAction>
#include <KFontSizeAction>
#include <KLocalizedString>
#include <ktoggleaction.h>
// Qt includes
#include <QAction>
#include <QActionGroup>
#include <QColorDialog>
#include <QTextList>
#include "klinkdialog_p.h"
// TODO: Add i18n context
/**
Private class that helps to provide binary compatibility between releases.
@internal
*/
//@cond PRIVATE
class KRichTextWidgetPrivate : public KRichTextEditPrivate
{
Q_DECLARE_PUBLIC(KRichTextWidget)
public:
KRichTextWidgetPrivate(KRichTextWidget *qq)
: KRichTextEditPrivate(qq)
{
}
QList<QAction *> richTextActionList;
QTextCharFormat painterFormat;
KRichTextWidget::RichTextSupport richTextSupport;
bool painterActive = false;
bool richTextEnabled = false;
KToggleAction *enableRichText = nullptr;
QAction *action_text_foreground_color = nullptr;
QAction *action_text_background_color = nullptr;
KToggleAction *action_text_bold = nullptr;
KToggleAction *action_text_italic = nullptr;
KToggleAction *action_text_underline = nullptr;
KToggleAction *action_text_strikeout = nullptr;
KFontAction *action_font_family = nullptr;
KFontSizeAction *action_font_size = nullptr;
KSelectAction *action_list_style = nullptr;
QAction *action_list_indent = nullptr;
QAction *action_list_dedent = nullptr;
QAction *action_manage_link = nullptr;
QAction *action_insert_horizontal_rule = nullptr;
QAction *action_format_painter = nullptr;
QAction *action_to_plain_text = nullptr;
KToggleAction *action_align_left = nullptr;
KToggleAction *action_align_right = nullptr;
KToggleAction *action_align_center = nullptr;
KToggleAction *action_align_justify = nullptr;
KToggleAction *action_direction_ltr = nullptr;
KToggleAction *action_direction_rtl = nullptr;
KToggleAction *action_text_superscript = nullptr;
KToggleAction *action_text_subscript = nullptr;
KSelectAction *action_heading_level = nullptr;
//
// Normal functions
//
void init();
//
// Slots
//
/**
* @brief Opens a dialog to allow the user to select a foreground color.
*/
void _k_setTextForegroundColor();
/**
* @brief Opens a dialog to allow the user to select a background color.
*/
void _k_setTextBackgroundColor();
/**
* Opens a dialog which lets the user turn the currently selected text into
* a link.
* If no text is selected, the word under the cursor will be taken.
* If the cursor is already over a link, the user can edit that link.
*
*/
void _k_manageLink();
/**
* Activates a format painter to allow the user to copy font/text formatting
* to different parts of the document.
*
*/
void _k_formatPainter(bool active);
/**
* @brief Update actions relating to text format (bold, size etc.).
*/
void updateCharFormatActions(const QTextCharFormat &format);
/**
* Update actions not covered by text formatting, such as alignment,
* list style and level.
*/
void updateMiscActions();
/**
* Change the style of the current list or create a new list with the style given by @a index.
*/
void _k_setListStyle(int index);
/**
* Change the heading level of a current line to a level given by @a level
*/
void _k_setHeadingLevel(int level);
};
//@endcond
void KRichTextWidgetPrivate::init()
{
Q_Q(KRichTextWidget);
q->setRichTextSupport(KRichTextWidget::FullSupport);
}
KRichTextWidget::KRichTextWidget(QWidget *parent)
: KRichTextEdit(*new KRichTextWidgetPrivate(this), parent)
{
Q_D(KRichTextWidget);
d->init();
}
KRichTextWidget::KRichTextWidget(const QString &text, QWidget *parent)
: KRichTextEdit(*new KRichTextWidgetPrivate(this), text, parent)
{
Q_D(KRichTextWidget);
d->init();
}
KRichTextWidget::~KRichTextWidget() = default;
KRichTextWidget::RichTextSupport KRichTextWidget::richTextSupport() const
{
Q_D(const KRichTextWidget);
return d->richTextSupport;
}
void KRichTextWidget::setRichTextSupport(const KRichTextWidget::RichTextSupport &support)
{
Q_D(KRichTextWidget);
d->richTextSupport = support;
}
QList<QAction *> KRichTextWidget::createActions()
{
Q_D(KRichTextWidget);
// Note to maintainers: If adding new functionality here, make sure to disconnect
// and delete actions which should not be supported.
//
// New Actions need to be added to the following places:
// - possibly the RichTextSupportValues enum
// - the API documentation for createActions()
// - this function
// - the action needs to be added to the private class as a member
// - the constructor of the private class
// - depending on the action, some slot that changes the toggle state when
// appropriate, such as updateCharFormatActions or updateMiscActions.
// The list of actions currently supported is also stored internally.
// This is used to disable all actions at once in setActionsEnabled.
d->richTextActionList.clear();
if (d->richTextSupport & SupportTextForegroundColor) {
// Foreground Color
d->action_text_foreground_color = new QAction(QIcon::fromTheme(QStringLiteral("format-stroke-color")), i18nc("@action", "Text &Color…"), this);
d->action_text_foreground_color->setIconText(i18nc("@label stroke color", "Color"));
d->richTextActionList.append((d->action_text_foreground_color));
d->action_text_foreground_color->setObjectName(QStringLiteral("format_text_foreground_color"));
connect(d->action_text_foreground_color, &QAction::triggered, this, [this]() {
Q_D(KRichTextWidget);
d->_k_setTextForegroundColor();
});
} else {
d->action_text_foreground_color = nullptr;
}
if (d->richTextSupport & SupportTextBackgroundColor) {
// Background Color
d->action_text_background_color = new QAction(QIcon::fromTheme(QStringLiteral("format-fill-color")), i18nc("@action", "Text &Highlight…"), this);
d->richTextActionList.append((d->action_text_background_color));
d->action_text_background_color->setObjectName(QStringLiteral("format_text_background_color"));
connect(d->action_text_background_color, &QAction::triggered, this, [this]() {
Q_D(KRichTextWidget);
d->_k_setTextBackgroundColor();
});
} else {
d->action_text_background_color = nullptr;
}
if (d->richTextSupport & SupportFontFamily) {
// Font Family
d->action_font_family = new KFontAction(i18nc("@action", "&Font"), this);
d->richTextActionList.append((d->action_font_family));
d->action_font_family->setObjectName(QStringLiteral("format_font_family"));
connect(d->action_font_family, &KSelectAction::textTriggered, this, &KRichTextWidget::setFontFamily);
} else {
d->action_font_family = nullptr;
}
if (d->richTextSupport & SupportFontSize) {
// Font Size
d->action_font_size = new KFontSizeAction(i18nc("@action", "Font &Size"), this);
d->richTextActionList.append((d->action_font_size));
d->action_font_size->setObjectName(QStringLiteral("format_font_size"));
connect(d->action_font_size, &KFontSizeAction::fontSizeChanged, this, &KRichTextEdit::setFontSize);
} else {
d->action_font_size = nullptr;
}
if (d->richTextSupport & SupportBold) {
d->action_text_bold = new KToggleAction(QIcon::fromTheme(QStringLiteral("format-text-bold")), i18nc("@action boldify selected text", "&Bold"), this);
QFont bold;
bold.setBold(true);
d->action_text_bold->setFont(bold);
d->richTextActionList.append((d->action_text_bold));
d->action_text_bold->setObjectName(QStringLiteral("format_text_bold"));
d->action_text_bold->setShortcut(Qt::CTRL | Qt::Key_B);
connect(d->action_text_bold, &QAction::triggered, this, &KRichTextEdit::setTextBold);
} else {
d->action_text_bold = nullptr;
}
if (d->richTextSupport & SupportItalic) {
d->action_text_italic =
new KToggleAction(QIcon::fromTheme(QStringLiteral("format-text-italic")), i18nc("@action italicize selected text", "&Italic"), this);
QFont italic;
italic.setItalic(true);
d->action_text_italic->setFont(italic);
d->richTextActionList.append((d->action_text_italic));
d->action_text_italic->setObjectName(QStringLiteral("format_text_italic"));
d->action_text_italic->setShortcut(Qt::CTRL | Qt::Key_I);
connect(d->action_text_italic, &QAction::triggered, this, &KRichTextEdit::setTextItalic);
} else {
d->action_text_italic = nullptr;
}
if (d->richTextSupport & SupportUnderline) {
d->action_text_underline =
new KToggleAction(QIcon::fromTheme(QStringLiteral("format-text-underline")), i18nc("@action underline selected text", "&Underline"), this);
QFont underline;
underline.setUnderline(true);
d->action_text_underline->setFont(underline);
d->richTextActionList.append((d->action_text_underline));
d->action_text_underline->setObjectName(QStringLiteral("format_text_underline"));
d->action_text_underline->setShortcut(Qt::CTRL | Qt::Key_U);
connect(d->action_text_underline, &QAction::triggered, this, &KRichTextEdit::setTextUnderline);
} else {
d->action_text_underline = nullptr;
}
if (d->richTextSupport & SupportStrikeOut) {
d->action_text_strikeout = new KToggleAction(QIcon::fromTheme(QStringLiteral("format-text-strikethrough")), i18nc("@action", "&Strike Out"), this);
QFont strikeout;
strikeout.setStrikeOut(true);
d->action_text_strikeout->setFont(strikeout);
d->richTextActionList.append((d->action_text_strikeout));
d->action_text_strikeout->setObjectName(QStringLiteral("format_text_strikeout"));
d->action_text_strikeout->setShortcut(Qt::CTRL | Qt::Key_L);
connect(d->action_text_strikeout, &QAction::triggered, this, &KRichTextEdit::setTextStrikeOut);
} else {
d->action_text_strikeout = nullptr;
}
if (d->richTextSupport & SupportAlignment) {
// Alignment
d->action_align_left = new KToggleAction(QIcon::fromTheme(QStringLiteral("format-justify-left")), i18nc("@action", "Align &Left"), this);
d->action_align_left->setIconText(i18nc("@label left justify", "Left"));
d->richTextActionList.append((d->action_align_left));
d->action_align_left->setObjectName(QStringLiteral("format_align_left"));
connect(d->action_align_left, &QAction::triggered, this, &KRichTextEdit::alignLeft);
d->action_align_center = new KToggleAction(QIcon::fromTheme(QStringLiteral("format-justify-center")), i18nc("@action", "Align &Center"), this);
d->action_align_center->setIconText(i18nc("@label center justify", "Center"));
d->richTextActionList.append((d->action_align_center));
d->action_align_center->setObjectName(QStringLiteral("format_align_center"));
connect(d->action_align_center, &QAction::triggered, this, &KRichTextEdit::alignCenter);
d->action_align_right = new KToggleAction(QIcon::fromTheme(QStringLiteral("format-justify-right")), i18nc("@action", "Align &Right"), this);
d->action_align_right->setIconText(i18nc("@label right justify", "Right"));
d->richTextActionList.append((d->action_align_right));
d->action_align_right->setObjectName(QStringLiteral("format_align_right"));
connect(d->action_align_right, &QAction::triggered, this, &KRichTextEdit::alignRight);
d->action_align_justify = new KToggleAction(QIcon::fromTheme(QStringLiteral("format-justify-fill")), i18nc("@action", "&Justify"), this);
d->action_align_justify->setIconText(i18nc("@label justify fill", "Justify"));
d->richTextActionList.append((d->action_align_justify));
d->action_align_justify->setObjectName(QStringLiteral("format_align_justify"));
connect(d->action_align_justify, &QAction::triggered, this, &KRichTextEdit::alignJustify);
QActionGroup *alignmentGroup = new QActionGroup(this);
alignmentGroup->addAction(d->action_align_left);
alignmentGroup->addAction(d->action_align_center);
alignmentGroup->addAction(d->action_align_right);
alignmentGroup->addAction(d->action_align_justify);
} else {
d->action_align_left = nullptr;
d->action_align_center = nullptr;
d->action_align_right = nullptr;
d->action_align_justify = nullptr;
}
if (d->richTextSupport & SupportDirection) {
d->action_direction_ltr = new KToggleAction(QIcon::fromTheme(QStringLiteral("format-text-direction-ltr")), i18nc("@action", "Left-to-Right"), this);
d->action_direction_ltr->setIconText(i18nc("@label left-to-right", "Left-to-Right"));
d->richTextActionList.append(d->action_direction_ltr);
d->action_direction_ltr->setObjectName(QStringLiteral("direction_ltr"));
connect(d->action_direction_ltr, &QAction::triggered, this, &KRichTextEdit::makeLeftToRight);
d->action_direction_rtl = new KToggleAction(QIcon::fromTheme(QStringLiteral("format-text-direction-rtl")), i18nc("@action", "Right-to-Left"), this);
d->action_direction_rtl->setIconText(i18nc("@label right-to-left", "Right-to-Left"));
d->richTextActionList.append(d->action_direction_rtl);
d->action_direction_rtl->setObjectName(QStringLiteral("direction_rtl"));
connect(d->action_direction_rtl, &QAction::triggered, this, &KRichTextEdit::makeRightToLeft);
QActionGroup *directionGroup = new QActionGroup(this);
directionGroup->addAction(d->action_direction_ltr);
directionGroup->addAction(d->action_direction_rtl);
} else {
d->action_direction_ltr = nullptr;
d->action_direction_rtl = nullptr;
}
if (d->richTextSupport & SupportChangeListStyle) {
d->action_list_style = new KSelectAction(QIcon::fromTheme(QStringLiteral("format-list-unordered")), i18nc("@title:menu", "List Style"), this);
QStringList listStyles;
/* clang-format off */
listStyles << i18nc("@item:inmenu no list style", "None")
<< i18nc("@item:inmenu disc list style", "Disc")
<< i18nc("@item:inmenu circle list style", "Circle")
<< i18nc("@item:inmenu square list style", "Square")
<< i18nc("@item:inmenu numbered lists", "123")
<< i18nc("@item:inmenu lowercase abc lists", "abc")
<< i18nc("@item:inmenu uppercase abc lists", "ABC")
<< i18nc("@item:inmenu lower case roman numerals", "i ii iii")
<< i18nc("@item:inmenu upper case roman numerals", "I II III");
/* clang-format on */
d->action_list_style->setItems(listStyles);
d->action_list_style->setCurrentItem(0);
d->richTextActionList.append((d->action_list_style));
d->action_list_style->setObjectName(QStringLiteral("format_list_style"));
connect(d->action_list_style, &KSelectAction::indexTriggered, this, [this](int style) {
Q_D(KRichTextWidget);
d->_k_setListStyle(style);
});
connect(d->action_list_style, &QAction::triggered, this, [d]() {
d->updateMiscActions();
});
} else {
d->action_list_style = nullptr;
}
if (d->richTextSupport & SupportIndentLists) {
d->action_list_indent = new QAction(QIcon::fromTheme(QStringLiteral("format-indent-more")), i18nc("@action", "Increase Indent"), this);
d->richTextActionList.append((d->action_list_indent));
d->action_list_indent->setObjectName(QStringLiteral("format_list_indent_more"));
connect(d->action_list_indent, &QAction::triggered, this, &KRichTextEdit::indentListMore);
connect(d->action_list_indent, &QAction::triggered, this, [d]() {
d->updateMiscActions();
});
} else {
d->action_list_indent = nullptr;
}
if (d->richTextSupport & SupportDedentLists) {
d->action_list_dedent = new QAction(QIcon::fromTheme(QStringLiteral("format-indent-less")), i18nc("@action", "Decrease Indent"), this);
d->richTextActionList.append((d->action_list_dedent));
d->action_list_dedent->setObjectName(QStringLiteral("format_list_indent_less"));
connect(d->action_list_dedent, &QAction::triggered, this, &KRichTextEdit::indentListLess);
connect(d->action_list_dedent, &QAction::triggered, this, [d]() {
d->updateMiscActions();
});
} else {
d->action_list_dedent = nullptr;
}
if (d->richTextSupport & SupportRuleLine) {
d->action_insert_horizontal_rule = new QAction(QIcon::fromTheme(QStringLiteral("insert-horizontal-rule")), i18nc("@action", "Insert Rule Line"), this);
d->richTextActionList.append((d->action_insert_horizontal_rule));
d->action_insert_horizontal_rule->setObjectName(QStringLiteral("insert_horizontal_rule"));
connect(d->action_insert_horizontal_rule, &QAction::triggered, this, &KRichTextEdit::insertHorizontalRule);
} else {
d->action_insert_horizontal_rule = nullptr;
}
if (d->richTextSupport & SupportHyperlinks) {
d->action_manage_link = new QAction(QIcon::fromTheme(QStringLiteral("insert-link")), i18nc("@action", "Link"), this);
d->richTextActionList.append((d->action_manage_link));
d->action_manage_link->setObjectName(QStringLiteral("manage_link"));
connect(d->action_manage_link, &QAction::triggered, this, [this]() {
Q_D(KRichTextWidget);
d->_k_manageLink();
});
} else {
d->action_manage_link = nullptr;
}
if (d->richTextSupport & SupportFormatPainting) {
d->action_format_painter = new KToggleAction(QIcon::fromTheme(QStringLiteral("draw-brush")), i18nc("@action", "Format Painter"), this);
d->richTextActionList.append((d->action_format_painter));
d->action_format_painter->setObjectName(QStringLiteral("format_painter"));
connect(d->action_format_painter, &QAction::toggled, this, [this](bool state) {
Q_D(KRichTextWidget);
d->_k_formatPainter(state);
});
} else {
d->action_format_painter = nullptr;
}
if (d->richTextSupport & SupportToPlainText) {
d->action_to_plain_text = new KToggleAction(i18nc("@action", "To Plain Text"), this);
d->richTextActionList.append((d->action_to_plain_text));
d->action_to_plain_text->setObjectName(QStringLiteral("action_to_plain_text"));
connect(d->action_to_plain_text, &QAction::triggered, this, &KRichTextEdit::switchToPlainText);
} else {
d->action_to_plain_text = nullptr;
}
if (d->richTextSupport & SupportSuperScriptAndSubScript) {
d->action_text_subscript = new KToggleAction(QIcon::fromTheme(QStringLiteral("format-text-subscript")), i18nc("@action", "Subscript"), this);
d->richTextActionList.append((d->action_text_subscript));
d->action_text_subscript->setObjectName(QStringLiteral("format_text_subscript"));
connect(d->action_text_subscript, &QAction::triggered, this, &KRichTextEdit::setTextSubScript);
d->action_text_superscript = new KToggleAction(QIcon::fromTheme(QStringLiteral("format-text-superscript")), i18nc("@action", "Superscript"), this);
d->richTextActionList.append((d->action_text_superscript));
d->action_text_superscript->setObjectName(QStringLiteral("format_text_superscript"));
connect(d->action_text_superscript, &QAction::triggered, this, &KRichTextEdit::setTextSuperScript);
} else {
d->action_text_subscript = nullptr;
d->action_text_superscript = nullptr;
}
if (d->richTextSupport & SupportHeading) {
// TODO: an icon maybe?
d->action_heading_level = new KSelectAction(i18nc("@title:menu", "Heading Level"), this);
const QStringList headingLevels = {i18nc("@item:inmenu no heading", "Basic text"),
i18nc("@item:inmenu heading level 1 (largest)", "Title"),
i18nc("@item:inmenu heading level 2", "Subtitle"),
i18nc("@item:inmenu heading level 3", "Section"),
i18nc("@item:inmenu heading level 4", "Subsection"),
i18nc("@item:inmenu heading level 5", "Paragraph"),
i18nc("@item:inmenu heading level 6 (smallest)", "Subparagraph")};
d->action_heading_level->setItems(headingLevels);
d->action_heading_level->setCurrentItem(0);
d->richTextActionList.append(d->action_heading_level);
d->action_heading_level->setObjectName(QStringLiteral("format_heading_level"));
connect(d->action_heading_level, &KSelectAction::indexTriggered, this, [this](int level) {
Q_D(KRichTextWidget);
d->_k_setHeadingLevel(level);
});
} else {
d->action_heading_level = nullptr;
}
disconnect(this, &QTextEdit::currentCharFormatChanged, this, nullptr);
disconnect(this, &QTextEdit::cursorPositionChanged, this, nullptr);
connect(this, &QTextEdit::currentCharFormatChanged, this, [d](const QTextCharFormat &format) {
d->updateCharFormatActions(format);
});
connect(this, &QTextEdit::cursorPositionChanged, this, [d]() {
d->updateMiscActions();
});
d->updateMiscActions();
d->updateCharFormatActions(currentCharFormat());
return d->richTextActionList;
}
void KRichTextWidget::setActionsEnabled(bool enabled)
{
Q_D(KRichTextWidget);
for (QAction *action : std::as_const(d->richTextActionList)) {
action->setEnabled(enabled);
}
d->richTextEnabled = enabled;
}
void KRichTextWidgetPrivate::_k_setListStyle(int index)
{
Q_Q(KRichTextWidget);
q->setListStyle(index);
updateMiscActions();
}
void KRichTextWidgetPrivate::_k_setHeadingLevel(int level)
{
Q_Q(KRichTextWidget);
q->setHeadingLevel(level);
updateMiscActions();
}
void KRichTextWidgetPrivate::updateCharFormatActions(const QTextCharFormat &format)
{
QFont f = format.font();
if (richTextSupport & KRichTextWidget::SupportFontFamily) {
action_font_family->setFont(f.family());
}
if (richTextSupport & KRichTextWidget::SupportFontSize) {
if (f.pointSize() > 0) {
action_font_size->setFontSize(f.pointSize());
}
}
if (richTextSupport & KRichTextWidget::SupportBold) {
action_text_bold->setChecked(f.bold());
}
if (richTextSupport & KRichTextWidget::SupportItalic) {
action_text_italic->setChecked(f.italic());
}
if (richTextSupport & KRichTextWidget::SupportUnderline) {
action_text_underline->setChecked(f.underline());
}
if (richTextSupport & KRichTextWidget::SupportStrikeOut) {
action_text_strikeout->setChecked(f.strikeOut());
}
if (richTextSupport & KRichTextWidget::SupportSuperScriptAndSubScript) {
QTextCharFormat::VerticalAlignment vAlign = format.verticalAlignment();
action_text_superscript->setChecked(vAlign == QTextCharFormat::AlignSuperScript);
action_text_subscript->setChecked(vAlign == QTextCharFormat::AlignSubScript);
}
}
void KRichTextWidgetPrivate::updateMiscActions()
{
Q_Q(KRichTextWidget);
if (richTextSupport & KRichTextWidget::SupportAlignment) {
Qt::Alignment a = q->alignment();
if (a & Qt::AlignLeft) {
action_align_left->setChecked(true);
} else if (a & Qt::AlignHCenter) {
action_align_center->setChecked(true);
} else if (a & Qt::AlignRight) {
action_align_right->setChecked(true);
} else if (a & Qt::AlignJustify) {
action_align_justify->setChecked(true);
}
}
if (richTextSupport & KRichTextWidget::SupportChangeListStyle) {
if (q->textCursor().currentList()) {
action_list_style->setCurrentItem(-q->textCursor().currentList()->format().style());
} else {
action_list_style->setCurrentItem(0);
}
}
if (richTextSupport & KRichTextWidget::SupportIndentLists) {
if (richTextEnabled) {
action_list_indent->setEnabled(q->canIndentList());
} else {
action_list_indent->setEnabled(false);
}
}
if (richTextSupport & KRichTextWidget::SupportDedentLists) {
if (richTextEnabled) {
action_list_dedent->setEnabled(q->canDedentList());
} else {
action_list_dedent->setEnabled(false);
}
}
if (richTextSupport & KRichTextWidget::SupportDirection) {
const Qt::LayoutDirection direction = q->textCursor().blockFormat().layoutDirection();
action_direction_ltr->setChecked(direction == Qt::LeftToRight);
action_direction_rtl->setChecked(direction == Qt::RightToLeft);
}
if (richTextSupport & KRichTextWidget::SupportHeading) {
action_heading_level->setCurrentItem(q->textCursor().blockFormat().headingLevel());
}
}
void KRichTextWidgetPrivate::_k_setTextForegroundColor()
{
Q_Q(KRichTextWidget);
const QColor currentColor = q->textColor();
const QColor defaultColor = KColorScheme(QPalette::Active, KColorScheme::View).foreground().color();
const QColor selectedColor = QColorDialog::getColor(currentColor.isValid() ? currentColor : defaultColor, q);
if (!selectedColor.isValid() && !currentColor.isValid()) {
q->setTextForegroundColor(defaultColor);
} else if (selectedColor.isValid()) {
q->setTextForegroundColor(selectedColor);
}
}
void KRichTextWidgetPrivate::_k_setTextBackgroundColor()
{
Q_Q(KRichTextWidget);
QTextCharFormat fmt = q->textCursor().charFormat();
const QColor currentColor = fmt.background().color();
const QColor defaultColor = KColorScheme(QPalette::Active, KColorScheme::View).foreground().color();
const QColor selectedColor = QColorDialog::getColor(currentColor.isValid() ? currentColor : defaultColor, q);
if (!selectedColor.isValid() && !currentColor.isValid()) {
q->setTextBackgroundColor(defaultColor);
} else if (selectedColor.isValid()) {
q->setTextBackgroundColor(selectedColor);
}
}
void KRichTextWidgetPrivate::_k_manageLink()
{
Q_Q(KRichTextWidget);
q->selectLinkText();
KLinkDialog *linkDialog = new KLinkDialog(q);
linkDialog->setLinkText(q->currentLinkText());
linkDialog->setLinkUrl(q->currentLinkUrl());
linkDialog->setAttribute(Qt::WA_DeleteOnClose);
QObject::connect(linkDialog, &QDialog::accepted, linkDialog, [linkDialog, this]() {
Q_Q(KRichTextWidget);
q->updateLink(linkDialog->linkUrl(), linkDialog->linkText());
});
linkDialog->show();
}
void KRichTextWidget::mouseReleaseEvent(QMouseEvent *event)
{
Q_D(KRichTextWidget);
if (d->painterActive) {
// If the painter is active, paint the selection with the
// correct format.
if (textCursor().hasSelection()) {
QTextCursor c = textCursor();
c.setCharFormat(d->painterFormat);
setTextCursor(c);
}
d->painterActive = false;
d->action_format_painter->setChecked(false);
}
KRichTextEdit::mouseReleaseEvent(event);
}
void KRichTextWidgetPrivate::_k_formatPainter(bool active)
{
Q_Q(KRichTextWidget);
if (active) {
painterFormat = q->currentCharFormat();
painterActive = true;
q->viewport()->setCursor(QCursor(QIcon::fromTheme(QStringLiteral("draw-brush")).pixmap(32, 32), 0, 32));
} else {
painterFormat = QTextCharFormat();
painterActive = false;
q->viewport()->setCursor(Qt::IBeamCursor);
}
}
void KRichTextWidget::updateActionStates()
{
Q_D(KRichTextWidget);
d->updateMiscActions();
d->updateCharFormatActions(currentCharFormat());
}
#include "moc_krichtextwidget.cpp"
@@ -0,0 +1,359 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2008 Stephen Kelly <steveire@gmail.com>
SPDX-FileCopyrightText: 2008 Thomas McGuire <thomas.mcguire@gmx.net>
SPDX-License-Identifier: LGPL-2.0-only
*/
#ifndef KRICHTEXTWIDGET_H
#define KRICHTEXTWIDGET_H
#include "krichtextedit.h"
#include "ktextwidgets_export.h"
class QAction;
class KRichTextWidgetPrivate;
/**
* @class KRichTextWidget krichtextwidget.h <KRichTextWidget>
*
* @brief A KRichTextEdit with common actions
*
* This class implements common actions which are often used with KRichTextEdit.
* All you need to do is to call createActions(), and the actions will be
* added to your KXMLGUIWindow. Remember to also add the chosen actions to
* your application ui.rc file.
*
* See the KRichTextWidget::RichTextSupportValues enum for an overview of
* supported actions.
*
* @author Stephen Kelly <steveire@gmail.com>
* @author Thomas McGuire <thomas.mcguire@gmx.net>
*
* \image html krichtextedit.png "KRichTextWidget Widget"
*
* @since 4.1
*/
class KTEXTWIDGETS_EXPORT KRichTextWidget : public KRichTextEdit
{
Q_OBJECT
Q_PROPERTY(RichTextSupport richTextSupport READ richTextSupport WRITE setRichTextSupport)
public:
/**
* These flags describe what actions will be created by createActions() after
* passing a combination of these flags to setRichTextSupport().
* @see RichTextSupport
*/
enum RichTextSupportValues {
/**
* No rich text support at all, no actions will be created. Do not use
* in combination with other flags.
*/
DisableRichText = 0x00,
/**
* Action to format the selected text as bold. If no text is selected,
* the word under the cursor is formatted bold.
* This is a KToggleAction. The status is automatically updated when
* the text cursor is moved.
*/
SupportBold = 0x01,
/**
* Action to format the selected text as italic. If no text is selected,
* the word under the cursor is formatted italic.
* This is a KToggleAction. The status is automatically updated when
* the text cursor is moved.
*/
SupportItalic = 0x02,
/**
* Action to underline the selected text. If no text is selected,
* the word under the cursor is underlined.
* This is a KToggleAction. The status is automatically updated when
* the text cursor is moved.
*/
SupportUnderline = 0x04,
/**
* Action to strike out the selected text. If no text is selected,
* the word under the cursor is struck out.
* This is a KToggleAction. The status is automatically updated when
* the text cursor is moved.
*/
SupportStrikeOut = 0x08,
/**
* Action to change the font family of the currently selected text. If
* no text is selected, the font family of the word under the cursor is
* changed.
* Displayed as a combobox when inserted into the toolbar.
* This is a KFontAction. The status is automatically updated when
* the text cursor is moved.
*/
SupportFontFamily = 0x10,
/**
* Action to change the font size of the currently selected text. If no
* text is selected, the font size of the word under the cursor is changed.
* Displayed as a combobox when inserted into the toolbar.
* This is a KFontSizeAction. The status is automatically updated when
* the text cursor is moved.
*/
SupportFontSize = 0x20,
/**
* Action to change the text color of the currently selected text. If no
* text is selected, the text color of the word under the cursor is
* changed.
* Opens a QColorDialog to select the color.
*/
SupportTextForegroundColor = 0x40,
/**
* Action to change the background color of the currently selected text. If no
* text is selected, the background color of the word under the cursor is
* changed.
* Opens a QColorDialog to select the color.
*/
SupportTextBackgroundColor = 0x80,
/**
* A combination of all the flags above.
* Includes all actions that change the format of the text.
*/
FullTextFormattingSupport = 0xff,
/**
* Action to make the current line a list element, change the
* list style or remove list formatting.
* Displayed as a combobox when inserted into a toolbar.
* This is a KSelectAction. The status is automatically updated when
* the text cursor is moved.
*/
SupportChangeListStyle = 0x100,
/**
* Action to increase the current list nesting level. This makes it
* possible to create nested lists.
*/
SupportIndentLists = 0x200,
/**
* Action to decrease the current list nesting level.
*/
SupportDedentLists = 0x400,
/**
* All of the three list actions above.
* Includes all list-related actions.
*/
FullListSupport = 0xf00,
// Not implemented yet.
// SupportCreateTables = 0x1000,
// SupportChangeCellMargin = 0x2000,
// SupportChangeCellPadding = 0x4000,
// SupportChangeTableBorderWidth = 0x8000,
// SupportChangeTableBorderColor = 0x10000,
// SupportChangeTableBorderStyle = 0x20000,
// SupportChangeCellBackground = 0x40000,
// SupportCellFillPatterns = 0x80000,
//
// FullTableSupport = 0xff000,
/**
* Actions to align the current paragraph left, righ, center or justify.
* These actions are KToogleActions. The status is automatically updated when
* the text cursor is moved.
*/
SupportAlignment = 0x100000,
// Not yet implemented SupportImages = 0x200000,
/**
* Action to insert a horizontal line.
*/
SupportRuleLine = 0x400000,
/**
* Action to convert the current text to a hyperlink. If no text is selected,
* the word under the cursor is converted.
* This action opens a dialog where the user can enter the link target.
*/
SupportHyperlinks = 0x800000,
/**
* Action to make the mouse cursor a format painter. The user can select
* text with that painter. The selected text gets the same format as the
* text that was previously selected.
*/
SupportFormatPainting = 0x1000000,
/**
* Action to change the text of the whole text edit to plain text.
* All rich text formatting will get lost.
*/
SupportToPlainText = 0x2000000,
/**
* Actions to format text as superscript or subscript. If no text is selected,
* the word under the cursor is formatted as selected.
* This is a KToggleAction. The status is automatically updated when
* the text cursor is moved.
*/
SupportSuperScriptAndSubScript = 0x4000000,
// SupportChangeParagraphSpacing = 0x200000,
/**
* Action to change direction of text to Right-To-Left or Left-To-Right.
*/
SupportDirection = 0x8000000,
/**
* Action to make the current line a heading (up to six levels,
* corresponding to HTML h1...h6 tags)
* Displayed as a combobox when inserted into a toolbar.
* This is a KSelectAction. The status is automatically updated when
* the text cursor is moved.
*
* @since 5.70
*/
SupportHeading = 0x10000000,
/**
* Includes all above actions for full rich text support
*/
FullSupport = 0xffffffff,
};
/**
* Stores a combination of #RichTextSupportValues values.
*/
Q_DECLARE_FLAGS(RichTextSupport, RichTextSupportValues)
Q_FLAG(RichTextSupport)
/**
* @brief Constructor
* @param parent the parent widget
*/
explicit KRichTextWidget(QWidget *parent);
/**
* Constructs a KRichTextWidget object
*
* @param text The initial text of the text edit, which is interpreted as
* HTML.
* @param parent The parent widget
*/
explicit KRichTextWidget(const QString &text, QWidget *parent = nullptr);
/**
* @brief Destructor
*/
~KRichTextWidget() override;
/**
* @brief Creates the actions and adds them to the given action collection.
*
* Call this before calling setupGUI() in your application, but after
* calling setRichTextSupport().
*
* The XML file of your KXmlGuiWindow needs to have the action names in
* them, so that the actions actually appear in the menu and in the toolbars.
*
* Below is a list of actions that are created,depending on the supported rich text
* subset set by setRichTextSupport(). The list contains action names.
* Those names need to be the same in your XML file.
*
* See the KRichTextWidget::RichTextSupportValues enum documentation for a
* detailed explanation of each action.
*
* <table>
* <tr><td><b>XML Name</b></td><td><b>RichTextSupportValues flag</b></td></tr>
* <tr><td>format_text_foreground_color</td><td>SupportTextForegroundColor</td></tr>
* <tr><td>format_text_background_color</td><td>SupportTextBackgroundColor</td></tr>
* <tr><td>format_font_family</td><td>SupportFontFamily</td></tr>
* <tr><td>format_font_size</td><td>SupportFontSize</td></tr>
* <tr><td>format_text_bold</td><td>SupportBold</td></tr>
* <tr><td>format_text_italic</td><td>SupportItalic</td></tr>
* <tr><td>format_text_underline</td><td>SupportUnderline</td></tr>
* <tr><td>format_text_strikeout</td><td>SupportStrikeOut</td></tr>
* <tr><td>format_align_left</td><td>SupportAlignment</td></tr>
* <tr><td>format_align_center</td><td>SupportAlignment</td></tr>
* <tr><td>format_align_right</td><td>SupportAlignment</td></tr>
* <tr><td>format_align_justify</td><td>SupportAlignment</td></tr>
* <tr><td>direction_ltr</td><td>SupportDirection</td></tr>
* <tr><td>direction_rtl</td><td>SupportDirection</td></tr>
* <tr><td>format_list_style</td><td>SupportChangeListStyle</td></tr>
* <tr><td>format_list_indent_more</td><td>SupportIndentLists</td></tr>
* <tr><td>format_list_indent_less</td><td>SupportDedentLists</td></tr>
* <tr><td>insert_horizontal_rule</td><td>SupportRuleLine</td></tr>
* <tr><td>manage_link</td><td>SupportHyperlinks</td></tr>
* <tr><td>format_painter</td><td>SupportFormatPainting</td></tr>
* <tr><td>action_to_plain_text</td><td>SupportToPlainText</td></tr>
* <tr><td>format_text_subscript & format_text_superscript</td><td>SupportSuperScriptAndSubScript</td></tr>
* <tr><td>format_heading_level</td><td>SupportHeading</td></tr>
* </table>
*
* @since 5.0
*/
virtual QList<QAction *> createActions();
/**
* @brief Sets the supported rich text subset available.
*
* The default is KRichTextWidget::FullSupport and will be set in the
* constructor.
*
* You need to call createActions() afterwards.
*
* @param support The supported subset.
*/
void setRichTextSupport(const KRichTextWidget::RichTextSupport &support);
/**
* @brief Returns the supported rich text subset available.
* @return The supported subset.
*/
RichTextSupport richTextSupport() const;
/**
* Tells KRichTextWidget to update the state of the actions created by
* createActions().
* This is normally automatically done, but there might be a few cases where
* you'll need to manually call this function.
*
* Call this function only after calling createActions().
*/
void updateActionStates();
public Q_SLOTS:
/**
* Disables or enables all of the actions created by
* createActions().
* This may be useful in cases where rich text mode may be set on or off.
*
* @param enabled Whether to enable or disable the actions.
*/
void setActionsEnabled(bool enabled);
protected:
/**
* Reimplemented.
* Catches mouse release events. Used to know when a selection has been completed.
*/
void mouseReleaseEvent(QMouseEvent *event) override;
private:
//@cond PRIVATE
Q_DECLARE_PRIVATE(KRichTextWidget)
//@endcond
};
Q_DECLARE_OPERATORS_FOR_FLAGS(KRichTextWidget::RichTextSupport)
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,369 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2002 Carsten Pfeiffer <pfeiffer@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KTEXTEDIT_H
#define KTEXTEDIT_H
#include "ktextwidgets_export.h"
#include <QTextEdit>
#include <memory>
#include <sonnet/highlighter.h>
namespace Sonnet
{
class SpellCheckDecorator;
}
class KTextEditPrivate;
/**
* @class KTextEdit ktextedit.h <KTextEdit>
*
* @short A KDE'ified QTextEdit
*
* This is just a little subclass of QTextEdit, implementing
* some standard KDE features, like cursor auto-hiding, configurable
* wheelscrolling (fast-scroll or zoom), spell checking and deleting of entire
* words with Ctrl-Backspace or Ctrl-Delete.
*
* This text edit provides two ways of spell checking: background checking,
* which will mark incorrectly spelled words red, and a spell check dialog,
* which lets the user check and correct all incorrectly spelled words.
*
* Basic rule: whenever you want to use QTextEdit, use KTextEdit!
*
* \image html ktextedit.png "KTextEdit Widget"
*
* @see QTextEdit
* @author Carsten Pfeiffer <pfeiffer@kde.org>
*/
class KTEXTWIDGETS_EXPORT KTextEdit : public QTextEdit // krazy:exclude=qclasses
{
Q_OBJECT
Q_PROPERTY(bool checkSpellingEnabled READ checkSpellingEnabled WRITE setCheckSpellingEnabled)
Q_PROPERTY(QString spellCheckingLanguage READ spellCheckingLanguage WRITE setSpellCheckingLanguage)
public:
/**
* Constructs a KTextEdit object. See QTextEdit::QTextEdit
* for details.
*/
explicit KTextEdit(const QString &text, QWidget *parent = nullptr);
/**
* Constructs a KTextEdit object. See QTextEdit::QTextEdit
* for details.
*/
explicit KTextEdit(QWidget *parent = nullptr);
/**
* Destroys the KTextEdit object.
*/
~KTextEdit() override;
/**
* Reimplemented to set a proper "deactivated" background color.
*/
virtual void setReadOnly(bool readOnly);
/**
* Turns background spell checking for this text edit on or off.
* Note that spell checking is only available in read-writable KTextEdits.
*
* Enabling spell checking will set back the current highlighter to the one
* returned by createHighlighter().
*
* @see checkSpellingEnabled()
* @see isReadOnly()
* @see setReadOnly()
*/
virtual void setCheckSpellingEnabled(bool check);
/**
* Returns true if background spell checking is enabled for this text edit.
* Note that it even returns true if this is a read-only KTextEdit,
* where spell checking is actually disabled.
* By default spell checking is disabled.
*
* @see setCheckSpellingEnabled()
*/
virtual bool checkSpellingEnabled() const;
/**
* Returns true if the given paragraph or block should be spellcheck.
* For example, a mail client does not want to check quoted text, and
* would return false here (by checking whether the block starts with a
* quote sign).
*
* Always returns true by default.
*
*/
virtual bool shouldBlockBeSpellChecked(const QString &block) const;
/**
* Selects the characters at the specified position. Any previous
* selection will be lost. The cursor is moved to the first character
* of the new selection.
*
* @param length The length of the selection, in number of characters
* @param pos The position of the first character of the selection
*/
void highlightWord(int length, int pos);
/**
* Allows to create a specific highlighter if reimplemented.
*
* By default, it creates a normal highlighter, based on the config
* file given to setSpellCheckingConfigFileName().
*
* This highlighter is set each time spell checking is toggled on by
* calling setCheckSpellingEnabled(), but can later be overridden by calling
* setHighlighter().
*
* @see setHighlighter()
* @see highlighter()
* @see setSpellCheckingConfigFileName()
*/
virtual void createHighlighter();
/**
* Returns the current highlighter, which is 0 if spell checking is disabled.
* The default highlighter is the one created by createHighlighter(), but
* might be overridden by setHighlighter().
*
* @see setHighlighter()
* @see createHighlighter()
*/
Sonnet::Highlighter *highlighter() const;
/**
* Sets a custom background spell highlighter for this text edit.
* Normally, the highlighter returned by createHighlighter() will be
* used to detect and highlight incorrectly spelled words, but this
* function allows to set a custom highlighter.
*
* This has to be called after enabling spell checking with
* setCheckSpellingEnabled(), otherwise it has no effect.
*
* Ownership is transferred to the KTextEdit
*
* @see highlighter()
* @see createHighlighter()
* @param highLighter the new highlighter which will be used now
*/
void setHighlighter(Sonnet::Highlighter *_highLighter);
/**
* Return standard KTextEdit popupMenu
* @since 4.1
*/
virtual QMenu *mousePopupMenu();
/**
* Enable find replace action.
* @since 4.1
*/
void enableFindReplace(bool enabled);
/**
* @return the spell checking language which was set by
* setSpellCheckingLanguage(), the spellcheck dialog or the spellcheck
* config dialog, or an empty string if that has never been called.
* @since 4.2
*/
const QString &spellCheckingLanguage() const;
/**
* @since 4.10
*/
void showTabAction(bool show);
/**
* @since 4.10
*/
void showAutoCorrectButton(bool show);
/**
* @since 4.10
* create a modal spellcheck dialogbox and spellCheckingFinished signal we sent when
* we finish spell checking or spellCheckingCanceled signal when we cancel spell checking
*/
void forceSpellChecking();
Q_SIGNALS:
/**
* emit signal when we activate or not autospellchecking
*
* @since 4.1
*/
void checkSpellingChanged(bool);
/**
* Signal sends when spell checking is finished/stopped/completed
* @since 4.1
*/
void spellCheckStatus(const QString &);
/**
* Emitted when the user changes the language in the spellcheck dialog
* shown by checkSpelling() or when calling setSpellCheckingLanguage().
*
* @param language the new language the user selected
* @since 4.1
*/
void languageChanged(const QString &language);
/**
* Emitted before the context menu is displayed.
*
* The signal allows you to add your own entries into the
* the context menu that is created on demand.
*
* NOTE: Do not store the pointer to the QMenu
* provided through since it is created and deleted
* on demand.
*
* @param p the context menu about to be displayed
* @since 4.5
*/
void aboutToShowContextMenu(QMenu *menu);
/**
* @since 4.10
*/
void spellCheckerAutoCorrect(const QString &currentWord, const QString &autoCorrectWord);
/**
* signal spellCheckingFinished is sent when we finish spell check or we click on "Terminate" button in sonnet dialogbox
* @since 4.10
*/
void spellCheckingFinished();
/**
* signal spellCheckingCanceled is sent when we cancel spell checking.
* @since 4.10
*/
void spellCheckingCanceled();
public Q_SLOTS:
/**
* Set the spell check language which will be used for highlighting spelling
* mistakes and for the spellcheck dialog.
* The languageChanged() signal will be emitted when the new language is
* different from the old one.
*
* @since 4.1
*/
void setSpellCheckingLanguage(const QString &language);
/**
* Show a dialog to check the spelling. The spellCheckStatus() signal
* will be emitted when the spell checking dialog is closed.
*/
void checkSpelling();
/**
* Opens a Sonnet::ConfigDialog for this text edit.
* The spellcheck language of the config dialog is set to the current spellcheck
* language of the textedit. If the user changes the language in that dialog,
* the languageChanged() signal is emitted.
*
* @param configFileName The file which is used to store and load the config
* settings
* @param windowIcon the icon which is used for the titlebar of the spell dialog
* window. Can be empty, then no icon is set.
*
* @since 4.2
*/
void showSpellConfigDialog(const QString &windowIcon = QString());
/**
* Create replace dialogbox
* @since 4.1
*/
void replace();
/**
* Add custom spell checker decorator
* @since 5.11
*/
void addTextDecorator(Sonnet::SpellCheckDecorator *decorator);
/**
* @brief clearDecorator clear the spellcheckerdecorator
* @since 5.11
*/
void clearDecorator();
protected Q_SLOTS:
/**
* @since 4.1
*/
void slotDoReplace();
void slotReplaceNext();
void slotDoFind();
void slotFind();
void slotFindNext();
/**
* @since 5.11
*/
void slotFindPrevious();
void slotReplace();
/**
* @since 4.3
*/
void slotSpeakText();
protected:
/**
* Reimplemented to catch "delete word" shortcut events.
*/
bool event(QEvent *) override;
/**
* Reimplemented for internal reasons
*/
void keyPressEvent(QKeyEvent *) override;
/**
* Reimplemented to instantiate a KDictSpellingHighlighter, if
* spellchecking is enabled.
*/
void focusInEvent(QFocusEvent *) override;
/**
* Deletes a word backwards from the current cursor position,
* if available.
*/
virtual void deleteWordBack();
/**
* Deletes a word forwards from the current cursor position,
* if available.
*/
virtual void deleteWordForward();
/**
* Reimplemented from QTextEdit to add spelling related items
* when appropriate.
*/
void contextMenuEvent(QContextMenuEvent *) override;
protected:
KTEXTWIDGETS_NO_EXPORT KTextEdit(KTextEditPrivate &dd, const QString &text, QWidget *parent);
KTEXTWIDGETS_NO_EXPORT KTextEdit(KTextEditPrivate &dd, QWidget *parent);
protected:
std::unique_ptr<class KTextEditPrivate> const d_ptr;
private:
Q_DECLARE_PRIVATE(KTextEdit)
};
#endif // KTEXTEDIT_H
@@ -0,0 +1,120 @@
/*
This file is part of the KDE libraries
SPDX-FileCopyrightText: 2002 Carsten Pfeiffer <pfeiffer@kde.org>
SPDX-FileCopyrightText: 2005 Michael Brade <brade@kde.org>
SPDX-FileCopyrightText: 2012 Laurent Montel <montel@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KTEXTEDIT_P_H
#define KTEXTEDIT_P_H
#include "kfind.h"
#include "kfinddialog.h"
#include "kreplace.h"
#include "kreplacedialog.h"
#include <Sonnet/SpellCheckDecorator>
#include <Sonnet/Speller>
#include <QSettings>
#include <QTextDocumentFragment>
#ifdef HAVE_SPEECH
#include <QTextToSpeech>
#endif
class KTextEditPrivate
{
Q_DECLARE_PUBLIC(KTextEdit)
public:
explicit KTextEditPrivate(KTextEdit *qq)
: q_ptr(qq)
, customPalette(false)
, spellCheckingEnabled(false)
, findReplaceEnabled(true)
, showTabAction(true)
, showAutoCorrectionButton(false)
{
// 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();
}
virtual ~KTextEditPrivate()
{
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 *const q_ptr;
QAction *autoSpellCheckAction;
QAction *allowTab;
QAction *spellCheckAction;
QMenu *languagesMenu = nullptr;
bool customPalette : 1;
bool spellCheckingEnabled : 1;
bool findReplaceEnabled : 1;
bool showTabAction : 1;
bool showAutoCorrectionButton : 1;
QTextDocumentFragment originalDoc;
QString spellCheckingLanguage;
Sonnet::SpellCheckDecorator *decorator = nullptr;
Sonnet::Speller *speller = nullptr;
KFindDialog *findDlg = nullptr;
KFind *find = nullptr;
KReplaceDialog *repDlg = nullptr;
KReplace *replace = nullptr;
#ifdef HAVE_SPEECH
QTextToSpeech *textToSpeech = nullptr;
#endif
int findIndex = 0;
int repIndex = 0;
int lastReplacedPosition = -1;
};
#endif
@@ -0,0 +1,284 @@
/*
Nested list helper
SPDX-FileCopyrightText: 2008 Stephen Kelly <steveire@gmail.com>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "nestedlisthelper_p.h"
#include <QKeyEvent>
#include <QTextBlock>
#include <QTextCursor>
#include <QTextList>
#include "ktextedit.h"
NestedListHelper::NestedListHelper(QTextEdit *te)
: textEdit(te)
{
}
NestedListHelper::~NestedListHelper()
{
}
bool NestedListHelper::handleKeyPressEvent(QKeyEvent *event)
{
QTextCursor cursor = textEdit->textCursor();
if (!cursor.currentList()) {
return false;
}
if (event->key() == Qt::Key_Backspace && !cursor.hasSelection() && cursor.atBlockStart() && canDedent()) {
changeIndent(-1);
return true;
}
if (event->key() == Qt::Key_Return && !cursor.hasSelection() && cursor.block().text().isEmpty() && canDedent()) {
changeIndent(-1);
return true;
}
if (event->key() == Qt::Key_Tab && (cursor.atBlockStart() || cursor.hasSelection()) && canIndent()) {
changeIndent(+1);
return true;
}
return false;
}
bool NestedListHelper::canIndent() const
{
const QTextCursor cursor = topOfSelection();
const QTextBlock block = cursor.block();
if (!block.isValid()) {
return false;
}
if (!block.textList()) {
return true;
}
const QTextBlock prevBlock = block.previous();
if (!prevBlock.textList()) {
return false;
}
return block.textList()->format().indent() <= prevBlock.textList()->format().indent();
}
bool NestedListHelper::canDedent() const
{
const QTextCursor cursor = bottomOfSelection();
const QTextBlock block = cursor.block();
if (!block.isValid()) {
return false;
}
if (!block.textList() || block.textList()->format().indent() <= 0) {
return false;
}
const QTextBlock nextBlock = block.next();
if (!nextBlock.textList()) {
return true;
}
return block.textList()->format().indent() >= nextBlock.textList()->format().indent();
}
bool NestedListHelper::handleAfterDropEvent(QDropEvent *dropEvent)
{
Q_UNUSED(dropEvent);
QTextCursor cursor = topOfSelection();
QTextBlock droppedBlock = cursor.block();
int firstDroppedItemIndent = droppedBlock.textList()->format().indent();
int minimumIndent = droppedBlock.previous().textList()->format().indent();
if (firstDroppedItemIndent < minimumIndent) {
cursor = QTextCursor(droppedBlock);
QTextListFormat fmt = droppedBlock.textList()->format();
fmt.setIndent(minimumIndent);
QTextList *list = cursor.createList(fmt);
int endOfDrop = bottomOfSelection().position();
while (droppedBlock.next().position() < endOfDrop) {
droppedBlock = droppedBlock.next();
if (droppedBlock.textList()->format().indent() != firstDroppedItemIndent) {
// new list?
}
list->add(droppedBlock);
}
// list.add( droppedBlock );
}
return true;
}
void NestedListHelper::processList(QTextList *list)
{
QTextBlock block = list->item(0);
int thisListIndent = list->format().indent();
QTextCursor cursor = QTextCursor(block);
list = cursor.createList(list->format());
bool processingSubList = false;
while (block.next().textList() != nullptr) {
block = block.next();
QTextList *nextList = block.textList();
int nextItemIndent = nextList->format().indent();
if (nextItemIndent < thisListIndent) {
return;
} else if (nextItemIndent > thisListIndent) {
if (processingSubList) {
continue;
}
processingSubList = true;
processList(nextList);
} else {
processingSubList = false;
list->add(block);
}
}
// delete nextList;
// nextList = 0;
}
void NestedListHelper::reformatList(QTextBlock block)
{
if (block.textList()) {
int minimumIndent = block.textList()->format().indent();
// Start at the top of the list
while (block.previous().textList() != nullptr) {
if (block.previous().textList()->format().indent() < minimumIndent) {
break;
}
block = block.previous();
}
processList(block.textList());
}
}
void NestedListHelper::reformatList()
{
QTextCursor cursor = textEdit->textCursor();
reformatList(cursor.block());
}
QTextCursor NestedListHelper::topOfSelection() const
{
QTextCursor cursor = textEdit->textCursor();
if (cursor.hasSelection()) {
cursor.setPosition(qMin(cursor.position(), cursor.anchor()));
}
return cursor;
}
QTextCursor NestedListHelper::bottomOfSelection() const
{
QTextCursor cursor = textEdit->textCursor();
if (cursor.hasSelection()) {
cursor.setPosition(qMax(cursor.position(), cursor.anchor()));
}
return cursor;
}
void NestedListHelper::changeIndent(int delta)
{
QTextCursor cursor = textEdit->textCursor();
cursor.beginEditBlock();
const int top = qMin(cursor.position(), cursor.anchor());
const int bottom = qMax(cursor.position(), cursor.anchor());
// A reformatList should be called on the block inside selection
// with the lowest indentation level
int minIndentPosition;
int minIndent = -1;
// Changing indentation of all blocks between top and bottom
cursor.setPosition(top);
do {
QTextList *list = cursor.currentList();
// Setting up listFormat
QTextListFormat listFmt;
if (!list) {
if (delta > 0) {
// No list, we're increasing indentation -> create a new one
listFmt.setStyle(QTextListFormat::ListDisc);
listFmt.setIndent(delta);
}
// else do nothing
} else {
const int newIndent = list->format().indent() + delta;
if (newIndent > 0) {
listFmt = list->format();
listFmt.setIndent(newIndent);
} else {
listFmt.setIndent(0);
}
}
if (listFmt.indent() > 0) {
// This block belongs to a list: here we create a new one
// for each block, and then let reformatList() sort it out
cursor.createList(listFmt);
if (minIndent == -1 || minIndent > listFmt.indent()) {
minIndent = listFmt.indent();
minIndentPosition = cursor.block().position();
}
} else {
// If the block belonged to a list, remove it from there
if (list) {
list->remove(cursor.block());
}
// The removal does not change the indentation, we need to do it explicitly
QTextBlockFormat blkFmt;
blkFmt.setIndent(0);
cursor.mergeBlockFormat(blkFmt);
}
if (!cursor.block().next().isValid()) {
break;
}
cursor.movePosition(QTextCursor::NextBlock);
} while (cursor.position() < bottom);
// Reformatting the whole list
if (minIndent != -1) {
cursor.setPosition(minIndentPosition);
reformatList(cursor.block());
}
cursor.setPosition(top);
reformatList(cursor.block());
cursor.endEditBlock();
}
void NestedListHelper::handleOnBulletType(int styleIndex)
{
QTextCursor cursor = textEdit->textCursor();
if (styleIndex != 0) {
QTextListFormat::Style style = static_cast<QTextListFormat::Style>(styleIndex);
QTextList *currentList = cursor.currentList();
QTextListFormat listFmt;
cursor.beginEditBlock();
if (currentList) {
listFmt = currentList->format();
listFmt.setStyle(style);
currentList->setFormat(listFmt);
} else {
listFmt.setStyle(style);
cursor.createList(listFmt);
}
cursor.endEditBlock();
} else {
QTextBlockFormat bfmt;
bfmt.setObjectIndex(-1);
cursor.setBlockFormat(bfmt);
}
reformatList();
}
@@ -0,0 +1,114 @@
/*
Nested list helper
SPDX-FileCopyrightText: 2008 Stephen Kelly <steveire@gmail.com>
SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef NESTEDLISTHELPER_H
#define NESTEDLISTHELPER_H
//@cond PRIVATE
class QTextEdit;
class QKeyEvent;
class QDropEvent;
class QTextCursor;
class QTextList;
class QTextBlock;
/**
*
* @short Helper class for automatic handling of nested lists in a text edit
*
*
* @author Stephen Kelly
* @since 4.1
* @internal
*/
class NestedListHelper
{
public:
/**
* Create a helper
*
* @param te The text edit object to handle lists in.
*/
explicit NestedListHelper(QTextEdit *te);
/**
* Destructor
*/
~NestedListHelper();
/**
*
* Handles a key press before it is processed by the text edit widget.
*
* This includes:
* 1. Backspace at the beginning of a line decreases nesting level
* 2. Return at the empty list element decreases nesting level
* 3. Tab at the beginning of a line OR with a multi-line selection
* increases nesting level
*
* @param event The event to be handled
* @return Whether the event was completely handled by this method.
*/
bool handleKeyPressEvent(QKeyEvent *event);
bool handleAfterDropEvent(QDropEvent *event);
/**
* Changes the indent (nesting level) on a current list item or selection
* by the value @p delta (typically, +1 or -1)
*/
void changeIndent(int delta);
/**
* Changes the style of the current list or creates a new list with
* the specified style.
*
* @param styleIndex The QTextListStyle of the list.
*/
void handleOnBulletType(int styleIndex);
/**
* @brief Check whether the current item in the list may be indented.
*
* An list item must have an item above it on the same or greater level
* if it can be indented.
*
* Also, a block which is currently part of a list can be indented.
*
* @sa canDedent
*
* @return Whether the item can be indented.
*/
bool canIndent() const;
/**
* \brief Check whether the current item in the list may be dedented.
*
* An item may be dedented if it is part of a list.
* The next item must be at the same or lesser level.
*
* @sa canIndent
*
* @return Whether the item can be dedented.
*/
bool canDedent() const;
private:
QTextCursor topOfSelection() const;
QTextCursor bottomOfSelection() const;
void processList(QTextList *list);
void reformatList(QTextBlock block);
void reformatList();
QTextEdit *const textEdit;
};
//@endcond
#endif