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