| 1 | /* |
| 2 | This file is part of the KDE project |
| 3 | SPDX-FileCopyrightText: 2001 S.R. Haque <srhaque@iee.org>. |
| 4 | SPDX-FileCopyrightText: 2002 David Faure <david@mandrakesoft.com> |
| 5 | SPDX-FileCopyrightText: 2004 Arend van Beelen jr. <arend@auton.nl> |
| 6 | |
| 7 | SPDX-License-Identifier: LGPL-2.0-only |
| 8 | */ |
| 9 | |
| 10 | #ifndef KFIND_H |
| 11 | #define KFIND_H |
| 12 | |
| 13 | #include "ktextwidgets_export.h" |
| 14 | |
| 15 | #include <QObject> |
| 16 | #include <memory> |
| 17 | |
| 18 | class QDialog; |
| 19 | class KFindPrivate; |
| 20 | |
| 21 | /*! |
| 22 | * \class KFind |
| 23 | * \inmodule KTextWidgets |
| 24 | * |
| 25 | * \brief A generic implementation of the "find" function. |
| 26 | * |
| 27 | * \b Detail: |
| 28 | * |
| 29 | * This class includes prompt handling etc. Also provides some |
| 30 | * static functions which can be used to create custom behavior |
| 31 | * instead of using the class directly. |
| 32 | * |
| 33 | * \b Example: |
| 34 | * |
| 35 | * To use the class to implement a complete find feature: |
| 36 | * |
| 37 | * In the slot connected to the find action, after using KFindDialog: |
| 38 | * \code |
| 39 | * |
| 40 | * // This creates a find-next-prompt dialog if needed. |
| 41 | * m_find = new KFind(pattern, options, this); |
| 42 | * |
| 43 | * // Connect textFound() signal to code which handles highlighting of found text. |
| 44 | * connect(m_find, &KFind::textFound, this, [this](const QString &text, int matchingIndex, int matchedLength) { |
| 45 | * slotHighlight(text, matchingIndex, matchedLength); |
| 46 | * })); |
| 47 | * // Connect findNext signal - called when pressing the button in the dialog |
| 48 | * connect(m_find, SIGNAL(findNext()), |
| 49 | * this, SLOT(slotFindNext())); |
| 50 | * \endcode |
| 51 | * |
| 52 | * Then initialize the variables determining the "current position" |
| 53 | * (to the cursor, if the option FromCursor is set, |
| 54 | * to the beginning of the selection if the option SelectedText is set, |
| 55 | * and to the beginning of the document otherwise). |
| 56 | * Initialize the "end of search" variables as well (end of doc or end of selection). |
| 57 | * Swap begin and end if FindBackwards. |
| 58 | * Finally, call slotFindNext(); |
| 59 | * |
| 60 | * \code |
| 61 | * void slotFindNext() |
| 62 | * { |
| 63 | * KFind::Result res = KFind::NoMatch; |
| 64 | * while (res == KFind::NoMatch && <position not at end>) { |
| 65 | * if (m_find->needData()) |
| 66 | * m_find->setData(<current text fragment>); |
| 67 | * |
| 68 | * // Let KFind inspect the text fragment, and display a dialog if a match is found |
| 69 | * res = m_find->find(); |
| 70 | * |
| 71 | * if (res == KFind::NoMatch) { |
| 72 | * <Move to the next text fragment, honoring the FindBackwards setting for the direction> |
| 73 | * } |
| 74 | * } |
| 75 | * |
| 76 | * if (res == KFind::NoMatch) // i.e. at end |
| 77 | * <Call either m_find->displayFinalDialog(); m_find->deleteLater(); m_find = nullptr; |
| 78 | * or if (m_find->shouldRestart()) { reinit (w/o FromCursor); m_find->resetCounts(); slotFindNext(); } |
| 79 | * else { m_find->closeFindNextDialog(); }> |
| 80 | * } |
| 81 | * \endcode |
| 82 | * |
| 83 | * Don't forget to delete m_find in the destructor of your class, |
| 84 | * unless you gave it a parent widget on construction. |
| 85 | * |
| 86 | * This implementation allows to have a "Find Next" action, which resumes the |
| 87 | * search, even if the user closed the "Find Next" dialog. |
| 88 | * |
| 89 | * A "Find Previous" action can simply switch temporarily the value of |
| 90 | * FindBackwards and call slotFindNext() - and reset the value afterwards. |
| 91 | */ |
| 92 | class KTEXTWIDGETS_EXPORT KFind : public QObject |
| 93 | { |
| 94 | Q_OBJECT |
| 95 | |
| 96 | public: |
| 97 | /*! |
| 98 | * \enum KFind::Options |
| 99 | * |
| 100 | * \value WholeWordsOnly |
| 101 | * Match whole words only. |
| 102 | * \value FromCursor |
| 103 | * Start from current cursor position. |
| 104 | * \value SelectedText |
| 105 | * Only search selected area. |
| 106 | * \value CaseSensitive |
| 107 | * Consider case when matching. |
| 108 | * \value FindBackwards |
| 109 | * Go backwards. |
| 110 | * \value RegularExpression |
| 111 | * Interpret the pattern as a regular expression. |
| 112 | * \value FindIncremental |
| 113 | * Find incremental. |
| 114 | * \value MinimumUserOption |
| 115 | * User options start with this bit |
| 116 | */ |
| 117 | enum Options { |
| 118 | WholeWordsOnly = 1, |
| 119 | FromCursor = 2, |
| 120 | SelectedText = 4, |
| 121 | CaseSensitive = 8, |
| 122 | FindBackwards = 16, |
| 123 | RegularExpression = 32, |
| 124 | FindIncremental = 64, |
| 125 | MinimumUserOption = 65536, |
| 126 | }; |
| 127 | Q_DECLARE_FLAGS(SearchOptions, Options) |
| 128 | |
| 129 | /*! |
| 130 | * Only use this constructor if you don't use KFindDialog, or if |
| 131 | * you use it as a modal dialog. |
| 132 | */ |
| 133 | KFind(const QString &pattern, long options, QWidget *parent); |
| 134 | |
| 135 | /*! |
| 136 | * This is the recommended constructor if you also use KFindDialog (non-modal). |
| 137 | * You should pass the pointer to it here, so that when a message box |
| 138 | * appears it has the right parent. Don't worry about deletion, KFind |
| 139 | * will notice if the find dialog is closed. |
| 140 | */ |
| 141 | KFind(const QString &pattern, long options, QWidget *parent, QWidget *findDialog); |
| 142 | ~KFind() override; |
| 143 | |
| 144 | /*! |
| 145 | * \value NoMatch |
| 146 | * \value Match |
| 147 | */ |
| 148 | enum Result { |
| 149 | NoMatch, |
| 150 | Match, |
| 151 | }; |
| 152 | |
| 153 | /*! |
| 154 | * Returns true if the application must supply a new text fragment |
| 155 | * It also means the last call returned "NoMatch". But by storing this here |
| 156 | * the application doesn't have to store it in a member variable (between |
| 157 | * calls to slotFindNext()). |
| 158 | */ |
| 159 | bool needData() const; |
| 160 | |
| 161 | /*! |
| 162 | * Call this when needData returns true, before calling find(). |
| 163 | * |
| 164 | * \a data is the text fragment (line). |
| 165 | * |
| 166 | * \a startPos if set, the index at which the search should start. |
| 167 | * |
| 168 | * Usually \a startPos is only necessary for the very first call to setData, |
| 169 | * for the 'find in selection' feature. The default value of -1 |
| 170 | * means "process all the data", i.e. either 0 or data.length()-1 depending |
| 171 | * on FindBackwards. |
| 172 | */ |
| 173 | void setData(const QString &data, int startPos = -1); |
| 174 | |
| 175 | /*! |
| 176 | * Call this when needData returns true, before calling find(). The use of |
| 177 | * ID's is especially useful if you're using the FindIncremental option. |
| 178 | * |
| 179 | * \a id the id of the text fragment. |
| 180 | * |
| 181 | * \a data the text fragment (line). |
| 182 | * |
| 183 | * \a startPos if set, the index at which the search should start. |
| 184 | * This is only necessary for the very first call to setData usually, |
| 185 | * for the 'find in selection' feature. A value of -1 (the default value) |
| 186 | * means "process all the data", i.e. either 0 or data.length()-1 depending |
| 187 | * on FindBackwards. |
| 188 | */ |
| 189 | void setData(int id, const QString &data, int startPos = -1); |
| 190 | |
| 191 | /*! |
| 192 | * Walk the text fragment (e.g. in a text-processor line or spreadsheet |
| 193 | * cell ...etc) looking for matches. |
| 194 | * For each match, emits the textFound() signal and displays the find-again |
| 195 | * dialog to ask if the user wants to find the same text again. |
| 196 | */ |
| 197 | Result find(); |
| 198 | |
| 199 | /*! |
| 200 | * Return the current options. |
| 201 | * |
| 202 | * Warning: this is usually the same value as the one passed to the constructor, |
| 203 | * but options might change _during_ the replace operation: |
| 204 | * e.g. the "All" button resets the PromptOnReplace flag. |
| 205 | * |
| 206 | * \sa KFind::Options |
| 207 | */ |
| 208 | long options() const; |
| 209 | |
| 210 | /*! |
| 211 | * Set new \a options. Usually this is used for setting or clearing the |
| 212 | * FindBackwards options. |
| 213 | * |
| 214 | * \sa KFind::Options |
| 215 | */ |
| 216 | virtual void setOptions(long options); |
| 217 | |
| 218 | /*! |
| 219 | * Returns the pattern we're currently looking for |
| 220 | */ |
| 221 | QString pattern() const; |
| 222 | |
| 223 | /*! |
| 224 | * Sets the \a pattern to look for |
| 225 | */ |
| 226 | void setPattern(const QString &pattern); |
| 227 | |
| 228 | /*! |
| 229 | * Returns the number of matches found (i.e. the number of times the textFound() |
| 230 | * signal was emitted). |
| 231 | * If 0, can be used in a dialog box to tell the user "no match was found". |
| 232 | * The final dialog does so already, unless you used setDisplayFinalDialog(false). |
| 233 | */ |
| 234 | int numMatches() const; |
| 235 | |
| 236 | /*! |
| 237 | * Call this to reset the numMatches count |
| 238 | * (and the numReplacements count for a KReplace). |
| 239 | * Can be useful if reusing the same KReplace for different operations, |
| 240 | * or when restarting from the beginning of the document. |
| 241 | */ |
| 242 | virtual void resetCounts(); |
| 243 | |
| 244 | /*! |
| 245 | * Virtual method, which allows applications to add extra checks for |
| 246 | * validating a candidate match. It's only necessary to reimplement this |
| 247 | * if the find dialog extension has been used to provide additional |
| 248 | * criteria. |
| 249 | * |
| 250 | * \a text The current text fragment |
| 251 | * |
| 252 | * \a index The starting index where the candidate match was found |
| 253 | * |
| 254 | * \a matchedlength The length of the candidate match |
| 255 | * |
| 256 | * Returns true if the candidate matches. |
| 257 | */ |
| 258 | virtual bool validateMatch(const QString &text, int index, int matchedlength); |
| 259 | |
| 260 | /*! |
| 261 | * Returns true if we should restart the search from scratch. |
| 262 | * Can ask the user, or return false (if we already searched the whole document). |
| 263 | * |
| 264 | * \a forceAsking set to true if the user modified the document during the |
| 265 | * search. In that case it makes sense to restart the search again. |
| 266 | * |
| 267 | * \a showNumMatches set to true if the dialog should show the number of |
| 268 | * matches. Set to false if the application provides a "find previous" action, |
| 269 | * in which case the match count will be erroneous when hitting the end, |
| 270 | * and we could even be hitting the beginning of the document (so not all |
| 271 | * matches have even been seen). |
| 272 | */ |
| 273 | virtual bool shouldRestart(bool forceAsking = false, bool showNumMatches = true) const; |
| 274 | |
| 275 | /*! |
| 276 | * Search \a text for \a pattern. If a match is found, the length of the matched |
| 277 | * string will be stored in \a matchedLength and the index of the matched string |
| 278 | * will be returned. If no match is found -1 is returned. |
| 279 | * |
| 280 | * If the KFind::RegularExpression flag is set, the \a pattern will be iterpreted |
| 281 | * as a regular expression (using QRegularExpression). |
| 282 | * |
| 283 | * \note Unicode support is always enabled (by setting the QRegularExpression::UseUnicodePropertiesOption flag). |
| 284 | * |
| 285 | * \a text The string to search in |
| 286 | * |
| 287 | * \a pattern The pattern to search for |
| 288 | * |
| 289 | * \a index The index in \a text from which to start the search |
| 290 | * |
| 291 | * \a options The options to use |
| 292 | * |
| 293 | * \a matchedLength If there is a match, its length will be stored in this parameter |
| 294 | * |
| 295 | * \a rmatch If there is a regular expression match (implies that the KFind::RegularExpression |
| 296 | * flag is set) and \a rmatch is not a nullptr the match result will be stored |
| 297 | * in this QRegularExpressionMatch object |
| 298 | * |
| 299 | * Returns The index at which a match was found otherwise -1 |
| 300 | * |
| 301 | * \since 5.70 |
| 302 | */ |
| 303 | static int find(const QString &text, const QString &pattern, int index, long options, int *matchedLength, QRegularExpressionMatch *rmatch); |
| 304 | |
| 305 | /*! |
| 306 | * Displays the final dialog saying "no match was found", if that was the case. |
| 307 | * Call either this or shouldRestart(). |
| 308 | */ |
| 309 | virtual void displayFinalDialog() const; |
| 310 | |
| 311 | /*! |
| 312 | * Returns (or creates if \a create is true) the dialog that shows the "find next?" prompt. |
| 313 | * Usually you don't need to call this. |
| 314 | * One case where it can be useful, is when the user selects the "Find" |
| 315 | * menu item while a find operation is under way. In that case, the |
| 316 | * program may want to call setActiveWindow() on that dialog. |
| 317 | */ |
| 318 | QDialog *findNextDialog(bool create = false); |
| 319 | |
| 320 | /*! |
| 321 | * Close the "find next?" dialog. The application should do this when |
| 322 | * the last match was hit. If the application deletes the KFind, then |
| 323 | * "find previous" won't be possible anymore. |
| 324 | * |
| 325 | * IMPORTANT: you should also call this if you are using a non-modal |
| 326 | * find dialog, to tell KFind not to pop up its own dialog. |
| 327 | */ |
| 328 | void closeFindNextDialog(); |
| 329 | |
| 330 | /*! |
| 331 | * Returns the current matching index (or -1). |
| 332 | * Same as the matchingIndex parameter passed to the textFound() signal. |
| 333 | * You usually don't need to use this, except maybe when updating the current data, |
| 334 | * so you need to call setData(newData, index()). |
| 335 | */ |
| 336 | int index() const; |
| 337 | |
| 338 | Q_SIGNALS: |
| 339 | /*! |
| 340 | * Connect to this signal to implement highlighting of found text during the find |
| 341 | * operation. |
| 342 | * |
| 343 | * If you've set data with setData(id, text), use the textFoundAtId(int, int, int) signal. |
| 344 | * |
| 345 | * WARNING: If you're using the FindIncremental option, the text argument |
| 346 | * passed by this signal is not necessarily the data last set through |
| 347 | * setData(), but can also be an earlier set data block. |
| 348 | * |
| 349 | * \a text is the text that was found |
| 350 | * |
| 351 | * \a matchingIndex is the index of the start of the matched text |
| 352 | * |
| 353 | * \a matchedLength is the length of the matched text |
| 354 | * |
| 355 | * \sa setData() |
| 356 | * |
| 357 | * \since 5.81 |
| 358 | */ |
| 359 | void textFound(const QString &text, int matchingIndex, int matchedLength); |
| 360 | |
| 361 | /*! |
| 362 | * Connect to this signal to implement highlighting of found text during |
| 363 | * the find operation. |
| 364 | * |
| 365 | * Use this signal if you've set your data with setData(id, text), |
| 366 | * otherwise use the textFound(text, matchingIndex, matchedLength) signal. |
| 367 | * |
| 368 | * WARNING: If you're using the FindIncremental option, the \a id |
| 369 | * passed by this signal is not necessarily the id of the data last set |
| 370 | * through setData(), but can also be of an earlier set data block. |
| 371 | * |
| 372 | * \a matchingIndex is the index of the start of the matched text |
| 373 | * |
| 374 | * \a matchedLength is the length of the matched text |
| 375 | * |
| 376 | * \sa setData() |
| 377 | * |
| 378 | * \since 5.81 |
| 379 | */ |
| 380 | void textFoundAtId(int id, int matchingIndex, int matchedLength); |
| 381 | |
| 382 | // ## TODO docu |
| 383 | // findprevious will also emit findNext, after temporarily switching the value |
| 384 | // of FindBackwards |
| 385 | void findNext(); |
| 386 | |
| 387 | /*! |
| 388 | * Emitted when the options have changed. |
| 389 | * This can happen e.g. with "Replace All", or if our 'find next' dialog |
| 390 | * gets a "find previous" one day. |
| 391 | */ |
| 392 | void optionsChanged(); |
| 393 | |
| 394 | /*! |
| 395 | * Emitted when the 'find next' dialog is being closed. |
| 396 | * Some apps might want to remove the highlighted text when this happens. |
| 397 | * Apps without support for "Find Next" can also do m_find->deleteLater() |
| 398 | * to terminate the find operation. |
| 399 | */ |
| 400 | void dialogClosed(); |
| 401 | |
| 402 | protected: |
| 403 | QWidget *parentWidget() const; |
| 404 | QWidget *dialogsParent() const; |
| 405 | |
| 406 | protected: |
| 407 | KTEXTWIDGETS_NO_EXPORT KFind(KFindPrivate &dd, const QString &pattern, long options, QWidget *parent); |
| 408 | KTEXTWIDGETS_NO_EXPORT KFind(KFindPrivate &dd, const QString &pattern, long options, QWidget *parent, QWidget *findDialog); |
| 409 | |
| 410 | protected: |
| 411 | std::unique_ptr<class KFindPrivate> const d_ptr; |
| 412 | |
| 413 | private: |
| 414 | Q_DECLARE_PRIVATE(KFind) |
| 415 | }; |
| 416 | |
| 417 | Q_DECLARE_OPERATORS_FOR_FLAGS(KFind::SearchOptions) |
| 418 | |
| 419 | #endif |
| 420 | |