| 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 | |