File indexing completed on 2024-05-12 09:51:15

0001 /*
0002  * SPDX-FileCopyrightText: 2006-2009 Peter Penz <peter.penz19@gmail.com>
0003  * SPDX-FileCopyrightText: 2006 Gregor Kališnik <gregor@podnapisi.net>
0004  *
0005  * SPDX-License-Identifier: GPL-2.0-or-later
0006  */
0007 
0008 #include "dolphinview.h"
0009 
0010 #include "dolphin_detailsmodesettings.h"
0011 #include "dolphin_generalsettings.h"
0012 #include "dolphinitemlistview.h"
0013 #include "dolphinnewfilemenuobserver.h"
0014 #include "draganddrophelper.h"
0015 #include "kitemviews/kfileitemlistview.h"
0016 #include "kitemviews/kfileitemmodel.h"
0017 #include "kitemviews/kitemlistcontainer.h"
0018 #include "kitemviews/kitemlistcontroller.h"
0019 #include "kitemviews/kitemlistheader.h"
0020 #include "kitemviews/kitemlistselectionmanager.h"
0021 #include "kitemviews/private/kitemlistroleeditor.h"
0022 #include "selectionmode/singleclickselectionproxystyle.h"
0023 #include "settings/viewmodes/viewmodesettings.h"
0024 #include "versioncontrol/versioncontrolobserver.h"
0025 #include "viewproperties.h"
0026 #include "views/tooltips/tooltipmanager.h"
0027 #include "zoomlevelinfo.h"
0028 
0029 #if HAVE_BALOO
0030 #include <Baloo/IndexerConfig>
0031 #endif
0032 #include <KColorScheme>
0033 #include <KDesktopFile>
0034 #include <KDirModel>
0035 #include <KFileItemListProperties>
0036 #include <KFormat>
0037 #include <KIO/CopyJob>
0038 #include <KIO/DeleteOrTrashJob>
0039 #include <KIO/DropJob>
0040 #include <KIO/JobUiDelegate>
0041 #include <KIO/Paste>
0042 #include <KIO/PasteJob>
0043 #include <KIO/RenameFileDialog>
0044 #include <KJobWidgets>
0045 #include <KLocalizedString>
0046 #include <KMessageBox>
0047 #include <KProtocolManager>
0048 #include <KUrlMimeData>
0049 
0050 #include <kwidgetsaddons_version.h>
0051 
0052 #include <QAbstractItemView>
0053 #include <QActionGroup>
0054 #include <QApplication>
0055 #include <QClipboard>
0056 #include <QDropEvent>
0057 #include <QGraphicsOpacityEffect>
0058 #include <QGraphicsSceneDragDropEvent>
0059 #include <QLabel>
0060 #include <QMenu>
0061 #include <QMimeDatabase>
0062 #include <QPixmapCache>
0063 #include <QScrollBar>
0064 #include <QSize>
0065 #include <QTimer>
0066 #include <QToolTip>
0067 #include <QVBoxLayout>
0068 
0069 DolphinView::DolphinView(const QUrl &url, QWidget *parent)
0070     : QWidget(parent)
0071     , m_active(true)
0072     , m_tabsForFiles(false)
0073     , m_assureVisibleCurrentIndex(false)
0074     , m_isFolderWritable(true)
0075     , m_dragging(false)
0076     , m_selectNextItem(false)
0077     , m_url(url)
0078     , m_viewPropertiesContext()
0079     , m_mode(DolphinView::IconsView)
0080     , m_visibleRoles()
0081     , m_topLayout(nullptr)
0082     , m_model(nullptr)
0083     , m_view(nullptr)
0084     , m_container(nullptr)
0085     , m_toolTipManager(nullptr)
0086     , m_selectionChangedTimer(nullptr)
0087     , m_currentItemUrl()
0088     , m_scrollToCurrentItem(false)
0089     , m_restoredContentsPosition()
0090     , m_controlWheelAccumulatedDelta(0)
0091     , m_selectedUrls()
0092     , m_clearSelectionBeforeSelectingNewItems(false)
0093     , m_markFirstNewlySelectedItemAsCurrent(false)
0094     , m_versionControlObserver(nullptr)
0095     , m_twoClicksRenamingTimer(nullptr)
0096     , m_placeholderLabel(nullptr)
0097     , m_showLoadingPlaceholderTimer(nullptr)
0098 {
0099     m_topLayout = new QVBoxLayout(this);
0100     m_topLayout->setSpacing(0);
0101     m_topLayout->setContentsMargins(0, 0, 0, 0);
0102 
0103     // When a new item has been created by the "Create New..." menu, the item should
0104     // get selected and it must be assured that the item will get visible. As the
0105     // creation is done asynchronously, several signals must be checked:
0106     connect(&DolphinNewFileMenuObserver::instance(), &DolphinNewFileMenuObserver::itemCreated, this, &DolphinView::observeCreatedItem);
0107 
0108     m_selectionChangedTimer = new QTimer(this);
0109     m_selectionChangedTimer->setSingleShot(true);
0110     m_selectionChangedTimer->setInterval(300);
0111     connect(m_selectionChangedTimer, &QTimer::timeout, this, &DolphinView::emitSelectionChangedSignal);
0112 
0113     m_model = new KFileItemModel(this);
0114     m_view = new DolphinItemListView();
0115     m_view->setEnabledSelectionToggles(DolphinItemListView::SelectionTogglesEnabled::FollowSetting);
0116     m_view->setVisibleRoles({"text"});
0117     applyModeToView();
0118 
0119     KItemListController *controller = new KItemListController(m_model, m_view, this);
0120     const int delay = GeneralSettings::autoExpandFolders() ? 750 : -1;
0121     controller->setAutoActivationDelay(delay);
0122 
0123     // The EnlargeSmallPreviews setting can only be changed after the model
0124     // has been set in the view by KItemListController.
0125     m_view->setEnlargeSmallPreviews(GeneralSettings::enlargeSmallPreviews());
0126 
0127     m_container = new KItemListContainer(controller, this);
0128     m_container->installEventFilter(this);
0129 #ifndef QT_NO_ACCESSIBILITY
0130     m_view->setAccessibleParentsObject(m_container);
0131 #endif
0132     setFocusProxy(m_container);
0133     connect(m_container->horizontalScrollBar(), &QScrollBar::valueChanged, this, [=] {
0134         hideToolTip();
0135     });
0136     connect(m_container->verticalScrollBar(), &QScrollBar::valueChanged, this, [=] {
0137         hideToolTip();
0138     });
0139 
0140     m_showLoadingPlaceholderTimer = new QTimer(this);
0141     m_showLoadingPlaceholderTimer->setInterval(500);
0142     m_showLoadingPlaceholderTimer->setSingleShot(true);
0143     connect(m_showLoadingPlaceholderTimer, &QTimer::timeout, this, &DolphinView::showLoadingPlaceholder);
0144 
0145     // Show some placeholder text for empty folders
0146     // This is made using a heavily-modified QLabel rather than a KTitleWidget
0147     // because KTitleWidget can't be told to turn off mouse-selectable text
0148     m_placeholderLabel = new QLabel(this);
0149     // Don't consume mouse events
0150     m_placeholderLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
0151 
0152     QFont placeholderLabelFont;
0153     // To match the size of a level 2 Heading/KTitleWidget
0154     placeholderLabelFont.setPointSize(qRound(placeholderLabelFont.pointSize() * 1.3));
0155     m_placeholderLabel->setFont(placeholderLabelFont);
0156     m_placeholderLabel->setWordWrap(true);
0157     m_placeholderLabel->setAlignment(Qt::AlignCenter);
0158     // Match opacity of QML placeholder label component
0159     auto *effect = new QGraphicsOpacityEffect(m_placeholderLabel);
0160     effect->setOpacity(0.5);
0161     m_placeholderLabel->setGraphicsEffect(effect);
0162     // Set initial text and visibility
0163     updatePlaceholderLabel();
0164 
0165     auto *centeringLayout = new QVBoxLayout(m_container);
0166     centeringLayout->addWidget(m_placeholderLabel);
0167     centeringLayout->setAlignment(m_placeholderLabel, Qt::AlignCenter);
0168 
0169     controller->setSelectionBehavior(KItemListController::MultiSelection);
0170     connect(controller, &KItemListController::itemActivated, this, &DolphinView::slotItemActivated);
0171     connect(controller, &KItemListController::itemsActivated, this, &DolphinView::slotItemsActivated);
0172     connect(controller, &KItemListController::itemMiddleClicked, this, &DolphinView::slotItemMiddleClicked);
0173     connect(controller, &KItemListController::itemContextMenuRequested, this, &DolphinView::slotItemContextMenuRequested);
0174     connect(controller, &KItemListController::viewContextMenuRequested, this, &DolphinView::slotViewContextMenuRequested);
0175     connect(controller, &KItemListController::headerContextMenuRequested, this, &DolphinView::slotHeaderContextMenuRequested);
0176     connect(controller, &KItemListController::mouseButtonPressed, this, &DolphinView::slotMouseButtonPressed);
0177     connect(controller, &KItemListController::itemHovered, this, &DolphinView::slotItemHovered);
0178     connect(controller, &KItemListController::itemUnhovered, this, &DolphinView::slotItemUnhovered);
0179     connect(controller, &KItemListController::itemDropEvent, this, &DolphinView::slotItemDropEvent);
0180     connect(controller, &KItemListController::escapePressed, this, &DolphinView::stopLoading);
0181     connect(controller, &KItemListController::modelChanged, this, &DolphinView::slotModelChanged);
0182     connect(controller, &KItemListController::selectedItemTextPressed, this, &DolphinView::slotSelectedItemTextPressed);
0183     connect(controller, &KItemListController::increaseZoom, this, &DolphinView::slotIncreaseZoom);
0184     connect(controller, &KItemListController::decreaseZoom, this, &DolphinView::slotDecreaseZoom);
0185     connect(controller, &KItemListController::swipeUp, this, &DolphinView::slotSwipeUp);
0186     connect(controller, &KItemListController::selectionModeChangeRequested, this, &DolphinView::selectionModeChangeRequested);
0187 
0188     connect(m_model, &KFileItemModel::directoryLoadingStarted, this, &DolphinView::slotDirectoryLoadingStarted);
0189     connect(m_model, &KFileItemModel::directoryLoadingCompleted, this, &DolphinView::slotDirectoryLoadingCompleted);
0190     connect(m_model, &KFileItemModel::directoryLoadingCanceled, this, &DolphinView::slotDirectoryLoadingCanceled);
0191     connect(m_model, &KFileItemModel::directoryLoadingProgress, this, &DolphinView::directoryLoadingProgress);
0192     connect(m_model, &KFileItemModel::directorySortingProgress, this, &DolphinView::directorySortingProgress);
0193     connect(m_model, &KFileItemModel::itemsChanged, this, &DolphinView::slotItemsChanged);
0194     connect(m_model, &KFileItemModel::itemsRemoved, this, &DolphinView::itemCountChanged);
0195     connect(m_model, &KFileItemModel::itemsInserted, this, &DolphinView::itemCountChanged);
0196     connect(m_model, &KFileItemModel::infoMessage, this, &DolphinView::infoMessage);
0197     connect(m_model, &KFileItemModel::errorMessage, this, &DolphinView::errorMessage);
0198     connect(m_model, &KFileItemModel::directoryRedirection, this, &DolphinView::slotDirectoryRedirection);
0199     connect(m_model, &KFileItemModel::urlIsFileError, this, &DolphinView::urlIsFileError);
0200     connect(m_model, &KFileItemModel::fileItemsChanged, this, &DolphinView::fileItemsChanged);
0201     connect(m_model, &KFileItemModel::currentDirectoryRemoved, this, &DolphinView::currentDirectoryRemoved);
0202 
0203     connect(this, &DolphinView::itemCountChanged, this, &DolphinView::updatePlaceholderLabel);
0204 
0205     m_view->installEventFilter(this);
0206     connect(m_view, &DolphinItemListView::sortOrderChanged, this, &DolphinView::slotSortOrderChangedByHeader);
0207     connect(m_view, &DolphinItemListView::sortRoleChanged, this, &DolphinView::slotSortRoleChangedByHeader);
0208     connect(m_view, &DolphinItemListView::visibleRolesChanged, this, &DolphinView::slotVisibleRolesChangedByHeader);
0209     connect(m_view, &DolphinItemListView::roleEditingCanceled, this, &DolphinView::slotRoleEditingCanceled);
0210 
0211     connect(m_view, &DolphinItemListView::columnHovered, this, [this](int columnIndex) {
0212         m_hoveredColumnHeaderIndex = columnIndex;
0213     });
0214     connect(m_view, &DolphinItemListView::columnUnHovered, this, [this](int /* columnIndex */) {
0215         m_hoveredColumnHeaderIndex = std::nullopt;
0216     });
0217     connect(m_view->header(), &KItemListHeader::columnWidthChangeFinished, this, &DolphinView::slotHeaderColumnWidthChangeFinished);
0218     connect(m_view->header(), &KItemListHeader::sidePaddingChanged, this, &DolphinView::slotSidePaddingWidthChanged);
0219 
0220     KItemListSelectionManager *selectionManager = controller->selectionManager();
0221     connect(selectionManager, &KItemListSelectionManager::selectionChanged, this, &DolphinView::slotSelectionChanged);
0222 
0223 #if HAVE_BALOO
0224     m_toolTipManager = new ToolTipManager(this);
0225     connect(m_toolTipManager, &ToolTipManager::urlActivated, this, &DolphinView::urlActivated);
0226 #endif
0227 
0228     m_versionControlObserver = new VersionControlObserver(this);
0229     m_versionControlObserver->setView(this);
0230     m_versionControlObserver->setModel(m_model);
0231     connect(m_versionControlObserver, &VersionControlObserver::infoMessage, this, &DolphinView::infoMessage);
0232     connect(m_versionControlObserver, &VersionControlObserver::errorMessage, this, &DolphinView::errorMessage);
0233     connect(m_versionControlObserver, &VersionControlObserver::operationCompletedMessage, this, &DolphinView::operationCompletedMessage);
0234 
0235     m_twoClicksRenamingTimer = new QTimer(this);
0236     m_twoClicksRenamingTimer->setSingleShot(true);
0237     connect(m_twoClicksRenamingTimer, &QTimer::timeout, this, &DolphinView::slotTwoClicksRenamingTimerTimeout);
0238 
0239     applyViewProperties();
0240     m_topLayout->addWidget(m_container);
0241 
0242     loadDirectory(url);
0243 }
0244 
0245 DolphinView::~DolphinView()
0246 {
0247     disconnect(m_container->controller(), &KItemListController::modelChanged, this, &DolphinView::slotModelChanged);
0248 }
0249 
0250 QUrl DolphinView::url() const
0251 {
0252     return m_url;
0253 }
0254 
0255 void DolphinView::setActive(bool active)
0256 {
0257     if (active == m_active) {
0258         return;
0259     }
0260 
0261     m_active = active;
0262 
0263     updatePalette();
0264 
0265     if (active) {
0266         m_container->setFocus();
0267         Q_EMIT activated();
0268     }
0269 }
0270 
0271 bool DolphinView::isActive() const
0272 {
0273     return m_active;
0274 }
0275 
0276 void DolphinView::setViewMode(Mode mode)
0277 {
0278     if (mode != m_mode) {
0279         // Reset scrollbars before changing the view mode.
0280         m_container->horizontalScrollBar()->setValue(0);
0281         m_container->verticalScrollBar()->setValue(0);
0282 
0283         ViewProperties props(viewPropertiesUrl());
0284         props.setViewMode(mode);
0285 
0286         // We pass the new ViewProperties to applyViewProperties, rather than
0287         // storing them on disk and letting applyViewProperties() read them
0288         // from there, to prevent that changing the view mode fails if the
0289         // .directory file is not writable (see bug 318534).
0290         applyViewProperties(props);
0291     }
0292 }
0293 
0294 DolphinView::Mode DolphinView::viewMode() const
0295 {
0296     return m_mode;
0297 }
0298 
0299 void DolphinView::setSelectionModeEnabled(const bool enabled)
0300 {
0301     if (enabled) {
0302         if (!m_proxyStyle) {
0303             m_proxyStyle = std::make_unique<SelectionMode::SingleClickSelectionProxyStyle>();
0304         }
0305         setStyle(m_proxyStyle.get());
0306         m_view->setStyle(m_proxyStyle.get());
0307         m_view->setEnabledSelectionToggles(DolphinItemListView::SelectionTogglesEnabled::False);
0308     } else {
0309         setStyle(nullptr);
0310         m_view->setStyle(nullptr);
0311         m_view->setEnabledSelectionToggles(DolphinItemListView::SelectionTogglesEnabled::FollowSetting);
0312     }
0313     m_container->controller()->setSelectionModeEnabled(enabled);
0314 }
0315 
0316 bool DolphinView::selectionMode() const
0317 {
0318     return m_container->controller()->selectionMode();
0319 }
0320 
0321 void DolphinView::setPreviewsShown(bool show)
0322 {
0323     if (previewsShown() == show) {
0324         return;
0325     }
0326 
0327     ViewProperties props(viewPropertiesUrl());
0328     props.setPreviewsShown(show);
0329 
0330     const int oldZoomLevel = m_view->zoomLevel();
0331     m_view->setPreviewsShown(show);
0332     Q_EMIT previewsShownChanged(show);
0333 
0334     const int newZoomLevel = m_view->zoomLevel();
0335     if (newZoomLevel != oldZoomLevel) {
0336         Q_EMIT zoomLevelChanged(newZoomLevel, oldZoomLevel);
0337     }
0338 }
0339 
0340 bool DolphinView::previewsShown() const
0341 {
0342     return m_view->previewsShown();
0343 }
0344 
0345 void DolphinView::setHiddenFilesShown(bool show)
0346 {
0347     if (m_model->showHiddenFiles() == show) {
0348         return;
0349     }
0350 
0351     const KFileItemList itemList = selectedItems();
0352     m_selectedUrls.clear();
0353     m_selectedUrls = itemList.urlList();
0354 
0355     ViewProperties props(viewPropertiesUrl());
0356     props.setHiddenFilesShown(show);
0357 
0358     m_model->setShowHiddenFiles(show);
0359     Q_EMIT hiddenFilesShownChanged(show);
0360 }
0361 
0362 bool DolphinView::hiddenFilesShown() const
0363 {
0364     return m_model->showHiddenFiles();
0365 }
0366 
0367 void DolphinView::setGroupedSorting(bool grouped)
0368 {
0369     if (grouped == groupedSorting()) {
0370         return;
0371     }
0372 
0373     ViewProperties props(viewPropertiesUrl());
0374     props.setGroupedSorting(grouped);
0375     props.save();
0376 
0377     m_container->controller()->model()->setGroupedSorting(grouped);
0378 
0379     Q_EMIT groupedSortingChanged(grouped);
0380 }
0381 
0382 bool DolphinView::groupedSorting() const
0383 {
0384     return m_model->groupedSorting();
0385 }
0386 
0387 KFileItemList DolphinView::items() const
0388 {
0389     KFileItemList list;
0390     const int itemCount = m_model->count();
0391     list.reserve(itemCount);
0392 
0393     for (int i = 0; i < itemCount; ++i) {
0394         list.append(m_model->fileItem(i));
0395     }
0396 
0397     return list;
0398 }
0399 
0400 int DolphinView::itemsCount() const
0401 {
0402     return m_model->count();
0403 }
0404 
0405 KFileItemList DolphinView::selectedItems() const
0406 {
0407     const KItemListSelectionManager *selectionManager = m_container->controller()->selectionManager();
0408 
0409     KFileItemList selectedItems;
0410     const auto items = selectionManager->selectedItems();
0411     selectedItems.reserve(items.count());
0412     for (int index : items) {
0413         selectedItems.append(m_model->fileItem(index));
0414     }
0415     return selectedItems;
0416 }
0417 
0418 int DolphinView::selectedItemsCount() const
0419 {
0420     const KItemListSelectionManager *selectionManager = m_container->controller()->selectionManager();
0421     return selectionManager->selectedItems().count();
0422 }
0423 
0424 void DolphinView::markUrlsAsSelected(const QList<QUrl> &urls)
0425 {
0426     m_selectedUrls = urls;
0427     m_selectJobCreatedItems = false;
0428 }
0429 
0430 void DolphinView::markUrlAsCurrent(const QUrl &url)
0431 {
0432     m_currentItemUrl = url;
0433     m_scrollToCurrentItem = true;
0434 }
0435 
0436 void DolphinView::selectItems(const QRegularExpression &regexp, bool enabled)
0437 {
0438     const KItemListSelectionManager::SelectionMode mode = enabled ? KItemListSelectionManager::Select : KItemListSelectionManager::Deselect;
0439     KItemListSelectionManager *selectionManager = m_container->controller()->selectionManager();
0440 
0441     for (int index = 0; index < m_model->count(); index++) {
0442         const KFileItem item = m_model->fileItem(index);
0443         if (regexp.match(item.text()).hasMatch()) {
0444             // An alternative approach would be to store the matching items in a KItemSet and
0445             // select them in one go after the loop, but we'd need a new function
0446             // KItemListSelectionManager::setSelected(KItemSet, SelectionMode mode)
0447             // for that.
0448             selectionManager->setSelected(index, 1, mode);
0449         }
0450     }
0451 }
0452 
0453 void DolphinView::setZoomLevel(int level)
0454 {
0455     const int oldZoomLevel = zoomLevel();
0456     m_view->setZoomLevel(level);
0457     if (zoomLevel() != oldZoomLevel) {
0458         hideToolTip();
0459         Q_EMIT zoomLevelChanged(zoomLevel(), oldZoomLevel);
0460     }
0461 }
0462 
0463 int DolphinView::zoomLevel() const
0464 {
0465     return m_view->zoomLevel();
0466 }
0467 
0468 void DolphinView::setSortRole(const QByteArray &role)
0469 {
0470     if (role != sortRole()) {
0471         updateSortRole(role);
0472     }
0473 }
0474 
0475 QByteArray DolphinView::sortRole() const
0476 {
0477     const KItemModelBase *model = m_container->controller()->model();
0478     return model->sortRole();
0479 }
0480 
0481 void DolphinView::setSortOrder(Qt::SortOrder order)
0482 {
0483     if (sortOrder() != order) {
0484         updateSortOrder(order);
0485     }
0486 }
0487 
0488 Qt::SortOrder DolphinView::sortOrder() const
0489 {
0490     return m_model->sortOrder();
0491 }
0492 
0493 void DolphinView::setSortFoldersFirst(bool foldersFirst)
0494 {
0495     if (sortFoldersFirst() != foldersFirst) {
0496         updateSortFoldersFirst(foldersFirst);
0497     }
0498 }
0499 
0500 bool DolphinView::sortFoldersFirst() const
0501 {
0502     return m_model->sortDirectoriesFirst();
0503 }
0504 
0505 void DolphinView::setSortHiddenLast(bool hiddenLast)
0506 {
0507     if (sortHiddenLast() != hiddenLast) {
0508         updateSortHiddenLast(hiddenLast);
0509     }
0510 }
0511 
0512 bool DolphinView::sortHiddenLast() const
0513 {
0514     return m_model->sortHiddenLast();
0515 }
0516 
0517 void DolphinView::setVisibleRoles(const QList<QByteArray> &roles)
0518 {
0519     const QList<QByteArray> previousRoles = roles;
0520 
0521     ViewProperties props(viewPropertiesUrl());
0522     props.setVisibleRoles(roles);
0523 
0524     m_visibleRoles = roles;
0525     m_view->setVisibleRoles(roles);
0526 
0527     Q_EMIT visibleRolesChanged(m_visibleRoles, previousRoles);
0528 }
0529 
0530 QList<QByteArray> DolphinView::visibleRoles() const
0531 {
0532     return m_visibleRoles;
0533 }
0534 
0535 void DolphinView::reload()
0536 {
0537     QByteArray viewState;
0538     QDataStream saveStream(&viewState, QIODevice::WriteOnly);
0539     saveState(saveStream);
0540 
0541     setUrl(url());
0542     loadDirectory(url(), true);
0543 
0544     QDataStream restoreStream(viewState);
0545     restoreState(restoreStream);
0546 }
0547 
0548 void DolphinView::readSettings()
0549 {
0550     const int oldZoomLevel = m_view->zoomLevel();
0551 
0552     GeneralSettings::self()->load();
0553     m_view->readSettings();
0554     applyViewProperties();
0555 
0556     const int delay = GeneralSettings::autoExpandFolders() ? 750 : -1;
0557     m_container->controller()->setAutoActivationDelay(delay);
0558 
0559     const int newZoomLevel = m_view->zoomLevel();
0560     if (newZoomLevel != oldZoomLevel) {
0561         Q_EMIT zoomLevelChanged(newZoomLevel, oldZoomLevel);
0562     }
0563 }
0564 
0565 void DolphinView::writeSettings()
0566 {
0567     GeneralSettings::self()->save();
0568     m_view->writeSettings();
0569 }
0570 
0571 void DolphinView::setNameFilter(const QString &nameFilter)
0572 {
0573     m_model->setNameFilter(nameFilter);
0574 }
0575 
0576 QString DolphinView::nameFilter() const
0577 {
0578     return m_model->nameFilter();
0579 }
0580 
0581 void DolphinView::setMimeTypeFilters(const QStringList &filters)
0582 {
0583     return m_model->setMimeTypeFilters(filters);
0584 }
0585 
0586 QStringList DolphinView::mimeTypeFilters() const
0587 {
0588     return m_model->mimeTypeFilters();
0589 }
0590 
0591 void DolphinView::requestStatusBarText()
0592 {
0593     if (m_statJobForStatusBarText) {
0594         // Kill the pending request.
0595         m_statJobForStatusBarText->kill();
0596     }
0597 
0598     if (m_container->controller()->selectionManager()->hasSelection()) {
0599         int folderCount = 0;
0600         int fileCount = 0;
0601         KIO::filesize_t totalFileSize = 0;
0602 
0603         // Give a summary of the status of the selected files
0604         const KFileItemList list = selectedItems();
0605         for (const KFileItem &item : list) {
0606             if (item.isDir()) {
0607                 ++folderCount;
0608             } else {
0609                 ++fileCount;
0610                 totalFileSize += item.size();
0611             }
0612         }
0613 
0614         if (folderCount + fileCount == 1) {
0615             // If only one item is selected, show info about it
0616             Q_EMIT statusBarTextChanged(list.first().getStatusBarInfo());
0617         } else {
0618             // At least 2 items are selected
0619             emitStatusBarText(folderCount, fileCount, totalFileSize, HasSelection);
0620         }
0621     } else { // has no selection
0622         if (!m_model->rootItem().url().isValid()) {
0623             return;
0624         }
0625 
0626         m_statJobForStatusBarText = KIO::stat(m_model->rootItem().url(), KIO::StatJob::SourceSide, KIO::StatRecursiveSize, KIO::HideProgressInfo);
0627         connect(m_statJobForStatusBarText, &KJob::result, this, &DolphinView::slotStatJobResult);
0628         m_statJobForStatusBarText->start();
0629     }
0630 }
0631 
0632 void DolphinView::emitStatusBarText(const int folderCount, const int fileCount, KIO::filesize_t totalFileSize, const Selection selection)
0633 {
0634     QString foldersText;
0635     QString filesText;
0636     QString summary;
0637 
0638     if (selection == HasSelection) {
0639         // At least 2 items are selected because the case of 1 selected item is handled in
0640         // DolphinView::requestStatusBarText().
0641         foldersText = i18ncp("@info:status", "1 folder selected", "%1 folders selected", folderCount);
0642         filesText = i18ncp("@info:status", "1 file selected", "%1 files selected", fileCount);
0643     } else {
0644         foldersText = i18ncp("@info:status", "1 folder", "%1 folders", folderCount);
0645         filesText = i18ncp("@info:status", "1 file", "%1 files", fileCount);
0646     }
0647 
0648     if (fileCount > 0 && folderCount > 0) {
0649         summary = i18nc("@info:status folders, files (size)", "%1, %2 (%3)", foldersText, filesText, KFormat().formatByteSize(totalFileSize));
0650     } else if (fileCount > 0) {
0651         summary = i18nc("@info:status files (size)", "%1 (%2)", filesText, KFormat().formatByteSize(totalFileSize));
0652     } else if (folderCount > 0) {
0653         summary = foldersText;
0654     } else {
0655         summary = i18nc("@info:status", "0 folders, 0 files");
0656     }
0657     Q_EMIT statusBarTextChanged(summary);
0658 }
0659 
0660 QList<QAction *> DolphinView::versionControlActions(const KFileItemList &items) const
0661 {
0662     QList<QAction *> actions;
0663 
0664     if (items.isEmpty()) {
0665         const KFileItem item = m_model->rootItem();
0666         if (!item.isNull()) {
0667             actions = m_versionControlObserver->actions(KFileItemList() << item);
0668         }
0669     } else {
0670         actions = m_versionControlObserver->actions(items);
0671     }
0672 
0673     return actions;
0674 }
0675 
0676 void DolphinView::setUrl(const QUrl &url)
0677 {
0678     if (url == m_url) {
0679         return;
0680     }
0681 
0682     clearSelection();
0683 
0684     m_url = url;
0685 
0686     hideToolTip();
0687 
0688     disconnect(m_view, &DolphinItemListView::roleEditingFinished, this, &DolphinView::slotRoleEditingFinished);
0689 
0690     // It is important to clear the items from the model before
0691     // applying the view properties, otherwise expensive operations
0692     // might be done on the existing items although they get cleared
0693     // anyhow afterwards by loadDirectory().
0694     m_model->clear();
0695     applyViewProperties();
0696     loadDirectory(url);
0697 
0698     Q_EMIT urlChanged(url);
0699 }
0700 
0701 void DolphinView::selectAll()
0702 {
0703     KItemListSelectionManager *selectionManager = m_container->controller()->selectionManager();
0704     selectionManager->setSelected(0, m_model->count());
0705 }
0706 
0707 void DolphinView::invertSelection()
0708 {
0709     KItemListSelectionManager *selectionManager = m_container->controller()->selectionManager();
0710     selectionManager->setSelected(0, m_model->count(), KItemListSelectionManager::Toggle);
0711 }
0712 
0713 void DolphinView::clearSelection()
0714 {
0715     m_selectJobCreatedItems = false;
0716     m_selectedUrls.clear();
0717     m_container->controller()->selectionManager()->clearSelection();
0718 }
0719 
0720 void DolphinView::renameSelectedItems()
0721 {
0722     const KFileItemList items = selectedItems();
0723     if (items.isEmpty()) {
0724         return;
0725     }
0726 
0727     if (items.count() == 1 && GeneralSettings::renameInline()) {
0728         const int index = m_model->index(items.first());
0729 
0730         QMetaObject::Connection *const connection = new QMetaObject::Connection;
0731         *connection = connect(m_view, &KItemListView::scrollingStopped, this, [=]() {
0732             QObject::disconnect(*connection);
0733             delete connection;
0734 
0735             m_view->editRole(index, "text");
0736 
0737             hideToolTip();
0738 
0739             connect(m_view, &DolphinItemListView::roleEditingFinished, this, &DolphinView::slotRoleEditingFinished);
0740         });
0741         m_view->scrollToItem(index);
0742 
0743     } else {
0744         KIO::RenameFileDialog *dialog = new KIO::RenameFileDialog(items, this);
0745         connect(dialog, &KIO::RenameFileDialog::renamingFinished, this, &DolphinView::slotRenameDialogRenamingFinished);
0746 
0747         dialog->open();
0748     }
0749 
0750     // Assure that the current index remains visible when KFileItemModel
0751     // will notify the view about changed items (which might result in
0752     // a changed sorting).
0753     m_assureVisibleCurrentIndex = true;
0754 }
0755 
0756 void DolphinView::trashSelectedItems()
0757 {
0758     const QList<QUrl> list = simplifiedSelectedUrls();
0759 
0760     using Iface = KIO::AskUserActionInterface;
0761     auto *trashJob = new KIO::DeleteOrTrashJob(list, Iface::Trash, Iface::DefaultConfirmation, this);
0762     connect(trashJob, &KJob::result, this, &DolphinView::slotTrashFileFinished);
0763     m_selectNextItem = true;
0764     trashJob->start();
0765 }
0766 
0767 void DolphinView::deleteSelectedItems()
0768 {
0769     const QList<QUrl> list = simplifiedSelectedUrls();
0770 
0771     using Iface = KIO::AskUserActionInterface;
0772     auto *trashJob = new KIO::DeleteOrTrashJob(list, Iface::Delete, Iface::DefaultConfirmation, this);
0773     connect(trashJob, &KJob::result, this, &DolphinView::slotTrashFileFinished);
0774     m_selectNextItem = true;
0775     trashJob->start();
0776 }
0777 
0778 void DolphinView::cutSelectedItemsToClipboard()
0779 {
0780     QMimeData *mimeData = selectionMimeData();
0781     KIO::setClipboardDataCut(mimeData, true);
0782     KUrlMimeData::exportUrlsToPortal(mimeData);
0783     QApplication::clipboard()->setMimeData(mimeData);
0784 }
0785 
0786 void DolphinView::copySelectedItemsToClipboard()
0787 {
0788     QMimeData *mimeData = selectionMimeData();
0789     KUrlMimeData::exportUrlsToPortal(mimeData);
0790     QApplication::clipboard()->setMimeData(mimeData);
0791 }
0792 
0793 void DolphinView::copySelectedItems(const KFileItemList &selection, const QUrl &destinationUrl)
0794 {
0795     if (selection.isEmpty() || !destinationUrl.isValid()) {
0796         return;
0797     }
0798 
0799     m_clearSelectionBeforeSelectingNewItems = true;
0800     m_markFirstNewlySelectedItemAsCurrent = true;
0801     m_selectJobCreatedItems = true;
0802 
0803     KIO::CopyJob *job = KIO::copy(selection.urlList(), destinationUrl, KIO::DefaultFlags);
0804     KJobWidgets::setWindow(job, this);
0805 
0806     connect(job, &KIO::DropJob::result, this, &DolphinView::slotJobResult);
0807     connect(job, &KIO::CopyJob::copying, this, &DolphinView::slotItemCreatedFromJob);
0808     connect(job, &KIO::CopyJob::copyingDone, this, &DolphinView::slotItemCreatedFromJob);
0809     KIO::FileUndoManager::self()->recordCopyJob(job);
0810 }
0811 
0812 void DolphinView::moveSelectedItems(const KFileItemList &selection, const QUrl &destinationUrl)
0813 {
0814     if (selection.isEmpty() || !destinationUrl.isValid()) {
0815         return;
0816     }
0817 
0818     m_clearSelectionBeforeSelectingNewItems = true;
0819     m_markFirstNewlySelectedItemAsCurrent = true;
0820     m_selectJobCreatedItems = true;
0821 
0822     KIO::CopyJob *job = KIO::move(selection.urlList(), destinationUrl, KIO::DefaultFlags);
0823     KJobWidgets::setWindow(job, this);
0824 
0825     connect(job, &KIO::DropJob::result, this, &DolphinView::slotJobResult);
0826     connect(job, &KIO::CopyJob::moving, this, &DolphinView::slotItemCreatedFromJob);
0827     connect(job, &KIO::CopyJob::copyingDone, this, &DolphinView::slotItemCreatedFromJob);
0828     KIO::FileUndoManager::self()->recordCopyJob(job);
0829 }
0830 
0831 void DolphinView::paste()
0832 {
0833     pasteToUrl(url());
0834 }
0835 
0836 void DolphinView::pasteIntoFolder()
0837 {
0838     const KFileItemList items = selectedItems();
0839     if ((items.count() == 1) && items.first().isDir()) {
0840         pasteToUrl(items.first().url());
0841     }
0842 }
0843 
0844 void DolphinView::duplicateSelectedItems()
0845 {
0846     const KFileItemList itemList = selectedItems();
0847     if (itemList.isEmpty()) {
0848         return;
0849     }
0850 
0851     const QMimeDatabase db;
0852 
0853     // Duplicate all selected items and append "copy" to the end of the file name
0854     // but before the filename extension, if present
0855     QList<QUrl> newSelection;
0856     for (const auto &item : itemList) {
0857         const QUrl originalURL = item.url();
0858         const QString originalDirectoryPath = originalURL.adjusted(QUrl::RemoveFilename).path();
0859         const QString originalFileName = item.name();
0860 
0861         QString extension = db.suffixForFileName(originalFileName);
0862 
0863         QUrl duplicateURL = originalURL;
0864 
0865         // No extension; new filename is "<oldfilename> copy"
0866         if (extension.isEmpty()) {
0867             duplicateURL.setPath(originalDirectoryPath + i18nc("<filename> copy", "%1 copy", originalFileName));
0868             // There's an extension; new filename is "<oldfilename> copy.<extension>"
0869         } else {
0870             // Need to add a dot since QMimeDatabase::suffixForFileName() doesn't include it
0871             extension = QLatin1String(".") + extension;
0872             const QString originalFilenameWithoutExtension = originalFileName.chopped(extension.size());
0873             // Preserve file's original filename extension in case the casing differs
0874             // from what QMimeDatabase::suffixForFileName() returned
0875             const QString originalExtension = originalFileName.right(extension.size());
0876             duplicateURL.setPath(originalDirectoryPath + i18nc("<filename> copy", "%1 copy", originalFilenameWithoutExtension) + originalExtension);
0877         }
0878 
0879         KIO::CopyJob *job = KIO::copyAs(originalURL, duplicateURL);
0880         KJobWidgets::setWindow(job, this);
0881 
0882         if (job) {
0883             newSelection << duplicateURL;
0884             KIO::FileUndoManager::self()->recordCopyJob(job);
0885         }
0886     }
0887 
0888     forceUrlsSelection(newSelection.first(), newSelection);
0889 }
0890 
0891 void DolphinView::stopLoading()
0892 {
0893     m_model->cancelDirectoryLoading();
0894 }
0895 
0896 void DolphinView::updatePalette()
0897 {
0898     QColor color = KColorScheme(isActiveWindow() ? QPalette::Active : QPalette::Inactive, KColorScheme::View).background().color();
0899     if (!m_active) {
0900         color.setAlpha(150);
0901     }
0902 
0903     QWidget *viewport = m_container->viewport();
0904     if (viewport) {
0905         QPalette palette;
0906         palette.setColor(viewport->backgroundRole(), color);
0907         viewport->setPalette(palette);
0908     }
0909 
0910     update();
0911 }
0912 
0913 void DolphinView::abortTwoClicksRenaming()
0914 {
0915     m_twoClicksRenamingItemUrl.clear();
0916     m_twoClicksRenamingTimer->stop();
0917 }
0918 
0919 bool DolphinView::eventFilter(QObject *watched, QEvent *event)
0920 {
0921     switch (event->type()) {
0922     case QEvent::PaletteChange:
0923         updatePalette();
0924         QPixmapCache::clear();
0925         break;
0926 
0927     case QEvent::WindowActivate:
0928     case QEvent::WindowDeactivate:
0929         updatePalette();
0930         break;
0931 
0932     case QEvent::KeyPress:
0933         hideToolTip(ToolTipManager::HideBehavior::Instantly);
0934         if (GeneralSettings::useTabForSwitchingSplitView()) {
0935             QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
0936             if (keyEvent->key() == Qt::Key_Tab && keyEvent->modifiers() == Qt::NoModifier) {
0937                 Q_EMIT toggleActiveViewRequested();
0938                 return true;
0939             }
0940         }
0941         break;
0942     case QEvent::KeyRelease:
0943         if (static_cast<QKeyEvent *>(event)->key() == Qt::Key_Control) {
0944             m_controlWheelAccumulatedDelta = 0;
0945         }
0946         break;
0947     case QEvent::FocusIn:
0948         if (watched == m_container) {
0949             setActive(true);
0950         }
0951         break;
0952 
0953     case QEvent::GraphicsSceneDragEnter:
0954         if (watched == m_view) {
0955             m_dragging = true;
0956             abortTwoClicksRenaming();
0957         }
0958         break;
0959 
0960     case QEvent::GraphicsSceneDragLeave:
0961         if (watched == m_view) {
0962             m_dragging = false;
0963         }
0964         break;
0965 
0966     case QEvent::GraphicsSceneDrop:
0967         if (watched == m_view) {
0968             m_dragging = false;
0969         }
0970         break;
0971 
0972     case QEvent::ToolTip: {
0973         const auto helpEvent = static_cast<QHelpEvent *>(event);
0974         if (tryShowNameToolTip(helpEvent)) {
0975             return true;
0976 
0977         } else if (m_hoveredColumnHeaderIndex) {
0978             const auto rolesInfo = KFileItemModel::rolesInformation();
0979             const auto visibleRole = m_visibleRoles.value(*m_hoveredColumnHeaderIndex);
0980 
0981             for (const KFileItemModel::RoleInfo &info : rolesInfo) {
0982                 if (visibleRole == info.role) {
0983                     QToolTip::showText(helpEvent->globalPos(), info.tooltip, this);
0984                     return true;
0985                 }
0986             }
0987         }
0988         break;
0989     }
0990     default:
0991         break;
0992     }
0993 
0994     return QWidget::eventFilter(watched, event);
0995 }
0996 
0997 void DolphinView::wheelEvent(QWheelEvent *event)
0998 {
0999     if (event->modifiers().testFlag(Qt::ControlModifier)) {
1000         m_controlWheelAccumulatedDelta += event->angleDelta().y();
1001 
1002         if (m_controlWheelAccumulatedDelta <= -QWheelEvent::DefaultDeltasPerStep) {
1003             slotDecreaseZoom();
1004             m_controlWheelAccumulatedDelta += QWheelEvent::DefaultDeltasPerStep;
1005         } else if (m_controlWheelAccumulatedDelta >= QWheelEvent::DefaultDeltasPerStep) {
1006             slotIncreaseZoom();
1007             m_controlWheelAccumulatedDelta -= QWheelEvent::DefaultDeltasPerStep;
1008         }
1009 
1010         event->accept();
1011     } else {
1012         event->ignore();
1013     }
1014 }
1015 
1016 void DolphinView::hideEvent(QHideEvent *event)
1017 {
1018     hideToolTip();
1019     QWidget::hideEvent(event);
1020 }
1021 
1022 bool DolphinView::event(QEvent *event)
1023 {
1024     if (event->type() == QEvent::WindowDeactivate) {
1025         /* See Bug 297355
1026          * Dolphin leaves file preview tooltips open even when is not visible.
1027          *
1028          * Hide tool-tip when Dolphin loses focus.
1029          */
1030         hideToolTip();
1031         abortTwoClicksRenaming();
1032     }
1033 
1034     return QWidget::event(event);
1035 }
1036 
1037 void DolphinView::activate()
1038 {
1039     setActive(true);
1040 }
1041 
1042 void DolphinView::slotItemActivated(int index)
1043 {
1044     abortTwoClicksRenaming();
1045 
1046     const KFileItem item = m_model->fileItem(index);
1047     if (!item.isNull()) {
1048         Q_EMIT itemActivated(item);
1049     }
1050 }
1051 
1052 void DolphinView::slotItemsActivated(const KItemSet &indexes)
1053 {
1054     Q_ASSERT(indexes.count() >= 2);
1055 
1056     abortTwoClicksRenaming();
1057 
1058     const auto modifiers = QGuiApplication::keyboardModifiers();
1059 
1060     if (indexes.count() > 5) {
1061         QString question = i18np("Are you sure you want to open 1 item?", "Are you sure you want to open %1 items?", indexes.count());
1062         const int answer = KMessageBox::warningContinueCancel(
1063             this,
1064             question,
1065             {},
1066             KGuiItem(i18ncp("@action:button", "Open %1 Item", "Open %1 Items", indexes.count()), QStringLiteral("document-open")),
1067             KStandardGuiItem::cancel(),
1068             QStringLiteral("ConfirmOpenManyFolders"));
1069         if (answer != KMessageBox::PrimaryAction && answer != KMessageBox::Continue) {
1070             return;
1071         }
1072     }
1073 
1074     KFileItemList items;
1075     items.reserve(indexes.count());
1076 
1077     for (int index : indexes) {
1078         KFileItem item = m_model->fileItem(index);
1079         const QUrl &url = openItemAsFolderUrl(item);
1080 
1081         if (!url.isEmpty()) {
1082             // Open folders in new tabs or in new windows depending on the modifier
1083             // The ctrl+shift behavior is ignored because we are handling multiple items
1084             // keep in sync with KUrlNavigator::slotNavigatorButtonClicked
1085             if (modifiers & Qt::ShiftModifier && !(modifiers & Qt::ControlModifier)) {
1086                 Q_EMIT windowRequested(url);
1087             } else {
1088                 Q_EMIT tabRequested(url);
1089             }
1090         } else {
1091             items.append(item);
1092         }
1093     }
1094 
1095     if (items.count() == 1) {
1096         Q_EMIT itemActivated(items.first());
1097     } else if (items.count() > 1) {
1098         Q_EMIT itemsActivated(items);
1099     }
1100 }
1101 
1102 void DolphinView::slotItemMiddleClicked(int index)
1103 {
1104     const KFileItem &item = m_model->fileItem(index);
1105     const QUrl &url = openItemAsFolderUrl(item);
1106     const auto modifiers = QGuiApplication::keyboardModifiers();
1107     if (!url.isEmpty()) {
1108         // keep in sync with KUrlNavigator::slotNavigatorButtonClicked
1109         if (modifiers & Qt::ShiftModifier) {
1110             Q_EMIT activeTabRequested(url);
1111         } else {
1112             Q_EMIT tabRequested(url);
1113         }
1114     } else if (isTabsForFilesEnabled()) {
1115         // keep in sync with KUrlNavigator::slotNavigatorButtonClicked
1116         if (modifiers & Qt::ShiftModifier) {
1117             Q_EMIT activeTabRequested(item.url());
1118         } else {
1119             Q_EMIT tabRequested(item.url());
1120         }
1121     } else {
1122         Q_EMIT fileMiddleClickActivated(item);
1123     }
1124 }
1125 
1126 void DolphinView::slotItemContextMenuRequested(int index, const QPointF &pos)
1127 {
1128     // Force emit of a selection changed signal before we request the
1129     // context menu, to update the edit-actions first. (See Bug 294013)
1130     if (m_selectionChangedTimer->isActive()) {
1131         emitSelectionChangedSignal();
1132     }
1133 
1134     const KFileItem item = m_model->fileItem(index);
1135     Q_EMIT requestContextMenu(pos.toPoint(), item, selectedItems(), url());
1136 }
1137 
1138 void DolphinView::slotViewContextMenuRequested(const QPointF &pos)
1139 {
1140     Q_EMIT requestContextMenu(pos.toPoint(), KFileItem(), selectedItems(), url());
1141 }
1142 
1143 void DolphinView::slotHeaderContextMenuRequested(const QPointF &pos)
1144 {
1145     ViewProperties props(viewPropertiesUrl());
1146 
1147     QPointer<QMenu> menu = new QMenu(this);
1148 
1149     KItemListView *view = m_container->controller()->view();
1150     const QList<QByteArray> visibleRolesSet = view->visibleRoles();
1151 
1152     bool indexingEnabled = false;
1153 #if HAVE_BALOO
1154     Baloo::IndexerConfig config;
1155     indexingEnabled = config.fileIndexingEnabled();
1156 #endif
1157 
1158     QString groupName;
1159     QMenu *groupMenu = nullptr;
1160 
1161     // Add all roles to the menu that can be shown or hidden by the user
1162     const QList<KFileItemModel::RoleInfo> rolesInfo = KFileItemModel::rolesInformation();
1163     for (const KFileItemModel::RoleInfo &info : rolesInfo) {
1164         if (info.role == "text") {
1165             // It should not be possible to hide the "text" role
1166             continue;
1167         }
1168 
1169         const QString text = m_model->roleDescription(info.role);
1170         QAction *action = nullptr;
1171         if (info.group.isEmpty()) {
1172             action = menu->addAction(text);
1173         } else {
1174             if (!groupMenu || info.group != groupName) {
1175                 groupName = info.group;
1176                 groupMenu = menu->addMenu(groupName);
1177             }
1178 
1179             action = groupMenu->addAction(text);
1180         }
1181 
1182         action->setCheckable(true);
1183         action->setChecked(visibleRolesSet.contains(info.role));
1184         action->setData(info.role);
1185         action->setToolTip(info.tooltip);
1186 
1187         const bool enable = (!info.requiresBaloo && !info.requiresIndexer) || (info.requiresBaloo) || (info.requiresIndexer && indexingEnabled);
1188         action->setEnabled(enable);
1189     }
1190 
1191     menu->addSeparator();
1192 
1193     QActionGroup *widthsGroup = new QActionGroup(menu);
1194     const bool autoColumnWidths = props.headerColumnWidths().isEmpty();
1195 
1196     QAction *toggleSidePaddingAction = menu->addAction(i18nc("@action:inmenu", "Side Padding"));
1197     toggleSidePaddingAction->setCheckable(true);
1198     toggleSidePaddingAction->setChecked(view->header()->sidePadding() > 0);
1199 
1200     QAction *autoAdjustWidthsAction = menu->addAction(i18nc("@action:inmenu", "Automatic Column Widths"));
1201     autoAdjustWidthsAction->setCheckable(true);
1202     autoAdjustWidthsAction->setChecked(autoColumnWidths);
1203     autoAdjustWidthsAction->setActionGroup(widthsGroup);
1204 
1205     QAction *customWidthsAction = menu->addAction(i18nc("@action:inmenu", "Custom Column Widths"));
1206     customWidthsAction->setCheckable(true);
1207     customWidthsAction->setChecked(!autoColumnWidths);
1208     customWidthsAction->setActionGroup(widthsGroup);
1209 
1210     QAction *action = menu->exec(pos.toPoint());
1211     if (menu && action) {
1212         KItemListHeader *header = view->header();
1213 
1214         if (action == autoAdjustWidthsAction) {
1215             // Clear the column-widths from the viewproperties and turn on
1216             // the automatic resizing of the columns
1217             props.setHeaderColumnWidths(QList<int>());
1218             header->setAutomaticColumnResizing(true);
1219         } else if (action == customWidthsAction) {
1220             // Apply the current column-widths as custom column-widths and turn
1221             // off the automatic resizing of the columns
1222             QList<int> columnWidths;
1223             const auto visibleRoles = view->visibleRoles();
1224             columnWidths.reserve(visibleRoles.count());
1225             for (const QByteArray &role : visibleRoles) {
1226                 columnWidths.append(header->columnWidth(role));
1227             }
1228             props.setHeaderColumnWidths(columnWidths);
1229             header->setAutomaticColumnResizing(false);
1230         } else if (action == toggleSidePaddingAction) {
1231             header->setSidePadding(toggleSidePaddingAction->isChecked() ? 20 : 0);
1232         } else {
1233             // Show or hide the selected role
1234             const QByteArray selectedRole = action->data().toByteArray();
1235 
1236             QList<QByteArray> visibleRoles = view->visibleRoles();
1237             if (action->isChecked()) {
1238                 visibleRoles.append(selectedRole);
1239             } else {
1240                 visibleRoles.removeOne(selectedRole);
1241             }
1242 
1243             view->setVisibleRoles(visibleRoles);
1244             props.setVisibleRoles(visibleRoles);
1245 
1246             QList<int> columnWidths;
1247             if (!header->automaticColumnResizing()) {
1248                 const auto visibleRoles = view->visibleRoles();
1249                 columnWidths.reserve(visibleRoles.count());
1250                 for (const QByteArray &role : visibleRoles) {
1251                     columnWidths.append(header->columnWidth(role));
1252                 }
1253             }
1254             props.setHeaderColumnWidths(columnWidths);
1255         }
1256     }
1257 
1258     delete menu;
1259 }
1260 
1261 void DolphinView::slotHeaderColumnWidthChangeFinished(const QByteArray &role, qreal current)
1262 {
1263     const QList<QByteArray> visibleRoles = m_view->visibleRoles();
1264 
1265     ViewProperties props(viewPropertiesUrl());
1266     QList<int> columnWidths = props.headerColumnWidths();
1267     if (columnWidths.count() != visibleRoles.count()) {
1268         columnWidths.clear();
1269         columnWidths.reserve(visibleRoles.count());
1270         const KItemListHeader *header = m_view->header();
1271         for (const QByteArray &role : visibleRoles) {
1272             const int width = header->columnWidth(role);
1273             columnWidths.append(width);
1274         }
1275     }
1276 
1277     const int roleIndex = visibleRoles.indexOf(role);
1278     Q_ASSERT(roleIndex >= 0 && roleIndex < columnWidths.count());
1279     columnWidths[roleIndex] = current;
1280 
1281     props.setHeaderColumnWidths(columnWidths);
1282 }
1283 
1284 void DolphinView::slotSidePaddingWidthChanged(qreal width)
1285 {
1286     ViewProperties props(viewPropertiesUrl());
1287     DetailsModeSettings::setSidePadding(int(width));
1288     m_view->writeSettings();
1289 }
1290 
1291 void DolphinView::slotItemHovered(int index)
1292 {
1293     const KFileItem item = m_model->fileItem(index);
1294 
1295     if (GeneralSettings::showToolTips() && !m_dragging) {
1296         QRectF itemRect = m_container->controller()->view()->itemContextRect(index);
1297         const QPoint pos = m_container->mapToGlobal(itemRect.topLeft().toPoint());
1298         itemRect.moveTo(pos);
1299 
1300 #if HAVE_BALOO
1301         auto nativeParent = nativeParentWidget();
1302         if (nativeParent) {
1303             m_toolTipManager->showToolTip(item, itemRect, nativeParent->windowHandle());
1304         }
1305 #endif
1306     }
1307 
1308     Q_EMIT requestItemInfo(item);
1309 }
1310 
1311 void DolphinView::slotItemUnhovered(int index)
1312 {
1313     Q_UNUSED(index)
1314     hideToolTip();
1315     Q_EMIT requestItemInfo(KFileItem());
1316 }
1317 
1318 void DolphinView::slotItemDropEvent(int index, QGraphicsSceneDragDropEvent *event)
1319 {
1320     QUrl destUrl;
1321     KFileItem destItem = m_model->fileItem(index);
1322     if (destItem.isNull() || (!destItem.isDir() && !destItem.isDesktopFile())) {
1323         // Use the URL of the view as drop target if the item is no directory
1324         // or desktop-file
1325         destItem = m_model->rootItem();
1326         destUrl = url();
1327     } else {
1328         // The item represents a directory or desktop-file
1329         destUrl = destItem.mostLocalUrl();
1330     }
1331 
1332     QDropEvent dropEvent(event->pos().toPoint(), event->possibleActions(), event->mimeData(), event->buttons(), event->modifiers());
1333     dropUrls(destUrl, &dropEvent, this);
1334 
1335     setActive(true);
1336 }
1337 
1338 void DolphinView::dropUrls(const QUrl &destUrl, QDropEvent *dropEvent, QWidget *dropWidget)
1339 {
1340     KIO::DropJob *job = DragAndDropHelper::dropUrls(destUrl, dropEvent, dropWidget);
1341 
1342     if (job) {
1343         connect(job, &KIO::DropJob::result, this, &DolphinView::slotJobResult);
1344 
1345         if (destUrl == url()) {
1346             // Mark the dropped urls as selected.
1347             m_clearSelectionBeforeSelectingNewItems = true;
1348             m_markFirstNewlySelectedItemAsCurrent = true;
1349             m_selectJobCreatedItems = true;
1350             connect(job, &KIO::DropJob::itemCreated, this, &DolphinView::slotItemCreated);
1351             connect(job, &KIO::DropJob::copyJobStarted, this, [this](const KIO::CopyJob *copyJob) {
1352                 connect(copyJob, &KIO::CopyJob::copying, this, &DolphinView::slotItemCreatedFromJob);
1353                 connect(copyJob, &KIO::CopyJob::moving, this, &DolphinView::slotItemCreatedFromJob);
1354                 connect(copyJob, &KIO::CopyJob::linking, this, [this](KIO::Job *job, const QString &src, const QUrl &dest) {
1355                     Q_UNUSED(job)
1356                     Q_UNUSED(src)
1357                     slotItemCreated(dest);
1358                 });
1359             });
1360         }
1361     }
1362 }
1363 
1364 void DolphinView::slotModelChanged(KItemModelBase *current, KItemModelBase *previous)
1365 {
1366     if (previous != nullptr) {
1367         Q_ASSERT(qobject_cast<KFileItemModel *>(previous));
1368         KFileItemModel *fileItemModel = static_cast<KFileItemModel *>(previous);
1369         disconnect(fileItemModel, &KFileItemModel::directoryLoadingCompleted, this, &DolphinView::slotDirectoryLoadingCompleted);
1370         m_versionControlObserver->setModel(nullptr);
1371     }
1372 
1373     if (current) {
1374         Q_ASSERT(qobject_cast<KFileItemModel *>(current));
1375         KFileItemModel *fileItemModel = static_cast<KFileItemModel *>(current);
1376         connect(fileItemModel, &KFileItemModel::directoryLoadingCompleted, this, &DolphinView::slotDirectoryLoadingCompleted);
1377         m_versionControlObserver->setModel(fileItemModel);
1378     }
1379 }
1380 
1381 void DolphinView::slotMouseButtonPressed(int itemIndex, Qt::MouseButtons buttons)
1382 {
1383     Q_UNUSED(itemIndex)
1384 
1385     hideToolTip();
1386 
1387     if (buttons & Qt::BackButton) {
1388         Q_EMIT goBackRequested();
1389     } else if (buttons & Qt::ForwardButton) {
1390         Q_EMIT goForwardRequested();
1391     }
1392 }
1393 
1394 void DolphinView::slotSelectedItemTextPressed(int index)
1395 {
1396     if (GeneralSettings::renameInline() && !m_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick)) {
1397         const KFileItem item = m_model->fileItem(index);
1398         const KFileItemListProperties capabilities(KFileItemList() << item);
1399         if (capabilities.supportsMoving()) {
1400             m_twoClicksRenamingItemUrl = item.url();
1401             m_twoClicksRenamingTimer->start(QApplication::doubleClickInterval());
1402         }
1403     }
1404 }
1405 
1406 void DolphinView::slotItemCreatedFromJob(KIO::Job *, const QUrl &, const QUrl &to)
1407 {
1408     slotItemCreated(to);
1409 }
1410 
1411 void DolphinView::slotItemCreated(const QUrl &url)
1412 {
1413     if (m_markFirstNewlySelectedItemAsCurrent) {
1414         markUrlAsCurrent(url);
1415         m_markFirstNewlySelectedItemAsCurrent = false;
1416     }
1417     if (m_selectJobCreatedItems && !m_selectedUrls.contains(url)) {
1418         m_selectedUrls << url;
1419     }
1420 }
1421 
1422 void DolphinView::onDirectoryLoadingCompletedAfterJob()
1423 {
1424     // the model should now contain all the items created by the job
1425     m_selectJobCreatedItems = true; // to make sure we overwrite selection
1426     // update the view: scroll into View and selection
1427     updateViewState();
1428     m_selectJobCreatedItems = false;
1429     m_selectedUrls.clear();
1430 }
1431 
1432 void DolphinView::slotJobResult(KJob *job)
1433 {
1434     if (job->error() && job->error() != KIO::ERR_USER_CANCELED) {
1435         Q_EMIT errorMessage(job->errorString());
1436     }
1437     if (!m_selectJobCreatedItems) {
1438         m_selectedUrls.clear();
1439         return;
1440     }
1441     if (!m_selectedUrls.isEmpty()) {
1442         m_selectedUrls = KDirModel::simplifiedUrlList(m_selectedUrls);
1443 
1444         updateSelectionState();
1445         if (!m_selectedUrls.isEmpty()) {
1446             // not all urls were found, the model may not be up to date
1447             connect(m_model, &KFileItemModel::directoryLoadingCompleted, this, &DolphinView::onDirectoryLoadingCompletedAfterJob, Qt::SingleShotConnection);
1448         } else {
1449             m_selectJobCreatedItems = false;
1450             m_selectedUrls.clear();
1451         }
1452     }
1453 }
1454 
1455 void DolphinView::slotSelectionChanged(const KItemSet &current, const KItemSet &previous)
1456 {
1457     m_selectNextItem = false;
1458     const int currentCount = current.count();
1459     const int previousCount = previous.count();
1460     const bool selectionStateChanged = (currentCount == 0 && previousCount > 0) || (currentCount > 0 && previousCount == 0);
1461 
1462     // If nothing has been selected before and something got selected (or if something
1463     // was selected before and now nothing is selected) the selectionChangedSignal must
1464     // be emitted asynchronously as fast as possible to update the edit-actions.
1465     m_selectionChangedTimer->setInterval(selectionStateChanged ? 0 : 300);
1466     m_selectionChangedTimer->start();
1467 }
1468 
1469 void DolphinView::emitSelectionChangedSignal()
1470 {
1471     m_selectionChangedTimer->stop();
1472     Q_EMIT selectionChanged(selectedItems());
1473 }
1474 
1475 void DolphinView::slotStatJobResult(KJob *job)
1476 {
1477     int folderCount = 0;
1478     int fileCount = 0;
1479     KIO::filesize_t totalFileSize = 0;
1480     bool countFileSize = true;
1481 
1482     const auto entry = static_cast<KIO::StatJob *>(job)->statResult();
1483     if (entry.contains(KIO::UDSEntry::UDS_RECURSIVE_SIZE)) {
1484         // We have a precomputed value.
1485         totalFileSize = static_cast<KIO::filesize_t>(entry.numberValue(KIO::UDSEntry::UDS_RECURSIVE_SIZE));
1486         countFileSize = false;
1487     }
1488 
1489     const int itemCount = m_model->count();
1490     for (int i = 0; i < itemCount; ++i) {
1491         const KFileItem item = m_model->fileItem(i);
1492         if (item.isDir()) {
1493             ++folderCount;
1494         } else {
1495             ++fileCount;
1496             if (countFileSize) {
1497                 totalFileSize += item.size();
1498             }
1499         }
1500     }
1501     emitStatusBarText(folderCount, fileCount, totalFileSize, NoSelection);
1502 }
1503 
1504 void DolphinView::updateSortRole(const QByteArray &role)
1505 {
1506     ViewProperties props(viewPropertiesUrl());
1507     props.setSortRole(role);
1508 
1509     KItemModelBase *model = m_container->controller()->model();
1510     model->setSortRole(role);
1511 
1512     Q_EMIT sortRoleChanged(role);
1513 }
1514 
1515 void DolphinView::updateSortOrder(Qt::SortOrder order)
1516 {
1517     ViewProperties props(viewPropertiesUrl());
1518     props.setSortOrder(order);
1519 
1520     m_model->setSortOrder(order);
1521 
1522     Q_EMIT sortOrderChanged(order);
1523 }
1524 
1525 void DolphinView::updateSortFoldersFirst(bool foldersFirst)
1526 {
1527     ViewProperties props(viewPropertiesUrl());
1528     props.setSortFoldersFirst(foldersFirst);
1529 
1530     m_model->setSortDirectoriesFirst(foldersFirst);
1531 
1532     Q_EMIT sortFoldersFirstChanged(foldersFirst);
1533 }
1534 
1535 void DolphinView::updateSortHiddenLast(bool hiddenLast)
1536 {
1537     ViewProperties props(viewPropertiesUrl());
1538     props.setSortHiddenLast(hiddenLast);
1539 
1540     m_model->setSortHiddenLast(hiddenLast);
1541 
1542     Q_EMIT sortHiddenLastChanged(hiddenLast);
1543 }
1544 
1545 QPair<bool, QString> DolphinView::pasteInfo() const
1546 {
1547     const QMimeData *mimeData = QApplication::clipboard()->mimeData();
1548     QPair<bool, QString> info;
1549     info.second = KIO::pasteActionText(mimeData, &info.first, rootItem());
1550     return info;
1551 }
1552 
1553 void DolphinView::setTabsForFilesEnabled(bool tabsForFiles)
1554 {
1555     m_tabsForFiles = tabsForFiles;
1556 }
1557 
1558 bool DolphinView::isTabsForFilesEnabled() const
1559 {
1560     return m_tabsForFiles;
1561 }
1562 
1563 bool DolphinView::itemsExpandable() const
1564 {
1565     return m_mode == DetailsView;
1566 }
1567 
1568 bool DolphinView::isExpanded(const KFileItem &item) const
1569 {
1570     Q_ASSERT(item.isDir());
1571     Q_ASSERT(items().contains(item));
1572     if (!itemsExpandable()) {
1573         return false;
1574     }
1575     return m_model->isExpanded(m_model->index(item));
1576 }
1577 
1578 void DolphinView::restoreState(QDataStream &stream)
1579 {
1580     // Read the version number of the view state and check if the version is supported.
1581     quint32 version = 0;
1582     stream >> version;
1583     if (version != 1) {
1584         // The version of the view state isn't supported, we can't restore it.
1585         return;
1586     }
1587 
1588     // Restore the current item that had the keyboard focus
1589     stream >> m_currentItemUrl;
1590 
1591     // Restore the previously selected items
1592     stream >> m_selectedUrls;
1593 
1594     // Restore the view position
1595     stream >> m_restoredContentsPosition;
1596 
1597     // Restore expanded folders (only relevant for the details view - will be ignored by the view in other view modes)
1598     QSet<QUrl> urls;
1599     stream >> urls;
1600     m_model->restoreExpandedDirectories(urls);
1601 }
1602 
1603 void DolphinView::saveState(QDataStream &stream)
1604 {
1605     stream << quint32(1); // View state version
1606 
1607     // Save the current item that has the keyboard focus
1608     const int currentIndex = m_container->controller()->selectionManager()->currentItem();
1609     if (currentIndex != -1) {
1610         KFileItem item = m_model->fileItem(currentIndex);
1611         Q_ASSERT(!item.isNull()); // If the current index is valid a item must exist
1612         QUrl currentItemUrl = item.url();
1613         stream << currentItemUrl;
1614     } else {
1615         stream << QUrl();
1616     }
1617 
1618     // Save the selected urls
1619     stream << selectedItems().urlList();
1620 
1621     // Save view position
1622     const qreal x = m_container->horizontalScrollBar()->value();
1623     const qreal y = m_container->verticalScrollBar()->value();
1624     stream << QPoint(x, y);
1625 
1626     // Save expanded folders (only relevant for the details view - the set will be empty in other view modes)
1627     stream << m_model->expandedDirectories();
1628 }
1629 
1630 KFileItem DolphinView::rootItem() const
1631 {
1632     return m_model->rootItem();
1633 }
1634 
1635 void DolphinView::setViewPropertiesContext(const QString &context)
1636 {
1637     m_viewPropertiesContext = context;
1638 }
1639 
1640 QString DolphinView::viewPropertiesContext() const
1641 {
1642     return m_viewPropertiesContext;
1643 }
1644 
1645 QUrl DolphinView::openItemAsFolderUrl(const KFileItem &item, const bool browseThroughArchives)
1646 {
1647     if (item.isNull()) {
1648         return QUrl();
1649     }
1650 
1651     QUrl url = item.targetUrl();
1652 
1653     if (item.isDir()) {
1654         return url;
1655     }
1656 
1657     if (item.isMimeTypeKnown()) {
1658         const QString &mimetype = item.mimetype();
1659 
1660         if (browseThroughArchives && item.isFile() && url.isLocalFile()) {
1661             // Generic mechanism for redirecting to tar:/<path>/ when clicking on a tar file,
1662             // zip:/<path>/ when clicking on a zip file, etc.
1663             // The .protocol file specifies the mimetype that the kioslave handles.
1664             // Note that we don't use mimetype inheritance since we don't want to
1665             // open OpenDocument files as zip folders...
1666             const QString &protocol = KProtocolManager::protocolForArchiveMimetype(mimetype);
1667             if (!protocol.isEmpty()) {
1668                 url.setScheme(protocol);
1669                 return url;
1670             }
1671         }
1672 
1673         if (mimetype == QLatin1String("application/x-desktop")) {
1674             // Redirect to the URL in Type=Link desktop files, unless it is a http(s) URL.
1675             KDesktopFile desktopFile(url.toLocalFile());
1676             if (desktopFile.hasLinkType()) {
1677                 const QString linkUrl = desktopFile.readUrl();
1678                 if (!linkUrl.startsWith(QLatin1String("http"))) {
1679                     return QUrl::fromUserInput(linkUrl);
1680                 }
1681             }
1682         }
1683     }
1684 
1685     return QUrl();
1686 }
1687 
1688 void DolphinView::resetZoomLevel()
1689 {
1690     ViewModeSettings settings{m_mode};
1691     settings.useDefaults(true);
1692     const int defaultIconSize = settings.iconSize();
1693     settings.useDefaults(false);
1694 
1695     setZoomLevel(ZoomLevelInfo::zoomLevelForIconSize(QSize(defaultIconSize, defaultIconSize)));
1696 }
1697 
1698 void DolphinView::observeCreatedItem(const QUrl &url)
1699 {
1700     if (m_active) {
1701         forceUrlsSelection(url, {url});
1702     }
1703 }
1704 
1705 void DolphinView::slotDirectoryRedirection(const QUrl &oldUrl, const QUrl &newUrl)
1706 {
1707     if (oldUrl.matches(url(), QUrl::StripTrailingSlash)) {
1708         Q_EMIT redirection(oldUrl, newUrl);
1709         m_url = newUrl; // #186947
1710     }
1711 }
1712 
1713 void DolphinView::updateSelectionState()
1714 {
1715     if (!m_selectedUrls.isEmpty()) {
1716         KItemListSelectionManager *selectionManager = m_container->controller()->selectionManager();
1717 
1718         // if there is a selection already, leave it that way
1719         // unless some drop/paste job are in the process of creating items
1720         if (!selectionManager->hasSelection() || m_selectJobCreatedItems) {
1721             if (m_clearSelectionBeforeSelectingNewItems) {
1722                 selectionManager->clearSelection();
1723                 m_clearSelectionBeforeSelectingNewItems = false;
1724             }
1725 
1726             KItemSet selectedItems = selectionManager->selectedItems();
1727 
1728             QList<QUrl>::iterator it = m_selectedUrls.begin();
1729             while (it != m_selectedUrls.end()) {
1730                 const int index = m_model->index(*it);
1731                 if (index >= 0) {
1732                     selectedItems.insert(index);
1733                     it = m_selectedUrls.erase(it);
1734                 } else {
1735                     ++it;
1736                 }
1737             }
1738 
1739             if (!selectedItems.isEmpty()) {
1740                 selectionManager->beginAnchoredSelection(selectionManager->currentItem());
1741                 selectionManager->setSelectedItems(selectedItems);
1742             }
1743         }
1744     }
1745 }
1746 
1747 void DolphinView::updateViewState()
1748 {
1749     if (m_currentItemUrl != QUrl()) {
1750         KItemListSelectionManager *selectionManager = m_container->controller()->selectionManager();
1751 
1752         // if there is a selection already, leave it that way
1753         if (!selectionManager->hasSelection()) {
1754             const int currentIndex = m_model->index(m_currentItemUrl);
1755             if (currentIndex != -1) {
1756                 selectionManager->setCurrentItem(currentIndex);
1757 
1758                 // scroll to current item and reset the state
1759                 if (m_scrollToCurrentItem) {
1760                     m_view->scrollToItem(currentIndex, KItemListView::ViewItemPosition::Middle);
1761                     m_scrollToCurrentItem = false;
1762                 }
1763                 m_currentItemUrl = QUrl();
1764             } else {
1765                 selectionManager->setCurrentItem(0);
1766             }
1767         } else {
1768             m_currentItemUrl = QUrl();
1769         }
1770     }
1771 
1772     if (!m_restoredContentsPosition.isNull()) {
1773         const int x = m_restoredContentsPosition.x();
1774         const int y = m_restoredContentsPosition.y();
1775         m_restoredContentsPosition = QPoint();
1776 
1777         m_container->horizontalScrollBar()->setValue(x);
1778         m_container->verticalScrollBar()->setValue(y);
1779     }
1780 
1781     updateSelectionState();
1782 }
1783 
1784 void DolphinView::hideToolTip(const ToolTipManager::HideBehavior behavior)
1785 {
1786     if (GeneralSettings::showToolTips()) {
1787 #if HAVE_BALOO
1788         m_toolTipManager->hideToolTip(behavior);
1789 #else
1790         Q_UNUSED(behavior)
1791 #endif
1792     } else if (m_mode == DolphinView::IconsView) {
1793         QToolTip::hideText();
1794     }
1795 }
1796 
1797 bool DolphinView::handleSpaceAsNormalKey() const
1798 {
1799     return !m_container->hasFocus() || m_container->controller()->isSearchAsYouTypeActive();
1800 }
1801 
1802 void DolphinView::slotTwoClicksRenamingTimerTimeout()
1803 {
1804     const KItemListSelectionManager *selectionManager = m_container->controller()->selectionManager();
1805 
1806     // verify that only one item is selected
1807     if (selectionManager->selectedItems().count() == 1) {
1808         const int index = selectionManager->currentItem();
1809         const QUrl fileItemUrl = m_model->fileItem(index).url();
1810 
1811         // check if the selected item was the same item that started the twoClicksRenaming
1812         if (fileItemUrl.isValid() && m_twoClicksRenamingItemUrl == fileItemUrl) {
1813             renameSelectedItems();
1814         }
1815     }
1816 }
1817 
1818 void DolphinView::slotTrashFileFinished(KJob *job)
1819 {
1820     if (job->error() == 0) {
1821         selectNextItem(); // Fixes BUG: 419914 via selecting next item
1822         Q_EMIT operationCompletedMessage(i18nc("@info:status", "Trash operation completed."));
1823     } else if (job->error() != KIO::ERR_USER_CANCELED) {
1824         Q_EMIT errorMessage(job->errorString());
1825     }
1826 }
1827 
1828 void DolphinView::slotDeleteFileFinished(KJob *job)
1829 {
1830     if (job->error() == 0) {
1831         selectNextItem(); // Fixes BUG: 419914 via selecting next item
1832         Q_EMIT operationCompletedMessage(i18nc("@info:status", "Delete operation completed."));
1833     } else if (job->error() != KIO::ERR_USER_CANCELED) {
1834         Q_EMIT errorMessage(job->errorString());
1835     }
1836 }
1837 
1838 void DolphinView::selectNextItem()
1839 {
1840     if (m_active && m_selectNextItem) {
1841         KItemListSelectionManager *selectionManager = m_container->controller()->selectionManager();
1842         if (selectedItems().isEmpty()) {
1843             Q_ASSERT_X(false, "DolphinView", "Selecting the next item failed.");
1844             return;
1845         }
1846         const auto lastSelectedIndex = m_model->index(selectedItems().last());
1847         if (lastSelectedIndex < 0) {
1848             Q_ASSERT_X(false, "DolphinView", "Selecting the next item failed.");
1849             return;
1850         }
1851         auto nextItem = lastSelectedIndex + 1;
1852         if (nextItem >= itemsCount()) {
1853             nextItem = lastSelectedIndex - selectedItemsCount();
1854         }
1855         if (nextItem >= 0) {
1856             selectionManager->setSelected(nextItem, 1);
1857         }
1858         m_selectNextItem = false;
1859     }
1860 }
1861 
1862 void DolphinView::slotRenamingResult(KJob *job)
1863 {
1864     if (job->error()) {
1865         KIO::CopyJob *copyJob = qobject_cast<KIO::CopyJob *>(job);
1866         Q_ASSERT(copyJob);
1867         const QUrl newUrl = copyJob->destUrl();
1868         const int index = m_model->index(newUrl);
1869         if (index >= 0) {
1870             QHash<QByteArray, QVariant> data;
1871             const QUrl oldUrl = copyJob->srcUrls().at(0);
1872             data.insert("text", oldUrl.fileName());
1873             m_model->setData(index, data);
1874         }
1875     }
1876 }
1877 
1878 void DolphinView::slotDirectoryLoadingStarted()
1879 {
1880     m_loadingState = LoadingState::Loading;
1881     updatePlaceholderLabel();
1882 
1883     // Disable the writestate temporary until it can be determined in a fast way
1884     // in DolphinView::slotDirectoryLoadingCompleted()
1885     if (m_isFolderWritable) {
1886         m_isFolderWritable = false;
1887         Q_EMIT writeStateChanged(m_isFolderWritable);
1888     }
1889 
1890     Q_EMIT directoryLoadingStarted();
1891 }
1892 
1893 void DolphinView::slotDirectoryLoadingCompleted()
1894 {
1895     m_loadingState = LoadingState::Completed;
1896 
1897     // Update the view-state. This has to be done asynchronously
1898     // because the view might not be in its final state yet.
1899     QTimer::singleShot(0, this, &DolphinView::updateViewState);
1900 
1901     // Update the placeholder label in case we found that the folder was empty
1902     // after loading it
1903 
1904     Q_EMIT directoryLoadingCompleted();
1905 
1906     updatePlaceholderLabel();
1907     updateWritableState();
1908 }
1909 
1910 void DolphinView::slotDirectoryLoadingCanceled()
1911 {
1912     m_loadingState = LoadingState::Canceled;
1913 
1914     updatePlaceholderLabel();
1915 
1916     Q_EMIT directoryLoadingCanceled();
1917 }
1918 
1919 void DolphinView::slotItemsChanged()
1920 {
1921     m_assureVisibleCurrentIndex = false;
1922 }
1923 
1924 void DolphinView::slotSortOrderChangedByHeader(Qt::SortOrder current, Qt::SortOrder previous)
1925 {
1926     Q_UNUSED(previous)
1927     Q_ASSERT(m_model->sortOrder() == current);
1928 
1929     ViewProperties props(viewPropertiesUrl());
1930     props.setSortOrder(current);
1931 
1932     Q_EMIT sortOrderChanged(current);
1933 }
1934 
1935 void DolphinView::slotSortRoleChangedByHeader(const QByteArray &current, const QByteArray &previous)
1936 {
1937     Q_UNUSED(previous)
1938     Q_ASSERT(m_model->sortRole() == current);
1939 
1940     ViewProperties props(viewPropertiesUrl());
1941     props.setSortRole(current);
1942 
1943     Q_EMIT sortRoleChanged(current);
1944 }
1945 
1946 void DolphinView::slotVisibleRolesChangedByHeader(const QList<QByteArray> &current, const QList<QByteArray> &previous)
1947 {
1948     Q_UNUSED(previous)
1949     Q_ASSERT(m_container->controller()->view()->visibleRoles() == current);
1950 
1951     const QList<QByteArray> previousVisibleRoles = m_visibleRoles;
1952 
1953     m_visibleRoles = current;
1954 
1955     ViewProperties props(viewPropertiesUrl());
1956     props.setVisibleRoles(m_visibleRoles);
1957 
1958     Q_EMIT visibleRolesChanged(m_visibleRoles, previousVisibleRoles);
1959 }
1960 
1961 void DolphinView::slotRoleEditingCanceled()
1962 {
1963     disconnect(m_view, &DolphinItemListView::roleEditingFinished, this, &DolphinView::slotRoleEditingFinished);
1964 }
1965 
1966 void DolphinView::slotRoleEditingFinished(int index, const QByteArray &role, const QVariant &value)
1967 {
1968     disconnect(m_view, &DolphinItemListView::roleEditingFinished, this, &DolphinView::slotRoleEditingFinished);
1969 
1970     const KFileItemList items = selectedItems();
1971     if (items.count() != 1) {
1972         return;
1973     }
1974 
1975     if (role == "text") {
1976         const KFileItem oldItem = items.first();
1977         const EditResult retVal = value.value<EditResult>();
1978         const QString newName = retVal.newName;
1979         if (!newName.isEmpty() && newName != oldItem.text() && newName != QLatin1Char('.') && newName != QLatin1String("..")) {
1980             const QUrl oldUrl = oldItem.url();
1981 
1982             QUrl newUrl = oldUrl.adjusted(QUrl::RemoveFilename);
1983             newUrl.setPath(newUrl.path() + KIO::encodeFileName(newName));
1984 
1985 #ifndef Q_OS_WIN
1986             // Confirm hiding file/directory by renaming inline
1987             if (!hiddenFilesShown() && newName.startsWith(QLatin1Char('.')) && !oldItem.name().startsWith(QLatin1Char('.'))) {
1988                 KGuiItem yesGuiItem(i18nc("@action:button", "Rename and Hide"), QStringLiteral("view-hidden"));
1989 
1990                 const auto code =
1991                     KMessageBox::questionTwoActions(this,
1992                                                     oldItem.isFile() ? i18n("Adding a dot to the beginning of this file's name will hide it from view.\n"
1993                                                                             "Do you still want to rename it?")
1994                                                                      : i18n("Adding a dot to the beginning of this folder's name will hide it from view.\n"
1995                                                                             "Do you still want to rename it?"),
1996                                                     oldItem.isFile() ? i18n("Hide this File?") : i18n("Hide this Folder?"),
1997                                                     yesGuiItem,
1998                                                     KStandardGuiItem::cancel(),
1999                                                     QStringLiteral("ConfirmHide"));
2000 
2001                 if (code == KMessageBox::SecondaryAction) {
2002                     return;
2003                 }
2004             }
2005 #endif
2006 
2007             const bool newNameExistsAlready = (m_model->index(newUrl) >= 0);
2008             if (!newNameExistsAlready && m_model->index(oldUrl) == index) {
2009                 // Only change the data in the model if no item with the new name
2010                 // is in the model yet. If there is an item with the new name
2011                 // already, calling KIO::CopyJob will open a dialog
2012                 // asking for a new name, and KFileItemModel will update the
2013                 // data when the dir lister signals that the file name has changed.
2014                 QHash<QByteArray, QVariant> data;
2015                 data.insert(role, retVal.newName);
2016                 m_model->setData(index, data);
2017             }
2018 
2019             KIO::Job *job = KIO::moveAs(oldUrl, newUrl);
2020             KJobWidgets::setWindow(job, this);
2021             KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Rename, {oldUrl}, newUrl, job);
2022             job->uiDelegate()->setAutoErrorHandlingEnabled(true);
2023 
2024             if (!newNameExistsAlready) {
2025                 forceUrlsSelection(newUrl, {newUrl});
2026 
2027                 // Only connect the result signal if there is no item with the new name
2028                 // in the model yet, see bug 328262.
2029                 connect(job, &KJob::result, this, &DolphinView::slotRenamingResult);
2030             }
2031         }
2032         if (retVal.direction != EditDone) {
2033             const short indexShift = retVal.direction == EditNext ? 1 : -1;
2034             m_container->controller()->selectionManager()->setSelected(index, 1, KItemListSelectionManager::Deselect);
2035             m_container->controller()->selectionManager()->setSelected(index + indexShift, 1, KItemListSelectionManager::Select);
2036             renameSelectedItems();
2037         }
2038     }
2039 }
2040 
2041 void DolphinView::loadDirectory(const QUrl &url, bool reload)
2042 {
2043     if (!url.isValid()) {
2044         const QString location(url.toDisplayString(QUrl::PreferLocalFile));
2045         if (location.isEmpty()) {
2046             Q_EMIT errorMessage(i18nc("@info:status", "The location is empty."));
2047         } else {
2048             Q_EMIT errorMessage(i18nc("@info:status", "The location '%1' is invalid.", location));
2049         }
2050         return;
2051     }
2052 
2053     if (reload) {
2054         m_model->refreshDirectory(url);
2055     } else {
2056         m_model->loadDirectory(url);
2057     }
2058 }
2059 
2060 void DolphinView::applyViewProperties()
2061 {
2062     const ViewProperties props(viewPropertiesUrl());
2063     applyViewProperties(props);
2064 }
2065 
2066 void DolphinView::applyViewProperties(const ViewProperties &props)
2067 {
2068     m_view->beginTransaction();
2069 
2070     const Mode mode = props.viewMode();
2071     if (m_mode != mode) {
2072         const Mode previousMode = m_mode;
2073         m_mode = mode;
2074 
2075         // Changing the mode might result in changing
2076         // the zoom level. Remember the old zoom level so
2077         // that zoomLevelChanged() can get emitted.
2078         const int oldZoomLevel = m_view->zoomLevel();
2079         applyModeToView();
2080 
2081         Q_EMIT modeChanged(m_mode, previousMode);
2082 
2083         if (m_view->zoomLevel() != oldZoomLevel) {
2084             Q_EMIT zoomLevelChanged(m_view->zoomLevel(), oldZoomLevel);
2085         }
2086     }
2087 
2088     const bool hiddenFilesShown = props.hiddenFilesShown();
2089     if (hiddenFilesShown != m_model->showHiddenFiles()) {
2090         m_model->setShowHiddenFiles(hiddenFilesShown);
2091         Q_EMIT hiddenFilesShownChanged(hiddenFilesShown);
2092     }
2093 
2094     const bool groupedSorting = props.groupedSorting();
2095     if (groupedSorting != m_model->groupedSorting()) {
2096         m_model->setGroupedSorting(groupedSorting);
2097         Q_EMIT groupedSortingChanged(groupedSorting);
2098     }
2099 
2100     const QByteArray sortRole = props.sortRole();
2101     if (sortRole != m_model->sortRole()) {
2102         m_model->setSortRole(sortRole);
2103         Q_EMIT sortRoleChanged(sortRole);
2104     }
2105 
2106     const Qt::SortOrder sortOrder = props.sortOrder();
2107     if (sortOrder != m_model->sortOrder()) {
2108         m_model->setSortOrder(sortOrder);
2109         Q_EMIT sortOrderChanged(sortOrder);
2110     }
2111 
2112     const bool sortFoldersFirst = props.sortFoldersFirst();
2113     if (sortFoldersFirst != m_model->sortDirectoriesFirst()) {
2114         m_model->setSortDirectoriesFirst(sortFoldersFirst);
2115         Q_EMIT sortFoldersFirstChanged(sortFoldersFirst);
2116     }
2117 
2118     const bool sortHiddenLast = props.sortHiddenLast();
2119     if (sortHiddenLast != m_model->sortHiddenLast()) {
2120         m_model->setSortHiddenLast(sortHiddenLast);
2121         Q_EMIT sortHiddenLastChanged(sortHiddenLast);
2122     }
2123 
2124     const QList<QByteArray> visibleRoles = props.visibleRoles();
2125     if (visibleRoles != m_visibleRoles) {
2126         const QList<QByteArray> previousVisibleRoles = m_visibleRoles;
2127         m_visibleRoles = visibleRoles;
2128         m_view->setVisibleRoles(visibleRoles);
2129         Q_EMIT visibleRolesChanged(m_visibleRoles, previousVisibleRoles);
2130     }
2131 
2132     const bool previewsShown = props.previewsShown();
2133     if (previewsShown != m_view->previewsShown()) {
2134         const int oldZoomLevel = zoomLevel();
2135 
2136         m_view->setPreviewsShown(previewsShown);
2137         Q_EMIT previewsShownChanged(previewsShown);
2138 
2139         // Changing the preview-state might result in a changed zoom-level
2140         if (oldZoomLevel != zoomLevel()) {
2141             Q_EMIT zoomLevelChanged(zoomLevel(), oldZoomLevel);
2142         }
2143     }
2144 
2145     KItemListView *itemListView = m_container->controller()->view();
2146     if (itemListView->isHeaderVisible()) {
2147         KItemListHeader *header = itemListView->header();
2148         const QList<int> headerColumnWidths = props.headerColumnWidths();
2149         const int rolesCount = m_visibleRoles.count();
2150         if (headerColumnWidths.count() == rolesCount) {
2151             header->setAutomaticColumnResizing(false);
2152 
2153             QHash<QByteArray, qreal> columnWidths;
2154             for (int i = 0; i < rolesCount; ++i) {
2155                 columnWidths.insert(m_visibleRoles[i], headerColumnWidths[i]);
2156             }
2157             header->setColumnWidths(columnWidths);
2158         } else {
2159             header->setAutomaticColumnResizing(true);
2160         }
2161         header->setSidePadding(DetailsModeSettings::sidePadding());
2162     }
2163 
2164     m_view->endTransaction();
2165 }
2166 
2167 void DolphinView::applyModeToView()
2168 {
2169     switch (m_mode) {
2170     case IconsView:
2171         m_view->setItemLayout(KFileItemListView::IconsLayout);
2172         break;
2173     case CompactView:
2174         m_view->setItemLayout(KFileItemListView::CompactLayout);
2175         break;
2176     case DetailsView:
2177         m_view->setItemLayout(KFileItemListView::DetailsLayout);
2178         break;
2179     default:
2180         Q_ASSERT(false);
2181         break;
2182     }
2183 }
2184 
2185 void DolphinView::pasteToUrl(const QUrl &url)
2186 {
2187     KIO::PasteJob *job = KIO::paste(QApplication::clipboard()->mimeData(), url);
2188     KJobWidgets::setWindow(job, this);
2189     m_clearSelectionBeforeSelectingNewItems = true;
2190     m_markFirstNewlySelectedItemAsCurrent = true;
2191     m_selectJobCreatedItems = true;
2192     // TODO KF6 use KIO::PasteJob::copyJobStarted to hook to earlier events copying/moving
2193     connect(job, &KIO::PasteJob::itemCreated, this, &DolphinView::slotItemCreated);
2194     connect(job, &KIO::PasteJob::result, this, &DolphinView::slotJobResult);
2195 }
2196 
2197 QList<QUrl> DolphinView::simplifiedSelectedUrls() const
2198 {
2199     QList<QUrl> urls;
2200 
2201     const KFileItemList items = selectedItems();
2202     urls.reserve(items.count());
2203     for (const KFileItem &item : items) {
2204         urls.append(item.url());
2205     }
2206 
2207     if (itemsExpandable()) {
2208         // TODO: Check if we still need KDirModel for this in KDE 5.0
2209         urls = KDirModel::simplifiedUrlList(urls);
2210     }
2211 
2212     return urls;
2213 }
2214 
2215 QMimeData *DolphinView::selectionMimeData() const
2216 {
2217     const KItemListSelectionManager *selectionManager = m_container->controller()->selectionManager();
2218     const KItemSet selectedIndexes = selectionManager->selectedItems();
2219 
2220     return m_model->createMimeData(selectedIndexes);
2221 }
2222 
2223 void DolphinView::updateWritableState()
2224 {
2225     const bool wasFolderWritable = m_isFolderWritable;
2226     m_isFolderWritable = false;
2227 
2228     KFileItem item = m_model->rootItem();
2229     if (item.isNull()) {
2230         // Try to find out if the URL is writable even if the "root item" is
2231         // null, see https://bugs.kde.org/show_bug.cgi?id=330001
2232         item = KFileItem(url());
2233         item.setDelayedMimeTypes(true);
2234     }
2235 
2236     KFileItemListProperties capabilities(KFileItemList() << item);
2237     m_isFolderWritable = capabilities.supportsWriting();
2238 
2239     if (m_isFolderWritable != wasFolderWritable) {
2240         Q_EMIT writeStateChanged(m_isFolderWritable);
2241     }
2242 }
2243 
2244 bool DolphinView::isFolderWritable() const
2245 {
2246     return m_isFolderWritable;
2247 }
2248 
2249 QUrl DolphinView::viewPropertiesUrl() const
2250 {
2251     if (m_viewPropertiesContext.isEmpty()) {
2252         return m_url;
2253     }
2254 
2255     QUrl url;
2256     url.setScheme(m_url.scheme());
2257     url.setPath(m_viewPropertiesContext);
2258     return url;
2259 }
2260 
2261 void DolphinView::slotRenameDialogRenamingFinished(const QList<QUrl> &urls)
2262 {
2263     forceUrlsSelection(urls.first(), urls);
2264 }
2265 
2266 void DolphinView::forceUrlsSelection(const QUrl &current, const QList<QUrl> &selected)
2267 {
2268     clearSelection();
2269     m_clearSelectionBeforeSelectingNewItems = true;
2270     markUrlAsCurrent(current);
2271     markUrlsAsSelected(selected);
2272 }
2273 
2274 void DolphinView::copyPathToClipboard()
2275 {
2276     const KFileItemList list = selectedItems();
2277     if (list.isEmpty()) {
2278         return;
2279     }
2280     const KFileItem &item = list.at(0);
2281     QString path = item.localPath();
2282     if (path.isEmpty()) {
2283         path = item.url().toDisplayString();
2284     }
2285     QClipboard *clipboard = QApplication::clipboard();
2286     if (clipboard == nullptr) {
2287         return;
2288     }
2289     clipboard->setText(QDir::toNativeSeparators(path));
2290 }
2291 
2292 void DolphinView::slotIncreaseZoom()
2293 {
2294     setZoomLevel(zoomLevel() + 1);
2295 }
2296 
2297 void DolphinView::slotDecreaseZoom()
2298 {
2299     setZoomLevel(zoomLevel() - 1);
2300 }
2301 
2302 void DolphinView::slotSwipeUp()
2303 {
2304     Q_EMIT goUpRequested();
2305 }
2306 
2307 void DolphinView::showLoadingPlaceholder()
2308 {
2309     m_placeholderLabel->setText(i18n("Loading…"));
2310     m_placeholderLabel->setVisible(true);
2311 }
2312 
2313 void DolphinView::updatePlaceholderLabel()
2314 {
2315     m_showLoadingPlaceholderTimer->stop();
2316     if (itemsCount() > 0) {
2317         m_placeholderLabel->setVisible(false);
2318         return;
2319     }
2320 
2321     if (m_loadingState == LoadingState::Loading) {
2322         m_placeholderLabel->setVisible(false);
2323         m_showLoadingPlaceholderTimer->start();
2324         return;
2325     }
2326 
2327     if (m_loadingState == LoadingState::Canceled) {
2328         m_placeholderLabel->setText(i18n("Loading canceled"));
2329     } else if (!nameFilter().isEmpty()) {
2330         m_placeholderLabel->setText(i18n("No items matching the filter"));
2331     } else if (m_url.scheme() == QLatin1String("baloosearch") || m_url.scheme() == QLatin1String("filenamesearch")) {
2332         m_placeholderLabel->setText(i18n("No items matching the search"));
2333     } else if (m_url.scheme() == QLatin1String("trash") && m_url.path() == QLatin1String("/")) {
2334         m_placeholderLabel->setText(i18n("Trash is empty"));
2335     } else if (m_url.scheme() == QLatin1String("tags")) {
2336         if (m_url.path() == QLatin1Char('/')) {
2337             m_placeholderLabel->setText(i18n("No tags"));
2338         } else {
2339             const QString tagName = m_url.path().mid(1); // Remove leading /
2340             m_placeholderLabel->setText(i18n("No files tagged with \"%1\"", tagName));
2341         }
2342 
2343     } else if (m_url.scheme() == QLatin1String("recentlyused")) {
2344         m_placeholderLabel->setText(i18n("No recently used items"));
2345     } else if (m_url.scheme() == QLatin1String("smb")) {
2346         m_placeholderLabel->setText(i18n("No shared folders found"));
2347     } else if (m_url.scheme() == QLatin1String("network")) {
2348         m_placeholderLabel->setText(i18n("No relevant network resources found"));
2349     } else if (m_url.scheme() == QLatin1String("mtp") && m_url.path() == QLatin1String("/")) {
2350         m_placeholderLabel->setText(i18n("No MTP-compatible devices found"));
2351     } else if (m_url.scheme() == QLatin1String("afc") && m_url.path() == QLatin1String("/")) {
2352         m_placeholderLabel->setText(i18n("No Apple devices found"));
2353     } else if (m_url.scheme() == QLatin1String("bluetooth")) {
2354         m_placeholderLabel->setText(i18n("No Bluetooth devices found"));
2355     } else {
2356         m_placeholderLabel->setText(i18n("Folder is empty"));
2357     }
2358 
2359     m_placeholderLabel->setVisible(true);
2360 }
2361 
2362 bool DolphinView::tryShowNameToolTip(QHelpEvent *event)
2363 {
2364     if (!GeneralSettings::showToolTips() && m_mode == DolphinView::IconsView) {
2365         const std::optional<int> index = m_view->itemAt(event->pos());
2366 
2367         if (!index.has_value()) {
2368             return false;
2369         }
2370 
2371         // Check whether the filename has been elided
2372         const bool isElided = m_view->isElided(index.value());
2373 
2374         if (isElided) {
2375             const KFileItem item = m_model->fileItem(index.value());
2376             const QString text = item.text();
2377             const QPoint pos = mapToGlobal(event->pos());
2378             QToolTip::showText(pos, text);
2379             return true;
2380         }
2381     }
2382     return false;
2383 }
2384 
2385 #include "moc_dolphinview.cpp"