X-Git-Url: https://git.lyx.org/gitweb/?a=blobdiff_plain;ds=sidebyside;f=src%2Ffrontends%2Fqt%2FGuiSearch.cpp;h=3ca4f4904d649bacfcf1cf44cb80791275a733b9;hb=cc257b4927f8833836a6f6caab6f91c5022063db;hp=3fffb6ea2b7a2b19aa04b699d8f6e16c6c350180;hpb=27ba830b8ef513c8332e15747797844a97807791;p=lyx.git diff --git a/src/frontends/qt/GuiSearch.cpp b/src/frontends/qt/GuiSearch.cpp index 3fffb6ea2b..3ca4f4904d 100644 --- a/src/frontends/qt/GuiSearch.cpp +++ b/src/frontends/qt/GuiSearch.cpp @@ -12,22 +12,42 @@ #include +#include "GuiApplication.h" +#include "GuiSearch.h" + #include "lyxfind.h" #include "qt_helpers.h" #include "FuncRequest.h" +#include "LyX.h" #include "BufferView.h" #include "Buffer.h" #include "Cursor.h" -#include "GuiSearch.h" +#include "FuncRequest.h" +#include "KeyMap.h" +#include "GuiKeySymbol.h" #include "GuiView.h" +#include "qt_helpers.h" +#include "support/filetools.h" +#include "support/debug.h" #include "support/gettext.h" +#include "support/FileName.h" #include "frontends/alert.h" +#include "frontends/Clipboard.h" +#include +#include #include +#include +#include #include +#include "QSizePolicy" +#include using namespace std; +using namespace lyx::support; + +using lyx::KeySymbol; namespace lyx { namespace frontend { @@ -42,8 +62,8 @@ static void uniqueInsert(QComboBox * box, QString const & text) } -GuiSearch::GuiSearch(GuiView & lv) - : GuiDialog(lv, "findreplace", qt_("Find and Replace")) +GuiSearchWidget::GuiSearchWidget(QWidget * parent, GuiView & view) + : QWidget(parent), view_(view) { setupUi(this); @@ -51,106 +71,606 @@ GuiSearch::GuiSearch(GuiView & lv) setFixedHeight(sizeHint().height()); // align items in grid on top - mainGridLayout->setAlignment(Qt::AlignTop); + gridLayout->setAlignment(Qt::AlignTop); - connect(buttonBox, SIGNAL(clicked(QAbstractButton *)), - this, SLOT(slotButtonBox(QAbstractButton *))); connect(findPB, SIGNAL(clicked()), this, SLOT(findClicked())); + connect(findPrevPB, SIGNAL(clicked()), this, SLOT(findPrevClicked())); + connect(minimizePB, SIGNAL(clicked()), this, SLOT(minimizeClicked())); connect(replacePB, SIGNAL(clicked()), this, SLOT(replaceClicked())); + connect(replacePrevPB, SIGNAL(clicked()), this, SLOT(replacePrevClicked())); connect(replaceallPB, SIGNAL(clicked()), this, SLOT(replaceallClicked())); + connect(instantSearchCB, SIGNAL(clicked()), this, SLOT(immediateClicked())); connect(findCO, SIGNAL(editTextChanged(QString)), this, SLOT(findChanged())); + if(qApp->clipboard()->supportsFindBuffer()) { + connect(qApp->clipboard(), SIGNAL(findBufferChanged()), + this, SLOT(findBufferChanged())); + findBufferChanged(); + } setFocusProxy(findCO); - bc().setPolicy(ButtonPolicy::NoRepeatedApplyReadOnlyPolicy); - bc().setCancel(buttonBox->button(QDialogButtonBox::Close)); - bc().addReadOnly(replaceCO); - bc().addReadOnly(replacePB); - bc().addReadOnly(replaceallPB); - - findCO->setCompleter(0); - replaceCO->setCompleter(0); + // Use a FancyLineEdit due to the indicator icons + findLE_ = new FancyLineEdit(this); + findCO->setLineEdit(findLE_); + + // And a menu in minimal mode + menu_ = new QMenu(); + act_casesense_ = new QAction(qt_("&Case sensitive[[search]]"), this); + act_casesense_->setCheckable(true); + act_wholewords_ = new QAction(qt_("Wh&ole words"), this); + act_wholewords_->setCheckable(true); + act_selection_ = new QAction(qt_("Selection onl&y"), this); + act_selection_->setCheckable(true); + act_immediate_ = new QAction(qt_("Search as yo&u type"), this); + act_immediate_->setCheckable(true); + act_wrap_ = new QAction(qt_("&Wrap"), this); + act_wrap_->setCheckable(true); + + menu_->addAction(act_casesense_); + menu_->addAction(act_wholewords_); + menu_->addAction(act_selection_); + menu_->addAction(act_immediate_); + menu_->addAction(act_wrap_); + findLE_->setButtonMenu(FancyLineEdit::Right, menu_); + + connect(act_casesense_, SIGNAL(triggered()), this, SLOT(caseSenseActTriggered())); + connect(act_wholewords_, SIGNAL(triggered()), this, SLOT(wholeWordsActTriggered())); + connect(act_selection_, SIGNAL(triggered()), this, SLOT(searchSelActTriggered())); + connect(act_immediate_, SIGNAL(triggered()), this, SLOT(immediateActTriggered())); + connect(act_wrap_, SIGNAL(triggered()), this, SLOT(wrapActTriggered())); + + findCO->setCompleter(nullptr); + replaceCO->setCompleter(nullptr); replacePB->setEnabled(false); + replacePrevPB->setEnabled(false); replaceallPB->setEnabled(false); } -void GuiSearch::showEvent(QShowEvent * e) +bool GuiSearchWidget::initialiseParams(std::string const & str) { - findPB->setFocus(); + if (!str.empty()) { + // selectAll & insert rather than setText in order to keep undo stack + findCO->lineEdit()->selectAll(); + findCO->lineEdit()->insert(toqstr(str)); + } + findCO->setFocus(); findCO->lineEdit()->selectAll(); - GuiDialog::showEvent(e); + return true; +} + + +void GuiSearchWidget::keyPressEvent(QKeyEvent * ev) +{ + KeySymbol sym; + setKeySymbol(&sym, ev); + + // catch Return and Shift-Return + if (ev->key() == Qt::Key_Return || ev->key() == Qt::Key_Enter) { + doFind(ev->modifiers() == Qt::ShiftModifier); + return; + } + if (ev->key() == Qt::Key_Escape) { + Qt::KeyboardModifiers mod = ev->modifiers(); + if (mod & Qt::AltModifier) { + QWidget::keyPressEvent(ev); + return; + } + + dispatch(FuncRequest(LFUN_DIALOG_HIDE, "findreplace")); + view_.setFocus(); + bv_->buffer().updateBuffer(); + return; + } + + // we catch the key sequences for forward and backwards search + if (sym.isOK()) { + KeyModifier mod = lyx::q_key_state(ev->modifiers()); + KeySequence keyseq(&theTopLevelKeymap(), &theTopLevelKeymap()); + FuncRequest fr = keyseq.addkey(sym, mod); + if (fr == FuncRequest(LFUN_WORD_FIND_FORWARD) + || fr == FuncRequest(LFUN_WORD_FIND)) { + doFind(); + return; + } + if (fr == FuncRequest(LFUN_WORD_FIND_BACKWARD)) { + doFind(true); + return; + } + if (fr == FuncRequest(LFUN_DIALOG_TOGGLE, "findreplace")) { + dispatch(fr); + return; + } + } + QWidget::keyPressEvent(ev); } -void GuiSearch::findChanged() +void GuiSearchWidget::minimizeClicked(bool const toggle) { - if (findCO->currentText().isEmpty()) { - findPB->setEnabled(false); - replacePB->setEnabled(false); - replaceallPB->setEnabled(false); + if (toggle) + minimized_ = !minimized_; + + replaceLA->setHidden(minimized_); + replaceCO->setHidden(minimized_); + replacePB->setHidden(minimized_); + replacePrevPB->setHidden(minimized_); + replaceallPB->setHidden(minimized_); + CBFrame->setHidden(minimized_); + + if (minimized_) { + minimizePB->setText(qt_("Ex&pand")); + minimizePB->setToolTip(qt_("Show replace and option widgets")); + // update menu items + blockSignals(true); + act_casesense_->setChecked(caseCB->isChecked()); + act_immediate_->setChecked(instantSearchCB->isChecked()); + act_selection_->setChecked(selectionCB->isChecked()); + act_selection_->setEnabled(!instantSearchCB->isChecked()); + act_wholewords_->setChecked(wordsCB->isChecked()); + act_wrap_->setChecked(wrapCB->isChecked()); + blockSignals(false); } else { - findPB->setEnabled(true); - replacePB->setEnabled(!isBufferReadonly()); - replaceallPB->setEnabled(!isBufferReadonly()); + minimizePB->setText(qt_("&Minimize")); + minimizePB->setToolTip(qt_("Hide replace and option widgets")); } + + Q_EMIT needSizeUpdate(); + Q_EMIT needTitleBarUpdate(); + handleIndicators(); } -void GuiSearch::findClicked() +void GuiSearchWidget::handleIndicators() { - docstring const needle = qstring_to_ucs4(findCO->currentText()); - find(needle, caseCB->isChecked(), wordsCB->isChecked(), - !backwardsCB->isChecked()); - uniqueInsert(findCO, findCO->currentText()); - findCO->lineEdit()->selectAll(); + findLE_->setButtonVisible(FancyLineEdit::Right, minimized_); + + QString tip; + + if (minimized_) { + int pms = 0; + if (caseCB->isChecked()) + ++pms; + if (wordsCB->isChecked()) + ++pms; + if (selectionCB->isChecked()) + ++pms; + if (instantSearchCB->isChecked()) + ++pms; + if (wrapCB->isChecked()) + ++pms; + + bool const dark_mode = guiApp && guiApp->isInDarkMode(); + qreal dpr = 1.0; + // Consider device/pixel ratio (HiDPI) + if (guiApp && guiApp->currentView()) + dpr = guiApp->currentView()->devicePixelRatio(); + QString imagedir = "images/"; + QPixmap bpixmap = getPixmap("images/", "search-options", "svgz,png"); + QPixmap pm = bpixmap; + + if (pms > 0) { + int const gap = 3; + QPixmap scaled_pm = QPixmap(bpixmap.size() * dpr); + pm = QPixmap(pms * scaled_pm.width() + ((pms - 1) * gap), + scaled_pm.height()); + pm.fill(Qt::transparent); + QPainter painter(&pm); + int x = 0; + + tip = qt_("Active options:"); + tip += "
    "; + if (caseCB->isChecked()) { + tip += "
  • " + qt_("Case sensitive search"); + QPixmap spixmap = getPixmap("images/", "search-case-sensitive", "svgz,png"); + // We render SVG directly for HiDPI scalability + FileName fname = imageLibFileSearch(imagedir, "search-case-sensitive", "svgz,png"); + QString fpath = toqstr(fname.absFileName()); + if (!fpath.isEmpty()) { + QSvgRenderer svgRenderer(fpath); + if (svgRenderer.isValid()) + svgRenderer.render(&painter, QRectF(0, 0, spixmap.width() * dpr, + spixmap.height() * dpr)); + } + x += (spixmap.width() * dpr) + gap; + } + if (wordsCB->isChecked()) { + tip += "
  • " + qt_("Whole words only"); + QPixmap spixmap = getPixmap("images/", "search-whole-words", "svgz,png"); + FileName fname = imageLibFileSearch(imagedir, "search-whole-words", "svgz,png"); + QString fpath = toqstr(fname.absFileName()); + if (!fpath.isEmpty()) { + QSvgRenderer svgRenderer(fpath); + if (svgRenderer.isValid()) + svgRenderer.render(&painter, QRectF(x, 0, spixmap.width() * dpr, + spixmap.height() * dpr)); + } + x += (spixmap.width() * dpr) + gap; + } + if (selectionCB->isChecked()) { + tip += "
  • " + qt_("Search only in selection"); + QPixmap spixmap = getPixmap("images/", "search-selection", "svgz,png"); + FileName fname = imageLibFileSearch(imagedir, "search-selection", "svgz,png"); + QString fpath = toqstr(fname.absFileName()); + if (!fpath.isEmpty()) { + QSvgRenderer svgRenderer(fpath); + if (svgRenderer.isValid()) + svgRenderer.render(&painter, QRectF(x, 0, spixmap.width() * dpr, + spixmap.height() * dpr)); + } + x += (spixmap.width() * dpr) + gap; + } + if (instantSearchCB->isChecked()) { + tip += "
  • " + qt_("Search as you type"); + QPixmap spixmap = getPixmap("images/", "search-instant", "svgz,png"); + FileName fname = imageLibFileSearch(imagedir, "search-instant", "svgz,png"); + QString fpath = toqstr(fname.absFileName()); + if (!fpath.isEmpty()) { + QSvgRenderer svgRenderer(fpath); + if (svgRenderer.isValid()) + svgRenderer.render(&painter, QRectF(x, 0, spixmap.width() * dpr, + spixmap.height() * dpr)); + } + x += (spixmap.width() * dpr) + gap; + } + if (wrapCB->isChecked()) { + tip += "
  • " + qt_("Wrap search"); + QPixmap spixmap = getPixmap("images/", "search-wrap", "svgz,png"); + FileName fname = imageLibFileSearch(imagedir, "search-wrap", "svgz,png"); + QString fpath = toqstr(fname.absFileName()); + if (!fpath.isEmpty()) { + QSvgRenderer svgRenderer(fpath); + if (svgRenderer.isValid()) + svgRenderer.render(&painter, QRectF(x, 0, spixmap.width() * dpr, + spixmap.height() * dpr)); + } + x += (spixmap.width() * dpr) + gap; + } + tip += "
"; + pm.setDevicePixelRatio(dpr); + painter.end(); + } else { + tip = qt_("Click here to change search options"); + // We render SVG directly for HiDPI scalability + FileName fname = imageLibFileSearch(imagedir, "search-options", "svgz,png"); + QString fpath = toqstr(fname.absFileName()); + if (!fpath.isEmpty()) { + QSvgRenderer svgRenderer(fpath); + if (svgRenderer.isValid()) { + pm = QPixmap(bpixmap.size() * dpr); + pm.fill(Qt::transparent); + QPainter painter(&pm); + svgRenderer.render(&painter); + pm.setDevicePixelRatio(dpr); + } + } + } + if (dark_mode) { + QImage img = pm.toImage(); + img.invertPixels(); + pm.convertFromImage(img); + } + findLE_->setButtonPixmap(FancyLineEdit::Right, pm); + } + findLE_->setButtonToolTip(FancyLineEdit::Right, tip); } -void GuiSearch::replaceClicked() +void GuiSearchWidget::caseSenseActTriggered() { - docstring const needle = qstring_to_ucs4(findCO->currentText()); - docstring const repl = qstring_to_ucs4(replaceCO->currentText()); - replace(needle, repl, caseCB->isChecked(), wordsCB->isChecked(), - !backwardsCB->isChecked(), false); - uniqueInsert(findCO, findCO->currentText()); - uniqueInsert(replaceCO, replaceCO->currentText()); + caseCB->setChecked(act_casesense_->isChecked()); + handleIndicators(); +} + + +void GuiSearchWidget::wholeWordsActTriggered() +{ + wordsCB->setChecked(act_wholewords_->isChecked()); + handleIndicators(); +} + + +void GuiSearchWidget::searchSelActTriggered() +{ + selectionCB->setChecked(act_selection_->isChecked()); + handleIndicators(); +} + + +void GuiSearchWidget::immediateActTriggered() +{ + bool const immediate = act_immediate_->isChecked(); + instantSearchCB->setChecked(immediate); + // FIXME: make these two work together eventually. + selectionCB->setEnabled(!immediate); + act_selection_->setEnabled(!immediate); + if (immediate) { + selectionCB->setChecked(false); + act_selection_->setChecked(false); + } + handleIndicators(); +} + + +void GuiSearchWidget::immediateClicked() +{ + // FIXME: make these two work together eventually. + bool const immediate = instantSearchCB->isChecked(); + selectionCB->setEnabled(!immediate); + if (immediate) + selectionCB->setChecked(false); +} + + +void GuiSearchWidget::wrapActTriggered() +{ + wrapCB->setChecked(act_wrap_->isChecked()); + handleIndicators(); +} + + +void GuiSearchWidget::showEvent(QShowEvent * e) +{ + findChanged(); + QWidget::showEvent(e); +} + + +void GuiSearchWidget::hideEvent(QHideEvent *) +{ + dispatch(FuncRequest(LFUN_DIALOG_HIDE, "findreplace")); + view_.setFocus(); + // update toolbar status + if (bv_) + bv_->buffer().updateBuffer(); +} + + +void GuiSearchWidget::findBufferChanged() +{ + docstring search = theClipboard().getFindBuffer(); + // update from find buffer, but only if the strings differ (else we + // might end up in loops with search as you type) + if (!search.empty() && toqstr(search) != findCO->lineEdit()->text()) { + LYXERR(Debug::CLIPBOARD, "from findbuffer: " << search); + // selectAll & insert rather than setText in order to keep undo stack + findCO->lineEdit()->selectAll(); + findCO->lineEdit()->insert(toqstr(search)); + } +} + + +void GuiSearchWidget::findChanged() +{ + bool const emptytext = findCO->currentText().isEmpty(); + findPB->setEnabled(!emptytext); + findPrevPB->setEnabled(!emptytext); + bool const replace = !emptytext && bv_ && !bv_->buffer().isReadonly(); + replacePB->setEnabled(replace); + replacePrevPB->setEnabled(replace); + replaceallPB->setEnabled(replace); + if (instantSearchCB->isChecked()) + doFind(false, true); +} + + +void GuiSearchWidget::findClicked() +{ + doFind(); +} + + +void GuiSearchWidget::findPrevClicked() +{ + doFind(true); +} + + +void GuiSearchWidget::replaceClicked() +{ + doReplace(); } -void GuiSearch::replaceallClicked() +void GuiSearchWidget::replacePrevClicked() +{ + doReplace(true); +} + + +void GuiSearchWidget::replaceallClicked() { replace(qstring_to_ucs4(findCO->currentText()), qstring_to_ucs4(replaceCO->currentText()), - caseCB->isChecked(), wordsCB->isChecked(), true, true); + caseCB->isChecked(), wordsCB->isChecked(), + true, true, true, selectionCB->isChecked()); uniqueInsert(findCO, findCO->currentText()); uniqueInsert(replaceCO, replaceCO->currentText()); } -void GuiSearch::find(docstring const & search, bool casesensitive, - bool matchword, bool forward) +void GuiSearchWidget::doFind(bool const backwards, bool const instant) +{ + docstring const needle = qstring_to_ucs4(findCO->currentText()); + find(needle, caseCB->isChecked(), wordsCB->isChecked(), !backwards, + instant, wrapCB->isChecked(), selectionCB->isChecked()); + uniqueInsert(findCO, findCO->currentText()); + if (!instant) + findCO->lineEdit()->selectAll(); +} + + +void GuiSearchWidget::find(docstring const & search, bool casesensitive, + bool matchword, bool forward, bool instant, + bool wrap, bool onlysel) { docstring const sdata = - find2string(search, casesensitive, matchword, forward); + find2string(search, casesensitive, matchword, + forward, wrap, instant, onlysel); + dispatch(FuncRequest(LFUN_WORD_FIND, sdata)); } -void GuiSearch::replace(docstring const & search, docstring const & replace, +void GuiSearchWidget::doReplace(bool const backwards) +{ + docstring const needle = qstring_to_ucs4(findCO->currentText()); + docstring const repl = qstring_to_ucs4(replaceCO->currentText()); + replace(needle, repl, caseCB->isChecked(), wordsCB->isChecked(), + !backwards, false, wrapCB->isChecked(), selectionCB->isChecked()); + uniqueInsert(findCO, findCO->currentText()); + uniqueInsert(replaceCO, replaceCO->currentText()); +} + + +void GuiSearchWidget::replace(docstring const & search, docstring const & replace, bool casesensitive, bool matchword, - bool forward, bool all) + bool forward, bool all, bool wrap, bool onlysel) { docstring const sdata = replace2string(replace, search, casesensitive, - matchword, all, forward); + matchword, all, forward, true, wrap, onlysel); + dispatch(FuncRequest(LFUN_WORD_REPLACE, sdata)); } +void GuiSearchWidget::saveSession(QSettings & settings, QString const & session_key) const +{ + settings.setValue(session_key + "/casesensitive", caseCB->isChecked()); + settings.setValue(session_key + "/words", wordsCB->isChecked()); + settings.setValue(session_key + "/instant", instantSearchCB->isChecked()); + settings.setValue(session_key + "/wrap", wrapCB->isChecked()); + settings.setValue(session_key + "/selection", selectionCB->isChecked()); + settings.setValue(session_key + "/minimized", minimized_); +} + + +void GuiSearchWidget::restoreSession(QString const & session_key) +{ + QSettings settings; + caseCB->setChecked(settings.value(session_key + "/casesensitive", false).toBool()); + act_casesense_->setChecked(settings.value(session_key + "/casesensitive", false).toBool()); + wordsCB->setChecked(settings.value(session_key + "/words", false).toBool()); + act_wholewords_->setChecked(settings.value(session_key + "/words", false).toBool()); + bool const immediate = settings.value(session_key + "/instant", false).toBool(); + instantSearchCB->setChecked(immediate); + act_immediate_->setChecked(immediate); + wrapCB->setChecked(settings.value(session_key + "/wrap", true).toBool()); + act_wrap_->setChecked(settings.value(session_key + "/wrap", true).toBool()); + selectionCB->setChecked(settings.value(session_key + "/selection", false).toBool() && !immediate); + selectionCB->setEnabled(!immediate); + act_selection_->setChecked(settings.value(session_key + "/selection", false).toBool()); + minimized_ = settings.value(session_key + "/minimized", false).toBool(); + // initialize hidings + minimizeClicked(false); +} + + +GuiSearch::GuiSearch(GuiView & parent, Qt::DockWidgetArea area, Qt::WindowFlags flags) + : DockView(parent, "findreplace", qt_("Search and Replace"), area, flags), + widget_(new GuiSearchWidget(this, parent)) +{ + setWidget(widget_); + widget_->setBufferView(bufferview()); + setFocusProxy(widget_); + + connect(widget_, SIGNAL(needTitleBarUpdate()), this, SLOT(updateTitle())); + connect(widget_, SIGNAL(needSizeUpdate()), this, SLOT(updateSize())); +} + + +void GuiSearch::mousePressEvent(QMouseEvent * event) +{ + if (isFloating() && event->button() == Qt::LeftButton) { +#if QT_VERSION >= 0x060000 + dragPosition = event->globalPosition().toPoint() - frameGeometry().topLeft(); +#else + dragPosition = event->globalPos() - frameGeometry().topLeft(); +#endif + event->accept(); + } else + DockView::mousePressEvent(event); +} + + +void GuiSearch::mouseMoveEvent(QMouseEvent * event) +{ + if (isFloating() && event->buttons() & Qt::LeftButton) { +#if QT_VERSION >= 0x060000 + move(event->globalPosition().toPoint() - dragPosition); +#else + move(event->globalPos() - dragPosition); +#endif + event->accept(); + } else + DockView::mouseMoveEvent(event); +} + + +void GuiSearch::mouseDoubleClickEvent(QMouseEvent * event) +{ + if (event->button() == Qt::LeftButton) + setFloating(!isFloating()); + else + DockView::mouseDoubleClickEvent(event); +} + + +void GuiSearch::onBufferViewChanged() +{ + widget_->setEnabled(static_cast(bufferview())); + widget_->setBufferView(bufferview()); +} + + +void GuiSearch::updateView() +{ + updateTitle(); + updateSize(); +} + + +void GuiSearch::saveSession(QSettings & settings) const +{ + Dialog::saveSession(settings); + widget_->saveSession(settings, sessionKey()); +} + + +void GuiSearch::restoreSession() +{ + DockView::restoreSession(); + widget_->restoreSession(sessionKey()); +} -Dialog * createGuiSearch(GuiView & lv) { return new GuiSearch(lv); } + +void GuiSearch::updateTitle() +{ + if (widget_->isMinimized()) { + // remove title bar + setTitleBarWidget(new QWidget()); + titleBarWidget()->hide(); + } else if (titleBarWidget()) { + // restore title bar + setTitleBarWidget(nullptr); + } +} + + +void GuiSearch::updateSize() +{ + widget_->setFixedHeight(widget_->sizeHint().height()); + if (widget_->isMinimized()) + setFixedHeight(widget_->sizeHint().height()); + else { + // undo setFixedHeight + setMaximumHeight(QWIDGETSIZE_MAX); + setMinimumHeight(0); + } + update(); +} } // namespace frontend