krbookmarkhandler.cpp 31.1 KB
Newer Older
Fathi Boudra's avatar
Fathi Boudra committed
1 2 3
/*****************************************************************************
 * Copyright (C) 2002 Shie Erlich <erlich@users.sourceforge.net>             *
 * Copyright (C) 2002 Rafi Yanai <yanai@users.sourceforge.net>               *
Davide Gianforte's avatar
Davide Gianforte committed
4
 * Copyright (C) 2004-2020 Krusader Krew [https://krusader.org]              *
Fathi Boudra's avatar
Fathi Boudra committed
5
 *                                                                           *
6 7 8
 * This file is part of Krusader [https://krusader.org].                     *
 *                                                                           *
 * Krusader is free software: you can redistribute it and/or modify          *
Fathi Boudra's avatar
Fathi Boudra committed
9
 * it under the terms of the GNU General Public License as published by      *
10
 * the Free Software Foundation, either version 2 of the License, or         *
Fathi Boudra's avatar
Fathi Boudra committed
11 12
 * (at your option) any later version.                                       *
 *                                                                           *
13
 * Krusader is distributed in the hope that it will be useful,               *
Fathi Boudra's avatar
Fathi Boudra committed
14 15 16 17 18
 * but WITHOUT ANY WARRANTY; without even the implied warranty of            *
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the             *
 * GNU General Public License for more details.                              *
 *                                                                           *
 * You should have received a copy of the GNU General Public License         *
19
 * along with Krusader.  If not, see [http://www.gnu.org/licenses/].         *
Fathi Boudra's avatar
Fathi Boudra committed
20 21
 *****************************************************************************/

Shie Erlich's avatar
Shie Erlich committed
22
#include "krbookmarkhandler.h"
Shie Erlich's avatar
Shie Erlich committed
23
#include "kraddbookmarkdlg.h"
24 25

#include "../krglobal.h"
26
#include "../icon.h"
Shie Erlich's avatar
Shie Erlich committed
27
#include "../krslots.h"
28
#include "../kractions.h"
29
#include "../krmainwindow.h"
30
#include "../Dialogs/popularurls.h"
31
#include "../FileSystem/filesystem.h"
32
#include "../Panel/krpanel.h"
33
#include "../Panel/listpanelactions.h"
34

35 36 37 38 39
// QtCore
#include <QTextStream>
#include <QFile>
#include <QEvent>
#include <QStandardPaths>
40
#include <QDebug>
41
#include <QTimer>
42 43 44
// QtGui
#include <QMouseEvent>
#include <QCursor>
Shie Erlich's avatar
Shie Erlich committed
45

Simon Persson's avatar
Simon Persson committed
46 47
#include <KConfigCore/KSharedConfig>
#include <KI18n/KLocalizedString>
48 49 50
#include <KWidgetsAddons/KMessageBox>
#include <KXmlGui/KActionCollection>
#include <KBookmarks/KBookmarkManager>
51
#include <utility>
52

Fathi Boudra's avatar
Fathi Boudra committed
53
#define SPECIAL_BOOKMARKS true
Shie Erlich's avatar
Shie Erlich committed
54

Shie Erlich's avatar
Shie Erlich committed
55
// ------------------------ for internal use
Fathi Boudra's avatar
Fathi Boudra committed
56
#define BOOKMARKS_FILE "krusader/krbookmarks.xml"
57
#define CONNECT_BM(X) { disconnect(X, SIGNAL(activated(QUrl)), 0, 0); connect(X, SIGNAL(activated(QUrl)), this, SLOT(slotActivated(QUrl))); }
Fathi Boudra's avatar
Fathi Boudra committed
58

59 60 61 62
KrBookmarkHandler::KrBookmarkHandler(KrMainWindow *mainWindow) :
    QObject(mainWindow->widget()),
    _mainWindow(mainWindow),
    _middleClick(false),
63
    _mainBookmarkPopup(nullptr),
64 65 66
    _quickSearchAction(nullptr),
    _quickSearchBar(nullptr),
    _quickSearchMenu(nullptr)
Fathi Boudra's avatar
Fathi Boudra committed
67 68
{
    // create our own action collection and make the shortcuts apply only to parent
69 70
    _privateCollection = new KActionCollection(this);
    _collection = _mainWindow->actions();
Fathi Boudra's avatar
Fathi Boudra committed
71 72 73

    // create _root: father of all bookmarks. it is a dummy bookmark and never shown
    _root = new KrBookmark(i18n("Bookmarks"));
Simon Persson's avatar
Simon Persson committed
74
    _root->setParent(this);
Fathi Boudra's avatar
Fathi Boudra committed
75 76 77 78

    // load bookmarks
    importFromFile();

79
    // create bookmark manager
80 81
    QString filename = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + BOOKMARKS_FILE;
    manager = KBookmarkManager::managerForFile(filename, QStringLiteral("krusader"));
82
    connect(manager, &KBookmarkManager::changed, this, &KrBookmarkHandler::bookmarksChanged);
83

84
    // create the quick search bar and action
85
    _quickSearchAction = new QWidgetAction(this);
86
    _quickSearchBar = new QLineEdit();
87
    _quickSearchBar->setPlaceholderText(i18n("Type to search..."));
88
    _quickSearchAction->setDefaultWidget(_quickSearchBar);  // ownership of the bar is transferred to the action
89
    _quickSearchAction->setEnabled(false);
90
    _setQuickSearchText("");
91

92 93 94 95
    // 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();
Shie Erlich's avatar
Shie Erlich committed
96 97
}

Fathi Boudra's avatar
Fathi Boudra committed
98 99 100 101
KrBookmarkHandler::~KrBookmarkHandler()
{
    delete manager;
    delete _privateCollection;
102 103
}

104
void KrBookmarkHandler::bookmarkCurrent(QUrl url)
Fathi Boudra's avatar
Fathi Boudra committed
105
{
106
    QPointer<KrAddBookmarkDlg> dlg = new KrAddBookmarkDlg(_mainWindow->widget(), std::move(url));
Simon Persson's avatar
Simon Persson committed
107
    if (dlg->exec() == QDialog::Accepted) {
Fathi Boudra's avatar
Fathi Boudra committed
108 109
        KrBookmark *bm = new KrBookmark(dlg->name(), dlg->url(), _collection);
        addBookmark(bm, dlg->folder());
Fathi Boudra's avatar
Fathi Boudra committed
110
    }
Fathi Boudra's avatar
Fathi Boudra committed
111
    delete dlg;
Shie Erlich's avatar
Shie Erlich committed
112 113
}

Fathi Boudra's avatar
Fathi Boudra committed
114 115
void KrBookmarkHandler::addBookmark(KrBookmark *bm, KrBookmark *folder)
{
116
    if (folder == nullptr)
Fathi Boudra's avatar
Fathi Boudra committed
117
        folder = _root;
Shie Erlich's avatar
Shie Erlich committed
118

Fathi Boudra's avatar
Fathi Boudra committed
119 120 121 122
    // add to the list (bottom)
    folder->children().append(bm);

    exportToFile();
Shie Erlich's avatar
Shie Erlich committed
123 124
}

Fathi Boudra's avatar
Fathi Boudra committed
125 126 127 128 129 130 131 132 133 134
void KrBookmarkHandler::deleteBookmark(KrBookmark *bm)
{
    if (bm->isFolder())
        clearBookmarks(bm);   // remove the child bookmarks
    removeReferences(_root, bm);
    foreach(QWidget *w, bm->associatedWidgets())
    w->removeAction(bm);
    delete bm;

    exportToFile();
135 136
}

Fathi Boudra's avatar
Fathi Boudra committed
137 138 139 140 141 142 143 144 145 146 147 148
void KrBookmarkHandler::removeReferences(KrBookmark *root, KrBookmark *bmToRemove)
{
    int index = root->children().indexOf(bmToRemove);
    if (index >= 0)
        root->children().removeAt(index);

    QListIterator<KrBookmark *> it(root->children());
    while (it.hasNext()) {
        KrBookmark *bm = it.next();
        if (bm->isFolder())
            removeReferences(bm, bmToRemove);
    }
Shie Erlich's avatar
Shie Erlich committed
149 150
}

Fathi Boudra's avatar
Fathi Boudra committed
151 152 153 154 155 156 157 158
void KrBookmarkHandler::exportToFileBookmark(QDomDocument &doc, QDomElement &where, KrBookmark *bm)
{
    if (bm->isSeparator()) {
        QDomElement bookmark = doc.createElement("separator");
        where.appendChild(bookmark);
    } else {
        QDomElement bookmark = doc.createElement("bookmark");
        // url
159
        bookmark.setAttribute("href", bm->url().toDisplayString());
Fathi Boudra's avatar
Fathi Boudra committed
160 161 162 163 164 165 166 167 168
        // icon
        bookmark.setAttribute("icon", bm->iconName());
        // title
        QDomElement title = doc.createElement("title");
        title.appendChild(doc.createTextNode(bm->text()));
        bookmark.appendChild(title);

        where.appendChild(bookmark);
    }
Shie Erlich's avatar
Shie Erlich committed
169 170
}

Fathi Boudra's avatar
Fathi Boudra committed
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
void KrBookmarkHandler::exportToFileFolder(QDomDocument &doc, QDomElement &parent, KrBookmark *folder)
{
    QListIterator<KrBookmark *> it(folder->children());
    while (it.hasNext()) {
        KrBookmark *bm = it.next();

        if (bm->isFolder()) {
            QDomElement newFolder = doc.createElement("folder");
            newFolder.setAttribute("icon", bm->iconName());
            parent.appendChild(newFolder);
            QDomElement title = doc.createElement("title");
            title.appendChild(doc.createTextNode(bm->text()));
            newFolder.appendChild(title);
            exportToFileFolder(doc, newFolder, bm);
        } else {
            exportToFileBookmark(doc, parent, bm);
        }
    }
189 190
}

Shie Erlich's avatar
Shie Erlich committed
191 192 193
// export to file using the xbel standard
//
//  <xbel>
194
//    <bookmark href="https://techbase.kde.org/"><title>Developer Web Site</title></bookmark>
Shie Erlich's avatar
Shie Erlich committed
195 196
//    <folder folded="no">
//      <title>Title of this folder</title>
197
//      <bookmark icon="kde" href="https://www.kde.org"><title>KDE Web Site</title></bookmark>
Shie Erlich's avatar
Shie Erlich committed
198 199
//      <folder toolbar="yes">
//        <title>My own bookmarks</title>
200
//        <bookmark href="https://www.calligra.org/"><title>Calligra Suite Web Site</title></bookmark>
Shie Erlich's avatar
Shie Erlich committed
201
//        <separator/>
202
//        <bookmark href="https://www.kdevelop.org/"><title>KDevelop Web Site</title></bookmark>
Shie Erlich's avatar
Shie Erlich committed
203 204 205
//      </folder>
//    </folder>
//  </xbel>
Fathi Boudra's avatar
Fathi Boudra committed
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
void KrBookmarkHandler::exportToFile()
{
    QDomDocument doc("xbel");
    QDomElement root = doc.createElement("xbel");
    doc.appendChild(root);

    exportToFileFolder(doc, root, _root);
    if (!doc.firstChild().isProcessingInstruction()) {
        // adding: <?xml version="1.0" encoding="UTF-8" ?> if not already present
        QDomProcessingInstruction instr = doc.createProcessingInstruction("xml",
                                          "version=\"1.0\" encoding=\"UTF-8\" ");
        doc.insertBefore(instr, doc.firstChild());
    }


221
    QString filename = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + BOOKMARKS_FILE;
Fathi Boudra's avatar
Fathi Boudra committed
222 223 224 225 226 227 228
    QFile file(filename);
    if (file.open(QIODevice::WriteOnly)) {
        QTextStream stream(&file);
        stream.setCodec("UTF-8");
        stream << doc.toString();
        file.close();
    } else {
229
        KMessageBox::error(_mainWindow->widget(), i18n("Unable to write to %1", filename), i18n("Error"));
Fathi Boudra's avatar
Fathi Boudra committed
230
    }
Shie Erlich's avatar
Shie Erlich committed
231 232
}

233
bool KrBookmarkHandler::importFromFileBookmark(QDomElement &e, KrBookmark *parent, const QString& path, QString *errorMsg)
Fathi Boudra's avatar
Fathi Boudra committed
234
{
235
    QString url, name, iconName;
Fathi Boudra's avatar
Fathi Boudra committed
236 237
    // verify tag
    if (e.tagName() != "bookmark") {
Pino Toscano's avatar
Pino Toscano committed
238
        *errorMsg = i18n("%1 instead of %2", e.tagName(), QLatin1String("bookmark"));
Fathi Boudra's avatar
Fathi Boudra committed
239 240 241 242
        return false;
    }
    // verify href
    if (!e.hasAttribute("href")) {
Pino Toscano's avatar
Pino Toscano committed
243
        *errorMsg = i18n("missing tag %1", QLatin1String("href"));
Fathi Boudra's avatar
Fathi Boudra committed
244 245 246 247 248
        return false;
    } else url = e.attribute("href");
    // verify title
    QDomElement te = e.firstChild().toElement();
    if (te.tagName() != "title") {
Pino Toscano's avatar
Pino Toscano committed
249
        *errorMsg = i18n("missing tag %1", QLatin1String("title"));
Fathi Boudra's avatar
Fathi Boudra committed
250 251 252 253
        return false;
    } else name = te.text();
    // do we have an icon?
    if (e.hasAttribute("icon")) {
254
        iconName = e.attribute("icon");
Fathi Boudra's avatar
Fathi Boudra committed
255 256 257 258
    }
    // ok: got name and url, let's add a bookmark
    KrBookmark *bm = KrBookmark::getExistingBookmark(path + name, _collection);
    if (!bm) {
259 260 261 262
        bm = new KrBookmark(name, QUrl(url), _collection, iconName, path + name);
    } else {
        bm->setURL(QUrl(url));
        bm->setIconName(iconName);
Fathi Boudra's avatar
Fathi Boudra committed
263
    }
264
    parent->children().append(bm);
Fathi Boudra's avatar
Fathi Boudra committed
265 266

    return true;
Shie Erlich's avatar
Shie Erlich committed
267 268
}

269
bool KrBookmarkHandler::importFromFileFolder(QDomNode &first, KrBookmark *parent, const QString& path, QString *errorMsg)
Fathi Boudra's avatar
Fathi Boudra committed
270 271 272 273 274 275 276 277 278 279 280 281 282 283
{
    QString name;
    QDomNode n = first;
    while (!n.isNull()) {
        QDomElement e = n.toElement();
        if (e.tagName() == "bookmark") {
            if (!importFromFileBookmark(e, parent, path, errorMsg))
                return false;
        } else if (e.tagName() == "folder") {
            QString iconName = "";
            if (e.hasAttribute("icon")) iconName = e.attribute("icon");
            // the title is the first child of the folder
            QDomElement tmp = e.firstChild().toElement();
            if (tmp.tagName() != "title") {
Pino Toscano's avatar
Pino Toscano committed
284
                *errorMsg = i18n("missing tag %1", QLatin1String("title"));
Fathi Boudra's avatar
Fathi Boudra committed
285 286 287 288 289 290 291 292 293 294 295 296 297 298
                return false;
            } else name = tmp.text();
            KrBookmark *folder = new KrBookmark(name, iconName);
            parent->children().append(folder);

            QDomNode nextOne = tmp.nextSibling();
            if (!importFromFileFolder(nextOne, folder, path + name + '/', errorMsg))
                return false;
        } else if (e.tagName() == "separator") {
            parent->children().append(KrBookmark::separator());
        }
        n = n.nextSibling();
    }
    return true;
Shie Erlich's avatar
Shie Erlich committed
299 300
}

Shie Erlich's avatar
Shie Erlich committed
301

Fathi Boudra's avatar
Fathi Boudra committed
302 303
void KrBookmarkHandler::importFromFile()
{
304
    clearBookmarks(_root, false);
Fathi Boudra's avatar
Fathi Boudra committed
305

306
    QString filename = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + BOOKMARKS_FILE;
Fathi Boudra's avatar
Fathi Boudra committed
307 308 309 310 311 312 313 314 315
    QFile file(filename);
    if (!file.open(QIODevice::ReadOnly))
        return; // no bookmarks file

    QString errorMsg;
    QDomNode n;
    QDomElement e;
    QDomDocument doc("xbel");
    if (!doc.setContent(&file, &errorMsg)) {
316
        goto BM_ERROR;
Fathi Boudra's avatar
Fathi Boudra committed
317 318 319 320 321 322 323
    }
    // iterate through the document: first child should be "xbel" (skip all until we find it)
    n = doc.firstChild();
    while (!n.isNull() && n.toElement().tagName() != "xbel")
        n = n.nextSibling();

    if (n.isNull() || n.toElement().tagName() != "xbel") {
Pino Toscano's avatar
Pino Toscano committed
324
        errorMsg = i18n("%1 does not seem to be a valid bookmarks file", filename);
325
        goto BM_ERROR;
Fathi Boudra's avatar
Fathi Boudra committed
326 327
    } else n = n.firstChild(); // skip the xbel part
    importFromFileFolder(n, _root, "", &errorMsg);
328
    goto BM_SUCCESS;
Fathi Boudra's avatar
Fathi Boudra committed
329

330
BM_ERROR:
331
    KMessageBox::error(_mainWindow->widget(), i18n("Error reading bookmarks file: %1", errorMsg), i18n("Error"));
Shie Erlich's avatar
Shie Erlich committed
332

333
BM_SUCCESS:
Fathi Boudra's avatar
Fathi Boudra committed
334
    file.close();
Shie Erlich's avatar
Shie Erlich committed
335 336
}

337 338
void KrBookmarkHandler::_setQuickSearchText(const QString &text)
{
339 340
    bool isEmptyQuickSearchBarVisible = KConfigGroup(krConfig, "Look&Feel").readEntry("Always show search bar", true);

341 342 343
    _quickSearchBar->setText(text);

    auto length = text.length();
344 345 346 347
    bool isVisible = isEmptyQuickSearchBarVisible || length > 0;
    _quickSearchAction->setVisible(isVisible);
    _quickSearchBar->setVisible(isVisible);

348
    if (length == 0) {
349 350 351 352
        qDebug() << "Bookmark search: reset";
        _resetActionTextAndHighlighting();
    } else {
        qDebug() << "Bookmark search: query =" << text;
353 354 355 356 357 358 359 360
    }
}

QString KrBookmarkHandler::_quickSearchText() const
{
    return _quickSearchBar->text();
}

361 362 363 364 365 366 367
void KrBookmarkHandler::_highlightAction(QAction *action, bool isMatched)
{
    auto font = action->font();
    font.setBold(isMatched);
    action->setFont(font);
}

Simon Persson's avatar
Simon Persson committed
368
void KrBookmarkHandler::populate(QMenu *menu)
Fathi Boudra's avatar
Fathi Boudra committed
369
{
370 371
    // removing action from previous menu is necessary
    // otherwise it won't be displayed in the currently populating menu
372 373 374
    if (_mainBookmarkPopup) {
        _mainBookmarkPopup->removeAction(_quickSearchAction);
    }
Fathi Boudra's avatar
Fathi Boudra committed
375 376 377 378
    _mainBookmarkPopup = menu;
    menu->clear();
    _specialBookmarks.clear();
    buildMenu(_root, menu);
Shie Erlich's avatar
Shie Erlich committed
379 380
}

381
void KrBookmarkHandler::buildMenu(KrBookmark *parent, QMenu *menu, int depth)
Fathi Boudra's avatar
Fathi Boudra committed
382
{
383 384 385 386
    // add search bar widget to the top of the menu
    if (depth == 0) {
        menu->addAction(_quickSearchAction);
    }
Fathi Boudra's avatar
Fathi Boudra committed
387 388 389 390 391 392 393 394

    // 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
    QListIterator<KrBookmark *> it(parent->children());
    while (it.hasNext()) {
        KrBookmark *bm = it.next();

        if (!bm->isFolder()) continue;
395
        auto *newMenu = new QMenu(menu);
396
        newMenu->setIcon(Icon(bm->iconName()));
Fathi Boudra's avatar
Fathi Boudra committed
397 398 399 400 401 402
        newMenu->setTitle(bm->text());
        QAction *menuAction = menu->addMenu(newMenu);
        QVariant v;
        v.setValue<KrBookmark *>(bm);
        menuAction->setData(v);

403
        buildMenu(bm, newMenu, depth + 1);
Fathi Boudra's avatar
Fathi Boudra committed
404 405 406 407 408 409 410 411 412 413
    }

    it.toFront();
    while (it.hasNext()) {
        KrBookmark *bm = it.next();
        if (bm->isFolder()) continue;
        if (bm->isSeparator()) {
            menu->addSeparator();
            continue;
        }
414 415 416 417 418 419 420 421 422 423

        QUrl urlToSet = bm->url();
        if (!urlToSet.isEmpty() && urlToSet.isRelative()) {
            // Make it possible that the url can be used later by Krusader.
            // This avoids users seeing the effects described in the "Editing a local path in Bookmark
            // Manager breaks a bookmark" bug report (https://bugs.kde.org/show_bug.cgi?id=393320),
            // though it would be better to solve that upstream frameworks-kbookmarks bug
            bm->setURL(QUrl::fromUserInput(urlToSet.toString(), QString(), QUrl::AssumeLocalFile));
        }

Fathi Boudra's avatar
Fathi Boudra committed
424 425 426 427
        menu->addAction(bm);
        CONNECT_BM(bm);
    }

428
    if (depth == 0) {
Fathi Boudra's avatar
Fathi Boudra committed
429 430 431 432 433 434 435 436 437 438 439
        KConfigGroup group(krConfig, "Private");
        bool hasPopularURLs = group.readEntry("BM Popular URLs", true);
        bool hasTrash       = group.readEntry("BM Trash",        true);
        bool hasLan         = group.readEntry("BM Lan",          true);
        bool hasVirtualFS   = group.readEntry("BM Virtual FS",   true);
        bool hasJumpback    = group.readEntry("BM Jumpback",     true);

        if (hasPopularURLs) {
            menu->addSeparator();

            // add the popular links submenu
440
            auto *newMenu = new QMenu(menu);
Fathi Boudra's avatar
Fathi Boudra committed
441
            newMenu->setTitle(i18n("Popular URLs"));
442
            newMenu->setIcon(Icon("folder-bookmark"));
Fathi Boudra's avatar
Fathi Boudra committed
443 444 445 446
            QAction *bmfAct  = menu->addMenu(newMenu);
            _specialBookmarks.append(bmfAct);
            // add the top 15 urls
#define MAX 15
447 448
            QList<QUrl> list = _mainWindow->popularUrls()->getMostPopularUrls(MAX);
            QList<QUrl>::Iterator it;
Fathi Boudra's avatar
Fathi Boudra committed
449 450 451
            for (it = list.begin(); it != list.end(); ++it) {
                QString name;
                if ((*it).isLocalFile()) name = (*it).path();
452
                else name = (*it).toDisplayString();
Fathi Boudra's avatar
Fathi Boudra committed
453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468
                // note: these bookmark are put into the private collection
                // as to not spam the general collection
                KrBookmark *bm = KrBookmark::getExistingBookmark(name, _privateCollection);
                if (!bm)
                    bm = new KrBookmark(name, *it, _privateCollection);
                newMenu->addAction(bm);
                CONNECT_BM(bm);
            }

            newMenu->addSeparator();
            newMenu->addAction(krPopularUrls);
            newMenu->installEventFilter(this);
        }

        // do we need to add special bookmarks?
        if (SPECIAL_BOOKMARKS) {
469
            if (hasTrash || hasLan || hasVirtualFS)
Fathi Boudra's avatar
Fathi Boudra committed
470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497
                menu->addSeparator();

            KrBookmark *bm;

            // note: special bookmarks are not kept inside the _bookmarks list and added ad-hoc
            if (hasTrash) {
                bm = KrBookmark::trash(_collection);
                menu->addAction(bm);
                _specialBookmarks.append(bm);
                CONNECT_BM(bm);
            }

            if (hasLan) {
                bm = KrBookmark::lan(_collection);
                menu->addAction(bm);
                _specialBookmarks.append(bm);
                CONNECT_BM(bm);
            }

            if (hasVirtualFS) {
                bm = KrBookmark::virt(_collection);
                menu->addAction(bm);
                _specialBookmarks.append(bm);
                CONNECT_BM(bm);
            }

            if (hasJumpback) {
                menu->addSeparator();
498

499
                ListPanelActions *actions = _mainWindow->listPanelActions();
500

501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520
                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);
Fathi Boudra's avatar
Fathi Boudra committed
521 522 523
            }
        }

524
        menu->addSeparator();
525 526
        menu->addAction(KrActions::actAddBookmark);
        _specialBookmarks.append(KrActions::actAddBookmark);
527
        QAction *bmAct = menu->addAction(Icon("bookmarks"),
Fathi Boudra's avatar
Fathi Boudra committed
528 529 530 531
                                         i18n("Manage Bookmarks"), manager, SLOT(slotEditBookmarks()));
        _specialBookmarks.append(bmAct);

        // make sure the menu is connected to us
532
        disconnect(menu, SIGNAL(triggered(QAction*)), nullptr, nullptr);
Fathi Boudra's avatar
Fathi Boudra committed
533 534 535
    }

    menu->installEventFilter(this);
Shie Erlich's avatar
Shie Erlich committed
536
}
Shie Erlich's avatar
Shie Erlich committed
537

538
void KrBookmarkHandler::clearBookmarks(KrBookmark *root, bool removeBookmarks)
Fathi Boudra's avatar
Fathi Boudra committed
539
{
540
    for (auto it = root->children().begin(); it != root->children().end(); it = root->children().erase(it)) {
Fathi Boudra's avatar
Fathi Boudra committed
541 542
        KrBookmark *bm = *it;

543 544 545 546 547 548 549 550 551
        if (bm->isFolder()) {
            clearBookmarks(bm, removeBookmarks);
            delete bm;
        } else if (bm->isSeparator()) {
            delete bm;
        } else if (removeBookmarks) {
            foreach (QWidget *w, bm->associatedWidgets()) {
                w->removeAction(bm);
            }
Fathi Boudra's avatar
Fathi Boudra committed
552 553 554
            delete bm;
        }
    }
Shie Erlich's avatar
Shie Erlich committed
555 556
}

Fathi Boudra's avatar
Fathi Boudra committed
557 558 559
void KrBookmarkHandler::bookmarksChanged(const QString&, const QString&)
{
    importFromFile();
Shie Erlich's avatar
Shie Erlich committed
560
}
561

Fathi Boudra's avatar
Fathi Boudra committed
562 563
bool KrBookmarkHandler::eventFilter(QObject *obj, QEvent *ev)
{
564
    auto eventType = ev->type();
565
    auto *menu = qobject_cast<QMenu *>(obj);
566 567 568 569 570

    if (eventType == QEvent::Show && menu) {
        _setQuickSearchText("");
        _quickSearchMenu = menu;
        qDebug() << "Bookmark search: menu" << menu << "is shown";
571 572

        return QObject::eventFilter(obj, ev);
573 574
    }

575 576 577 578 579
    if (eventType == QEvent::Close && menu && _quickSearchMenu) {
        if (_quickSearchMenu == menu) {
            qDebug() << "Bookmark search: stopped on menu" << menu;
            _setQuickSearchText("");
            _quickSearchMenu = nullptr;
580
        } else {
581 582 583 584 585 586 587 588 589 590 591 592 593
            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();
                }
            });
        }
594 595

        return QObject::eventFilter(obj, ev);
596 597 598
    }

    // Having it occur on keypress is consistent with other shortcuts,
599
    // such as Ctrl+W and accelerator keys
600
    if (eventType == QEvent::KeyPress && menu) {
601
        auto *kev = dynamic_cast<QKeyEvent *>(ev);
Alexander Lohnau's avatar
Alexander Lohnau committed
602
        const QList<QAction *> acts = menu->actions();
603
        bool quickSearchStarted = false;
604
        bool searchInSpecialItems = KConfigGroup(krConfig, "Look&Feel").readEntry("Search in special items", false);
605

606 607 608 609 610
        if (kev->key() == Qt::Key_Left && kev->modifiers() == Qt::NoModifier) {
            menu->close();
            return true;
        }

611 612 613 614 615
        if ((kev->modifiers() != Qt::ShiftModifier &&
             kev->modifiers() != Qt::NoModifier) ||
            kev->text().isEmpty()                ||
            kev->key() == Qt::Key_Delete         ||
            kev->key() == Qt::Key_Return         ||
616
            kev->key() == Qt::Key_Escape) {
617 618 619
            return QObject::eventFilter(obj, ev);
        }

620
        // update quick search text
621
        if (kev->key() == Qt::Key_Backspace) {
622 623 624 625 626
            auto newSearchText = _quickSearchText();
            newSearchText.chop(1);
            _setQuickSearchText(newSearchText);

            if (_quickSearchText().length() == 0) {
627 628 629
                return QObject::eventFilter(obj, ev);
            }
        } else {
630
            quickSearchStarted = _quickSearchText().length() == 0;
631
            _setQuickSearchText(_quickSearchText().append(kev->text()));
632 633
        }

634
        if (quickSearchStarted) {
635 636
            _quickSearchMenu = menu;
            qDebug() << "Bookmark search: started on menu" << menu;
637 638 639
        }

        // match actions
640
        QAction *matchedAction = nullptr;
641
        int nMatches = 0;
642 643
        const Qt::CaseSensitivity matchCase =
            _quickSearchText() == _quickSearchText().toLower() ? Qt::CaseInsensitive : Qt::CaseSensitive;
644
        for (auto act : acts) {
Yuri Chornoivan's avatar
Yuri Chornoivan committed
645
            if (act->isSeparator() || act->text().isEmpty()) {
646 647 648
                continue;
            }

649 650 651 652
            if (!searchInSpecialItems && _specialBookmarks.contains(act)) {
                continue;
            }

653 654
            if (quickSearchStarted) {
                // if the first key press is an accelerator key, let the accelerator handler process this event
655
                if (act->text().contains('&' + kev->text(), Qt::CaseInsensitive)) {
656
                    qDebug() << "Bookmark search: hit accelerator key of" << act;
657
                    _setQuickSearchText("");
658
                    return QObject::eventFilter(obj, ev);
659 660
                }

661 662 663 664
                // 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));
665 666
            }

667
            // match prefix of the action text to the query
668
            if (act->text().left(_quickSearchText().length()).compare(_quickSearchText(), matchCase) == 0) {
669
                _highlightAction(act);
670 671 672
                if (!matchedAction || matchedAction->menu()) {
                    // Can't highlight menus (see comment below), hopefully pick something we can
                    matchedAction = act;
673
                }
674 675 676
                nMatches++;
            } else {
                _highlightAction(act, false);
677 678 679
            }
        }

680 681
        if (matchedAction) {
            qDebug() << "Bookmark search: primary match =" << matchedAction->text() << ", number of matches =" << nMatches;
682 683 684 685 686 687 688
        } else {
            qDebug() << "Bookmark search: no matches";
        }

        // trigger the matched menu item or set an active item accordingly
        if (nMatches == 1) {
            _setQuickSearchText("");
689 690
            if ((bool) matchedAction->menu()) {
                menu->setActiveAction(matchedAction);
691
            } else {
692 693 694 695 696 697 698 699 700
                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);
701
            }
702
        } else {
703
            menu->setActiveAction(nullptr);
704
        }
705
        return true;
706 707
    }

708
    if (eventType == QEvent::MouseButtonRelease) {
709
        switch (dynamic_cast<QMouseEvent *>(ev)->button()) {
Fathi Boudra's avatar
Fathi Boudra committed
710 711 712
        case Qt::RightButton:
            _middleClick = false;
            if (obj->inherits("QMenu")) {
713 714
                auto *menu = dynamic_cast<QMenu *>(obj);
                QAction *act = menu->actionAt(dynamic_cast<QMouseEvent *>(ev)->pos());
Fathi Boudra's avatar
Fathi Boudra committed
715 716 717 718 719 720

                if (obj == _mainBookmarkPopup && _specialBookmarks.contains(act)) {
                    rightClickOnSpecialBookmark();
                    return true;
                }

721
                auto *bm = qobject_cast<KrBookmark *>(act);
722
                if (bm != nullptr) {
Fathi Boudra's avatar
Fathi Boudra committed
723 724 725
                    rightClicked(menu, bm);
                    return true;
                } else if (act && act->data().canConvert<KrBookmark *>()) {
726
                    auto *bm = act->data().value<KrBookmark *>();
Fathi Boudra's avatar
Fathi Boudra committed
727 728 729
                    rightClicked(menu, bm);
                }
            }
730
            break;
Fathi Boudra's avatar
Fathi Boudra committed
731 732 733 734 735 736 737 738 739 740 741
        case Qt::LeftButton:
            _middleClick = false;
            break;
        case Qt::MidButton:
            _middleClick = true;
            break;
        default:
            break;
        }
    }
    return QObject::eventFilter(obj, ev);
742 743
}

744
void KrBookmarkHandler::_resetActionTextAndHighlighting()
745
{
746 747
    for (QHash<QAction *, QString>::const_iterator i = _quickSearchOriginalActionTitles.constBegin();
         i != _quickSearchOriginalActionTitles.constEnd(); ++i) {
748 749 750
        QAction *action = i.key();
        action->setText(i.value());
        _highlightAction(action, false);
751 752
    }

753
    _quickSearchOriginalActionTitles.clear();
754 755
}

756
#define POPULAR_URLS_ID        100100
757
#define TRASH_ID               100101
758 759 760 761
#define LAN_ID                 100103
#define VIRTUAL_FS_ID          100102
#define JUMP_BACK_ID           100104

Fathi Boudra's avatar
Fathi Boudra committed
762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831
void KrBookmarkHandler::rightClickOnSpecialBookmark()
{
    KConfigGroup group(krConfig, "Private");
    bool hasPopularURLs = group.readEntry("BM Popular URLs", true);
    bool hasTrash       = group.readEntry("BM Trash",      true);
    bool hasLan         = group.readEntry("BM Lan",          true);
    bool hasVirtualFS   = group.readEntry("BM Virtual FS",   true);
    bool hasJumpback    = group.readEntry("BM Jumpback",     true);

    QMenu menu(_mainBookmarkPopup);
    menu.setTitle(i18n("Enable special bookmarks"));

    QAction *act;

    act = menu.addAction(i18n("Popular URLs"));
    act->setData(QVariant(POPULAR_URLS_ID));
    act->setCheckable(true);
    act->setChecked(hasPopularURLs);
    act = menu.addAction(i18n("Trash bin"));
    act->setData(QVariant(TRASH_ID));
    act->setCheckable(true);
    act->setChecked(hasTrash);
    act = menu.addAction(i18n("Local Network"));
    act->setData(QVariant(LAN_ID));
    act->setCheckable(true);
    act->setChecked(hasLan);
    act = menu.addAction(i18n("Virtual Filesystem"));
    act->setData(QVariant(VIRTUAL_FS_ID));
    act->setCheckable(true);
    act->setChecked(hasVirtualFS);
    act = menu.addAction(i18n("Jump back"));
    act->setData(QVariant(JUMP_BACK_ID));
    act->setCheckable(true);
    act->setChecked(hasJumpback);

    connect(_mainBookmarkPopup, SIGNAL(highlighted(int)), &menu, SLOT(close()));
    connect(_mainBookmarkPopup, SIGNAL(activated(int)), &menu, SLOT(close()));

    int result = -1;
    QAction *res = menu.exec(QCursor::pos());
    if (res && res->data().canConvert<int>())
        result = res->data().toInt();

    bool doCloseMain = true;

    switch (result) {
    case POPULAR_URLS_ID:
        group.writeEntry("BM Popular URLs", !hasPopularURLs);
        break;
    case TRASH_ID:
        group.writeEntry("BM Trash", !hasTrash);
        break;
    case LAN_ID:
        group.writeEntry("BM Lan", !hasLan);
        break;
    case VIRTUAL_FS_ID:
        group.writeEntry("BM Virtual FS", !hasVirtualFS);
        break;
    case JUMP_BACK_ID:
        group.writeEntry("BM Jumpback", !hasJumpback);
        break;
    default:
        doCloseMain = false;
        break;
    }

    menu.close();

    if (doCloseMain && _mainBookmarkPopup)
        _mainBookmarkPopup->close();
832 833 834 835 836 837
}

#define OPEN_ID           100200
#define OPEN_NEW_TAB_ID   100201
#define DELETE_ID         100202

Fathi Boudra's avatar
Fathi Boudra committed
838 839 840 841 842 843
void KrBookmarkHandler::rightClicked(QMenu *menu, KrBookmark * bm)
{
    QMenu popup(_mainBookmarkPopup);
    QAction * act;

    if (!bm->isFolder()) {
844
        act = popup.addAction(Icon("document-open"), i18n("Open"));
Fathi Boudra's avatar
Fathi Boudra committed
845
        act->setData(QVariant(OPEN_ID));
846
        act = popup.addAction(Icon("tab-new"), i18n("Open in a new tab"));
Fathi Boudra's avatar
Fathi Boudra committed
847 848 849
        act->setData(QVariant(OPEN_NEW_TAB_ID));
        popup.addSeparator();
    }
850
    act = popup.addAction(Icon("edit-delete"), i18n("Delete"));
Fathi Boudra's avatar
Fathi Boudra committed
851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870
    act->setData(QVariant(DELETE_ID));

    connect(menu, SIGNAL(highlighted(int)), &popup, SLOT(close()));
    connect(menu, SIGNAL(activated(int)), &popup, SLOT(close()));

    int result = -1;
    QAction *res = popup.exec(QCursor::pos());
    if (res && res->data().canConvert<int> ())
        result = res->data().toInt();

    popup.close();
    if (_mainBookmarkPopup && result >= OPEN_ID && result <= DELETE_ID) {
        _mainBookmarkPopup->close();
    }

    switch (result) {
    case OPEN_ID:
        SLOTS->refresh(bm->url());
        break;
    case OPEN_NEW_TAB_ID:
871
        _mainWindow->activeManager()->newTab(bm->url());
Fathi Boudra's avatar
Fathi Boudra committed
872 873 874 875 876
        break;
    case DELETE_ID:
        deleteBookmark(bm);
        break;
    }
877 878
}

879 880
// used to monitor middle clicks. if mid is found, then the
// bookmark is opened in a new tab. ugly, but easier than overloading
881
// KAction and KActionCollection.
882
void KrBookmarkHandler::slotActivated(const QUrl &url)
Fathi Boudra's avatar
Fathi Boudra committed
883 884 885 886
{
    if (_mainBookmarkPopup && !_mainBookmarkPopup->isHidden())
        _mainBookmarkPopup->close();
    if (_middleClick)
887 888 889
        _mainWindow->activeManager()->newTab(url);
    else
        SLOTS->refresh(url);
890 891
}