File indexing completed on 2024-04-21 03:55:25

0001 /*
0002     SPDX-FileCopyrightText: 2006-2010 Peter Penz <peter.penz@gmx.at>
0003     SPDX-FileCopyrightText: 2006 Aaron J. Seigo <aseigo@kde.org>
0004     SPDX-FileCopyrightText: 2007 Kevin Ottens <ervin@kde.org>
0005     SPDX-FileCopyrightText: 2007 Urs Wolfer <uwolfer @ kde.org>
0006 
0007     SPDX-License-Identifier: LGPL-2.0-or-later
0008 */
0009 
0010 #include "kurlnavigator.h"
0011 #include "kcoreurlnavigator.h"
0012 
0013 #include "../utils_p.h"
0014 #include "kurlnavigatorbutton_p.h"
0015 #include "kurlnavigatordropdownbutton_p.h"
0016 #include "kurlnavigatorpathselectoreventfilter_p.h"
0017 #include "kurlnavigatorplacesselector_p.h"
0018 #include "kurlnavigatorschemecombo_p.h"
0019 #include "kurlnavigatortogglebutton_p.h"
0020 
0021 #include <KIO/StatJob>
0022 #include <KLocalizedString>
0023 #include <kfileitem.h>
0024 #include <kfileplacesmodel.h>
0025 #include <kprotocolinfo.h>
0026 #include <kurifilter.h>
0027 #include <kurlcombobox.h>
0028 #include <kurlcompletion.h>
0029 
0030 #include <QActionGroup>
0031 #include <QApplication>
0032 #include <QClipboard>
0033 #include <QDir>
0034 #include <QDropEvent>
0035 #include <QHBoxLayout>
0036 #include <QKeyEvent>
0037 #include <QMenu>
0038 #include <QMetaMethod>
0039 #include <QMimeData>
0040 #include <QMimeDatabase>
0041 #include <QTimer>
0042 #include <QUrlQuery>
0043 
0044 #include <algorithm>
0045 #include <numeric>
0046 
0047 using namespace KDEPrivate;
0048 
0049 struct KUrlNavigatorData {
0050     QByteArray state;
0051 };
0052 Q_DECLARE_METATYPE(KUrlNavigatorData)
0053 
0054 class KUrlNavigatorPrivate
0055 {
0056 public:
0057     KUrlNavigatorPrivate(const QUrl &url, KUrlNavigator *qq, KFilePlacesModel *placesModel);
0058 
0059     ~KUrlNavigatorPrivate()
0060     {
0061         m_dropDownButton->removeEventFilter(q);
0062         m_pathBox->removeEventFilter(q);
0063         m_toggleEditableMode->removeEventFilter(q);
0064 
0065         for (KUrlNavigatorButton *button : std::as_const(m_navButtons)) {
0066             button->removeEventFilter(q);
0067         }
0068     }
0069 
0070     /** Applies the edited URL in m_pathBox to the URL navigator */
0071     void applyUncommittedUrl();
0072     void slotApplyUrl(QUrl url);
0073     // Returns true if "text" matched a URI filter (i.e. was fitlered),
0074     // otherwise returns false
0075     bool slotCheckFilters(const QString &text);
0076 
0077     void slotReturnPressed();
0078     void slotSchemeChanged(const QString &);
0079     void openPathSelectorMenu();
0080 
0081     /**
0082      * Appends the widget at the end of the URL navigator. It is assured
0083      * that the filler widget remains as last widget to fill the remaining
0084      * width.
0085      */
0086     void appendWidget(QWidget *widget, int stretch = 0);
0087 
0088     /**
0089      * This slot is connected to the clicked signal of the navigation bar button. It calls switchView().
0090      * Moreover, if switching from "editable" mode to the breadcrumb view, it calls applyUncommittedUrl().
0091      */
0092     void slotToggleEditableButtonPressed();
0093 
0094     /**
0095      * Switches the navigation bar between the breadcrumb view and the
0096      * traditional view (see setUrlEditable()).
0097      */
0098     void switchView();
0099 
0100     /** Emits the signal urlsDropped(). */
0101     void dropUrls(const QUrl &destination, QDropEvent *event, KUrlNavigatorButton *dropButton);
0102 
0103     /**
0104      * Is invoked when a navigator button has been clicked.
0105      * Different combinations of mouse clicks and keyboard modifiers have different effects on how
0106      * the url is opened. The behaviours are the following:
0107      * - shift+middle-click or ctrl+shift+left-click => activeTabRequested() signal is emitted
0108      * - ctrl+left-click or middle-click => tabRequested() signal is emitted
0109      * - shift+left-click => newWindowRequested() signal is emitted
0110      * - left-click => open the new url in-place
0111      */
0112     void slotNavigatorButtonClicked(const QUrl &url, Qt::MouseButton button, Qt::KeyboardModifiers modifiers);
0113 
0114     void openContextMenu(const QPoint &p);
0115 
0116     void slotPathBoxChanged(const QString &text);
0117 
0118     void updateContent();
0119 
0120     /**
0121      * Updates all buttons to have one button for each part of the
0122      * current URL. Existing buttons, which are available by m_navButtons,
0123      * are reused if possible. If the URL is longer, new buttons will be
0124      * created, if the URL is shorter, the remaining buttons will be deleted.
0125      * @param startIndex    Start index of URL part (/), where the buttons
0126      *                      should be created for each following part.
0127      */
0128     void updateButtons(int startIndex);
0129 
0130     /**
0131      * Updates the visibility state of all buttons describing the URL. If the
0132      * width of the URL navigator is too small, the buttons representing the upper
0133      * paths of the URL will be hidden and moved to a drop down menu.
0134      */
0135     void updateButtonVisibility();
0136 
0137     /**
0138      * @return Text for the first button of the URL navigator.
0139      */
0140     QString firstButtonText() const;
0141 
0142     /**
0143      * Returns the URL that should be applied for the button with the index \a index.
0144      */
0145     QUrl buttonUrl(int index) const;
0146 
0147     void switchToBreadcrumbMode();
0148 
0149     /**
0150      * Deletes all URL navigator buttons. m_navButtons is
0151      * empty after this operation.
0152      */
0153     void deleteButtons();
0154 
0155     /**
0156      * Retrieves the place url for the current url.
0157      * E. g. for the path "fish://root@192.168.0.2/var/lib" the string
0158      * "fish://root@192.168.0.2" will be returned, which leads to the
0159      * navigation indication 'Custom Path > var > lib". For e. g.
0160      * "settings:///System/" the path "settings://" will be returned.
0161      */
0162     QUrl retrievePlaceUrl() const;
0163 
0164     KUrlNavigator *const q;
0165 
0166     QHBoxLayout *m_layout = new QHBoxLayout(q);
0167     KCoreUrlNavigator *m_coreUrlNavigator = nullptr;
0168     QList<KUrlNavigatorButton *> m_navButtons;
0169     QStringList m_supportedSchemes;
0170     QUrl m_homeUrl;
0171     KUrlNavigatorPlacesSelector *m_placesSelector = nullptr;
0172     KUrlComboBox *m_pathBox = nullptr;
0173     KUrlNavigatorSchemeCombo *m_schemes = nullptr;
0174     KUrlNavigatorDropDownButton *m_dropDownButton = nullptr;
0175     KUrlNavigatorButtonBase *m_toggleEditableMode = nullptr;
0176     QWidget *m_dropWidget = nullptr;
0177 
0178     bool m_editable = false;
0179     bool m_active = true;
0180     bool m_showPlacesSelector = false;
0181     bool m_showFullPath = false;
0182 
0183     struct {
0184         bool showHidden = false;
0185         bool sortHiddenLast = false;
0186     } m_subfolderOptions;
0187 };
0188 
0189 KUrlNavigatorPrivate::KUrlNavigatorPrivate(const QUrl &url, KUrlNavigator *qq, KFilePlacesModel *placesModel)
0190     : q(qq)
0191     , m_coreUrlNavigator(new KCoreUrlNavigator(url, qq))
0192     , m_showPlacesSelector(placesModel != nullptr)
0193 {
0194     m_layout->setSpacing(0);
0195     m_layout->setContentsMargins(0, 0, 0, 0);
0196 
0197     q->connect(m_coreUrlNavigator, &KCoreUrlNavigator::currentLocationUrlChanged, q, [this]() {
0198         Q_EMIT q->urlChanged(m_coreUrlNavigator->currentLocationUrl());
0199     });
0200     q->connect(m_coreUrlNavigator, &KCoreUrlNavigator::currentUrlAboutToChange, q, [this](const QUrl &url) {
0201         Q_EMIT q->urlAboutToBeChanged(url);
0202     });
0203     q->connect(m_coreUrlNavigator, &KCoreUrlNavigator::historySizeChanged, q, [this]() {
0204         Q_EMIT q->historyChanged();
0205     });
0206     q->connect(m_coreUrlNavigator, &KCoreUrlNavigator::historyIndexChanged, q, [this]() {
0207         Q_EMIT q->historyChanged();
0208     });
0209     q->connect(m_coreUrlNavigator, &KCoreUrlNavigator::historyChanged, q, [this]() {
0210         Q_EMIT q->historyChanged();
0211     });
0212     q->connect(m_coreUrlNavigator, &KCoreUrlNavigator::urlSelectionRequested, q, [this](const QUrl &url) {
0213         Q_EMIT q->urlSelectionRequested(url);
0214     });
0215 
0216     // initialize the places selector
0217     q->setAutoFillBackground(false);
0218 
0219     if (placesModel != nullptr) {
0220         m_placesSelector = new KUrlNavigatorPlacesSelector(q, placesModel);
0221         q->connect(m_placesSelector, &KUrlNavigatorPlacesSelector::placeActivated, q, &KUrlNavigator::setLocationUrl);
0222         q->connect(m_placesSelector, &KUrlNavigatorPlacesSelector::tabRequested, q, &KUrlNavigator::tabRequested);
0223 
0224         auto updateContentFunc = [this]() {
0225             updateContent();
0226         };
0227         q->connect(placesModel, &KFilePlacesModel::rowsInserted, q, updateContentFunc);
0228         q->connect(placesModel, &KFilePlacesModel::rowsRemoved, q, updateContentFunc);
0229         q->connect(placesModel, &KFilePlacesModel::dataChanged, q, updateContentFunc);
0230     }
0231 
0232     // create scheme combo
0233     m_schemes = new KUrlNavigatorSchemeCombo(QString(), q);
0234     q->connect(m_schemes, &KUrlNavigatorSchemeCombo::activated, q, [this](const QString &schene) {
0235         slotSchemeChanged(schene);
0236     });
0237 
0238     // create drop down button for accessing all paths of the URL
0239     m_dropDownButton = new KUrlNavigatorDropDownButton(q);
0240     m_dropDownButton->setForegroundRole(QPalette::WindowText);
0241     m_dropDownButton->installEventFilter(q);
0242     q->connect(m_dropDownButton, &KUrlNavigatorDropDownButton::clicked, q, [this]() {
0243         openPathSelectorMenu();
0244     });
0245 
0246     // initialize the path box of the traditional view
0247     m_pathBox = new KUrlComboBox(KUrlComboBox::Directories, true, q);
0248     m_pathBox->setSizeAdjustPolicy(QComboBox::AdjustToContentsOnFirstShow);
0249     m_pathBox->installEventFilter(q);
0250 
0251     KUrlCompletion *kurlCompletion = new KUrlCompletion(KUrlCompletion::DirCompletion);
0252     m_pathBox->setCompletionObject(kurlCompletion);
0253     m_pathBox->setAutoDeleteCompletionObject(true);
0254 
0255     // TODO KF6: remove this QOverload, only KUrlComboBox::returnPressed(const QString &) will remain
0256     q->connect(m_pathBox, &KUrlComboBox::returnPressed, q, [this]() {
0257         slotReturnPressed();
0258     });
0259     q->connect(m_pathBox, &KUrlComboBox::urlActivated, q, &KUrlNavigator::setLocationUrl);
0260     q->connect(m_pathBox, &QComboBox::editTextChanged, q, [this](const QString &text) {
0261         slotPathBoxChanged(text);
0262     });
0263 
0264     // create toggle button which allows to switch between
0265     // the breadcrumb and traditional view
0266     m_toggleEditableMode = new KUrlNavigatorToggleButton(q);
0267     m_toggleEditableMode->installEventFilter(q);
0268     m_toggleEditableMode->setMinimumWidth(20);
0269     q->connect(m_toggleEditableMode, &KUrlNavigatorToggleButton::clicked, q, [this]() {
0270         slotToggleEditableButtonPressed();
0271     });
0272 
0273     if (m_placesSelector != nullptr) {
0274         m_layout->addWidget(m_placesSelector);
0275     }
0276     m_layout->addWidget(m_schemes);
0277     m_layout->addWidget(m_dropDownButton);
0278     m_layout->addWidget(m_pathBox, 1);
0279     m_layout->addWidget(m_toggleEditableMode);
0280 
0281     q->setContextMenuPolicy(Qt::CustomContextMenu);
0282     q->connect(q, &QWidget::customContextMenuRequested, q, [this](const QPoint &pos) {
0283         openContextMenu(pos);
0284     });
0285 }
0286 
0287 void KUrlNavigatorPrivate::appendWidget(QWidget *widget, int stretch)
0288 {
0289     m_layout->insertWidget(m_layout->count() - 1, widget, stretch);
0290 }
0291 
0292 void KUrlNavigatorPrivate::slotApplyUrl(QUrl url)
0293 {
0294     // Parts of the following code have been taken from the class KateFileSelector
0295     // located in kate/app/katefileselector.hpp of Kate.
0296     // SPDX-FileCopyrightText: 2001 Christoph Cullmann <cullmann@kde.org>
0297     // SPDX-FileCopyrightText: 2001 Joseph Wenninger <jowenn@kde.org>
0298     // SPDX-FileCopyrightText: 2001 Anders Lund <anders.lund@lund.tdcadsl.dk>
0299 
0300     // For example "desktop:/" _not_ "desktop:", see the comment in slotSchemeChanged()
0301     if (!url.isEmpty() && url.path().isEmpty() && KProtocolInfo::protocolClass(url.scheme()) == QLatin1String(":local")) {
0302         url.setPath(QStringLiteral("/"));
0303     }
0304 
0305     const auto urlStr = url.toString();
0306     QStringList urls = m_pathBox->urls();
0307     urls.removeAll(urlStr);
0308     urls.prepend(urlStr);
0309     m_pathBox->setUrls(urls, KUrlComboBox::RemoveBottom);
0310 
0311     q->setLocationUrl(url);
0312     // The URL might have been adjusted by KUrlNavigator::setUrl(), hence
0313     // synchronize the result in the path box.
0314     m_pathBox->setUrl(q->locationUrl());
0315 }
0316 
0317 bool KUrlNavigatorPrivate::slotCheckFilters(const QString &text)
0318 {
0319     KUriFilterData filteredData(text);
0320     filteredData.setCheckForExecutables(false);
0321     // Using kshorturifilter to fix up e.g. "ftp.kde.org" ---> "ftp://ftp.kde.org"
0322     const auto filtersList = QStringList{QStringLiteral("kshorturifilter")};
0323     const bool wasFiltered = KUriFilter::self()->filterUri(filteredData, filtersList);
0324     if (wasFiltered) {
0325         slotApplyUrl(filteredData.uri()); // The text was filtered
0326     }
0327     return wasFiltered;
0328 }
0329 
0330 void KUrlNavigatorPrivate::applyUncommittedUrl()
0331 {
0332     const QString text = m_pathBox->currentText().trimmed();
0333     QUrl url = q->locationUrl();
0334 
0335     // Using the stat job below, check if the url and text match a local dir; but first
0336     // handle a special case where "url" is empty in the unittests which use
0337     // KUrlNavigator::setLocationUrl(QUrl()); in practice (e.g. in Dolphin, or KFileWidget),
0338     // locationUrl() is never empty
0339     if (url.isEmpty() && !text.isEmpty()) {
0340         if (slotCheckFilters(text)) {
0341             return;
0342         }
0343     }
0344 
0345     // Treat absolute paths as absolute paths.
0346     // Relative paths get appended to the current path.
0347     if (text.startsWith(QLatin1Char('/'))) {
0348         url.setPath(text);
0349     } else {
0350         url.setPath(Utils::concatPaths(url.path(), text));
0351     }
0352 
0353     // Dirs and symlinks to dirs
0354     constexpr auto details = KIO::StatBasic | KIO::StatResolveSymlink;
0355     auto *job = KIO::stat(url, KIO::StatJob::DestinationSide, details, KIO::HideProgressInfo);
0356     q->connect(job, &KJob::result, q, [this, job, text]() {
0357         // If there is a dir matching "text" relative to the current url, use that, e.g.:
0358         // - typing "bar" while at "/path/to/foo" ---> "/path/to/foo/bar/"
0359         // - typing ".config" while at "/home/foo" ---> "/home/foo/.config"
0360         if (!job->error() && job->statResult().isDir()) {
0361             slotApplyUrl(job->url());
0362             return;
0363         }
0364 
0365         // Check if text matches a URI filter
0366         if (slotCheckFilters(text)) {
0367             return;
0368         }
0369 
0370         // ... otherwise fallback to whatever QUrl::fromUserInput() returns
0371         slotApplyUrl(QUrl::fromUserInput(text));
0372     });
0373 }
0374 
0375 void KUrlNavigatorPrivate::slotReturnPressed()
0376 {
0377     applyUncommittedUrl();
0378 
0379     Q_EMIT q->returnPressed();
0380 
0381     if (QApplication::keyboardModifiers() & Qt::ControlModifier) {
0382         // Pressing Ctrl+Return automatically switches back to the breadcrumb mode.
0383         // The switch must be done asynchronously, as we are in the context of the
0384         // editor.
0385         auto switchModeFunc = [this]() {
0386             switchToBreadcrumbMode();
0387         };
0388         QMetaObject::invokeMethod(q, switchModeFunc, Qt::QueuedConnection);
0389     }
0390 }
0391 
0392 void KUrlNavigatorPrivate::slotSchemeChanged(const QString &scheme)
0393 {
0394     Q_ASSERT(m_editable);
0395 
0396     QUrl url;
0397     url.setScheme(scheme);
0398     if (KProtocolInfo::protocolClass(scheme) == QLatin1String(":local")) {
0399         // E.g. "file:/" or "desktop:/", _not_ "file:" or "desktop:" respectively.
0400         // This is the more expected behaviour, "file:somedir" treats somedir as
0401         // a path relative to current dir; file:/somedir is an absolute path to /somedir.
0402         url.setPath(QStringLiteral("/"));
0403     } else {
0404         // With no authority set we'll get e.g. "ftp:" instead of "ftp://".
0405         // We want the latter, so let's set an empty authority.
0406         url.setAuthority(QString());
0407     }
0408 
0409     m_pathBox->setEditUrl(url);
0410 }
0411 
0412 void KUrlNavigatorPrivate::openPathSelectorMenu()
0413 {
0414     if (m_navButtons.count() <= 0) {
0415         return;
0416     }
0417 
0418     const QUrl firstVisibleUrl = m_navButtons.constFirst()->url();
0419 
0420     QString spacer;
0421     QPointer<QMenu> popup = new QMenu(q);
0422 
0423     auto *popupFilter = new KUrlNavigatorPathSelectorEventFilter(popup.data());
0424     q->connect(popupFilter, &KUrlNavigatorPathSelectorEventFilter::tabRequested, q, &KUrlNavigator::tabRequested);
0425     popup->installEventFilter(popupFilter);
0426 
0427     const QUrl placeUrl = retrievePlaceUrl();
0428     int idx = placeUrl.path().count(QLatin1Char('/')); // idx points to the first directory
0429     // after the place path
0430 
0431     const QString path = m_coreUrlNavigator->locationUrl(m_coreUrlNavigator->historyIndex()).path();
0432     QString dirName = path.section(QLatin1Char('/'), idx, idx);
0433     if (dirName.isEmpty()) {
0434         if (placeUrl.isLocalFile()) {
0435             dirName = QStringLiteral("/");
0436         } else {
0437             dirName = placeUrl.toDisplayString();
0438         }
0439     }
0440     do {
0441         const QString text = spacer + dirName;
0442 
0443         QAction *action = new QAction(text, popup);
0444         const QUrl currentUrl = buttonUrl(idx);
0445         if (currentUrl == firstVisibleUrl) {
0446             popup->addSeparator();
0447         }
0448         action->setData(QVariant(currentUrl.toString()));
0449         popup->addAction(action);
0450 
0451         ++idx;
0452         spacer.append(QLatin1String("  "));
0453         dirName = path.section(QLatin1Char('/'), idx, idx);
0454     } while (!dirName.isEmpty());
0455 
0456     const QPoint pos = q->mapToGlobal(m_dropDownButton->geometry().bottomRight());
0457     const QAction *activatedAction = popup->exec(pos);
0458     if (activatedAction != nullptr) {
0459         const QUrl url(activatedAction->data().toString());
0460         q->setLocationUrl(url);
0461     }
0462 
0463     // Delete the menu, unless it has been deleted in its own nested event loop already.
0464     if (popup) {
0465         popup->deleteLater();
0466     }
0467 }
0468 
0469 void KUrlNavigatorPrivate::slotToggleEditableButtonPressed()
0470 {
0471     if (m_editable) {
0472         applyUncommittedUrl();
0473     }
0474 
0475     switchView();
0476 }
0477 
0478 void KUrlNavigatorPrivate::switchView()
0479 {
0480     m_toggleEditableMode->setFocus();
0481     m_editable = !m_editable;
0482     m_toggleEditableMode->setChecked(m_editable);
0483     updateContent();
0484     if (q->isUrlEditable()) {
0485         m_pathBox->setFocus();
0486     }
0487 
0488     q->requestActivation();
0489     Q_EMIT q->editableStateChanged(m_editable);
0490 }
0491 
0492 void KUrlNavigatorPrivate::dropUrls(const QUrl &destination, QDropEvent *event, KUrlNavigatorButton *dropButton)
0493 {
0494     if (event->mimeData()->hasUrls()) {
0495         m_dropWidget = qobject_cast<QWidget *>(dropButton);
0496         Q_EMIT q->urlsDropped(destination, event);
0497     }
0498 }
0499 
0500 void KUrlNavigatorPrivate::slotNavigatorButtonClicked(const QUrl &url, Qt::MouseButton button, Qt::KeyboardModifiers modifiers)
0501 {
0502     if ((button & Qt::MiddleButton && modifiers & Qt::ShiftModifier) || (button & Qt::LeftButton && modifiers & (Qt::ControlModifier | Qt::ShiftModifier))) {
0503         Q_EMIT q->activeTabRequested(url);
0504     } else if (button & Qt::MiddleButton || (button & Qt::LeftButton && modifiers & Qt::ControlModifier)) {
0505         Q_EMIT q->tabRequested(url);
0506     } else if (button & Qt::LeftButton && modifiers & Qt::ShiftModifier) {
0507         Q_EMIT q->newWindowRequested(url);
0508     } else if (button & Qt::LeftButton) {
0509         q->setLocationUrl(url);
0510     }
0511 }
0512 
0513 void KUrlNavigatorPrivate::openContextMenu(const QPoint &p)
0514 {
0515     q->setActive(true);
0516 
0517     QPointer<QMenu> popup = new QMenu(q);
0518 
0519     // provide 'Copy' action, which copies the current URL of
0520     // the URL navigator into the clipboard
0521     QAction *copyAction = popup->addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy"));
0522 
0523     // provide 'Paste' action, which copies the current clipboard text
0524     // into the URL navigator
0525     QAction *pasteAction = popup->addAction(QIcon::fromTheme(QStringLiteral("edit-paste")), i18n("Paste"));
0526     QClipboard *clipboard = QApplication::clipboard();
0527     pasteAction->setEnabled(!clipboard->text().isEmpty());
0528 
0529     popup->addSeparator();
0530 
0531     // We are checking whether the signal is connected because it's odd to have a tab entry even
0532     // if it's not supported, like in the case of the open dialog
0533     const bool isTabSignal = q->isSignalConnected(QMetaMethod::fromSignal(&KUrlNavigator::tabRequested));
0534     const bool isWindowSignal = q->isSignalConnected(QMetaMethod::fromSignal(&KUrlNavigator::newWindowRequested));
0535     if (isTabSignal || isWindowSignal) {
0536         auto it = std::find_if(m_navButtons.cbegin(), m_navButtons.cend(), [&p](const KUrlNavigatorButton *button) {
0537             return button->geometry().contains(p);
0538         });
0539         if (it != m_navButtons.cend()) {
0540             const auto *button = *it;
0541             const QUrl url = button->url();
0542             const QString text = button->text();
0543 
0544             if (isTabSignal) {
0545                 QAction *openInTab = popup->addAction(QIcon::fromTheme(QStringLiteral("tab-new")), i18nc("@item:inmenu", "Open \"%1\" in New Tab", text));
0546                 q->connect(openInTab, &QAction::triggered, q, [this, url]() {
0547                     Q_EMIT q->tabRequested(url);
0548                 });
0549             }
0550 
0551             if (isWindowSignal) {
0552                 QAction *openInWindow =
0553                     popup->addAction(QIcon::fromTheme(QStringLiteral("window-new")), i18nc("@item:inmenu", "Open \"%1\" in New Window", text));
0554                 q->connect(openInWindow, &QAction::triggered, q, [this, url]() {
0555                     Q_EMIT q->newWindowRequested(url);
0556                 });
0557             }
0558         }
0559     }
0560 
0561     // provide radiobuttons for toggling between the edit and the navigation mode
0562     QAction *editAction = popup->addAction(i18n("Edit"));
0563     editAction->setCheckable(true);
0564 
0565     QAction *navigateAction = popup->addAction(i18n("Navigate"));
0566     navigateAction->setCheckable(true);
0567 
0568     QActionGroup *modeGroup = new QActionGroup(popup);
0569     modeGroup->addAction(editAction);
0570     modeGroup->addAction(navigateAction);
0571     if (q->isUrlEditable()) {
0572         editAction->setChecked(true);
0573     } else {
0574         navigateAction->setChecked(true);
0575     }
0576 
0577     popup->addSeparator();
0578 
0579     // allow showing of the full path
0580     QAction *showFullPathAction = popup->addAction(i18n("Show Full Path"));
0581     showFullPathAction->setCheckable(true);
0582     showFullPathAction->setChecked(q->showFullPath());
0583 
0584     QAction *activatedAction = popup->exec(QCursor::pos());
0585     if (activatedAction == copyAction) {
0586         QMimeData *mimeData = new QMimeData();
0587         mimeData->setText(q->locationUrl().toDisplayString(QUrl::PreferLocalFile));
0588         clipboard->setMimeData(mimeData);
0589     } else if (activatedAction == pasteAction) {
0590         q->setLocationUrl(QUrl::fromUserInput(clipboard->text()));
0591     } else if (activatedAction == editAction) {
0592         q->setUrlEditable(true);
0593     } else if (activatedAction == navigateAction) {
0594         q->setUrlEditable(false);
0595     } else if (activatedAction == showFullPathAction) {
0596         q->setShowFullPath(showFullPathAction->isChecked());
0597     }
0598 
0599     // Delete the menu, unless it has been deleted in its own nested event loop already.
0600     if (popup) {
0601         popup->deleteLater();
0602     }
0603 }
0604 
0605 void KUrlNavigatorPrivate::slotPathBoxChanged(const QString &text)
0606 {
0607     if (text.isEmpty()) {
0608         const QString scheme = q->locationUrl().scheme();
0609         m_schemes->setScheme(scheme);
0610         if (m_supportedSchemes.count() != 1) {
0611             m_schemes->show();
0612         }
0613     } else {
0614         m_schemes->hide();
0615     }
0616 }
0617 
0618 void KUrlNavigatorPrivate::updateContent()
0619 {
0620     const QUrl currentUrl = q->locationUrl();
0621     if (m_placesSelector != nullptr) {
0622         m_placesSelector->updateSelection(currentUrl);
0623     }
0624 
0625     if (m_editable) {
0626         m_schemes->hide();
0627         m_dropDownButton->hide();
0628 
0629         deleteButtons();
0630         m_toggleEditableMode->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
0631         q->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
0632 
0633         m_pathBox->show();
0634         m_pathBox->setUrl(currentUrl);
0635     } else {
0636         m_pathBox->hide();
0637 
0638         m_schemes->hide();
0639 
0640         m_toggleEditableMode->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
0641         q->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
0642 
0643         // Calculate the start index for the directories that should be shown as buttons
0644         // and create the buttons
0645         QUrl placeUrl;
0646         if ((m_placesSelector != nullptr) && !m_showFullPath) {
0647             placeUrl = m_placesSelector->selectedPlaceUrl();
0648         }
0649 
0650         if (!placeUrl.isValid()) {
0651             placeUrl = retrievePlaceUrl();
0652         }
0653         QString placePath = Utils::trailingSlashRemoved(placeUrl.path());
0654 
0655         const int startIndex = placePath.count(QLatin1Char('/'));
0656         updateButtons(startIndex);
0657     }
0658 }
0659 
0660 void KUrlNavigatorPrivate::updateButtons(int startIndex)
0661 {
0662     QUrl currentUrl = q->locationUrl();
0663     if (!currentUrl.isValid()) { // QFileDialog::setDirectory not called yet
0664         return;
0665     }
0666 
0667     const QString path = currentUrl.path();
0668 
0669     const int oldButtonCount = m_navButtons.count();
0670 
0671     int idx = startIndex;
0672     bool hasNext = true;
0673     do {
0674         const bool createButton = (idx - startIndex) >= oldButtonCount;
0675         const bool isFirstButton = (idx == startIndex);
0676         const QString dirName = path.section(QLatin1Char('/'), idx, idx);
0677         hasNext = isFirstButton || !dirName.isEmpty();
0678         if (hasNext) {
0679             KUrlNavigatorButton *button = nullptr;
0680             if (createButton) {
0681                 button = new KUrlNavigatorButton(buttonUrl(idx), q);
0682                 button->installEventFilter(q);
0683                 button->setForegroundRole(QPalette::WindowText);
0684                 q->connect(button, &KUrlNavigatorButton::urlsDroppedOnNavButton, q, [this, button](const QUrl &destination, QDropEvent *event) {
0685                     dropUrls(destination, event, button);
0686                 });
0687 
0688                 auto activatedFunc = [this](const QUrl &url, Qt::MouseButton btn, Qt::KeyboardModifiers modifiers) {
0689                     slotNavigatorButtonClicked(url, btn, modifiers);
0690                 };
0691                 q->connect(button, &KUrlNavigatorButton::navigatorButtonActivated, q, activatedFunc);
0692 
0693                 q->connect(button, &KUrlNavigatorButton::finishedTextResolving, q, [this]() {
0694                     updateButtonVisibility();
0695                 });
0696 
0697                 appendWidget(button);
0698             } else {
0699                 button = m_navButtons[idx - startIndex];
0700                 button->setUrl(buttonUrl(idx));
0701             }
0702 
0703             if (isFirstButton) {
0704                 button->setText(firstButtonText());
0705             }
0706             button->setActive(q->isActive());
0707 
0708             if (createButton) {
0709                 if (!isFirstButton) {
0710                     q->setTabOrder(m_navButtons.constLast(), button);
0711                 }
0712                 m_navButtons.append(button);
0713             }
0714 
0715             ++idx;
0716             button->setActiveSubDirectory(path.section(QLatin1Char('/'), idx, idx));
0717         }
0718     } while (hasNext);
0719 
0720     // delete buttons which are not used anymore
0721     const int newButtonCount = idx - startIndex;
0722     if (newButtonCount < oldButtonCount) {
0723         const auto itBegin = m_navButtons.begin() + newButtonCount;
0724         const auto itEnd = m_navButtons.end();
0725         for (auto it = itBegin; it != itEnd; ++it) {
0726             auto *navBtn = *it;
0727             navBtn->hide();
0728             navBtn->deleteLater();
0729         }
0730         m_navButtons.erase(itBegin, itEnd);
0731     }
0732 
0733     q->setTabOrder(m_dropDownButton, m_navButtons.constFirst());
0734     q->setTabOrder(m_navButtons.constLast(), m_toggleEditableMode);
0735 
0736     updateButtonVisibility();
0737 }
0738 
0739 void KUrlNavigatorPrivate::updateButtonVisibility()
0740 {
0741     if (m_editable) {
0742         return;
0743     }
0744 
0745     const int buttonsCount = m_navButtons.count();
0746     if (buttonsCount == 0) {
0747         m_dropDownButton->hide();
0748         return;
0749     }
0750 
0751     // Subtract all widgets from the available width, that must be shown anyway
0752     int availableWidth = q->width() - m_toggleEditableMode->minimumWidth();
0753 
0754     if ((m_placesSelector != nullptr) && m_placesSelector->isVisible()) {
0755         availableWidth -= m_placesSelector->width();
0756     }
0757 
0758     if ((m_schemes != nullptr) && m_schemes->isVisible()) {
0759         availableWidth -= m_schemes->width();
0760     }
0761 
0762     // Check whether buttons must be hidden at all...
0763     int requiredButtonWidth = 0;
0764     for (const auto *button : std::as_const(m_navButtons)) {
0765         requiredButtonWidth += button->minimumWidth();
0766     }
0767 
0768     if (requiredButtonWidth > availableWidth) {
0769         // At least one button must be hidden. This implies that the
0770         // drop-down button must get visible, which again decreases the
0771         // available width.
0772         availableWidth -= m_dropDownButton->width();
0773     }
0774 
0775     // Hide buttons...
0776     bool isLastButton = true;
0777     bool hasHiddenButtons = false;
0778     QList<KUrlNavigatorButton *> buttonsToShow;
0779     for (auto it = m_navButtons.crbegin(); it != m_navButtons.crend(); ++it) {
0780         KUrlNavigatorButton *button = *it;
0781         availableWidth -= button->minimumWidth();
0782         if ((availableWidth <= 0) && !isLastButton) {
0783             button->hide();
0784             hasHiddenButtons = true;
0785         } else {
0786             // Don't show the button immediately, as setActive()
0787             // might change the size and a relayout gets triggered
0788             // after showing the button. So the showing of all buttons
0789             // is postponed until all buttons have the correct
0790             // activation state.
0791             buttonsToShow.append(button);
0792         }
0793         isLastButton = false;
0794     }
0795 
0796     // All buttons have the correct activation state and
0797     // can be shown now
0798     for (KUrlNavigatorButton *button : std::as_const(buttonsToShow)) {
0799         button->show();
0800     }
0801 
0802     if (hasHiddenButtons) {
0803         m_dropDownButton->show();
0804     } else {
0805         // Check whether going upwards is possible. If this is the case, show the drop-down button.
0806         QUrl url(m_navButtons.front()->url());
0807         const bool visible = !url.matches(KIO::upUrl(url), QUrl::StripTrailingSlash) //
0808             && url.scheme() != QLatin1String("baloosearch") //
0809             && url.scheme() != QLatin1String("filenamesearch");
0810         m_dropDownButton->setVisible(visible);
0811     }
0812 }
0813 
0814 QString KUrlNavigatorPrivate::firstButtonText() const
0815 {
0816     QString text;
0817 
0818     // The first URL navigator button should get the name of the
0819     // place instead of the directory name
0820     if ((m_placesSelector != nullptr) && !m_showFullPath) {
0821         text = m_placesSelector->selectedPlaceText();
0822     }
0823 
0824     const QUrl currentUrl = q->locationUrl();
0825 
0826     if (text.isEmpty()) {
0827         if (currentUrl.isLocalFile()) {
0828 #ifdef Q_OS_WIN
0829             text = currentUrl.path().length() > 1 ? currentUrl.path().left(2) : QDir::rootPath();
0830 #else
0831             text = QStringLiteral("/");
0832 #endif
0833         }
0834     }
0835 
0836     if (text.isEmpty()) {
0837         if (currentUrl.path().isEmpty() || currentUrl.path() == QLatin1Char('/')) {
0838             QUrlQuery query(currentUrl);
0839             text = query.queryItemValue(QStringLiteral("title"));
0840         }
0841     }
0842 
0843     if (text.isEmpty()) {
0844         text = currentUrl.scheme() + QLatin1Char(':');
0845         if (!currentUrl.host().isEmpty()) {
0846             text += QLatin1Char(' ') + currentUrl.host();
0847         }
0848     }
0849 
0850     return text;
0851 }
0852 
0853 QUrl KUrlNavigatorPrivate::buttonUrl(int index) const
0854 {
0855     if (index < 0) {
0856         index = 0;
0857     }
0858 
0859     // Keep scheme, hostname etc. as this is needed for e. g. browsing
0860     // FTP directories
0861     QUrl url = q->locationUrl();
0862     QString path = url.path();
0863 
0864     if (!path.isEmpty()) {
0865         if (index == 0) {
0866             // prevent the last "/" from being stripped
0867             // or we end up with an empty path
0868 #ifdef Q_OS_WIN
0869             path = path.length() > 1 ? path.left(2) : QDir::rootPath();
0870 #else
0871             path = QStringLiteral("/");
0872 #endif
0873         } else {
0874             path = path.section(QLatin1Char('/'), 0, index);
0875         }
0876     }
0877 
0878     url.setPath(path);
0879     return url;
0880 }
0881 
0882 void KUrlNavigatorPrivate::switchToBreadcrumbMode()
0883 {
0884     q->setUrlEditable(false);
0885 }
0886 
0887 void KUrlNavigatorPrivate::deleteButtons()
0888 {
0889     for (KUrlNavigatorButton *button : std::as_const(m_navButtons)) {
0890         button->hide();
0891         button->deleteLater();
0892     }
0893     m_navButtons.clear();
0894 }
0895 
0896 QUrl KUrlNavigatorPrivate::retrievePlaceUrl() const
0897 {
0898     QUrl currentUrl = q->locationUrl();
0899     currentUrl.setPath(QString());
0900     return currentUrl;
0901 }
0902 
0903 // ------------------------------------------------------------------------------------------------
0904 
0905 KUrlNavigator::KUrlNavigator(QWidget *parent)
0906     : KUrlNavigator(nullptr, QUrl{}, parent)
0907 {
0908 }
0909 
0910 KUrlNavigator::KUrlNavigator(KFilePlacesModel *placesModel, const QUrl &url, QWidget *parent)
0911     : QWidget(parent)
0912     , d(new KUrlNavigatorPrivate(url, this, placesModel))
0913 {
0914     const int minHeight = d->m_pathBox->sizeHint().height();
0915     setMinimumHeight(minHeight);
0916 
0917     setMinimumWidth(100);
0918 
0919     d->updateContent();
0920 }
0921 
0922 KUrlNavigator::~KUrlNavigator()
0923 {
0924     d->m_dropDownButton->removeEventFilter(this);
0925     d->m_pathBox->removeEventFilter(this);
0926     for (auto *button : std::as_const(d->m_navButtons)) {
0927         button->removeEventFilter(this);
0928     }
0929 }
0930 
0931 QUrl KUrlNavigator::locationUrl(int historyIndex) const
0932 {
0933     return d->m_coreUrlNavigator->locationUrl(historyIndex);
0934 }
0935 
0936 void KUrlNavigator::saveLocationState(const QByteArray &state)
0937 {
0938     auto current = d->m_coreUrlNavigator->locationState().value<KUrlNavigatorData>();
0939     current.state = state;
0940     d->m_coreUrlNavigator->saveLocationState(QVariant::fromValue(current));
0941 }
0942 
0943 QByteArray KUrlNavigator::locationState(int historyIndex) const
0944 {
0945     return d->m_coreUrlNavigator->locationState(historyIndex).value<KUrlNavigatorData>().state;
0946 }
0947 
0948 bool KUrlNavigator::goBack()
0949 {
0950     return d->m_coreUrlNavigator->goBack();
0951 }
0952 
0953 bool KUrlNavigator::goForward()
0954 {
0955     return d->m_coreUrlNavigator->goForward();
0956 }
0957 
0958 bool KUrlNavigator::goUp()
0959 {
0960     return d->m_coreUrlNavigator->goUp();
0961 }
0962 
0963 void KUrlNavigator::goHome()
0964 {
0965     if (d->m_homeUrl.isEmpty() || !d->m_homeUrl.isValid()) {
0966         setLocationUrl(QUrl::fromLocalFile(QDir::homePath()));
0967     } else {
0968         setLocationUrl(d->m_homeUrl);
0969     }
0970 }
0971 
0972 void KUrlNavigator::setHomeUrl(const QUrl &url)
0973 {
0974     d->m_homeUrl = url;
0975 }
0976 
0977 QUrl KUrlNavigator::homeUrl() const
0978 {
0979     return d->m_homeUrl;
0980 }
0981 
0982 void KUrlNavigator::setUrlEditable(bool editable)
0983 {
0984     if (d->m_editable != editable) {
0985         d->switchView();
0986     }
0987 }
0988 
0989 bool KUrlNavigator::isUrlEditable() const
0990 {
0991     return d->m_editable;
0992 }
0993 
0994 void KUrlNavigator::setShowFullPath(bool show)
0995 {
0996     if (d->m_showFullPath != show) {
0997         d->m_showFullPath = show;
0998         d->updateContent();
0999     }
1000 }
1001 
1002 bool KUrlNavigator::showFullPath() const
1003 {
1004     return d->m_showFullPath;
1005 }
1006 
1007 void KUrlNavigator::setActive(bool active)
1008 {
1009     if (active != d->m_active) {
1010         d->m_active = active;
1011 
1012         d->m_dropDownButton->setActive(active);
1013         for (KUrlNavigatorButton *button : std::as_const(d->m_navButtons)) {
1014             button->setActive(active);
1015         }
1016 
1017         update();
1018         if (active) {
1019             Q_EMIT activated();
1020         }
1021     }
1022 }
1023 
1024 bool KUrlNavigator::isActive() const
1025 {
1026     return d->m_active;
1027 }
1028 
1029 void KUrlNavigator::setPlacesSelectorVisible(bool visible)
1030 {
1031     if (visible == d->m_showPlacesSelector) {
1032         return;
1033     }
1034 
1035     if (visible && (d->m_placesSelector == nullptr)) {
1036         // the places selector cannot get visible as no
1037         // places model is available
1038         return;
1039     }
1040 
1041     d->m_showPlacesSelector = visible;
1042     d->m_placesSelector->setVisible(visible);
1043 }
1044 
1045 bool KUrlNavigator::isPlacesSelectorVisible() const
1046 {
1047     return d->m_showPlacesSelector;
1048 }
1049 
1050 QUrl KUrlNavigator::uncommittedUrl() const
1051 {
1052     KUriFilterData filteredData(d->m_pathBox->currentText().trimmed());
1053     filteredData.setCheckForExecutables(false);
1054     if (KUriFilter::self()->filterUri(filteredData, QStringList{QStringLiteral("kshorturifilter")})) {
1055         return filteredData.uri();
1056     } else {
1057         return QUrl::fromUserInput(filteredData.typedString());
1058     }
1059 }
1060 
1061 void KUrlNavigator::setLocationUrl(const QUrl &newUrl)
1062 {
1063     d->m_coreUrlNavigator->setCurrentLocationUrl(newUrl);
1064 
1065     d->updateContent();
1066 
1067     requestActivation();
1068 }
1069 
1070 void KUrlNavigator::requestActivation()
1071 {
1072     setActive(true);
1073 }
1074 
1075 void KUrlNavigator::setFocus()
1076 {
1077     if (isUrlEditable()) {
1078         d->m_pathBox->setFocus();
1079     } else {
1080         QWidget::setFocus();
1081     }
1082 }
1083 
1084 void KUrlNavigator::keyPressEvent(QKeyEvent *event)
1085 {
1086     if (isUrlEditable() && (event->key() == Qt::Key_Escape)) {
1087         setUrlEditable(false);
1088     } else {
1089         QWidget::keyPressEvent(event);
1090     }
1091 }
1092 
1093 void KUrlNavigator::keyReleaseEvent(QKeyEvent *event)
1094 {
1095     QWidget::keyReleaseEvent(event);
1096 }
1097 
1098 void KUrlNavigator::mousePressEvent(QMouseEvent *event)
1099 {
1100     if (event->button() == Qt::MiddleButton) {
1101         requestActivation();
1102     }
1103     QWidget::mousePressEvent(event);
1104 }
1105 
1106 void KUrlNavigator::mouseReleaseEvent(QMouseEvent *event)
1107 {
1108     if (event->button() == Qt::MiddleButton) {
1109         const QRect bounds = d->m_toggleEditableMode->geometry();
1110         if (bounds.contains(event->pos())) {
1111             // The middle mouse button has been clicked above the
1112             // toggle-editable-mode-button. Paste the clipboard content
1113             // as location URL.
1114             QClipboard *clipboard = QApplication::clipboard();
1115             const QMimeData *mimeData = clipboard->mimeData();
1116             if (mimeData->hasText()) {
1117                 const QString text = mimeData->text();
1118                 setLocationUrl(QUrl::fromUserInput(text));
1119             }
1120         }
1121     }
1122     QWidget::mouseReleaseEvent(event);
1123 }
1124 
1125 void KUrlNavigator::resizeEvent(QResizeEvent *event)
1126 {
1127     QTimer::singleShot(0, this, [this]() {
1128         d->updateButtonVisibility();
1129     });
1130     QWidget::resizeEvent(event);
1131 }
1132 
1133 void KUrlNavigator::wheelEvent(QWheelEvent *event)
1134 {
1135     setActive(true);
1136     QWidget::wheelEvent(event);
1137 }
1138 
1139 bool KUrlNavigator::eventFilter(QObject *watched, QEvent *event)
1140 {
1141     switch (event->type()) {
1142     case QEvent::FocusIn:
1143         if (watched == d->m_pathBox) {
1144             requestActivation();
1145             setFocus();
1146         }
1147         for (KUrlNavigatorButton *button : std::as_const(d->m_navButtons)) {
1148             button->setShowMnemonic(true);
1149         }
1150         break;
1151 
1152     case QEvent::FocusOut:
1153         for (KUrlNavigatorButton *button : std::as_const(d->m_navButtons)) {
1154             button->setShowMnemonic(false);
1155         }
1156         break;
1157 
1158     default:
1159         break;
1160     }
1161 
1162     return QWidget::eventFilter(watched, event);
1163 }
1164 
1165 int KUrlNavigator::historySize() const
1166 {
1167     return d->m_coreUrlNavigator->historySize();
1168 }
1169 
1170 int KUrlNavigator::historyIndex() const
1171 {
1172     return d->m_coreUrlNavigator->historyIndex();
1173 }
1174 
1175 KUrlComboBox *KUrlNavigator::editor() const
1176 {
1177     return d->m_pathBox;
1178 }
1179 
1180 void KUrlNavigator::setSupportedSchemes(const QStringList &schemes)
1181 {
1182     d->m_supportedSchemes = schemes;
1183     d->m_schemes->setSupportedSchemes(d->m_supportedSchemes);
1184 }
1185 
1186 QStringList KUrlNavigator::supportedSchemes() const
1187 {
1188     return d->m_supportedSchemes;
1189 }
1190 
1191 QWidget *KUrlNavigator::dropWidget() const
1192 {
1193     return d->m_dropWidget;
1194 }
1195 
1196 void KUrlNavigator::setShowHiddenFolders(bool showHiddenFolders)
1197 {
1198     d->m_subfolderOptions.showHidden = showHiddenFolders;
1199 }
1200 
1201 bool KUrlNavigator::showHiddenFolders() const
1202 {
1203     return d->m_subfolderOptions.showHidden;
1204 }
1205 
1206 void KUrlNavigator::setSortHiddenFoldersLast(bool sortHiddenFoldersLast)
1207 {
1208     d->m_subfolderOptions.sortHiddenLast = sortHiddenFoldersLast;
1209 }
1210 
1211 bool KUrlNavigator::sortHiddenFoldersLast() const
1212 {
1213     return d->m_subfolderOptions.sortHiddenLast;
1214 }
1215 
1216 #include "moc_kurlnavigator.cpp"