diff --git a/doc/bookmarks.docbook b/doc/bookmarks.docbook --- a/doc/bookmarks.docbook +++ b/doc/bookmarks.docbook @@ -46,17 +46,6 @@ quickly connect to the remote machine, but you cannot bookmark inside an archive. -
Bookmark menu @@ -71,7 +60,8 @@ Using Bookmarks Usually, you click on the - BookMan II button when you are in the target + BookMan II button (the rightmost button to the right of + the address bar at the top of the active panel) when you are in the target folder. For example, to bookmark /usr/tmp, navigate &krusader; there and click the @@ -128,6 +118,12 @@ bookmark is executed. + + + The search bar will always be visible in the bookmarks menu if you check the + corresponding item on the Panel Konfigurator's page. + + Password handling @@ -152,7 +148,6 @@ The submenu Popular URLs - holds persistent the most popular visited &URL;s (local or remote). This submenu displays the top 15 popular URLs, sorted by ranking, so that the top &URL; is @@ -166,7 +161,6 @@ Open the Popular URLs - or use &Ctrl; Z @@ -253,15 +247,10 @@ The default system bookmarks are stored in the kfileplaces/bookmarks.xml file in the directory which can be determined using the qtpaths --paths GenericDataLocation command. - You can import bookmarks from - IE, - Opera, - Galeon, &kde;, - Mozilla, &Netscape;. + KeditBookmarks is easy to use, however, if you need more information, please read the - KeditBookmarks or the &konqueror; - handbook. + KeditBookmarks handbook.
<application>KeditBookmarks</application> Bookmark Manager diff --git a/doc/konfigurator.docbook b/doc/konfigurator.docbook --- a/doc/konfigurator.docbook +++ b/doc/konfigurator.docbook @@ -295,6 +295,21 @@ and Filter. You can change the mode later using the search bar itself. + + + <guilabel>Bookmark Search</guilabel> + + + + Always show search bar: If checked, + make bookmark search bar always visible. + + + + Search in special items: If checked, + bookmark search is also applied to special items in bookmark menu like Trash, Popular URLs, Jump Back, &etc; + + <guilabel>Status/Totalsbar</guilabel> diff --git a/krusader/BookMan/krbookmark.h b/krusader/BookMan/krbookmark.h --- a/krusader/BookMan/krbookmark.h +++ b/krusader/BookMan/krbookmark.h @@ -28,8 +28,9 @@ // QtWidgets #include <QAction> - class KActionCollection; +class ListPanelActions; + class KrBookmark: public QAction { @@ -59,12 +60,14 @@ return _children; } - static KrBookmark* getExistingBookmark(QString actionName, KActionCollection *collection); + static KrBookmark * getExistingBookmark(QString actionName, KActionCollection *collection); + // ----- special bookmarks - static KrBookmark* trash(KActionCollection *collection); - static KrBookmark* virt(KActionCollection *collection); - static KrBookmark* lan(KActionCollection *collection); - static KrBookmark* separator(); + static KrBookmark * trash(KActionCollection *collection); + static KrBookmark * virt(KActionCollection *collection); + static KrBookmark * lan(KActionCollection *collection); + static QAction * jumpBackAction(KActionCollection *collection, bool isSetter = false, ListPanelActions *sourceActions = 0); + static KrBookmark * separator(); signals: void activated(const QUrl &url); diff --git a/krusader/BookMan/krbookmark.cpp b/krusader/BookMan/krbookmark.cpp --- a/krusader/BookMan/krbookmark.cpp +++ b/krusader/BookMan/krbookmark.cpp @@ -23,6 +23,7 @@ #include "../krglobal.h" #include "../Archive/krarchandler.h" #include "../FileSystem/krtrashhandler.h" +#include "../Panel/listpanelactions.h" #include <KI18n/KLocalizedString> #include <KIconThemes/KIconLoader> @@ -73,12 +74,12 @@ } } -KrBookmark* KrBookmark::getExistingBookmark(QString actionName, KActionCollection *collection) +KrBookmark * KrBookmark::getExistingBookmark(QString actionName, KActionCollection *collection) { return static_cast<KrBookmark*>(collection->action(BM_NAME(actionName))); } -KrBookmark* KrBookmark::trash(KActionCollection *collection) +KrBookmark * KrBookmark::trash(KActionCollection *collection) { KrBookmark *bm = getExistingBookmark(i18n(NAME_TRASH), collection); if (!bm) @@ -88,7 +89,7 @@ return bm; } -KrBookmark* KrBookmark::virt(KActionCollection *collection) +KrBookmark * KrBookmark::virt(KActionCollection *collection) { KrBookmark *bm = getExistingBookmark(i18n(NAME_VIRTUAL), collection); if (!bm) { @@ -98,7 +99,7 @@ return bm; } -KrBookmark* KrBookmark::lan(KActionCollection *collection) +KrBookmark * KrBookmark::lan(KActionCollection *collection) { KrBookmark *bm = getExistingBookmark(i18n(NAME_LAN), collection); if (!bm) { @@ -108,7 +109,32 @@ return bm; } -KrBookmark* KrBookmark::separator() +QAction * KrBookmark::jumpBackAction(KActionCollection *collection, bool isSetter, ListPanelActions *sourceActions) +{ + auto actionName = isSetter ? QString("setJumpBack") : QString("jumpBack"); + auto action = collection->action(actionName); + if (action) { + return action; + } + + if (!sourceActions) { + return nullptr; + } + + // copy essential part of source action + auto sourceAction = isSetter ? sourceActions->actSetJumpBack : sourceActions->actJumpBack; + action = new QAction(sourceAction->icon(), sourceAction->text(), sourceAction); + action->setShortcut(sourceAction->shortcut()); + action->setShortcutContext(Qt::WidgetShortcut); + connect(action, &QAction::triggered, sourceAction, &QAction::trigger); + // ensure there are no accelerator keys coming from another menu + action->setText(KLocalizedString::removeAcceleratorMarker(action->text())); + + collection->addAction(actionName, action); + return action; +} + +KrBookmark * KrBookmark::separator() { KrBookmark *bm = new KrBookmark(""); bm->_separator = true; diff --git a/krusader/BookMan/krbookmarkbutton.cpp b/krusader/BookMan/krbookmarkbutton.cpp --- a/krusader/BookMan/krbookmarkbutton.cpp +++ b/krusader/BookMan/krbookmarkbutton.cpp @@ -43,14 +43,10 @@ acmBookmarks = new KActionMenu(QIcon::fromTheme("bookmarks"), i18n("Bookmarks"), this); acmBookmarks->setDelayed(false); - // TODO KF5 : explicit cast as QMenu doesn't have those methods - //(acmBookmarks->menu())->setKeyboardShortcutsEnabled(true); - //(acmBookmarks->menu())->setKeyboardShortcutsExecute(true); setMenu(acmBookmarks->menu()); connect(acmBookmarks->menu(), SIGNAL(aboutToShow()), this, SLOT(populate())); connect(acmBookmarks->menu(), SIGNAL(aboutToShow()), this, SIGNAL(aboutToShow())); - populate(); } void KrBookmarkButton::populate() diff --git a/krusader/BookMan/krbookmarkhandler.h b/krusader/BookMan/krbookmarkhandler.h --- a/krusader/BookMan/krbookmarkhandler.h +++ b/krusader/BookMan/krbookmarkhandler.h @@ -32,6 +32,8 @@ #include <QDomEntity> // QtWidgets #include <QMenu> +#include <QWidgetAction> +#include <QLineEdit> #include "krbookmark.h" @@ -60,10 +62,9 @@ void exportToFileFolder(QDomDocument &doc, QDomElement &parent, KrBookmark *folder); void exportToFileBookmark(QDomDocument &doc, QDomElement &where, KrBookmark *bm); void clearBookmarks(KrBookmark *root); - void buildMenu(KrBookmark *parent, QMenu *menu); + void buildMenu(KrBookmark *parent, QMenu *menu, int depth = 0); bool eventFilter(QObject *obj, QEvent *ev); - void rightClicked(QMenu *menu, KrBookmark *bm); void rightClickOnSpecialBookmark(); @@ -83,6 +84,16 @@ QPointer<QMenu> _mainBookmarkPopup; // main bookmark popup menu QList<QAction *> _specialBookmarks; // the action list of the special bookmarks + + QWidgetAction *_quickSearchAction; ///< Search bar container action + QLineEdit *_quickSearchBar; ///< Search bar containing current query + QMenu *_quickSearchMenu; ///< The menu where the search is performed + QHash<QAction *, QString> _quickSearchOriginalActionTitles; ///< Saved original action text values to restore after search + + void _setQuickSearchText(const QString &text); + QString _quickSearchText() const; + static void _highlightAction(QAction *action, bool isMatched = true); + void _resetActionTextAndHighlighting(); }; Q_DECLARE_METATYPE(KrBookmark *) diff --git a/krusader/BookMan/krbookmarkhandler.cpp b/krusader/BookMan/krbookmarkhandler.cpp --- a/krusader/BookMan/krbookmarkhandler.cpp +++ b/krusader/BookMan/krbookmarkhandler.cpp @@ -36,6 +36,8 @@ #include <QFile> #include <QEvent> #include <QStandardPaths> +#include <QDebug> +#include <QTimer> // QtGui #include <QMouseEvent> #include <QCursor> @@ -53,8 +55,15 @@ #define BOOKMARKS_FILE "krusader/krbookmarks.xml" #define CONNECT_BM(X) { disconnect(X, SIGNAL(activated(QUrl)), 0, 0); connect(X, SIGNAL(activated(QUrl)), this, SLOT(slotActivated(QUrl))); } -KrBookmarkHandler::KrBookmarkHandler(KrMainWindow *mainWindow) : QObject(mainWindow->widget()), - _mainWindow(mainWindow), _middleClick(false), _mainBookmarkPopup(0), _specialBookmarks() +KrBookmarkHandler::KrBookmarkHandler(KrMainWindow *mainWindow) : + QObject(mainWindow->widget()), + _mainWindow(mainWindow), + _middleClick(false), + _mainBookmarkPopup(0), + _specialBookmarks(), + _quickSearchAction(nullptr), + _quickSearchBar(nullptr), + _quickSearchMenu(nullptr) { // create our own action collection and make the shortcuts apply only to parent _privateCollection = new KActionCollection(this); @@ -67,10 +76,23 @@ // load bookmarks importFromFile(); - // hack + // create bookmark manager QString filename = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + BOOKMARKS_FILE; manager = KBookmarkManager::managerForFile(filename, QStringLiteral("krusader")); connect(manager, SIGNAL(changed(QString,QString)), this, SLOT(bookmarksChanged(QString,QString))); + + // create the quick search bar and action + _quickSearchAction = new QWidgetAction(this); + _quickSearchBar = new QLineEdit(); + _quickSearchBar->setPlaceholderText(i18n("Type to search...")); + _quickSearchAction->setDefaultWidget(_quickSearchBar); // ownership of the bar is transferred to the action + _quickSearchAction->setEnabled(false); + _setQuickSearchText(""); + + // fill a dummy menu to properly init actions (allows toolbar bookmark buttons to work properly) + auto menu = new QMenu(mainWindow->widget()); + populate(menu); + menu->deleteLater(); } KrBookmarkHandler::~KrBookmarkHandler() @@ -309,17 +331,56 @@ file.close(); } +void KrBookmarkHandler::_setQuickSearchText(const QString &text) +{ + bool isEmptyQuickSearchBarVisible = KConfigGroup(krConfig, "Look&Feel").readEntry("Always show search bar", true); + + _quickSearchBar->setText(text); + + auto length = text.length(); + bool isVisible = isEmptyQuickSearchBarVisible || length > 0; + _quickSearchAction->setVisible(isVisible); + _quickSearchBar->setVisible(isVisible); + + if (length == 0) { + qDebug() << "Bookmark search: reset"; + _resetActionTextAndHighlighting(); + } else { + qDebug() << "Bookmark search: query =" << text; + } +} + +QString KrBookmarkHandler::_quickSearchText() const +{ + return _quickSearchBar->text(); +} + +void KrBookmarkHandler::_highlightAction(QAction *action, bool isMatched) +{ + auto font = action->font(); + font.setBold(isMatched); + action->setFont(font); +} + void KrBookmarkHandler::populate(QMenu *menu) { + // removing action from previous menu is necessary + // otherwise it won't be displayed in the currently populating menu + if (_mainBookmarkPopup) { + _mainBookmarkPopup->removeAction(_quickSearchAction); + } _mainBookmarkPopup = menu; menu->clear(); _specialBookmarks.clear(); buildMenu(_root, menu); } -void KrBookmarkHandler::buildMenu(KrBookmark *parent, QMenu *menu) +void KrBookmarkHandler::buildMenu(KrBookmark *parent, QMenu *menu, int depth) { - static int inSecondaryMenu = 0; // used to know if we're on the top menu + // add search bar widget to the top of the menu + if (depth == 0) { + menu->addAction(_quickSearchAction); + } // run the loop twice, in order to put the folders on top. stupid but easy :-) // note: this code drops the separators put there by the user @@ -336,9 +397,7 @@ v.setValue<KrBookmark *>(bm); menuAction->setData(v); - ++inSecondaryMenu; - buildMenu(bm, newMenu); - --inSecondaryMenu; + buildMenu(bm, newMenu, depth + 1); } it.toFront(); @@ -353,7 +412,7 @@ CONNECT_BM(bm); } - if (!inSecondaryMenu) { + if (depth == 0) { KConfigGroup group(krConfig, "Private"); bool hasPopularURLs = group.readEntry("BM Popular URLs", true); bool hasTrash = group.readEntry("BM Trash", true); @@ -394,7 +453,7 @@ // do we need to add special bookmarks? if (SPECIAL_BOOKMARKS) { - if (hasTrash || hasLan || hasVirtualFS || hasJumpback) + if (hasTrash || hasLan || hasVirtualFS) menu->addSeparator(); KrBookmark *bm; @@ -422,19 +481,34 @@ } if (hasJumpback) { - // add the jump-back button - ListPanelActions *actions = _mainWindow->listPanelActions(); - menu->addAction(actions->actJumpBack); - _specialBookmarks.append(actions->actJumpBack); menu->addSeparator(); - menu->addAction(actions->actSetJumpBack); - _specialBookmarks.append(actions->actSetJumpBack); + + ListPanelActions *actions = _mainWindow->listPanelActions(); + + auto slotTriggered = [=] { + if (_mainBookmarkPopup && !_mainBookmarkPopup->isHidden()) { + _mainBookmarkPopup->close(); + } + }; + auto addJumpBackAction = [=](bool isSetter) { + auto action = KrBookmark::jumpBackAction(_privateCollection, isSetter, actions); + if (action) { + menu->addAction(action); + _specialBookmarks.append(action); + + // disconnecting from this as a receiver is important: + // we don't want to break connections established by KrBookmark::jumpBackAction + disconnect(action, &QAction::triggered, this, nullptr); + connect(action, &QAction::triggered, this, slotTriggered); + } + }; + + addJumpBackAction(true); + addJumpBackAction(false); } } - if (!hasJumpback) - menu->addSeparator(); - + menu->addSeparator(); menu->addAction(KrActions::actAddBookmark); _specialBookmarks.append(KrActions::actAddBookmark); QAction *bmAct = menu->addAction(krLoader->loadIcon("bookmarks", KIconLoader::Small), @@ -473,7 +547,151 @@ bool KrBookmarkHandler::eventFilter(QObject *obj, QEvent *ev) { - if (ev->type() == QEvent::MouseButtonRelease) { + auto eventType = ev->type(); + QMenu *menu = dynamic_cast<QMenu *>(obj); + + if (eventType == QEvent::Show && menu) { + _setQuickSearchText(""); + _quickSearchMenu = menu; + qDebug() << "Bookmark search: menu" << menu << "is shown"; + + return QObject::eventFilter(obj, ev); + } + + if (eventType == QEvent::Close && menu && _quickSearchMenu) { + if (_quickSearchMenu == menu) { + qDebug() << "Bookmark search: stopped on menu" << menu; + _setQuickSearchText(""); + _quickSearchMenu = nullptr; + } else { + qDebug() << "Bookmark search: active action =" << _quickSearchMenu->activeAction(); + + // fix automatic deactivation of current action due to spurious close event from submenu + auto quickSearchMenu = _quickSearchMenu; + auto activeAction = _quickSearchMenu->activeAction(); + QTimer::singleShot(0, this, [=]() { + qDebug() << "Bookmark search: active action =" << quickSearchMenu->activeAction(); + if (!quickSearchMenu->activeAction() && activeAction) { + quickSearchMenu->setActiveAction(activeAction); + qDebug() << "Bookmark search: restored active action =" << quickSearchMenu->activeAction(); + } + }); + } + + return QObject::eventFilter(obj, ev); + } + + // Having it occur on keypress is consistent with other shortcuts, + // such as Ctrl+W and accelerator keys + if (eventType == QEvent::KeyPress && menu) { + QKeyEvent *kev = static_cast<QKeyEvent *>(ev); + QList<QAction *> acts = menu->actions(); + bool quickSearchStarted = false; + bool searchInSpecialItems = KConfigGroup(krConfig, "Look&Feel").readEntry("Search in special items", false); + + if (kev->key() == Qt::Key_Left && kev->modifiers() == Qt::NoModifier) { + menu->close(); + return true; + } + + if ((kev->modifiers() != Qt::ShiftModifier && + kev->modifiers() != Qt::NoModifier) || + kev->text().isEmpty() || + kev->key() == Qt::Key_Delete || + kev->key() == Qt::Key_Return || + kev->key() == Qt::Key_Escape) { + return QObject::eventFilter(obj, ev); + } + + // update quick search text + if (kev->key() == Qt::Key_Backspace) { + auto newSearchText = _quickSearchText(); + newSearchText.chop(1); + _setQuickSearchText(newSearchText); + + if (_quickSearchText().length() == 0) { + return QObject::eventFilter(obj, ev); + } + } else { + quickSearchStarted = _quickSearchText().length() == 0; + _setQuickSearchText(_quickSearchText().append(kev->text())); + } + + if (quickSearchStarted) { + _quickSearchMenu = menu; + qDebug() << "Bookmark search: started on menu" << menu; + } + + // match actions + QAction *matchedAction = nullptr; + int nMatches = 0; + const Qt::CaseSensitivity matchCase = + _quickSearchText() == _quickSearchText().toLower() ? Qt::CaseInsensitive : Qt::CaseSensitive; + for (auto act : acts) { + if (act->isSeparator() || act->text() == "") { + continue; + } + + if (!searchInSpecialItems && _specialBookmarks.contains(act)) { + continue; + } + + if (quickSearchStarted) { + // if the first key press is an accelerator key, let the accelerator handler process this event + if (act->text().contains('&' + kev->text(), Qt::CaseInsensitive)) { + qDebug() << "Bookmark search: hit accelerator key of" << act; + _setQuickSearchText(""); + return QObject::eventFilter(obj, ev); + } + + // strip accelerator keys from actions so they don't interfere with the search key press events + auto text = act->text(); + _quickSearchOriginalActionTitles.insert(act, text); + act->setText(KLocalizedString::removeAcceleratorMarker(text)); + } + + // match prefix of the action text to the query + if (act->text().left(_quickSearchText().length()).compare(_quickSearchText(), matchCase) == 0) { + _highlightAction(act); + if (!matchedAction || matchedAction->menu()) { + // Can't highlight menus (see comment below), hopefully pick something we can + matchedAction = act; + } + nMatches++; + } else { + _highlightAction(act, false); + } + } + + if (matchedAction) { + qDebug() << "Bookmark search: primary match =" << matchedAction->text() << ", number of matches =" << nMatches; + } else { + qDebug() << "Bookmark search: no matches"; + } + + // trigger the matched menu item or set an active item accordingly + if (nMatches == 1) { + _setQuickSearchText(""); + if ((bool) matchedAction->menu()) { + menu->setActiveAction(matchedAction); + } else { + matchedAction->activate(QAction::Trigger); + } + } else if (nMatches > 1) { + // Because of a bug submenus cannot be highlighted + // https://bugreports.qt.io/browse/QTBUG-939 + if (!matchedAction->menu()) { + menu->setActiveAction(matchedAction); + } else { + menu->setActiveAction(nullptr); + } + } else { + menu->setActiveAction(nullptr); + } + return true; + } + + if (eventType == QEvent::MouseButtonRelease) { switch (static_cast<QMouseEvent *>(ev)->button()) { case Qt::RightButton: _middleClick = false; @@ -509,6 +727,18 @@ return QObject::eventFilter(obj, ev); } +void KrBookmarkHandler::_resetActionTextAndHighlighting() +{ + for (QHash<QAction *, QString>::const_iterator i = _quickSearchOriginalActionTitles.begin(); + i != _quickSearchOriginalActionTitles.end(); ++i) { + QAction *action = i.key(); + action->setText(i.value()); + _highlightAction(action, false); + } + + _quickSearchOriginalActionTitles.clear(); +} + #define POPULAR_URLS_ID 100100 #define TRASH_ID 100101 #define LAN_ID 100103 diff --git a/krusader/Konfigurator/kgpanel.cpp b/krusader/Konfigurator/kgpanel.cpp --- a/krusader/Konfigurator/kgpanel.cpp +++ b/krusader/Konfigurator/kgpanel.cpp @@ -198,6 +198,22 @@ gridLayout->addLayout(hbox, 1, 1); layout->addWidget(groupBox); +// -------------------------------------------------------------------------------------------- +// ------------------------------- Bookmark search settings ---------------------------------- +// -------------------------------------------------------------------------------------------- + groupBox = createFrame(i18n("Bookmark Search"), tab); + gridLayout = createGridLayout(groupBox); + + KONFIGURATOR_CHECKBOX_PARAM bookmarkSearchSettings[] = + { + {"Look&Feel", "Always show search bar", true, i18n("Always show search bar"), false, i18n("Make bookmark search bar always visible") }, + {"Look&Feel", "Search in special items", false, i18n("Search in special items"), false, i18n("Bookmark search is also applied to special items in bookmark menu like Trash, Popular URLs, Jump Back, etc.") }, + }; + KonfiguratorCheckBoxGroup *bookmarkSearchSettingsGroup = createCheckBoxGroup(2, 0, bookmarkSearchSettings, + 2 /*count*/, groupBox, PAGE_GENERAL); + gridLayout->addWidget(bookmarkSearchSettingsGroup, 1, 0, 1, 2); + layout->addWidget(groupBox); + // -------------------------------------------------------------------------------------------- // ------------------------------- Status/Totalsbar settings ---------------------------------- // -------------------------------------------------------------------------------------------- diff --git a/krusader/krusader.cpp b/krusader/krusader.cpp --- a/krusader/krusader.cpp +++ b/krusader/krusader.cpp @@ -145,14 +145,15 @@ KrGlobal::mountMan = new KMountMan(this); connect(KrGlobal::mountMan, SIGNAL(refreshPanel(QUrl)), SLOTS, SLOT(refresh(QUrl))); + // create popular URLs container + _popularUrls = new PopularUrls(this); + // create bookman krBookMan = new KrBookmarkHandler(this); // create job manager krJobMan = new JobMan(this); - _popularUrls = new PopularUrls(this); - // create the main view MAIN_VIEW = new KrusaderView(this);