diff options
Diffstat (limited to 'src')
97 files changed, 1945 insertions, 4533 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b97a5d7c0..bb898b5a7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -31,7 +31,7 @@ generate_export_header(dolphinvcs BASE_NAME dolphinvcs) target_link_libraries( dolphinvcs PUBLIC - Qt5::Widgets + Qt${QT_MAJOR_VERSION}::Widgets ) set_target_properties(dolphinvcs PROPERTIES @@ -50,7 +50,7 @@ ecm_generate_headers(dolphinvcs_LIB_HEADERS install(TARGETS dolphinvcs EXPORT DolphinVcsTargets ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) -install(FILES views/versioncontrol/fileviewversioncontrolplugin.desktop DESTINATION ${KDE_INSTALL_KSERVICETYPES5DIR}) +install(FILES views/versioncontrol/fileviewversioncontrolplugin.desktop DESTINATION ${KDE_INSTALL_KSERVICETYPESDIR}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/dolphinvcs_export.h DESTINATION ${KDE_INSTALL_INCLUDEDIR} COMPONENT Devel) install(FILES ${dolphinvcs_LIB_HEADERS} DESTINATION "${KDE_INSTALL_INCLUDEDIR}/Dolphin" COMPONENT Devel) @@ -74,11 +74,9 @@ target_sources(dolphinprivate PRIVATE kitemviews/kitemlistwidget.cpp kitemviews/kitemmodelbase.cpp kitemviews/kitemset.cpp - kitemviews/kstandarditem.cpp kitemviews/kstandarditemlistgroupheader.cpp kitemviews/kstandarditemlistwidget.cpp kitemviews/kstandarditemlistview.cpp - kitemviews/kstandarditemmodel.cpp kitemviews/private/kdirectorycontentscounter.cpp kitemviews/private/kdirectorycontentscounterworker.cpp kitemviews/private/kfileitemclipboard.cpp @@ -147,8 +145,8 @@ generate_export_header(dolphinprivate BASE_NAME dolphin) target_link_libraries( dolphinprivate PUBLIC dolphinvcs - Qt5::Concurrent - Qt5::Gui + Qt${QT_MAJOR_VERSION}::Concurrent + Qt${QT_MAJOR_VERSION}::Gui KF5::I18n KF5::IconThemes KF5::KIOCore @@ -200,7 +198,7 @@ target_link_libraries(dolphinpart install(TARGETS dolphinpart DESTINATION ${KDE_INSTALL_PLUGINDIR}/kf5/parts) -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/dolphinpart.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/dolphinpart.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR}) ########################################## @@ -223,12 +221,6 @@ target_sources(dolphinstatic PRIVATE trash/dolphintrash.cpp filterbar/filterbar.cpp panels/places/placespanel.cpp - panels/places/placesitem.cpp - panels/places/placesitemlistgroupheader.cpp - panels/places/placesitemlistwidget.cpp - panels/places/placesitemmodel.cpp - panels/places/placesitemsignalhandler.cpp - panels/places/placesview.cpp panels/panel.cpp panels/folders/foldersitemlistwidget.cpp panels/folders/treeviewcontextmenu.cpp @@ -299,10 +291,10 @@ kconfig_add_kcfg_files(dolphinstatic GENERATE_MOC settings/dolphin_generalsettings.kcfgc ) -qt5_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/dolphinmainwindow.h org.kde.DolphinMainWindow.xml) -qt5_add_dbus_adaptor(dolphin_dbus_SRCS ${CMAKE_CURRENT_BINARY_DIR}/org.kde.DolphinMainWindow.xml dolphinmainwindow.h DolphinMainWindow) -qt5_add_dbus_interface(dolphin_dbus_SRCS ${CMAKE_CURRENT_BINARY_DIR}/org.kde.DolphinMainWindow.xml dolphinmainwindowinterface) -qt5_add_dbus_interface(dolphin_dbus_SRCS panels/terminal/org.kde.KIOFuse.VFS.xml kiofuse_interface) +qt_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/dolphinmainwindow.h org.kde.DolphinMainWindow.xml) +qt_add_dbus_adaptor(dolphin_dbus_SRCS ${CMAKE_CURRENT_BINARY_DIR}/org.kde.DolphinMainWindow.xml dolphinmainwindow.h DolphinMainWindow) +qt_add_dbus_interface(dolphin_dbus_SRCS ${CMAKE_CURRENT_BINARY_DIR}/org.kde.DolphinMainWindow.xml dolphinmainwindowinterface) +qt_add_dbus_interface(dolphin_dbus_SRCS panels/terminal/org.kde.KIOFuse.VFS.xml kiofuse_interface) target_sources(dolphinstatic PRIVATE ${dolphin_dbus_SRCS} @@ -340,6 +332,10 @@ target_sources(dolphin PRIVATE main.cpp ) +if(FLATPAK) + target_compile_definitions(dolphin PRIVATE FLATPAK) +endif() + # Sets the icon on Windows and OSX file(GLOB ICONS_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/icons/*system-file-manager.png") ecm_add_app_icon(dolphin_APPICON_SRCS ICONS ${ICONS_SRCS}) @@ -419,9 +415,9 @@ if(NOT WIN32) target_link_libraries(kcm_dolphingeneral dolphinprivate) install( FILES org.kde.dolphin.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR} ) - install( FILES settings/kcm/kcmdolphinviewmodes.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR} ) - install( FILES settings/kcm/kcmdolphinnavigation.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR} ) - install( FILES settings/kcm/kcmdolphingeneral.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR} ) + install( FILES settings/kcm/kcmdolphinviewmodes.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR} ) + install( FILES settings/kcm/kcmdolphinnavigation.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR} ) + install( FILES settings/kcm/kcmdolphingeneral.desktop DESTINATION ${KDE_INSTALL_KSERVICESDIR} ) install(TARGETS kcm_dolphinviewmodes DESTINATION ${KDE_INSTALL_PLUGINDIR}/dolphin/kcms ) install(TARGETS kcm_dolphinnavigation DESTINATION ${KDE_INSTALL_PLUGINDIR}/dolphin/kcms ) @@ -453,6 +449,5 @@ install( FILES settings/dolphin_directoryviewpropertysettings.kcfg DESTINATION ${KDE_INSTALL_KCFGDIR} ) if(BUILD_TESTING) - find_package(Qt5Test ${QT_MIN_VERSION} CONFIG REQUIRED) add_subdirectory(tests) endif() diff --git a/src/dolphincontextmenu.cpp b/src/dolphincontextmenu.cpp index 41f03aa1a..340af6bd0 100644 --- a/src/dolphincontextmenu.cpp +++ b/src/dolphincontextmenu.cpp @@ -13,8 +13,6 @@ #include "dolphinplacesmodelsingleton.h" #include "dolphinremoveaction.h" #include "dolphinviewcontainer.h" -#include "panels/places/placesitem.h" -#include "panels/places/placesitemmodel.h" #include "trash/dolphintrash.h" #include "views/dolphinview.h" #include "views/viewmodecontroller.h" @@ -197,7 +195,6 @@ void DolphinContextMenu::addDirectoryItemContextMenu() // set up 'Create New' menu DolphinNewFileMenu* newFileMenu = new DolphinNewFileMenu(m_mainWindow->actionCollection(), m_mainWindow); const DolphinView* view = m_mainWindow->activeViewContainer()->view(); - newFileMenu->setViewShowsHiddenFiles(view->hiddenFilesShown()); newFileMenu->checkUpToDate(); newFileMenu->setPopupFiles(QList<QUrl>() << m_fileInfo.url()); newFileMenu->setEnabled(selectedItemsProps.supportsWriting()); @@ -315,7 +312,6 @@ void DolphinContextMenu::openViewportContextMenu() // Set up and insert 'Create New' menu KNewFileMenu* newFileMenu = m_mainWindow->newFileMenu(); - newFileMenu->setViewShowsHiddenFiles(view->hiddenFilesShown()); newFileMenu->checkUpToDate(); newFileMenu->setPopupFiles(QList<QUrl>() << m_baseUrl); addMenu(newFileMenu->menu()); @@ -477,7 +473,7 @@ KFileItem DolphinContextMenu::baseFileItem() void DolphinContextMenu::addOpenWithActions() { // insert 'Open With...' action or sub menu - m_fileItemActions->addOpenWithActionsTo(this, QStringLiteral("DesktopEntryName != '%1'").arg(qApp->desktopFileName())); + m_fileItemActions->insertOpenWithActionsTo(nullptr, this, QStringList{qApp->desktopFileName()}); } void DolphinContextMenu::addCustomActions() diff --git a/src/dolphinmainwindow.cpp b/src/dolphinmainwindow.cpp index fa105b15a..ff3cfa3bf 100644 --- a/src/dolphinmainwindow.cpp +++ b/src/dolphinmainwindow.cpp @@ -17,12 +17,12 @@ #include "dolphinnavigatorswidgetaction.h" #include "dolphinnewfilemenu.h" #include "dolphinrecenttabsmenu.h" +#include "dolphinplacesmodelsingleton.h" #include "dolphinurlnavigatorscontroller.h" #include "dolphinviewcontainer.h" #include "dolphintabpage.h" #include "middleclickactioneventfilter.h" #include "panels/folders/folderspanel.h" -#include "panels/places/placesitemmodel.h" #include "panels/places/placespanel.h" #include "panels/terminal/terminalpanel.h" #include "settings/dolphinsettingsdialog.h" @@ -53,6 +53,7 @@ #include <KProtocolInfo> #include <KProtocolManager> #include <KShell> +#include <KShortcutsDialog> #include <KStandardAction> #include <KStartupInfo> #include <KSycoca> @@ -64,7 +65,6 @@ #include <KUrlNavigator> #include <KWindowSystem> #include <KXMLGUIFactory> -#include <kxmlgui_version.h> #include <kio_version.h> @@ -123,9 +123,7 @@ DolphinMainWindow::DolphinMainWindow() : setComponentName(QStringLiteral("dolphin"), QGuiApplication::applicationDisplayName()); setObjectName(QStringLiteral("Dolphin#")); -#if KXMLGUI_VERSION >= QT_VERSION_CHECK(5, 88, 0) setStateConfigGroup("State"); -#endif connect(&DolphinNewFileMenuObserver::instance(), &DolphinNewFileMenuObserver::errorMessage, this, &DolphinMainWindow::showErrorMessage); @@ -133,7 +131,7 @@ DolphinMainWindow::DolphinMainWindow() : KIO::FileUndoManager* undoManager = KIO::FileUndoManager::self(); undoManager->setUiInterface(new UndoUiInterface()); - connect(undoManager, QOverload<bool>::of(&KIO::FileUndoManager::undoAvailable), + connect(undoManager, &KIO::FileUndoManager::undoAvailable, this, &DolphinMainWindow::slotUndoAvailable); connect(undoManager, &KIO::FileUndoManager::undoTextChanged, this, &DolphinMainWindow::slotUndoTextChanged); @@ -173,7 +171,7 @@ DolphinMainWindow::DolphinMainWindow() : setupDockWidgets(); - setupGUI(Keys | Save | Create | ToolBar); + setupGUI(Save | Create | ToolBar); stateChanged(QStringLiteral("new_file")); QClipboard* clipboard = QApplication::clipboard(); @@ -211,7 +209,7 @@ DolphinMainWindow::DolphinMainWindow() : setupWhatsThis(); - connect(KSycoca::self(), QOverload<>::of(&KSycoca::databaseChanged), this, &DolphinMainWindow::updateOpenPreferredSearchToolAction); + connect(KSycoca::self(), &KSycoca::databaseChanged, this, &DolphinMainWindow::updateOpenPreferredSearchToolAction); QTimer::singleShot(0, this, &DolphinMainWindow::updateOpenPreferredSearchToolAction); @@ -429,14 +427,13 @@ void DolphinMainWindow::addToPlaces() name = dirToAdd.name(); } if (url.isValid()) { - PlacesItemModel model; QString icon; if (m_activeViewContainer->isSearchModeEnabled()) { icon = QStringLiteral("folder-saved-search-symbolic"); } else { icon = KIO::iconNameForUrl(url); } - model.createPlacesItem(name, url, icon); + DolphinPlacesModelSingleton::instance().placesModel()->addPlace(name, url, icon); } } @@ -445,6 +442,16 @@ void DolphinMainWindow::openNewTab(const QUrl& url) m_tabWidget->openNewTab(url, QUrl()); } +void DolphinMainWindow::openNewTabAndActivate(const QUrl &url) +{ + m_tabWidget->openNewActivatedTab(url, QUrl()); +} + +void DolphinMainWindow::openNewWindow(const QUrl &url) +{ + Dolphin::openNewWindow({url}, this); +} + void DolphinMainWindow::slotSplitViewChanged() { m_tabWidget->currentTabPage()->setSplitViewEnabled(GeneralSettings::splitView(), WithAnimation); @@ -647,14 +654,12 @@ void DolphinMainWindow::readProperties(const KConfigGroup& group) void DolphinMainWindow::updateNewMenu() { - m_newFileMenu->setViewShowsHiddenFiles(activeViewContainer()->view()->hiddenFilesShown()); m_newFileMenu->checkUpToDate(); m_newFileMenu->setPopupFiles(QList<QUrl>() << activeViewContainer()->url()); } void DolphinMainWindow::createDirectory() { - m_newFileMenu->setViewShowsHiddenFiles(activeViewContainer()->view()->hiddenFilesShown()); m_newFileMenu->setPopupFiles(QList<QUrl>() << activeViewContainer()->url()); m_newFileMenu->createDirectory(); } @@ -1368,6 +1373,19 @@ void DolphinMainWindow::slotStorageTearDownExternallyRequested(const QString& mo } } +void DolphinMainWindow::slotKeyBindings() +{ + KShortcutsDialog dialog(KShortcutsEditor::AllActions, KShortcutsEditor::LetterShortcutsAllowed, this); + dialog.addCollection(actionCollection()); + if (m_terminalPanel) { + KActionCollection *konsolePartActionCollection = m_terminalPanel->actionCollection(); + if (konsolePartActionCollection) { + dialog.addCollection(konsolePartActionCollection, QStringLiteral("KonsolePart")); + } + } + dialog.configure(); +} + void DolphinMainWindow::setViewsToHomeIfMountPathOpen(const QString& mountPath) { const QVector<DolphinViewContainer*> theViewContainers = viewContainers(); @@ -1480,7 +1498,7 @@ void DolphinMainWindow::setupActions() QAction* showFilterBar = actionCollection()->addAction(QStringLiteral("show_filter_bar")); showFilterBar->setText(i18nc("@action:inmenu Tools", "Filter...")); - showFilterBar->setToolTip(i18nc("@info:tooltip", "Toggle Filter Bar")); + showFilterBar->setToolTip(i18nc("@info:tooltip", "Show Filter Bar")); showFilterBar->setWhatsThis(xi18nc("@info:whatsthis", "This opens the " "<emphasis>Filter Bar</emphasis> at the bottom of the window.<nl/> " "There you can enter a text to filter the files and folders currently displayed. " @@ -1705,6 +1723,7 @@ void DolphinMainWindow::setupActions() "contain mostly the same commands and configuration options.")); connect(showMenuBar, &KToggleAction::triggered, // Fixes #286822 this, &DolphinMainWindow::toggleShowMenuBar, Qt::QueuedConnection); + KStandardAction::keyBindings(this, &DolphinMainWindow::slotKeyBindings, actionCollection()); KStandardAction::preferences(this, &DolphinMainWindow::editSettings, actionCollection()); // setup 'Help' menu for the m_controlButton. The other one is set up in the base class. @@ -1861,8 +1880,10 @@ void DolphinMainWindow::setupDockWidgets() foldersPanel, &FoldersPanel::setUrl); connect(foldersPanel, &FoldersPanel::folderActivated, this, &DolphinMainWindow::changeUrl); - connect(foldersPanel, &FoldersPanel::folderMiddleClicked, + connect(foldersPanel, &FoldersPanel::folderInNewTab, this, &DolphinMainWindow::openNewTab); + connect(foldersPanel, &FoldersPanel::folderInNewActiveTab, + this, &DolphinMainWindow::openNewTabAndActivate); connect(foldersPanel, &FoldersPanel::errorMessage, this, &DolphinMainWindow::showErrorMessage); @@ -1944,8 +1965,13 @@ void DolphinMainWindow::setupDockWidgets() addDockWidget(Qt::LeftDockWidgetArea, placesDock); connect(m_placesPanel, &PlacesPanel::placeActivated, this, &DolphinMainWindow::slotPlaceActivated); - connect(m_placesPanel, &PlacesPanel::placeMiddleClicked, + connect(m_placesPanel, &PlacesPanel::tabRequested, this, &DolphinMainWindow::openNewTab); + connect(m_placesPanel, &PlacesPanel::activeTabRequested, + this, &DolphinMainWindow::openNewTabAndActivate); + connect(m_placesPanel, &PlacesPanel::newWindowRequested, this, [this](const QUrl &url) { + Dolphin::openNewWindow({url}, this); + }); connect(m_placesPanel, &PlacesPanel::errorMessage, this, &DolphinMainWindow::showErrorMessage); connect(this, &DolphinMainWindow::urlChanged, @@ -1968,14 +1994,9 @@ void DolphinMainWindow::setupDockWidgets() "appear semi-transparent unless you uncheck their hide property.")); connect(actionShowAllPlaces, &QAction::triggered, this, [actionShowAllPlaces, this](bool checked){ - actionShowAllPlaces->setIcon(QIcon::fromTheme(checked ? QStringLiteral("view-visible") : QStringLiteral("view-hidden"))); - m_placesPanel->showHiddenEntries(checked); + m_placesPanel->setShowAll(checked); }); - - connect(m_placesPanel, &PlacesPanel::showHiddenEntriesChanged, this, [actionShowAllPlaces] (bool checked){ - actionShowAllPlaces->setChecked(checked); - actionShowAllPlaces->setIcon(QIcon::fromTheme(checked ? QStringLiteral("view-visible") : QStringLiteral("view-hidden"))); - }); + connect(m_placesPanel, &PlacesPanel::allPlacesShownChanged, actionShowAllPlaces, &QAction::setChecked); actionCollection()->action(QStringLiteral("show_places_panel")) ->setWhatsThis(xi18nc("@info:whatsthis", "<para>This toggles the " @@ -2013,7 +2034,7 @@ void DolphinMainWindow::setupDockWidgets() panelsMenu->addAction(lockLayoutAction); connect(panelsMenu->menu(), &QMenu::aboutToShow, this, [actionShowAllPlaces, this]{ - actionShowAllPlaces->setEnabled(m_placesPanel->hiddenListCount()); + actionShowAllPlaces->setEnabled(DolphinPlacesModelSingleton::instance().placesModel()->hiddenCount()); }); } @@ -2136,6 +2157,10 @@ void DolphinMainWindow::connectViewSignals(DolphinViewContainer* container) this, &DolphinMainWindow::updateSearchAction); connect(container, &DolphinViewContainer::captionChanged, this, &DolphinMainWindow::updateWindowTitle); + connect(container, &DolphinViewContainer::tabRequested, + this, &DolphinMainWindow::openNewTab); + connect(container, &DolphinViewContainer::activeTabRequested, + this, &DolphinMainWindow::openNewTabAndActivate); const QAction* toggleSearchAction = actionCollection()->action(QStringLiteral("toggle_search")); connect(toggleSearchAction, &QAction::triggered, container, &DolphinViewContainer::setSearchModeEnabled); @@ -2149,6 +2174,10 @@ void DolphinMainWindow::connectViewSignals(DolphinViewContainer* container) this, &DolphinMainWindow::fileItemsChanged); connect(view, &DolphinView::tabRequested, this, &DolphinMainWindow::openNewTab); + connect(view, &DolphinView::activeTabRequested, + this, &DolphinMainWindow::openNewTabAndActivate); + connect(view, &DolphinView::windowRequested, + this, &DolphinMainWindow::openNewWindow); connect(view, &DolphinView::requestContextMenu, this, &DolphinMainWindow::openContextMenu); connect(view, &DolphinView::directoryLoadingStarted, @@ -2183,6 +2212,10 @@ void DolphinMainWindow::connectViewSignals(DolphinViewContainer* container) this, &DolphinMainWindow::slotEditableStateChanged); connect(navigator, &KUrlNavigator::tabRequested, this, &DolphinMainWindow::openNewTab); + connect(navigator, &KUrlNavigator::activeTabRequested, + this, &DolphinMainWindow::openNewTabAndActivate); + connect(navigator, &KUrlNavigator::newWindowRequested, + this, &DolphinMainWindow::openNewWindow); } diff --git a/src/dolphinmainwindow.h b/src/dolphinmainwindow.h index 46515cc8b..5ada022cb 100644 --- a/src/dolphinmainwindow.h +++ b/src/dolphinmainwindow.h @@ -171,6 +171,16 @@ public Q_SLOTS: */ void openNewTab(const QUrl& url); + /** + * Opens a new tab showing the URL \a url and activate it. + */ + void openNewTabAndActivate(const QUrl &url); + + /** + * Opens a new window showing the URL \a url. + */ + void openNewWindow(const QUrl &url); + /** @see GeneralSettings::splitViewChanged() */ void slotSplitViewChanged(); @@ -566,6 +576,12 @@ private Q_SLOTS: * to go to. */ void slotGoForward(QAction* action); + + /** + * Is called when configuring the keyboard shortcuts + */ + void slotKeyBindings(); + private: /** * Sets up the various menus and actions and connects them. diff --git a/src/dolphinnavigatorswidgetaction.h b/src/dolphinnavigatorswidgetaction.h index 3f50728e9..d33482201 100644 --- a/src/dolphinnavigatorswidgetaction.h +++ b/src/dolphinnavigatorswidgetaction.h @@ -57,7 +57,7 @@ public: * This method should preferably only be called when: * - Split view is activated in the active tab * OR - * - A switch to a tab that is already in split view mode is occuring + * - A switch to a tab that is already in split view mode is occurring */ void createSecondaryUrlNavigator(); @@ -92,7 +92,7 @@ protected: * this method always returns the same widget and reparents it. * You normally don't have to use this method directly because * QWidgetAction::requestWidget() is used to obtain the navigatorsWidget - * and to steal it from whereever it was prior. + * and to steal it from wherever it was prior. * @param parent the new parent of the navigatorsWidget. */ QWidget *createWidget(QWidget *parent) override; diff --git a/src/dolphinpart.cpp b/src/dolphinpart.cpp index 8d528f418..059508778 100644 --- a/src/dolphinpart.cpp +++ b/src/dolphinpart.cpp @@ -68,7 +68,7 @@ DolphinPart::DolphinPart(QWidget* parentWidget, QObject* parent, connect(&DolphinNewFileMenuObserver::instance(), &DolphinNewFileMenuObserver::errorMessage, this, &DolphinPart::slotErrorMessage); - connect(m_view, &DolphinView::directoryLoadingCompleted, this, QOverload<>::of(&KParts::ReadOnlyPart::completed)); + connect(m_view, &DolphinView::directoryLoadingCompleted, this, &KParts::ReadOnlyPart::completed); connect(m_view, &DolphinView::directoryLoadingCompleted, this, &DolphinPart::updatePasteAction); connect(m_view, &DolphinView::directoryLoadingProgress, this, &DolphinPart::updateProgress); connect(m_view, &DolphinView::errorMessage, this, &DolphinPart::slotErrorMessage); @@ -94,7 +94,7 @@ DolphinPart::DolphinPart(QWidget* parentWidget, QObject* parent, connect(m_view, &DolphinView::requestContextMenu, this, &DolphinPart::slotOpenContextMenu); connect(m_view, &DolphinView::selectionChanged, - m_extension, QOverload<const KFileItemList&>::of(&KParts::BrowserExtension::selectionInfo)); + m_extension, &KParts::BrowserExtension::selectionInfo); connect(m_view, &DolphinView::selectionChanged, this, &DolphinPart::slotSelectionChanged); connect(m_view, &DolphinView::requestItemInfo, @@ -146,8 +146,6 @@ DolphinPart::DolphinPart(QWidget* parentWidget, QObject* parent, // TODO there was a "always open a new window" (when clicking on a directory) setting in konqueror // (sort of spacial navigation) - - loadPlugins(this, this, componentName()); } DolphinPart::~DolphinPart() @@ -593,7 +591,6 @@ void DolphinPart::updateNewMenu() { // As requested by KNewFileMenu : m_newFileMenu->checkUpToDate(); - m_newFileMenu->setViewShowsHiddenFiles(m_view->hiddenFilesShown()); // And set the files that the menu apply on : m_newFileMenu->setPopupFiles(QList<QUrl>() << url()); } @@ -610,7 +607,6 @@ void DolphinPart::updateProgress(int percent) void DolphinPart::createDirectory() { - m_newFileMenu->setViewShowsHiddenFiles(m_view->hiddenFilesShown()); m_newFileMenu->setPopupFiles(QList<QUrl>() << url()); m_newFileMenu->createDirectory(); } diff --git a/src/dolphinplacesmodelsingleton.cpp b/src/dolphinplacesmodelsingleton.cpp index 30ec1b9b6..754070c92 100644 --- a/src/dolphinplacesmodelsingleton.cpp +++ b/src/dolphinplacesmodelsingleton.cpp @@ -5,12 +5,61 @@ */ #include "dolphinplacesmodelsingleton.h" +#include "trash/dolphintrash.h" #include <KAboutData> #include <KFilePlacesModel> +#include <QIcon> + +DolphinPlacesModel::DolphinPlacesModel(const QString &alternativeApplicationName, QObject *parent) + : KFilePlacesModel(alternativeApplicationName, parent) +{ + connect(&Trash::instance(), &Trash::emptinessChanged, this, &DolphinPlacesModel::slotTrashEmptinessChanged); +} + +DolphinPlacesModel::~DolphinPlacesModel() = default; + +QVariant DolphinPlacesModel::data(const QModelIndex &index, int role) const +{ + if (role == Qt::DecorationRole) { + if (isTrash(index)) { + if (m_isEmpty) { + return QIcon::fromTheme(QStringLiteral("user-trash")); + } else { + return QIcon::fromTheme(QStringLiteral("user-trash-full")); + } + } + } + + return KFilePlacesModel::data(index, role); +} + +void DolphinPlacesModel::slotTrashEmptinessChanged(bool isEmpty) +{ + if (m_isEmpty == isEmpty) { + return; + } + + // NOTE Trash::isEmpty() reads the config file whereas emptinessChanged is + // hooked up to whether a dirlister in trash:/ has any files and they disagree... + m_isEmpty = isEmpty; + + for (int i = 0; i < rowCount(); ++i) { + const QModelIndex index = this->index(i, 0); + if (isTrash(index)) { + Q_EMIT dataChanged(index, index, {Qt::DecorationRole}); + } + } +} + +bool DolphinPlacesModel::isTrash(const QModelIndex &index) const +{ + return url(index) == QUrl(QStringLiteral("trash:/")); +} + DolphinPlacesModelSingleton::DolphinPlacesModelSingleton() - : m_placesModel(new KFilePlacesModel(KAboutData::applicationData().componentName() + applicationNameSuffix())) + : m_placesModel(new DolphinPlacesModel(KAboutData::applicationData().componentName() + applicationNameSuffix())) { } diff --git a/src/dolphinplacesmodelsingleton.h b/src/dolphinplacesmodelsingleton.h index 645947aaa..7efe6e093 100644 --- a/src/dolphinplacesmodelsingleton.h +++ b/src/dolphinplacesmodelsingleton.h @@ -10,7 +10,33 @@ #include <QString> #include <QScopedPointer> -class KFilePlacesModel; +#include <KFilePlacesModel> + +/** + * @brief Dolphin's special-cased KFilePlacesModel + * + * It returns the trash's icon based on whether + * it is full or not. + */ +class DolphinPlacesModel : public KFilePlacesModel +{ + Q_OBJECT + +public: + explicit DolphinPlacesModel(const QString &alternativeApplicationName, QObject *parent = nullptr); + ~DolphinPlacesModel() override; + +protected: + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + +private Q_SLOTS: + void slotTrashEmptinessChanged(bool isEmpty); + +private: + bool isTrash(const QModelIndex &index) const; + + bool m_isEmpty = false; +}; /** * @brief Provides a global KFilePlacesModel instance. diff --git a/src/dolphintabbar.cpp b/src/dolphintabbar.cpp index 67a61b031..4c1d9e44a 100644 --- a/src/dolphintabbar.cpp +++ b/src/dolphintabbar.cpp @@ -36,7 +36,13 @@ void DolphinTabBar::dragEnterEvent(QDragEnterEvent* event) const int index = tabAt(event->pos()); if (mimeData->hasUrls()) { - event->acceptProposedAction(); + if (index >= 0) { + event->acceptProposedAction(); + } else { + event->setDropAction(Qt::IgnoreAction); + // Still need to accept it to receive dragMoveEvent + event->accept(); + } updateAutoActivationTimer(index); } @@ -56,6 +62,11 @@ void DolphinTabBar::dragMoveEvent(QDragMoveEvent* event) const int index = tabAt(event->pos()); if (mimeData->hasUrls()) { + if (index >= 0) { + event->acceptProposedAction(); + } else { + event->setDropAction(Qt::IgnoreAction); + } updateAutoActivationTimer(index); } diff --git a/src/dolphintabpage.h b/src/dolphintabpage.h index f1a784eb7..a8c1ba311 100644 --- a/src/dolphintabpage.h +++ b/src/dolphintabpage.h @@ -46,7 +46,7 @@ public: * * @param enabled If true, creates a secondary viewContainer in this tab. * Otherwise deletes it. - * @param animated Decides wether the effects of this method call should + * @param animated Decides whether the effects of this method call should * happen instantly or be transitioned to smoothly. * @param secondaryUrl If \p enabled is true, the new viewContainer will be opened at this * parameter. The default value will set the Url of the new viewContainer diff --git a/src/dolphinurlnavigatorscontroller.h b/src/dolphinurlnavigatorscontroller.h index 4f6802725..fec15f481 100644 --- a/src/dolphinurlnavigatorscontroller.h +++ b/src/dolphinurlnavigatorscontroller.h @@ -41,7 +41,7 @@ public Q_SLOTS: private: /** - * @return wether the places selector of DolphinUrlNavigators should be visible. + * @return whether the places selector of DolphinUrlNavigators should be visible. */ static bool placesSelectorVisible(); diff --git a/src/dolphinviewcontainer.cpp b/src/dolphinviewcontainer.cpp index 5520bf794..48e73ca89 100644 --- a/src/dolphinviewcontainer.cpp +++ b/src/dolphinviewcontainer.cpp @@ -32,6 +32,7 @@ #include <KUrlComboBox> #include <QDropEvent> +#include <QGuiApplication> #include <QLoggingCategory> #include <QMimeData> #include <QTimer> @@ -151,6 +152,10 @@ DolphinViewContainer::DolphinViewContainer(const QUrl& url, QWidget* parent) : this, &DolphinViewContainer::slotUrlIsFileError); connect(m_view, &DolphinView::activated, this, &DolphinViewContainer::activate); + connect(m_view, &DolphinView::hiddenFilesShownChanged, + this, &DolphinViewContainer::slotHiddenFilesShownChanged); + connect(m_view, &DolphinView::sortHiddenLastChanged, + this, &DolphinViewContainer::slotSortHiddenLastChanged); // Initialize status bar m_statusBar = new DolphinStatusBar(this); @@ -309,6 +314,8 @@ void DolphinViewContainer::connectUrlNavigator(DolphinUrlNavigator *urlNavigator Q_CHECK_PTR(m_view); urlNavigator->setLocationUrl(m_view->url()); + urlNavigator->setShowHiddenFolders(m_view->hiddenFilesShown()); + urlNavigator->setSortHiddenFoldersLast(m_view->sortHiddenLast()); if (m_urlNavigatorVisualState) { urlNavigator->setVisualState(*m_urlNavigatorVisualState.get()); m_urlNavigatorVisualState.reset(); @@ -636,7 +643,7 @@ void DolphinViewContainer::slotUrlIsFileError(const QUrl& url) } } -void DolphinViewContainer::slotItemActivated(const KFileItem& item) +void DolphinViewContainer::slotItemActivated(const KFileItem &item) { // It is possible to activate items on inactive views by // drag & drop operations. Assure that activating an item always @@ -645,13 +652,24 @@ void DolphinViewContainer::slotItemActivated(const KFileItem& item) const QUrl& url = DolphinView::openItemAsFolderUrl(item, GeneralSettings::browseThroughArchives()); if (!url.isEmpty()) { - setUrl(url); + const auto modifiers = QGuiApplication::keyboardModifiers(); + // keep in sync with KUrlNavigator::slotNavigatorButtonClicked + if (modifiers & Qt::ControlModifier && modifiers & Qt::ShiftModifier) { + Q_EMIT activeTabRequested(url); + } else if (modifiers & Qt::ControlModifier) { + Q_EMIT tabRequested(url); + } else if (modifiers & Qt::ShiftModifier) { + Dolphin::openNewWindow({KFilePlacesModel::convertedUrl(url)}, this); + } else { + setUrl(url); + } return; } KIO::OpenUrlJob *job = new KIO::OpenUrlJob(item.targetUrl()); - job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, this)); + job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoWarningHandlingEnabled, this)); job->setShowOpenOrExecuteDialog(true); + connect(job, &KIO::OpenUrlJob::finished, this, &DolphinViewContainer::slotOpenUrlFinished); job->start(); } @@ -660,7 +678,7 @@ void DolphinViewContainer::slotItemsActivated(const KFileItemList& items) Q_ASSERT(items.count() >= 2); KFileItemActions fileItemActions(this); - fileItemActions.runPreferredApplications(items, QString()); + fileItemActions.runPreferredApplications(items); } void DolphinViewContainer::showItemInfo(const KFileItem& item) @@ -809,6 +827,27 @@ void DolphinViewContainer::slotPlacesModelChanged() } } +void DolphinViewContainer::slotHiddenFilesShownChanged(bool showHiddenFiles) +{ + if (m_urlNavigatorConnected) { + m_urlNavigatorConnected->setShowHiddenFolders(showHiddenFiles); + } +} + +void DolphinViewContainer::slotSortHiddenLastChanged(bool hiddenLast) +{ + if (m_urlNavigatorConnected) { + m_urlNavigatorConnected->setSortHiddenFoldersLast(hiddenLast); + } +} + +void DolphinViewContainer::slotOpenUrlFinished(KJob *job) +{ + if (job->error() && job->error() != KIO::ERR_USER_CANCELED) { + showErrorMessage(job->errorString()); + } +} + bool DolphinViewContainer::isSearchUrl(const QUrl& url) const { return url.scheme().contains(QLatin1String("search")); diff --git a/src/dolphinviewcontainer.h b/src/dolphinviewcontainer.h index 304c9958d..f78f85e55 100644 --- a/src/dolphinviewcontainer.h +++ b/src/dolphinviewcontainer.h @@ -126,7 +126,7 @@ public: void connectUrlNavigator(DolphinUrlNavigator *urlNavigator); /** - * Disconnects the navigator that is currently controling the view. + * Disconnects the navigator that is currently controlling the view. * This method completely reverses connectUrlNavigator(). */ void disconnectUrlNavigator(); @@ -229,6 +229,16 @@ Q_SIGNALS: */ void captionChanged(); + /** + * Is emitted if a new tab should be opened in the background for the URL \a url. + */ + void tabRequested(const QUrl &url); + + /** + * Is emitted if a new tab should be opened for the URL \a url and set as active. + */ + void activeTabRequested(const QUrl &url); + private Q_SLOTS: /** * Updates the number of items (= number of files + number of @@ -281,7 +291,7 @@ private Q_SLOTS: * directory is opened in the view. If the item is a file, the file * gets started by the corresponding application. */ - void slotItemActivated(const KFileItem& item); + void slotItemActivated(const KFileItem &item); /** * Handles activation of multiple files. The files get started by @@ -361,6 +371,11 @@ private Q_SLOTS: */ void slotPlacesModelChanged(); + void slotHiddenFilesShownChanged(bool showHiddenFiles); + void slotSortHiddenLastChanged(bool hiddenLast); + + void slotOpenUrlFinished(KJob* job); + private: /** * @return True if the URL protocol is a search URL (e. g. baloosearch:// or filenamesearch://). diff --git a/src/kitemviews/kfileitemlistwidget.cpp b/src/kitemviews/kfileitemlistwidget.cpp index 587603ab3..4198df027 100644 --- a/src/kitemviews/kfileitemlistwidget.cpp +++ b/src/kitemviews/kfileitemlistwidget.cpp @@ -94,6 +94,11 @@ QString KFileItemListWidgetInformant::roleText(const QByteArray& role, if (dateTime.isValid()) { text = formatDate(dateTime); } + } else if (role == "dimensions") { + const auto dimensions = roleValue.toSize(); + if (dimensions.isValid()) { + text = i18nc("width x height", "%1 x %2", dimensions.width(), dimensions.height()); + } } else { text = KStandardItemListWidgetInformant::roleText(role, values); } @@ -109,7 +114,6 @@ QFont KFileItemListWidgetInformant::customizedFontForLinks(const QFont& baseFont return font; } - KFileItemListWidget::KFileItemListWidget(KItemListWidgetInformant* informant, QGraphicsItem* parent) : KStandardItemListWidget(informant, parent) { diff --git a/src/kitemviews/kfileitemmodel.cpp b/src/kitemviews/kfileitemmodel.cpp index d12355100..23afb087a 100644 --- a/src/kitemviews/kfileitemmodel.cpp +++ b/src/kitemviews/kfileitemmodel.cpp @@ -16,6 +16,7 @@ #include <KDirLister> #include <KIO/Job> #include <KLocalizedString> +#include <KLazyLocalizedString> #include <KUrlMimeData> #include <QElapsedTimer> @@ -25,6 +26,8 @@ #include <QWidget> #include <QRecursiveMutex> #include <QIcon> +#include <algorithm> +#include <klazylocalizedstring.h> Q_GLOBAL_STATIC(QRecursiveMutex, s_collatorMutex) @@ -64,15 +67,15 @@ KFileItemModel::KFileItemModel(QObject* parent) : } connect(m_dirLister, &KCoreDirLister::started, this, &KFileItemModel::directoryLoadingStarted); - connect(m_dirLister, QOverload<>::of(&KCoreDirLister::canceled), this, &KFileItemModel::slotCanceled); + connect(m_dirLister, &KCoreDirLister::canceled, this, &KFileItemModel::slotCanceled); connect(m_dirLister, &KCoreDirLister::itemsAdded, this, &KFileItemModel::slotItemsAdded); connect(m_dirLister, &KCoreDirLister::itemsDeleted, this, &KFileItemModel::slotItemsDeleted); connect(m_dirLister, &KCoreDirLister::refreshItems, this, &KFileItemModel::slotRefreshItems); - connect(m_dirLister, QOverload<>::of(&KCoreDirLister::clear), this, &KFileItemModel::slotClear); + connect(m_dirLister, &KCoreDirLister::clear, this, &KFileItemModel::slotClear); connect(m_dirLister, &KCoreDirLister::infoMessage, this, &KFileItemModel::infoMessage); connect(m_dirLister, &KCoreDirLister::jobError, this, &KFileItemModel::slotListerError); connect(m_dirLister, &KCoreDirLister::percent, this, &KFileItemModel::directoryLoadingProgress); - connect(m_dirLister, QOverload<const QUrl&, const QUrl&>::of(&KCoreDirLister::redirection), this, &KFileItemModel::directoryRedirection); + connect(m_dirLister, &KCoreDirLister::redirection, this, &KFileItemModel::directoryRedirection); connect(m_dirLister, &KCoreDirLister::listingDirCompleted, this, &KFileItemModel::slotCompleted); // Apply default roles that should be determined @@ -149,6 +152,18 @@ QHash<QByteArray, QVariant> KFileItemModel::data(int index) const ItemData* data = m_itemData.at(index); if (data->values.isEmpty()) { data->values = retrieveData(data->item, data->parent); + } else if (data->values.count() <= 2 && data->values.value("isExpanded").toBool()) { + // Special case dealt by slotRefreshItems(), avoid losing the "isExpanded" and "expandedParentsCount" state when refreshing + // slotRefreshItems() makes sure folders keep the "isExpanded" and "expandedParentsCount" while clearing the remaining values + // so this special request of different behavior can be identified here. + bool hasExpandedParentsCount = false; + const int expandedParentsCount = data->values.value("expandedParentsCount").toInt(&hasExpandedParentsCount); + + data->values = retrieveData(data->item, data->parent); + data->values.insert("isExpanded", true); + if (hasExpandedParentsCount) { + data->values.insert("expandedParentsCount", expandedParentsCount); + } } return data->values; @@ -312,10 +327,10 @@ QString KFileItemModel::roleDescription(const QByteArray& role) const int count = 0; const RoleInfoMap* map = rolesInfoMap(count); for (int i = 0; i < count; ++i) { - if (!map[i].roleTranslation) { + if (map[i].roleTranslation.isEmpty()) { continue; } - description.insert(map[i].role, i18nc(map[i].roleTranslationContext, map[i].roleTranslation)); + description.insert(map[i].role, map[i].roleTranslation.toString()); } } @@ -712,7 +727,7 @@ void KFileItemModel::applyFilters() ItemData *itemData = m_itemData.at(index); if (m_filter.matches(itemData->item) - || (itemShownBelow && itemShownBelow->parent == itemData && itemData->values.value("isExpanded").toBool())) { + || (itemShownBelow && itemShownBelow->parent == itemData)) { // We could've entered here for two reasons: // 1. This item passes the filter itself // 2. This is an expanded folder that doesn't pass the filter but sees a filter-passing child just below @@ -815,9 +830,9 @@ QList<KFileItemModel::RoleInfo> KFileItemModel::rolesInformation() if (map[i].roleType != NoRole) { RoleInfo info; info.role = map[i].role; - info.translation = i18nc(map[i].roleTranslationContext, map[i].roleTranslation); - if (map[i].groupTranslation) { - info.group = i18nc(map[i].groupTranslationContext, map[i].groupTranslation); + info.translation = map[i].roleTranslation.toString(); + if (!map[i].groupTranslation.isEmpty()) { + info.group = map[i].groupTranslation.toString(); } else { // For top level roles, groupTranslation is 0. We must make sure that // info.group is an empty string then because the code that generates @@ -1010,12 +1025,7 @@ void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemLis { Q_ASSERT(!items.isEmpty()); - QUrl parentUrl; - if (m_expandedDirs.contains(directoryUrl)) { - parentUrl = m_expandedDirs.value(directoryUrl); - } else { - parentUrl = directoryUrl.adjusted(QUrl::StripTrailingSlash); - } + const QUrl parentUrl = m_expandedDirs.value(directoryUrl, directoryUrl.adjusted(QUrl::StripTrailingSlash)); if (m_requestRole[ExpandedParentsCountRole]) { // If the expanding of items is enabled, the call @@ -1049,16 +1059,28 @@ void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemLis if (!m_filter.hasSetFilters()) { m_pendingItemsToInsert.append(itemDataList); } else { + QSet<ItemData *> parentsToEnsureVisible; + // The name or type filter is active. Hide filtered items // before inserting them into the model and remember // the filtered items in m_filteredItems. for (ItemData* itemData : itemDataList) { if (m_filter.matches(itemData->item)) { m_pendingItemsToInsert.append(itemData); + if (itemData->parent) { + parentsToEnsureVisible.insert(itemData->parent); + } } else { m_filteredItems.insert(itemData->item, itemData); } } + + // Entire parental chains must be shown + for (ItemData *parent : parentsToEnsureVisible) { + for (; parent && m_filteredItems.remove(parent->item); parent = parent->parent) { + m_pendingItemsToInsert.append(parent); + } + } } if (!m_maximumUpdateIntervalTimer->isActive()) { @@ -1070,6 +1092,42 @@ void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemLis Q_EMIT fileItemsChanged({KFileItem(directoryUrl)}); } +int KFileItemModel::filterChildlessParents(KItemRangeList &removedItemRanges, const QSet<ItemData *> &parentsToEnsureVisible) +{ + int filteredParentsCount = 0; + // The childless parents not yet removed will always be right above the start of a removed range. + // We iterate backwards to ensure the deepest folders are processed before their parents + for (int i = removedItemRanges.size() - 1; i >= 0; i--) { + KItemRange itemRange = removedItemRanges.at(i); + const ItemData *const firstInRange = m_itemData.at(itemRange.index); + ItemData *itemAbove = itemRange.index - 1 >= 0 ? m_itemData.at(itemRange.index - 1) : nullptr; + const ItemData *const itemBelow = itemRange.index + itemRange.count < m_itemData.count() ? m_itemData.at(itemRange.index + itemRange.count) : nullptr; + + if (itemAbove && firstInRange->parent == itemAbove && !m_filter.matches(itemAbove->item) && (!itemBelow || itemBelow->parent != itemAbove) + && !parentsToEnsureVisible.contains(itemAbove)) { + // The item above exists, is the parent, doesn't pass the filter, does not belong to parentsToEnsureVisible + // and this deleted range covers all of its descendents, so none will be left. + m_filteredItems.insert(itemAbove->item, itemAbove); + // This range's starting index will be extended to include the parent above: + --itemRange.index; + ++itemRange.count; + ++filteredParentsCount; + KItemRange previousRange = i > 0 ? removedItemRanges.at(i - 1) : KItemRange(); + // We must check if this caused the range to touch the previous range, if that's the case they shall be merged + if (i > 0 && previousRange.index + previousRange.count == itemRange.index) { + previousRange.count += itemRange.count; + removedItemRanges.replace(i - 1, previousRange); + removedItemRanges.removeAt(i); + } else { + removedItemRanges.replace(i, itemRange); + // We must revisit this range in the next iteration since its starting index changed + ++i; + } + } + } + return filteredParentsCount; +} + void KFileItemModel::slotItemsDeleted(const KFileItemList& items) { dispatchPendingItemsToInsert(); @@ -1119,9 +1177,17 @@ void KFileItemModel::slotItemsDeleted(const KFileItemList& items) indexesToRemove = indexesToRemoveWithChildren; } - const KItemRangeList itemRanges = KItemRangeList::fromSortedContainer(indexesToRemove); + KItemRangeList itemRanges = KItemRangeList::fromSortedContainer(indexesToRemove); removeFilteredChildren(itemRanges); - removeItems(itemRanges, DeleteItemData); + + // This call will update itemRanges to include the childless parents that have been filtered. + const int filteredParentsCount = filterChildlessParents(itemRanges); + + // If any childless parents were filtered, then itemRanges got updated and now contains items that were really deleted + // mixed with expanded folders that are just being filtered out. + // If that's the case, we pass 'DeleteItemDataIfUnfiltered' as a hint + // so removeItems() will check m_filteredItems to differentiate which is which. + removeItems(itemRanges, filteredParentsCount > 0 ? DeleteItemDataIfUnfiltered : DeleteItemData); Q_EMIT fileItemsChanged(dirsChanged); } @@ -1149,6 +1215,7 @@ void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem> >& QList<ItemData*> newVisibleItems; QListIterator<QPair<KFileItem, KFileItem> > it(items); + while (it.hasNext()) { const QPair<KFileItem, KFileItem>& itemPair = it.next(); const KFileItem& oldItem = itemPair.first; @@ -1172,8 +1239,14 @@ void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem> >& } m_items.remove(oldItem.url()); - if (newItemMatchesFilter) { - m_items.insert(newItem.url(), indexForItem); + // We must maintain m_items consistent with m_itemData for now, this very loop is using it. + // We leave it to be cleared by removeItems() later, when m_itemData actually gets updated. + m_items.insert(newItem.url(), indexForItem); + if (newItemMatchesFilter + || (itemData->values.value("isExpanded").toBool() + && (indexForItem + 1 < m_itemData.count() && m_itemData.at(indexForItem + 1)->parent == itemData))) { + // We are lenient with expanded folders that originally had visible children. + // If they become childless now they will be caught by filterChildlessParents() changedFiles.append(newItem); indexes.append(indexForItem); } else { @@ -1184,12 +1257,23 @@ void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem> >& // Check if 'oldItem' is one of the filtered items. QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.find(oldItem); if (it != m_filteredItems.end()) { - ItemData* itemData = it.value(); + ItemData *const itemData = it.value(); itemData->item = newItem; // The data stored in 'values' might have changed. Therefore, we clear // 'values' and re-populate it the next time it is requested via data(int). + // Before clearing, we must remember if it was expanded and the expanded parents count, + // otherwise these states would be lost. The data() method will deal with this special case. + const bool isExpanded = itemData->values.value("isExpanded").toBool(); + bool hasExpandedParentsCount = false; + const int expandedParentsCount = itemData->values.value("expandedParentsCount").toInt(&hasExpandedParentsCount); itemData->values.clear(); + if (isExpanded) { + itemData->values.insert("isExpanded", true); + if (hasExpandedParentsCount) { + itemData->values.insert("expandedParentsCount", expandedParentsCount); + } + } m_filteredItems.erase(it); if (newItemMatchesFilter) { @@ -1201,21 +1285,66 @@ void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem> >& } } - // Hide items, previously visible that should get hidden - const KItemRangeList removedRanges = KItemRangeList::fromSortedContainer(newFilteredIndexes); + std::sort(newFilteredIndexes.begin(), newFilteredIndexes.end()); + + // We must keep track of parents of new visible items since they must be shown no matter what + // They will be considered "immune" to filterChildlessParents() + QSet<ItemData *> parentsToEnsureVisible; + + for (ItemData *item : newVisibleItems) { + for (ItemData *parent = item->parent; parent && !parentsToEnsureVisible.contains(parent); parent = parent->parent) { + parentsToEnsureVisible.insert(parent); + } + } + for (ItemData *parent : parentsToEnsureVisible) { + // We make sure they are all unfiltered. + if (m_filteredItems.remove(parent->item)) { + // If it is being unfiltered now, we mark it to be inserted by appending it to newVisibleItems + newVisibleItems.append(parent); + // It could be in newFilteredIndexes, we must remove it if it's there: + const int parentIndex = index(parent->item); + if (parentIndex >= 0) { + QVector<int>::iterator it = std::lower_bound(newFilteredIndexes.begin(), newFilteredIndexes.end(), parentIndex); + if (it != newFilteredIndexes.end() && *it == parentIndex) { + newFilteredIndexes.erase(it); + } + } + } + } + + KItemRangeList removedRanges = KItemRangeList::fromSortedContainer(newFilteredIndexes); + + // This call will update itemRanges to include the childless parents that have been filtered. + filterChildlessParents(removedRanges, parentsToEnsureVisible); + removeItems(removedRanges, KeepItemData); // Show previously hidden items that should get visible insertItems(newVisibleItems); + // Final step: we will emit 'itemsChanged' and 'fileItemsChanged' signals and trigger the asynchronous re-sorting logic. + // If the changed items have been created recently, they might not be in m_items yet. // In that case, the list 'indexes' might be empty. if (indexes.isEmpty()) { return; } + if (newVisibleItems.count() > 0 || removedRanges.count() > 0) { + // The original indexes have changed and are now worthless since items were removed and/or inserted. + indexes.clear(); + // m_items is not yet rebuilt at this point, so we use our own means to resolve the new indexes. + const QSet<const KFileItem> changedFilesSet(changedFiles.cbegin(), changedFiles.cend()); + for (int i = 0; i < m_itemData.count(); i++) { + if (changedFilesSet.contains(m_itemData.at(i)->item)) { + indexes.append(i); + } + } + } else { + std::sort(indexes.begin(), indexes.end()); + } + // Extract the item-ranges out of the changed indexes - std::sort(indexes.begin(), indexes.end()); const KItemRangeList itemRangeList = KItemRangeList::fromSortedContainer(indexes); emitItemsChangedAndTriggerResorting(itemRangeList, changedRoles); @@ -1380,7 +1509,7 @@ void KFileItemModel::removeItems(const KItemRangeList& itemRanges, RemoveItemsBe removedItemsCount += range.count; for (int index = range.index; index < range.index + range.count; ++index) { - if (behavior == DeleteItemData) { + if (behavior == DeleteItemData || (behavior == DeleteItemDataIfUnfiltered && !m_filteredItems.contains(m_itemData.at(index)->item))) { delete m_itemData.at(index); } @@ -1424,8 +1553,9 @@ QList<KFileItemModel::ItemData*> KFileItemModel::createItemDataList(const QUrl& determineMimeTypes(items, 200); } + // We search for the parent in m_itemData and then in m_filteredItems if necessary const int parentIndex = index(parentUrl); - ItemData* parentItem = parentIndex < 0 ? nullptr : m_itemData.at(parentIndex); + ItemData *parentItem = parentIndex < 0 ? m_filteredItems.value(KFileItem(parentUrl), nullptr) : m_itemData.at(parentIndex); QList<ItemData*> itemDataList; itemDataList.reserve(items.count()); @@ -1959,6 +2089,19 @@ int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b, const break; } + case DimensionsRole: { + const QByteArray role = roleForType(m_sortRole); + const QSize dimensionsA = a->values.value(role).toSize(); + const QSize dimensionsB = b->values.value(role).toSize(); + + if (dimensionsA.width() == dimensionsB.width()) { + result = dimensionsA.height() - dimensionsB.height(); + } else { + result = dimensionsA.width() - dimensionsB.width(); + } + break; + } + default: { const QByteArray role = roleForType(m_sortRole); const QString roleValueA = a->values.value(role).toString(); @@ -2451,40 +2594,41 @@ void KFileItemModel::emitSortProgress(int resolvedCount) const KFileItemModel::RoleInfoMap* KFileItemModel::rolesInfoMap(int& count) { static const RoleInfoMap rolesInfoMap[] = { - // | role | roleType | role translation | group translation | requires Baloo | requires indexer - { nullptr, NoRole, nullptr, nullptr, nullptr, nullptr, false, false }, - { "text", NameRole, I18NC_NOOP("@label", "Name"), nullptr, nullptr, false, false }, - { "size", SizeRole, I18NC_NOOP("@label", "Size"), nullptr, nullptr, false, false }, - { "modificationtime", ModificationTimeRole, I18NC_NOOP("@label", "Modified"), nullptr, nullptr, false, false }, - { "creationtime", CreationTimeRole, I18NC_NOOP("@label", "Created"), nullptr, nullptr, false, false }, - { "accesstime", AccessTimeRole, I18NC_NOOP("@label", "Accessed"), nullptr, nullptr, false, false }, - { "type", TypeRole, I18NC_NOOP("@label", "Type"), nullptr, nullptr, false, false }, - { "rating", RatingRole, I18NC_NOOP("@label", "Rating"), nullptr, nullptr, true, false }, - { "tags", TagsRole, I18NC_NOOP("@label", "Tags"), nullptr, nullptr, true, false }, - { "comment", CommentRole, I18NC_NOOP("@label", "Comment"), nullptr, nullptr, true, false }, - { "title", TitleRole, I18NC_NOOP("@label", "Title"), I18NC_NOOP("@label", "Document"), true, true }, - { "wordCount", WordCountRole, I18NC_NOOP("@label", "Word Count"), I18NC_NOOP("@label", "Document"), true, true }, - { "lineCount", LineCountRole, I18NC_NOOP("@label", "Line Count"), I18NC_NOOP("@label", "Document"), true, true }, - { "imageDateTime", ImageDateTimeRole, I18NC_NOOP("@label", "Date Photographed"), I18NC_NOOP("@label", "Image"), true, true }, - { "width", WidthRole, I18NC_NOOP("@label", "Width"), I18NC_NOOP("@label", "Image"), true, true }, - { "height", HeightRole, I18NC_NOOP("@label", "Height"), I18NC_NOOP("@label", "Image"), true, true }, - { "orientation", OrientationRole, I18NC_NOOP("@label", "Orientation"), I18NC_NOOP("@label", "Image"), true, true }, - { "artist", ArtistRole, I18NC_NOOP("@label", "Artist"), I18NC_NOOP("@label", "Audio"), true, true }, - { "genre", GenreRole, I18NC_NOOP("@label", "Genre"), I18NC_NOOP("@label", "Audio"), true, true }, - { "album", AlbumRole, I18NC_NOOP("@label", "Album"), I18NC_NOOP("@label", "Audio"), true, true }, - { "duration", DurationRole, I18NC_NOOP("@label", "Duration"), I18NC_NOOP("@label", "Audio"), true, true }, - { "bitrate", BitrateRole, I18NC_NOOP("@label", "Bitrate"), I18NC_NOOP("@label", "Audio"), true, true }, - { "track", TrackRole, I18NC_NOOP("@label", "Track"), I18NC_NOOP("@label", "Audio"), true, true }, - { "releaseYear", ReleaseYearRole, I18NC_NOOP("@label", "Release Year"), I18NC_NOOP("@label", "Audio"), true, true }, - { "aspectRatio", AspectRatioRole, I18NC_NOOP("@label", "Aspect Ratio"), I18NC_NOOP("@label", "Video"), true, true }, - { "frameRate", FrameRateRole, I18NC_NOOP("@label", "Frame Rate"), I18NC_NOOP("@label", "Video"), true, true }, - { "path", PathRole, I18NC_NOOP("@label", "Path"), I18NC_NOOP("@label", "Other"), false, false }, - { "deletiontime", DeletionTimeRole, I18NC_NOOP("@label", "Deletion Time"), I18NC_NOOP("@label", "Other"), false, false }, - { "destination", DestinationRole, I18NC_NOOP("@label", "Link Destination"), I18NC_NOOP("@label", "Other"), false, false }, - { "originUrl", OriginUrlRole, I18NC_NOOP("@label", "Downloaded From"), I18NC_NOOP("@label", "Other"), true, false }, - { "permissions", PermissionsRole, I18NC_NOOP("@label", "Permissions"), I18NC_NOOP("@label", "Other"), false, false }, - { "owner", OwnerRole, I18NC_NOOP("@label", "Owner"), I18NC_NOOP("@label", "Other"), false, false }, - { "group", GroupRole, I18NC_NOOP("@label", "User Group"), I18NC_NOOP("@label", "Other"), false, false }, + // | role | roleType | role translation | group translation | requires Baloo | requires indexer + { nullptr, NoRole, KLazyLocalizedString(), KLazyLocalizedString(), false, false }, + { "text", NameRole, kli18nc("@label", "Name"), KLazyLocalizedString(), false, false }, + { "size", SizeRole, kli18nc("@label", "Size"), KLazyLocalizedString(), false, false }, + { "modificationtime", ModificationTimeRole, kli18nc("@label", "Modified"), KLazyLocalizedString(), false, false }, + { "creationtime", CreationTimeRole, kli18nc("@label", "Created"), KLazyLocalizedString(), false, false }, + { "accesstime", AccessTimeRole, kli18nc("@label", "Accessed"), KLazyLocalizedString(), false, false }, + { "type", TypeRole, kli18nc("@label", "Type"), KLazyLocalizedString(), false, false }, + { "rating", RatingRole, kli18nc("@label", "Rating"), KLazyLocalizedString(), true, false }, + { "tags", TagsRole, kli18nc("@label", "Tags"), KLazyLocalizedString(), true, false }, + { "comment", CommentRole, kli18nc("@label", "Comment"), KLazyLocalizedString(), true, false }, + { "title", TitleRole, kli18nc("@label", "Title"), kli18nc("@label", "Document"), true, true }, + { "wordCount", WordCountRole, kli18nc("@label", "Word Count"), kli18nc("@label", "Document"), true, true }, + { "lineCount", LineCountRole, kli18nc("@label", "Line Count"), kli18nc("@label", "Document"), true, true }, + { "imageDateTime", ImageDateTimeRole, kli18nc("@label", "Date Photographed"), kli18nc("@label", "Image"), true, true }, + { "dimensions", DimensionsRole, kli18nc("@label width x height", "Dimensions"), kli18nc("@label", "Image"), true, true }, + { "width", WidthRole, kli18nc("@label", "Width"), kli18nc("@label", "Image"), true, true }, + { "height", HeightRole, kli18nc("@label", "Height"), kli18nc("@label", "Image"), true, true }, + { "orientation", OrientationRole, kli18nc("@label", "Orientation"), kli18nc("@label", "Image"), true, true }, + { "artist", ArtistRole, kli18nc("@label", "Artist"), kli18nc("@label", "Audio"), true, true }, + { "genre", GenreRole, kli18nc("@label", "Genre"), kli18nc("@label", "Audio"), true, true }, + { "album", AlbumRole, kli18nc("@label", "Album"), kli18nc("@label", "Audio"), true, true }, + { "duration", DurationRole, kli18nc("@label", "Duration"), kli18nc("@label", "Audio"), true, true }, + { "bitrate", BitrateRole, kli18nc("@label", "Bitrate"), kli18nc("@label", "Audio"), true, true }, + { "track", TrackRole, kli18nc("@label", "Track"), kli18nc("@label", "Audio"), true, true }, + { "releaseYear", ReleaseYearRole, kli18nc("@label", "Release Year"), kli18nc("@label", "Audio"), true, true }, + { "aspectRatio", AspectRatioRole, kli18nc("@label", "Aspect Ratio"), kli18nc("@label", "Video"), true, true }, + { "frameRate", FrameRateRole, kli18nc("@label", "Frame Rate"), kli18nc("@label", "Video"), true, true }, + { "path", PathRole, kli18nc("@label", "Path"), kli18nc("@label", "Other"), false, false }, + { "deletiontime", DeletionTimeRole, kli18nc("@label", "Deletion Time"), kli18nc("@label", "Other"), false, false }, + { "destination", DestinationRole, kli18nc("@label", "Link Destination"), kli18nc("@label", "Other"), false, false }, + { "originUrl", OriginUrlRole, kli18nc("@label", "Downloaded From"), kli18nc("@label", "Other"), true, false }, + { "permissions", PermissionsRole, kli18nc("@label", "Permissions"), kli18nc("@label", "Other"), false, false }, + { "owner", OwnerRole, kli18nc("@label", "Owner"), kli18nc("@label", "Other"), false, false }, + { "group", GroupRole, kli18nc("@label", "User Group"), kli18nc("@label", "Other"), false, false }, }; count = sizeof(rolesInfoMap) / sizeof(RoleInfoMap); diff --git a/src/kitemviews/kfileitemmodel.h b/src/kitemviews/kfileitemmodel.h index 161f6a0e2..cc39a0084 100644 --- a/src/kitemviews/kfileitemmodel.h +++ b/src/kitemviews/kfileitemmodel.h @@ -12,6 +12,7 @@ #include "kitemviews/private/kfileitemmodelfilter.h" #include <KFileItem> +#include <KLazyLocalizedString> #include <QCollator> #include <QHash> @@ -291,7 +292,7 @@ private: NoRole, NameRole, SizeRole, ModificationTimeRole, CreationTimeRole, AccessTimeRole, PermissionsRole, OwnerRole, GroupRole, TypeRole, DestinationRole, PathRole, DeletionTimeRole, // User visible roles available with Baloo: - CommentRole, TagsRole, RatingRole, WidthRole, HeightRole, ImageDateTimeRole, OrientationRole, + CommentRole, TagsRole, RatingRole, DimensionsRole, WidthRole, HeightRole, ImageDateTimeRole, OrientationRole, WordCountRole, TitleRole, LineCountRole, ArtistRole, GenreRole, AlbumRole, DurationRole, TrackRole, ReleaseYearRole, BitrateRole, OriginUrlRole, AspectRatioRole, FrameRateRole, // Non-visible roles: @@ -309,7 +310,8 @@ private: enum RemoveItemsBehavior { KeepItemData, - DeleteItemData + DeleteItemData, + DeleteItemDataIfUnfiltered }; void insertItems(QList<ItemData*>& items); @@ -439,10 +441,8 @@ private: { const char* const role; const RoleType roleType; - const char* const roleTranslationContext; - const char* const roleTranslation; - const char* const groupTranslationContext; - const char* const groupTranslation; + const KLazyLocalizedString roleTranslation; + const KLazyLocalizedString groupTranslation; const bool requiresBaloo; const bool requiresIndexer; }; @@ -469,6 +469,15 @@ private: */ bool isConsistent() const; + /** + * Filters out the expanded folders that don't pass the filter themselves and don't have any filter-passing children. + * Will update the removedItemRanges arguments to include the parents that have been filtered. + * @returns the number of parents that have been filtered. + * @param removedItemRanges The ranges of items being deleted/filtered, will get updated + * @param parentsToEnsureVisible Parents that must be visible no matter what due to being ancestors of newly visible items + */ + int filterChildlessParents(KItemRangeList &removedItemRanges, const QSet<ItemData *> &parentsToEnsureVisible = QSet<ItemData *>()); + private: KDirLister *m_dirLister = nullptr; diff --git a/src/kitemviews/kfileitemmodelrolesupdater.cpp b/src/kitemviews/kfileitemmodelrolesupdater.cpp index 978f5df6e..49657a9b1 100644 --- a/src/kitemviews/kfileitemmodelrolesupdater.cpp +++ b/src/kitemviews/kfileitemmodelrolesupdater.cpp @@ -18,7 +18,7 @@ #include <KIconLoader> #include <KJobWidgets> #include <KOverlayIconPlugin> -#include <KPluginLoader> +#include <KPluginMetaData> #include <KSharedConfig> #ifdef HAVE_BALOO @@ -30,6 +30,7 @@ #include <QApplication> #include <QIcon> #include <QPainter> +#include <QPluginLoader> #include <QElapsedTimer> #include <QTimer> @@ -120,15 +121,16 @@ KFileItemModelRolesUpdater::KFileItemModelRolesUpdater(KFileItemModel* model, QO connect(m_directoryContentsCounter, &KDirectoryContentsCounter::result, this, &KFileItemModelRolesUpdater::slotDirectoryContentsCountReceived); - const auto plugins = KPluginLoader::instantiatePlugins(QStringLiteral("kf5/overlayicon"), nullptr, qApp); - for (QObject *it : plugins) { - auto plugin = qobject_cast<KOverlayIconPlugin*>(it); + const auto plugins = KPluginMetaData::findPlugins(QStringLiteral("kf5/overlayicon")); + for (const KPluginMetaData &data : plugins) { + auto instance = QPluginLoader(data.fileName()).instance(); + auto plugin = qobject_cast<KOverlayIconPlugin *>(instance); if (plugin) { m_overlayIconsPlugin.append(plugin); connect(plugin, &KOverlayIconPlugin::overlaysChanged, this, &KFileItemModelRolesUpdater::slotOverlaysChanged); } else { // not our/valid plugin, so delete the created object - it->deleteLater(); + delete instance; } } } @@ -1405,10 +1407,19 @@ QList<int> KFileItemModelRolesUpdater::indexesToResolve() const (2 * m_maximumVisibleItems))); // Add visible items. + // Resolve files first, their previews are quicker. + QList<int> visibleDirs; for (int i = m_firstVisibleIndex; i <= m_lastVisibleIndex; ++i) { - result.append(i); + const KFileItem item = m_model->fileItem(i); + if (item.isDir()) { + visibleDirs.append(i); + } else { + result.append(i); + } } + result.append(visibleDirs); + // We need a reasonable upper limit for number of items to resolve after // and before the visible range. m_maximumVisibleItems can be quite large // when using Compact View. diff --git a/src/kitemviews/kitemlistcontroller.cpp b/src/kitemviews/kitemlistcontroller.cpp index 8687872ee..80c28f25c 100644 --- a/src/kitemviews/kitemlistcontroller.cpp +++ b/src/kitemviews/kitemlistcontroller.cpp @@ -46,7 +46,7 @@ KItemListController::KItemListController(KItemModelBase* model, KItemListView* v m_view(nullptr), m_selectionManager(new KItemListSelectionManager(this)), m_keyboardManager(new KItemListKeyboardSearchManager(this)), - m_pressedIndex(-1), + m_pressedIndex(std::nullopt), m_pressedMousePos(), m_autoActivationTimer(nullptr), m_swipeGesture(Qt::CustomGesture), @@ -574,16 +574,16 @@ bool KItemListController::mouseMoveEvent(QGraphicsSceneMouseEvent* event, const return false; } - if (m_pressedIndex >= 0) { + if (m_pressedIndex.has_value() && !m_view->rubberBand()->isActive()) { // Check whether a dragging should be started if (event->buttons() & Qt::LeftButton) { const QPointF pos = transform.map(event->pos()); if ((pos - m_pressedMousePos).manhattanLength() >= QApplication::startDragDistance()) { - if (!m_selectionManager->isSelected(m_pressedIndex)) { + if (!m_selectionManager->isSelected(m_pressedIndex.value())) { // Always assure that the dragged item gets selected. Usually this is already // done on the mouse-press event, but when using the selection-toggle on a // selected item the dragged item is not selected yet. - m_selectionManager->setSelected(m_pressedIndex, 1, KItemListSelectionManager::Toggle); + m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Toggle); } else { // A selected item has been clicked to drag all selected items // -> the selection should not be cleared when the mouse button is released. @@ -599,12 +599,12 @@ bool KItemListController::mouseMoveEvent(QGraphicsSceneMouseEvent* event, const QPointF endPos = transform.map(event->pos()); // Update the current item. - const int newCurrent = m_view->itemAt(endPos); - if (newCurrent >= 0) { + const std::optional<int> newCurrent = m_view->itemAt(endPos); + if (newCurrent.has_value()) { // It's expected that the new current index is also the new anchor (bug 163451). m_selectionManager->endAnchoredSelection(); - m_selectionManager->setCurrentItem(newCurrent); - m_selectionManager->beginAnchoredSelection(newCurrent); + m_selectionManager->setCurrentItem(newCurrent.value()); + m_selectionManager->beginAnchoredSelection(newCurrent.value()); } if (m_view->scrollOrientation() == Qt::Vertical) { @@ -642,7 +642,7 @@ bool KItemListController::mouseReleaseEvent(QGraphicsSceneMouseEvent* event, con return false; } - Q_EMIT mouseButtonReleased(m_pressedIndex, event->buttons()); + Q_EMIT mouseButtonReleased(m_pressedIndex.value_or(-1), event->buttons()); return onRelease(transform.map(event->pos()), event->modifiers(), event->button(), false); } @@ -650,21 +650,21 @@ bool KItemListController::mouseReleaseEvent(QGraphicsSceneMouseEvent* event, con bool KItemListController::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* event, const QTransform& transform) { const QPointF pos = transform.map(event->pos()); - const int index = m_view->itemAt(pos); + const std::optional<int> index = m_view->itemAt(pos); // Expand item if desired - See Bug 295573 if (m_mouseDoubleClickAction != ActivateItemOnly) { - if (m_view && m_model && m_view->supportsItemExpanding() && m_model->isExpandable(index)) { - const bool expanded = m_model->isExpanded(index); - m_model->setExpanded(index, !expanded); + if (m_view && m_model && m_view->supportsItemExpanding() && m_model->isExpandable(index.value_or(-1))) { + const bool expanded = m_model->isExpanded(index.value()); + m_model->setExpanded(index.value(), !expanded); } } if (event->button() & Qt::RightButton) { m_selectionManager->clearSelection(); - if (index >= 0) { - m_selectionManager->setSelected(index); - Q_EMIT itemContextMenuRequested(index, event->screenPos()); + if (index.has_value()) { + m_selectionManager->setSelected(index.value()); + Q_EMIT itemContextMenuRequested(index.value(), event->screenPos()); } else { const QRectF headerBounds = m_view->headerBoundaries(); if (headerBounds.contains(event->pos())) { @@ -678,9 +678,9 @@ bool KItemListController::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* event, bool emitItemActivated = !(m_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick) || m_singleClickActivationEnforced) && (event->button() & Qt::LeftButton) && - index >= 0 && index < m_model->count(); + index.has_value() && index.value() < m_model->count(); if (emitItemActivated) { - Q_EMIT itemActivated(index); + Q_EMIT itemActivated(index.value()); } return false; } @@ -805,7 +805,7 @@ bool KItemListController::dropEvent(QGraphicsSceneDragDropEvent* event, const QT Q_EMIT aboveItemDropEvent(dropAboveIndex, event); } else if (!event->mimeData()->hasFormat(m_model->blacklistItemDropEventMimeType())) { // Something has been dropped on an item or on an empty part of the view. - Q_EMIT itemDropEvent(m_view->itemAt(pos), event); + Q_EMIT itemDropEvent(m_view->itemAt(pos).value_or(-1), event); } QAccessibleEvent accessibilityEvent(view(), QAccessible::DragDropEnd); @@ -828,27 +828,76 @@ bool KItemListController::hoverMoveEvent(QGraphicsSceneHoverEvent* event, const return false; } - KItemListWidget* oldHoveredWidget = hoveredWidget(); - const QPointF pos = transform.map(event->pos()); - KItemListWidget* newHoveredWidget = widgetForPos(pos); + // We identify the widget whose expansionArea had been hovered before this hoverMoveEvent() triggered. + // we can't use hoveredWidget() here (it handles the icon+text rect, not the expansion rect) + // like hoveredWidget(), we find the hovered widget for the expansion rect + const auto visibleItemListWidgets = m_view->visibleItemListWidgets(); + const auto oldHoveredExpansionWidgetIterator = std::find_if(visibleItemListWidgets.begin(), visibleItemListWidgets.end(), [](auto &widget) { + return widget->expansionAreaHovered(); + }); + const auto oldHoveredExpansionWidget = oldHoveredExpansionWidgetIterator == visibleItemListWidgets.end() ? + std::nullopt : std::make_optional(*oldHoveredExpansionWidgetIterator); - if (oldHoveredWidget != newHoveredWidget) { - if (oldHoveredWidget) { + const auto unhoverOldHoveredWidget = [&]() { + if (auto oldHoveredWidget = hoveredWidget(); oldHoveredWidget) { + // handle the text+icon one oldHoveredWidget->setHovered(false); Q_EMIT itemUnhovered(oldHoveredWidget->index()); } + }; - if (newHoveredWidget) { - newHoveredWidget->setHovered(true); - const QPointF mappedPos = newHoveredWidget->mapFromItem(m_view, pos); - newHoveredWidget->setHoverPosition(mappedPos); - Q_EMIT itemHovered(newHoveredWidget->index()); + const auto unhoverOldExpansionWidget = [&]() { + if (oldHoveredExpansionWidget) { + // then the expansion toggle + (*oldHoveredExpansionWidget)->setExpansionAreaHovered(false); } - } else if (oldHoveredWidget) { - const QPointF mappedPos = oldHoveredWidget->mapFromItem(m_view, pos); - oldHoveredWidget->setHoverPosition(mappedPos); - } + }; + + const QPointF pos = transform.map(event->pos()); + if (KItemListWidget *newHoveredWidget = widgetForPos(pos); newHoveredWidget) { + // something got hovered, work out which part and set hover for the appropriate widget + const auto mappedPos = newHoveredWidget->mapFromItem(m_view, pos); + const bool isOnExpansionToggle = newHoveredWidget->expansionToggleRect().contains(mappedPos); + + if (isOnExpansionToggle) { + // make sure we unhover the old one first if old!=new + if (oldHoveredExpansionWidget && *oldHoveredExpansionWidget != newHoveredWidget) { + (*oldHoveredExpansionWidget)->setExpansionAreaHovered(false); + } + // we also unhover any old icon+text hovers, in case the mouse movement from icon+text to expansion toggle is too fast (i.e. newHoveredWidget is never null between the transition) + unhoverOldHoveredWidget(); + + newHoveredWidget->setExpansionAreaHovered(true); + } else { + // make sure we unhover the old one first if old!=new + if (auto oldHoveredWidget = hoveredWidget(); oldHoveredWidget && oldHoveredWidget != newHoveredWidget) { + oldHoveredWidget->setHovered(false); + Q_EMIT itemUnhovered(oldHoveredWidget->index()); + } + // we also unhover any old expansion toggle hovers, in case the mouse movement from expansion toggle to icon+text is too fast (i.e. newHoveredWidget is never null between the transition) + unhoverOldExpansionWidget(); + + const bool isOverIconAndText = newHoveredWidget->iconRect().contains(mappedPos) || newHoveredWidget->textRect().contains(mappedPos); + const bool hasMultipleSelection = m_selectionManager->selectedItems().count() > 1; + + if (hasMultipleSelection && !isOverIconAndText) { + // In case we have multiple selections, clicking on any row will deselect the selection. + // So, as a visual cue for signalling that clicking anywhere won't select, but clear current highlights, + // we disable hover of the *row*(i.e. blank space to the right of the icon+text) + + // (no-op in this branch for masked hover) + } else { + newHoveredWidget->setHovered(true); + newHoveredWidget->setHoverPosition(mappedPos); + Q_EMIT itemHovered(newHoveredWidget->index()); + } + } + } else { + // unhover any currently hovered expansion and text+icon widgets + unhoverOldHoveredWidget(); + unhoverOldExpansionWidget(); + } return false; } @@ -998,10 +1047,10 @@ void KItemListController::tapAndHoldTriggered(QGestureEvent* event, const QTrans m_pressedMousePos = transform.map(event->mapToGraphicsScene(tap->position())); m_pressedIndex = m_view->itemAt(m_pressedMousePos); - if (m_pressedIndex >= 0 && !m_selectionManager->isSelected(m_pressedIndex)) { + if (m_pressedIndex.has_value() && !m_selectionManager->isSelected(m_pressedIndex.value())) { m_selectionManager->clearSelection(); - m_selectionManager->setSelected(m_pressedIndex); - } else if (m_pressedIndex == -1) { + m_selectionManager->setSelected(m_pressedIndex.value()); + } else if (!m_pressedIndex.has_value()) { m_selectionManager->clearSelection(); startRubberBand(); } @@ -1064,9 +1113,9 @@ void KItemListController::swipeTriggered(QGestureEvent* event, const QTransform& Q_EMIT scrollerStop(); if (swipe->swipeAngle() <= 20 || swipe->swipeAngle() >= 340) { - Q_EMIT mouseButtonPressed(m_pressedIndex, Qt::BackButton); + Q_EMIT mouseButtonPressed(m_pressedIndex.value_or(-1), Qt::BackButton); } else if (swipe->swipeAngle() <= 200 && swipe->swipeAngle() >= 160) { - Q_EMIT mouseButtonPressed(m_pressedIndex, Qt::ForwardButton); + Q_EMIT mouseButtonPressed(m_pressedIndex.value_or(-1), Qt::ForwardButton); } else if (swipe->swipeAngle() <= 110 && swipe->swipeAngle() >= 60) { Q_EMIT swipeUp(); } @@ -1085,7 +1134,7 @@ void KItemListController::twoFingerTapTriggered(QGestureEvent* event, const QTra if (twoTap->state() == Qt::GestureStarted) { m_pressedMousePos = transform.map(twoTap->pos()); m_pressedIndex = m_view->itemAt(m_pressedMousePos); - if (m_pressedIndex >= 0) { + if (m_pressedIndex.has_value()) { onPress(twoTap->screenPos().toPoint(), twoTap->pos().toPoint(), Qt::ControlModifier, Qt::LeftButton); onRelease(transform.map(twoTap->pos()), Qt::ControlModifier, Qt::LeftButton, false); } @@ -1303,10 +1352,7 @@ KItemListWidget* KItemListController::widgetForPos(const QPointF& pos) const const auto widgets = m_view->visibleItemListWidgets(); for (KItemListWidget* widget : widgets) { const QPointF mappedPos = widget->mapFromItem(m_view, pos); - - const bool hovered = widget->contains(mappedPos) && - !widget->expansionToggleRect().contains(mappedPos); - if (hovered) { + if (widget->contains(mappedPos) || widget->selectionRect().contains(mappedPos)) { return widget; } } @@ -1419,7 +1465,7 @@ void KItemListController::updateExtendedSelectionRegion() bool KItemListController::onPress(const QPoint& screenPos, const QPointF& pos, const Qt::KeyboardModifiers modifiers, const Qt::MouseButtons buttons) { - Q_EMIT mouseButtonPressed(m_pressedIndex, buttons); + Q_EMIT mouseButtonPressed(m_pressedIndex.value_or(-1), buttons); if (buttons & (Qt::BackButton | Qt::ForwardButton)) { // Do not select items when clicking the back/forward buttons, see @@ -1427,26 +1473,27 @@ bool KItemListController::onPress(const QPoint& screenPos, const QPointF& pos, c return true; } - if (m_view->isAboveExpansionToggle(m_pressedIndex, m_pressedMousePos)) { + if (m_view->isAboveExpansionToggle(m_pressedIndex.value_or(-1), m_pressedMousePos)) { m_selectionManager->endAnchoredSelection(); - m_selectionManager->setCurrentItem(m_pressedIndex); - m_selectionManager->beginAnchoredSelection(m_pressedIndex); + m_selectionManager->setCurrentItem(m_pressedIndex.value()); + m_selectionManager->beginAnchoredSelection(m_pressedIndex.value()); return true; } - m_selectionTogglePressed = m_view->isAboveSelectionToggle(m_pressedIndex, m_pressedMousePos); + m_selectionTogglePressed = m_view->isAboveSelectionToggle(m_pressedIndex.value_or(-1), m_pressedMousePos); if (m_selectionTogglePressed) { - m_selectionManager->setSelected(m_pressedIndex, 1, KItemListSelectionManager::Toggle); + m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Toggle); // The previous anchored selection has been finished already in // KItemListSelectionManager::setSelected(). We can safely change // the current item and start a new anchored selection now. - m_selectionManager->setCurrentItem(m_pressedIndex); - m_selectionManager->beginAnchoredSelection(m_pressedIndex); + m_selectionManager->setCurrentItem(m_pressedIndex.value()); + m_selectionManager->beginAnchoredSelection(m_pressedIndex.value()); return true; } const bool shiftPressed = modifiers & Qt::ShiftModifier; const bool controlPressed = modifiers & Qt::ControlModifier; + const bool rightClick = buttons & Qt::RightButton; // The previous selection is cleared if either // 1. The selection mode is SingleSelection, or @@ -1456,11 +1503,35 @@ bool KItemListController::onPress(const QPoint& screenPos, const QPointF& pos, c // - start dragging multiple items, or // - open the context menu and perform an action for all selected items. const bool shiftOrControlPressed = shiftPressed || controlPressed; - const bool pressedItemAlreadySelected = m_pressedIndex >= 0 && m_selectionManager->isSelected(m_pressedIndex); + const bool pressedItemAlreadySelected = m_pressedIndex.has_value() && m_selectionManager->isSelected(m_pressedIndex.value()); const bool clearSelection = m_selectionBehavior == SingleSelection || (!shiftOrControlPressed && !pressedItemAlreadySelected); + + + // When this method returns false, a rubberBand selection is created using KItemListController::startRubberBand via the caller. if (clearSelection) { + const int selectedItemsCount = m_selectionManager->selectedItems().count(); m_selectionManager->clearSelection(); + // clear and bail when we got an existing multi-selection + if (selectedItemsCount > 1 && m_pressedIndex.has_value()) { + const auto row = m_view->m_visibleItems.value(m_pressedIndex.value()); + const auto mappedPos = row->mapFromItem(m_view, pos); + if (pressedItemAlreadySelected || row->iconRect().contains(mappedPos) || row->textRect().contains(mappedPos)) { + // we are indeed inside the text/icon rect, keep m_pressedIndex what it is + // and short-circuit for single-click activation (it will then propagate to onRelease and activate the item) + // or we just keep going for double-click activation + if (m_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick) || m_singleClickActivationEnforced) { + return true; // event handled, don't create rubber band + } + } else { + // we're not inside the text/icon rect, as we've already cleared the selection + // we can just stop here and make sure handlers down the line (i.e. onRelease) don't activate + m_pressedIndex.reset(); + // we don't stop event propagation and proceed to create a rubber band and let onRelease + // decide (based on m_pressedIndex) whether we're in a drag (drag => new rubber band, click => don't select the item) + return false; + } + } } else if (pressedItemAlreadySelected && !shiftOrControlPressed && (buttons & Qt::LeftButton)) { // The user might want to start dragging multiple items, but if he clicks the item // in order to trigger it instead, the other selected items must be deselected. @@ -1469,8 +1540,8 @@ bool KItemListController::onPress(const QPoint& screenPos, const QPointF& pos, c // clear the selection in mouseReleaseEvent(), unless the items are dragged. m_clearSelectionIfItemsAreNotDragged = true; - if (m_selectionManager->selectedItems().count() == 1 && m_view->isAboveText(m_pressedIndex, m_pressedMousePos)) { - Q_EMIT selectedItemTextPressed(m_pressedIndex); + if (m_selectionManager->selectedItems().count() == 1 && m_view->isAboveText(m_pressedIndex.value_or(-1), m_pressedMousePos)) { + Q_EMIT selectedItemTextPressed(m_pressedIndex.value_or(-1)); } } @@ -1479,7 +1550,14 @@ bool KItemListController::onPress(const QPoint& screenPos, const QPointF& pos, c m_selectionManager->endAnchoredSelection(); } - if (buttons & Qt::RightButton) { + if (rightClick) { + + // Do header hit check and short circuit before commencing any state changing effects + if (m_view->headerBoundaries().contains(pos)) { + Q_EMIT headerContextMenuRequested(screenPos); + return true; + } + // Stop rubber band from persisting after right-clicks KItemListRubberBand* rubberBand = m_view->rubberBand(); if (rubberBand->isActive()) { @@ -1489,25 +1567,37 @@ bool KItemListController::onPress(const QPoint& screenPos, const QPointF& pos, c } } - if (m_pressedIndex >= 0) { - m_selectionManager->setCurrentItem(m_pressedIndex); + if (m_pressedIndex.has_value()) { + m_selectionManager->setCurrentItem(m_pressedIndex.value()); + const auto row = m_view->m_visibleItems.value(m_pressedIndex.value()); // anything outside of row.contains() will be the empty region of the row rect + const bool hitTargetIsRowEmptyRegion = !row->contains(row->mapFromItem(m_view, pos)); + // again, when this method returns false, a rubberBand selection is created as the event is not consumed; + // createRubberBand here tells us whether to return true or false. + bool createRubberBand = (hitTargetIsRowEmptyRegion && m_selectionManager->selectedItems().isEmpty()); + + if (rightClick && hitTargetIsRowEmptyRegion) { + // we got a right click outside the text rect, default to action on the current url and not the pressed item + Q_EMIT itemContextMenuRequested(m_pressedIndex.value(), screenPos); + return true; + } switch (m_selectionBehavior) { case NoSelection: break; case SingleSelection: - m_selectionManager->setSelected(m_pressedIndex); + m_selectionManager->setSelected(m_pressedIndex.value()); break; case MultiSelection: if (controlPressed && !shiftPressed) { - m_selectionManager->setSelected(m_pressedIndex, 1, KItemListSelectionManager::Toggle); - m_selectionManager->beginAnchoredSelection(m_pressedIndex); + m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Toggle); + m_selectionManager->beginAnchoredSelection(m_pressedIndex.value()); + createRubberBand = false; // multi selection, don't propagate any further } else if (!shiftPressed || !m_selectionManager->isAnchoredSelectionActive()) { // Select the pressed item and start a new anchored selection - m_selectionManager->setSelected(m_pressedIndex, 1, KItemListSelectionManager::Select); - m_selectionManager->beginAnchoredSelection(m_pressedIndex); + m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Select); + m_selectionManager->beginAnchoredSelection(m_pressedIndex.value()); } break; @@ -1516,20 +1606,16 @@ bool KItemListController::onPress(const QPoint& screenPos, const QPointF& pos, c break; } - if (buttons & Qt::RightButton) { - Q_EMIT itemContextMenuRequested(m_pressedIndex, screenPos); + if (rightClick) { + Q_EMIT itemContextMenuRequested(m_pressedIndex.value(), screenPos); } - - return true; + return !createRubberBand; } - if (buttons & Qt::RightButton) { - const QRectF headerBounds = m_view->headerBoundaries(); - if (headerBounds.contains(pos)) { - Q_EMIT headerContextMenuRequested(screenPos); - } else { - Q_EMIT viewContextMenuRequested(screenPos); - } + if (rightClick) { + // header right click handling would have been done before this so just normal context + // menu here is fine + Q_EMIT viewContextMenuRequested(screenPos); return true; } @@ -1538,14 +1624,14 @@ bool KItemListController::onPress(const QPoint& screenPos, const QPointF& pos, c bool KItemListController::onRelease(const QPointF& pos, const Qt::KeyboardModifiers modifiers, const Qt::MouseButtons buttons, bool touch) { - const bool isAboveSelectionToggle = m_view->isAboveSelectionToggle(m_pressedIndex, m_pressedMousePos); + const bool isAboveSelectionToggle = m_view->isAboveSelectionToggle(m_pressedIndex.value_or(-1), m_pressedMousePos); if (isAboveSelectionToggle) { m_selectionTogglePressed = false; return true; } if (!isAboveSelectionToggle && m_selectionTogglePressed) { - m_selectionManager->setSelected(m_pressedIndex, 1, KItemListSelectionManager::Toggle); + m_selectionManager->setSelected(m_pressedIndex.value_or(-1), 1, KItemListSelectionManager::Toggle); m_selectionTogglePressed = false; return true; } @@ -1554,60 +1640,77 @@ bool KItemListController::onRelease(const QPointF& pos, const Qt::KeyboardModifi const bool shiftOrControlPressed = modifiers & Qt::ShiftModifier || controlPressed; + const std::optional<int> index = m_view->itemAt(pos); + KItemListRubberBand* rubberBand = m_view->rubberBand(); + bool rubberBandRelease = false; if (rubberBand->isActive()) { disconnect(rubberBand, &KItemListRubberBand::endPositionChanged, this, &KItemListController::slotRubberBandChanged); rubberBand->setActive(false); m_oldSelection.clear(); m_view->setAutoScroll(false); + rubberBandRelease = true; + // We check for actual rubber band drag here: if delta between start and end is less than drag threshold, + // then we have a single click on one of the rows + if ((rubberBand->endPosition() - rubberBand->startPosition()).manhattanLength() < QApplication::startDragDistance()) { + rubberBandRelease = false; // since we're only selecting, unmark rubber band release flag + // m_pressedIndex will have no value if we came from a multi-selection clearing onPress + // in that case, we don't select anything + if (index.has_value() && m_pressedIndex.has_value()) { + if (controlPressed && m_selectionBehavior == MultiSelection) { + m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Toggle); + } else { + m_selectionManager->setSelected(index.value()); + } + if (!m_selectionManager->isAnchoredSelectionActive()) { + m_selectionManager->beginAnchoredSelection(index.value()); + } + } + } } - const int index = m_view->itemAt(pos); - - if (index >= 0 && index == m_pressedIndex) { + if (index.has_value() && index == m_pressedIndex) { // The release event is done above the same item as the press event if (m_clearSelectionIfItemsAreNotDragged) { // A selected item has been clicked, but no drag operation has been started // -> clear the rest of the selection. m_selectionManager->clearSelection(); - m_selectionManager->setSelected(m_pressedIndex, 1, KItemListSelectionManager::Select); - m_selectionManager->beginAnchoredSelection(m_pressedIndex); + m_selectionManager->setSelected(m_pressedIndex.value(), 1, KItemListSelectionManager::Select); + m_selectionManager->beginAnchoredSelection(m_pressedIndex.value()); } if (buttons & Qt::LeftButton) { bool emitItemActivated = true; - if (m_view->isAboveExpansionToggle(index, pos)) { - const bool expanded = m_model->isExpanded(index); - m_model->setExpanded(index, !expanded); + if (m_view->isAboveExpansionToggle(index.value(), pos)) { + const bool expanded = m_model->isExpanded(index.value()); + m_model->setExpanded(index.value(), !expanded); - Q_EMIT itemExpansionToggleClicked(index); + Q_EMIT itemExpansionToggleClicked(index.value()); emitItemActivated = false; - } else if (shiftOrControlPressed) { - // The mouse click should only update the selection, not trigger the item + } else if (shiftOrControlPressed && m_selectionBehavior != SingleSelection) { + // The mouse click should only update the selection, not trigger the item, except when + // we are in single selection mode emitItemActivated = false; - // When Ctrl-clicking an item when in single selection mode - // i.e. where Ctrl won't change the selection, pretend it was middle clicked - if (controlPressed && m_selectionBehavior == SingleSelection) { - Q_EMIT itemMiddleClicked(index); - } - } else if (!(m_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick) || m_singleClickActivationEnforced)) { - if (touch) { - emitItemActivated = true; + } else { + const bool singleClickActivation = m_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick) || m_singleClickActivationEnforced; + if (!singleClickActivation) { + emitItemActivated = touch; } else { - emitItemActivated = false; + // activate on single click only if we didn't come from a rubber band release + emitItemActivated = !rubberBandRelease; } } if (emitItemActivated) { - Q_EMIT itemActivated(index); + Q_EMIT itemActivated(index.value()); } } else if (buttons & Qt::MiddleButton) { - Q_EMIT itemMiddleClicked(index); + Q_EMIT itemMiddleClicked(index.value()); } } m_pressedMousePos = QPointF(); - m_pressedIndex = -1; + m_pressedIndex = std::nullopt; m_clearSelectionIfItemsAreNotDragged = false; return false; } diff --git a/src/kitemviews/kitemlistcontroller.h b/src/kitemviews/kitemlistcontroller.h index 24339134e..5fe195e9f 100644 --- a/src/kitemviews/kitemlistcontroller.h +++ b/src/kitemviews/kitemlistcontroller.h @@ -9,6 +9,8 @@ #ifndef KITEMLISTCONTROLLER_H #define KITEMLISTCONTROLLER_H +#include <optional> + #include "dolphin_export.h" #include "kitemset.h" @@ -137,7 +139,7 @@ Q_SIGNALS: * Is emitted if more than one item has been activated by pressing Return/Enter * when having a selection. */ - void itemsActivated(const KItemSet& indexes); + void itemsActivated(const KItemSet &indexes); void itemMiddleClicked(int index); @@ -329,7 +331,7 @@ private: KItemListView* m_view; KItemListSelectionManager* m_selectionManager; KItemListKeyboardSearchManager* m_keyboardManager; - int m_pressedIndex; + std::optional<int> m_pressedIndex; QPointF m_pressedMousePos; QTimer* m_autoActivationTimer; diff --git a/src/kitemviews/kitemlistgroupheader.cpp b/src/kitemviews/kitemlistgroupheader.cpp index 80dd94149..f0ff52503 100644 --- a/src/kitemviews/kitemlistgroupheader.cpp +++ b/src/kitemviews/kitemlistgroupheader.cpp @@ -161,6 +161,7 @@ void KItemListGroupHeader::resizeEvent(QGraphicsSceneResizeEvent* event) if (event->oldSize().height() != event->newSize().height()) { m_dirtyCache = true; } + updateSize(); } void KItemListGroupHeader::updateCache() @@ -174,6 +175,13 @@ void KItemListGroupHeader::updateCache() m_separatorColor = mixedColor(c1, c2, 10); m_roleColor = mixedColor(c1, c2, 60); + updateSize(); + + m_dirtyCache = false; +} + +void KItemListGroupHeader::updateSize() +{ const int padding = qMax(1, m_styleOption.padding); const int horizontalMargin = qMax(2, m_styleOption.horizontalMargin); @@ -187,7 +195,7 @@ void KItemListGroupHeader::updateCache() size().width() - 2 * padding - horizontalMargin, roleHeight); - m_dirtyCache = false; + update(); } QColor KItemListGroupHeader::mixedColor(const QColor& c1, const QColor& c2, int c1Percent) diff --git a/src/kitemviews/kitemlistgroupheader.h b/src/kitemviews/kitemlistgroupheader.h index 4ba469056..48af1e9e0 100644 --- a/src/kitemviews/kitemlistgroupheader.h +++ b/src/kitemviews/kitemlistgroupheader.h @@ -94,6 +94,7 @@ protected: private: void updateCache(); + void updateSize(); static QColor mixedColor(const QColor& c1, const QColor& c2, int c1Percent = 50); diff --git a/src/kitemviews/kitemlistheader.cpp b/src/kitemviews/kitemlistheader.cpp index dedeb57e3..22e70603b 100644 --- a/src/kitemviews/kitemlistheader.cpp +++ b/src/kitemviews/kitemlistheader.cpp @@ -61,6 +61,20 @@ qreal KItemListHeader::preferredColumnWidth(const QByteArray& role) const return m_headerWidget->preferredColumnWidth(role); } +void KItemListHeader::setLeadingPadding(qreal width){ + if (m_headerWidget->leadingPadding() != width) { + m_headerWidget->setLeadingPadding(width); + if (m_headerWidget->automaticColumnResizing()) { + m_view->applyAutomaticColumnWidths(); + } + m_view->doLayout(KItemListView::NoAnimation); + } +} + +qreal KItemListHeader::leadingPadding() const{ + return m_headerWidget->leadingPadding(); +} + KItemListHeader::KItemListHeader(KItemListView* listView) : QObject(listView), m_view(listView) @@ -72,5 +86,7 @@ KItemListHeader::KItemListHeader(KItemListView* listView) : this, &KItemListHeader::columnWidthChanged); connect(m_headerWidget, &KItemListHeaderWidget::columnWidthChangeFinished, this, &KItemListHeader::columnWidthChangeFinished); + connect(m_headerWidget, &KItemListHeaderWidget::leadingPaddingChanged, + this, &KItemListHeader::leadingPaddingChanged); } diff --git a/src/kitemviews/kitemlistheader.h b/src/kitemviews/kitemlistheader.h index 6875a0f5e..e2653e41e 100644 --- a/src/kitemviews/kitemlistheader.h +++ b/src/kitemviews/kitemlistheader.h @@ -58,7 +58,16 @@ public: */ qreal preferredColumnWidth(const QByteArray& role) const; + /** + * Sets the width of the column *before* the first column. + * This is intended to facilitate an empty region for deselection in the main viewport. + */ + void setLeadingPadding(qreal width); + qreal leadingPadding() const; + Q_SIGNALS: + void leadingPaddingChanged(qreal width); + /** * Is emitted if the width of a column has been adjusted by the user with the mouse * (no signal is emitted if KItemListHeader::setColumnWidth() is invoked). diff --git a/src/kitemviews/kitemlistview.cpp b/src/kitemviews/kitemlistview.cpp index 5c8c712e8..86583db1e 100644 --- a/src/kitemviews/kitemlistview.cpp +++ b/src/kitemviews/kitemlistview.cpp @@ -389,7 +389,7 @@ qreal KItemListView::verticalPageStep() const return size().height() - headerHeight; } -int KItemListView::itemAt(const QPointF& pos) const +std::optional<int> KItemListView::itemAt(const QPointF& pos) const { QHashIterator<int, KItemListWidget*> it(m_visibleItems); while (it.hasNext()) { @@ -397,12 +397,12 @@ int KItemListView::itemAt(const QPointF& pos) const const KItemListWidget* widget = it.value(); const QPointF mappedPos = widget->mapFromItem(this, pos); - if (widget->contains(mappedPos)) { + if (widget->contains(mappedPos) || widget->selectionRect().contains(mappedPos)) { return it.key(); } } - return -1; + return std::nullopt; } bool KItemListView::isAboveSelectionToggle(int index, const QPointF& pos) const @@ -458,7 +458,7 @@ int KItemListView::lastVisibleIndex() const return m_layouter->lastVisibleIndex(); } -void KItemListView::calculateItemSizeHints(QVector<qreal>& logicalHeightHints, qreal& logicalWidthHint) const +void KItemListView::calculateItemSizeHints(QVector<std::pair<qreal, bool>>& logicalHeightHints, qreal& logicalWidthHint) const { widgetCreator()->calculateItemSizeHints(logicalHeightHints, logicalWidthHint, this); } @@ -477,6 +477,32 @@ bool KItemListView::supportsItemExpanding() const return m_supportsItemExpanding; } +void KItemListView::setHighlightEntireRow(bool highlightEntireRow) +{ + if (m_highlightEntireRow != highlightEntireRow) { + m_highlightEntireRow = highlightEntireRow; + onHighlightEntireRowChanged(highlightEntireRow); + } +} + +bool KItemListView::highlightEntireRow() const +{ + return m_highlightEntireRow; +} + +void KItemListView::setAlternateBackgrounds(bool alternate) +{ + if (m_alternateBackgrounds != alternate) { + m_alternateBackgrounds = alternate; + updateAlternateBackgrounds(); + } +} + +bool KItemListView::alternateBackgrounds() const +{ + return m_alternateBackgrounds; +} + QRectF KItemListView::itemRect(int index) const { return m_layouter->itemRect(index); @@ -495,6 +521,11 @@ QRectF KItemListView::itemContextRect(int index) const return contextRect; } +bool KItemListView::isElided(int index) const +{ + return m_sizeHintResolver->isElided(index); +} + void KItemListView::scrollToItem(int index) { QRectF viewGeometry = geometry(); @@ -576,6 +607,8 @@ void KItemListView::setHeaderVisible(bool visible) connect(m_headerWidget, &KItemListHeaderWidget::columnWidthChanged, this, &KItemListView::slotHeaderColumnWidthChanged); + connect(m_headerWidget, &KItemListHeaderWidget::leadingPaddingChanged, + this, &KItemListView::slotLeadingPaddingChanged); connect(m_headerWidget, &KItemListHeaderWidget::columnMoved, this, &KItemListView::slotHeaderColumnMoved); connect(m_headerWidget, &KItemListHeaderWidget::sortOrderChanged, @@ -588,6 +621,8 @@ void KItemListView::setHeaderVisible(bool visible) } else if (!visible && m_headerWidget->isVisible()) { disconnect(m_headerWidget, &KItemListHeaderWidget::columnWidthChanged, this, &KItemListView::slotHeaderColumnWidthChanged); + disconnect(m_headerWidget, &KItemListHeaderWidget::leadingPaddingChanged, + this, &KItemListView::slotLeadingPaddingChanged); disconnect(m_headerWidget, &KItemListHeaderWidget::columnMoved, this, &KItemListView::slotHeaderColumnMoved); disconnect(m_headerWidget, &KItemListHeaderWidget::sortOrderChanged, @@ -748,7 +783,7 @@ void KItemListView::setItemSize(const QSizeF& size) size, m_layouter->itemMargin()); - const bool alternateBackgroundsChanged = (m_visibleRoles.count() > 1) && + const bool alternateBackgroundsChanged = m_alternateBackgrounds && (( m_itemSize.isEmpty() && !size.isEmpty()) || (!m_itemSize.isEmpty() && size.isEmpty())); @@ -924,6 +959,11 @@ void KItemListView::onStyleOptionChanged(const KItemListStyleOption& current, co Q_UNUSED(previous) } +void KItemListView::onHighlightEntireRowChanged(bool highlightEntireRow) +{ + Q_UNUSED(highlightEntireRow) +} + void KItemListView::onSupportsItemExpandingChanged(bool supportsExpanding) { Q_UNUSED(supportsExpanding) @@ -1516,6 +1556,16 @@ void KItemListView::slotHeaderColumnWidthChanged(const QByteArray& role, doLayout(NoAnimation); } +void KItemListView::slotLeadingPaddingChanged(qreal width) +{ + Q_UNUSED(width) + if (m_headerWidget->automaticColumnResizing()) { + applyAutomaticColumnWidths(); + } + applyColumnWidthsFromHeader(); + doLayout(NoAnimation); +} + void KItemListView::slotHeaderColumnMoved(const QByteArray& role, int currentIndex, int previousIndex) @@ -2220,7 +2270,7 @@ void KItemListView::updateAlternateBackgroundForWidget(KItemListWidget* widget) bool KItemListView::useAlternateBackgrounds() const { - return m_itemSize.isEmpty() && m_visibleRoles.count() > 1; + return m_alternateBackgrounds && m_itemSize.isEmpty(); } QHash<QByteArray, qreal> KItemListView::preferredColumnWidths(const KItemRangeList& itemRanges) const @@ -2237,11 +2287,11 @@ QHash<QByteArray, qreal> KItemListView::preferredColumnWidths(const KItemRangeLi const int headerMargin = m_headerWidget->style()->pixelMetric(QStyle::PM_HeaderMargin); for (const QByteArray& visibleRole : qAsConst(m_visibleRoles)) { const QString headerText = m_model->roleDescription(visibleRole); - const qreal headerWidth = fontMetrics.width(headerText) + gripMargin + headerMargin * 2; + const qreal headerWidth = fontMetrics.horizontalAdvance(headerText) + gripMargin + headerMargin * 2; widths.insert(visibleRole, headerWidth); } - // Calculate the preferred column withs for each item and ignore values + // Calculate the preferred column widths for each item and ignore values // smaller than the width for showing the headline unclipped. const KItemListWidgetCreatorBase* creator = widgetCreator(); int calculatedItemCount = 0; @@ -2278,7 +2328,7 @@ QHash<QByteArray, qreal> KItemListView::preferredColumnWidths(const KItemRangeLi void KItemListView::applyColumnWidthsFromHeader() { // Apply the new size to the layouter - const qreal requiredWidth = columnWidthsSum(); + const qreal requiredWidth = columnWidthsSum() + m_headerWidget->leadingPadding(); const QSizeF dynamicItemSize(qMax(size().width(), requiredWidth), m_itemSize.height()); m_layouter->setItemSize(dynamicItemSize); @@ -2296,6 +2346,7 @@ void KItemListView::updateWidgetColumnWidths(KItemListWidget* widget) for (const QByteArray& role : qAsConst(m_visibleRoles)) { widget->setColumnWidth(role, m_headerWidget->columnWidth(role)); } + widget->setLeadingPadding(m_headerWidget->leadingPadding()); } void KItemListView::updatePreferredColumnWidths(const KItemRangeList& itemRanges) @@ -2373,7 +2424,7 @@ void KItemListView::applyAutomaticColumnWidths() qreal firstColumnWidth = m_headerWidget->columnWidth(firstRole); QSizeF dynamicItemSize = m_itemSize; - qreal requiredWidth = columnWidthsSum(); + qreal requiredWidth = columnWidthsSum() + m_headerWidget->leadingPadding(); const qreal availableWidth = size().width(); if (requiredWidth < availableWidth) { // Stretch the first column to use the whole remaining width diff --git a/src/kitemviews/kitemlistview.h b/src/kitemviews/kitemlistview.h index 760e0a415..228989cc4 100644 --- a/src/kitemviews/kitemlistview.h +++ b/src/kitemviews/kitemlistview.h @@ -9,6 +9,8 @@ #ifndef KITEMLISTVIEW_H #define KITEMLISTVIEW_H +#include <optional> + #include "dolphin_export.h" #include "kitemviews/kitemliststyleoption.h" #include "kitemviews/kitemlistwidget.h" @@ -160,10 +162,10 @@ public: * @return Index of the item that is below the point \a pos. * The position is relative to the upper right of * the visible area. Only (at least partly) visible - * items are considered. -1 is returned if no item is - * below the position. + * items are considered. std::nullopt is returned if + * no item is below the position. */ - int itemAt(const QPointF& pos) const; + std::optional<int> itemAt(const QPointF& pos) const; bool isAboveSelectionToggle(int index, const QPointF& pos) const; bool isAboveExpansionToggle(int index, const QPointF& pos) const; bool isAboveText(int index, const QPointF& pos) const; @@ -189,7 +191,7 @@ public: * @note the logical height (width) is actually the * width (height) if the scroll orientation is Qt::Vertical! */ - void calculateItemSizeHints(QVector<qreal>& logicalHeightHints, qreal& logicalWidthHint) const; + void calculateItemSizeHints(QVector<std::pair<qreal, bool>>& logicalHeightHints, qreal& logicalWidthHint) const; /** * If set to true, items having child-items can be expanded to show the child-items as @@ -203,6 +205,12 @@ public: void setSupportsItemExpanding(bool supportsExpanding); bool supportsItemExpanding() const; + void setHighlightEntireRow(bool highlightEntireRow); + bool highlightEntireRow() const; + + void setAlternateBackgrounds(bool alternate); + bool alternateBackgrounds() const; + /** * @return The rectangle of the item relative to the top/left of * the currently visible area (see KItemListView::offset()). @@ -222,6 +230,12 @@ public: QRectF itemContextRect(int index) const; /** + * @return Whether or not the name of the file has been elided. At present this will + * only ever be true when in icons view. + */ + bool isElided(int index) const; + + /** * Scrolls to the item with the index \a index so that the item * will be fully visible. */ @@ -366,6 +380,7 @@ protected: virtual void onScrollOffsetChanged(qreal current, qreal previous); virtual void onVisibleRolesChanged(const QList<QByteArray>& current, const QList<QByteArray>& previous); virtual void onStyleOptionChanged(const KItemListStyleOption& current, const KItemListStyleOption& previous); + virtual void onHighlightEntireRowChanged(bool highlightEntireRow); virtual void onSupportsItemExpandingChanged(bool supportsExpanding); virtual void onTransactionBegin(); @@ -384,6 +399,8 @@ protected: virtual void updateFont(); virtual void updatePalette(); + KItemListSizeHintResolver* m_sizeHintResolver; + protected Q_SLOTS: virtual void slotItemsInserted(const KItemRangeList& itemRanges); virtual void slotItemsRemoved(const KItemRangeList& itemRanges); @@ -416,6 +433,8 @@ private Q_SLOTS: qreal currentWidth, qreal previousWidth); + void slotLeadingPaddingChanged(qreal width); + /** * Is invoked if a column has been moved by the user. Applies * the moved role to the view. @@ -597,7 +616,7 @@ private: /** * Resizes the column-widths of m_headerWidget based on the preferred widths - * and the vailable view-size. + * and the available view-size. */ void applyAutomaticColumnWidths(); @@ -697,6 +716,8 @@ private: private: bool m_enabledSelectionToggles; bool m_grouped; + bool m_highlightEntireRow; + bool m_alternateBackgrounds; bool m_supportsItemExpanding; bool m_editingRole; int m_activeTransactions; // Counter for beginTransaction()/endTransaction() @@ -723,7 +744,6 @@ private: QHash<int, Cell> m_visibleCells; int m_scrollBarExtent; - KItemListSizeHintResolver* m_sizeHintResolver; KItemListViewLayouter* m_layouter; KItemListViewAnimation* m_animation; @@ -803,7 +823,7 @@ public: virtual void recycle(KItemListWidget* widget); - virtual void calculateItemSizeHints(QVector<qreal>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const = 0; + virtual void calculateItemSizeHints(QVector<std::pair<qreal, bool>>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const = 0; virtual qreal preferredRoleColumnWidth(const QByteArray& role, int index, @@ -822,7 +842,7 @@ public: KItemListWidget* create(KItemListView* view) override; - void calculateItemSizeHints(QVector<qreal>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const override; + void calculateItemSizeHints(QVector<std::pair<qreal, bool>>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const override; qreal preferredRoleColumnWidth(const QByteArray& role, int index, @@ -856,7 +876,7 @@ KItemListWidget* KItemListWidgetCreator<T>::create(KItemListView* view) } template<class T> -void KItemListWidgetCreator<T>::calculateItemSizeHints(QVector<qreal>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const +void KItemListWidgetCreator<T>::calculateItemSizeHints(QVector<std::pair<qreal, bool>>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const { return m_informant->calculateItemSizeHints(logicalHeightHints, logicalWidthHint, view); } @@ -873,7 +893,7 @@ qreal KItemListWidgetCreator<T>::preferredRoleColumnWidth(const QByteArray& role * @brief Base class for creating KItemListGroupHeaders. * * It is recommended that applications simply use the KItemListGroupHeaderCreator-template class. - * For a custom implementation the methods create() and recyle() must be reimplemented. + * For a custom implementation the methods create() and recycle() must be reimplemented. * The intention of the group-header creator is to prevent repetitive and expensive instantiations and * deletions of KItemListGroupHeaders by recycling existing header instances. */ diff --git a/src/kitemviews/kitemlistviewaccessible.cpp b/src/kitemviews/kitemlistviewaccessible.cpp index ffc088bdb..a1afadff4 100644 --- a/src/kitemviews/kitemlistviewaccessible.cpp +++ b/src/kitemviews/kitemlistviewaccessible.cpp @@ -5,7 +5,6 @@ */ #ifndef QT_NO_ACCESSIBILITY - #include "kitemlistviewaccessible.h" #include "kitemlistcontainer.h" @@ -202,8 +201,8 @@ QAccessible::State KItemListViewAccessible::state() const QAccessibleInterface* KItemListViewAccessible::childAt(int x, int y) const { const QPointF point = QPointF(x, y); - int itemIndex = view()->itemAt(view()->mapFromScene(point)); - return child(itemIndex); + const std::optional<int> itemIndex = view()->itemAt(view()->mapFromScene(point)); + return child(itemIndex.value_or(-1)); } QAccessibleInterface* KItemListViewAccessible::parent() const diff --git a/src/kitemviews/kitemlistwidget.cpp b/src/kitemviews/kitemlistwidget.cpp index 69a38432a..79ffee210 100644 --- a/src/kitemviews/kitemlistwidget.cpp +++ b/src/kitemviews/kitemlistwidget.cpp @@ -34,6 +34,7 @@ KItemListWidget::KItemListWidget(KItemListWidgetInformant* informant, QGraphicsI m_selected(false), m_current(false), m_hovered(false), + m_expansionAreaHovered(false), m_alternateBackground(false), m_enabledSelectionToggle(false), m_data(), @@ -180,6 +181,18 @@ qreal KItemListWidget::columnWidth(const QByteArray& role) const return m_columnWidths.value(role); } +qreal KItemListWidget::leadingPadding() const { + return m_leadingPadding; +} + +void KItemListWidget::setLeadingPadding(qreal width) { + if (m_leadingPadding != width){ + m_leadingPadding = width; + leadingPaddingChanged(width); + update(); + } +} + void KItemListWidget::setStyleOption(const KItemListStyleOption& option) { if (m_styleOption == option) { @@ -280,6 +293,20 @@ bool KItemListWidget::isHovered() const return m_hovered; } +void KItemListWidget::setExpansionAreaHovered(bool hovered) +{ + if (hovered == m_expansionAreaHovered) { + return; + } + m_expansionAreaHovered = hovered; + update(); +} + +bool KItemListWidget::expansionAreaHovered() const +{ + return m_expansionAreaHovered; +} + void KItemListWidget::setHoverPosition(const QPointF& pos) { if (m_selectionToggle) { @@ -416,6 +443,11 @@ void KItemListWidget::columnWidthChanged(const QByteArray& role, Q_UNUSED(previous) } +void KItemListWidget::leadingPaddingChanged(qreal width) +{ + Q_UNUSED(width) +} + void KItemListWidget::styleOptionChanged(const KItemListStyleOption& current, const KItemListStyleOption& previous) { diff --git a/src/kitemviews/kitemlistwidget.h b/src/kitemviews/kitemlistwidget.h index 2de82c6fb..0b82266c4 100644 --- a/src/kitemviews/kitemlistwidget.h +++ b/src/kitemviews/kitemlistwidget.h @@ -35,7 +35,7 @@ public: KItemListWidgetInformant(); virtual ~KItemListWidgetInformant(); - virtual void calculateItemSizeHints(QVector<qreal>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const = 0; + virtual void calculateItemSizeHints(QVector<std::pair<qreal, bool>>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const = 0; virtual qreal preferredRoleColumnWidth(const QByteArray& role, int index, @@ -80,6 +80,9 @@ public: void setColumnWidth(const QByteArray& role, qreal width); qreal columnWidth(const QByteArray& role) const; + void setLeadingPadding(qreal width); + qreal leadingPadding() const; + void setStyleOption(const KItemListStyleOption& option); const KItemListStyleOption& styleOption() const; @@ -94,6 +97,9 @@ public: void setHovered(bool hovered); bool isHovered() const; + void setExpansionAreaHovered(bool hover); + bool expansionAreaHovered() const; + void setHoverPosition(const QPointF& pos); void setAlternateBackground(bool enable); @@ -182,6 +188,7 @@ protected: virtual void dataChanged(const QHash<QByteArray, QVariant>& current, const QSet<QByteArray>& roles = QSet<QByteArray>()); virtual void visibleRolesChanged(const QList<QByteArray>& current, const QList<QByteArray>& previous); virtual void columnWidthChanged(const QByteArray& role, qreal current, qreal previous); + virtual void leadingPaddingChanged(qreal width); virtual void styleOptionChanged(const KItemListStyleOption& current, const KItemListStyleOption& previous); virtual void currentChanged(bool current); virtual void selectedChanged(bool selected); @@ -190,6 +197,7 @@ protected: virtual void siblingsInformationChanged(const QBitArray& current, const QBitArray& previous); virtual void editedRoleChanged(const QByteArray& current, const QByteArray& previous); void resizeEvent(QGraphicsSceneResizeEvent* event) override; + void clearHoverCache(); /** * Called when the user starts hovering this item. @@ -225,7 +233,6 @@ private Q_SLOTS: private: void initializeSelectionToggle(); void setHoverOpacity(qreal opacity); - void clearHoverCache(); void drawItemStyleOption(QPainter* painter, QWidget* widget, QStyle::State styleState); private: @@ -236,11 +243,13 @@ private: bool m_selected; bool m_current; bool m_hovered; + bool m_expansionAreaHovered; bool m_alternateBackground; bool m_enabledSelectionToggle; QHash<QByteArray, QVariant> m_data; QList<QByteArray> m_visibleRoles; QHash<QByteArray, qreal> m_columnWidths; + qreal m_leadingPadding; KItemListStyleOption m_styleOption; QBitArray m_siblingsInfo; diff --git a/src/kitemviews/kstandarditem.cpp b/src/kitemviews/kstandarditem.cpp deleted file mode 100644 index fcaa50b9d..000000000 --- a/src/kitemviews/kstandarditem.cpp +++ /dev/null @@ -1,129 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2012 Peter Penz <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#include "kstandarditem.h" -#include "kstandarditemmodel.h" - -KStandardItem::KStandardItem(KStandardItem* parent) : - QObject(parent), - m_model(nullptr), - m_data() -{ -} - -KStandardItem::KStandardItem(const QString& text, KStandardItem* parent) : - QObject(parent), - m_model(nullptr), - m_data() -{ - setText(text); -} - -KStandardItem::KStandardItem(const QString& icon, const QString& text, KStandardItem* parent) : - QObject(parent), - m_model(nullptr), - m_data() -{ - setIcon(icon); - setText(text); -} - -KStandardItem::~KStandardItem() -{ -} - -void KStandardItem::setText(const QString& text) -{ - setDataValue("text", text); -} - -QString KStandardItem::text() const -{ - return m_data["text"].toString(); -} - -void KStandardItem::setIcon(const QString& icon) -{ - setDataValue("iconName", icon); -} - -QString KStandardItem::icon() const -{ - return m_data["iconName"].toString(); -} - -void KStandardItem::setIconOverlays(const QStringList& overlays) -{ - setDataValue("iconOverlays", overlays); -} - -QStringList KStandardItem::iconOverlays() const -{ - return m_data["iconOverlays"].toStringList(); -} - -void KStandardItem::setGroup(const QString& group) -{ - setDataValue("group", group); -} - -QString KStandardItem::group() const -{ - return m_data["group"].toString(); -} - -void KStandardItem::setDataValue(const QByteArray& role, const QVariant& value) -{ - const QVariant previous = m_data.value(role); - if (previous == value) { - return; - } - - m_data.insert(role, value); - onDataValueChanged(role, value, previous); - - if (m_model) { - const int index = m_model->index(this); - QSet<QByteArray> changedRoles; - changedRoles.insert(role); - m_model->onItemChanged(index, changedRoles); - Q_EMIT m_model->itemsChanged(KItemRangeList() << KItemRange(index, 1), changedRoles); - } -} - -QVariant KStandardItem::dataValue(const QByteArray& role) const -{ - return m_data[role]; -} - -void KStandardItem::setData(const QHash<QByteArray, QVariant>& values) -{ - const QHash<QByteArray, QVariant> previous = m_data; - m_data = values; - onDataChanged(values, previous); -} - -QHash<QByteArray, QVariant> KStandardItem::data() const -{ - return m_data; -} - -void KStandardItem::onDataValueChanged(const QByteArray& role, - const QVariant& current, - const QVariant& previous) -{ - Q_UNUSED(role) - Q_UNUSED(current) - Q_UNUSED(previous) -} - -void KStandardItem::onDataChanged(const QHash<QByteArray, QVariant>& current, - const QHash<QByteArray, QVariant>& previous) -{ - Q_UNUSED(current) - Q_UNUSED(previous) -} - diff --git a/src/kitemviews/kstandarditem.h b/src/kitemviews/kstandarditem.h deleted file mode 100644 index fb64b334d..000000000 --- a/src/kitemviews/kstandarditem.h +++ /dev/null @@ -1,81 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2012 Peter Penz <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#ifndef KSTANDARDITEM_H -#define KSTANDARDITEM_H - -#include "dolphin_export.h" - -#include <QByteArray> -#include <QHash> -#include <QObject> -#include <QSet> -#include <QVariant> - -class KStandardItemModel; - -/** - * @brief Represents and item of KStandardItemModel. - * - * Provides setter- and getter-methods for the most commonly - * used roles. It is possible to assign values for custom - * roles by using setDataValue(). - */ -class DOLPHIN_EXPORT KStandardItem : public QObject -{ - Q_OBJECT -public: - explicit KStandardItem(KStandardItem* parent = nullptr); - explicit KStandardItem(const QString& text, KStandardItem* parent = nullptr); - KStandardItem(const QString& icon, const QString& text, KStandardItem* parent = nullptr); - ~KStandardItem() override; - - /** - * Sets the text for the "text"-role. - */ - void setText(const QString& text); - QString text() const; - - /** - * Sets the icon for the "iconName"-role. - */ - void setIcon(const QString& icon); - QString icon() const; - - void setIconOverlays(const QStringList& overlays); - QStringList iconOverlays() const; - - /** - * Sets the group for the "group"-role. - */ - void setGroup(const QString& group); - QString group() const; - - void setDataValue(const QByteArray& role, const QVariant& value); - QVariant dataValue(const QByteArray& role) const; - - void setData(const QHash<QByteArray, QVariant>& values); - QHash<QByteArray, QVariant> data() const; - -protected: - virtual void onDataValueChanged(const QByteArray& role, - const QVariant& current, - const QVariant& previous); - - virtual void onDataChanged(const QHash<QByteArray, QVariant>& current, - const QHash<QByteArray, QVariant>& previous); - -private: - KStandardItemModel* m_model; - - QHash<QByteArray, QVariant> m_data; - - friend class KStandardItemModel; -}; - -#endif - - diff --git a/src/kitemviews/kstandarditemlistgroupheader.cpp b/src/kitemviews/kstandarditemlistgroupheader.cpp index 22eefe5ea..28497ddfc 100644 --- a/src/kitemviews/kstandarditemlistgroupheader.cpp +++ b/src/kitemviews/kstandarditemlistgroupheader.cpp @@ -17,8 +17,7 @@ KStandardItemListGroupHeader::KStandardItemListGroupHeader(QGraphicsWidget* pare m_text(), m_pixmap() { - m_text.setTextFormat(Qt::PlainText); - m_text.setPerformanceHint(QStaticText::AggressiveCaching); + } KStandardItemListGroupHeader::~KStandardItemListGroupHeader() @@ -37,7 +36,7 @@ void KStandardItemListGroupHeader::paintRole(QPainter* painter, const QRectF& ro { if (m_pixmap.isNull()) { painter->setPen(color); - painter->drawStaticText(roleBounds.topLeft(), m_text); + painter->drawText(roleBounds, 0, m_text); } else { painter->drawPixmap(roleBounds.topLeft(), m_pixmap); } @@ -55,7 +54,11 @@ void KStandardItemListGroupHeader::paintSeparator(QPainter* painter, const QColo if (scrollOrientation() == Qt::Horizontal) { painter->drawLine(0, 0, 0, size().height() - 1); } else { - painter->drawLine(0, 0, size().width() - 1, 0); + if (layoutDirection() == Qt::LeftToRight) { + painter->drawLine(0, 0, size().width() - 1, 0); + } else { + painter->drawLine(1, 0, size().width(), 0); + } } } @@ -75,7 +78,7 @@ void KStandardItemListGroupHeader::dataChanged(const QVariant& current, const QV void KStandardItemListGroupHeader::resizeEvent(QGraphicsSceneResizeEvent* event) { - QGraphicsWidget::resizeEvent(event); + KItemListGroupHeader::resizeEvent(event); m_dirtyCache = true; } @@ -87,7 +90,7 @@ void KStandardItemListGroupHeader::updateCache() const qreal maxWidth = size().width() - 4 * styleOption().padding; if (role() == "rating") { - m_text.setText(QString()); + m_text = QString(); const qreal height = styleOption().fontMetrics.ascent(); const QSizeF pixmapSize(qMin(height * 5, maxWidth), height); @@ -104,7 +107,7 @@ void KStandardItemListGroupHeader::updateCache() QFontMetricsF fontMetrics(font()); const QString text = fontMetrics.elidedText(data().toString(), Qt::ElideRight, maxWidth); - m_text.setText(text); + m_text = text; } } diff --git a/src/kitemviews/kstandarditemlistgroupheader.h b/src/kitemviews/kstandarditemlistgroupheader.h index ff428c4ea..965bf995c 100644 --- a/src/kitemviews/kstandarditemlistgroupheader.h +++ b/src/kitemviews/kstandarditemlistgroupheader.h @@ -35,7 +35,7 @@ private: private: bool m_dirtyCache; - QStaticText m_text; + QString m_text; QPixmap m_pixmap; }; #endif diff --git a/src/kitemviews/kstandarditemlistview.cpp b/src/kitemviews/kstandarditemlistview.cpp index 6edbefad8..4b7c2d9a4 100644 --- a/src/kitemviews/kstandarditemlistview.cpp +++ b/src/kitemviews/kstandarditemlistview.cpp @@ -17,6 +17,7 @@ KStandardItemListView::KStandardItemListView(QGraphicsWidget* parent) : setAcceptDrops(true); setScrollOrientation(Qt::Vertical); setVisibleRoles({"text"}); + setAlternateBackgrounds(true); } KStandardItemListView::~KStandardItemListView() @@ -34,6 +35,8 @@ void KStandardItemListView::setItemLayout(ItemLayout layout) const ItemLayout previous = m_itemLayout; m_itemLayout = layout; + // keep the leading padding option unchanged here + setHighlightEntireRow(layout == DetailsLayout); setSupportsItemExpanding(itemLayoutSupportsItemExpanding(layout)); setScrollOrientation(layout == CompactLayout ? Qt::Horizontal : Qt::Vertical); @@ -69,6 +72,7 @@ void KStandardItemListView::initializeItemListWidget(KItemListWidget* item) default: Q_ASSERT(false); break; } + standardItemListWidget->setHighlightEntireRow(highlightEntireRow()); standardItemListWidget->setSupportsItemExpanding(supportsItemExpanding()); } diff --git a/src/kitemviews/kstandarditemlistwidget.cpp b/src/kitemviews/kstandarditemlistwidget.cpp index 9c527fa17..04d8e0f7e 100644 --- a/src/kitemviews/kstandarditemlistwidget.cpp +++ b/src/kitemviews/kstandarditemlistwidget.cpp @@ -36,7 +36,7 @@ KStandardItemListWidgetInformant::~KStandardItemListWidgetInformant() { } -void KStandardItemListWidgetInformant::calculateItemSizeHints(QVector<qreal>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const +void KStandardItemListWidgetInformant::calculateItemSizeHints(QVector<std::pair<qreal, bool>>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const { switch (static_cast<const KStandardItemListView*>(view)->itemLayout()) { case KStandardItemListView::IconsLayout: @@ -121,7 +121,7 @@ QFont KStandardItemListWidgetInformant::customizedFontForLinks(const QFont& base return baseFont; } -void KStandardItemListWidgetInformant::calculateIconsLayoutItemSizeHints(QVector<qreal>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const +void KStandardItemListWidgetInformant::calculateIconsLayoutItemSizeHints(QVector<std::pair<qreal, bool>>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const { const KItemListStyleOption& option = view->styleOption(); const QFont& normalFont = option.font; @@ -138,7 +138,7 @@ void KStandardItemListWidgetInformant::calculateIconsLayoutItemSizeHints(QVector textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); for (int index = 0; index < logicalHeightHints.count(); ++index) { - if (logicalHeightHints.at(index) > 0.0) { + if (logicalHeightHints.at(index).first > 0.0) { continue; } @@ -146,7 +146,7 @@ void KStandardItemListWidgetInformant::calculateIconsLayoutItemSizeHints(QVector const QFont& font = itemIsLink(index, view) ? linkFont : normalFont; const QString& text = KStringHandler::preProcessWrap(itemText(index, view)); - + // Calculate the number of lines required for wrapping the name qreal textHeight = 0; QTextLayout layout(text, font); @@ -154,6 +154,7 @@ void KStandardItemListWidgetInformant::calculateIconsLayoutItemSizeHints(QVector layout.beginLayout(); QTextLine line; int lineCount = 0; + bool isElided = false; while ((line = layout.createLine()).isValid()) { line.setLineWidth(maxWidth); line.naturalTextWidth(); @@ -161,6 +162,7 @@ void KStandardItemListWidgetInformant::calculateIconsLayoutItemSizeHints(QVector ++lineCount; if (lineCount == option.maxTextLines) { + isElided = layout.createLine().isValid(); break; } } @@ -169,13 +171,14 @@ void KStandardItemListWidgetInformant::calculateIconsLayoutItemSizeHints(QVector // Add one line for each additional information textHeight += additionalRolesSpacing; - logicalHeightHints[index] = textHeight + spacingAndIconHeight; + logicalHeightHints[index].first = textHeight + spacingAndIconHeight; + logicalHeightHints[index].second = isElided; } logicalWidthHint = itemWidth; } -void KStandardItemListWidgetInformant::calculateCompactLayoutItemSizeHints(QVector<qreal>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const +void KStandardItemListWidgetInformant::calculateCompactLayoutItemSizeHints(QVector<std::pair<qreal, bool>>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const { const KItemListStyleOption& option = view->styleOption(); const QFontMetrics& normalFontMetrics = option.fontMetrics; @@ -190,7 +193,7 @@ void KStandardItemListWidgetInformant::calculateCompactLayoutItemSizeHints(QVect const QFontMetrics linkFontMetrics(customizedFontForLinks(option.font)); for (int index = 0; index < logicalHeightHints.count(); ++index) { - if (logicalHeightHints.at(index) > 0.0) { + if (logicalHeightHints.at(index).first > 0.0) { continue; } @@ -217,13 +220,13 @@ void KStandardItemListWidgetInformant::calculateCompactLayoutItemSizeHints(QVect width = maxWidth; } - logicalHeightHints[index] = width; + logicalHeightHints[index].first = width; } logicalWidthHint = height; } -void KStandardItemListWidgetInformant::calculateDetailsLayoutItemSizeHints(QVector<qreal>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const +void KStandardItemListWidgetInformant::calculateDetailsLayoutItemSizeHints(QVector<std::pair<qreal, bool>>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const { const KItemListStyleOption& option = view->styleOption(); const qreal height = option.padding * 2 + qMax(option.iconSize, option.fontMetrics.height()); @@ -247,6 +250,7 @@ KStandardItemListWidget::KStandardItemListWidget(KItemListWidgetInformant* infor m_pixmapPos(), m_pixmap(), m_scaledPixmapSize(), + m_columnWidthSum(), m_iconRect(), m_hoverPixmap(), m_textRect(), @@ -290,6 +294,18 @@ KStandardItemListWidget::Layout KStandardItemListWidget::layout() const return m_layout; } +void KStandardItemListWidget::setHighlightEntireRow(bool highlightEntireRow) { + if (m_highlightEntireRow != highlightEntireRow) { + m_highlightEntireRow = highlightEntireRow; + m_dirtyLayout = true; + update(); + } +} + +bool KStandardItemListWidget::highlightEntireRow() const { + return m_highlightEntireRow; +} + void KStandardItemListWidget::setSupportsItemExpanding(bool supportsItemExpanding) { if (m_supportsItemExpanding != supportsItemExpanding) { @@ -491,7 +507,11 @@ QRectF KStandardItemListWidget::selectionRect() const case DetailsLayout: { const int padding = styleOption().padding; QRectF adjustedIconRect = iconRect().adjusted(-padding, -padding, padding, padding); - return adjustedIconRect | m_textRect; + QRectF result = adjustedIconRect | m_textRect; + if (m_highlightEntireRow) { + result.setRight(m_columnWidthSum + leadingPadding()); + } + return result; } default: @@ -708,6 +728,11 @@ void KStandardItemListWidget::columnWidthChanged(const QByteArray& role, m_dirtyLayout = true; } +void KStandardItemListWidget::leadingPaddingChanged(qreal padding) { + Q_UNUSED(padding) + m_dirtyLayout = true; +} + void KStandardItemListWidget::styleOptionChanged(const KItemListStyleOption& current, const KItemListStyleOption& previous) { @@ -900,10 +925,13 @@ void KStandardItemListWidget::triggerCacheRefreshing() m_isHidden = isHidden(); m_customizedFont = customizedFont(styleOption().font); m_customizedFontMetrics = QFontMetrics(m_customizedFont); + m_columnWidthSum = std::accumulate(m_sortedVisibleRoles.begin(), m_sortedVisibleRoles.end(), + qreal(), [this](qreal sum, const auto &role){ return sum + columnWidth(role); }); updateExpansionArea(); updateTextsCache(); updatePixmapCache(); + clearHoverCache(); m_dirtyLayout = false; m_dirtyContent = false; @@ -921,7 +949,8 @@ void KStandardItemListWidget::updateExpansionArea() const qreal inc = (widgetHeight - option.iconSize) / 2; const qreal x = expandedParentsCount * widgetHeight + inc; const qreal y = inc; - m_expansionArea = QRectF(x, y, option.iconSize, option.iconSize); + const qreal xPadding = m_highlightEntireRow ? leadingPadding() : 0; + m_expansionArea = QRectF(xPadding + x, y, option.iconSize, option.iconSize); return; } } @@ -1358,7 +1387,7 @@ void KStandardItemListWidget::updateDetailsLayoutTextCache() if (m_supportsItemExpanding) { firstColumnInc += (m_expansionArea.left() + m_expansionArea.right() + widgetHeight) / 2; } else { - firstColumnInc += option.padding; + firstColumnInc += option.padding + leadingPadding(); } qreal x = firstColumnInc; @@ -1374,7 +1403,7 @@ void KStandardItemListWidget::updateDetailsLayoutTextCache() const bool isTextRole = (role == "text"); if (isTextRole) { - availableTextWidth -= firstColumnInc; + availableTextWidth -= firstColumnInc - leadingPadding(); } if (requiredWidth > availableTextWidth) { @@ -1396,7 +1425,7 @@ void KStandardItemListWidget::updateDetailsLayoutTextCache() // The column after the name should always be aligned on the same x-position independent // from the expansion-level shown in the name column - x -= firstColumnInc; + x -= firstColumnInc - leadingPadding(); } else if (isRoleRightAligned(role)) { textInfo->pos.rx() += roleWidth - requiredWidth - columnWidthInc; } @@ -1408,8 +1437,11 @@ void KStandardItemListWidget::updateAdditionalInfoTextColor() QColor c1; if (m_customTextColor.isValid()) { c1 = m_customTextColor; - } else if (isSelected() && m_layout != DetailsLayout) { - c1 = styleOption().palette.highlightedText().color(); + } else if (isSelected()) { + // The detail text colour needs to match the main text (HighlightedText) for the same level + // of readability. We short circuit early here to avoid interpolating with another colour. + m_additionalInfoTextColor = styleOption().palette.color(QPalette::HighlightedText); + return; } else { c1 = styleOption().palette.text().color(); } @@ -1448,15 +1480,15 @@ void KStandardItemListWidget::drawSiblingsInformation(QPainter* painter) const int x = (m_expansionArea.left() + m_expansionArea.right() - siblingSize) / 2; QRect siblingRect(x, 0, siblingSize, siblingSize); - QStyleOption option; - option.palette.setColor(QPalette::Text, option.palette.color(normalTextColorRole())); bool isItemSibling = true; const QBitArray siblings = siblingsInformation(); + QStyleOption option; + const auto normalColor = option.palette.color(normalTextColorRole()); + const auto highlightColor = option.palette.color(expansionAreaHovered() ? QPalette::Highlight : normalTextColorRole()); for (int i = siblings.count() - 1; i >= 0; --i) { option.rect = siblingRect; option.state = siblings.at(i) ? QStyle::State_Sibling : QStyle::State_None; - if (isItemSibling) { option.state |= QStyle::State_Item; if (m_isExpandable) { @@ -1465,7 +1497,10 @@ void KStandardItemListWidget::drawSiblingsInformation(QPainter* painter) if (data().value("isExpanded").toBool()) { option.state |= QStyle::State_Open; } + option.palette.setColor(QPalette::Text, highlightColor); isItemSibling = false; + } else { + option.palette.setColor(QPalette::Text, normalColor); } style()->drawPrimitive(QStyle::PE_IndicatorBranch, &option, painter); diff --git a/src/kitemviews/kstandarditemlistwidget.h b/src/kitemviews/kstandarditemlistwidget.h index 7d33419bc..3edec0db5 100644 --- a/src/kitemviews/kstandarditemlistwidget.h +++ b/src/kitemviews/kstandarditemlistwidget.h @@ -23,12 +23,13 @@ class DOLPHIN_EXPORT KStandardItemListWidgetInformant : public KItemListWidgetIn public: KStandardItemListWidgetInformant(); ~KStandardItemListWidgetInformant() override; - - void calculateItemSizeHints(QVector<qreal>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const override; + + void calculateItemSizeHints(QVector<std::pair<qreal /* height */, bool /* isElided */>>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const override; qreal preferredRoleColumnWidth(const QByteArray& role, int index, const KItemListView* view) const override; + protected: /** * @return The value of the "text" role. The default implementation returns @@ -59,9 +60,9 @@ protected: */ virtual QFont customizedFontForLinks(const QFont& baseFont) const; - void calculateIconsLayoutItemSizeHints(QVector<qreal>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const; - void calculateCompactLayoutItemSizeHints(QVector<qreal>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const; - void calculateDetailsLayoutItemSizeHints(QVector<qreal>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const; + void calculateIconsLayoutItemSizeHints(QVector<std::pair<qreal, bool>>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const; + void calculateCompactLayoutItemSizeHints(QVector<std::pair<qreal, bool>>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const; + void calculateDetailsLayoutItemSizeHints(QVector<std::pair<qreal, bool>>& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const; friend class KStandardItemListWidget; // Accesses roleText() }; @@ -87,6 +88,9 @@ public: void setLayout(Layout layout); Layout layout() const; + void setHighlightEntireRow(bool highlightEntireRow); + bool highlightEntireRow() const; + void setSupportsItemExpanding(bool supportsItemExpanding); bool supportsItemExpanding() const; @@ -167,6 +171,7 @@ protected: void dataChanged(const QHash<QByteArray, QVariant>& current, const QSet<QByteArray>& roles = QSet<QByteArray>()) override; void visibleRolesChanged(const QList<QByteArray>& current, const QList<QByteArray>& previous) override; void columnWidthChanged(const QByteArray& role, qreal current, qreal previous) override; + void leadingPaddingChanged(qreal width) override; void styleOptionChanged(const KItemListStyleOption& current, const KItemListStyleOption& previous) override; void hoveredChanged(bool hovered) override; void selectedChanged(bool selected) override; @@ -240,6 +245,7 @@ private: QFont m_customizedFont; QFontMetrics m_customizedFontMetrics; bool m_isExpandable; + bool m_highlightEntireRow; bool m_supportsItemExpanding; bool m_dirtyLayout; @@ -251,6 +257,7 @@ private: QPixmap m_pixmap; QSize m_scaledPixmapSize; //Size of the pixmap in device independent pixels + qreal m_columnWidthSum; QRectF m_iconRect; // Cache for KItemListWidget::iconRect() QPixmap m_hoverPixmap; // Cache for modified m_pixmap when hovering the item diff --git a/src/kitemviews/kstandarditemmodel.cpp b/src/kitemviews/kstandarditemmodel.cpp deleted file mode 100644 index 128841ca8..000000000 --- a/src/kitemviews/kstandarditemmodel.cpp +++ /dev/null @@ -1,225 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2012 Peter Penz <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#include "kstandarditemmodel.h" - -#include "kstandarditem.h" - -KStandardItemModel::KStandardItemModel(QObject* parent) : - KItemModelBase(parent), - m_items(), - m_indexesForItems() -{ -} - -KStandardItemModel::~KStandardItemModel() -{ - qDeleteAll(m_items); - m_items.clear(); - m_indexesForItems.clear(); -} - -void KStandardItemModel::insertItem(int index, KStandardItem* item) -{ - if (index < 0 || index > count() || !item) { - delete item; - return; - } - - if (!m_indexesForItems.contains(item)) { - item->m_model = this; - m_items.insert(index, item); - m_indexesForItems.insert(item, index); - - // Inserting an item requires to update the indexes - // afterwards from m_indexesForItems. - for (int i = index + 1; i < m_items.count(); ++i) { - m_indexesForItems.insert(m_items[i], i); - } - - // TODO: no hierarchical items are handled yet - - onItemInserted(index); - Q_EMIT itemsInserted(KItemRangeList() << KItemRange(index, 1)); - } -} - -void KStandardItemModel::changeItem(int index, KStandardItem* item) -{ - if (index < 0 || index >= count() || !item) { - delete item; - return; - } - - item->m_model = this; - - QSet<QByteArray> changedRoles; - - KStandardItem* oldItem = m_items[index]; - const QHash<QByteArray, QVariant> oldData = oldItem->data(); - const QHash<QByteArray, QVariant> newData = item->data(); - - // Determine which roles have been changed - QHashIterator<QByteArray, QVariant> it(oldData); - while (it.hasNext()) { - it.next(); - const QByteArray role = it.key(); - const QVariant oldValue = it.value(); - if (newData.contains(role) && newData.value(role) != oldValue) { - changedRoles.insert(role); - } - } - - m_indexesForItems.remove(oldItem); - delete oldItem; - oldItem = nullptr; - - m_items[index] = item; - m_indexesForItems.insert(item, index); - - onItemChanged(index, changedRoles); - Q_EMIT itemsChanged(KItemRangeList() << KItemRange(index, 1), changedRoles); -} - -void KStandardItemModel::removeItem(int index) -{ - if (index >= 0 && index < count()) { - KStandardItem* item = m_items[index]; - m_indexesForItems.remove(item); - m_items.removeAt(index); - - // Removing an item requires to update the indexes - // afterwards from m_indexesForItems. - for (int i = index; i < m_items.count(); ++i) { - m_indexesForItems.insert(m_items[i], i); - } - - onItemRemoved(index, item); - - item->deleteLater(); - item = nullptr; - - Q_EMIT itemsRemoved(KItemRangeList() << KItemRange(index, 1)); - - // TODO: no hierarchical items are handled yet - } -} - -void KStandardItemModel::clear() -{ - int size = m_items.size(); - m_items.clear(); - m_indexesForItems.clear(); - - Q_EMIT itemsRemoved(KItemRangeList() << KItemRange(0, size)); -} - -KStandardItem* KStandardItemModel::item(int index) const -{ - if (index < 0 || index >= m_items.count()) { - return nullptr; - } - return m_items[index]; -} - -int KStandardItemModel::index(const KStandardItem* item) const -{ - return m_indexesForItems.value(item, -1); -} - -void KStandardItemModel::appendItem(KStandardItem *item) -{ - insertItem(m_items.count(), item); -} - -int KStandardItemModel::count() const -{ - return m_items.count(); -} - -QHash<QByteArray, QVariant> KStandardItemModel::data(int index) const -{ - if (index >= 0 && index < count()) { - const KStandardItem* item = m_items[index]; - if (item) { - return item->data(); - } - } - return QHash<QByteArray, QVariant>(); -} - -bool KStandardItemModel::setData(int index, const QHash<QByteArray, QVariant>& values) -{ - Q_UNUSED(values) - if (index < 0 || index >= count()) { - return false; - } - - return true; -} - -QMimeData* KStandardItemModel::createMimeData(const KItemSet& indexes) const -{ - Q_UNUSED(indexes) - return nullptr; -} - -int KStandardItemModel::indexForKeyboardSearch(const QString& text, int startFromIndex) const -{ - Q_UNUSED(text) - Q_UNUSED(startFromIndex) - return -1; -} - -bool KStandardItemModel::supportsDropping(int index) const -{ - Q_UNUSED(index) - return false; -} - -QString KStandardItemModel::roleDescription(const QByteArray& role) const -{ - Q_UNUSED(role) - return QString(); -} - -QList<QPair<int, QVariant> > KStandardItemModel::groups() const -{ - QList<QPair<int, QVariant> > groups; - - const QByteArray role = sortRole().isEmpty() ? "group" : sortRole(); - bool isFirstGroupValue = true; - QString groupValue; - const int maxIndex = count() - 1; - for (int i = 0; i <= maxIndex; ++i) { - const QString newGroupValue = m_items.at(i)->dataValue(role).toString(); - if (newGroupValue != groupValue || isFirstGroupValue) { - groupValue = newGroupValue; - groups.append(QPair<int, QVariant>(i, newGroupValue)); - isFirstGroupValue = false; - } - } - - return groups; -} - -void KStandardItemModel::onItemInserted(int index) -{ - Q_UNUSED(index) -} - -void KStandardItemModel::onItemChanged(int index, const QSet<QByteArray>& changedRoles) -{ - Q_UNUSED(index) - Q_UNUSED(changedRoles) -} - -void KStandardItemModel::onItemRemoved(int index, KStandardItem* removedItem) -{ - Q_UNUSED(index) - Q_UNUSED(removedItem) -} - diff --git a/src/kitemviews/kstandarditemmodel.h b/src/kitemviews/kstandarditemmodel.h deleted file mode 100644 index d92ec5d46..000000000 --- a/src/kitemviews/kstandarditemmodel.h +++ /dev/null @@ -1,101 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2012 Peter Penz <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#ifndef KSTANDARDITEMMODEL_H -#define KSTANDARDITEMMODEL_H - -#include "dolphin_export.h" -#include "kitemviews/kitemmodelbase.h" - -#include <QHash> -#include <QList> - -class KStandardItem; - -/** - * @brief Model counterpart for KStandardItemListView. - * - * Allows to add items to the model in an easy way by the - * class KStandardItem. - * - * @see KStandardItem - */ -class DOLPHIN_EXPORT KStandardItemModel : public KItemModelBase -{ - Q_OBJECT - -public: - explicit KStandardItemModel(QObject* parent = nullptr); - ~KStandardItemModel() override; - - /** - * Inserts the item \a item at the index \a index. If the index - * is equal to the number of items of the model, the item - * gets appended as last element. KStandardItemModel takes - * the ownership of the item. If the index is invalid, the item - * gets deleted. - */ - void insertItem(int index, KStandardItem* item); - - /** - * Changes the item on the index \a index to \a item. - * KStandardItemModel takes the ownership of the item. The - * old item gets deleted. If the index is invalid, the item - * gets deleted. - */ - void changeItem(int index, KStandardItem* item); - - void removeItem(int index); - KStandardItem* item(int index) const; - int index(const KStandardItem* item) const; - - /** - * Convenience method for insertItem(count(), item). - */ - void appendItem(KStandardItem* item); - - int count() const override; - QHash<QByteArray, QVariant> data(int index) const override; - bool setData(int index, const QHash<QByteArray, QVariant>& values) override; - QMimeData* createMimeData(const KItemSet& indexes) const override; - int indexForKeyboardSearch(const QString& text, int startFromIndex = 0) const override; - bool supportsDropping(int index) const override; - QString roleDescription(const QByteArray& role) const override; - QList<QPair<int, QVariant> > groups() const override; - - virtual void clear(); -protected: - /** - * Is invoked after an item has been inserted and before the signal - * itemsInserted() gets emitted. - */ - virtual void onItemInserted(int index); - - /** - * Is invoked after an item or one of its roles has been changed and - * before the signal itemsChanged() gets emitted. - */ - virtual void onItemChanged(int index, const QSet<QByteArray>& changedRoles); - - /** - * Is invoked after an item has been removed and before the signal - * itemsRemoved() gets emitted. The item \a removedItem has already - * been removed from the model and will get deleted after the - * execution of onItemRemoved(). - */ - virtual void onItemRemoved(int index, KStandardItem* removedItem); - -private: - QList<KStandardItem*> m_items; - QHash<const KStandardItem*, int> m_indexesForItems; - - friend class KStandardItem; - friend class KStandardItemModelTest; // For unit testing -}; - -#endif - - diff --git a/src/kitemviews/private/kbaloorolesprovider.cpp b/src/kitemviews/private/kbaloorolesprovider.cpp index 5c87de712..4c231e2ff 100644 --- a/src/kitemviews/private/kbaloorolesprovider.cpp +++ b/src/kitemviews/private/kbaloorolesprovider.cpp @@ -13,6 +13,7 @@ #include <QCollator> #include <QDebug> +#include <QSize> #include <QTime> namespace { @@ -117,6 +118,18 @@ QHash<QByteArray, QVariant> KBalooRolesProvider::roleValues(const Baloo::File& f rangeBegin = rangeEnd; } + if (roles.contains("dimensions")) { + bool widthOk = false; + bool heightOk = false; + + const int width = propMap.value(KFileMetaData::Property::Width).toInt(&widthOk); + const int height = propMap.value(KFileMetaData::Property::Height).toInt(&heightOk); + + if (widthOk && heightOk && width >= 0 && height >= 0) { + values.insert("dimensions", QSize(width, height)); + } + } + KFileMetaData::UserMetaData::Attributes attributes; if (roles.contains("tags")) { attributes |= KFileMetaData::UserMetaData::Tags; @@ -160,6 +173,7 @@ KBalooRolesProvider::KBalooRolesProvider() for (const auto& role : propertyRoleMap()) { m_roles.insert(role); } + m_roles.insert("dimensions"); // Display roles provided by UserMetaData m_roles.insert(QByteArrayLiteral("tags")); diff --git a/src/kitemviews/private/kitemlistheaderwidget.cpp b/src/kitemviews/private/kitemlistheaderwidget.cpp index e5cbc602f..5fb929e52 100644 --- a/src/kitemviews/private/kitemlistheaderwidget.cpp +++ b/src/kitemviews/private/kitemlistheaderwidget.cpp @@ -18,6 +18,7 @@ KItemListHeaderWidget::KItemListHeaderWidget(QGraphicsWidget* parent) : m_automaticColumnResizing(true), m_model(nullptr), m_offset(0), + m_leadingPadding(0), m_columns(), m_columnWidths(), m_preferredColumnWidths(), @@ -134,6 +135,20 @@ qreal KItemListHeaderWidget::offset() const return m_offset; } +void KItemListHeaderWidget::setLeadingPadding(qreal width) +{ + if (m_leadingPadding != width) { + m_leadingPadding = width; + leadingPaddingChanged(width); + update(); + } +} + +qreal KItemListHeaderWidget::leadingPadding() const +{ + return m_leadingPadding; +} + qreal KItemListHeaderWidget::minimumColumnWidth() const { QFontMetricsF fontMetrics(font()); @@ -153,7 +168,7 @@ void KItemListHeaderWidget::paint(QPainter* painter, const QStyleOptionGraphicsI painter->setFont(font()); painter->setPen(palette().text().color()); - qreal x = -m_offset; + qreal x = -m_offset + m_leadingPadding; int orderIndex = 0; for (const QByteArray& role : qAsConst(m_columns)) { const qreal roleWidth = m_columnWidths.value(role); @@ -172,10 +187,14 @@ void KItemListHeaderWidget::paint(QPainter* painter, const QStyleOptionGraphicsI void KItemListHeaderWidget::mousePressEvent(QGraphicsSceneMouseEvent* event) { if (event->button() & Qt::LeftButton) { - updatePressedRoleIndex(event->pos()); m_pressedMousePos = event->pos(); - m_roleOperation = isAboveRoleGrip(m_pressedMousePos, m_pressedRoleIndex) ? - ResizeRoleOperation : NoRoleOperation; + if (isAbovePaddingGrip(m_pressedMousePos, PaddingGrip::Leading)) { + m_roleOperation = ResizeLeadingColumnOperation; + } else { + updatePressedRoleIndex(event->pos()); + m_roleOperation = isAboveRoleGrip(m_pressedMousePos, m_pressedRoleIndex) ? + ResizeRoleOperation : NoRoleOperation; + } event->accept(); } else { event->ignore(); @@ -251,7 +270,7 @@ void KItemListHeaderWidget::mouseMoveEvent(QGraphicsSceneMouseEvent* event) case NoRoleOperation: if ((event->pos() - m_pressedMousePos).manhattanLength() >= QApplication::startDragDistance()) { // A role gets dragged by the user. Create a pixmap of the role that will get - // synchronized on each furter mouse-move-event with the mouse-position. + // synchronized on each further mouse-move-event with the mouse-position. m_roleOperation = MoveRoleOperation; const int roleIndex = roleIndexAt(m_pressedMousePos); m_movingRole.index = roleIndex; @@ -263,7 +282,7 @@ void KItemListHeaderWidget::mouseMoveEvent(QGraphicsSceneMouseEvent* event) } else { m_movingRole.pixmap = createRolePixmap(roleIndex); - qreal roleX = -m_offset; + qreal roleX = -m_offset + m_leadingPadding; for (int i = 0; i < roleIndex; ++i) { const QByteArray role = m_columns[i]; roleX += m_columnWidths.value(role); @@ -291,6 +310,20 @@ void KItemListHeaderWidget::mouseMoveEvent(QGraphicsSceneMouseEvent* event) break; } + case ResizeLeadingColumnOperation: { + qreal currentWidth = m_leadingPadding; + currentWidth += event->pos().x() - event->lastPos().x(); + currentWidth = qMax(0.0, currentWidth); + + m_leadingPadding = currentWidth; + + update(); + + Q_EMIT leadingPaddingChanged(currentWidth); + + break; + } + case MoveRoleOperation: { // TODO: It should be configurable whether moving the first role is allowed. // In the context of Dolphin this is not required, however this should be @@ -355,7 +388,9 @@ void KItemListHeaderWidget::hoverMoveEvent(QGraphicsSceneHoverEvent* event) const QPointF& pos = event->pos(); updateHoveredRoleIndex(pos); - if (m_hoveredRoleIndex >= 0 && isAboveRoleGrip(pos, m_hoveredRoleIndex)) { + if ((m_hoveredRoleIndex >= 0 && isAboveRoleGrip(pos, m_hoveredRoleIndex)) || + isAbovePaddingGrip(pos, PaddingGrip::Leading) || + isAbovePaddingGrip(pos, PaddingGrip::Trailing)) { setCursor(Qt::SplitHCursor); } else { unsetCursor(); @@ -385,6 +420,12 @@ void KItemListHeaderWidget::paintRole(QPainter* painter, // The following code is based on the code from QHeaderView::paintSection(). // SPDX-FileCopyrightText: 2011 Nokia Corporation and/or its subsidiary(-ies). QStyleOptionHeader option; + option.direction = widget->layoutDirection(); + option.textAlignment = + widget->layoutDirection() == Qt::LeftToRight + ? Qt::AlignLeft + : Qt::AlignRight; + option.section = orderIndex; option.state = QStyle::State_None | QStyle::State_Raised | QStyle::State_Horizontal; if (isEnabled()) { @@ -404,19 +445,39 @@ void KItemListHeaderWidget::paintRole(QPainter* painter, QStyleOptionHeader::SortDown : QStyleOptionHeader::SortUp; } option.rect = rect.toRect(); + option.orientation = Qt::Horizontal; + option.selectedPosition = QStyleOptionHeader::NotAdjacent; + option.text = m_model->roleDescription(role); - bool paintBackgroundForEmptyArea = false; + // First we paint any potential empty (padding) space on left and/or right of this role's column. + const auto paintPadding = [&](int section, const QRectF &rect, const QStyleOptionHeader::SectionPosition &pos){ + QStyleOptionHeader padding; + padding.state = QStyle::State_None | QStyle::State_Raised | QStyle::State_Horizontal; + padding.section = section; + padding.sortIndicator = QStyleOptionHeader::None; + padding.rect = rect.toRect(); + padding.position = pos; + padding.text = QString(); + style()->drawControl(QStyle::CE_Header, &padding, painter, widget); + }; if (m_columns.count() == 1) { - option.position = QStyleOptionHeader::OnlyOneSection; + option.position = QStyleOptionHeader::Middle; + paintPadding(0, QRectF(0.0, 0.0, rect.left(), rect.height()), QStyleOptionHeader::Beginning); + paintPadding(1, QRectF(rect.left(), 0.0, size().width() - rect.left(), rect.height()), QStyleOptionHeader::End); } else if (orderIndex == 0) { - option.position = QStyleOptionHeader::Beginning; + // Paint the header for the first column; check if there is some empty space to the left which needs to be filled. + if (rect.left() > 0) { + option.position = QStyleOptionHeader::Middle; + paintPadding(0,QRectF(0.0, 0.0, rect.left(), rect.height()), QStyleOptionHeader::Beginning); + } else { + option.position = QStyleOptionHeader::Beginning; + } } else if (orderIndex == m_columns.count() - 1) { - // We are just painting the header for the last column. Check if there - // is some empty space to the right which needs to be filled. + // Paint the header for the last column; check if there is some empty space to the right which needs to be filled. if (rect.right() < size().width()) { option.position = QStyleOptionHeader::Middle; - paintBackgroundForEmptyArea = true; + paintPadding(m_columns.count(), QRectF(rect.left(), 0.0, size().width() - rect.left(), rect.height()), QStyleOptionHeader::End); } else { option.position = QStyleOptionHeader::End; } @@ -424,25 +485,7 @@ void KItemListHeaderWidget::paintRole(QPainter* painter, option.position = QStyleOptionHeader::Middle; } - option.orientation = Qt::Horizontal; - option.selectedPosition = QStyleOptionHeader::NotAdjacent; - option.text = m_model->roleDescription(role); - style()->drawControl(QStyle::CE_Header, &option, painter, widget); - - if (paintBackgroundForEmptyArea) { - option.state = QStyle::State_None | QStyle::State_Raised | QStyle::State_Horizontal; - option.section = m_columns.count(); - option.sortIndicator = QStyleOptionHeader::None; - - qreal backgroundRectX = rect.x() + rect.width(); - QRectF backgroundRect(backgroundRectX, 0.0, size().width() - backgroundRectX, rect.height()); - option.rect = backgroundRect.toRect(); - option.position = QStyleOptionHeader::End; - option.text = QString(); - - style()->drawControl(QStyle::CE_Header, &option, painter, widget); - } } void KItemListHeaderWidget::updatePressedRoleIndex(const QPointF& pos) @@ -467,7 +510,7 @@ int KItemListHeaderWidget::roleIndexAt(const QPointF& pos) const { int index = -1; - qreal x = -m_offset; + qreal x = -m_offset + m_leadingPadding; for (const QByteArray& role : qAsConst(m_columns)) { ++index; x += m_columnWidths.value(role); @@ -481,7 +524,7 @@ int KItemListHeaderWidget::roleIndexAt(const QPointF& pos) const bool KItemListHeaderWidget::isAboveRoleGrip(const QPointF& pos, int roleIndex) const { - qreal x = -m_offset; + qreal x = -m_offset + m_leadingPadding; for (int i = 0; i <= roleIndex; ++i) { const QByteArray role = m_columns[i]; x += m_columnWidths.value(role); @@ -491,6 +534,27 @@ bool KItemListHeaderWidget::isAboveRoleGrip(const QPointF& pos, int roleIndex) c return pos.x() >= (x - grip) && pos.x() <= x; } +bool KItemListHeaderWidget::isAbovePaddingGrip(const QPointF& pos, PaddingGrip paddingGrip) const +{ + const qreal lx = -m_offset + m_leadingPadding; + const int grip = style()->pixelMetric(QStyle::PM_HeaderGripMargin); + + switch (paddingGrip) { + case Leading: + return pos.x() >= (lx - grip) && pos.x() <= lx; + case Trailing: + { + qreal rx = lx; + for (const QByteArray& role : qAsConst(m_columns)) { + rx += m_columnWidths.value(role); + } + return pos.x() >= (rx - grip) && pos.x() <= rx; + } + default: + return false; + } +} + QPixmap KItemListHeaderWidget::createRolePixmap(int roleIndex) const { const QByteArray role = m_columns[roleIndex]; @@ -522,7 +586,7 @@ int KItemListHeaderWidget::targetOfMovingRole() const const int movingRight = movingLeft + movingWidth - 1; int targetIndex = 0; - qreal targetLeft = -m_offset; + qreal targetLeft = -m_offset + m_leadingPadding; while (targetIndex < m_columns.count()) { const QByteArray role = m_columns[targetIndex]; const qreal targetWidth = m_columnWidths.value(role); @@ -548,7 +612,7 @@ int KItemListHeaderWidget::targetOfMovingRole() const qreal KItemListHeaderWidget::roleXPosition(const QByteArray& role) const { - qreal x = -m_offset; + qreal x = -m_offset + m_leadingPadding; for (const QByteArray& visibleRole : qAsConst(m_columns)) { if (visibleRole == role) { return x; diff --git a/src/kitemviews/private/kitemlistheaderwidget.h b/src/kitemviews/private/kitemlistheaderwidget.h index 44adc23c5..58f0dc98e 100644 --- a/src/kitemviews/private/kitemlistheaderwidget.h +++ b/src/kitemviews/private/kitemlistheaderwidget.h @@ -50,6 +50,9 @@ public: void setOffset(qreal offset); qreal offset() const; + void setLeadingPadding(qreal width); + qreal leadingPadding() const; + qreal minimumColumnWidth() const; void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget = nullptr) override; @@ -63,6 +66,8 @@ Q_SIGNALS: qreal currentWidth, qreal previousWidth); + void leadingPaddingChanged(qreal width); + /** * Is emitted if the user has released the mouse button after adjusting the * width of a visible role. @@ -105,6 +110,13 @@ private Q_SLOTS: void slotSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous); private: + + enum PaddingGrip + { + Leading, + Trailing, + }; + void paintRole(QPainter* painter, const QByteArray& role, const QRectF& rect, @@ -115,6 +127,7 @@ private: void updateHoveredRoleIndex(const QPointF& pos); int roleIndexAt(const QPointF& pos) const; bool isAboveRoleGrip(const QPointF& pos, int roleIndex) const; + bool isAbovePaddingGrip(const QPointF& pos, PaddingGrip paddingGrip) const; /** * Creates a pixmap of the role with the index \a roleIndex that is shown @@ -138,12 +151,14 @@ private: { NoRoleOperation, ResizeRoleOperation, + ResizeLeadingColumnOperation, MoveRoleOperation }; bool m_automaticColumnResizing; KItemModelBase* m_model; qreal m_offset; + qreal m_leadingPadding; QList<QByteArray> m_columns; QHash<QByteArray, qreal> m_columnWidths; QHash<QByteArray, qreal> m_preferredColumnWidths; diff --git a/src/kitemviews/private/kitemlistkeyboardsearchmanager.cpp b/src/kitemviews/private/kitemlistkeyboardsearchmanager.cpp index 57a954adf..0e6280ede 100644 --- a/src/kitemviews/private/kitemlistkeyboardsearchmanager.cpp +++ b/src/kitemviews/private/kitemlistkeyboardsearchmanager.cpp @@ -55,7 +55,7 @@ void KItemListKeyboardSearchManager::addKeys(const QString& keys) const bool searchFromNextItem = (!m_isSearchRestarted && newSearch) || sameKey; // to remember not to searchFromNextItem if selection was deselected - // loosing keyboard search context basically + // losing keyboard search context basically m_isSearchRestarted = false; Q_EMIT changeCurrentItem(sameKey ? firstKey : m_searchedString, searchFromNextItem); diff --git a/src/kitemviews/private/kitemlistsizehintresolver.cpp b/src/kitemviews/private/kitemlistsizehintresolver.cpp index 7deb5c621..0c2dd0b80 100644 --- a/src/kitemviews/private/kitemlistsizehintresolver.cpp +++ b/src/kitemviews/private/kitemlistsizehintresolver.cpp @@ -29,7 +29,12 @@ QSizeF KItemListSizeHintResolver::minSizeHint() QSizeF KItemListSizeHintResolver::sizeHint(int index) { updateCache(); - return QSizeF(m_logicalWidthHint, m_logicalHeightHintCache.at(index)); + return QSizeF(m_logicalWidthHint, m_logicalHeightHintCache.at(index).first); +} + +bool KItemListSizeHintResolver::isElided(int index) +{ + return m_logicalHeightHintCache.at(index).second; } void KItemListSizeHintResolver::itemsInserted(const KItemRangeList& itemRanges) @@ -44,7 +49,7 @@ void KItemListSizeHintResolver::itemsInserted(const KItemRangeList& itemRanges) // We build the new list from the end to the beginning to mimize the // number of moves. - m_logicalHeightHintCache.insert(m_logicalHeightHintCache.end(), insertedCount, 0.0); + m_logicalHeightHintCache.insert(m_logicalHeightHintCache.end(), insertedCount, std::make_pair(0.0, false)); int sourceIndex = currentCount - 1; int targetIndex = m_logicalHeightHintCache.count() - 1; @@ -63,7 +68,7 @@ void KItemListSizeHintResolver::itemsInserted(const KItemRangeList& itemRanges) // Then: insert QSizeF() for the items which are inserted into 'range'. while (targetIndex >= itemsToInsertBeforeCurrentRange + range.index) { - m_logicalHeightHintCache[targetIndex] = 0.0; + m_logicalHeightHintCache[targetIndex] = std::make_pair(0.0, false); --targetIndex; } } @@ -75,14 +80,14 @@ void KItemListSizeHintResolver::itemsInserted(const KItemRangeList& itemRanges) void KItemListSizeHintResolver::itemsRemoved(const KItemRangeList& itemRanges) { - const QVector<qreal>::iterator begin = m_logicalHeightHintCache.begin(); - const QVector<qreal>::iterator end = m_logicalHeightHintCache.end(); + const QVector<std::pair<qreal, bool>>::iterator begin = m_logicalHeightHintCache.begin(); + const QVector<std::pair<qreal, bool>>::iterator end = m_logicalHeightHintCache.end(); KItemRangeList::const_iterator rangeIt = itemRanges.constBegin(); const KItemRangeList::const_iterator rangeEnd = itemRanges.constEnd(); - QVector<qreal>::iterator destIt = begin + rangeIt->index; - QVector<qreal>::iterator srcIt = destIt + rangeIt->count; + QVector<std::pair<qreal, bool>>::iterator destIt = begin + rangeIt->index; + QVector<std::pair<qreal, bool>>::iterator srcIt = destIt + rangeIt->count; ++rangeIt; @@ -109,7 +114,7 @@ void KItemListSizeHintResolver::itemsRemoved(const KItemRangeList& itemRanges) void KItemListSizeHintResolver::itemsMoved(const KItemRange& range, const QList<int>& movedToIndexes) { - QVector<qreal> newLogicalHeightHintCache(m_logicalHeightHintCache); + QVector<std::pair<qreal, bool>> newLogicalHeightHintCache(m_logicalHeightHintCache); const int movedRangeEnd = range.index + range.count; for (int i = range.index; i < movedRangeEnd; ++i) { @@ -124,7 +129,7 @@ void KItemListSizeHintResolver::itemsChanged(int index, int count, const QSet<QB { Q_UNUSED(roles) while (count) { - m_logicalHeightHintCache[index] = 0.0; + m_logicalHeightHintCache[index] = std::make_pair(0.0, false); ++index; --count; } @@ -134,7 +139,7 @@ void KItemListSizeHintResolver::itemsChanged(int index, int count, const QSet<QB void KItemListSizeHintResolver::clearCache() { - m_logicalHeightHintCache.fill(0.0); + m_logicalHeightHintCache.fill(std::make_pair(0.0, false)); m_needsResolving = true; } diff --git a/src/kitemviews/private/kitemlistsizehintresolver.h b/src/kitemviews/private/kitemlistsizehintresolver.h index 9a0ab1f5b..a6cc56614 100644 --- a/src/kitemviews/private/kitemlistsizehintresolver.h +++ b/src/kitemviews/private/kitemlistsizehintresolver.h @@ -25,6 +25,7 @@ public: virtual ~KItemListSizeHintResolver(); QSizeF minSizeHint(); QSizeF sizeHint(int index); + bool isElided(int index); void itemsInserted(const KItemRangeList& itemRanges); void itemsRemoved(const KItemRangeList& itemRanges); @@ -36,7 +37,7 @@ public: private: const KItemListView* m_itemListView; - mutable QVector<qreal> m_logicalHeightHintCache; + mutable QVector<std::pair<qreal /* height */, bool /* isElided */>> m_logicalHeightHintCache; mutable qreal m_logicalWidthHint; mutable qreal m_minHeightHint; bool m_needsResolving; diff --git a/src/kitemviews/private/kitemlistviewlayouter.cpp b/src/kitemviews/private/kitemlistviewlayouter.cpp index 6de83ca87..4c22b4dbc 100644 --- a/src/kitemviews/private/kitemlistviewlayouter.cpp +++ b/src/kitemviews/private/kitemlistviewlayouter.cpp @@ -9,6 +9,9 @@ #include "kitemlistsizehintresolver.h" #include "kitemviews/kitemmodelbase.h" +#include <QGuiApplication> +#include <QScopeGuard> + // #define KITEMLISTVIEWLAYOUTER_DEBUG KItemListViewLayouter::KItemListViewLayouter(KItemListSizeHintResolver* sizeHintResolver, QObject* parent) : @@ -343,170 +346,177 @@ void KItemListViewLayouter::markAsDirty() void KItemListViewLayouter::doLayout() { - if (m_dirty) { + // we always want to update visible indexes after performing a layout + auto qsg = qScopeGuard([this] { updateVisibleIndexes(); }); + + if (!m_dirty) { + return; + } + #ifdef KITEMLISTVIEWLAYOUTER_DEBUG - QElapsedTimer timer; - timer.start(); + QElapsedTimer timer; + timer.start(); #endif - m_visibleIndexesDirty = true; + m_visibleIndexesDirty = true; - QSizeF itemSize = m_itemSize; - QSizeF itemMargin = m_itemMargin; - QSizeF size = m_size; + QSizeF itemSize = m_itemSize; + QSizeF itemMargin = m_itemMargin; + QSizeF size = m_size; - const bool grouped = createGroupHeaders(); + const bool grouped = createGroupHeaders(); - const bool horizontalScrolling = (m_scrollOrientation == Qt::Horizontal); - if (horizontalScrolling) { - // Flip everything so that the layout logically can work like having - // a vertical scrolling - itemSize.transpose(); - itemMargin.transpose(); - size.transpose(); + const bool horizontalScrolling = (m_scrollOrientation == Qt::Horizontal); + if (horizontalScrolling) { + // Flip everything so that the layout logically can work like having + // a vertical scrolling + itemSize.transpose(); + itemMargin.transpose(); + size.transpose(); - if (grouped) { - // In the horizontal scrolling case all groups are aligned - // at the top, which decreases the available height. For the - // flipped data this means that the width must be decreased. - size.rwidth() -= m_groupHeaderHeight; - } + if (grouped) { + // In the horizontal scrolling case all groups are aligned + // at the top, which decreases the available height. For the + // flipped data this means that the width must be decreased. + size.rwidth() -= m_groupHeaderHeight; } + } - m_columnWidth = itemSize.width() + itemMargin.width(); - const qreal widthForColumns = size.width() - itemMargin.width(); - m_columnCount = qMax(1, int(widthForColumns / m_columnWidth)); - m_xPosInc = itemMargin.width(); + m_columnWidth = itemSize.width() + itemMargin.width(); + const qreal widthForColumns = size.width() - itemMargin.width(); + m_columnCount = qMax(1, int(widthForColumns / m_columnWidth)); + m_xPosInc = itemMargin.width(); - const int itemCount = m_model->count(); - if (itemCount > m_columnCount && m_columnWidth >= 32) { - // Apply the unused width equally to each column - const qreal unusedWidth = widthForColumns - m_columnCount * m_columnWidth; - if (unusedWidth > 0) { - const qreal columnInc = unusedWidth / (m_columnCount + 1); - m_columnWidth += columnInc; - m_xPosInc += columnInc; - } + const int itemCount = m_model->count(); + if (itemCount > m_columnCount && m_columnWidth >= 32) { + // Apply the unused width equally to each column + const qreal unusedWidth = widthForColumns - m_columnCount * m_columnWidth; + if (unusedWidth > 0) { + const qreal columnInc = unusedWidth / (m_columnCount + 1); + m_columnWidth += columnInc; + m_xPosInc += columnInc; } + } - m_itemInfos.resize(itemCount); + m_itemInfos.resize(itemCount); - // Calculate the offset of each column, i.e., the x-coordinate where the column starts. - m_columnOffsets.resize(m_columnCount); - qreal currentOffset = m_xPosInc; + // Calculate the offset of each column, i.e., the x-coordinate where the column starts. + m_columnOffsets.resize(m_columnCount); + qreal currentOffset = QGuiApplication::isRightToLeft() ? widthForColumns : m_xPosInc; - if (grouped && horizontalScrolling) { - // All group headers will always be aligned on the top and not - // flipped like the other properties. - currentOffset += m_groupHeaderHeight; - } + if (grouped && horizontalScrolling) { + // All group headers will always be aligned on the top and not + // flipped like the other properties. + currentOffset += m_groupHeaderHeight; + } - for (int column = 0; column < m_columnCount; ++column) { - m_columnOffsets[column] = currentOffset; - currentOffset += m_columnWidth; - } + if (QGuiApplication::isLeftToRight()) for (int column = 0; column < m_columnCount; ++column) { + m_columnOffsets[column] = currentOffset; + currentOffset += m_columnWidth; + } + else for (int column = 0; column < m_columnCount; ++column) { + m_columnOffsets[column] = currentOffset - m_columnWidth; + currentOffset -= m_columnWidth; + } - // Prepare the QVector which stores the y-coordinate for each new row. - int numberOfRows = (itemCount + m_columnCount - 1) / m_columnCount; - if (grouped && m_columnCount > 1) { - // In the worst case, a new row will be started for every group. - // We could calculate the exact number of rows now to prevent that we reserve - // too much memory, but the code required to do that might need much more - // memory than it would save in the average case. - numberOfRows += m_groupItemIndexes.count(); - } - m_rowOffsets.resize(numberOfRows); + // Prepare the QVector which stores the y-coordinate for each new row. + int numberOfRows = (itemCount + m_columnCount - 1) / m_columnCount; + if (grouped && m_columnCount > 1) { + // In the worst case, a new row will be started for every group. + // We could calculate the exact number of rows now to prevent that we reserve + // too much memory, but the code required to do that might need much more + // memory than it would save in the average case. + numberOfRows += m_groupItemIndexes.count(); + } + m_rowOffsets.resize(numberOfRows); - qreal y = m_headerHeight + itemMargin.height(); - int row = 0; + qreal y = m_headerHeight + itemMargin.height(); + int row = 0; - int index = 0; - while (index < itemCount) { - qreal maxItemHeight = itemSize.height(); + int index = 0; + while (index < itemCount) { + qreal maxItemHeight = itemSize.height(); - if (grouped) { - if (m_groupItemIndexes.contains(index)) { - // The item is the first item of a group. - // Increase the y-position to provide space - // for the group header. - if (index > 0) { - // Only add a margin if there has been added another - // group already before - y += m_groupHeaderMargin; - } else if (!horizontalScrolling) { - // The first group header should be aligned on top - y -= itemMargin.height(); - } + if (grouped) { + if (m_groupItemIndexes.contains(index)) { + // The item is the first item of a group. + // Increase the y-position to provide space + // for the group header. + if (index > 0) { + // Only add a margin if there has been added another + // group already before + y += m_groupHeaderMargin; + } else if (!horizontalScrolling) { + // The first group header should be aligned on top + y -= itemMargin.height(); + } - if (!horizontalScrolling) { - y += m_groupHeaderHeight; - } + if (!horizontalScrolling) { + y += m_groupHeaderHeight; } } + } - m_rowOffsets[row] = y; + m_rowOffsets[row] = y; - int column = 0; - while (index < itemCount && column < m_columnCount) { - qreal requiredItemHeight = itemSize.height(); - const QSizeF sizeHint = m_sizeHintResolver->sizeHint(index); - const qreal sizeHintHeight = sizeHint.height(); - if (sizeHintHeight > requiredItemHeight) { - requiredItemHeight = sizeHintHeight; - } + int column = 0; + while (index < itemCount && column < m_columnCount) { + qreal requiredItemHeight = itemSize.height(); + const QSizeF sizeHint = m_sizeHintResolver->sizeHint(index); + const qreal sizeHintHeight = sizeHint.height(); + if (sizeHintHeight > requiredItemHeight) { + requiredItemHeight = sizeHintHeight; + } - ItemInfo& itemInfo = m_itemInfos[index]; - itemInfo.column = column; - itemInfo.row = row; + ItemInfo& itemInfo = m_itemInfos[index]; + itemInfo.column = column; + itemInfo.row = row; - if (grouped && horizontalScrolling) { - // When grouping is enabled in the horizontal mode, the header alignment - // looks like this: - // Header-1 Header-2 Header-3 - // Item 1 Item 4 Item 7 - // Item 2 Item 5 Item 8 - // Item 3 Item 6 Item 9 - // In this case 'requiredItemHeight' represents the column-width. We don't - // check the content of the header in the layouter to determine the required - // width, hence assure that at least a minimal width of 15 characters is given - // (in average a character requires the halve width of the font height). - // - // TODO: Let the group headers provide a minimum width and respect this width here - const qreal headerWidth = minimumGroupHeaderWidth(); - if (requiredItemHeight < headerWidth) { - requiredItemHeight = headerWidth; - } + if (grouped && horizontalScrolling) { + // When grouping is enabled in the horizontal mode, the header alignment + // looks like this: + // Header-1 Header-2 Header-3 + // Item 1 Item 4 Item 7 + // Item 2 Item 5 Item 8 + // Item 3 Item 6 Item 9 + // In this case 'requiredItemHeight' represents the column-width. We don't + // check the content of the header in the layouter to determine the required + // width, hence assure that at least a minimal width of 15 characters is given + // (in average a character requires the halve width of the font height). + // + // TODO: Let the group headers provide a minimum width and respect this width here + const qreal headerWidth = minimumGroupHeaderWidth(); + if (requiredItemHeight < headerWidth) { + requiredItemHeight = headerWidth; } + } - maxItemHeight = qMax(maxItemHeight, requiredItemHeight); - ++index; - ++column; + maxItemHeight = qMax(maxItemHeight, requiredItemHeight); + ++index; + ++column; - if (grouped && m_groupItemIndexes.contains(index)) { - // The item represents the first index of a group - // and must aligned in the first column - break; - } + if (grouped && m_groupItemIndexes.contains(index)) { + // The item represents the first index of a group + // and must aligned in the first column + break; } - - y += maxItemHeight + itemMargin.height(); - ++row; } - if (itemCount > 0) { - m_maximumScrollOffset = y; - m_maximumItemOffset = m_columnCount * m_columnWidth; - } else { - m_maximumScrollOffset = 0; - m_maximumItemOffset = 0; - } + y += maxItemHeight + itemMargin.height(); + ++row; + } -#ifdef KITEMLISTVIEWLAYOUTER_DEBUG - qCDebug(DolphinDebug) << "[TIME] doLayout() for " << m_model->count() << "items:" << timer.elapsed(); -#endif - m_dirty = false; + if (itemCount > 0) { + m_maximumScrollOffset = y; + m_maximumItemOffset = m_columnCount * m_columnWidth; + } else { + m_maximumScrollOffset = 0; + m_maximumItemOffset = 0; } - updateVisibleIndexes(); +#ifdef KITEMLISTVIEWLAYOUTER_DEBUG + qCDebug(DolphinDebug) << "[TIME] doLayout() for " << m_model->count() << "items:" << timer.elapsed(); +#endif + m_dirty = false; } void KItemListViewLayouter::updateVisibleIndexes() diff --git a/src/main.cpp b/src/main.cpp index 779690c1c..7a2d42ea5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -44,10 +44,16 @@ int main(int argc, char **argv) // Prohibit using sudo or kdesu (but allow using the root user directly) if (getuid() == 0) { if (!qEnvironmentVariableIsEmpty("SUDO_USER")) { - std::cout << "Executing Dolphin with sudo is not possible due to unfixable security vulnerabilities." << std::endl; + std::cout << "Running Dolphin with sudo can cause bugs and expose you to security vulnerabilities. " + "Instead use Dolphin normally and you will be prompted for elevated privileges when " + "performing file operations that require them." + << std::endl; return EXIT_FAILURE; } else if (!qEnvironmentVariableIsEmpty("KDESU_USER")) { - std::cout << "Executing Dolphin with kdesu is not possible due to unfixable security vulnerabilities." << std::endl; + std::cout << "Running Dolphin with kdesu can cause bugs and expose you to security vulnerabilities. " + "Instead use Dolphin normally and you will be prompted for elevated privileges when " + "performing file operations that require them." + << std::endl; return EXIT_FAILURE; } } @@ -148,7 +154,11 @@ int main(int argc, char **argv) QObject::connect(&app, &QGuiApplication::commitDataRequest, disableSessionManagement); QObject::connect(&app, &QGuiApplication::saveStateRequest, disableSessionManagement); +#ifdef FLATPAK + KDBusService dolphinDBusService(KDBusService::NoExitOnFailure); +#else KDBusService dolphinDBusService; +#endif DBusInterface interface; interface.setAsDaemon(); return app.exec(); diff --git a/src/org.kde.dolphin.appdata.xml b/src/org.kde.dolphin.appdata.xml index 81d1df861..cec2c982b 100644 --- a/src/org.kde.dolphin.appdata.xml +++ b/src/org.kde.dolphin.appdata.xml @@ -419,4 +419,5 @@ <release version="21.12.0" date="2021-12-09"/> <release version="21.08.3" date="2021-11-04"/> </releases> + <content_rating type="oars-1.1"/> </component> diff --git a/src/org.kde.dolphin.desktop b/src/org.kde.dolphin.desktop index 1d2f43f64..a967857c7 100755 --- a/src/org.kde.dolphin.desktop +++ b/src/org.kde.dolphin.desktop @@ -115,6 +115,30 @@ GenericName[zh_TW]=檔案管理員 Terminal=false MimeType=inode/directory; InitialPreference=10 +Keywords=files;file management;file browsing;samba;network shares;Explorer;Finder; +Keywords[ar]=ملف;ملفات;متصفح;سامبا;مشاركة;إدارة;إدارة ملفات;شبكة; +Keywords[az]=fayllar;fayl idarəetməsi;fayl bələdçisi;smaba:şəbəkədə paylaşımlar;Araşdırmaq;Tapmaq; +Keywords[ca]=fitxers;gestió de fitxers;explorar fitxers;samba;comparticions de xarxa;explorador;cercador; +Keywords[ca@valencia]=fitxers;gestió de fitxers;explorar fitxers;samba;comparticions de xarxa;explorador;cercador; +Keywords[en_GB]=files;file management;file browsing;samba;network shares;Explorer;Finder; +Keywords[es]=archivos;gestión de archivos;administración de archivos;exploración de archivos;samba;recursos compartidos de red;gestor de archivos;administrador de archivos;explorador;buscador; +Keywords[fr]=fichiers ; gestion de fichiers ; navigation parmi les fichiers ; samba ; partages sur réseau ; explorateur ; chercheur ; +Keywords[hu]=fájlok;fájlkezelés;fájlböngészés;samba;hálózati megosztások;Explorer;Finder; +Keywords[ia]=files; gestion de file; navigation de file;samba; partes de rete;Explorator;Trovator; +Keywords[is]=skrár;skráastjórn;skráaskoðun;samba;netsameignir;Explorer;Finder; +Keywords[it]=file;gestione dei file;navigazione dei file;samba;condivisioni di rete;Explorer;Finder; +Keywords[ko]=files;file management;file browsing;samba;network shares;Explorer;Finder;파일;파일 관리;파일 관리자;탐색;탐색기;삼바;네트워크 공유; +Keywords[nl]=bestanden;bestandsbeheer;bladeren in bestanden;samba;netwerk-shares;verkenner;zoeksysteem; +Keywords[nn]=filer;filhandsaming;filutforsking;samba;nettverksressursar;Explorer;Finder; +Keywords[pl]=pliki;zarządzenie plikami;przeglądanie plików;samba;udziały sieciowe;Przeglądarka;Finder; +Keywords[pt_BR]=arquivo;gerenciamento de arquivos;navegação de arquivos;samba;compartilhamentos de rede;explorador;localizador; +Keywords[ru]=files;file management;file browsing;samba;network shares;Explorer;Finder;файлы,управление файлами,просмотр файлов,сетевые папки +Keywords[sl]=datoteke;upravljanje z datotekami;brskanje po datotekah;samba;mrežni diski;Raziskovalec;Iskalec; +Keywords[sv]=filer;filhantering;filbläddring;samba;delade nätverksresurser;Utforskare;Finder; +Keywords[uk]=files;file management;file browsing;samba;network shares;Explorer;Finder;файли;керування файлами;навігація;самба;спільні ресурси;мережа;експлорер;провідник;файндер; +Keywords[vi]=files;file management;file browsing;samba;network shares;Explorer;Finder;tệp;quản lí tệp;duyệt tệp;chia sẻ mạng; +Keywords[x-test]=xxfilesxx;xxfile managementxx;xxfile browsingxx;xxsambaxx;xxnetwork sharesxx;xxExplorerxx;xxFinderxx; +Keywords[zh_CN]=files;file management;file browsing;samba;network shares;文件;文件管理;文件浏览;共享;共享文件夹;网络共享;浏览器;访达;查找器; X-DBUS-ServiceName=org.kde.dolphin X-KDE-Shortcuts=Meta+E StartupWMClass=dolphin diff --git a/src/panels/folders/folderspanel.cpp b/src/panels/folders/folderspanel.cpp index 19a05d2b6..d3d8b81f1 100644 --- a/src/panels/folders/folderspanel.cpp +++ b/src/panels/folders/folderspanel.cpp @@ -189,7 +189,19 @@ void FoldersPanel::slotItemActivated(int index) { const KFileItem item = m_model->fileItem(index); if (!item.isNull()) { - Q_EMIT folderActivated(item.url()); + const auto modifiers = QGuiApplication::keyboardModifiers(); + // keep in sync with KUrlNavigator::slotNavigatorButtonClicked + if (modifiers & Qt::ControlModifier && modifiers & Qt::ShiftModifier) { + Q_EMIT folderInNewActiveTab(item.url()); + } else if (modifiers & Qt::ControlModifier) { + Q_EMIT folderInNewTab(item.url()); + } else if (modifiers & Qt::ShiftModifier) { + // The shift modifier is not considered because it is used to expand the tree view without actually + // opening the folder + return; + } else { + Q_EMIT folderActivated(item.url()); + } } } @@ -197,7 +209,13 @@ void FoldersPanel::slotItemMiddleClicked(int index) { const KFileItem item = m_model->fileItem(index); if (!item.isNull()) { - Q_EMIT folderMiddleClicked(item.url()); + const auto modifiers = QGuiApplication::keyboardModifiers(); + // keep in sync with KUrlNavigator::slotNavigatorButtonClicked + if (modifiers & Qt::ShiftModifier) { + Q_EMIT folderInNewActiveTab(item.url()); + } else { + Q_EMIT folderInNewTab(item.url()); + } } } diff --git a/src/panels/folders/folderspanel.h b/src/panels/folders/folderspanel.h index 26c8e4cb9..3ce7870ff 100644 --- a/src/panels/folders/folderspanel.h +++ b/src/panels/folders/folderspanel.h @@ -42,7 +42,8 @@ public: Q_SIGNALS: void folderActivated(const QUrl& url); - void folderMiddleClicked(const QUrl& url); + void folderInNewTab(const QUrl &url); + void folderInNewActiveTab(const QUrl &url); void errorMessage(const QString& error); protected: diff --git a/src/panels/information/informationpanelcontent.cpp b/src/panels/information/informationpanelcontent.cpp index 98c012243..bb5f793cd 100644 --- a/src/panels/information/informationpanelcontent.cpp +++ b/src/panels/information/informationpanelcontent.cpp @@ -23,9 +23,6 @@ #include <Baloo/FileMetaDataWidget> -#include <panels/places/placesitem.h> -#include <panels/places/placesitemmodel.h> - #include <Phonon/BackendCapabilities> #include <Phonon/MediaObject> @@ -60,7 +57,6 @@ InformationPanelContent::InformationPanelContent(QWidget* parent) : m_nameLabel(nullptr), m_metaDataWidget(nullptr), m_metaDataArea(nullptr), - m_placesItemModel(nullptr), m_isVideo(false) { parent->installEventFilter(this); @@ -151,8 +147,6 @@ InformationPanelContent::InformationPanelContent(QWidget* parent) : layout->addWidget(m_configureButtons); grabGesture(Qt::TapAndHoldGesture); - - m_placesItemModel = new PlacesItemModel(this); } InformationPanelContent::~InformationPanelContent() diff --git a/src/panels/information/informationpanelcontent.h b/src/panels/information/informationpanelcontent.h index 78fcf3cd0..38383bd41 100644 --- a/src/panels/information/informationpanelcontent.h +++ b/src/panels/information/informationpanelcontent.h @@ -17,7 +17,6 @@ class KFileItemList; class PhononWidget; class PixmapViewer; -class PlacesItemModel; class QPixmap; class QDialogButtonBox; class QString; @@ -151,7 +150,6 @@ private: QLabel* m_configureLabel; QDialogButtonBox* m_configureButtons; - PlacesItemModel* m_placesItemModel; bool m_isVideo; }; diff --git a/src/panels/places/dolphin_placespanelsettings.kcfg b/src/panels/places/dolphin_placespanelsettings.kcfg index db0ac9495..58f77a959 100644 --- a/src/panels/places/dolphin_placespanelsettings.kcfg +++ b/src/panels/places/dolphin_placespanelsettings.kcfg @@ -8,7 +8,7 @@ <kcfgfile name="dolphinrc"/> <group name="PlacesPanel"> <entry name="IconSize" type="Int"> - <label>Size of icons in the Places Panel (-1 means "use the style's small size")</label> + <label>Size of icons in the Places Panel (-1 means "automatic")</label> <default code="true">KIconLoader::SizeSmallMedium</default> </entry> </group> diff --git a/src/panels/places/placesitem.cpp b/src/panels/places/placesitem.cpp deleted file mode 100644 index 9cac01f91..000000000 --- a/src/panels/places/placesitem.cpp +++ /dev/null @@ -1,274 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2012 Peter Penz <[email protected]> - * SPDX-FileCopyrightText: 2018 Elvis Angelaccio <[email protected]> - * - * Based on KFilePlacesItem from kdelibs: - * SPDX-FileCopyrightText: 2007 Kevin Ottens <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#include "placesitem.h" -#include "trash/dolphintrash.h" - -#include "dolphindebug.h" -#include "placesitemsignalhandler.h" - -#include <KDirLister> -#include <KLocalizedString> -#include <Solid/Block> - -PlacesItem::PlacesItem(const KBookmark& bookmark, PlacesItem* parent) : - KStandardItem(parent), - m_device(), - m_access(), - m_volume(), - m_disc(), - m_player(), - m_signalHandler(nullptr), - m_bookmark() -{ - m_signalHandler = new PlacesItemSignalHandler(this); - setBookmark(bookmark); -} - -PlacesItem::~PlacesItem() -{ - delete m_signalHandler; -} - -void PlacesItem::setUrl(const QUrl &url) -{ - // The default check in KStandardItem::setDataValue() - // for equal values does not work with a custom value - // like QUrl. Hence do a manual check to prevent that - // setting an equal URL results in an itemsChanged() - // signal. - if (dataValue("url").toUrl() != url) { - if (url.scheme() == QLatin1String("trash")) { - QObject::connect(&Trash::instance(), &Trash::emptinessChanged, m_signalHandler.data(), &PlacesItemSignalHandler::onTrashEmptinessChanged); - } - - setDataValue("url", url); - } -} - -QUrl PlacesItem::url() const -{ - return dataValue("url").toUrl(); -} - -void PlacesItem::setUdi(const QString& udi) -{ - setDataValue("udi", udi); -} - -QString PlacesItem::udi() const -{ - return dataValue("udi").toString(); -} - -void PlacesItem::setApplicationName(const QString &applicationName) -{ - setDataValue("applicationName", applicationName); -} - -QString PlacesItem::applicationName() const -{ - return dataValue("applicationName").toString(); -} - -void PlacesItem::setHidden(bool hidden) -{ - setDataValue("isHidden", hidden); -} - -bool PlacesItem::isHidden() const -{ - return dataValue("isHidden").toBool(); -} - -bool PlacesItem::isGroupHidden() const -{ - return dataValue("isGroupHidden").toBool(); -} - -void PlacesItem::setGroupHidden(bool hidden) -{ - setDataValue("isGroupHidden", hidden); -} - -void PlacesItem::setSystemItem(bool isSystemItem) -{ - setDataValue("isSystemItem", isSystemItem); -} - -bool PlacesItem::isSystemItem() const -{ - return dataValue("isSystemItem").toBool(); -} - -Solid::Device PlacesItem::device() const -{ - return m_device; -} - -void PlacesItem::setBookmark(const KBookmark& bookmark) -{ - const bool bookmarkDataChanged = !(bookmark == m_bookmark); - - // bookmark object must be updated to keep in sync with source model - m_bookmark = bookmark; - - if (!bookmarkDataChanged) { - return; - } - - delete m_access; - delete m_volume; - delete m_disc; - delete m_player; - - const QString udi = bookmark.metaDataItem(QStringLiteral("UDI")); - if (udi.isEmpty()) { - setIcon(bookmark.icon()); - setText(i18ndc("kio5", "KFile System Bookmarks", bookmark.text().toUtf8().constData())); - setUrl(bookmark.url()); - setSystemItem(bookmark.metaDataItem(QStringLiteral("isSystemItem")) == QLatin1String("true")); - } else { - initializeDevice(udi); - } - - setHidden(bookmark.metaDataItem(QStringLiteral("IsHidden")) == QLatin1String("true")); -} - -KBookmark PlacesItem::bookmark() const -{ - return m_bookmark; -} - -bool PlacesItem::storageSetupNeeded() const -{ - return m_access ? !m_access->isAccessible() : false; -} - -bool PlacesItem::isSearchOrTimelineUrl() const -{ - const QString urlScheme = url().scheme(); - return (urlScheme.contains("search") || urlScheme.contains("timeline")); -} - -void PlacesItem::onDataValueChanged(const QByteArray& role, - const QVariant& current, - const QVariant& previous) -{ - Q_UNUSED(current) - Q_UNUSED(previous) - - if (!m_bookmark.isNull()) { - updateBookmarkForRole(role); - } -} - -void PlacesItem::onDataChanged(const QHash<QByteArray, QVariant>& current, - const QHash<QByteArray, QVariant>& previous) -{ - Q_UNUSED(previous) - - if (!m_bookmark.isNull()) { - QHashIterator<QByteArray, QVariant> it(current); - while (it.hasNext()) { - it.next(); - updateBookmarkForRole(it.key()); - } - } -} - -void PlacesItem::initializeDevice(const QString& udi) -{ - m_device = Solid::Device(udi); - if (!m_device.isValid()) { - return; - } - - m_access = m_device.as<Solid::StorageAccess>(); - m_volume = m_device.as<Solid::StorageVolume>(); - m_disc = m_device.as<Solid::OpticalDisc>(); - m_player = m_device.as<Solid::PortableMediaPlayer>(); - - setText(m_device.displayName()); - setIcon(m_device.icon()); - setIconOverlays(m_device.emblems()); - setUdi(udi); - - if (m_access) { - setUrl(QUrl::fromLocalFile(m_access->filePath())); - QObject::connect(m_access.data(), &Solid::StorageAccess::accessibilityChanged, - m_signalHandler.data(), &PlacesItemSignalHandler::onAccessibilityChanged); - QObject::connect(m_access.data(), &Solid::StorageAccess::teardownRequested, - m_signalHandler.data(), &PlacesItemSignalHandler::onTearDownRequested); - } else if (m_disc && (m_disc->availableContent() & Solid::OpticalDisc::Audio) != 0) { - Solid::Block *block = m_device.as<Solid::Block>(); - if (block) { - const QString device = block->device(); - setUrl(QUrl(QStringLiteral("audiocd:/?device=%1").arg(device))); - } else { - setUrl(QUrl(QStringLiteral("audiocd:/"))); - } - } else if (m_player) { - const QStringList protocols = m_player->supportedProtocols(); - if (!protocols.isEmpty()) { - setUrl(QUrl(QStringLiteral("%1:udi=%2").arg(protocols.first(), m_device.udi()))); - } - } -} - -void PlacesItem::onAccessibilityChanged() -{ - setIconOverlays(m_device.emblems()); - setUrl(QUrl::fromLocalFile(m_access->filePath())); -} - -void PlacesItem::updateBookmarkForRole(const QByteArray& role) -{ - Q_ASSERT(!m_bookmark.isNull()); - if (role == "iconName") { - m_bookmark.setIcon(icon()); - } else if (role == "text") { - // Only store the text in the KBookmark if it is not the translation of - // the current text. This makes sure that the text is re-translated if - // the user chooses another language, or the translation itself changes. - // - // NOTE: It is important to use "KFile System Bookmarks" as context - // (see PlacesItemModel::createSystemBookmarks()). - if (text() != i18ndc("kio5", "KFile System Bookmarks", m_bookmark.text().toUtf8().data())) { - m_bookmark.setFullText(text()); - } - } else if (role == "url") { - m_bookmark.setUrl(url()); - } else if (role == "udi") { - m_bookmark.setMetaDataItem(QStringLiteral("UDI"), udi()); - } else if (role == "applicationName") { - m_bookmark.setMetaDataItem(QStringLiteral("OnlyInApp"), applicationName()); - } else if (role == "isSystemItem") { - m_bookmark.setMetaDataItem(QStringLiteral("isSystemItem"), isSystemItem() ? QStringLiteral("true") : QStringLiteral("false")); - } else if (role == "isHidden") { - m_bookmark.setMetaDataItem(QStringLiteral("IsHidden"), isHidden() ? QStringLiteral("true") : QStringLiteral("false")); - } -} - -QString PlacesItem::generateNewId() -{ - // The ID-generation must be different as done in KFilePlacesItem from kdelibs - // to prevent identical IDs, because 'count' is of course not shared. We append a - // " (V2)" to indicate that the ID has been generated by - // a new version of the places view. - static int count = 0; - return QString::number(QDateTime::currentDateTimeUtc().toTime_t()) + - '/' + QString::number(count++) + " (V2)"; -} - -PlacesItemSignalHandler *PlacesItem::signalHandler() const -{ - return m_signalHandler.data(); -} diff --git a/src/panels/places/placesitem.h b/src/panels/places/placesitem.h deleted file mode 100644 index 832592302..000000000 --- a/src/panels/places/placesitem.h +++ /dev/null @@ -1,105 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2012 Peter Penz <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#ifndef PLACESITEM_H -#define PLACESITEM_H - -#include "kitemviews/kstandarditem.h" - -#include <KBookmark> -#include <Solid/Device> -#include <Solid/OpticalDisc> -#include <Solid/PortableMediaPlayer> -#include <Solid/StorageAccess> -#include <Solid/StorageVolume> - -#include <QPointer> -#include <QUrl> - - -class KDirLister; -class PlacesItemSignalHandler; - -/** - * @brief Extends KStandardItem by places-specific properties. - */ -class PlacesItem : public KStandardItem -{ - -public: - explicit PlacesItem(const KBookmark& bookmark, PlacesItem* parent = nullptr); - ~PlacesItem() override; - - void setUrl(const QUrl& url); - QUrl url() const; - - void setUdi(const QString& udi); - QString udi() const; - - void setApplicationName(const QString& applicationName); - QString applicationName() const; - - void setHidden(bool hidden); - bool isHidden() const; - - void setGroupHidden(bool hidden); - bool isGroupHidden() const; - - void setSystemItem(bool isSystemItem); - bool isSystemItem() const; - - Solid::Device device() const; - - void setBookmark(const KBookmark& bookmark); - KBookmark bookmark() const; - - bool storageSetupNeeded() const; - - bool isSearchOrTimelineUrl() const; - - PlacesItemSignalHandler* signalHandler() const; - -protected: - void onDataValueChanged(const QByteArray& role, - const QVariant& current, - const QVariant& previous) override; - - void onDataChanged(const QHash<QByteArray, QVariant>& current, - const QHash<QByteArray, QVariant>& previous) override; - -private: - PlacesItem(const PlacesItem& item); - - void initializeDevice(const QString& udi); - - /** - * Is invoked if the accessibility of the storage access - * m_access has been changed and updates the emblem. - */ - void onAccessibilityChanged(); - - /** - * Applies the data-value from the role to m_bookmark. - */ - void updateBookmarkForRole(const QByteArray& role); - - static QString generateNewId(); - -private: - Solid::Device m_device; - QPointer<Solid::StorageAccess> m_access; - QPointer<Solid::StorageVolume> m_volume; - QPointer<Solid::OpticalDisc> m_disc; - QPointer<Solid::PortableMediaPlayer> m_player; - QPointer<PlacesItemSignalHandler> m_signalHandler; - KBookmark m_bookmark; - - friend class PlacesItemSignalHandler; // Calls onAccessibilityChanged() -}; - -#endif - - diff --git a/src/panels/places/placesitemlistgroupheader.cpp b/src/panels/places/placesitemlistgroupheader.cpp deleted file mode 100644 index 76fd670b3..000000000 --- a/src/panels/places/placesitemlistgroupheader.cpp +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2012 Peter Penz <[email protected]> - * - * Based on the Itemviews NG project from Trolltech Labs - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#include "placesitemlistgroupheader.h" - -PlacesItemListGroupHeader::PlacesItemListGroupHeader(QGraphicsWidget* parent) : - KStandardItemListGroupHeader(parent) -{ -} - -PlacesItemListGroupHeader::~PlacesItemListGroupHeader() -{ -} - -void PlacesItemListGroupHeader::paintSeparator(QPainter* painter, const QColor& color) -{ - Q_UNUSED(painter) - Q_UNUSED(color) -} - -QPalette::ColorRole PlacesItemListGroupHeader::normalTextColorRole() const -{ - return QPalette::WindowText; -} - diff --git a/src/panels/places/placesitemlistgroupheader.h b/src/panels/places/placesitemlistgroupheader.h deleted file mode 100644 index dfb91f847..000000000 --- a/src/panels/places/placesitemlistgroupheader.h +++ /dev/null @@ -1,27 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2012 Peter Penz <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#ifndef PLACESITEMLISTGROUPHEADER_H -#define PLACESITEMLISTGROUPHEADER_H - -#include "kitemviews/kstandarditemlistgroupheader.h" - -class PlacesItemListGroupHeader : public KStandardItemListGroupHeader -{ - Q_OBJECT - -public: - explicit PlacesItemListGroupHeader(QGraphicsWidget* parent = nullptr); - ~PlacesItemListGroupHeader() override; - -protected: - void paintSeparator(QPainter* painter, const QColor& color) override; - - QPalette::ColorRole normalTextColorRole() const override; -}; -#endif - - diff --git a/src/panels/places/placesitemlistwidget.cpp b/src/panels/places/placesitemlistwidget.cpp deleted file mode 100644 index ba7a0c4fa..000000000 --- a/src/panels/places/placesitemlistwidget.cpp +++ /dev/null @@ -1,146 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2012 Peter Penz <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#include "placesitemlistwidget.h" - -#include <QStyleOption> - -#include <KColorScheme> - -#include <Solid/Device> -#include <Solid/NetworkShare> - -#define CAPACITYBAR_HEIGHT 2 -#define CAPACITYBAR_MARGIN 2 -#define CAPACITYBAR_CACHE_TTL 60000 - - -PlacesItemListWidget::PlacesItemListWidget(KItemListWidgetInformant* informant, QGraphicsItem* parent) : - KStandardItemListWidget(informant, parent) - , m_drawCapacityBar(false) -{ -} - -PlacesItemListWidget::~PlacesItemListWidget() -{ -} - -bool PlacesItemListWidget::isHidden() const -{ - return data().value("isHidden").toBool() || - data().value("isGroupHidden").toBool(); -} - -QPalette::ColorRole PlacesItemListWidget::normalTextColorRole() const -{ - return QPalette::WindowText; -} - -void PlacesItemListWidget::updateCapacityBar() -{ - const QString udi = data().value("udi").toString(); - if (udi.isEmpty()) { - resetCapacityBar(); - return; - } - const Solid::Device device = Solid::Device(udi); - if (device.isDeviceInterface(Solid::DeviceInterface::NetworkShare) - || device.isDeviceInterface(Solid::DeviceInterface::OpticalDrive) - || device.isDeviceInterface(Solid::DeviceInterface::OpticalDisc)) { - resetCapacityBar(); - return; - } - const QUrl url = data().value("url").toUrl(); - - if (url.isEmpty() || m_freeSpaceInfo.job || !m_freeSpaceInfo.lastUpdated.hasExpired()) { - // No url, job running or cache is still valid. - return; - } - - m_freeSpaceInfo.job = KIO::fileSystemFreeSpace(url); - connect( - m_freeSpaceInfo.job, - &KIO::FileSystemFreeSpaceJob::result, - this, - [this](KIO::Job *job, KIO::filesize_t size, KIO::filesize_t available) { - // even if we receive an error we want to refresh lastUpdated to avoid repeatedly querying in this case - m_freeSpaceInfo.lastUpdated.setRemainingTime(CAPACITYBAR_CACHE_TTL); - - if (job->error()) { - return; - } - - m_freeSpaceInfo.size = size; - m_freeSpaceInfo.used = size - available; - m_freeSpaceInfo.usedRatio = (qreal)m_freeSpaceInfo.used / (qreal)m_freeSpaceInfo.size; - m_drawCapacityBar = size > 0; - - update(); - } - ); -} - -void PlacesItemListWidget::resetCapacityBar() -{ - m_drawCapacityBar = false; - delete m_freeSpaceInfo.job; - m_freeSpaceInfo.lastUpdated.setRemainingTime(0); - m_freeSpaceInfo.size = 0; - m_freeSpaceInfo.used = 0; - m_freeSpaceInfo.usedRatio = 0; -} - -void PlacesItemListWidget::polishEvent() -{ - updateCapacityBar(); - - QGraphicsWidget::polishEvent(); -} - -void PlacesItemListWidget::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) -{ - KStandardItemListWidget::paint(painter, option, widget); - - // We check if option=nullptr since it is null when the place is dragged (Bug #430441) - if (m_drawCapacityBar && option) { - const TextInfo* textInfo = m_textInfo.value("text"); - if (textInfo) { // See KStandarItemListWidget::paint() for info on why we check textInfo. - painter->save(); - - const QRect capacityRect( - textInfo->pos.x(), - option->rect.top() + option->rect.height() - CAPACITYBAR_HEIGHT - CAPACITYBAR_MARGIN, - qMin((qreal)option->rect.width(), selectionRect().width()) - (textInfo->pos.x() - option->rect.left()), - CAPACITYBAR_HEIGHT - ); - - const QPalette pal = palette(); - const QPalette::ColorGroup group = isActiveWindow() ? QPalette::Active : QPalette::Inactive; - - // Background - const QColor bgColor = isSelected() - ? pal.color(group, QPalette::Highlight).darker(180) - : pal.color(group, QPalette::Window).darker(120); - - painter->fillRect(capacityRect, bgColor); - - // Fill - const QRect fillRect(capacityRect.x(), capacityRect.y(), capacityRect.width() * m_freeSpaceInfo.usedRatio, capacityRect.height()); - if (m_freeSpaceInfo.usedRatio >= 0.95) { // More than 95% full! - const QColor dangerUsedColor = KColorScheme(group, KColorScheme::View).foreground(KColorScheme::NegativeText).color(); - painter->fillRect(fillRect, dangerUsedColor); - } else { - const QPalette::ColorRole role = isSelected() ? QPalette::HighlightedText : QPalette::Highlight; - const QColor normalUsedColor = styleOption().palette.color(group, role); - painter->fillRect(fillRect, normalUsedColor); - } - - painter->restore(); - } - } - - updateCapacityBar(); -} diff --git a/src/panels/places/placesitemlistwidget.h b/src/panels/places/placesitemlistwidget.h deleted file mode 100644 index 9c8272fb0..000000000 --- a/src/panels/places/placesitemlistwidget.h +++ /dev/null @@ -1,61 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2012 Peter Penz <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#ifndef PLACESITEMLISTWIDGET_H -#define PLACESITEMLISTWIDGET_H - -#include "kitemviews/kstandarditemlistwidget.h" - -#include <QDeadlineTimer> -#include <QPainter> -#include <QPointer> -#include <QStyleOptionGraphicsItem> -#include <QWidget> - -#include <KIO/FileSystemFreeSpaceJob> - - -// The free space / capacity bar is based on KFilePlacesView. -// https://invent.kde.org/frameworks/kio/-/commit/933887dc334f3498505af7a86d25db7faae91019 -struct PlaceFreeSpaceInfo -{ - QDeadlineTimer lastUpdated; - KIO::filesize_t used = 0; - KIO::filesize_t size = 0; - qreal usedRatio = 0; - QPointer<KIO::FileSystemFreeSpaceJob> job; -}; - - -/** - * @brief Extends KStandardItemListWidget to interpret the hidden - * property of the PlacesModel and use the right text color. -*/ -class PlacesItemListWidget : public KStandardItemListWidget -{ - Q_OBJECT - -public: - PlacesItemListWidget(KItemListWidgetInformant* informant, QGraphicsItem* parent); - ~PlacesItemListWidget() override; - - void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget = nullptr) override; - void polishEvent() override; - -protected: - bool isHidden() const override; - QPalette::ColorRole normalTextColorRole() const override; - void updateCapacityBar(); - void resetCapacityBar(); - -private: - bool m_drawCapacityBar; - PlaceFreeSpaceInfo m_freeSpaceInfo; -}; - -#endif - - diff --git a/src/panels/places/placesitemmodel.cpp b/src/panels/places/placesitemmodel.cpp deleted file mode 100644 index 3da6f7e1f..000000000 --- a/src/panels/places/placesitemmodel.cpp +++ /dev/null @@ -1,784 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2012 Peter Penz <[email protected]> - * - * Based on KFilePlacesModel from kdelibs: - * SPDX-FileCopyrightText: 2007 Kevin Ottens <[email protected]> - * SPDX-FileCopyrightText: 2007 David Faure <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#include "placesitemmodel.h" - -#include "dolphin_generalsettings.h" -#include "dolphindebug.h" -#include "dolphinplacesmodelsingleton.h" -#include "placesitem.h" -#include "placesitemsignalhandler.h" -#include "views/dolphinview.h" -#include "views/viewproperties.h" - -#include <KAboutData> -#include <KLocalizedString> -#include <KUrlMimeData> -#include <Solid/DeviceNotifier> -#include <Solid/OpticalDrive> -#include <KCoreAddons/KProcessList> -#include <KCoreAddons/KListOpenFilesJob> - -#include <QAction> -#include <QIcon> -#include <QMimeData> -#include <QTimer> - -PlacesItemModel::PlacesItemModel(QObject* parent) : - KStandardItemModel(parent), - m_hiddenItemsShown(false), - m_deviceToTearDown(nullptr), - m_storageSetupInProgress(), - m_sourceModel(DolphinPlacesModelSingleton::instance().placesModel()) -{ - cleanupBookmarks(); - loadBookmarks(); - initializeDefaultViewProperties(); - - connect(m_sourceModel, &KFilePlacesModel::rowsInserted, this, &PlacesItemModel::onSourceModelRowsInserted); - connect(m_sourceModel, &KFilePlacesModel::rowsAboutToBeRemoved, this, &PlacesItemModel::onSourceModelRowsAboutToBeRemoved); - connect(m_sourceModel, &KFilePlacesModel::dataChanged, this, &PlacesItemModel::onSourceModelDataChanged); - connect(m_sourceModel, &KFilePlacesModel::rowsAboutToBeMoved, this, &PlacesItemModel::onSourceModelRowsAboutToBeMoved); - connect(m_sourceModel, &KFilePlacesModel::rowsMoved, this, &PlacesItemModel::onSourceModelRowsMoved); - connect(m_sourceModel, &KFilePlacesModel::groupHiddenChanged, this, &PlacesItemModel::onSourceModelGroupHiddenChanged); -} - -PlacesItemModel::~PlacesItemModel() -{ -} - -void PlacesItemModel::createPlacesItem(const QString &text, const QUrl &url, const QString &iconName, const QString &appName) -{ - createPlacesItem(text, url, iconName, appName, -1); -} - -void PlacesItemModel::createPlacesItem(const QString &text, const QUrl &url, const QString &iconName, const QString &appName, int after) -{ - m_sourceModel->addPlace(text, url, iconName, appName, mapToSource(after)); -} - -PlacesItem* PlacesItemModel::placesItem(int index) const -{ - return dynamic_cast<PlacesItem*>(item(index)); -} - -int PlacesItemModel::hiddenCount() const -{ - return m_sourceModel->hiddenCount(); -} - -void PlacesItemModel::setHiddenItemsShown(bool show) -{ - if (m_hiddenItemsShown == show) { - return; - } - - m_hiddenItemsShown = show; - - if (show) { - for (int r = 0, rMax = m_sourceModel->rowCount(); r < rMax; r++) { - const QModelIndex index = m_sourceModel->index(r, 0); - if (!m_sourceModel->isHidden(index)) { - continue; - } - addItemFromSourceModel(index); - } - } else { - for (int r = 0, rMax = m_sourceModel->rowCount(); r < rMax; r++) { - const QModelIndex index = m_sourceModel->index(r, 0); - if (m_sourceModel->isHidden(index)) { - removeItemByIndex(index); - } - } - } -} - -bool PlacesItemModel::hiddenItemsShown() const -{ - return m_hiddenItemsShown; -} - -int PlacesItemModel::closestItem(const QUrl& url) const -{ - return mapFromSource(m_sourceModel->closestItem(url)); -} - -// look for the correct position for the item based on source model -void PlacesItemModel::insertSortedItem(PlacesItem* item) -{ - if (!item) { - return; - } - - const KBookmark iBookmark = item->bookmark(); - const QString iBookmarkId = bookmarkId(iBookmark); - QModelIndex sourceIndex; - int pos = 0; - - for(int r = 0, rMax = m_sourceModel->rowCount(); r < rMax; r++) { - sourceIndex = m_sourceModel->index(r, 0); - const KBookmark sourceBookmark = m_sourceModel->bookmarkForIndex(sourceIndex); - - if (bookmarkId(sourceBookmark) == iBookmarkId) { - break; - } - - if (m_hiddenItemsShown || !m_sourceModel->isHidden(sourceIndex)) { - pos++; - } - } - - m_indexMap.insert(pos, sourceIndex); - insertItem(pos, item); -} - -void PlacesItemModel::onItemInserted(int index) -{ - KStandardItemModel::onItemInserted(index); -} - -void PlacesItemModel::onItemRemoved(int index, KStandardItem* removedItem) -{ - m_indexMap.removeAt(index); - - KStandardItemModel::onItemRemoved(index, removedItem); -} - -void PlacesItemModel::onItemChanged(int index, const QSet<QByteArray>& changedRoles) -{ - const QModelIndex sourceIndex = mapToSource(index); - const PlacesItem *changedItem = placesItem(mapFromSource(sourceIndex)); - - if (!changedItem || !sourceIndex.isValid()) { - qWarning() << "invalid item changed signal"; - return; - } - if (changedRoles.contains("isHidden")) { - if (m_sourceModel->isHidden(sourceIndex) != changedItem->isHidden()) { - m_sourceModel->setPlaceHidden(sourceIndex, changedItem->isHidden()); - } else { - m_sourceModel->refresh(); - } - } - KStandardItemModel::onItemChanged(index, changedRoles); -} - -QAction* PlacesItemModel::ejectAction(int index) const -{ - const PlacesItem* item = placesItem(index); - if (item && item->device().is<Solid::OpticalDisc>()) { - return new QAction(QIcon::fromTheme(QStringLiteral("media-eject")), i18nc("@item", "Eject"), nullptr); - } - - return nullptr; -} - -QAction* PlacesItemModel::teardownAction(int index) const -{ - const PlacesItem* item = placesItem(index); - if (!item) { - return nullptr; - } - - Solid::Device device = item->device(); - const bool providesTearDown = device.is<Solid::StorageAccess>() && - device.as<Solid::StorageAccess>()->isAccessible(); - if (!providesTearDown) { - return nullptr; - } - - Solid::StorageDrive* drive = device.as<Solid::StorageDrive>(); - if (!drive) { - drive = device.parent().as<Solid::StorageDrive>(); - } - - bool hotPluggable = false; - bool removable = false; - if (drive) { - hotPluggable = drive->isHotpluggable(); - removable = drive->isRemovable(); - } - - QString iconName; - QString text; - if (device.is<Solid::OpticalDisc>()) { - text = i18nc("@item", "Release"); - } else if (removable || hotPluggable) { - text = i18nc("@item", "Safely Remove"); - iconName = QStringLiteral("media-eject"); - } else { - text = i18nc("@item", "Unmount"); - iconName = QStringLiteral("media-eject"); - } - - if (iconName.isEmpty()) { - return new QAction(text, nullptr); - } - - return new QAction(QIcon::fromTheme(iconName), text, nullptr); -} - -void PlacesItemModel::requestEject(int index) -{ - const PlacesItem* item = placesItem(index); - if (item) { - Solid::OpticalDrive* drive = item->device().parent().as<Solid::OpticalDrive>(); - if (drive) { - connect(drive, &Solid::OpticalDrive::ejectDone, - this, &PlacesItemModel::slotStorageTearDownDone); - drive->eject(); - } else { - const QString label = item->text(); - const QString message = i18nc("@info", "The device '%1' is not a disk and cannot be ejected.", label); - Q_EMIT errorMessage(message); - } - } -} - -void PlacesItemModel::requestTearDown(int index) -{ - const PlacesItem* item = placesItem(index); - if (item) { - Solid::StorageAccess *tmp = item->device().as<Solid::StorageAccess>(); - if (tmp) { - m_deviceToTearDown = tmp; - // disconnect the Solid::StorageAccess::teardownRequested - // to prevent emitting PlacesItemModel::storageTearDownExternallyRequested - // after we have emitted PlacesItemModel::storageTearDownRequested - disconnect(tmp, &Solid::StorageAccess::teardownRequested, - item->signalHandler(), &PlacesItemSignalHandler::onTearDownRequested); - Q_EMIT storageTearDownRequested(tmp->filePath()); - } - } -} - -bool PlacesItemModel::storageSetupNeeded(int index) const -{ - const PlacesItem* item = placesItem(index); - return item ? item->storageSetupNeeded() : false; -} - -void PlacesItemModel::requestStorageSetup(int index) -{ - const PlacesItem* item = placesItem(index); - if (!item) { - return; - } - - Solid::Device device = item->device(); - const bool setup = device.is<Solid::StorageAccess>() - && !m_storageSetupInProgress.contains(device.as<Solid::StorageAccess>()) - && !device.as<Solid::StorageAccess>()->isAccessible(); - if (setup) { - Solid::StorageAccess* access = device.as<Solid::StorageAccess>(); - - m_storageSetupInProgress[access] = index; - - connect(access, &Solid::StorageAccess::setupDone, - this, &PlacesItemModel::slotStorageSetupDone); - - access->setup(); - } -} - -QMimeData* PlacesItemModel::createMimeData(const KItemSet& indexes) const -{ - QList<QUrl> urls; - QByteArray itemData; - - QDataStream stream(&itemData, QIODevice::WriteOnly); - - for (int index : indexes) { - const QUrl itemUrl = placesItem(index)->url(); - if (itemUrl.isValid()) { - urls << itemUrl; - } - stream << index; - } - - QMimeData* mimeData = new QMimeData(); - if (!urls.isEmpty()) { - mimeData->setUrls(urls); - } else { - // #378954: prevent itemDropEvent() drops if there isn't a source url. - mimeData->setData(blacklistItemDropEventMimeType(), QByteArrayLiteral("true")); - } - mimeData->setData(internalMimeType(), itemData); - - return mimeData; -} - -bool PlacesItemModel::supportsDropping(int index) const -{ - return index >= 0 && index < count(); -} - -void PlacesItemModel::dropMimeDataBefore(int index, const QMimeData* mimeData) -{ - if (mimeData->hasFormat(internalMimeType())) { - // The item has been moved inside the view - QByteArray itemData = mimeData->data(internalMimeType()); - QDataStream stream(&itemData, QIODevice::ReadOnly); - int oldIndex; - stream >> oldIndex; - - QModelIndex sourceIndex = mapToSource(index); - QModelIndex oldSourceIndex = mapToSource(oldIndex); - - m_sourceModel->movePlace(oldSourceIndex.row(), sourceIndex.row()); - } else if (mimeData->hasFormat(QStringLiteral("text/uri-list"))) { - // One or more items must be added to the model - const QList<QUrl> urls = KUrlMimeData::urlsFromMimeData(mimeData); - for (int i = urls.count() - 1; i >= 0; --i) { - const QUrl& url = urls[i]; - - QString text = url.fileName(); - if (text.isEmpty()) { - text = url.host(); - } - - if ((url.isLocalFile() && !QFileInfo(url.toLocalFile()).isDir()) - || url.scheme() == QLatin1String("trash")) { - // Only directories outside the trash are allowed - continue; - } - - createPlacesItem(text, url, KIO::iconNameForUrl(url), {}, qMax(0, index - 1)); - } - } - // will save bookmark alteration and fix sort if that is broken by the drag/drop operation - refresh(); -} - -void PlacesItemModel::addItemFromSourceModel(const QModelIndex &index) -{ - if (!m_hiddenItemsShown && m_sourceModel->isHidden(index)) { - return; - } - - const KBookmark bookmark = m_sourceModel->bookmarkForIndex(index); - Q_ASSERT(!bookmark.isNull()); - PlacesItem *item = new PlacesItem(bookmark); - updateItem(item, index); - insertSortedItem(item); - - if (m_sourceModel->isDevice(index)) { - connect(item->signalHandler(), &PlacesItemSignalHandler::tearDownExternallyRequested, - this, &PlacesItemModel::storageTearDownExternallyRequested); - } -} - -void PlacesItemModel::removeItemByIndex(const QModelIndex &sourceIndex) -{ - QString id = bookmarkId(m_sourceModel->bookmarkForIndex(sourceIndex)); - - for (int i = 0, iMax = count(); i < iMax; ++i) { - if (bookmarkId(placesItem(i)->bookmark()) == id) { - removeItem(i); - return; - } - } -} - -QString PlacesItemModel::bookmarkId(const KBookmark &bookmark) const -{ - QString id = bookmark.metaDataItem(QStringLiteral("UDI")); - if (id.isEmpty()) { - id = bookmark.metaDataItem(QStringLiteral("ID")); - } - return id; -} - -void PlacesItemModel::initializeDefaultViewProperties() const -{ - for(int i = 0, iMax = m_sourceModel->rowCount(); i < iMax; i++) { - const QModelIndex index = m_sourceModel->index(i, 0); - const PlacesItem *item = placesItem(mapFromSource(index)); - if (!item) { - continue; - } - - // Create default view-properties for all "Search For" and "Recently Saved" bookmarks - // in case the user has not already created custom view-properties for a corresponding - // query yet. - const bool createDefaultViewProperties = item->isSearchOrTimelineUrl() && !GeneralSettings::self()->globalViewProps(); - if (createDefaultViewProperties) { - const QUrl itemUrl = item->url(); - ViewProperties props(KFilePlacesModel::convertedUrl(itemUrl)); - if (!props.exist()) { - const QString path = itemUrl.path(); - if (path == QLatin1String("/documents")) { - props.setViewMode(DolphinView::DetailsView); - props.setPreviewsShown(false); - props.setVisibleRoles({"text", "path"}); - } else if (path == QLatin1String("/images")) { - props.setViewMode(DolphinView::IconsView); - props.setPreviewsShown(true); - props.setVisibleRoles({"text", "height", "width"}); - } else if (path == QLatin1String("/audio")) { - props.setViewMode(DolphinView::DetailsView); - props.setPreviewsShown(false); - props.setVisibleRoles({"text", "artist", "album"}); - } else if (path == QLatin1String("/videos")) { - props.setViewMode(DolphinView::IconsView); - props.setPreviewsShown(true); - props.setVisibleRoles({"text"}); - } else if (itemUrl.scheme() == QLatin1String("timeline")) { - props.setViewMode(DolphinView::DetailsView); - props.setVisibleRoles({"text", "modificationtime"}); - } - props.save(); - } - } - } -} - -void PlacesItemModel::updateItem(PlacesItem *item, const QModelIndex &index) -{ - item->setGroup(index.data(KFilePlacesModel::GroupRole).toString()); - item->setIcon(index.data(KFilePlacesModel::IconNameRole).toString()); - item->setGroupHidden(index.data(KFilePlacesModel::GroupHiddenRole).toBool()); -} - -void PlacesItemModel::slotStorageTearDownDone(Solid::ErrorType error, const QVariant& errorData) -{ - if (error && errorData.isValid()) { - if (error == Solid::ErrorType::DeviceBusy) { - KListOpenFilesJob* listOpenFilesJob = new KListOpenFilesJob(m_deviceToTearDown->filePath()); - connect(listOpenFilesJob, &KIO::Job::result, this, [this, listOpenFilesJob](KJob*) { - const KProcessList::KProcessInfoList blockingProcesses = listOpenFilesJob->processInfoList(); - QString errorString; - if (blockingProcesses.isEmpty()) { - errorString = i18n("One or more files on this device are open within an application."); - } else { - QStringList blockingApps; - for (const auto& process : blockingProcesses) { - blockingApps << process.name(); - } - blockingApps.removeDuplicates(); - errorString = xi18np("One or more files on this device are opened in application <application>\"%2\"</application>.", - "One or more files on this device are opened in following applications: <application>%2</application>.", - blockingApps.count(), blockingApps.join(i18nc("separator in list of apps blocking device unmount", ", "))); - } - Q_EMIT errorMessage(errorString); - }); - listOpenFilesJob->start(); - } else { - Q_EMIT errorMessage(errorData.toString()); - } - } else { - // No error; it must have been unmounted successfully - Q_EMIT storageTearDownSuccessful(); - } - disconnect(m_deviceToTearDown, &Solid::StorageAccess::teardownDone, - this, &PlacesItemModel::slotStorageTearDownDone); - m_deviceToTearDown = nullptr; -} - -void PlacesItemModel::slotStorageSetupDone(Solid::ErrorType error, - const QVariant& errorData, - const QString& udi) -{ - Q_UNUSED(udi) - - const int index = m_storageSetupInProgress.take(sender()); - const PlacesItem* item = placesItem(index); - if (!item) { - return; - } - - if (error != Solid::NoError) { - if (errorData.isValid()) { - Q_EMIT errorMessage(i18nc("@info", "An error occurred while accessing '%1', the system responded: %2", - item->text(), - errorData.toString())); - } else { - Q_EMIT errorMessage(i18nc("@info", "An error occurred while accessing '%1'", - item->text())); - } - Q_EMIT storageSetupDone(index, false); - } else { - Q_EMIT storageSetupDone(index, true); - } -} - -void PlacesItemModel::onSourceModelRowsInserted(const QModelIndex &parent, int first, int last) -{ - for (int i = first; i <= last; i++) { - const QModelIndex index = m_sourceModel->index(i, 0, parent); - addItemFromSourceModel(index); - } -} - -void PlacesItemModel::onSourceModelRowsAboutToBeRemoved(const QModelIndex &parent, int first, int last) -{ - for(int r = first; r <= last; r++) { - const QModelIndex index = m_sourceModel->index(r, 0, parent); - int oldIndex = mapFromSource(index); - if (oldIndex != -1) { - removeItem(oldIndex); - } - } -} - -void PlacesItemModel::onSourceModelRowsAboutToBeMoved(const QModelIndex &parent, int start, int end, const QModelIndex &destination, int row) -{ - Q_UNUSED(destination) - Q_UNUSED(row) - - for(int r = start; r <= end; r++) { - const QModelIndex sourceIndex = m_sourceModel->index(r, 0, parent); - // remove moved item - removeItem(mapFromSource(sourceIndex)); - } -} - -void PlacesItemModel::onSourceModelRowsMoved(const QModelIndex &parent, int start, int end, const QModelIndex &destination, int row) -{ - Q_UNUSED(destination) - Q_UNUSED(parent) - - const int blockSize = (end - start) + 1; - - for (int r = start; r <= end; r++) { - // insert the moved item in the new position - const int targetRow = row + (start - r) - (r < row ? blockSize : 0); - const QModelIndex targetIndex = m_sourceModel->index(targetRow, 0, destination); - - addItemFromSourceModel(targetIndex); - } -} - -void PlacesItemModel::onSourceModelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles) -{ - Q_UNUSED(roles) - - for (int r = topLeft.row(); r <= bottomRight.row(); r++) { - const QModelIndex sourceIndex = m_sourceModel->index(r, 0); - const KBookmark bookmark = m_sourceModel->bookmarkForIndex(sourceIndex); - PlacesItem *placeItem = itemFromBookmark(bookmark); - - if (placeItem && (!m_hiddenItemsShown && m_sourceModel->isHidden(sourceIndex))) { - //hide item if it became invisible - removeItem(index(placeItem)); - return; - } - - if (!placeItem && (m_hiddenItemsShown || !m_sourceModel->isHidden(sourceIndex))) { - //show item if it became visible - addItemFromSourceModel(sourceIndex); - return; - } - - if (placeItem && !m_sourceModel->isDevice(sourceIndex)) { - // must update the bookmark object - placeItem->setBookmark(bookmark); - } - } -} - -void PlacesItemModel::onSourceModelGroupHiddenChanged(KFilePlacesModel::GroupType group, bool hidden) -{ - const auto groupIndexes = m_sourceModel->groupIndexes(group); - for (const QModelIndex &sourceIndex : groupIndexes) { - PlacesItem *item = placesItem(mapFromSource(sourceIndex)); - if (item) { - item->setGroupHidden(hidden); - } - } -} - -void PlacesItemModel::cleanupBookmarks() -{ - // KIO model now provides support for baloo urls, and because of that we - // need to remove old URLs that were visible only in Dolphin to avoid duplication - - static const QVector<QUrl> balooURLs = { - QUrl(QStringLiteral("timeline:/today")), - QUrl(QStringLiteral("timeline:/yesterday")), - QUrl(QStringLiteral("timeline:/thismonth")), - QUrl(QStringLiteral("timeline:/lastmonth")), - QUrl(QStringLiteral("search:/documents")), - QUrl(QStringLiteral("search:/images")), - QUrl(QStringLiteral("search:/audio")), - QUrl(QStringLiteral("search:/videos")) - }; - - int row = 0; - do { - const QModelIndex sourceIndex = m_sourceModel->index(row, 0); - const KBookmark bookmark = m_sourceModel->bookmarkForIndex(sourceIndex); - const QUrl url = bookmark.url(); - const QString appName = bookmark.metaDataItem(QStringLiteral("OnlyInApp")); - - if ((appName == KAboutData::applicationData().componentName() || - appName == KAboutData::applicationData().componentName() + DolphinPlacesModelSingleton::applicationNameSuffix()) && balooURLs.contains(url)) { - qCDebug(DolphinDebug) << "Removing old baloo url:" << url; - m_sourceModel->removePlace(sourceIndex); - } else { - row++; - } - } while (row < m_sourceModel->rowCount()); -} - -void PlacesItemModel::loadBookmarks() -{ - for(int r = 0, rMax = m_sourceModel->rowCount(); r < rMax; r++) { - const QModelIndex sourceIndex = m_sourceModel->index(r, 0); - if (m_hiddenItemsShown || !m_sourceModel->isHidden(sourceIndex)) { - addItemFromSourceModel(sourceIndex); - } - } -} - -void PlacesItemModel::clear() { - KStandardItemModel::clear(); -} - -void PlacesItemModel::proceedWithTearDown() -{ - Q_ASSERT(m_deviceToTearDown); - - connect(m_deviceToTearDown, &Solid::StorageAccess::teardownDone, - this, &PlacesItemModel::slotStorageTearDownDone); - m_deviceToTearDown->teardown(); -} - -void PlacesItemModel::deleteItem(int index) -{ - QModelIndex sourceIndex = mapToSource(index); - Q_ASSERT(sourceIndex.isValid()); - m_sourceModel->removePlace(sourceIndex); -} - -void PlacesItemModel::refresh() -{ - m_sourceModel->refresh(); -} - -void PlacesItemModel::hideItem(int index) -{ - PlacesItem* shownItem = placesItem(index); - if (!shownItem) { - return; - } - - shownItem->setHidden(true); -} - -QString PlacesItemModel::internalMimeType() const -{ - return "application/x-dolphinplacesmodel-" + - QString::number((qptrdiff)this); -} - -int PlacesItemModel::groupedDropIndex(int index, const PlacesItem* item) const -{ - Q_ASSERT(item); - - int dropIndex = index; - const QString group = item->group(); - - const int itemCount = count(); - if (index < 0) { - dropIndex = itemCount; - } - - // Search nearest previous item with the same group - int previousIndex = -1; - for (int i = dropIndex - 1; i >= 0; --i) { - if (placesItem(i)->group() == group) { - previousIndex = i; - break; - } - } - - // Search nearest next item with the same group - int nextIndex = -1; - for (int i = dropIndex; i < count(); ++i) { - if (placesItem(i)->group() == group) { - nextIndex = i; - break; - } - } - - // Adjust the drop-index to be inserted to the - // nearest item with the same group. - if (previousIndex >= 0 && nextIndex >= 0) { - dropIndex = (dropIndex - previousIndex < nextIndex - dropIndex) ? - previousIndex + 1 : nextIndex; - } else if (previousIndex >= 0) { - dropIndex = previousIndex + 1; - } else if (nextIndex >= 0) { - dropIndex = nextIndex; - } - - return dropIndex; -} - -bool PlacesItemModel::equalBookmarkIdentifiers(const KBookmark& b1, const KBookmark& b2) -{ - const QString udi1 = b1.metaDataItem(QStringLiteral("UDI")); - const QString udi2 = b2.metaDataItem(QStringLiteral("UDI")); - if (!udi1.isEmpty() && !udi2.isEmpty()) { - return udi1 == udi2; - } else { - return b1.metaDataItem(QStringLiteral("ID")) == b2.metaDataItem(QStringLiteral("ID")); - } -} - -int PlacesItemModel::mapFromSource(const QModelIndex &index) const -{ - if (!index.isValid()) { - return -1; - } - - return m_indexMap.indexOf(index); -} - -bool PlacesItemModel::isDir(int index) const -{ - Q_UNUSED(index) - return true; -} - -KFilePlacesModel::GroupType PlacesItemModel::groupType(int row) const -{ - return m_sourceModel->groupType(mapToSource(row)); -} - -bool PlacesItemModel::isGroupHidden(KFilePlacesModel::GroupType type) const -{ - return m_sourceModel->isGroupHidden(type); -} - -void PlacesItemModel::setGroupHidden(KFilePlacesModel::GroupType type, bool hidden) -{ - return m_sourceModel->setGroupHidden(type, hidden); -} - -QModelIndex PlacesItemModel::mapToSource(int row) const -{ - return m_indexMap.value(row); -} - -PlacesItem *PlacesItemModel::itemFromBookmark(const KBookmark &bookmark) const -{ - const QString id = bookmarkId(bookmark); - for (int i = 0, iMax = count(); i < iMax; i++) { - PlacesItem *item = placesItem(i); - const KBookmark itemBookmark = item->bookmark(); - if (bookmarkId(itemBookmark) == id) { - return item; - } - } - return nullptr; -} - diff --git a/src/panels/places/placesitemmodel.h b/src/panels/places/placesitemmodel.h deleted file mode 100644 index cd4079a73..000000000 --- a/src/panels/places/placesitemmodel.h +++ /dev/null @@ -1,216 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2012 Peter Penz <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#ifndef PLACESITEMMODEL_H -#define PLACESITEMMODEL_H - -#include "kitemviews/kstandarditemmodel.h" - -#include <KFilePlacesModel> -#include <Solid/Predicate> -#include <Solid/StorageAccess> - -#include <QHash> -#include <QList> -#include <QSet> -#include <QUrl> - -class KBookmark; -class PlacesItem; -class QAction; - -/** - * @brief Model for maintaining the bookmarks of the places panel. - * - * It is based on KFilePlacesModel from KIO. - */ -class PlacesItemModel: public KStandardItemModel -{ - Q_OBJECT - -public: - explicit PlacesItemModel(QObject* parent = nullptr); - ~PlacesItemModel() override; - - /** - * @brief Create a new place entry in the bookmark file - * and add it to the model - */ - void createPlacesItem(const QString& text, const QUrl& url, const QString& iconName = {}, const QString& appName = {}); - void createPlacesItem(const QString& text, const QUrl& url, const QString& iconName, const QString& appName, int after); - - PlacesItem* placesItem(int index) const; - - /** - * @brief Mark an item as hidden - * @param index of the item to be hidden - */ - void hideItem(int index); - - /** - * If set to true, all items that are marked as hidden - * will be shown in the view. The items will - * stay marked as hidden, which is visually indicated - * by the view by desaturating the icon and the text. - */ - void setHiddenItemsShown(bool show); - bool hiddenItemsShown() const; - - /** - * @return Number of items that are marked as hidden. - * Note that this does not mean that the items - * are really hidden - * (see PlacesItemModel::setHiddenItemsShown()). - */ - int hiddenCount() const; - - /** - * Search the item which is equal to the URL or at least - * is a parent URL. If there are more than one possible - * candidates, return the item which covers the biggest - * range of the URL. -1 is returned if no closest item - * could be found. - */ - int closestItem(const QUrl& url) const; - - QAction* ejectAction(int index) const; - QAction* teardownAction(int index) const; - - void requestEject(int index); - void requestTearDown(int index); - - bool storageSetupNeeded(int index) const; - void requestStorageSetup(int index); - - QMimeData* createMimeData(const KItemSet& indexes) const override; - - bool supportsDropping(int index) const override; - - void dropMimeDataBefore(int index, const QMimeData* mimeData); - - /** - * @return Converts the URL, which contains "virtual" URLs for system-items like - * "search:/documents" into a Query-URL that will be handled by - * the corresponding IO-slave. Virtual URLs for bookmarks are used to - * be independent from internal format changes. - */ - static QUrl convertedUrl(const QUrl& url); - - void clear() override; - - void proceedWithTearDown(); - - /** - * @brief Remove item from bookmark - * - * This function remove the index from bookmark file permanently - * - * @param index - the item to be removed - */ - void deleteItem(int index); - - /** - * Force a sync on the bookmarks and indicates to other applications that the - * state of the bookmarks has been changed. - */ - void refresh(); - - bool isDir(int index) const override; - - - KFilePlacesModel::GroupType groupType(int row) const; - bool isGroupHidden(KFilePlacesModel::GroupType type) const; - void setGroupHidden(KFilePlacesModel::GroupType type, bool hidden); - -Q_SIGNALS: - void errorMessage(const QString& message); - void storageSetupDone(int index, bool success); - void storageTearDownRequested(const QString& mountPath); - void storageTearDownExternallyRequested(const QString& mountPath); - void storageTearDownSuccessful(); - -protected: - void onItemInserted(int index) override; - void onItemRemoved(int index, KStandardItem* removedItem) override; - void onItemChanged(int index, const QSet<QByteArray>& changedRoles) override; - -private Q_SLOTS: - void slotStorageTearDownDone(Solid::ErrorType error, const QVariant& errorData); - void slotStorageSetupDone(Solid::ErrorType error, const QVariant& errorData, const QString& udi); - - // source model control - void onSourceModelRowsInserted(const QModelIndex &parent, int first, int last); - void onSourceModelRowsAboutToBeRemoved(const QModelIndex &parent, int first, int last); - void onSourceModelRowsAboutToBeMoved(const QModelIndex &parent, int start, int end, const QModelIndex &destination, int row); - void onSourceModelRowsMoved(const QModelIndex &parent, int start, int end, const QModelIndex &destination, int row); - void onSourceModelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles); - void onSourceModelGroupHiddenChanged(KFilePlacesModel::GroupType group, bool hidden); - -private: - /** - * Remove bookmarks created by the previous version of dolphin that are - * not valid anymore - */ - void cleanupBookmarks(); - - /** - * Loads the bookmarks from the bookmark-manager and creates items for - * the model or moves hidden items to m_bookmarkedItems. - */ - void loadBookmarks(); - - QString internalMimeType() const; - - /** - * @return Adjusted drop index which assures that the item is aligned - * into the same group as specified by PlacesItem::groupType(). - */ - int groupedDropIndex(int index, const PlacesItem* item) const; - - /** - * @return True if the bookmarks have the same identifiers. The identifier - * is the unique "ID"-property in case if no UDI is set, otherwise - * the UDI is used as identifier. - */ - static bool equalBookmarkIdentifiers(const KBookmark& b1, const KBookmark& b2); - - /** - * Appends the item \a item as last element of the group - * the item belongs to. If no item with the same group is - * present, the item gets appended as last element of the - * model. PlacesItemModel takes the ownership - * of the item. - */ - void insertSortedItem(PlacesItem* item); - - PlacesItem *itemFromBookmark(const KBookmark &bookmark) const; - - void addItemFromSourceModel(const QModelIndex &index); - void removeItemByIndex(const QModelIndex &mapToSource); - - QString bookmarkId(const KBookmark &bookmark) const; - void initializeDefaultViewProperties() const; - - int mapFromSource(const QModelIndex &index) const; - QModelIndex mapToSource(int row) const; - - static void updateItem(PlacesItem *item, const QModelIndex &index); - -private: - bool m_hiddenItemsShown; - - Solid::StorageAccess *m_deviceToTearDown; - - QHash<QObject*, int> m_storageSetupInProgress; - - KFilePlacesModel *m_sourceModel; - - QVector<QPersistentModelIndex> m_indexMap; -}; - -#endif - - diff --git a/src/panels/places/placesitemsignalhandler.cpp b/src/panels/places/placesitemsignalhandler.cpp deleted file mode 100644 index 19f16c7b5..000000000 --- a/src/panels/places/placesitemsignalhandler.cpp +++ /dev/null @@ -1,46 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2012 Peter Penz <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#include "placesitemsignalhandler.h" - -#include "placesitem.h" - -PlacesItemSignalHandler::PlacesItemSignalHandler(PlacesItem* item, - QObject* parent) : - QObject(parent), - m_item(item) -{ -} - -PlacesItemSignalHandler::~PlacesItemSignalHandler() -{ -} - -void PlacesItemSignalHandler::onAccessibilityChanged() -{ - if (m_item) { - m_item->onAccessibilityChanged(); - } -} - -void PlacesItemSignalHandler::onTearDownRequested(const QString& udi) -{ - Q_UNUSED(udi) - if (m_item) { - Solid::StorageAccess *tmp = m_item->device().as<Solid::StorageAccess>(); - if (tmp) { - Q_EMIT tearDownExternallyRequested(tmp->filePath()); - } - } -} - -void PlacesItemSignalHandler::onTrashEmptinessChanged(bool isTrashEmpty) -{ - if (m_item) { - m_item->setIcon(isTrashEmpty ? QStringLiteral("user-trash") : QStringLiteral("user-trash-full")); - } -} - diff --git a/src/panels/places/placesitemsignalhandler.h b/src/panels/places/placesitemsignalhandler.h deleted file mode 100644 index da4783915..000000000 --- a/src/panels/places/placesitemsignalhandler.h +++ /dev/null @@ -1,57 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2012 Peter Penz <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#ifndef PLACESITEMSIGNALHANDLER_H -#define PLACESITEMSIGNALHANDLER_H - -#include <QObject> - -class PlacesItem; - -/** - * @brief Helper class for PlacesItem to be able to listen to signals - * and performing a corresponding action. - * - * PlacesItem is derived from KStandardItem, which is no QObject-class - * on purpose. To be able to internally listen to signals and performing a - * corresponding action, PlacesItemSignalHandler is used. - * - * E.g. if the PlacesItem wants to react on accessibility-changes of a storage-access, - * the signal-handler can be used like this: - * <code> - * QObject::connect(storageAccess, SIGNAL(accessibilityChanged(bool,QString)), - * signalHandler, SLOT(onAccessibilityChanged())); - * </code> - * - * The slot PlacesItemSignalHandler::onAccessibilityChanged() will call - * the method PlacesItem::onAccessibilityChanged(). - */ -class PlacesItemSignalHandler: public QObject -{ - Q_OBJECT - -public: - explicit PlacesItemSignalHandler(PlacesItem* item, QObject* parent = nullptr); - ~PlacesItemSignalHandler() override; - -public Q_SLOTS: - /** - * Calls PlacesItem::onAccessibilityChanged() - */ - void onAccessibilityChanged(); - - void onTearDownRequested(const QString& udi); - - void onTrashEmptinessChanged(bool isTrashEmpty); - -Q_SIGNALS: - void tearDownExternallyRequested(const QString& udi); - -private: - PlacesItem* m_item; -}; - -#endif diff --git a/src/panels/places/placespanel.cpp b/src/panels/places/placespanel.cpp index 83e014a82..5b2f040d1 100644 --- a/src/panels/places/placespanel.cpp +++ b/src/panels/places/placespanel.cpp @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2008-2012 Peter Penz <[email protected]> + * SPDX-FileCopyrightText: 2021 Kai Uwe Broulik <[email protected]> * * Based on KFilePlacesView from kdelibs: * SPDX-FileCopyrightText: 2007 Kevin Ottens <[email protected]> @@ -10,461 +11,188 @@ #include "placespanel.h" +#include "dolphinplacesmodelsingleton.h" #include "dolphin_generalsettings.h" +#include "dolphin_placespanelsettings.h" #include "global.h" -#include "kitemviews/kitemlistcontainer.h" -#include "kitemviews/kitemlistcontroller.h" -#include "kitemviews/kitemlistselectionmanager.h" -#include "kitemviews/kstandarditem.h" -#include "placesitem.h" -#include "placesitemlistgroupheader.h" -#include "placesitemlistwidget.h" -#include "placesitemmodel.h" -#include "placesview.h" -#include "trash/dolphintrash.h" #include "views/draganddrophelper.h" #include "settings/dolphinsettingsdialog.h" -#include <KFilePlaceEditDialog> #include <KFilePlacesModel> #include <KIO/DropJob> -#include <KIO/EmptyTrashJob> #include <KIO/Job> -#include <KIconLoader> +#include <KListOpenFilesJob> #include <KLocalizedString> -#include <KMountPoint> -#include <KPropertiesDialog> -#include <QActionGroup> -#include <QGraphicsSceneDragDropEvent> #include <QIcon> #include <QMenu> -#include <QMimeData> -#include <QVBoxLayout> -#include <QToolTip> +#include <QShowEvent> +#include <QTimer> -PlacesPanel::PlacesPanel(QWidget* parent) : - Panel(parent), - m_controller(nullptr), - m_model(nullptr), - m_view(nullptr), - m_storageSetupFailedUrl(), - m_triggerStorageSetupButton(), - m_itemDropEventIndex(-1), - m_itemDropEventMimeData(nullptr), - m_itemDropEvent(nullptr), - m_tooltipTimer() -{ - m_tooltipTimer.setInterval(500); - m_tooltipTimer.setSingleShot(true); - connect(&m_tooltipTimer, &QTimer::timeout, this, &PlacesPanel::slotShowTooltip); -} +#include <Solid/StorageAccess> -PlacesPanel::~PlacesPanel() +PlacesPanel::PlacesPanel(QWidget* parent) + : KFilePlacesView(parent) { -} + setDropOnPlaceEnabled(true); + connect(this, &PlacesPanel::urlsDropped, + this, &PlacesPanel::slotUrlsDropped); -void PlacesPanel::proceedWithTearDown() -{ - m_model->proceedWithTearDown(); -} + setAutoResizeItemsEnabled(false); -bool PlacesPanel::urlChanged() -{ - if (!url().isValid() || url().scheme().contains(QLatin1String("search"))) { - // Skip results shown by a search, as possible identical - // directory names are useless without parent-path information. - return false; - } + setTeardownFunction([this](const QModelIndex &index) { + slotTearDownRequested(index); + }); - if (m_controller) { - selectItem(); - } + m_configureTrashAction = new QAction(QIcon::fromTheme(QStringLiteral("configure")), i18nc("@action:inmenu", "Configure Trash…")); + m_configureTrashAction->setPriority(QAction::HighPriority); + connect(m_configureTrashAction, &QAction::triggered, this, &PlacesPanel::slotConfigureTrash); + addAction(m_configureTrashAction); - return true; -} + connect(this, &PlacesPanel::contextMenuAboutToShow, this, &PlacesPanel::slotContextMenuAboutToShow); -void PlacesPanel::readSettings() -{ - if (m_controller) { - const int delay = GeneralSettings::autoExpandFolders() ? 750 : -1; - m_controller->setAutoActivationDelay(delay); - } + connect(this, &PlacesPanel::iconSizeChanged, this, [this](const QSize &newSize) { + int iconSize = qMin(newSize.width(), newSize.height()); + if (iconSize == 0) { + // Don't store 0 size, let's keep -1 for default/small/automatic + iconSize = -1; + } + PlacesPanelSettings* settings = PlacesPanelSettings::self(); + settings->setIconSize(iconSize); + settings->save(); + }); } -void PlacesPanel::showEvent(QShowEvent* event) -{ - if (event->spontaneous()) { - Panel::showEvent(event); - return; - } - - if (!m_controller) { - // Postpone the creating of the controller to the first show event. - // This assures that no performance and memory overhead is given when the folders panel is not - // used at all and stays invisible. - m_model = new PlacesItemModel(this); - m_model->setGroupedSorting(true); - connect(m_model, &PlacesItemModel::errorMessage, - this, &PlacesPanel::errorMessage); - connect(m_model, &PlacesItemModel::storageTearDownRequested, - this, &PlacesPanel::storageTearDownRequested); - connect(m_model, &PlacesItemModel::storageTearDownExternallyRequested, - this, &PlacesPanel::storageTearDownExternallyRequested); - connect(m_model, &PlacesItemModel::storageTearDownSuccessful, - this, &PlacesPanel::storageTearDownSuccessful); - - m_view = new PlacesView(); - m_view->setWidgetCreator(new KItemListWidgetCreator<PlacesItemListWidget>()); - m_view->setGroupHeaderCreator(new KItemListGroupHeaderCreator<PlacesItemListGroupHeader>()); - - installEventFilter(this); - - m_controller = new KItemListController(m_model, m_view, this); - m_controller->setSelectionBehavior(KItemListController::SingleSelection); - m_controller->setSingleClickActivationEnforced(true); - - readSettings(); - - connect(m_controller, &KItemListController::itemActivated, this, &PlacesPanel::slotItemActivated); - connect(m_controller, &KItemListController::itemMiddleClicked, this, &PlacesPanel::slotItemMiddleClicked); - connect(m_controller, &KItemListController::itemContextMenuRequested, this, &PlacesPanel::slotItemContextMenuRequested); - connect(m_controller, &KItemListController::viewContextMenuRequested, this, &PlacesPanel::slotViewContextMenuRequested); - connect(m_controller, &KItemListController::itemDropEvent, this, &PlacesPanel::slotItemDropEvent); - connect(m_controller, &KItemListController::aboveItemDropEvent, this, &PlacesPanel::slotAboveItemDropEvent); +PlacesPanel::~PlacesPanel() = default; - KItemListContainer* container = new KItemListContainer(m_controller, this); - container->setEnabledFrame(false); - - QVBoxLayout* layout = new QVBoxLayout(this); - layout->setContentsMargins(0, 0, 0, 0); - layout->addWidget(container); - - selectItem(); - } - - Panel::showEvent(event); -} - -bool PlacesPanel::eventFilter(QObject * /* obj */, QEvent *event) +void PlacesPanel::setUrl(const QUrl &url) { - if (event->type() == QEvent::ToolTip) { - - QHelpEvent *hoverEvent = reinterpret_cast<QHelpEvent *>(event); - - m_hoveredIndex = m_view->itemAt(hoverEvent->pos()); - m_hoverPos = mapToGlobal(hoverEvent->pos()); - - m_tooltipTimer.start(); - return true; - } - return false; + // KFilePlacesView::setUrl no-ops when no model is set but we only set it in showEvent() + // Remember the URL and set it in showEvent + m_url = url; + KFilePlacesView::setUrl(url); } -void PlacesPanel::slotItemActivated(int index) +QList<QAction*> PlacesPanel::customContextMenuActions() const { - triggerItem(index, Qt::LeftButton); + return m_customContextMenuActions; } -void PlacesPanel::slotItemMiddleClicked(int index) +void PlacesPanel::setCustomContextMenuActions(const QList<QAction *> &actions) { - triggerItem(index, Qt::MiddleButton); + m_customContextMenuActions = actions; } -void PlacesPanel::slotItemContextMenuRequested(int index, const QPointF& pos) +void PlacesPanel::proceedWithTearDown() { - PlacesItem* item = m_model->placesItem(index); - if (!item) { - return; - } - - QMenu menu(this); - - QAction* emptyTrashAction = nullptr; - QAction* configureTrashAction = nullptr; - QAction* editAction = nullptr; - QAction* teardownAction = nullptr; - QAction* ejectAction = nullptr; - QAction* mountAction = nullptr; - - const bool isDevice = !item->udi().isEmpty(); - const bool isTrash = (item->url().scheme() == QLatin1String("trash")); - if (isTrash) { - emptyTrashAction = menu.addAction(QIcon::fromTheme(QStringLiteral("trash-empty")), i18nc("@action:inmenu", "Empty Trash")); - emptyTrashAction->setEnabled(item->icon() == QLatin1String("user-trash-full")); - menu.addSeparator(); - } - - QAction* openInNewTabAction = menu.addAction(QIcon::fromTheme(QStringLiteral("tab-new")), i18nc("@item:inmenu", "Open in New Tab")); - QAction* openInNewWindowAction = menu.addAction(QIcon::fromTheme(QStringLiteral("window-new")), i18nc("@item:inmenu", "Open in New Window")); - QAction* propertiesAction = nullptr; - if (item->url().isLocalFile()) { - propertiesAction = menu.addAction(QIcon::fromTheme(QStringLiteral("document-properties")), i18nc("@action:inmenu", "Properties")); - } - if (!isDevice) { - menu.addSeparator(); - } - - if (isDevice) { - ejectAction = m_model->ejectAction(index); - if (ejectAction) { - ejectAction->setParent(&menu); - menu.addAction(ejectAction); - } - - teardownAction = m_model->teardownAction(index); - if (teardownAction) { - // Disable teardown option for root and home partitions - bool teardownEnabled = item->url() != QUrl::fromLocalFile(QDir::rootPath()); - if (teardownEnabled) { - KMountPoint::Ptr mountPoint = KMountPoint::currentMountPoints().findByPath(QDir::homePath()); - if (mountPoint && item->url() == QUrl::fromLocalFile(mountPoint->mountPoint())) { - teardownEnabled = false; - } - } - teardownAction->setEnabled(teardownEnabled); - - teardownAction->setParent(&menu); - menu.addAction(teardownAction); - } + Q_ASSERT(m_deviceToTearDown); - if (item->storageSetupNeeded()) { - mountAction = menu.addAction(QIcon::fromTheme(QStringLiteral("media-mount")), i18nc("@action:inmenu", "Mount")); - } + connect(m_deviceToTearDown, &Solid::StorageAccess::teardownDone, + this, &PlacesPanel::slotTearDownDone); + m_deviceToTearDown->teardown(); +} - if (teardownAction || ejectAction || mountAction) { - menu.addSeparator(); +void PlacesPanel::readSettings() +{ + if (GeneralSettings::autoExpandFolders()) { + if (!m_dragActivationTimer) { + m_dragActivationTimer = new QTimer(this); + m_dragActivationTimer->setInterval(750); + m_dragActivationTimer->setSingleShot(true); + connect(m_dragActivationTimer, &QTimer::timeout, + this, &PlacesPanel::slotDragActivationTimeout); } + } else { + delete m_dragActivationTimer; + m_dragActivationTimer = nullptr; + m_pendingDragActivation = QPersistentModelIndex(); } - if (isTrash) { - configureTrashAction = menu.addAction(QIcon::fromTheme(QStringLiteral("configure")), i18nc("@action:inmenu", "Configure Trash...")); - } - - if (!isDevice) { - editAction = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-entry")), i18nc("@item:inmenu", "Edit...")); - } + const int iconSize = qMax(0, PlacesPanelSettings::iconSize()); + setIconSize(QSize(iconSize, iconSize)); +} - QAction* removeAction = nullptr; - if (!isDevice && !item->isSystemItem()) { - removeAction = menu.addAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18nc("@item:inmenu", "Remove")); - } +void PlacesPanel::showEvent(QShowEvent* event) +{ + if (!event->spontaneous() && !model()) { + readSettings(); - QAction* hideAction = menu.addAction(QIcon::fromTheme(QStringLiteral("view-hidden")), i18nc("@item:inmenu", "Hide")); - hideAction->setCheckable(true); - hideAction->setChecked(item->isHidden()); + auto *placesModel = DolphinPlacesModelSingleton::instance().placesModel(); + setModel(placesModel); - buildGroupContextMenu(&menu, index); + connect(placesModel, &KFilePlacesModel::errorMessage, this, &PlacesPanel::errorMessage); - QAction* action = menu.exec(pos.toPoint()); - if (action) { - if (action == emptyTrashAction) { - Trash::empty(this); - } else if (action == configureTrashAction) { - DolphinSettingsDialog* settingsDialog = new DolphinSettingsDialog(item->url(), this); - settingsDialog->setCurrentPage(settingsDialog->trashSettings); - settingsDialog->setAttribute(Qt::WA_DeleteOnClose); - settingsDialog->show(); - } else { - // The index might have changed if devices were added/removed while - // the context menu was open. - index = m_model->index(item); - if (index < 0) { - // The item is not in the model any more, probably because it was an - // external device that has been removed while the context menu was open. - return; - } + connect(placesModel, &QAbstractItemModel::rowsInserted, this, &PlacesPanel::slotRowsInserted); + connect(placesModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &PlacesPanel::slotRowsAboutToBeRemoved); - if (action == editAction) { - editEntry(index); - } else if (action == removeAction) { - m_model->deleteItem(index); - } else if (action == hideAction) { - item->setHidden(hideAction->isChecked()); - if (!m_model->hiddenCount()) { - showHiddenEntries(false); - } - } else if (action == openInNewWindowAction) { - Dolphin::openNewWindow({KFilePlacesModel::convertedUrl(m_model->data(index).value("url").toUrl())}, this); - } else if (action == openInNewTabAction) { - // TriggerItem does set up the storage first and then it will - // emit the slotItemMiddleClicked signal, because of Qt::MiddleButton. - triggerItem(index, Qt::MiddleButton); - } else if (action == mountAction) { - m_model->requestStorageSetup(index); - } else if (action == teardownAction) { - m_model->requestTearDown(index); - } else if (action == ejectAction) { - m_model->requestEject(index); - } else if (action == propertiesAction) { - KPropertiesDialog* dialog = new KPropertiesDialog(item->url(), this); - dialog->setAttribute(Qt::WA_DeleteOnClose); - dialog->show(); - } + for (int i = 0; i < model()->rowCount(); ++i) { + connectDeviceSignals(model()->index(i, 0, QModelIndex())); } + + setUrl(m_url); } - selectItem(); + KFilePlacesView::showEvent(event); } -void PlacesPanel::slotViewContextMenuRequested(const QPointF& pos) +void PlacesPanel::dragMoveEvent(QDragMoveEvent *event) { - QMenu menu(this); - - QAction* addAction = menu.addAction(QIcon::fromTheme(QStringLiteral("document-new")), i18nc("@item:inmenu", "Add Entry...")); - - QAction* showAllAction = menu.addAction(i18nc("@item:inmenu", "Show Hidden Places")); - showAllAction->setCheckable(true); - showAllAction->setChecked(m_model->hiddenItemsShown()); - showAllAction->setIcon(QIcon::fromTheme(m_model->hiddenItemsShown() ? QStringLiteral("view-visible") : QStringLiteral("view-hidden"))); - showAllAction->setEnabled(m_model->hiddenCount()); - - buildGroupContextMenu(&menu, m_controller->indexCloseToMousePressedPosition()); - - QMenu* iconSizeSubMenu = new QMenu(i18nc("@item:inmenu", "Icon Size"), &menu); - - struct IconSizeInfo - { - int size; - const char* context; - const char* text; - }; - - const int iconSizeCount = 4; - static const IconSizeInfo iconSizes[iconSizeCount] = { - {KIconLoader::SizeSmall, I18NC_NOOP("Small icon size", "Small (%1x%2)")}, - {KIconLoader::SizeSmallMedium, I18NC_NOOP("Medium icon size", "Medium (%1x%2)")}, - {KIconLoader::SizeMedium, I18NC_NOOP("Large icon size", "Large (%1x%2)")}, - {KIconLoader::SizeLarge, I18NC_NOOP("Huge icon size", "Huge (%1x%2)")} - }; - - QHash<QAction*, int> iconSizeActionMap; - QActionGroup* iconSizeGroup = new QActionGroup(iconSizeSubMenu); - - for (int i = 0; i < iconSizeCount; ++i) { - const int size = iconSizes[i].size; - const QString text = i18nc(iconSizes[i].context, iconSizes[i].text, - size, size); + KFilePlacesView::dragMoveEvent(event); - QAction* action = iconSizeSubMenu->addAction(text); - iconSizeActionMap.insert(action, size); - action->setActionGroup(iconSizeGroup); - action->setCheckable(true); - action->setChecked(m_view->iconSize() == size); + if (!m_dragActivationTimer) { + return; } - menu.addMenu(iconSizeSubMenu); - - menu.addSeparator(); - const auto actions = customContextMenuActions(); - for (QAction* action : actions) { - menu.addAction(action); + const QModelIndex index = indexAt(event->pos()); + if (!index.isValid()) { + return; } - QAction* action = menu.exec(pos.toPoint()); - if (action) { - if (action == addAction) { - addEntry(); - } else if (action == showAllAction) { - showHiddenEntries(showAllAction->isChecked()); - } else if (iconSizeActionMap.contains(action)) { - m_view->setIconSize(iconSizeActionMap.value(action)); - } + QPersistentModelIndex persistentIndex(index); + if (!m_pendingDragActivation.isValid() || m_pendingDragActivation != persistentIndex) { + m_pendingDragActivation = persistentIndex; + m_dragActivationTimer->start(); } - - selectItem(); } -QAction *PlacesPanel::buildGroupContextMenu(QMenu *menu, int index) +void PlacesPanel::dragLeaveEvent(QDragLeaveEvent *event) { - if (index == -1) { - return nullptr; - } - - KFilePlacesModel::GroupType groupType = m_model->groupType(index); - QAction *hideGroupAction = menu->addAction(QIcon::fromTheme(QStringLiteral("view-hidden")), i18nc("@item:inmenu", "Hide Section '%1'", m_model->item(index)->group())); - hideGroupAction->setCheckable(true); - hideGroupAction->setChecked(m_model->isGroupHidden(groupType)); + KFilePlacesView::dragLeaveEvent(event); - connect(hideGroupAction, &QAction::triggered, this, [this, groupType, hideGroupAction]{ - m_model->setGroupHidden(groupType, hideGroupAction->isChecked()); - if (!m_model->hiddenCount()) { - showHiddenEntries(false); - } - }); - - return hideGroupAction; + if (m_dragActivationTimer) { + m_dragActivationTimer->stop(); + m_pendingDragActivation = QPersistentModelIndex(); + } } -void PlacesPanel::slotItemDropEvent(int index, QGraphicsSceneDragDropEvent* event) +void PlacesPanel::dropEvent(QDropEvent *event) { - if (index < 0) { - return; - } - - const PlacesItem* destItem = m_model->placesItem(index); + KFilePlacesView::dropEvent(event); - if (destItem->isSearchOrTimelineUrl()) { - return; + if (m_dragActivationTimer) { + m_dragActivationTimer->stop(); + m_pendingDragActivation = QPersistentModelIndex(); } - - if (m_model->storageSetupNeeded(index)) { - connect(m_model, &PlacesItemModel::storageSetupDone, - this, &PlacesPanel::slotItemDropEventStorageSetupDone); - - m_itemDropEventIndex = index; - - // Make a full copy of the Mime-Data - m_itemDropEventMimeData = new QMimeData; - m_itemDropEventMimeData->setText(event->mimeData()->text()); - m_itemDropEventMimeData->setHtml(event->mimeData()->html()); - m_itemDropEventMimeData->setUrls(event->mimeData()->urls()); - m_itemDropEventMimeData->setImageData(event->mimeData()->imageData()); - m_itemDropEventMimeData->setColorData(event->mimeData()->colorData()); - - m_itemDropEvent = new QDropEvent(event->pos().toPoint(), - event->possibleActions(), - m_itemDropEventMimeData, - event->buttons(), - event->modifiers()); - - m_model->requestStorageSetup(index); - return; - } - - QUrl destUrl = destItem->url(); - QDropEvent dropEvent(event->pos().toPoint(), - event->possibleActions(), - event->mimeData(), - event->buttons(), - event->modifiers()); - - slotUrlsDropped(destUrl, &dropEvent, this); } -void PlacesPanel::slotItemDropEventStorageSetupDone(int index, bool success) +void PlacesPanel::slotConfigureTrash() { - disconnect(m_model, &PlacesItemModel::storageSetupDone, - this, &PlacesPanel::slotItemDropEventStorageSetupDone); - - if ((index == m_itemDropEventIndex) && m_itemDropEvent && m_itemDropEventMimeData) { - if (success) { - QUrl destUrl = m_model->placesItem(index)->url(); - slotUrlsDropped(destUrl, m_itemDropEvent, this); - } + const QUrl url = currentIndex().data(KFilePlacesModel::UrlRole).toUrl(); - delete m_itemDropEventMimeData; - delete m_itemDropEvent; - - m_itemDropEventIndex = -1; - m_itemDropEventMimeData = nullptr; - m_itemDropEvent = nullptr; - } + DolphinSettingsDialog* settingsDialog = new DolphinSettingsDialog(url, this); + settingsDialog->setCurrentPage(settingsDialog->trashSettings); + settingsDialog->setAttribute(Qt::WA_DeleteOnClose); + settingsDialog->show(); } -void PlacesPanel::slotAboveItemDropEvent(int index, QGraphicsSceneDragDropEvent* event) +void PlacesPanel::slotDragActivationTimeout() { - m_model->dropMimeDataBefore(index, event->mimeData()); + if (!m_pendingDragActivation.isValid()) { + return; + } + + auto *placesModel = static_cast<KFilePlacesModel *>(model()); + Q_EMIT placeActivated(KFilePlacesModel::convertedUrl(placesModel->url(m_pendingDragActivation))); } void PlacesPanel::slotUrlsDropped(const QUrl& dest, QDropEvent* event, QWidget* parent) @@ -475,123 +203,121 @@ void PlacesPanel::slotUrlsDropped(const QUrl& dest, QDropEvent* event, QWidget* } } -void PlacesPanel::slotStorageSetupDone(int index, bool success) +void PlacesPanel::slotContextMenuAboutToShow(const QModelIndex &index, QMenu *menu) { - disconnect(m_model, &PlacesItemModel::storageSetupDone, - this, &PlacesPanel::slotStorageSetupDone); + Q_UNUSED(menu); - if (m_triggerStorageSetupButton == Qt::NoButton) { - return; - } + auto *placesModel = static_cast<KFilePlacesModel *>(model()); + const QUrl url = placesModel->url(index); + const Solid::Device device = placesModel->deviceForIndex(index); - if (success) { - Q_ASSERT(!m_model->storageSetupNeeded(index)); - triggerItem(index, m_triggerStorageSetupButton); - m_triggerStorageSetupButton = Qt::NoButton; + m_configureTrashAction->setVisible(url.scheme() == QLatin1String("trash")); + + // show customContextMenuActions only on the view's context menu + if (!url.isValid() && !device.isValid()) { + addActions(m_customContextMenuActions); } else { - setUrl(m_storageSetupFailedUrl); - m_storageSetupFailedUrl = QUrl(); + const auto actions = this->actions(); + for (QAction *action : actions) { + if (m_customContextMenuActions.contains(action)) { + removeAction(action); + } + } } } -void PlacesPanel::slotShowTooltip() -{ - const QUrl url = m_model->data(m_hoveredIndex).value("url").value<QUrl>(); - const QString text = url.toDisplayString(QUrl::PreferLocalFile); - QToolTip::showText(m_hoverPos, text); -} - -void PlacesPanel::addEntry() +void PlacesPanel::slotTearDownRequested(const QModelIndex &index) { - const int index = m_controller->selectionManager()->currentItem(); - const QUrl url = m_model->data(index).value("url").toUrl(); - const QString text = url.fileName().isEmpty() ? url.toDisplayString(QUrl::PreferLocalFile) : url.fileName(); + auto *placesModel = static_cast<KFilePlacesModel *>(model()); - QPointer<KFilePlaceEditDialog> dialog = new KFilePlaceEditDialog(true, url, text, QString(), true, false, KIconLoader::SizeMedium, this); - if (dialog->exec() == QDialog::Accepted) { - const QString appName = dialog->applicationLocal() ? QCoreApplication::applicationName() : QString(); - m_model->createPlacesItem(dialog->label(), dialog->url(), dialog->icon(), appName); + Solid::StorageAccess *storageAccess = placesModel->deviceForIndex(index).as<Solid::StorageAccess>(); + if (!storageAccess) { + return; } - delete dialog; + m_deviceToTearDown = storageAccess; + + // disconnect the Solid::StorageAccess::teardownRequested + // to prevent emitting PlacesPanel::storageTearDownExternallyRequested + // after we have emitted PlacesPanel::storageTearDownRequested + disconnect(storageAccess, &Solid::StorageAccess::teardownRequested, this, &PlacesPanel::slotTearDownRequestedExternally); + Q_EMIT storageTearDownRequested(storageAccess->filePath()); } -void PlacesPanel::editEntry(int index) +void PlacesPanel::slotTearDownRequestedExternally(const QString &udi) { - QHash<QByteArray, QVariant> data = m_model->data(index); - const QUrl url = data.value("url").toUrl(); - const QString text = data.value("text").toString(); - const QString iconName = data.value("iconName").toString(); - const bool applicationLocal = !data.value("applicationName").toString().isEmpty(); - - QPointer<KFilePlaceEditDialog> dialog = new KFilePlaceEditDialog(true, url, text, iconName, true, applicationLocal, KIconLoader::SizeMedium, this); - if (dialog->exec() == QDialog::Accepted) { - PlacesItem* oldItem = m_model->placesItem(index); - if (oldItem) { - const QString appName = dialog->applicationLocal() ? QCoreApplication::applicationName() : QString(); - oldItem->setApplicationName(appName); - oldItem->setText(dialog->label()); - oldItem->setUrl(dialog->url()); - oldItem->setIcon(dialog->icon()); - m_model->refresh(); - } - } + Q_UNUSED(udi); + auto *storageAccess = static_cast<Solid::StorageAccess*>(sender()); - delete dialog; + Q_EMIT storageTearDownExternallyRequested(storageAccess->filePath()); } -void PlacesPanel::selectItem() +void PlacesPanel::slotTearDownDone(Solid::ErrorType error, const QVariant& errorData) { - const int index = m_model->closestItem(url()); - KItemListSelectionManager* selectionManager = m_controller->selectionManager(); - selectionManager->setCurrentItem(index); - selectionManager->clearSelection(); - - const QUrl closestUrl = m_model->url(index); - if (!closestUrl.path().isEmpty() && url() == closestUrl) { - selectionManager->setSelected(index); + if (error && errorData.isValid()) { + if (error == Solid::ErrorType::DeviceBusy) { + KListOpenFilesJob* listOpenFilesJob = new KListOpenFilesJob(m_deviceToTearDown->filePath()); + connect(listOpenFilesJob, &KIO::Job::result, this, [this, listOpenFilesJob](KJob*) { + const KProcessList::KProcessInfoList blockingProcesses = listOpenFilesJob->processInfoList(); + QString errorString; + if (blockingProcesses.isEmpty()) { + errorString = i18n("One or more files on this device are open within an application."); + } else { + QStringList blockingApps; + for (const auto& process : blockingProcesses) { + blockingApps << process.name(); + } + blockingApps.removeDuplicates(); + errorString = xi18np("One or more files on this device are opened in application <application>\"%2\"</application>.", + "One or more files on this device are opened in following applications: <application>%2</application>.", + blockingApps.count(), blockingApps.join(i18nc("separator in list of apps blocking device unmount", ", "))); + } + Q_EMIT errorMessage(errorString); + }); + listOpenFilesJob->start(); + } else { + Q_EMIT errorMessage(errorData.toString()); + } + } else { + // No error; it must have been unmounted successfully + Q_EMIT storageTearDownSuccessful(); } + disconnect(m_deviceToTearDown, &Solid::StorageAccess::teardownDone, + this, &PlacesPanel::slotTearDownDone); + m_deviceToTearDown = nullptr; } -void PlacesPanel::triggerItem(int index, Qt::MouseButton button) +void PlacesPanel::slotRowsInserted(const QModelIndex &parent, int first, int last) { - const PlacesItem* item = m_model->placesItem(index); - if (!item) { - return; + for (int i = first; i <= last; ++i) { + connectDeviceSignals(model()->index(first, 0, parent)); } +} - if (m_model->storageSetupNeeded(index)) { - m_triggerStorageSetupButton = button; - m_storageSetupFailedUrl = url(); - - connect(m_model, &PlacesItemModel::storageSetupDone, - this, &PlacesPanel::slotStorageSetupDone); +void PlacesPanel::slotRowsAboutToBeRemoved(const QModelIndex &parent, int first, int last) +{ + auto *placesModel = static_cast<KFilePlacesModel *>(model()); - m_model->requestStorageSetup(index); - } else { - m_triggerStorageSetupButton = Qt::NoButton; + for (int i = first; i <= last; ++i) { + const QModelIndex index = placesModel->index(i, 0, parent); - const QUrl url = m_model->data(index).value("url").toUrl(); - if (!url.isEmpty()) { - if (button == Qt::MiddleButton) { - Q_EMIT placeMiddleClicked(KFilePlacesModel::convertedUrl(url)); - } else { - Q_EMIT placeActivated(KFilePlacesModel::convertedUrl(url)); - } + Solid::StorageAccess *storageAccess = placesModel->deviceForIndex(index).as<Solid::StorageAccess>(); + if (!storageAccess) { + continue; } + + disconnect(storageAccess, &Solid::StorageAccess::teardownRequested, this, nullptr); } } -void PlacesPanel::showHiddenEntries(bool shown) +void PlacesPanel::connectDeviceSignals(const QModelIndex &index) { - m_model->setHiddenItemsShown(shown); - Q_EMIT showHiddenEntriesChanged(shown); -} + auto *placesModel = static_cast<KFilePlacesModel *>(model()); -int PlacesPanel::hiddenListCount() -{ - if(!m_model) { - return 0; + Solid::StorageAccess *storageAccess = placesModel->deviceForIndex(index).as<Solid::StorageAccess>(); + if (!storageAccess) { + return; } - return m_model->hiddenCount(); + + connect(storageAccess, &Solid::StorageAccess::teardownRequested, this, &PlacesPanel::slotTearDownRequestedExternally); } diff --git a/src/panels/places/placespanel.h b/src/panels/places/placespanel.h index 39f8da365..570fc43be 100644 --- a/src/panels/places/placespanel.h +++ b/src/panels/places/placespanel.h @@ -1,6 +1,7 @@ /* * SPDX-FileCopyrightText: 2008-2012 Peter Penz <[email protected]> * SPDX-FileCopyrightText: 2010 Christian Muehlhaeuser <[email protected]> + * SPDX-FileCopyrightText: 2021 Kai Uwe Broulik <[email protected]> * * SPDX-License-Identifier: GPL-2.0-or-later */ @@ -11,86 +12,76 @@ #include "panels/panel.h" #include <QUrl> -#include <QTimer> +#include <KFilePlacesView> + +#include <Solid/SolidNamespace> // Solid::ErrorType + +class QTimer; +namespace Solid +{ +class StorageAccess; +} -class KItemListController; -class PlacesItemModel; -class PlacesView; -class QGraphicsSceneDragDropEvent; -class QMenu; -class QMimeData; /** * @brief Combines bookmarks and mounted devices as list. */ -class PlacesPanel : public Panel +class PlacesPanel : public KFilePlacesView { Q_OBJECT public: explicit PlacesPanel(QWidget* parent); ~PlacesPanel() override; + + void setUrl(const QUrl &url); // override + + // for compatibility with Panel, actions that are shown + // on the view's context menu + QList<QAction*> customContextMenuActions() const; + void setCustomContextMenuActions(const QList<QAction*>& actions); + + void requestTearDown(); void proceedWithTearDown(); - bool eventFilter(QObject *obj, QEvent *event) override; +public Q_SLOTS: + void readSettings(); Q_SIGNALS: - void placeActivated(const QUrl& url); - void placeMiddleClicked(const QUrl& url); void errorMessage(const QString& error); void storageTearDownRequested(const QString& mountPath); void storageTearDownExternallyRequested(const QString& mountPath); - void showHiddenEntriesChanged(bool shown); void storageTearDownSuccessful(); protected: - bool urlChanged() override; void showEvent(QShowEvent* event) override; - -public Q_SLOTS: - void readSettings() override; - void showHiddenEntries(bool shown); - int hiddenListCount(); + void dragMoveEvent(QDragMoveEvent *event) override; + void dragLeaveEvent(QDragLeaveEvent *event) override; + void dropEvent(QDropEvent *event) override; private Q_SLOTS: - void slotItemActivated(int index); - void slotItemMiddleClicked(int index); - void slotItemContextMenuRequested(int index, const QPointF& pos); - void slotViewContextMenuRequested(const QPointF& pos); - void slotItemDropEvent(int index, QGraphicsSceneDragDropEvent* event); - void slotItemDropEventStorageSetupDone(int index, bool success); - void slotAboveItemDropEvent(int index, QGraphicsSceneDragDropEvent* event); + void slotConfigureTrash(); + void slotDragActivationTimeout(); void slotUrlsDropped(const QUrl& dest, QDropEvent* event, QWidget* parent); - void slotStorageSetupDone(int index, bool success); - void slotShowTooltip(); + void slotContextMenuAboutToShow(const QModelIndex &index, QMenu *menu); + void slotTearDownRequested(const QModelIndex &index); + void slotTearDownRequestedExternally(const QString &udi); + void slotTearDownDone(Solid::ErrorType error, const QVariant& errorData); + void slotRowsInserted(const QModelIndex &parent, int first, int last); + void slotRowsAboutToBeRemoved(const QModelIndex &parent, int first, int last); private: - void addEntry(); - void editEntry(int index); + void connectDeviceSignals(const QModelIndex &idx); - /** - * Selects the item that matches the URL set - * for the panel (see Panel::setUrl()). - */ - void selectItem(); + QUrl m_url; // only used for initial setUrl + QList<QAction*> m_customContextMenuActions; - void triggerItem(int index, Qt::MouseButton button); - - QAction* buildGroupContextMenu(QMenu* menu, int index); - -private: - KItemListController* m_controller; - PlacesItemModel* m_model; - PlacesView* m_view; + QTimer *m_dragActivationTimer = nullptr; + QPersistentModelIndex m_pendingDragActivation; - QUrl m_storageSetupFailedUrl; - Qt::MouseButton m_triggerStorageSetupButton; + Solid::StorageAccess *m_deviceToTearDown = nullptr; - int m_itemDropEventIndex; - QMimeData* m_itemDropEventMimeData; - QDropEvent* m_itemDropEvent; - QTimer m_tooltipTimer; - int m_hoveredIndex; - QPoint m_hoverPos; + QAction *m_configureTrashAction; + QAction *m_lockPanelsAction; }; #endif // PLACESPANEL_H diff --git a/src/panels/places/placesview.cpp b/src/panels/places/placesview.cpp deleted file mode 100644 index dc264e411..000000000 --- a/src/panels/places/placesview.cpp +++ /dev/null @@ -1,49 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2012 Frank Reininghaus <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#include "placesview.h" - -#include "dolphin_placespanelsettings.h" -#include "kitemviews/kitemlistheader.h" - -#include <QGraphicsSceneResizeEvent> - -PlacesView::PlacesView(QGraphicsWidget* parent) : - KStandardItemListView(parent) -{ - header()->setAutomaticColumnResizing(false); - - const int iconSize = PlacesPanelSettings::iconSize(); - if (iconSize >= 0) { - setIconSize(iconSize); - } -} - -void PlacesView::setIconSize(int size) -{ - if (size != iconSize()) { - PlacesPanelSettings* settings = PlacesPanelSettings::self(); - settings->setIconSize(size); - settings->save(); - - KItemListStyleOption option = styleOption(); - option.iconSize = size; - setStyleOption(option); - } -} - -int PlacesView::iconSize() const -{ - const KItemListStyleOption option = styleOption(); - return option.iconSize; -} - -void PlacesView::resizeEvent(QGraphicsSceneResizeEvent *event) -{ - KStandardItemListView::resizeEvent(event); - - header()->setColumnWidth(QByteArrayLiteral("text"), event->newSize().width()); -} diff --git a/src/panels/places/placesview.h b/src/panels/places/placesview.h deleted file mode 100644 index 86515f56b..000000000 --- a/src/panels/places/placesview.h +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2012 Frank Reininghaus <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#ifndef PLACESVIEW_H -#define PLACESVIEW_H - -#include "kitemviews/kstandarditemlistview.h" - -/** - * @brief View class for the Places Panel. - * - * Reads the icon size from GeneralSettings::placesPanelIconSize(). - */ -class PlacesView : public KStandardItemListView -{ - Q_OBJECT - -public: - explicit PlacesView(QGraphicsWidget* parent = nullptr); - - void setIconSize(int size); - int iconSize() const; - -protected: - void resizeEvent(QGraphicsSceneResizeEvent *event) override; -}; - -#endif diff --git a/src/panels/terminal/terminalpanel.cpp b/src/panels/terminal/terminalpanel.cpp index 3af2cdcad..75bfe93d0 100644 --- a/src/panels/terminal/terminalpanel.cpp +++ b/src/panels/terminal/terminalpanel.cpp @@ -6,6 +6,7 @@ #include "terminalpanel.h" +#include <KActionCollection> #include <KIO/DesktopExecParser> #include <KIO/Job> #include <KIO/JobUiDelegate> @@ -15,9 +16,10 @@ #include <KMountPoint> #include <KParts/ReadOnlyPart> #include <KPluginFactory> -#include <KPluginLoader> #include <KProtocolInfo> #include <KShell> +#include <KXMLGUIBuilder> +#include <KXMLGUIFactory> #include <kde_terminal_interface.h> #include <QAction> @@ -101,6 +103,21 @@ QString TerminalPanel::runningProgramName() const return m_terminal ? m_terminal->foregroundProcessName() : QString(); } +KActionCollection *TerminalPanel::actionCollection() +{ + // m_terminal is the only reference reset to nullptr in case the terminal is + // closed again + if (m_terminal && m_konsolePart && m_terminalWidget) { + const auto guiClients = m_konsolePart->childClients(); + for (auto *client : guiClients) { + if (client->actionCollection()->associatedWidgets().contains(m_terminalWidget)) { + return client->actionCollection(); + } + } + } + return nullptr; +} + bool TerminalPanel::hasProgramRunning() const { return m_terminal && (m_terminal->foregroundProcessId() != -1); @@ -129,8 +146,7 @@ void TerminalPanel::showEvent(QShowEvent* event) if (!m_terminal) { m_clearTerminal = true; - KPluginLoader loader(QStringLiteral("konsolepart")); - KPluginFactory* factory = loader.factory(); + KPluginFactory *factory = KPluginFactory::loadFactory(KPluginMetaData(QStringLiteral("konsolepart"))).plugin; m_konsolePart = factory ? (factory->create<KParts::ReadOnlyPart>(this)) : nullptr; if (m_konsolePart) { connect(m_konsolePart, &KParts::ReadOnlyPart::destroyed, this, &TerminalPanel::terminalExited); @@ -141,6 +157,23 @@ void TerminalPanel::showEvent(QShowEvent* event) m_layout->removeWidget(m_konsolePartMissingMessage); } m_terminal = qobject_cast<TerminalInterface*>(m_konsolePart); + + // needed to collect the correct KonsolePart actionCollection + // namely the one of the single inner terminal and not the outer KonsolePart + if (!m_konsolePart->factory() && m_terminalWidget) { + if (!m_konsolePart->clientBuilder()) { + m_konsolePart->setClientBuilder(new KXMLGUIBuilder(m_terminalWidget)); + } + + auto factory = new KXMLGUIFactory(m_konsolePart->clientBuilder(), this); + factory->addClient(m_konsolePart); + + // Prevents the KXMLGui warning about removing the client + connect(m_terminalWidget, &QObject::destroyed, this, [factory, this] { + factory->removeClient(m_konsolePart); + }); + } + } else if (!m_konsolePartMissingMessage) { const auto konsoleInstallUrl = QUrl("appstream://org.kde.konsole.desktop"); const auto konsoleNotInstalledText = i18n("Terminal cannot be shown because Konsole is not installed. " diff --git a/src/panels/terminal/terminalpanel.h b/src/panels/terminal/terminalpanel.h index dc6605da6..5a2b0bb83 100644 --- a/src/panels/terminal/terminalpanel.h +++ b/src/panels/terminal/terminalpanel.h @@ -7,12 +7,13 @@ #ifndef TERMINALPANEL_H #define TERMINALPANEL_H -#include "panels/panel.h" #include "kiofuse_interface.h" +#include "panels/panel.h" #include <QQueue> class TerminalInterface; +class KActionCollection; class KMessageWidget; class QVBoxLayout; class QWidget; @@ -47,6 +48,7 @@ public: bool terminalHasFocus() const; bool hasProgramRunning() const; QString runningProgramName() const; + KActionCollection *actionCollection(); public Q_SLOTS: void terminalExited(); diff --git a/src/search/dolphinfacetswidget.cpp b/src/search/dolphinfacetswidget.cpp index db53d595f..cc125a2d9 100644 --- a/src/search/dolphinfacetswidget.cpp +++ b/src/search/dolphinfacetswidget.cpp @@ -235,7 +235,7 @@ void DolphinFacetsWidget::initComboBox(QComboBox* combo) combo->setFrame(false); combo->setMinimumHeight(parentWidget()->height()); combo->setCurrentIndex(0); - connect(combo, QOverload<int>::of(&QComboBox::activated), this, &DolphinFacetsWidget::facetChanged); + connect(combo, &QComboBox::activated, this, &DolphinFacetsWidget::facetChanged); } void DolphinFacetsWidget::updateTagsSelector() diff --git a/src/search/dolphinsearchbox.cpp b/src/search/dolphinsearchbox.cpp index 9143ddcb7..860d9f6cd 100644 --- a/src/search/dolphinsearchbox.cpp +++ b/src/search/dolphinsearchbox.cpp @@ -9,8 +9,8 @@ #include "dolphin_searchsettings.h" #include "dolphinfacetswidget.h" +#include "dolphinplacesmodelsingleton.h" #include "dolphinquery.h" -#include "panels/places/placesitemmodel.h" #include <KLocalizedString> #include <KNS3/KMoreToolsMenuFactory> @@ -288,11 +288,8 @@ void DolphinSearchBox::slotSearchSaved() { const QUrl searchURL = urlForSearching(); if (searchURL.isValid()) { - PlacesItemModel model; const QString label = i18n("Search for %1 in %2", text(), searchPath().fileName()); - model.createPlacesItem(label, - searchURL, - QStringLiteral("folder-saved-search-symbolic")); + DolphinPlacesModelSingleton::instance().placesModel()->addPlace(label, searchURL, QStringLiteral("folder-saved-search-symbolic")); } } diff --git a/src/settings/contextmenu/contextmenusettingspage.cpp b/src/settings/contextmenu/contextmenusettingspage.cpp index 8ebac2e12..cec1f9649 100644 --- a/src/settings/contextmenu/contextmenusettingspage.cpp +++ b/src/settings/contextmenu/contextmenusettingspage.cpp @@ -14,13 +14,14 @@ #include "global.h" #include <KDesktopFile> +#include <KDesktopFileActions> +#include <KFileUtils> #include <KLocalizedString> #include <KMessageBox> #include <KNS3/Button> #include <KPluginMetaData> #include <KService> #include <KServiceTypeTrader> -#include <KDesktopFileActions> #include <kio_version.h> @@ -272,10 +273,14 @@ void ContextMenuSettingsPage::loadServices() const KConfigGroup showGroup = config.group("Show"); // Load generic services - const KService::List entries = KServiceTypeTrader::self()->query(QStringLiteral("KonqPopupMenu/Plugin")); - for (const KService::Ptr &service : entries) { - const QString file = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "kservices5/" % service->entryPath()); - const QList<KServiceAction> serviceActions = KDesktopFileActions::userDefinedServices(file, true); + const auto locations = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kio/servicemenus"), QStandardPaths::LocateDirectory); + QStringList files = KFileUtils::findAllUniqueFiles(locations); + const KService::List services = KServiceTypeTrader::self()->query(QStringLiteral("KonqPopupMenu/Plugin")); + for (const KService::Ptr &service : services) { + files << QStandardPaths::locate(QStandardPaths::GenericDataLocation, "kservices5/" % service->entryPath()); + } + for (const auto &file : qAsConst(files)) { + const QList<KServiceAction> serviceActions = KDesktopFileActions::userDefinedServices(KService(file), true); const KDesktopFile desktopFile(file); const QString subMenuName = desktopFile.desktopGroup().readEntry("X-KDE-Submenu"); @@ -307,7 +312,7 @@ void ContextMenuSettingsPage::loadServices() #endif // Load JSON-based plugins that implement the KFileItemActionPlugin interface - const auto jsonPlugins = KPluginLoader::findPlugins(QStringLiteral("kf5/kfileitemaction")); + const auto jsonPlugins = KPluginMetaData::findPlugins(QStringLiteral("kf5/kfileitemaction")); for (const auto &jsonMetadata : jsonPlugins) { const QString desktopEntryName = jsonMetadata.pluginId(); @@ -328,7 +333,7 @@ void ContextMenuSettingsPage::loadVersionControlSystems() // Create a checkbox for each available version control plugin QSet<QString> loadedPlugins; - const QVector<KPluginMetaData> plugins = KPluginLoader::findPlugins(QStringLiteral("dolphin/vcs")); + const QVector<KPluginMetaData> plugins = KPluginMetaData::findPlugins(QStringLiteral("dolphin/vcs")); for (const auto &plugin : plugins) { const QString pluginName = plugin.name(); addRow(QStringLiteral("code-class"), @@ -338,18 +343,6 @@ void ContextMenuSettingsPage::loadVersionControlSystems() loadedPlugins += pluginName; } - const KService::List pluginServices = KServiceTypeTrader::self()->query(QStringLiteral("FileViewVersionControlPlugin")); - for (const auto &plugin : pluginServices) { - const QString pluginName = plugin->name(); - if (loadedPlugins.contains(pluginName)) { - continue; - } - addRow(QStringLiteral("code-class"), - pluginName, - VersionControlServicePrefix + pluginName, - enabledPlugins.contains(pluginName)); - } - m_sortModel->sort(Qt::DisplayRole); } diff --git a/src/settings/contextmenu/servicemenu.knsrc b/src/settings/contextmenu/servicemenu.knsrc index 59d459895..4374082c7 100644 --- a/src/settings/contextmenu/servicemenu.knsrc +++ b/src/settings/contextmenu/servicemenu.knsrc @@ -1,5 +1,5 @@ [KNewStuff3] -ProvidersUrl=https://download.kde.org/ocs/providers.xml +ProvidersUrl=https://autoconfig.kde.org/ocs/providers.xml Categories=Dolphin Service Menus TargetDir=servicemenu-download Uncompress=never diff --git a/src/settings/contextmenu/servicemenuinstaller/CMakeLists.txt b/src/settings/contextmenu/servicemenuinstaller/CMakeLists.txt index 46b159079..da9f83955 100644 --- a/src/settings/contextmenu/servicemenuinstaller/CMakeLists.txt +++ b/src/settings/contextmenu/servicemenuinstaller/CMakeLists.txt @@ -3,8 +3,8 @@ add_definitions(-DTRANSLATION_DOMAIN=\"dolphin_servicemenuinstaller\") add_executable(servicemenuinstaller servicemenuinstaller.cpp) target_link_libraries(servicemenuinstaller PRIVATE - Qt5::Core - Qt5::Gui + Qt${QT_MAJOR_VERSION}::Core + Qt${QT_MAJOR_VERSION}::Gui KF5::I18n KF5::CoreAddons ) diff --git a/src/settings/dolphin_detailsmodesettings.kcfg b/src/settings/dolphin_detailsmodesettings.kcfg index c8238f1e8..2a70cc1ff 100644 --- a/src/settings/dolphin_detailsmodesettings.kcfg +++ b/src/settings/dolphin_detailsmodesettings.kcfg @@ -40,6 +40,10 @@ <label>Position of columns</label> <default>0,1,2,3,4,5,6,7,8</default> </entry> + <entry name="LeadingPadding" type="UInt"> + <label>Leading Column Padding</label> + <default>20</default> + </entry> <entry name="ExpandableFolders" type="Bool"> <label>Expandable folders</label> <default>true</default> diff --git a/src/settings/dolphin_iconsmodesettings.kcfg b/src/settings/dolphin_iconsmodesettings.kcfg index 9b286d139..220dd8c42 100644 --- a/src/settings/dolphin_iconsmodesettings.kcfg +++ b/src/settings/dolphin_iconsmodesettings.kcfg @@ -42,7 +42,7 @@ </entry> <entry name="MaximumTextLines" type="Int"> <label>Maximum textlines (0 means unlimited)</label> - <default>0</default> + <default>3</default> </entry> </group> </kcfg> diff --git a/src/settings/dolphinsettingsdialog.cpp b/src/settings/dolphinsettingsdialog.cpp index 3b7d4b267..d699ef894 100644 --- a/src/settings/dolphinsettingsdialog.cpp +++ b/src/settings/dolphinsettingsdialog.cpp @@ -90,7 +90,7 @@ DolphinSettingsDialog::DolphinSettingsDialog(const QUrl& url, QWidget* parent, K }); KPageWidgetItem* contextMenuSettingsFrame = addPage(contextMenuSettingsPage, i18nc("@title:group", "Context Menu")); - contextMenuSettingsFrame->setIcon(QIcon::fromTheme(QStringLiteral("application-menu"))); + contextMenuSettingsFrame->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-menu-edit"))); connect(contextMenuSettingsPage, &ContextMenuSettingsPage::changed, this, &DolphinSettingsDialog::enableApply); // Trash diff --git a/src/settings/general/configurepreviewplugindialog.cpp b/src/settings/general/configurepreviewplugindialog.cpp index 26b7deb88..b60ba5a0c 100644 --- a/src/settings/general/configurepreviewplugindialog.cpp +++ b/src/settings/general/configurepreviewplugindialog.cpp @@ -11,7 +11,7 @@ #include <KIO/ThumbCreator> #include <KJobWidgets> #include <KLocalizedString> -#include <KPluginLoader> +#include <QPluginLoader> #include <QDialogButtonBox> #include <QPushButton> @@ -25,7 +25,7 @@ ConfigurePreviewPluginDialog::ConfigurePreviewPluginDialog(const QString& plugin QDialog(parent) { QSharedPointer<ThumbCreator> previewPlugin; - const QString pluginPath = KPluginLoader::findPlugin(desktopEntryName); + const QString pluginPath = QPluginLoader(desktopEntryName).fileName(); if (!pluginPath.isEmpty()) { newCreator create = (newCreator)QLibrary::resolve(pluginPath, "new_creator"); if (create) { diff --git a/src/settings/general/previewssettingspage.cpp b/src/settings/general/previewssettingspage.cpp index 564715ae3..f12338aef 100644 --- a/src/settings/general/previewssettingspage.cpp +++ b/src/settings/general/previewssettingspage.cpp @@ -94,8 +94,8 @@ PreviewsSettingsPage::PreviewsSettingsPage(QWidget* parent) : loadSettings(); connect(m_listView, &QListView::clicked, this, &PreviewsSettingsPage::changed); - connect(m_localFileSizeBox, QOverload<int>::of(&QSpinBox::valueChanged), this, &PreviewsSettingsPage::changed); - connect(m_remoteFileSizeBox, QOverload<int>::of(&QSpinBox::valueChanged), this, &PreviewsSettingsPage::changed); + connect(m_localFileSizeBox, &QSpinBox::valueChanged, this, &PreviewsSettingsPage::changed); + connect(m_remoteFileSizeBox, &QSpinBox::valueChanged, this, &PreviewsSettingsPage::changed); } PreviewsSettingsPage::~PreviewsSettingsPage() @@ -168,17 +168,17 @@ void PreviewsSettingsPage::loadPreviewPlugins() { QAbstractItemModel* model = m_listView->model(); - const KService::List plugins = KServiceTypeTrader::self()->query(QStringLiteral("ThumbCreator")); - for (const KService::Ptr& service : plugins) { - const bool configurable = service->property(QStringLiteral("Configurable"), QVariant::Bool).toBool(); - const bool show = m_enabledPreviewPlugins.contains(service->desktopEntryName()); + const QVector<KPluginMetaData> plugins = KIO::PreviewJob::availableThumbnailerPlugins(); + for (const KPluginMetaData &plugin : plugins) { + const bool configurable = plugin.value(QStringLiteral("Configurable"), false); + const bool show = m_enabledPreviewPlugins.contains(plugin.pluginId()); model->insertRow(0); const QModelIndex index = model->index(0, 0); model->setData(index, show, Qt::CheckStateRole); model->setData(index, configurable, ServiceModel::ConfigurableRole); - model->setData(index, service->name(), Qt::DisplayRole); - model->setData(index, service->desktopEntryName(), ServiceModel::DesktopEntryNameRole); + model->setData(index, plugin.name(), Qt::DisplayRole); + model->setData(index, plugin.pluginId(), ServiceModel::DesktopEntryNameRole); } model->sort(Qt::DisplayRole); diff --git a/src/settings/trash/trashsettingspage.cpp b/src/settings/trash/trashsettingspage.cpp index df627fa1c..048ee0e9a 100644 --- a/src/settings/trash/trashsettingspage.cpp +++ b/src/settings/trash/trashsettingspage.cpp @@ -15,12 +15,12 @@ TrashSettingsPage::TrashSettingsPage(QWidget* parent) : { QFormLayout* topLayout = new QFormLayout(this); - m_proxy = new KCModuleProxy(QStringLiteral("kcmtrash")); + m_proxy = new KCModuleProxy(KPluginMetaData(QStringLiteral("kcm_trash"))); topLayout->addRow(m_proxy); loadSettings(); - connect(m_proxy, QOverload<bool>::of(&KCModuleProxy::changed), this, &TrashSettingsPage::changed); + connect(m_proxy, &KCModuleProxy::changed, this, &TrashSettingsPage::changed); } TrashSettingsPage::~TrashSettingsPage() diff --git a/src/settings/viewmodes/dolphinfontrequester.cpp b/src/settings/viewmodes/dolphinfontrequester.cpp index cb66870af..a4663e94a 100644 --- a/src/settings/viewmodes/dolphinfontrequester.cpp +++ b/src/settings/viewmodes/dolphinfontrequester.cpp @@ -27,7 +27,7 @@ DolphinFontRequester::DolphinFontRequester(QWidget* parent) : m_modeCombo = new QComboBox(this); m_modeCombo->addItem(i18nc("@item:inlistbox Font", "System Font")); m_modeCombo->addItem(i18nc("@item:inlistbox Font", "Custom Font")); - connect(m_modeCombo, QOverload<int>::of(&QComboBox::activated), + connect(m_modeCombo, &QComboBox::activated, this, &DolphinFontRequester::changeMode); m_chooseFontButton = new QPushButton(i18nc("@action:button Choose font", "Choose..."), this); diff --git a/src/settings/viewmodes/viewsettingstab.cpp b/src/settings/viewmodes/viewsettingstab.cpp index 7ea8d5809..1e109aab0 100644 --- a/src/settings/viewmodes/viewsettingstab.cpp +++ b/src/settings/viewmodes/viewsettingstab.cpp @@ -112,7 +112,7 @@ ViewSettingsTab::ViewSettingsTab(Mode mode, QWidget* parent) : sortingModeGroup->addButton(m_sizeOfContents); m_recursiveDirectorySizeLimit = new QSpinBox(); - connect(m_recursiveDirectorySizeLimit, QOverload<int>::of(&QSpinBox::valueChanged), this, [this](int value) { + connect(m_recursiveDirectorySizeLimit, &QSpinBox::valueChanged, this, [this](int value) { m_recursiveDirectorySizeLimit->setSuffix(i18np(" level deep", " levels deep", value)); }); m_recursiveDirectorySizeLimit->setRange(1, 20); @@ -155,16 +155,16 @@ ViewSettingsTab::ViewSettingsTab(Mode mode, QWidget* parent) : switch (m_mode) { case IconsMode: - connect(m_widthBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ViewSettingsTab::changed); - connect(m_maxLinesBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ViewSettingsTab::changed); + connect(m_widthBox, &QComboBox::currentIndexChanged, this, &ViewSettingsTab::changed); + connect(m_maxLinesBox, &QComboBox::currentIndexChanged, this, &ViewSettingsTab::changed); break; case CompactMode: - connect(m_widthBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &ViewSettingsTab::changed); + connect(m_widthBox, &QComboBox::currentIndexChanged, this, &ViewSettingsTab::changed); break; case DetailsMode: connect(m_expandableFolders, &QCheckBox::toggled, this, &ViewSettingsTab::changed); #ifndef Q_OS_WIN - connect(m_recursiveDirectorySizeLimit, QOverload<int>::of(&QSpinBox::valueChanged), this, &ViewSettingsTab::changed); + connect(m_recursiveDirectorySizeLimit, &QSpinBox::valueChanged, this, &ViewSettingsTab::changed); connect(m_numberOfItems, &QRadioButton::toggled, this, &ViewSettingsTab::changed); connect(m_sizeOfContents, &QRadioButton::toggled, this, [=]() { m_recursiveDirectorySizeLimit->setEnabled(m_sizeOfContents->isChecked()); diff --git a/src/settings/viewpropertiesdialog.cpp b/src/settings/viewpropertiesdialog.cpp index 6659d79b6..318c2e1cf 100644 --- a/src/settings/viewpropertiesdialog.cpp +++ b/src/settings/viewpropertiesdialog.cpp @@ -150,11 +150,11 @@ ViewPropertiesDialog::ViewPropertiesDialog(DolphinView* dolphinView) : layout->addRow(QString(), m_showHiddenFiles); layout->addRow(QString(), m_sortHiddenLast); - connect(m_viewMode, QOverload<int>::of(&QComboBox::currentIndexChanged), + connect(m_viewMode, &QComboBox::currentIndexChanged, this, &ViewPropertiesDialog::slotViewModeChanged); - connect(m_sorting, QOverload<int>::of(&QComboBox::currentIndexChanged), + connect(m_sorting, &QComboBox::currentIndexChanged, this, &ViewPropertiesDialog::slotSortingChanged); - connect(m_sortOrder, QOverload<int>::of(&QComboBox::currentIndexChanged), + connect(m_sortOrder, &QComboBox::currentIndexChanged, this, &ViewPropertiesDialog::slotSortOrderChanged); connect(m_sortFoldersFirst, &QCheckBox::clicked, this, &ViewPropertiesDialog::slotSortFoldersFirstChanged); diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index e9a0e2dce..64003850c 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -1,78 +1,68 @@ set( EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR} ) -find_package(Qt5Test CONFIG REQUIRED) +find_package(Qt${QT_MAJOR_VERSION}Test CONFIG REQUIRED) include(ECMAddTests) include(FindGem) # For servicemenutest, see bottom of this file # KItemSetTest -ecm_add_test(kitemsettest.cpp LINK_LIBRARIES dolphinprivate Qt5::Test) +ecm_add_test(kitemsettest.cpp LINK_LIBRARIES dolphinprivate Qt${QT_MAJOR_VERSION}::Test) # KItemRangeTest -ecm_add_test(kitemrangetest.cpp LINK_LIBRARIES dolphinprivate Qt5::Test) +ecm_add_test(kitemrangetest.cpp LINK_LIBRARIES dolphinprivate Qt${QT_MAJOR_VERSION}::Test) # KItemListSelectionManagerTest -ecm_add_test(kitemlistselectionmanagertest.cpp LINK_LIBRARIES dolphinprivate Qt5::Test) +ecm_add_test(kitemlistselectionmanagertest.cpp LINK_LIBRARIES dolphinprivate Qt${QT_MAJOR_VERSION}::Test) # KItemListControllerTest ecm_add_test(kitemlistcontrollertest.cpp testdir.cpp TEST_NAME kitemlistcontrollertest -LINK_LIBRARIES dolphinprivate Qt5::Test) +LINK_LIBRARIES dolphinprivate Qt${QT_MAJOR_VERSION}::Test) # KFileItemListViewTest ecm_add_test(kfileitemlistviewtest.cpp testdir.cpp TEST_NAME kfileitemlistviewtest -LINK_LIBRARIES dolphinprivate Qt5::Test) +LINK_LIBRARIES dolphinprivate Qt${QT_MAJOR_VERSION}::Test) # KFileItemModelTest ecm_add_test(kfileitemmodeltest.cpp testdir.cpp TEST_NAME kfileitemmodeltest -LINK_LIBRARIES dolphinprivate dolphinstatic Qt5::Test) +LINK_LIBRARIES dolphinprivate dolphinstatic Qt${QT_MAJOR_VERSION}::Test) # KFileItemModelBenchmark, not run automatically with `ctest` or `make test` add_executable(kfileitemmodelbenchmark kfileitemmodelbenchmark.cpp testdir.cpp) -target_link_libraries(kfileitemmodelbenchmark dolphinprivate Qt5::Test) +target_link_libraries(kfileitemmodelbenchmark dolphinprivate Qt${QT_MAJOR_VERSION}::Test) # KItemListKeyboardSearchManagerTest -ecm_add_test(kitemlistkeyboardsearchmanagertest.cpp LINK_LIBRARIES dolphinprivate Qt5::Test) +ecm_add_test(kitemlistkeyboardsearchmanagertest.cpp LINK_LIBRARIES dolphinprivate Qt${QT_MAJOR_VERSION}::Test) # DolphinSearchBox if (KF5Baloo_FOUND) ecm_add_test(dolphinsearchboxtest.cpp TEST_NAME dolphinsearchboxtest - LINK_LIBRARIES dolphinprivate dolphinstatic Qt5::Test) + LINK_LIBRARIES dolphinprivate dolphinstatic Qt${QT_MAJOR_VERSION}::Test) endif() # DolphinQuery if (KF5Baloo_FOUND) ecm_add_test(dolphinquerytest.cpp TEST_NAME dolphinquerytest - LINK_LIBRARIES dolphinprivate dolphinstatic Qt5::Test) + LINK_LIBRARIES dolphinprivate dolphinstatic Qt${QT_MAJOR_VERSION}::Test) endif() -# KStandardItemModelTest -ecm_add_test(kstandarditemmodeltest.cpp -TEST_NAME kstandarditemmodeltest -LINK_LIBRARIES dolphinprivate dolphinstatic Qt5::Test) - # ViewPropertiesTest ecm_add_test(viewpropertiestest.cpp testdir.cpp TEST_NAME viewpropertiestest -LINK_LIBRARIES dolphinprivate dolphinstatic Qt5::Test) +LINK_LIBRARIES dolphinprivate dolphinstatic Qt${QT_MAJOR_VERSION}::Test) # DolphinMainWindowTest ecm_add_test(dolphinmainwindowtest.cpp ${CMAKE_SOURCE_DIR}/src/dolphin.qrc TEST_NAME dolphinmainwindowtest -LINK_LIBRARIES dolphinprivate dolphinstatic Qt5::Test) +LINK_LIBRARIES dolphinprivate dolphinstatic Qt${QT_MAJOR_VERSION}::Test) # DragAndDropHelperTest -ecm_add_test(draganddrophelpertest.cpp LINK_LIBRARIES dolphinprivate Qt5::Test) - -# PlacesItemModelTest -ecm_add_test(placesitemmodeltest.cpp -TEST_NAME placesitemmodeltest -LINK_LIBRARIES dolphinprivate dolphinstatic Qt5::Test) +ecm_add_test(draganddrophelpertest.cpp LINK_LIBRARIES dolphinprivate Qt${QT_MAJOR_VERSION}::Test) find_gem(test-unit) set_package_properties(Gem:test-unit PROPERTIES diff --git a/src/tests/kfileitemmodeltest.cpp b/src/tests/kfileitemmodeltest.cpp index 7a22a1a7f..679b8ab3a 100644 --- a/src/tests/kfileitemmodeltest.cpp +++ b/src/tests/kfileitemmodeltest.cpp @@ -73,6 +73,10 @@ private Q_SLOTS: void testNameFilter(); void testEmptyPath(); void testRefreshExpandedItem(); + void testAddItemToFilteredExpandedFolder(); + void testDeleteItemsWithExpandedFolderWithFilter(); + void testRefreshItemsWithFilter(); + void testRefreshExpandedFolderWithFilter(); void testRemoveHiddenItems(); void collapseParentOfHiddenItems(); void removeParentOfHiddenItems(); @@ -516,7 +520,7 @@ void KFileItemModelTest::testExpandItems() // Test expanding subfolders in a folder with the items "a/", "a/a/", "a/a/1", "a/a-1/", "a/a-1/1". // Besides testing the basic item expansion functionality, the test makes sure that // KFileItemModel::expansionLevelsCompare(const KFileItem& a, const KFileItem& b) - // yields the correct result for "a/a/1" and "a/a-1/", whis is non-trivial because they share the + // yields the correct result for "a/a/1" and "a/a-1/", which is non-trivial because they share the // first three characters. QSet<QByteArray> originalModelRoles = m_model->roles(); QSet<QByteArray> modelRoles = originalModelRoles; @@ -1144,6 +1148,219 @@ void KFileItemModelTest::testRefreshExpandedItem() } /** + * Verifies that adding an item to an expanded folder that's filtered makes the parental chain visible. + */ +void KFileItemModelTest::testAddItemToFilteredExpandedFolder() +{ + QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); + QSignalSpy fileItemsChangedSpy(m_model, &KFileItemModel::fileItemsChanged); + + QSet<QByteArray> modelRoles = m_model->roles(); + modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; + m_model->setRoles(modelRoles); + + m_testDir->createFile("a/b/file"); + + m_model->loadDirectory(m_testDir->url()); + QVERIFY(itemsInsertedSpy.wait()); + QCOMPARE(m_model->count(), 1); // "a + + // Expand "a/". + m_model->setExpanded(0, true); + QVERIFY(itemsInsertedSpy.wait()); + + // Expand "a/b/". + m_model->setExpanded(1, true); + QVERIFY(itemsInsertedSpy.wait()); + + QCOMPARE(m_model->count(), 3); // 3 items: "a/", "a/b/", "a/b/file" + + const QUrl urlB = m_model->fileItem(1).url(); + + // Set a filter that matches ".txt" extension + m_model->setNameFilter("*.txt"); + QCOMPARE(m_model->count(), 0); // Everything got hidden since we don't have a .txt file yet + + m_model->slotItemsAdded(urlB, KFileItemList() << KFileItem(QUrl("a/b/newItem.txt"))); + m_model->slotCompleted(); + + // Entire parental chain should now be shown + QCOMPARE(m_model->count(), 3); // 3 items: "a/", "a/b/", "a/b/newItem.txt" + QCOMPARE(itemsInModel(), QStringList() << "a" << "b" << "newItem.txt"); + + // Items should be indented in hierarchy + QCOMPARE(m_model->expandedParentsCount(0), 0); + QCOMPARE(m_model->expandedParentsCount(1), 1); + QCOMPARE(m_model->expandedParentsCount(2), 2); +} + +/** + * Verifies that deleting the last filter-passing child from expanded folders + * makes the parental chain hidden. + */ +void KFileItemModelTest::testDeleteItemsWithExpandedFolderWithFilter() +{ + QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); + QSignalSpy itemsRemovedSpy(m_model, &KFileItemModel::itemsRemoved); + + QSet<QByteArray> modelRoles = m_model->roles(); + modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; + m_model->setRoles(modelRoles); + + m_testDir->createFile("a/b/file"); + + m_model->loadDirectory(m_testDir->url()); + QVERIFY(itemsInsertedSpy.wait()); + QCOMPARE(m_model->count(), 1); // "a + + // Expand "a/". + m_model->setExpanded(0, true); + QVERIFY(itemsInsertedSpy.wait()); + + // Expand "a/b/". + m_model->setExpanded(1, true); + QVERIFY(itemsInsertedSpy.wait()); + + QCOMPARE(m_model->count(), 3); // 3 items: "a/", "a/b/", "a/b/file" + + // Set a filter that matches "file" extension + m_model->setNameFilter("file"); + QCOMPARE(m_model->count(), 3); // Everything is still shown + + // Delete "file" + QCOMPARE(itemsRemovedSpy.count(), 0); + m_model->slotItemsDeleted(KFileItemList() << m_model->fileItem(2)); + QCOMPARE(itemsRemovedSpy.count(), 1); + + // Entire parental chain should now be filtered + QCOMPARE(m_model->count(), 0); + QCOMPARE(m_model->m_filteredItems.size(), 2); +} + +/** + * Verifies that the fileItemsChanged signal is raised with the correct index after renaming files with filter set. + * The rename operation will cause one item to be filtered out and another item to be reordered. + */ +void KFileItemModelTest::testRefreshItemsWithFilter() +{ + QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); + QSignalSpy itemsRemovedSpy(m_model, &KFileItemModel::itemsRemoved); + QSignalSpy itemsChangedSpy(m_model, &KFileItemModel::itemsChanged); + QSignalSpy itemsMovedSpy(m_model, &KFileItemModel::itemsMoved); + + // Creates three .txt files + m_testDir->createFiles({"b.txt", "c.txt", "d.txt"}); + + m_model->loadDirectory(m_testDir->url()); + QVERIFY(itemsInsertedSpy.wait()); + + QCOMPARE(m_model->count(), 3); // "b.txt", "c.txt", "d.txt" + + // Set a filter that matches ".txt" extension + m_model->setNameFilter("*.txt"); + QCOMPARE(m_model->count(), 3); // Still all items are shown + QCOMPARE(itemsInModel(), QStringList() << "b.txt" << "c.txt" << "d.txt"); + + // Objects used to rename + const KFileItem fileItemC_txt = m_model->fileItem(1); + KFileItem fileItemC_cfg = fileItemC_txt; + fileItemC_cfg.setUrl(QUrl("c.cfg")); + + const KFileItem fileItemD_txt = m_model->fileItem(2); + KFileItem fileItemA_txt = fileItemD_txt; + fileItemA_txt.setUrl(QUrl("a.txt")); + + // Rename "c.txt" to "c.cfg"; and rename "d.txt" to "a.txt" + QCOMPARE(itemsRemovedSpy.count(), 0); + QCOMPARE(itemsChangedSpy.count(), 0); + m_model->slotRefreshItems({qMakePair(fileItemC_txt, fileItemC_cfg), qMakePair(fileItemD_txt, fileItemA_txt)}); + QCOMPARE(itemsRemovedSpy.count(), 1); + QCOMPARE(itemsChangedSpy.count(), 1); + QCOMPARE(m_model->count(), 2); // Only "a.txt" and "b.txt". "c.cfg" got filtered out + + QList<QVariant> arguments = itemsChangedSpy.takeLast(); + KItemRangeList itemRangeList = arguments.at(0).value<KItemRangeList>(); + + // We started with the order "b.txt", "c.txt", "d.txt" + // "d.txt" started with index "2" + // "c.txt" got renamed and got filtered out + // "d.txt" index shifted from index "2" to "1" + // So we expect index "1" in this argument, meaning "d.txt" was renamed + QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(1, 1)); + + // Re-sorting is done asynchronously: + QCOMPARE(itemsInModel(), QStringList() << "b.txt" << "a.txt"); // Files should still be in the incorrect order + QVERIFY(itemsMovedSpy.wait()); + QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "b.txt"); // Files were re-sorted and should now be in the correct order +} + + +/** + * Verifies that parental chains are hidden and shown as needed while their children get filtered/unfiltered due to renaming. + * Also verifies that the "isExpanded" and "expandedParentsCount" values are kept for expanded folders that get refreshed. + */ +void KFileItemModelTest::testRefreshExpandedFolderWithFilter() { + QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); + QSignalSpy itemsRemovedSpy(m_model, &KFileItemModel::itemsRemoved); + + QSet<QByteArray> modelRoles = m_model->roles(); + modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; + m_model->setRoles(modelRoles); + + m_testDir->createFile("a/b/someFolder/someFile"); + + m_model->loadDirectory(m_testDir->url()); + QVERIFY(itemsInsertedSpy.wait()); + + QCOMPARE(m_model->count(), 1); // Only "a/" + + // Expand "a/". + m_model->setExpanded(0, true); + QVERIFY(itemsInsertedSpy.wait()); + + // Expand "a/b/". + m_model->setExpanded(1, true); + QVERIFY(itemsInsertedSpy.wait()); + + // Expand "a/b/someFolder/". + m_model->setExpanded(2, true); + QVERIFY(itemsInsertedSpy.wait()); + QCOMPARE(m_model->count(), 4); // 4 items: "a/", "a/b/", "a/b/someFolder", "a/b/someFolder/someFile" + + // Set a filter that matches the expanded folder "someFolder" + m_model->setNameFilter("someFolder"); + QCOMPARE(m_model->count(), 3); // 3 items: "a/", "a/b/", "a/b/someFolder" + + // Objects used to rename + const KFileItem fileItemA = m_model->fileItem(0); + KFileItem fileItemARenamed = fileItemA; + fileItemARenamed.setUrl(QUrl("a_renamed")); + + const KFileItem fileItemSomeFolder = m_model->fileItem(2); + KFileItem fileItemRenamedFolder = fileItemSomeFolder; + fileItemRenamedFolder.setUrl(QUrl("/a_renamed/b/renamedFolder")); + + // Rename "a" to "a_renamed" + // This way we test if the algorithm is sane as to NOT hide "a_renamed" since it will have visible children + m_model->slotRefreshItems({qMakePair(fileItemA, fileItemARenamed)}); + QCOMPARE(m_model->count(), 3); // Entire parental chain must still be shown + QCOMPARE(itemsInModel(), QStringList() << "a_renamed" << "b" << "someFolder"); + + // Rename "a_renamed" back to "a"; and "someFolder" to "renamedFolder" + m_model->slotRefreshItems({qMakePair(fileItemARenamed, fileItemA), qMakePair(fileItemSomeFolder, fileItemRenamedFolder)}); + QCOMPARE(m_model->count(), 0); // Entire parental chain became hidden + + // Rename "renamedFolder" back to "someFolder". Filter is passing again + m_model->slotRefreshItems({qMakePair(fileItemRenamedFolder, fileItemSomeFolder)}); + QCOMPARE(m_model->count(), 3); // Entire parental chain is shown again + QCOMPARE(itemsInModel(), QStringList() << "a" << "b" << "someFolder"); + + // slotRefreshItems() should preserve "isExpanded" and "expandedParentsCount" values explicitly in this case + QCOMPARE(m_model->m_itemData.at(2)->values.value("isExpanded").toBool(), true); + QCOMPARE(m_model->m_itemData.at(2)->values.value("expandedParentsCount"), 2); +} + +/** * Verify that removing hidden files and folders from the model does not * result in a crash, see https://bugs.kde.org/show_bug.cgi?id=314046 */ @@ -1298,8 +1515,7 @@ void KFileItemModelTest::removeParentOfHiddenItems() // Simulate the deletion of the directory "a/b/". m_model->slotItemsDeleted(KFileItemList() << m_model->fileItem(1)); QCOMPARE(itemsRemovedSpy.count(), 2); - QCOMPARE(m_model->count(), 1); - QCOMPARE(itemsInModel(), QStringList() << "a"); + QCOMPARE(m_model->count(), 0); // "a" will be filtered out since it doesn't pass the filter and doesn't have visible children // Remove the filter -> only the file "a/1" should appear. m_model->setNameFilter(QString()); diff --git a/src/tests/kstandarditemmodeltest.cpp b/src/tests/kstandarditemmodeltest.cpp deleted file mode 100644 index 943a85214..000000000 --- a/src/tests/kstandarditemmodeltest.cpp +++ /dev/null @@ -1,113 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2011 Peter Penz <[email protected]> - * SPDX-FileCopyrightText: 2011 Frank Reininghaus <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#include "dolphindebug.h" - -#include "kitemviews/kstandarditem.h" -#include "kitemviews/kstandarditemmodel.h" - -#include <QStandardPaths> -#include <QTest> - -class KStandardItemModelTest : public QObject -{ - Q_OBJECT - -private Q_SLOTS: - void initTestCase(); - void init(); - void cleanup(); - - void testNewItems(); - void testRemoveItems(); - -private: - bool isModelConsistent() const; - -private: - KStandardItemModel* m_model; -}; - -void KStandardItemModelTest::initTestCase() -{ - QStandardPaths::setTestModeEnabled(true); -} - -void KStandardItemModelTest::init() -{ - m_model = new KStandardItemModel(); -} - -void KStandardItemModelTest::cleanup() -{ - delete m_model; - m_model = nullptr; -} - -void KStandardItemModelTest::testNewItems() -{ - m_model->insertItem(0, new KStandardItem("item 1")); - m_model->insertItem(0, new KStandardItem("item 2")); - m_model->insertItem(2, new KStandardItem("item 3")); - m_model->appendItem(new KStandardItem("item 4")); - m_model->insertItem(-1, new KStandardItem("invalid 1")); - m_model->insertItem(5, new KStandardItem("invalid 2")); - QCOMPARE(m_model->count(), 4); - QCOMPARE(m_model->item(0)->text(), QString("item 2")); - QCOMPARE(m_model->item(1)->text(), QString("item 1")); - QCOMPARE(m_model->item(2)->text(), QString("item 3")); - QCOMPARE(m_model->item(3)->text(), QString("item 4")); - - QVERIFY(isModelConsistent()); -} - -void KStandardItemModelTest::testRemoveItems() -{ - for (int i = 1; i <= 10; ++i) { - m_model->appendItem(new KStandardItem("item " + QString::number(i))); - } - - m_model->removeItem(0); - m_model->removeItem(3); - m_model->removeItem(5); - m_model->removeItem(-1); - QCOMPARE(m_model->count(), 7); - QCOMPARE(m_model->item(0)->text(), QString("item 2")); - QCOMPARE(m_model->item(1)->text(), QString("item 3")); - QCOMPARE(m_model->item(2)->text(), QString("item 4")); - QCOMPARE(m_model->item(3)->text(), QString("item 6")); - QCOMPARE(m_model->item(4)->text(), QString("item 7")); - QCOMPARE(m_model->item(5)->text(), QString("item 9")); - QCOMPARE(m_model->item(6)->text(), QString("item 10")); -} - -bool KStandardItemModelTest::isModelConsistent() const -{ - if (m_model->m_items.count() != m_model->m_indexesForItems.count()) { - return false; - } - - for (int i = 0; i < m_model->count(); ++i) { - const KStandardItem* item = m_model->item(i); - if (!item) { - qCWarning(DolphinDebug) << "Item" << i << "is null"; - return false; - } - - const int itemIndex = m_model->index(item); - if (itemIndex != i) { - qCWarning(DolphinDebug) << "Item" << i << "has a wrong index:" << itemIndex; - return false; - } - } - - return true; -} - -QTEST_GUILESS_MAIN(KStandardItemModelTest) - -#include "kstandarditemmodeltest.moc" diff --git a/src/tests/placesitemmodeltest.cpp b/src/tests/placesitemmodeltest.cpp deleted file mode 100644 index 15a494691..000000000 --- a/src/tests/placesitemmodeltest.cpp +++ /dev/null @@ -1,939 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2017 Renato Araujo Oliveira <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#include <QTest> -#include <QSignalSpy> -#include <QStandardPaths> -#include <QAction> -#include <QDBusInterface> - -#include <KBookmarkManager> -#include <KConfig> -#include <KConfigGroup> -#include <KAboutData> -#include <KFilePlacesModel> -#include <KProtocolInfo> - -#include "dolphin_generalsettings.h" -#include "panels/places/placesitemmodel.h" -#include "panels/places/placesitem.h" -#include "views/viewproperties.h" - -Q_DECLARE_METATYPE(KItemRangeList) -Q_DECLARE_METATYPE(KItemRange) - -static QString bookmarksFile() -{ - return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/user-places.xbel"; -} - -class PlacesItemModelTest : public QObject -{ - Q_OBJECT - -private Q_SLOTS: - void init(); - void cleanup(); - - void initTestCase(); - void cleanupTestCase(); - - void testModelSort(); - void testGroups(); - void testDeletePlace(); - void testPlaceItem_data(); - void testPlaceItem(); - void testTearDownDevice(); - void testDefaultViewProperties_data(); - void testDefaultViewProperties(); - void testClear(); - void testHideItem(); - void testSystemItems(); - void testEditBookmark(); - void testEditAfterCreation(); - void testEditMetadata(); - void testRefresh(); - void testIcons_data(); - void testIcons(); - void testDragAndDrop(); - void testHideDevices(); - void testDuplicatedEntries(); - void renameAfterCreation(); - -private: - PlacesItemModel* m_model; - QSet<int> m_tobeRemoved; - QMap<QString, QDBusInterface *> m_interfacesMap; - int m_expectedModelCount = 14; - bool m_hasDesktopFolder = false; - bool m_hasDocumentsFolder = false; - bool m_hasDownloadsFolder = false; - bool m_hasMusicFolder = false; - bool m_hasPicturesFolder = false; - bool m_hasVideosFolder = false; - - void setBalooEnabled(bool enabled); - int indexOf(const QUrl &url); - QDBusInterface *fakeManager(); - QDBusInterface *fakeDevice(const QString &udi); - QStringList placesUrls(PlacesItemModel *model = nullptr) const; - QStringList initialUrls() const; - void createPlaceItem(const QString &text, const QUrl &url, const QString &icon); - void schedulePlaceRemoval(int index); - void cancelPlaceRemoval(int index); - QMimeData *createMimeData(const QList<int> &indexes) const; - void increaseIndexIfNeeded(int &index) const; - QTemporaryDir m_tempHomeDir; -}; - -#define CHECK_PLACES_URLS(urls) \ - { \ - QStringList places = placesUrls(); \ - if (places != urls) { \ - qWarning() << "Expected:" << urls; \ - qWarning() << "Got:" << places; \ - QCOMPARE(places, urls); \ - } \ - } - -void PlacesItemModelTest::setBalooEnabled(bool enabled) -{ - KConfig config(QStringLiteral("baloofilerc")); - KConfigGroup basicSettings = config.group("Basic Settings"); - basicSettings.writeEntry("Indexing-Enabled", enabled); - config.sync(); -} - -int PlacesItemModelTest::indexOf(const QUrl &url) -{ - for (int r = 0; r < m_model->count(); r++) { - if (m_model->placesItem(r)->url() == url) { - return r; - } - } - return -1; -} - -QDBusInterface *PlacesItemModelTest::fakeManager() -{ - return fakeDevice(QStringLiteral("/org/kde/solid/fakehw")); -} - -QDBusInterface *PlacesItemModelTest::fakeDevice(const QString &udi) -{ - if (m_interfacesMap.contains(udi)) { - return m_interfacesMap[udi]; - } - - QDBusInterface *iface = new QDBusInterface(QDBusConnection::sessionBus().baseService(), udi); - m_interfacesMap[udi] = iface; - - return iface; -} - -QStringList PlacesItemModelTest::placesUrls(PlacesItemModel *model) const -{ - QStringList urls; - if (!model) { - model = m_model; - } - - for (int row = 0; row < model->count(); ++row) { - urls << model->placesItem(row)->url().toDisplayString(QUrl::PreferLocalFile); - } - return urls; -} - -QStringList PlacesItemModelTest::initialUrls() const -{ - static QStringList urls; - if (urls.isEmpty()) { - urls << QDir::homePath(); - - if (m_hasDesktopFolder) { - urls << QDir::homePath() + QStringLiteral("/Desktop"); - } - - if (m_hasDocumentsFolder) { - urls << QDir::homePath() + QStringLiteral("/Documents"); - } - - if (m_hasDownloadsFolder) { - urls << QDir::homePath() + QStringLiteral("/Downloads"); - } - - if (m_hasMusicFolder) { - urls << QDir::homePath() + QStringLiteral("/Music"); - } - - if (m_hasPicturesFolder) { - urls << QDir::homePath() + QStringLiteral("/Pictures"); - } - - if (m_hasVideosFolder) { - urls << QDir::homePath() + QStringLiteral("/Videos"); - } - - urls << QStringLiteral("trash:/") - << QStringLiteral("remote:/") - << QStringLiteral("/media/nfs"); - - if (qEnvironmentVariableIsSet("KDE_FULL_SESSION") && KProtocolInfo::isKnownProtocol(QStringLiteral("recentlyused"))) { - urls << QStringLiteral("recentlyused:/files"); - urls << QStringLiteral("recentlyused:/locations"); - } else { - urls << QStringLiteral("timeline:/today") - << QStringLiteral("timeline:/yesterday"); - } - - urls << QStringLiteral("search:/documents") << QStringLiteral("search:/images") << QStringLiteral("search:/audio") << QStringLiteral("search:/videos") - << QStringLiteral("/foreign") - << QStringLiteral("/media/floppy0") << QStringLiteral("/media/XO-Y4") << QStringLiteral("/media/cdrom"); - } - return urls; -} - -void PlacesItemModelTest::createPlaceItem(const QString &text, const QUrl &url, const QString &icon) -{ - m_model->createPlacesItem(text, url, icon); -} - -void PlacesItemModelTest::schedulePlaceRemoval(int index) -{ - m_tobeRemoved.insert(index); -} - -void PlacesItemModelTest::cancelPlaceRemoval(int index) -{ - m_tobeRemoved.remove(index); -} - -QMimeData *PlacesItemModelTest::createMimeData(const QList<int> &indexes) const -{ - QByteArray itemData; - QDataStream stream(&itemData, QIODevice::WriteOnly); - QList<QUrl> urls; - - for (int index : indexes) { - const QUrl itemUrl = m_model->placesItem(index)->url(); - if (itemUrl.isValid()) { - urls << itemUrl; - } - stream << index; - } - - QMimeData* mimeData = new QMimeData(); - mimeData->setUrls(urls); - // copied from PlacesItemModel::internalMimeType() - const QString internalMimeType = "application/x-dolphinplacesmodel-" + - QString::number((qptrdiff)m_model); - mimeData->setData(internalMimeType, itemData); - return mimeData; -} - -void PlacesItemModelTest::increaseIndexIfNeeded(int &index) const -{ - if (m_hasDesktopFolder) { - index++; - } - if (m_hasDocumentsFolder) { - index++; - } - if (m_hasDownloadsFolder) { - index++; - } - if (m_hasMusicFolder) { - index++; - } - if (m_hasPicturesFolder) { - index++; - } - if (m_hasVideosFolder) { - index++; - } -} - -void PlacesItemModelTest::init() -{ - m_model = new PlacesItemModel(); - // WORKAROUND: need to wait for bookmark to load - QTest::qWait(300); - QCOMPARE(m_model->count(), m_expectedModelCount); -} - -void PlacesItemModelTest::cleanup() -{ - const auto tobeRemoved = m_tobeRemoved; - for (const int i : tobeRemoved) { - int before = m_model->count(); - m_model->deleteItem(i); - QTRY_COMPARE(m_model->count(), before - 1); - } - m_tobeRemoved.clear(); - delete m_model; - m_model = nullptr; -} - -void PlacesItemModelTest::initTestCase() -{ - QVERIFY(m_tempHomeDir.isValid()); - QVERIFY(qputenv("HOME", m_tempHomeDir.path().toUtf8())); - QVERIFY(qputenv("KDE_FORK_SLAVES", "yes")); - - QStandardPaths::setTestModeEnabled(true); - - const QString fakeHw = QFINDTESTDATA("data/fakecomputer.xml"); - QVERIFY(!fakeHw.isEmpty()); - qputenv("SOLID_FAKEHW", QFile::encodeName(fakeHw)); - - setBalooEnabled(true); - const QString bookmarsFileName = bookmarksFile(); - if (QFileInfo::exists(bookmarsFileName)) { - // Ensure we'll have a clean bookmark file to start - QVERIFY(QFile::remove(bookmarsFileName)); - } - - if (QDir(QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)).exists()) { - m_hasDesktopFolder = true; - m_expectedModelCount++; - } - - if (QDir(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)).exists()) { - m_hasDocumentsFolder = true; - m_expectedModelCount++; - } - - if (QDir(QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)).exists()) { - m_hasDownloadsFolder = true; - m_expectedModelCount++; - } - - if (QDir(QStandardPaths::writableLocation(QStandardPaths::MusicLocation)).exists()) { - m_hasMusicFolder = true; - m_expectedModelCount++; - } - - if (QDir(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)).exists()) { - m_hasPicturesFolder = true; - m_expectedModelCount++; - } - - if (QDir(QStandardPaths::writableLocation(QStandardPaths::MoviesLocation)).exists()) { - m_hasVideosFolder = true; - m_expectedModelCount++; - } - - qRegisterMetaType<KItemRangeList>(); - qRegisterMetaType<KItemRange>(); -} - -void PlacesItemModelTest::cleanupTestCase() -{ - qDeleteAll(m_interfacesMap); - QFile::remove(bookmarksFile()); -} - -void PlacesItemModelTest::testModelSort() -{ - CHECK_PLACES_URLS(initialUrls()); -} - -void PlacesItemModelTest::testGroups() -{ - const auto groups = m_model->groups(); - int expectedRemoteIndex = 2; - increaseIndexIfNeeded(expectedRemoteIndex); - - QCOMPARE(groups.size(), 6); - - QCOMPARE(groups.at(0).first, 0); - QCOMPARE(groups.at(0).second.toString(), QStringLiteral("Places")); - - QCOMPARE(groups.at(1).first, expectedRemoteIndex); - QCOMPARE(groups.at(1).second.toString(), QStringLiteral("Remote")); - - QCOMPARE(groups.at(2).first, expectedRemoteIndex + 2); - QCOMPARE(groups.at(2).second.toString(), QStringLiteral("Recent")); - - QCOMPARE(groups.at(3).first, expectedRemoteIndex + 4); - QCOMPARE(groups.at(3).second.toString(), QStringLiteral("Search For")); - - QCOMPARE(groups.at(4).first, expectedRemoteIndex + 8); - QCOMPARE(groups.at(4).second.toString(), QStringLiteral("Devices")); - - QCOMPARE(groups.at(5).first, expectedRemoteIndex + 9); - QCOMPARE(groups.at(5).second.toString(), QStringLiteral("Removable Devices")); -} - -void PlacesItemModelTest::testPlaceItem_data() -{ - QTest::addColumn<QUrl>("url"); - QTest::addColumn<bool>("expectedIsHidden"); - QTest::addColumn<bool>("expectedIsSystemItem"); - QTest::addColumn<QString>("expectedGroup"); - QTest::addColumn<bool>("expectedStorageSetupNeeded"); - - // places - QTest::newRow("Places - Home") << QUrl::fromLocalFile(QDir::homePath()) << false << true << QStringLiteral("Places") << false; - - // baloo -search - QTest::newRow("Baloo - Documents") << QUrl("search:/documents") << false << true << QStringLiteral("Search For") << false; - - // devices - QTest::newRow("Devices - Floppy") << QUrl("file:///media/floppy0") << false << false << QStringLiteral("Removable Devices") << false; -} - -void PlacesItemModelTest::testPlaceItem() -{ - QFETCH(QUrl, url); - QFETCH(bool, expectedIsHidden); - QFETCH(bool, expectedIsSystemItem); - QFETCH(QString, expectedGroup); - QFETCH(bool, expectedStorageSetupNeeded); - - const int index = indexOf(url); - PlacesItem *item = m_model->placesItem(index); - QCOMPARE(item->url(), url); - QCOMPARE(item->isHidden(), expectedIsHidden); - QCOMPARE(item->isSystemItem(), expectedIsSystemItem); - QCOMPARE(item->group(), expectedGroup); - QCOMPARE(item->storageSetupNeeded(), expectedStorageSetupNeeded); -} - -void PlacesItemModelTest::testDeletePlace() -{ - const QUrl tempUrl = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation)); - QStringList urls = initialUrls(); - QSignalSpy itemsInsertedSpy(m_model, &PlacesItemModel::itemsInserted); - QSignalSpy itemsRemovedSpy(m_model, &PlacesItemModel::itemsRemoved); - - PlacesItemModel *model = new PlacesItemModel(); - - int tempDirIndex = 2; - increaseIndexIfNeeded(tempDirIndex); - - // create a new place - createPlaceItem(QStringLiteral("Temporary Dir"), tempUrl, QString()); - urls.insert(tempDirIndex, tempUrl.toLocalFile()); - - // check if the new entry was created - QTRY_COMPARE(itemsInsertedSpy.count(), 1); - CHECK_PLACES_URLS(urls); - QTRY_COMPARE(model->count(), m_model->count()); - - // delete item - m_model->deleteItem(tempDirIndex); - - // make sure that the new item is removed - QTRY_COMPARE(itemsRemovedSpy.count(), 1); - QTRY_COMPARE(m_model->count(), m_expectedModelCount); - CHECK_PLACES_URLS(initialUrls()); - QTRY_COMPARE(model->count(), m_model->count()); -} - -void PlacesItemModelTest::testTearDownDevice() -{ - const QUrl mediaUrl = QUrl::fromLocalFile(QStringLiteral("/media/XO-Y4")); - int index = indexOf(mediaUrl); - QVERIFY(index != -1); - - auto ejectAction = m_model->ejectAction(index); - QVERIFY(!ejectAction); - - auto teardownAction = m_model->teardownAction(index); - QVERIFY(teardownAction); - - QCOMPARE(m_model->count(), m_expectedModelCount); - - QSignalSpy spyItemsRemoved(m_model, &PlacesItemModel::itemsRemoved); - fakeManager()->call(QStringLiteral("unplug"), "/org/kde/solid/fakehw/volume_part1_size_993284096"); - QTRY_COMPARE(m_model->count(), m_expectedModelCount - 1); - QCOMPARE(spyItemsRemoved.count(), 1); - const QList<QVariant> spyItemsRemovedArgs = spyItemsRemoved.takeFirst(); - const KItemRangeList removedRange = spyItemsRemovedArgs.at(0).value<KItemRangeList>(); - QCOMPARE(removedRange.size(), 1); - QCOMPARE(removedRange.first().index, index); - QCOMPARE(removedRange.first().count, 1); - - QCOMPARE(indexOf(mediaUrl), -1); - - QSignalSpy spyItemsInserted(m_model, &PlacesItemModel::itemsInserted); - fakeManager()->call(QStringLiteral("plug"), "/org/kde/solid/fakehw/volume_part1_size_993284096"); - QTRY_COMPARE(m_model->count(), m_expectedModelCount); - QCOMPARE(spyItemsInserted.count(), 1); - index = indexOf(mediaUrl); - - const QList<QVariant> args = spyItemsInserted.takeFirst(); - const KItemRangeList insertedRange = args.at(0).value<KItemRangeList>(); - QCOMPARE(insertedRange.size(), 1); - QCOMPARE(insertedRange.first().index, index); - QCOMPARE(insertedRange.first().count, 1); -} - -void PlacesItemModelTest::testDefaultViewProperties_data() -{ - QTest::addColumn<QUrl>("url"); - QTest::addColumn<DolphinView::Mode>("expectedViewMode"); - QTest::addColumn<bool>("expectedPreviewShow"); - QTest::addColumn<QList<QByteArray> >("expectedVisibleRole"); - - // places - QTest::newRow("Places - Home") << QUrl::fromLocalFile(QDir::homePath()) << DolphinView::IconsView << true << QList<QByteArray>({"text"}); - - // baloo -search - QTest::newRow("Baloo - Documents") << QUrl("search:/documents") << DolphinView::DetailsView << false << QList<QByteArray>({"text", "path"}); - - // audio files - QTest::newRow("Places - Audio") << QUrl("search:/audio") << DolphinView::DetailsView << false << QList<QByteArray>({"text", "artist", "album"}); - - // devices - QTest::newRow("Devices - Floppy") << QUrl("file:///media/floppy0") << DolphinView::IconsView << true << QList<QByteArray>({"text"}); - -} - -void PlacesItemModelTest::testDefaultViewProperties() -{ - QFETCH(QUrl, url); - QFETCH(DolphinView::Mode, expectedViewMode); - QFETCH(bool, expectedPreviewShow); - QFETCH(QList<QByteArray>, expectedVisibleRole); - - // In order to test the default view properties, turn off the global view properties and re-init the test to reload the model. - GeneralSettings* settings = GeneralSettings::self(); - settings->setGlobalViewProps(false); - settings->save(); - cleanup(); - init(); - - ViewProperties properties(KFilePlacesModel::convertedUrl(url)); - QCOMPARE(properties.viewMode(), expectedViewMode); - QCOMPARE(properties.previewsShown(), expectedPreviewShow); - QCOMPARE(properties.visibleRoles(), expectedVisibleRole); - - settings->setGlobalViewProps(true); - settings->save(); -} - -void PlacesItemModelTest::testClear() -{ - QCOMPARE(m_model->count(), m_expectedModelCount); - m_model->clear(); - QCOMPARE(m_model->count(), 0); - QCOMPARE(m_model->hiddenCount(), 0); - m_model->refresh(); - QTRY_COMPARE(m_model->count(), m_expectedModelCount); -} - -void PlacesItemModelTest::testHideItem() -{ - const QUrl mediaUrl = QUrl::fromLocalFile(QStringLiteral("/media/XO-Y4")); - const int index = indexOf(mediaUrl); - - PlacesItem *item = m_model->placesItem(index); - - QSignalSpy spyItemsRemoved(m_model, &PlacesItemModel::itemsRemoved); - QList<QVariant> spyItemsRemovedArgs; - KItemRangeList removedRange; - - QSignalSpy spyItemsInserted(m_model, &PlacesItemModel::itemsInserted); - QList<QVariant> spyItemsInsertedArgs; - KItemRangeList insertedRange; - QVERIFY(item); - - // hide an item - item->setHidden(true); - - // check if items removed was fired - QTRY_COMPARE(m_model->count(), m_expectedModelCount - 1); - QCOMPARE(spyItemsRemoved.count(), 1); - spyItemsRemovedArgs = spyItemsRemoved.takeFirst(); - removedRange = spyItemsRemovedArgs.at(0).value<KItemRangeList>(); - QCOMPARE(removedRange.size(), 1); - QCOMPARE(removedRange.first().index, index); - QCOMPARE(removedRange.first().count, 1); - - // allow model to show hidden items - m_model->setHiddenItemsShown(true); - - // check if the items inserted was fired - spyItemsInsertedArgs = spyItemsInserted.takeFirst(); - insertedRange = spyItemsInsertedArgs.at(0).value<KItemRangeList>(); - QCOMPARE(insertedRange.size(), 1); - QCOMPARE(insertedRange.first().index, index); - QCOMPARE(insertedRange.first().count, 1); - - // mark item as visible - item = m_model->placesItem(index); - item->setHidden(false); - - // mark model to hide invisible items - m_model->setHiddenItemsShown(true); - - QTRY_COMPARE(m_model->count(), m_expectedModelCount); -} - -void PlacesItemModelTest::testSystemItems() -{ - int tempDirIndex = 2; - increaseIndexIfNeeded(tempDirIndex); - - QCOMPARE(m_model->count(), m_expectedModelCount); - for (int r = 0; r < m_model->count(); r++) { - QCOMPARE(m_model->placesItem(r)->isSystemItem(), !m_model->placesItem(r)->device().isValid()); - } - - QSignalSpy itemsInsertedSpy(m_model, &PlacesItemModel::itemsInserted); - - // create a new entry (non system item) - createPlaceItem(QStringLiteral("Temporary Dir"), QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation)), QString()); - - // check if the new entry was created - QTRY_COMPARE(itemsInsertedSpy.count(), 1); - - // make sure the new place get removed - schedulePlaceRemoval(tempDirIndex); - - QList<QVariant> args = itemsInsertedSpy.takeFirst(); - KItemRangeList range = args.at(0).value<KItemRangeList>(); - QCOMPARE(range.first().index, tempDirIndex); - QCOMPARE(range.first().count, 1); - QVERIFY(!m_model->placesItem(tempDirIndex)->isSystemItem()); - QCOMPARE(m_model->count(), m_expectedModelCount + 1); - - QTest::qWait(300); - // check if the removal signal is correct - QSignalSpy itemsRemovedSpy(m_model, &PlacesItemModel::itemsRemoved); - m_model->deleteItem(tempDirIndex); - QTRY_COMPARE(itemsRemovedSpy.count(), 1); - args = itemsRemovedSpy.takeFirst(); - range = args.at(0).value<KItemRangeList>(); - QCOMPARE(range.first().index, tempDirIndex); - QCOMPARE(range.first().count, 1); - QTRY_COMPARE(m_model->count(), m_expectedModelCount); - - //cancel removal (it was removed above) - cancelPlaceRemoval(tempDirIndex); -} - -void PlacesItemModelTest::testEditBookmark() -{ - int tempDirIndex = 2; - increaseIndexIfNeeded(tempDirIndex); - - QScopedPointer<PlacesItemModel> other(new PlacesItemModel()); - - createPlaceItem(QStringLiteral("Temporary Dir"), QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation)), QString()); - - // make sure that the new item will be removed later - schedulePlaceRemoval(tempDirIndex); - - QSignalSpy itemsChangedSply(m_model, &PlacesItemModel::itemsChanged); - - // modify place text - m_model->item(tempDirIndex)->setText(QStringLiteral("Renamed place")); - m_model->refresh(); - - // check if the correct signal was fired - QTRY_COMPARE(itemsChangedSply.count(), 1); - QList<QVariant> args = itemsChangedSply.takeFirst(); - KItemRangeList range = args.at(0).value<KItemRangeList>(); - QCOMPARE(range.first().index, tempDirIndex); - QCOMPARE(range.first().count, 1); - QSet<QByteArray> roles = args.at(1).value<QSet<QByteArray> >(); - QCOMPARE(roles.size(), 1); - QCOMPARE(*roles.begin(), QByteArrayLiteral("text")); - QCOMPARE(m_model->item(tempDirIndex)->text(), QStringLiteral("Renamed place")); - - // check if the item was updated in the other model - QTRY_COMPARE(other->item(tempDirIndex)->text(), QStringLiteral("Renamed place")); -} - -void PlacesItemModelTest::testEditAfterCreation() -{ - int tempDirIndex = 2; - increaseIndexIfNeeded(tempDirIndex); - - const QUrl tempUrl = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation)); - QSignalSpy itemsInsertedSpy(m_model, &PlacesItemModel::itemsInserted); - - // create a new place - createPlaceItem(QStringLiteral("Temporary Dir"), tempUrl, QString()); - QTRY_COMPARE(itemsInsertedSpy.count(), 1); - - PlacesItemModel *model = new PlacesItemModel(); - QTRY_COMPARE(model->count(), m_model->count()); - - // make sure that the new item will be removed later - schedulePlaceRemoval(tempDirIndex); - - // modify place text - PlacesItem *item = m_model->placesItem(tempDirIndex); - item->setText(QStringLiteral("Renamed place")); - m_model->refresh(); - - // check if the second model got the changes - QTRY_COMPARE(model->count(), m_model->count()); - QTRY_COMPARE(model->placesItem(tempDirIndex)->text(), m_model->placesItem(tempDirIndex)->text()); - QTRY_COMPARE(model->placesItem(tempDirIndex)->bookmark().metaDataItem(QStringLiteral("OnlyInApp")), - m_model->placesItem(tempDirIndex)->bookmark().metaDataItem(QStringLiteral("OnlyInApp"))); - QTRY_COMPARE(model->placesItem(tempDirIndex)->icon(), m_model->placesItem(tempDirIndex)->icon()); - QTRY_COMPARE(model->placesItem(tempDirIndex)->url(), m_model->placesItem(tempDirIndex)->url()); -} - -void PlacesItemModelTest::testEditMetadata() -{ - int tempDirIndex = 2; - increaseIndexIfNeeded(tempDirIndex); - - const QUrl tempUrl = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation)); - QSignalSpy itemsInsertedSpy(m_model, &PlacesItemModel::itemsInserted); - - // create a new place - createPlaceItem(QStringLiteral("Temporary Dir"), tempUrl, QString()); - QTRY_COMPARE(itemsInsertedSpy.count(), 1); - - // check if the new entry was created - PlacesItemModel *model = new PlacesItemModel(); - QTRY_COMPARE(model->count(), m_model->count()); - - // make sure that the new item will be removed later - schedulePlaceRemoval(tempDirIndex); - - // modify place metadata - auto bookmark = m_model->placesItem(tempDirIndex)->bookmark(); - bookmark.setMetaDataItem(QStringLiteral("OnlyInApp"), KAboutData::applicationData().componentName()); - m_model->refresh(); - - // check if the place was modified in both models - QTRY_COMPARE(model->placesItem(tempDirIndex)->bookmark().metaDataItem(QStringLiteral("OnlyInApp")), - KAboutData::applicationData().componentName()); - QTRY_COMPARE(model->placesItem(tempDirIndex)->text(), m_model->placesItem(tempDirIndex)->text()); - QTRY_COMPARE(model->placesItem(tempDirIndex)->bookmark().metaDataItem(QStringLiteral("OnlyInApp")), - m_model->placesItem(tempDirIndex)->bookmark().metaDataItem(QStringLiteral("OnlyInApp"))); - QTRY_COMPARE(model->placesItem(tempDirIndex)->icon(), m_model->placesItem(tempDirIndex)->icon()); - QTRY_COMPARE(model->placesItem(tempDirIndex)->url(), m_model->placesItem(tempDirIndex)->url()); -} - -void PlacesItemModelTest::testRefresh() -{ - int tempDirIndex = 2; - increaseIndexIfNeeded(tempDirIndex); - - const QUrl tempUrl = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation)); - QSignalSpy itemsInsertedSpy(m_model, &PlacesItemModel::itemsInserted); - - // create a new place - createPlaceItem(QStringLiteral("Temporary Dir"), tempUrl, QString()); - QTRY_COMPARE(itemsInsertedSpy.count(), 1); - - PlacesItemModel *model = new PlacesItemModel(); - QTRY_COMPARE(model->count(), m_model->count()); - - // make sure that the new item will be removed later - schedulePlaceRemoval(tempDirIndex); - - PlacesItem *item = m_model->placesItem(tempDirIndex); - PlacesItem *sameItem = model->placesItem(tempDirIndex); - QCOMPARE(item->text(), sameItem->text()); - - // modify place text - item->setText(QStringLiteral("Renamed place")); - - // item from another model is not affected at the moment - QVERIFY(item->text() != sameItem->text()); - - // propagate change - m_model->refresh(); - - // item must be equal - QTRY_COMPARE(item->text(), sameItem->text()); -} - -void PlacesItemModelTest::testIcons_data() -{ - QTest::addColumn<QUrl>("url"); - QTest::addColumn<QString>("expectedIconName"); - - // places - QTest::newRow("Places - Home") << QUrl::fromLocalFile(QDir::homePath()) << QStringLiteral("user-home"); - - // baloo -search - QTest::newRow("Baloo - Documents") << QUrl("search:/documents") << QStringLiteral("folder-text"); - - // devices - QTest::newRow("Devices - Floppy") << QUrl("file:///media/floppy0") << QStringLiteral("blockdevice"); -} - -void PlacesItemModelTest::testIcons() -{ - QFETCH(QUrl, url); - QFETCH(QString, expectedIconName); - - PlacesItem *item = m_model->placesItem(indexOf(url)); - QCOMPARE(item->icon(), expectedIconName); - - for (int r = 0; r < m_model->count(); r++) { - QVERIFY(!m_model->placesItem(r)->icon().isEmpty()); - } -} - -void PlacesItemModelTest::testDragAndDrop() -{ - int lastIndex = 1; // last index of places group - increaseIndexIfNeeded(lastIndex); - - QList<QVariant> args; - KItemRangeList range; - QStringList urls = initialUrls(); - - QSignalSpy itemsInsertedSpy(m_model, &PlacesItemModel::itemsInserted); - QSignalSpy itemsRemovedSpy(m_model, &PlacesItemModel::itemsRemoved); - - CHECK_PLACES_URLS(initialUrls()); - // Move the home directory to the end of the places group - QMimeData *dropData = createMimeData(QList<int>() << 0); - m_model->dropMimeDataBefore(m_model->count() - 1, dropData); - urls.move(0, lastIndex); - delete dropData; - - QTRY_COMPARE(itemsInsertedSpy.count(), 1); - QTRY_COMPARE(itemsRemovedSpy.count(), 1); - - // remove item from actual position - args = itemsRemovedSpy.takeFirst(); - range = args.at(0).value<KItemRangeList>(); - QCOMPARE(range.size(), 1); - QCOMPARE(range.at(0).count, 1); - QCOMPARE(range.at(0).index, 0); - - // insert intem in his group - args = itemsInsertedSpy.takeFirst(); - range = args.at(0).value<KItemRangeList>(); - QCOMPARE(range.size(), 1); - QCOMPARE(range.at(0).count, 1); - QCOMPARE(range.at(0).index, lastIndex); - - CHECK_PLACES_URLS(urls); - - itemsInsertedSpy.clear(); - itemsRemovedSpy.clear(); - - // Move home directory item back to its original position - dropData = createMimeData(QList<int>() << lastIndex); - m_model->dropMimeDataBefore(0, dropData); - urls.move(lastIndex, 0); - delete dropData; - - QTRY_COMPARE(itemsInsertedSpy.count(), 1); - QTRY_COMPARE(itemsRemovedSpy.count(), 1); - - // remove item from actual position - args = itemsRemovedSpy.takeFirst(); - range = args.at(0).value<KItemRangeList>(); - QCOMPARE(range.size(), 1); - QCOMPARE(range.at(0).count, 1); - QCOMPARE(range.at(0).index, lastIndex); - - // insert intem in the requested position - args = itemsInsertedSpy.takeFirst(); - range = args.at(0).value<KItemRangeList>(); - QCOMPARE(range.size(), 1); - QCOMPARE(range.at(0).count, 1); - QCOMPARE(range.at(0).index, 0); - - CHECK_PLACES_URLS(urls); -} - -void PlacesItemModelTest::testHideDevices() -{ - QSignalSpy itemsRemoved(m_model, &PlacesItemModel::itemsRemoved); - QStringList urls = initialUrls(); - - m_model->setGroupHidden(KFilePlacesModel::RemovableDevicesType, true); - QTRY_VERIFY(m_model->isGroupHidden(KFilePlacesModel::RemovableDevicesType)); - QTRY_COMPARE(itemsRemoved.count(), 3); - - // remove removable-devices - urls.removeOne(QStringLiteral("/media/floppy0")); - urls.removeOne(QStringLiteral("/media/XO-Y4")); - urls.removeOne(QStringLiteral("/media/cdrom")); - - // check if the correct urls was removed - CHECK_PLACES_URLS(urls); - - delete m_model; - m_model = new PlacesItemModel(); - QTRY_COMPARE(m_model->count(), urls.count()); - CHECK_PLACES_URLS(urls); - - // revert changes - m_model->setGroupHidden(KFilePlacesModel::RemovableDevicesType, false); - urls = initialUrls(); - QTRY_COMPARE(m_model->count(), urls.count()); - CHECK_PLACES_URLS(urls); -} - -void PlacesItemModelTest::testDuplicatedEntries() -{ - QStringList urls = initialUrls(); - // create a duplicated entry on bookmark - KBookmarkManager *bookmarkManager = KBookmarkManager::managerForFile(bookmarksFile(), QStringLiteral("kfilePlaces")); - KBookmarkGroup root = bookmarkManager->root(); - KBookmark bookmark = root.addBookmark(QStringLiteral("Duplicated Search Videos"), QUrl("search:/videos"), {}); - - const QString id = QUuid::createUuid().toString(); - bookmark.setMetaDataItem(QStringLiteral("ID"), id); - bookmark.setMetaDataItem(QStringLiteral("OnlyInApp"), KAboutData::applicationData().componentName()); - bookmarkManager->emitChanged(bookmarkManager->root()); - - PlacesItemModel *newModel = new PlacesItemModel(); - QTRY_COMPARE(placesUrls(newModel).count(QStringLiteral("search:/videos")), 1); - QTRY_COMPARE(urls, placesUrls(newModel)); - delete newModel; -} - -void PlacesItemModelTest::renameAfterCreation() -{ - int tempDirIndex = 2; - increaseIndexIfNeeded(tempDirIndex); - - const QUrl tempUrl = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation)); - QStringList urls = initialUrls(); - PlacesItemModel *model = new PlacesItemModel(); - - CHECK_PLACES_URLS(urls); - QTRY_COMPARE(model->count(), m_model->count()); - - // create a new place - createPlaceItem(QStringLiteral("Temporary Dir"), tempUrl, QString()); - urls.insert(tempDirIndex, tempUrl.toLocalFile()); - - // make sure that the new item will be removed later - schedulePlaceRemoval(tempDirIndex); - - CHECK_PLACES_URLS(urls); - QCOMPARE(model->count(), m_model->count()); - - - // modify place text - QSignalSpy changedSpy(m_model, &PlacesItemModel::itemsChanged); - - PlacesItem *item = m_model->placesItem(tempDirIndex); - item->setText(QStringLiteral("New Temporary Dir")); - item->setUrl(item->url()); - item->setIcon(item->icon()); - m_model->refresh(); - - QTRY_COMPARE(changedSpy.count(), 1); - - // check if the place was modified in both models - QTRY_COMPARE(m_model->placesItem(tempDirIndex)->text(), QStringLiteral("New Temporary Dir")); - QTRY_COMPARE(model->placesItem(tempDirIndex)->text(), QStringLiteral("New Temporary Dir")); -} - -QTEST_MAIN(PlacesItemModelTest) - -#include "placesitemmodeltest.moc" diff --git a/src/tests/testdir.cpp b/src/tests/testdir.cpp index 51dbdbc58..5d75a5343 100644 --- a/src/tests/testdir.cpp +++ b/src/tests/testdir.cpp @@ -31,12 +31,12 @@ static void setTimeStamp(const QString& path, const QDateTime& mtime) { #ifdef Q_OS_UNIX struct utimbuf utbuf; - utbuf.actime = mtime.toTime_t(); + utbuf.actime = mtime.toSecsSinceEpoch(); utbuf.modtime = utbuf.actime; utime(QFile::encodeName(path), &utbuf); #elif defined(Q_OS_WIN) struct _utimbuf utbuf; - utbuf.actime = mtime.toTime_t(); + utbuf.actime = mtime.toSecsSinceEpoch(); utbuf.modtime = utbuf.actime; _wutime(reinterpret_cast<const wchar_t *>(path.utf16()), &utbuf); #endif diff --git a/src/trash/dolphintrash.cpp b/src/trash/dolphintrash.cpp index df8834556..1446ab388 100644 --- a/src/trash/dolphintrash.cpp +++ b/src/trash/dolphintrash.cpp @@ -28,7 +28,7 @@ Trash::Trash() bool isTrashEmpty = m_trashDirLister->items().isEmpty(); Q_EMIT emptinessChanged(isTrashEmpty); }; - connect(m_trashDirLister, QOverload<>::of(&KCoreDirLister::completed), this, trashDirContentChanged); + connect(m_trashDirLister, &KCoreDirLister::completed, this, trashDirContentChanged); connect(m_trashDirLister, &KDirLister::itemsDeleted, this, trashDirContentChanged); m_trashDirLister->openUrl(QUrl(QStringLiteral("trash:/"))); } diff --git a/src/views/dolphinitemlistview.cpp b/src/views/dolphinitemlistview.cpp index 864d180c8..923589246 100644 --- a/src/views/dolphinitemlistview.cpp +++ b/src/views/dolphinitemlistview.cpp @@ -73,6 +73,7 @@ void DolphinItemListView::readSettings() beginTransaction(); setEnabledSelectionToggles(GeneralSettings::showSelectionToggle()); + setHighlightEntireRow(DetailsModeSettings::leadingPadding()); setSupportsItemExpanding(itemLayoutSupportsItemExpanding(itemLayout())); updateFont(); diff --git a/src/views/dolphinremoteencoding.cpp b/src/views/dolphinremoteencoding.cpp index 41b3b6890..c7c8b09d1 100644 --- a/src/views/dolphinremoteencoding.cpp +++ b/src/views/dolphinremoteencoding.cpp @@ -90,7 +90,7 @@ void DolphinRemoteEncoding::fillMenu() QMenu* menu = m_menu->menu(); menu->clear(); - + menu->addAction(i18n("Default"), this, SLOT(slotDefault()), 0)->setCheckable(true); for (int i = 0; i < m_encodingDescriptions.size();i++) { QAction* action = new QAction(m_encodingDescriptions.at(i), this); action->setCheckable(true); @@ -100,7 +100,6 @@ void DolphinRemoteEncoding::fillMenu() menu->addSeparator(); menu->addAction(i18n("Reload"), this, SLOT(slotReload()), 0); - menu->addAction(i18n("Default"), this, SLOT(slotDefault()), 0)->setCheckable(true); m_idDefault = m_encodingDescriptions.size() + 2; connect(menu, &QMenu::triggered, this, &DolphinRemoteEncoding::slotItemSelected); diff --git a/src/views/dolphinview.cpp b/src/views/dolphinview.cpp index 9a063d857..0a9783fe1 100644 --- a/src/views/dolphinview.cpp +++ b/src/views/dolphinview.cpp @@ -8,6 +8,7 @@ #include "dolphinview.h" #include "dolphin_generalsettings.h" +#include "dolphin_detailsmodesettings.h" #include "dolphinitemlistview.h" #include "dolphinnewfilemenuobserver.h" #include "draganddrophelper.h" @@ -59,6 +60,7 @@ #include <QScrollBar> #include <QSize> #include <QTimer> +#include <QToolTip> #include <QVBoxLayout> DolphinView::DolphinView(const QUrl& url, QWidget* parent) : @@ -200,6 +202,8 @@ DolphinView::DolphinView(const QUrl& url, QWidget* parent) : this, &DolphinView::slotRoleEditingCanceled); connect(m_view->header(), &KItemListHeader::columnWidthChangeFinished, this, &DolphinView::slotHeaderColumnWidthChangeFinished); + connect(m_view->header(), &KItemListHeader::leadingPaddingChanged, + this, &DolphinView::slotLeadingPaddingWidthChanged); KItemListSelectionManager* selectionManager = controller->selectionManager(); connect(selectionManager, &KItemListSelectionManager::selectionChanged, @@ -918,6 +922,11 @@ bool DolphinView::eventFilter(QObject* watched, QEvent* event) if (watched == m_view) { m_dragging = false; } + break; + + case QEvent::ToolTip: + tryShowNameToolTip(event); + default: break; } @@ -974,12 +983,14 @@ void DolphinView::slotItemActivated(int index) } } -void DolphinView::slotItemsActivated(const KItemSet& indexes) +void DolphinView::slotItemsActivated(const KItemSet &indexes) { Q_ASSERT(indexes.count() >= 2); abortTwoClicksRenaming(); + const auto modifiers = QGuiApplication::keyboardModifiers(); + if (indexes.count() > 5) { QString question = i18np("Are you sure you want to open 1 item?", "Are you sure you want to open %1 items?", indexes.count()); const int answer = KMessageBox::warningYesNo(this, question); @@ -995,8 +1006,15 @@ void DolphinView::slotItemsActivated(const KItemSet& indexes) KFileItem item = m_model->fileItem(index); const QUrl& url = openItemAsFolderUrl(item); - if (!url.isEmpty()) { // Open folders in new tabs - Q_EMIT tabRequested(url); + if (!url.isEmpty()) { + // Open folders in new tabs or in new windows depending on the modifier + // The ctrl+shift behavior is ignored because we are handling multiple items + // keep in sync with KUrlNavigator::slotNavigatorButtonClicked + if (modifiers & Qt::ShiftModifier && !(modifiers & Qt::ControlModifier)) { + Q_EMIT windowRequested(url); + } else { + Q_EMIT tabRequested(url); + } } else { items.append(item); } @@ -1013,10 +1031,21 @@ void DolphinView::slotItemMiddleClicked(int index) { const KFileItem& item = m_model->fileItem(index); const QUrl& url = openItemAsFolderUrl(item); + const auto modifiers = QGuiApplication::keyboardModifiers(); if (!url.isEmpty()) { - Q_EMIT tabRequested(url); + // keep in sync with KUrlNavigator::slotNavigatorButtonClicked + if (modifiers & Qt::ShiftModifier) { + Q_EMIT activeTabRequested(url); + } else { + Q_EMIT tabRequested(url); + } } else if (isTabsForFilesEnabled()) { - Q_EMIT tabRequested(item.url()); + // keep in sync with KUrlNavigator::slotNavigatorButtonClicked + if (modifiers & Qt::ShiftModifier) { + Q_EMIT activeTabRequested(item.url()); + } else { + Q_EMIT tabRequested(item.url()); + } } } @@ -1091,6 +1120,10 @@ void DolphinView::slotHeaderContextMenuRequested(const QPointF& pos) QActionGroup* widthsGroup = new QActionGroup(menu); const bool autoColumnWidths = props.headerColumnWidths().isEmpty(); + QAction* toggleLeadingPaddingAction = menu->addAction(i18nc("@action:inmenu", "Leading Column Padding")); + toggleLeadingPaddingAction->setCheckable(true); + toggleLeadingPaddingAction->setChecked(view->header()->leadingPadding() > 0); + QAction* autoAdjustWidthsAction = menu->addAction(i18nc("@action:inmenu", "Automatic Column Widths")); autoAdjustWidthsAction->setCheckable(true); autoAdjustWidthsAction->setChecked(autoColumnWidths); @@ -1121,6 +1154,8 @@ void DolphinView::slotHeaderContextMenuRequested(const QPointF& pos) } props.setHeaderColumnWidths(columnWidths); header->setAutomaticColumnResizing(false); + } else if (action == toggleLeadingPaddingAction) { + header->setLeadingPadding(toggleLeadingPaddingAction->isChecked() ? 20 : 0); } else { // Show or hide the selected role const QByteArray selectedRole = action->data().toByteArray(); @@ -1173,6 +1208,13 @@ void DolphinView::slotHeaderColumnWidthChangeFinished(const QByteArray& role, qr props.setHeaderColumnWidths(columnWidths); } +void DolphinView::slotLeadingPaddingWidthChanged(qreal width) +{ + ViewProperties props(viewPropertiesUrl()); + DetailsModeSettings::setLeadingPadding(int(width)); + m_view->writeSettings(); +} + void DolphinView::slotItemHovered(int index) { const KFileItem item = m_model->fileItem(index); @@ -1617,13 +1659,15 @@ void DolphinView::updateViewState() void DolphinView::hideToolTip(const ToolTipManager::HideBehavior behavior) { -#ifdef HAVE_BALOO if (GeneralSettings::showToolTips()) { +#ifdef HAVE_BALOO m_toolTipManager->hideToolTip(behavior); - } #else Q_UNUSED(behavior) #endif + } else if (m_mode == DolphinView::IconsView) { + QToolTip::hideText(); + } } void DolphinView::slotTwoClicksRenamingTimerTimeout() @@ -1964,6 +2008,7 @@ void DolphinView::applyViewProperties(const ViewProperties& props) } else { header->setAutomaticColumnResizing(true); } + header->setLeadingPadding(DetailsModeSettings::leadingPadding()); } m_view->endTransaction(); @@ -2138,3 +2183,25 @@ void DolphinView::updatePlaceholderLabel() m_placeholderLabel->setVisible(true); } + +void DolphinView::tryShowNameToolTip(QEvent* event) +{ + if (!GeneralSettings::showToolTips() && m_mode == DolphinView::IconsView) { + QHelpEvent *hoverEvent = reinterpret_cast<QHelpEvent *>(event); + const std::optional<int> index = m_view->itemAt(hoverEvent->pos()); + + if (!index.has_value()) { + return; + } + + // Check whether the filename has been elided + const bool isElided = m_view->isElided(index.value()); + + if(isElided) { + const KFileItem item = m_model->fileItem(index.value()); + const QString text = item.text(); + const QPoint pos = mapToGlobal(hoverEvent->pos()); + QToolTip::showText(pos, text); + } + } +} diff --git a/src/views/dolphinview.h b/src/views/dolphinview.h index 75c9dd985..e93ca4fa0 100644 --- a/src/views/dolphinview.h +++ b/src/views/dolphinview.h @@ -425,13 +425,13 @@ Q_SIGNALS: /** * Is emitted when clicking on an item with the left mouse button. */ - void itemActivated(const KFileItem& item); + void itemActivated(const KFileItem &item); /** * Is emitted when multiple items have been activated by e. g. * context menu open with. */ - void itemsActivated(const KFileItemList& items); + void itemsActivated(const KFileItemList &items); /** * Is emitted if items have been added or deleted. @@ -444,6 +444,16 @@ Q_SIGNALS: void tabRequested(const QUrl& url); /** + * Is emitted if a new tab should be opened for the URL \a url and set as active. + */ + void activeTabRequested(const QUrl &url); + + /** + * Is emitted if a new window should be opened for the URL \a url. + */ + void windowRequested(const QUrl &url); + + /** * Is emitted if the view mode (IconsView, DetailsView, * PreviewsView) has been changed. */ @@ -458,7 +468,7 @@ Q_SIGNALS: /** Is emitted if the 'grouped sorting' property has been changed. */ void groupedSortingChanged(bool groupedSorting); - /** Is emmited in reaction to a requestStatusBarText() call. + /** Is emitted in reaction to a requestStatusBarText() call. * @see requestStatusBarText() */ void statusBarTextChanged(QString statusBarText); @@ -619,12 +629,13 @@ private Q_SLOTS: void activate(); void slotItemActivated(int index); - void slotItemsActivated(const KItemSet& indexes); + void slotItemsActivated(const KItemSet &indexes); void slotItemMiddleClicked(int index); void slotItemContextMenuRequested(int index, const QPointF& pos); void slotViewContextMenuRequested(const QPointF& pos); void slotHeaderContextMenuRequested(const QPointF& pos); void slotHeaderColumnWidthChangeFinished(const QByteArray& role, qreal current); + void slotLeadingPaddingWidthChanged(qreal width); void slotItemHovered(int index); void slotItemUnhovered(int index); void slotItemDropEvent(int index, QGraphicsSceneDragDropEvent* event); @@ -852,6 +863,8 @@ private: void updatePlaceholderLabel(); + void tryShowNameToolTip(QEvent* event); + private: void updatePalette(); void showLoadingPlaceholder(); diff --git a/src/views/dolphinviewactionhandler.cpp b/src/views/dolphinviewactionhandler.cpp index 47247ec35..2e524f8f2 100644 --- a/src/views/dolphinviewactionhandler.cpp +++ b/src/views/dolphinviewactionhandler.cpp @@ -213,7 +213,7 @@ void DolphinViewActionHandler::createActions() viewModeActions->addAction(compactAction); viewModeActions->addAction(detailsAction); viewModeActions->setToolBarMode(KSelectAction::MenuMode); - connect(viewModeActions, QOverload<QAction*>::of(&KSelectAction::triggered), this, &DolphinViewActionHandler::slotViewModeActionTriggered); + connect(viewModeActions, &KSelectAction::triggered, this, &DolphinViewActionHandler::slotViewModeActionTriggered); QAction* zoomInAction = KStandardAction::zoomIn(this, &DolphinViewActionHandler::zoomIn, diff --git a/src/views/tooltips/dolphinfilemetadatawidget.cpp b/src/views/tooltips/dolphinfilemetadatawidget.cpp index b147135bf..e914593fb 100644 --- a/src/views/tooltips/dolphinfilemetadatawidget.cpp +++ b/src/views/tooltips/dolphinfilemetadatawidget.cpp @@ -66,7 +66,7 @@ DolphinFileMetaDataWidget::DolphinFileMetaDataWidget(QWidget* parent) : QHBoxLayout* layout = new QHBoxLayout(this); layout->addWidget(m_preview); - layout->addSpacing(layout->margin()); + layout->addSpacing(layout->contentsMargins().left()); layout->addLayout(textLayout); } diff --git a/src/views/versioncontrol/kversioncontrolplugin.h b/src/views/versioncontrol/kversioncontrolplugin.h index c908be247..d3a39fbd6 100644 --- a/src/views/versioncontrol/kversioncontrolplugin.h +++ b/src/views/versioncontrol/kversioncontrolplugin.h @@ -45,7 +45,6 @@ class KFileItem; * - Add the following lines at the top of fileviewsvnplugin.cpp: * <code> * #include <KPluginFactory> - * #include <KPluginLoader> * K_PLUGIN_CLASS_WITH_JSON(FileViewSvnPlugin, "fileviewsvnplugin.json") * </code> * @@ -143,8 +142,8 @@ public: virtual QString fileName() const = 0; /** - * Returns the path of the local repository root for the versionned directory - * Returns an emtpy QString when directory is not part of a working copy + * Returns the path of the local repository root for the versioned directory + * Returns an empty QString when directory is not part of a working copy */ virtual QString localRepositoryRoot(const QString& directory) const; diff --git a/src/views/versioncontrol/versioncontrolobserver.cpp b/src/views/versioncontrol/versioncontrolobserver.cpp index 6766aa479..a773aef6b 100644 --- a/src/views/versioncontrol/versioncontrolobserver.cpp +++ b/src/views/versioncontrol/versioncontrolobserver.cpp @@ -14,7 +14,6 @@ #include <KLocalizedString> #include <KPluginFactory> -#include <KPluginLoader> #include <KPluginMetaData> #include <QTimer> @@ -136,7 +135,7 @@ void VersionControlObserver::slotItemsChanged(const KItemRangeList& itemRanges, { Q_UNUSED(itemRanges) - // Because "version" role is emitted by VCS plugin (ourselfs) we don't need to + // Because "version" role is emitted by VCS plugin (ourselves) we don't need to // analyze it and update directory item states information. So lets check if // there is only "version". if ( !(roles.count() == 1 && roles.contains("version")) ) { @@ -164,7 +163,7 @@ void VersionControlObserver::verifyDirectory() // by an immediate verification. m_dirVerificationTimer->setInterval(500); } else { - // View was versionned but should not be anymore + // View was versioned but should not be anymore updateItemStates(); } } else if ((m_plugin = searchPlugin(rootItem.url()))) { @@ -280,15 +279,13 @@ void VersionControlObserver::initPlugins() // all fileview version control plugins and remember them in 'plugins'. const QStringList enabledPlugins = VersionControlSettings::enabledPlugins(); - const QVector<KPluginMetaData> plugins = KPluginLoader::findPlugins(QStringLiteral("dolphin/vcs")); + const QVector<KPluginMetaData> plugins = KPluginMetaData::findPlugins(QStringLiteral("dolphin/vcs")); QSet<QString> loadedPlugins; for (const auto &p : plugins) { if (enabledPlugins.contains(p.name())) { - KPluginLoader loader(p.fileName()); - KPluginFactory *factory = loader.factory(); - KVersionControlPlugin *plugin = factory->create<KVersionControlPlugin>(); + auto plugin = KPluginFactory::instantiatePlugin<KVersionControlPlugin>(p).plugin; if (plugin) { m_plugins.append(plugin); loadedPlugins += p.name(); diff --git a/src/views/versioncontrol/versioncontrolobserver.h b/src/views/versioncontrol/versioncontrolobserver.h index 6e3977fb2..064a3088f 100644 --- a/src/views/versioncontrol/versioncontrolobserver.h +++ b/src/views/versioncontrol/versioncontrolobserver.h @@ -95,7 +95,7 @@ private Q_SLOTS: /** * Is invoked if the thread m_updateItemStatesThread has been finished - * and applys the item states. + * and applies the item states. */ void slotThreadFinished(); |
