1/*
2 SPDX-FileCopyrightText: 2006-2010 Peter Penz <peter.penz@gmx.at>
3 SPDX-FileCopyrightText: 2006 Aaron J. Seigo <aseigo@kde.org>
4 SPDX-FileCopyrightText: 2007 Kevin Ottens <ervin@kde.org>
5 SPDX-FileCopyrightText: 2007 Urs Wolfer <uwolfer @ kde.org>
6
7 SPDX-License-Identifier: LGPL-2.0-or-later
8*/
9
10#include "kurlnavigator.h"
11#include "kcoreurlnavigator.h"
12
13#include "../utils_p.h"
14#include "kurlnavigatorbutton_p.h"
15#include "kurlnavigatordropdownbutton_p.h"
16#include "kurlnavigatorpathselectoreventfilter_p.h"
17#include "kurlnavigatorplacesselector_p.h"
18#include "kurlnavigatorschemecombo_p.h"
19#include "kurlnavigatortogglebutton_p.h"
20
21#include <KIO/StatJob>
22#include <KLocalizedString>
23#include <kfileitem.h>
24#include <kfileplacesmodel.h>
25#include <kprotocolinfo.h>
26#include <kurifilter.h>
27#include <kurlcombobox.h>
28#include <kurlcompletion.h>
29
30#include <QActionGroup>
31#include <QApplication>
32#include <QClipboard>
33#include <QDir>
34#include <QDropEvent>
35#include <QHBoxLayout>
36#include <QKeyEvent>
37#include <QMenu>
38#include <QMetaMethod>
39#include <QMimeData>
40#include <QMimeDatabase>
41#include <QTimer>
42#include <QUrlQuery>
43
44#include <algorithm>
45#include <numeric>
46
47using namespace KDEPrivate;
48
49struct KUrlNavigatorData {
50 QByteArray state;
51};
52Q_DECLARE_METATYPE(KUrlNavigatorData)
53
54class KUrlNavigatorPrivate
55{
56public:
57 KUrlNavigatorPrivate(const QUrl &url, KUrlNavigator *qq, KFilePlacesModel *placesModel);
58
59 ~KUrlNavigatorPrivate()
60 {
61 m_dropDownButton->removeEventFilter(obj: q);
62 m_pathBox->removeEventFilter(obj: q);
63 m_toggleEditableMode->removeEventFilter(obj: q);
64
65 for (KUrlNavigatorButton *button : std::as_const(t&: m_navButtons)) {
66 button->removeEventFilter(obj: q);
67 }
68 }
69
70 /** Applies the edited URL in m_pathBox to the URL navigator */
71 void applyUncommittedUrl();
72 void slotApplyUrl(QUrl url);
73 // Returns true if "text" matched a URI filter (i.e. was fitlered),
74 // otherwise returns false
75 bool slotCheckFilters(const QString &text);
76
77 void slotReturnPressed();
78 void slotSchemeChanged(const QString &);
79 void openPathSelectorMenu();
80
81 /**
82 * Appends the widget at the end of the URL navigator. It is assured
83 * that the filler widget remains as last widget to fill the remaining
84 * width.
85 */
86 void appendWidget(QWidget *widget, int stretch = 0);
87
88 /**
89 * This slot is connected to the clicked signal of the navigation bar button. It calls switchView().
90 * Moreover, if switching from "editable" mode to the breadcrumb view, it calls applyUncommittedUrl().
91 */
92 void slotToggleEditableButtonPressed();
93
94 /**
95 * Switches the navigation bar between the breadcrumb view and the
96 * traditional view (see setUrlEditable()).
97 */
98 void switchView();
99
100 /** Emits the signal urlsDropped(). */
101 void dropUrls(const QUrl &destination, QDropEvent *event, KUrlNavigatorButton *dropButton);
102
103 /**
104 * Is invoked when a navigator button has been clicked.
105 * Different combinations of mouse clicks and keyboard modifiers have different effects on how
106 * the url is opened. The behaviours are the following:
107 * - shift+middle-click or ctrl+shift+left-click => activeTabRequested() signal is emitted
108 * - ctrl+left-click or middle-click => tabRequested() signal is emitted
109 * - shift+left-click => newWindowRequested() signal is emitted
110 * - left-click => open the new url in-place
111 */
112 void slotNavigatorButtonClicked(const QUrl &url, Qt::MouseButton button, Qt::KeyboardModifiers modifiers);
113
114 void openContextMenu(const QPoint &p);
115
116 void slotPathBoxChanged(const QString &text);
117
118 void updateContent();
119
120 /**
121 * Updates all buttons to have one button for each part of the
122 * current URL. Existing buttons, which are available by m_navButtons,
123 * are reused if possible. If the URL is longer, new buttons will be
124 * created, if the URL is shorter, the remaining buttons will be deleted.
125 * @param startIndex Start index of URL part (/), where the buttons
126 * should be created for each following part.
127 */
128 void updateButtons(int startIndex);
129
130 /**
131 * Updates the visibility state of all buttons describing the URL. If the
132 * width of the URL navigator is too small, the buttons representing the upper
133 * paths of the URL will be hidden and moved to a drop down menu.
134 */
135 void updateButtonVisibility();
136
137 /**
138 * @return Text for the first button of the URL navigator.
139 */
140 QString firstButtonText() const;
141
142 /**
143 * Returns the URL that should be applied for the button with the index \a index.
144 */
145 QUrl buttonUrl(int index) const;
146
147 void switchToBreadcrumbMode();
148
149 /**
150 * Deletes all URL navigator buttons. m_navButtons is
151 * empty after this operation.
152 */
153 void deleteButtons();
154
155 /**
156 * Retrieves the place url for the current url.
157 * E. g. for the path "fish://root@192.168.0.2/var/lib" the string
158 * "fish://root@192.168.0.2" will be returned, which leads to the
159 * navigation indication 'Custom Path > var > lib". For e. g.
160 * "settings:///System/" the path "settings://" will be returned.
161 */
162 QUrl retrievePlaceUrl() const;
163
164 KUrlNavigator *const q;
165
166 QHBoxLayout *m_layout = new QHBoxLayout(q);
167 KCoreUrlNavigator *m_coreUrlNavigator = nullptr;
168 QList<KUrlNavigatorButton *> m_navButtons;
169 QStringList m_supportedSchemes;
170 QUrl m_homeUrl;
171 KUrlNavigatorPlacesSelector *m_placesSelector = nullptr;
172 KUrlComboBox *m_pathBox = nullptr;
173 KUrlNavigatorSchemeCombo *m_schemes = nullptr;
174 KUrlNavigatorDropDownButton *m_dropDownButton = nullptr;
175 KUrlNavigatorButtonBase *m_toggleEditableMode = nullptr;
176 QWidget *m_dropWidget = nullptr;
177 QWidget *m_badgeWidgetContainer = nullptr;
178
179 bool m_editable = false;
180 bool m_active = true;
181 bool m_showPlacesSelector = false;
182 bool m_showFullPath = false;
183
184 struct {
185 bool showHidden = false;
186 bool sortHiddenLast = false;
187 } m_subfolderOptions;
188};
189
190KUrlNavigatorPrivate::KUrlNavigatorPrivate(const QUrl &url, KUrlNavigator *qq, KFilePlacesModel *placesModel)
191 : q(qq)
192 , m_coreUrlNavigator(new KCoreUrlNavigator(url, qq))
193 , m_showPlacesSelector(placesModel != nullptr)
194{
195 m_layout->setSpacing(0);
196 m_layout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
197
198 q->connect(sender: m_coreUrlNavigator, signal: &KCoreUrlNavigator::currentLocationUrlChanged, context: q, slot: [this]() {
199 Q_EMIT q->urlChanged(url: m_coreUrlNavigator->currentLocationUrl());
200 });
201 q->connect(sender: m_coreUrlNavigator, signal: &KCoreUrlNavigator::currentUrlAboutToChange, context: q, slot: [this](const QUrl &url) {
202 Q_EMIT q->urlAboutToBeChanged(newUrl: url);
203 });
204 q->connect(sender: m_coreUrlNavigator, signal: &KCoreUrlNavigator::historySizeChanged, context: q, slot: [this]() {
205 Q_EMIT q->historyChanged();
206 });
207 q->connect(sender: m_coreUrlNavigator, signal: &KCoreUrlNavigator::historyIndexChanged, context: q, slot: [this]() {
208 Q_EMIT q->historyChanged();
209 });
210 q->connect(sender: m_coreUrlNavigator, signal: &KCoreUrlNavigator::historyChanged, context: q, slot: [this]() {
211 Q_EMIT q->historyChanged();
212 });
213 q->connect(sender: m_coreUrlNavigator, signal: &KCoreUrlNavigator::urlSelectionRequested, context: q, slot: [this](const QUrl &url) {
214 Q_EMIT q->urlSelectionRequested(url);
215 });
216
217 // initialize the places selector
218 q->setAutoFillBackground(false);
219
220 if (placesModel != nullptr) {
221 m_placesSelector = new KUrlNavigatorPlacesSelector(q, placesModel);
222 q->connect(sender: m_placesSelector, signal: &KUrlNavigatorPlacesSelector::placeActivated, context: q, slot: &KUrlNavigator::setLocationUrl);
223 q->connect(sender: m_placesSelector, signal: &KUrlNavigatorPlacesSelector::tabRequested, context: q, slot: &KUrlNavigator::tabRequested);
224
225 auto updateContentFunc = [this]() {
226 updateContent();
227 };
228 q->connect(sender: placesModel, signal: &KFilePlacesModel::rowsInserted, context: q, slot&: updateContentFunc);
229 q->connect(sender: placesModel, signal: &KFilePlacesModel::rowsRemoved, context: q, slot&: updateContentFunc);
230 q->connect(sender: placesModel, signal: &KFilePlacesModel::dataChanged, context: q, slot&: updateContentFunc);
231 }
232
233 // create scheme combo
234 m_schemes = new KUrlNavigatorSchemeCombo(QString(), q);
235 q->connect(sender: m_schemes, signal: &KUrlNavigatorSchemeCombo::activated, context: q, slot: [this](const QString &schene) {
236 slotSchemeChanged(schene);
237 });
238
239 // create drop down button for accessing all paths of the URL
240 m_dropDownButton = new KUrlNavigatorDropDownButton(q);
241 m_dropDownButton->setForegroundRole(QPalette::WindowText);
242 m_dropDownButton->installEventFilter(filterObj: q);
243 q->connect(sender: m_dropDownButton, signal: &KUrlNavigatorDropDownButton::clicked, context: q, slot: [this]() {
244 openPathSelectorMenu();
245 });
246
247 // initialize the path box of the traditional view
248 m_pathBox = new KUrlComboBox(KUrlComboBox::Directories, true, q);
249 m_pathBox->setSizeAdjustPolicy(QComboBox::AdjustToContentsOnFirstShow);
250 m_pathBox->installEventFilter(filterObj: q);
251
252 KUrlCompletion *kurlCompletion = new KUrlCompletion(KUrlCompletion::DirCompletion);
253 m_pathBox->setCompletionObject(compObj: kurlCompletion);
254 m_pathBox->setAutoDeleteCompletionObject(true);
255
256 // TODO KF6: remove this QOverload, only KUrlComboBox::returnPressed(const QString &) will remain
257 q->connect(sender: m_pathBox, signal: &KUrlComboBox::returnPressed, context: q, slot: [this]() {
258 slotReturnPressed();
259 });
260 q->connect(sender: m_pathBox, signal: &KUrlComboBox::urlActivated, context: q, slot: &KUrlNavigator::setLocationUrl);
261 q->connect(sender: m_pathBox, signal: &QComboBox::editTextChanged, context: q, slot: [this](const QString &text) {
262 slotPathBoxChanged(text);
263 });
264
265 m_badgeWidgetContainer = new QWidget(q);
266 auto badgeLayout = new QHBoxLayout(m_badgeWidgetContainer);
267 badgeLayout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
268
269 // create toggle button which allows to switch between
270 // the breadcrumb and traditional view
271 m_toggleEditableMode = new KUrlNavigatorToggleButton(q);
272 m_toggleEditableMode->installEventFilter(filterObj: q);
273 m_toggleEditableMode->setMinimumWidth(20);
274 q->connect(sender: m_toggleEditableMode, signal: &KUrlNavigatorToggleButton::clicked, context: q, slot: [this]() {
275 slotToggleEditableButtonPressed();
276 });
277
278 if (m_placesSelector != nullptr) {
279 m_layout->addWidget(m_placesSelector);
280 }
281 m_layout->addWidget(m_schemes);
282 m_layout->addWidget(m_dropDownButton);
283 m_layout->addWidget(m_pathBox, stretch: 1);
284 m_layout->addWidget(m_badgeWidgetContainer);
285 m_layout->addWidget(m_toggleEditableMode);
286
287 q->setContextMenuPolicy(Qt::CustomContextMenu);
288 q->connect(sender: q, signal: &QWidget::customContextMenuRequested, context: q, slot: [this](const QPoint &pos) {
289 openContextMenu(p: pos);
290 });
291}
292
293void KUrlNavigatorPrivate::appendWidget(QWidget *widget, int stretch)
294{
295 // insert to the left of: m_badgeWidgetContainer, m_toggleEditableMode
296 m_layout->insertWidget(index: m_layout->count() - 2, widget, stretch);
297}
298
299void KUrlNavigatorPrivate::slotApplyUrl(QUrl url)
300{
301 // Parts of the following code have been taken from the class KateFileSelector
302 // located in kate/app/katefileselector.hpp of Kate.
303 // SPDX-FileCopyrightText: 2001 Christoph Cullmann <cullmann@kde.org>
304 // SPDX-FileCopyrightText: 2001 Joseph Wenninger <jowenn@kde.org>
305 // SPDX-FileCopyrightText: 2001 Anders Lund <anders.lund@lund.tdcadsl.dk>
306
307 // For example "desktop:/" _not_ "desktop:", see the comment in slotSchemeChanged()
308 if (!url.isEmpty() && url.path().isEmpty() && KProtocolInfo::protocolClass(protocol: url.scheme()) == QLatin1String(":local")) {
309 url.setPath(QStringLiteral("/"));
310 }
311
312 const auto urlStr = url.toString();
313 QStringList urls = m_pathBox->urls();
314 urls.removeAll(t: urlStr);
315 urls.prepend(t: urlStr);
316 m_pathBox->setUrls(urls, remove: KUrlComboBox::RemoveBottom);
317
318 q->setLocationUrl(url);
319 // The URL might have been adjusted by KUrlNavigator::setUrl(), hence
320 // synchronize the result in the path box.
321 m_pathBox->setUrl(q->locationUrl());
322}
323
324bool KUrlNavigatorPrivate::slotCheckFilters(const QString &text)
325{
326 KUriFilterData filteredData(text);
327 filteredData.setCheckForExecutables(false);
328 // Using kshorturifilter to fix up e.g. "ftp.kde.org" ---> "ftp://ftp.kde.org"
329 const auto filtersList = QStringList{QStringLiteral("kshorturifilter")};
330 const bool wasFiltered = KUriFilter::self()->filterUri(data&: filteredData, filters: filtersList);
331 if (wasFiltered) {
332 slotApplyUrl(url: filteredData.uri()); // The text was filtered
333 }
334 return wasFiltered;
335}
336
337void KUrlNavigatorPrivate::applyUncommittedUrl()
338{
339 const QString text = m_pathBox->currentText().trimmed();
340 QUrl url = q->locationUrl();
341
342 // Using the stat job below, check if the url and text match a local dir; but first
343 // handle a special case where "url" is empty in the unittests which use
344 // KUrlNavigator::setLocationUrl(QUrl()); in practice (e.g. in Dolphin, or KFileWidget),
345 // locationUrl() is never empty
346 if (url.isEmpty() && !text.isEmpty()) {
347 if (slotCheckFilters(text)) {
348 return;
349 }
350 }
351
352 // Treat absolute paths as absolute paths.
353 // Relative paths get appended to the current path.
354 if (text.startsWith(c: QLatin1Char('/'))) {
355 url.setPath(path: text);
356 } else {
357 url.setPath(path: Utils::concatPaths(path1: url.path(), path2: text));
358 }
359
360 // Dirs and symlinks to dirs
361 constexpr auto details = KIO::StatBasic | KIO::StatResolveSymlink;
362 auto *job = KIO::stat(url, side: KIO::StatJob::DestinationSide, details, flags: KIO::HideProgressInfo);
363 q->connect(sender: job, signal: &KJob::result, context: q, slot: [this, job, text]() {
364 // If there is a dir matching "text" relative to the current url, use that, e.g.:
365 // - typing "bar" while at "/path/to/foo" ---> "/path/to/foo/bar/"
366 // - typing ".config" while at "/home/foo" ---> "/home/foo/.config"
367 if (!job->error() && job->statResult().isDir()) {
368 slotApplyUrl(url: job->url());
369 return;
370 }
371
372 // Check if text matches a URI filter
373 if (slotCheckFilters(text)) {
374 return;
375 }
376
377 // ... otherwise fallback to whatever QUrl::fromUserInput() returns
378 slotApplyUrl(url: QUrl::fromUserInput(userInput: text));
379 });
380}
381
382void KUrlNavigatorPrivate::slotReturnPressed()
383{
384 applyUncommittedUrl();
385
386 Q_EMIT q->returnPressed();
387
388 if (QApplication::keyboardModifiers() & Qt::ControlModifier) {
389 // Pressing Ctrl+Return automatically switches back to the breadcrumb mode.
390 // The switch must be done asynchronously, as we are in the context of the
391 // editor.
392 auto switchModeFunc = [this]() {
393 switchToBreadcrumbMode();
394 };
395 QMetaObject::invokeMethod(object: q, function&: switchModeFunc, type: Qt::QueuedConnection);
396 }
397}
398
399void KUrlNavigatorPrivate::slotSchemeChanged(const QString &scheme)
400{
401 Q_ASSERT(m_editable);
402
403 QUrl url;
404 url.setScheme(scheme);
405 if (KProtocolInfo::protocolClass(protocol: scheme) == QLatin1String(":local")) {
406 // E.g. "file:/" or "desktop:/", _not_ "file:" or "desktop:" respectively.
407 // This is the more expected behaviour, "file:somedir" treats somedir as
408 // a path relative to current dir; file:/somedir is an absolute path to /somedir.
409 url.setPath(QStringLiteral("/"));
410 } else {
411 // With no authority set we'll get e.g. "ftp:" instead of "ftp://".
412 // We want the latter, so let's set an empty authority.
413 url.setAuthority(authority: QString());
414 }
415
416 m_pathBox->setEditUrl(url);
417}
418
419void KUrlNavigatorPrivate::openPathSelectorMenu()
420{
421 if (m_navButtons.count() <= 0) {
422 return;
423 }
424
425 const QUrl firstVisibleUrl = m_navButtons.constFirst()->url();
426
427 QString spacer;
428 QPointer<QMenu> popup = new QMenu(q);
429
430 auto *popupFilter = new KUrlNavigatorPathSelectorEventFilter(popup.data());
431 q->connect(sender: popupFilter, signal: &KUrlNavigatorPathSelectorEventFilter::tabRequested, context: q, slot: &KUrlNavigator::tabRequested);
432 popup->installEventFilter(filterObj: popupFilter);
433
434 const QUrl placeUrl = retrievePlaceUrl();
435 int idx = placeUrl.path().count(c: QLatin1Char('/')); // idx points to the first directory
436 // after the place path
437
438 const QString path = m_coreUrlNavigator->locationUrl(historyIndex: m_coreUrlNavigator->historyIndex()).path();
439 QString dirName = path.section(asep: QLatin1Char('/'), astart: idx, aend: idx);
440 if (dirName.isEmpty()) {
441 if (placeUrl.isLocalFile()) {
442 dirName = QStringLiteral("/");
443 } else {
444 dirName = placeUrl.toDisplayString();
445 }
446 }
447 do {
448 const QString text = spacer + dirName;
449
450 QAction *action = new QAction(text, popup);
451 const QUrl currentUrl = buttonUrl(index: idx);
452 if (currentUrl == firstVisibleUrl) {
453 popup->addSeparator();
454 }
455 action->setData(QVariant(currentUrl.toString()));
456 popup->addAction(action);
457
458 ++idx;
459 spacer.append(s: QLatin1String(" "));
460 dirName = path.section(asep: QLatin1Char('/'), astart: idx, aend: idx);
461 } while (!dirName.isEmpty());
462
463 const QPoint pos = q->mapToGlobal(m_dropDownButton->geometry().bottomRight());
464 const QAction *activatedAction = popup->exec(pos);
465 if (activatedAction != nullptr) {
466 const QUrl url(activatedAction->data().toString());
467 q->setLocationUrl(url);
468 }
469
470 // Delete the menu, unless it has been deleted in its own nested event loop already.
471 if (popup) {
472 popup->deleteLater();
473 }
474}
475
476void KUrlNavigatorPrivate::slotToggleEditableButtonPressed()
477{
478 if (m_editable) {
479 applyUncommittedUrl();
480 }
481
482 switchView();
483}
484
485void KUrlNavigatorPrivate::switchView()
486{
487 m_toggleEditableMode->setFocus();
488 m_editable = !m_editable;
489 m_toggleEditableMode->setChecked(m_editable);
490 updateContent();
491 if (q->isUrlEditable()) {
492 m_pathBox->setFocus();
493 }
494
495 q->requestActivation();
496 Q_EMIT q->editableStateChanged(editable: m_editable);
497}
498
499void KUrlNavigatorPrivate::dropUrls(const QUrl &destination, QDropEvent *event, KUrlNavigatorButton *dropButton)
500{
501 if (event->mimeData()->hasUrls()) {
502 m_dropWidget = qobject_cast<QWidget *>(o: dropButton);
503 Q_EMIT q->urlsDropped(destination, event);
504 }
505}
506
507void KUrlNavigatorPrivate::slotNavigatorButtonClicked(const QUrl &url, Qt::MouseButton button, Qt::KeyboardModifiers modifiers)
508{
509 if ((button & Qt::MiddleButton && modifiers & Qt::ShiftModifier) || (button & Qt::LeftButton && modifiers & (Qt::ControlModifier | Qt::ShiftModifier))) {
510 Q_EMIT q->activeTabRequested(url);
511 } else if (button & Qt::MiddleButton || (button & Qt::LeftButton && modifiers & Qt::ControlModifier)) {
512 Q_EMIT q->tabRequested(url);
513 } else if (button & Qt::LeftButton && modifiers & Qt::ShiftModifier) {
514 Q_EMIT q->newWindowRequested(url);
515 } else if (button & Qt::LeftButton) {
516 q->setLocationUrl(url);
517 }
518}
519
520void KUrlNavigatorPrivate::openContextMenu(const QPoint &p)
521{
522 q->setActive(true);
523
524 QPointer<QMenu> popup = new QMenu(q);
525
526 // provide 'Copy' action, which copies the current URL of
527 // the URL navigator into the clipboard
528 QAction *copyAction = popup->addAction(icon: QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy"));
529
530 // provide 'Paste' action, which copies the current clipboard text
531 // into the URL navigator
532 QAction *pasteAction = popup->addAction(icon: QIcon::fromTheme(QStringLiteral("edit-paste")), i18n("Paste"));
533 QClipboard *clipboard = QApplication::clipboard();
534 pasteAction->setEnabled(!clipboard->text().isEmpty());
535
536 popup->addSeparator();
537
538 // We are checking whether the signal is connected because it's odd to have a tab entry even
539 // if it's not supported, like in the case of the open dialog
540 const bool isTabSignal = q->isSignalConnected(signal: QMetaMethod::fromSignal(signal: &KUrlNavigator::tabRequested));
541 const bool isWindowSignal = q->isSignalConnected(signal: QMetaMethod::fromSignal(signal: &KUrlNavigator::newWindowRequested));
542 if (isTabSignal || isWindowSignal) {
543 auto it = std::find_if(first: m_navButtons.cbegin(), last: m_navButtons.cend(), pred: [&p](const KUrlNavigatorButton *button) {
544 return button->geometry().contains(p);
545 });
546 if (it != m_navButtons.cend()) {
547 const auto *button = *it;
548 const QUrl url = button->url();
549 const QString text = button->text();
550
551 if (isTabSignal) {
552 QAction *openInTab = popup->addAction(icon: QIcon::fromTheme(QStringLiteral("tab-new")), i18nc("@item:inmenu", "Open \"%1\" in New Tab", text));
553 q->connect(sender: openInTab, signal: &QAction::triggered, context: q, slot: [this, url]() {
554 Q_EMIT q->tabRequested(url);
555 });
556 }
557
558 if (isWindowSignal) {
559 QAction *openInWindow =
560 popup->addAction(icon: QIcon::fromTheme(QStringLiteral("window-new")), i18nc("@item:inmenu", "Open \"%1\" in New Window", text));
561 q->connect(sender: openInWindow, signal: &QAction::triggered, context: q, slot: [this, url]() {
562 Q_EMIT q->newWindowRequested(url);
563 });
564 }
565 }
566 }
567
568 // provide radiobuttons for toggling between the edit and the navigation mode
569 QAction *editAction = popup->addAction(i18n("Edit"));
570 editAction->setCheckable(true);
571
572 QAction *navigateAction = popup->addAction(i18n("Navigate"));
573 navigateAction->setCheckable(true);
574
575 QActionGroup *modeGroup = new QActionGroup(popup);
576 modeGroup->addAction(a: editAction);
577 modeGroup->addAction(a: navigateAction);
578 if (q->isUrlEditable()) {
579 editAction->setChecked(true);
580 } else {
581 navigateAction->setChecked(true);
582 }
583
584 popup->addSeparator();
585
586 // allow showing of the full path
587 QAction *showFullPathAction = popup->addAction(i18n("Show Full Path"));
588 showFullPathAction->setCheckable(true);
589 showFullPathAction->setChecked(q->showFullPath());
590
591 QAction *activatedAction = popup->exec(pos: QCursor::pos());
592 if (activatedAction == copyAction) {
593 QMimeData *mimeData = new QMimeData();
594 mimeData->setText(q->locationUrl().toDisplayString(options: QUrl::PreferLocalFile));
595 clipboard->setMimeData(data: mimeData);
596 } else if (activatedAction == pasteAction) {
597 q->setLocationUrl(QUrl::fromUserInput(userInput: clipboard->text()));
598 } else if (activatedAction == editAction) {
599 q->setUrlEditable(true);
600 } else if (activatedAction == navigateAction) {
601 q->setUrlEditable(false);
602 } else if (activatedAction == showFullPathAction) {
603 q->setShowFullPath(showFullPathAction->isChecked());
604 }
605
606 // Delete the menu, unless it has been deleted in its own nested event loop already.
607 if (popup) {
608 popup->deleteLater();
609 }
610}
611
612void KUrlNavigatorPrivate::slotPathBoxChanged(const QString &text)
613{
614 if (text.isEmpty()) {
615 const QString scheme = q->locationUrl().scheme();
616 m_schemes->setScheme(scheme);
617 if (m_supportedSchemes.count() != 1) {
618 m_schemes->show();
619 }
620 } else {
621 m_schemes->hide();
622 }
623}
624
625void KUrlNavigatorPrivate::updateContent()
626{
627 const QUrl currentUrl = q->locationUrl();
628 if (m_placesSelector != nullptr) {
629 m_placesSelector->updateSelection(url: currentUrl);
630 }
631
632 if (m_editable) {
633 m_schemes->hide();
634 m_dropDownButton->hide();
635 m_badgeWidgetContainer->hide();
636
637 deleteButtons();
638 m_toggleEditableMode->setSizePolicy(hor: QSizePolicy::Fixed, ver: QSizePolicy::Preferred);
639 q->setSizePolicy(hor: QSizePolicy::Minimum, ver: QSizePolicy::Fixed);
640
641 m_pathBox->show();
642 m_pathBox->setUrl(currentUrl);
643 } else {
644 m_pathBox->hide();
645 m_badgeWidgetContainer->show();
646
647 m_schemes->hide();
648
649 m_toggleEditableMode->setSizePolicy(hor: QSizePolicy::Expanding, ver: QSizePolicy::Preferred);
650 q->setSizePolicy(hor: QSizePolicy::Expanding, ver: QSizePolicy::Fixed);
651
652 // Calculate the start index for the directories that should be shown as buttons
653 // and create the buttons
654 QUrl placeUrl;
655 if ((m_placesSelector != nullptr) && !m_showFullPath) {
656 placeUrl = m_placesSelector->selectedPlaceUrl();
657 }
658
659 if (!placeUrl.isValid()) {
660 placeUrl = retrievePlaceUrl();
661 }
662 QString placePath = Utils::trailingSlashRemoved(path: placeUrl.path());
663
664 const int startIndex = placePath.count(c: QLatin1Char('/'));
665 updateButtons(startIndex);
666 }
667}
668
669void KUrlNavigatorPrivate::updateButtons(int startIndex)
670{
671 QUrl currentUrl = q->locationUrl();
672 if (!currentUrl.isValid()) { // QFileDialog::setDirectory not called yet
673 return;
674 }
675
676 const QString path = currentUrl.path();
677
678 const int oldButtonCount = m_navButtons.count();
679
680 int idx = startIndex;
681 bool hasNext = true;
682 do {
683 const bool createButton = (idx - startIndex) >= oldButtonCount;
684 const bool isFirstButton = (idx == startIndex);
685 const QString dirName = path.section(asep: QLatin1Char('/'), astart: idx, aend: idx);
686 hasNext = isFirstButton || !dirName.isEmpty();
687 if (hasNext) {
688 KUrlNavigatorButton *button = nullptr;
689 if (createButton) {
690 button = new KUrlNavigatorButton(buttonUrl(index: idx), q);
691 button->installEventFilter(filterObj: q);
692 button->setForegroundRole(QPalette::WindowText);
693 q->connect(sender: button, signal: &KUrlNavigatorButton::urlsDroppedOnNavButton, context: q, slot: [this, button](const QUrl &destination, QDropEvent *event) {
694 dropUrls(destination, event, dropButton: button);
695 });
696
697 auto activatedFunc = [this](const QUrl &url, Qt::MouseButton btn, Qt::KeyboardModifiers modifiers) {
698 slotNavigatorButtonClicked(url, button: btn, modifiers);
699 };
700 q->connect(sender: button, signal: &KUrlNavigatorButton::navigatorButtonActivated, context: q, slot&: activatedFunc);
701
702 q->connect(sender: button, signal: &KUrlNavigatorButton::finishedTextResolving, context: q, slot: [this]() {
703 updateButtonVisibility();
704 });
705
706 appendWidget(widget: button);
707 } else {
708 button = m_navButtons[idx - startIndex];
709 button->setUrl(buttonUrl(index: idx));
710 }
711
712 if (isFirstButton) {
713 button->setText(firstButtonText());
714 }
715 button->setActive(q->isActive());
716
717 if (createButton) {
718 if (!isFirstButton) {
719 q->setTabOrder(m_navButtons.constLast(), button);
720 }
721 m_navButtons.append(t: button);
722 }
723
724 ++idx;
725 button->setActiveSubDirectory(path.section(asep: QLatin1Char('/'), astart: idx, aend: idx));
726 }
727 } while (hasNext);
728
729 // delete buttons which are not used anymore
730 const int newButtonCount = idx - startIndex;
731 if (newButtonCount < oldButtonCount) {
732 const auto itBegin = m_navButtons.begin() + newButtonCount;
733 const auto itEnd = m_navButtons.end();
734 for (auto it = itBegin; it != itEnd; ++it) {
735 auto *navBtn = *it;
736 navBtn->hide();
737 navBtn->deleteLater();
738 }
739 m_navButtons.erase(abegin: itBegin, aend: itEnd);
740 }
741
742 q->setTabOrder(m_dropDownButton, m_navButtons.constFirst());
743 q->setTabOrder(m_navButtons.constLast(), m_toggleEditableMode);
744
745 updateButtonVisibility();
746}
747
748void KUrlNavigatorPrivate::updateButtonVisibility()
749{
750 if (m_editable) {
751 return;
752 }
753
754 const int buttonsCount = m_navButtons.count();
755 if (buttonsCount == 0) {
756 m_dropDownButton->hide();
757 return;
758 }
759
760 // Subtract all widgets from the available width, that must be shown anyway
761 int availableWidth = q->width() - m_toggleEditableMode->minimumWidth();
762
763 availableWidth -= m_badgeWidgetContainer->width();
764
765 if ((m_placesSelector != nullptr) && m_placesSelector->isVisible()) {
766 availableWidth -= m_placesSelector->width();
767 }
768
769 if ((m_schemes != nullptr) && m_schemes->isVisible()) {
770 availableWidth -= m_schemes->width();
771 }
772
773 // Check whether buttons must be hidden at all...
774 int requiredButtonWidth = 0;
775 for (const auto *button : std::as_const(t&: m_navButtons)) {
776 requiredButtonWidth += button->minimumWidth();
777 }
778
779 if (requiredButtonWidth > availableWidth) {
780 // At least one button must be hidden. This implies that the
781 // drop-down button must get visible, which again decreases the
782 // available width.
783 availableWidth -= m_dropDownButton->width();
784 }
785
786 // Hide buttons...
787 bool isLastButton = true;
788 bool hasHiddenButtons = false;
789 QList<KUrlNavigatorButton *> buttonsToShow;
790 for (auto it = m_navButtons.crbegin(); it != m_navButtons.crend(); ++it) {
791 KUrlNavigatorButton *button = *it;
792 availableWidth -= button->minimumWidth();
793 if ((availableWidth <= 0) && !isLastButton) {
794 button->hide();
795 hasHiddenButtons = true;
796 } else {
797 // Don't show the button immediately, as setActive()
798 // might change the size and a relayout gets triggered
799 // after showing the button. So the showing of all buttons
800 // is postponed until all buttons have the correct
801 // activation state.
802 buttonsToShow.append(t: button);
803 }
804 isLastButton = false;
805 }
806
807 // All buttons have the correct activation state and
808 // can be shown now
809 for (KUrlNavigatorButton *button : std::as_const(t&: buttonsToShow)) {
810 button->show();
811 }
812
813 if (hasHiddenButtons) {
814 m_dropDownButton->show();
815 } else {
816 // Check whether going upwards is possible. If this is the case, show the drop-down button.
817 QUrl url(m_navButtons.front()->url());
818 const bool visible = !url.matches(url: KIO::upUrl(url), options: QUrl::StripTrailingSlash) //
819 && url.scheme() != QLatin1String("baloosearch") //
820 && url.scheme() != QLatin1String("filenamesearch");
821 m_dropDownButton->setVisible(visible);
822 }
823}
824
825QString KUrlNavigatorPrivate::firstButtonText() const
826{
827 QString text;
828
829 // The first URL navigator button should get the name of the
830 // place instead of the directory name
831 if ((m_placesSelector != nullptr) && !m_showFullPath) {
832 text = m_placesSelector->selectedPlaceText();
833 }
834
835 const QUrl currentUrl = q->locationUrl();
836
837 if (text.isEmpty()) {
838 if (currentUrl.isLocalFile()) {
839#ifdef Q_OS_WIN
840 text = currentUrl.path().length() > 1 ? currentUrl.path().left(2) : QDir::rootPath();
841#else
842 text = QStringLiteral("/");
843#endif
844 }
845 }
846
847 if (text.isEmpty()) {
848 if (currentUrl.path().isEmpty() || currentUrl.path() == QLatin1Char('/')) {
849 QUrlQuery query(currentUrl);
850 text = query.queryItemValue(QStringLiteral("title"));
851 }
852 }
853
854 if (text.isEmpty()) {
855 text = currentUrl.scheme() + QLatin1Char(':');
856 if (!currentUrl.host().isEmpty()) {
857 text += QLatin1Char(' ') + currentUrl.host();
858 }
859 }
860
861 return text;
862}
863
864QUrl KUrlNavigatorPrivate::buttonUrl(int index) const
865{
866 if (index < 0) {
867 index = 0;
868 }
869
870 // Keep scheme, hostname etc. as this is needed for e. g. browsing
871 // FTP directories
872 QUrl url = q->locationUrl();
873 QString path = url.path();
874
875 if (!path.isEmpty()) {
876 if (index == 0) {
877 // prevent the last "/" from being stripped
878 // or we end up with an empty path
879#ifdef Q_OS_WIN
880 path = path.length() > 1 ? path.left(2) : QDir::rootPath();
881#else
882 path = QStringLiteral("/");
883#endif
884 } else {
885 path = path.section(asep: QLatin1Char('/'), astart: 0, aend: index);
886 }
887 }
888
889 url.setPath(path);
890 return url;
891}
892
893void KUrlNavigatorPrivate::switchToBreadcrumbMode()
894{
895 q->setUrlEditable(false);
896}
897
898void KUrlNavigatorPrivate::deleteButtons()
899{
900 for (KUrlNavigatorButton *button : std::as_const(t&: m_navButtons)) {
901 button->hide();
902 button->deleteLater();
903 }
904 m_navButtons.clear();
905}
906
907QUrl KUrlNavigatorPrivate::retrievePlaceUrl() const
908{
909 QUrl currentUrl = q->locationUrl();
910 currentUrl.setPath(path: QString());
911 return currentUrl;
912}
913
914// ------------------------------------------------------------------------------------------------
915
916KUrlNavigator::KUrlNavigator(QWidget *parent)
917 : KUrlNavigator(nullptr, QUrl{}, parent)
918{
919}
920
921KUrlNavigator::KUrlNavigator(KFilePlacesModel *placesModel, const QUrl &url, QWidget *parent)
922 : QWidget(parent)
923 , d(new KUrlNavigatorPrivate(url, this, placesModel))
924{
925 const int minHeight = d->m_pathBox->sizeHint().height();
926 setMinimumHeight(minHeight);
927
928 setMinimumWidth(100);
929
930 d->updateContent();
931}
932
933KUrlNavigator::~KUrlNavigator()
934{
935 d->m_dropDownButton->removeEventFilter(obj: this);
936 d->m_pathBox->removeEventFilter(obj: this);
937 for (auto *button : std::as_const(t&: d->m_navButtons)) {
938 button->removeEventFilter(obj: this);
939 }
940}
941
942QUrl KUrlNavigator::locationUrl(int historyIndex) const
943{
944 return d->m_coreUrlNavigator->locationUrl(historyIndex);
945}
946
947void KUrlNavigator::saveLocationState(const QByteArray &state)
948{
949 auto current = d->m_coreUrlNavigator->locationState().value<KUrlNavigatorData>();
950 current.state = state;
951 d->m_coreUrlNavigator->saveLocationState(state: QVariant::fromValue(value: current));
952}
953
954QByteArray KUrlNavigator::locationState(int historyIndex) const
955{
956 return d->m_coreUrlNavigator->locationState(historyIndex).value<KUrlNavigatorData>().state;
957}
958
959bool KUrlNavigator::goBack()
960{
961 return d->m_coreUrlNavigator->goBack();
962}
963
964bool KUrlNavigator::goForward()
965{
966 return d->m_coreUrlNavigator->goForward();
967}
968
969bool KUrlNavigator::goUp()
970{
971 return d->m_coreUrlNavigator->goUp();
972}
973
974void KUrlNavigator::goHome()
975{
976 if (d->m_homeUrl.isEmpty() || !d->m_homeUrl.isValid()) {
977 setLocationUrl(QUrl::fromLocalFile(localfile: QDir::homePath()));
978 } else {
979 setLocationUrl(d->m_homeUrl);
980 }
981}
982
983void KUrlNavigator::setHomeUrl(const QUrl &url)
984{
985 d->m_homeUrl = url;
986}
987
988QUrl KUrlNavigator::homeUrl() const
989{
990 return d->m_homeUrl;
991}
992
993void KUrlNavigator::setUrlEditable(bool editable)
994{
995 if (d->m_editable != editable) {
996 d->switchView();
997 }
998}
999
1000bool KUrlNavigator::isUrlEditable() const
1001{
1002 return d->m_editable;
1003}
1004
1005void KUrlNavigator::setShowFullPath(bool show)
1006{
1007 if (d->m_showFullPath != show) {
1008 d->m_showFullPath = show;
1009 d->updateContent();
1010 }
1011}
1012
1013bool KUrlNavigator::showFullPath() const
1014{
1015 return d->m_showFullPath;
1016}
1017
1018void KUrlNavigator::setActive(bool active)
1019{
1020 if (active != d->m_active) {
1021 d->m_active = active;
1022
1023 d->m_dropDownButton->setActive(active);
1024 for (KUrlNavigatorButton *button : std::as_const(t&: d->m_navButtons)) {
1025 button->setActive(active);
1026 }
1027
1028 update();
1029 if (active) {
1030 Q_EMIT activated();
1031 }
1032 }
1033}
1034
1035bool KUrlNavigator::isActive() const
1036{
1037 return d->m_active;
1038}
1039
1040void KUrlNavigator::setPlacesSelectorVisible(bool visible)
1041{
1042 if (visible == d->m_showPlacesSelector) {
1043 return;
1044 }
1045
1046 if (visible && (d->m_placesSelector == nullptr)) {
1047 // the places selector cannot get visible as no
1048 // places model is available
1049 return;
1050 }
1051
1052 d->m_showPlacesSelector = visible;
1053 d->m_placesSelector->setVisible(visible);
1054}
1055
1056bool KUrlNavigator::isPlacesSelectorVisible() const
1057{
1058 return d->m_showPlacesSelector;
1059}
1060
1061QUrl KUrlNavigator::uncommittedUrl() const
1062{
1063 KUriFilterData filteredData(d->m_pathBox->currentText().trimmed());
1064 filteredData.setCheckForExecutables(false);
1065 if (KUriFilter::self()->filterUri(data&: filteredData, filters: QStringList{QStringLiteral("kshorturifilter")})) {
1066 return filteredData.uri();
1067 } else {
1068 return QUrl::fromUserInput(userInput: filteredData.typedString());
1069 }
1070}
1071
1072void KUrlNavigator::setLocationUrl(const QUrl &newUrl)
1073{
1074 d->m_coreUrlNavigator->setCurrentLocationUrl(newUrl);
1075
1076 d->updateContent();
1077
1078 requestActivation();
1079}
1080
1081void KUrlNavigator::requestActivation()
1082{
1083 setActive(true);
1084}
1085
1086void KUrlNavigator::setFocus()
1087{
1088 if (isUrlEditable()) {
1089 d->m_pathBox->setFocus();
1090 } else {
1091 QWidget::setFocus();
1092 }
1093}
1094
1095void KUrlNavigator::keyPressEvent(QKeyEvent *event)
1096{
1097 if (isUrlEditable() && (event->key() == Qt::Key_Escape)) {
1098 setUrlEditable(false);
1099 } else {
1100 QWidget::keyPressEvent(event);
1101 }
1102}
1103
1104void KUrlNavigator::keyReleaseEvent(QKeyEvent *event)
1105{
1106 QWidget::keyReleaseEvent(event);
1107}
1108
1109void KUrlNavigator::mousePressEvent(QMouseEvent *event)
1110{
1111 if (event->button() == Qt::MiddleButton) {
1112 requestActivation();
1113 }
1114 QWidget::mousePressEvent(event);
1115}
1116
1117void KUrlNavigator::mouseReleaseEvent(QMouseEvent *event)
1118{
1119 if (event->button() == Qt::MiddleButton) {
1120 const QRect bounds = d->m_toggleEditableMode->geometry();
1121 if (bounds.contains(p: event->pos())) {
1122 // The middle mouse button has been clicked above the
1123 // toggle-editable-mode-button. Paste the clipboard content
1124 // as location URL.
1125 QClipboard *clipboard = QApplication::clipboard();
1126 const QMimeData *mimeData = clipboard->mimeData();
1127 if (mimeData->hasText()) {
1128 const QString text = mimeData->text();
1129 setLocationUrl(QUrl::fromUserInput(userInput: text));
1130 }
1131 }
1132 }
1133 QWidget::mouseReleaseEvent(event);
1134}
1135
1136void KUrlNavigator::resizeEvent(QResizeEvent *event)
1137{
1138 QTimer::singleShot(interval: 0, receiver: this, slot: [this]() {
1139 d->updateButtonVisibility();
1140 });
1141 QWidget::resizeEvent(event);
1142}
1143
1144void KUrlNavigator::wheelEvent(QWheelEvent *event)
1145{
1146 setActive(true);
1147 QWidget::wheelEvent(event);
1148}
1149
1150bool KUrlNavigator::eventFilter(QObject *watched, QEvent *event)
1151{
1152 switch (event->type()) {
1153 case QEvent::FocusIn:
1154 if (watched == d->m_pathBox) {
1155 requestActivation();
1156 setFocus();
1157 }
1158 for (KUrlNavigatorButton *button : std::as_const(t&: d->m_navButtons)) {
1159 button->setShowMnemonic(true);
1160 }
1161 break;
1162
1163 case QEvent::FocusOut:
1164 for (KUrlNavigatorButton *button : std::as_const(t&: d->m_navButtons)) {
1165 button->setShowMnemonic(false);
1166 }
1167 break;
1168
1169 default:
1170 break;
1171 }
1172
1173 return QWidget::eventFilter(watched, event);
1174}
1175
1176int KUrlNavigator::historySize() const
1177{
1178 return d->m_coreUrlNavigator->historySize();
1179}
1180
1181int KUrlNavigator::historyIndex() const
1182{
1183 return d->m_coreUrlNavigator->historyIndex();
1184}
1185
1186KUrlComboBox *KUrlNavigator::editor() const
1187{
1188 return d->m_pathBox;
1189}
1190
1191void KUrlNavigator::setSupportedSchemes(const QStringList &schemes)
1192{
1193 d->m_supportedSchemes = schemes;
1194 d->m_schemes->setSupportedSchemes(d->m_supportedSchemes);
1195}
1196
1197QStringList KUrlNavigator::supportedSchemes() const
1198{
1199 return d->m_supportedSchemes;
1200}
1201
1202QWidget *KUrlNavigator::dropWidget() const
1203{
1204 return d->m_dropWidget;
1205}
1206
1207void KUrlNavigator::setShowHiddenFolders(bool showHiddenFolders)
1208{
1209 d->m_subfolderOptions.showHidden = showHiddenFolders;
1210}
1211
1212bool KUrlNavigator::showHiddenFolders() const
1213{
1214 return d->m_subfolderOptions.showHidden;
1215}
1216
1217void KUrlNavigator::setSortHiddenFoldersLast(bool sortHiddenFoldersLast)
1218{
1219 d->m_subfolderOptions.sortHiddenLast = sortHiddenFoldersLast;
1220}
1221
1222bool KUrlNavigator::sortHiddenFoldersLast() const
1223{
1224 return d->m_subfolderOptions.sortHiddenLast;
1225}
1226
1227void KUrlNavigator::setBadgeWidget(QWidget *widget)
1228{
1229 QWidget *oldWidget = badgeWidget();
1230 if (oldWidget) {
1231 if (widget == oldWidget) {
1232 return;
1233 }
1234 d->m_badgeWidgetContainer->layout()->replaceWidget(from: oldWidget, to: widget);
1235 oldWidget->deleteLater();
1236 } else {
1237 d->m_badgeWidgetContainer->layout()->addWidget(w: widget);
1238 }
1239}
1240
1241QWidget *KUrlNavigator::badgeWidget() const
1242{
1243 QLayoutItem *item = d->m_badgeWidgetContainer->layout()->itemAt(index: 0);
1244 if (item) {
1245 return item->widget();
1246 } else {
1247 return nullptr;
1248 }
1249}
1250
1251#include "moc_kurlnavigator.cpp"
1252

source code of kio/src/filewidgets/kurlnavigator.cpp