From: milkyroute Date: Sun, 10 Aug 2025 11:06:17 +0000 (+0200) Subject: Merge remote-tracking branch 'fork/work/zakharafoniam/useful-groups' X-Git-Url: https://cloud.milkyroute.net/gitweb/dolphin.git/commitdiff_plain/b4e80645e8e39ef7fcc1545136bad06ab3dd5f3e?hp=-c Merge remote-tracking branch 'fork/work/zakharafoniam/useful-groups' --- b4e80645e8e39ef7fcc1545136bad06ab3dd5f3e diff --combined src/dolphincontextmenu.cpp index e1c67aad1,15c65ee56..b6c70b48a --- a/src/dolphincontextmenu.cpp +++ b/src/dolphincontextmenu.cpp @@@ -32,7 -32,6 +32,7 @@@ #include #include #include +#include DolphinContextMenu::DolphinContextMenu(DolphinMainWindow *parent, const KFileItem &fileInfo, @@@ -122,23 -121,11 +122,23 @@@ void DolphinContextMenu::addTrashContex { Q_ASSERT(m_context & TrashContext); - QAction *emptyTrashAction = addAction(QIcon::fromTheme(QStringLiteral("trash-empty")), i18nc("@action:inmenu", "Empty Trash"), [this]() { + QAction *emptyTrashAction = addAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18nc("@action:inmenu", "Empty Trash"), this, [this]() { Trash::empty(m_mainWindow); }); emptyTrashAction->setEnabled(!Trash::isEmpty()); + // Insert 'Sort By' and 'View Mode' + if (ContextMenuSettings::showSortBy() || ContextMenuSettings::showViewMode()) { + addSeparator(); + } + if (ContextMenuSettings::showSortBy()) { + addAction(m_mainWindow->actionCollection()->action(QStringLiteral("sort"))); + } + if (ContextMenuSettings::showViewMode()) { + addAction(m_mainWindow->actionCollection()->action(QStringLiteral("view_mode"))); + } + + addSeparator(); QAction *propertiesAction = m_mainWindow->actionCollection()->action(QStringLiteral("properties")); addAction(propertiesAction); } @@@ -148,38 -135,23 +148,38 @@@ void DolphinContextMenu::addTrashItemCo Q_ASSERT(m_context & TrashContext); Q_ASSERT(m_context & ItemContext); - addAction(QIcon::fromTheme("restoration"), i18nc("@action:inmenu", "Restore"), [this]() { - QList selectedUrls; - selectedUrls.reserve(m_selectedItems.count()); - for (const KFileItem &item : std::as_const(m_selectedItems)) { - selectedUrls.append(item.url()); - } + addAction(QIcon::fromTheme(QStringLiteral("edit-reset")), + i18ncp("@action:inmenu Restore the selected files that are in the trash to the place they lived at the moment they were trashed. Minimize the " + "length of this string if possible.", + "Restore to Former Location", + "Restore to Former Locations", + m_selectedItems.count()), + this, + [this]() { + QList selectedUrls; + selectedUrls.reserve(m_selectedItems.count()); + for (const KFileItem &item : std::as_const(m_selectedItems)) { + selectedUrls.append(item.url()); + } + + KIO::RestoreJob *job = KIO::restoreFromTrash(selectedUrls); + KJobWidgets::setWindow(job, m_mainWindow); + job->uiDelegate()->setAutoErrorHandlingEnabled(true); + }); - KIO::RestoreJob *job = KIO::restoreFromTrash(selectedUrls); - KJobWidgets::setWindow(job, m_mainWindow); - job->uiDelegate()->setAutoErrorHandlingEnabled(true); - }); + addSeparator(); + + addAction(m_mainWindow->actionCollection()->action(KStandardAction::name(KStandardAction::Cut))); + addAction(m_mainWindow->actionCollection()->action(KStandardAction::name(KStandardAction::Copy))); + + addSeparator(); QAction *deleteAction = m_mainWindow->actionCollection()->action(KStandardAction::name(KStandardAction::DeleteFile)); addAction(deleteAction); - QAction *propertiesAction = m_mainWindow->actionCollection()->action(QStringLiteral("properties")); - addAction(propertiesAction); + addSeparator(); + + addAction(m_mainWindow->actionCollection()->action(QStringLiteral("properties"))); } void DolphinContextMenu::addDirectoryItemContextMenu() @@@ -201,9 -173,7 +201,9 @@@ addOpenWithActions(); // set up 'Create New' menu - DolphinNewFileMenu *newFileMenu = new DolphinNewFileMenu(m_mainWindow->actionCollection()->action(QStringLiteral("create_dir")), m_mainWindow); + QAction *newDirAction = m_mainWindow->actionCollection()->action(QStringLiteral("create_dir")); + QAction *newFileAction = m_mainWindow->actionCollection()->action(QStringLiteral("create_file")); + DolphinNewFileMenu *newFileMenu = new DolphinNewFileMenu(newDirAction, newFileAction, this); newFileMenu->checkUpToDate(); newFileMenu->setWorkingDirectory(m_fileInfo.url()); newFileMenu->setEnabled(selectedItemsProps.supportsWriting()); @@@ -229,11 -199,7 +229,11 @@@ void DolphinContextMenu::addOpenParentF }); addAction(QIcon::fromTheme(QStringLiteral("tab-new")), i18nc("@action:inmenu", "Open Path in New Tab"), [this]() { - m_mainWindow->openNewTab(KIO::upUrl(m_fileInfo.targetUrl())); + const QUrl url = m_fileInfo.targetUrl(); + const QUrl parentUrl = KIO::upUrl(url); + DolphinTabPage *tabPage = m_mainWindow->openNewTab(parentUrl); + tabPage->activeViewContainer()->view()->markUrlsAsSelected({url}); + tabPage->activeViewContainer()->view()->markUrlAsCurrent(url); }); addAction(QIcon::fromTheme(QStringLiteral("window-new")), i18nc("@action:inmenu", "Open Path in New Window"), [this]() { @@@ -340,10 -306,13 +340,13 @@@ void DolphinContextMenu::addViewportCon } addSeparator(); - // Insert 'Sort By' and 'View Mode' + // Insert 'Sort By', 'Group By' and 'View Mode' if (ContextMenuSettings::showSortBy()) { addAction(m_mainWindow->actionCollection()->action(QStringLiteral("sort"))); } + if (ContextMenuSettings::showGroupBy()) { + addAction(m_mainWindow->actionCollection()->action(QStringLiteral("group"))); + } if (ContextMenuSettings::showViewMode()) { addAction(m_mainWindow->actionCollection()->action(QStringLiteral("view_mode"))); } @@@ -367,7 -336,7 +370,7 @@@ void DolphinContextMenu::insertDefaultI addAction(collection->action(KStandardAction::name(KStandardAction::Cut))); addAction(collection->action(KStandardAction::name(KStandardAction::Copy))); if (ContextMenuSettings::showCopyLocation()) { - QAction *copyPathAction = collection->action(QString("copy_location")); + QAction *copyPathAction = collection->action(QStringLiteral("copy_location")); copyPathAction->setEnabled(m_selectedItems.size() == 1); addAction(copyPathAction); } diff --combined src/dolphinmainwindow.cpp index 51772eac2,17396dabd..d08d6f6b3 --- a/src/dolphinmainwindow.cpp +++ b/src/dolphinmainwindow.cpp @@@ -26,10 -26,8 +26,10 @@@ #include "panels/folders/folderspanel.h" #include "panels/places/placespanel.h" #include "panels/terminal/terminalpanel.h" +#include "search/dolphinquery.h" #include "selectionmode/actiontexthelper.h" #include "settings/dolphinsettingsdialog.h" +#include "statusbar/diskspaceusagemenu.h" #include "statusbar/dolphinstatusbar.h" #include "views/dolphinnewfilemenuobserver.h" #include "views/dolphinremoteencoding.h" @@@ -43,7 -41,6 +43,7 @@@ #include #include #include +#include #include #include #include @@@ -57,7 -54,6 +57,7 @@@ #include #include #include +#include #include #include #include @@@ -127,7 -123,6 +127,7 @@@ DolphinMainWindow::DolphinMainWindow( , m_forwardAction(nullptr) , m_splitViewAction(nullptr) , m_splitViewMenuAction(nullptr) + , m_diskSpaceUsageMenu(nullptr) , m_sessionSaveTimer(nullptr) , m_sessionSaveWatcher(nullptr) , m_sessionSaveScheduled(false) @@@ -180,16 -175,10 +180,16 @@@ m_actionHandler = new DolphinViewActionHandler(actionCollection(), m_actionTextHelper, this); connect(m_actionHandler, &DolphinViewActionHandler::actionBeingHandled, this, &DolphinMainWindow::clearStatusBar); connect(m_actionHandler, &DolphinViewActionHandler::createDirectoryTriggered, this, &DolphinMainWindow::createDirectory); + connect(m_actionHandler, &DolphinViewActionHandler::createFileTriggered, this, &DolphinMainWindow::createFile); connect(m_actionHandler, &DolphinViewActionHandler::selectionModeChangeTriggered, this, &DolphinMainWindow::slotSetSelectionMode); - Q_CHECK_PTR(actionCollection()->action(QStringLiteral("create_dir"))); - m_newFileMenu->setNewFolderShortcutAction(actionCollection()->action(QStringLiteral("create_dir"))); + QAction *newDirAction = actionCollection()->action(QStringLiteral("create_dir")); + Q_CHECK_PTR(newDirAction); + m_newFileMenu->setNewFolderShortcutAction(newDirAction); + + QAction *newFileAction = actionCollection()->action(QStringLiteral("create_file")); + Q_CHECK_PTR(newFileAction); + m_newFileMenu->setNewFileShortcutAction(newFileAction); m_remoteEncoding = new DolphinRemoteEncoding(this, m_actionHandler); connect(this, &DolphinMainWindow::urlChanged, m_remoteEncoding, &DolphinRemoteEncoding::slotAboutToOpenUrl); @@@ -201,8 -190,7 +201,8 @@@ setupDockWidgets(); - setupGUI(Save | Create | ToolBar); + const bool usePhoneUi{KRuntimePlatform::runtimePlatform().contains(QLatin1String("phone"))}; + setupGUI(Save | Create | ToolBar, usePhoneUi ? QStringLiteral("dolphinuiforphones.rc") : QString() /* load the default dolphinui.rc file */); stateChanged(QStringLiteral("new_file")); QClipboard *clipboard = QApplication::clipboard(); @@@ -213,15 -201,6 +213,15 @@@ if (firstRun) { menuBar()->setVisible(false); + + if (usePhoneUi) { + Q_ASSERT(qobject_cast(m_placesPanel->parent())); + m_placesPanel->parentWidget()->hide(); + auto settings = GeneralSettings::self(); + settings->setShowZoomSlider(false); // Zooming can be done with pinch gestures instead and we are short on horizontal space. + settings->setRenameInline(false); // This works around inline renaming currently not working well with virtual keyboards. + settings->save(); // Otherwise the RenameInline setting is not picked up for the first time Dolphin is used. + } } const bool showMenu = !menuBar()->isHidden(); @@@ -238,7 -217,6 +238,7 @@@ } updateAllowedToolbarAreas(); + updateNavigatorsBackground(); // enable middle-click on back/forward/up to open in a new tab auto *middleClickEventFilter = new MiddleClickActionEventFilter(this); @@@ -394,10 -372,9 +394,10 @@@ void DolphinMainWindow::changeUrl(cons updatePasteAction(); updateViewActions(); updateGoActions(); + m_diskSpaceUsageMenu->setUrl(url); // will signal used urls to activities manager, too - m_recentFiles->addUrl(url); + m_recentFiles->addUrl(url, QString(), "inode/directory"); Q_EMIT urlChanged(url); } @@@ -409,9 -386,9 +409,9 @@@ void DolphinMainWindow::slotTerminalDir m_tearDownFromPlacesRequested = false; } - m_activeViewContainer->setAutoGrabFocus(false); + m_activeViewContainer->setGrabFocusOnUrlChange(false); changeUrl(url); - m_activeViewContainer->setAutoGrabFocus(true); + m_activeViewContainer->setGrabFocusOnUrlChange(true); } void DolphinMainWindow::slotEditableStateChanged(bool editable) @@@ -492,7 -469,7 +492,7 @@@ void DolphinMainWindow::addToPlaces( } if (url.isValid()) { QString icon; - if (m_activeViewContainer->isSearchModeEnabled()) { + if (isSearchUrl(url)) { icon = QStringLiteral("folder-saved-search-symbolic"); } else { icon = KIO::iconNameForUrl(url); @@@ -501,9 -478,9 +501,9 @@@ } } -void DolphinMainWindow::openNewTab(const QUrl &url) +DolphinTabPage *DolphinMainWindow::openNewTab(const QUrl &url) { - m_tabWidget->openNewTab(url, QUrl()); + return m_tabWidget->openNewTab(url, QUrl()); } void DolphinMainWindow::openNewTabAndActivate(const QUrl &url) @@@ -763,8 -740,6 +763,8 @@@ void DolphinMainWindow::slotSaveSession KConfig *config = KConfigGui::sessionConfig(); saveGlobalProperties(config); savePropertiesInternal(config, 1); + KConfigGroup group = config->group(QStringLiteral("Number")); + group.writeEntry("NumberOfWindows", 1); // Makes session restore aware that there is a window to restore. auto future = QtConcurrent::run([config]() { config->sync(); @@@ -830,15 -805,6 +830,15 @@@ void DolphinMainWindow::createDirectory } } +void DolphinMainWindow::createFile() +{ + // Use the same logic as in createDirectory() + if (!m_newFileMenu->isCreateFileRunning()) { + m_newFileMenu->setWorkingDirectory(activeViewContainer()->url()); + m_newFileMenu->createFile(); + } +} + void DolphinMainWindow::quit() { close(); @@@ -899,14 -865,13 +899,14 @@@ void DolphinMainWindow::paste( void DolphinMainWindow::find() { - m_activeViewContainer->setSearchModeEnabled(true); + m_activeViewContainer->setSearchBarVisible(true); + m_activeViewContainer->setFocusToSearchBar(); } void DolphinMainWindow::updateSearchAction() { QAction *toggleSearchAction = actionCollection()->action(QStringLiteral("toggle_search")); - toggleSearchAction->setChecked(m_activeViewContainer->isSearchModeEnabled()); + toggleSearchAction->setChecked(m_activeViewContainer->isSearchBarVisible()); } void DolphinMainWindow::updatePasteAction() @@@ -943,13 -908,9 +943,13 @@@ QAction *DolphinMainWindow::urlNavigato { const QUrl url = urlNavigator->locationUrl(historyIndex); - QString text = url.toDisplayString(QUrl::PreferLocalFile); + QString text; - if (!urlNavigator->showFullPath()) { + if (isSearchUrl(url)) { + text = Search::DolphinQuery(url, QUrl{}).title(); + } else if (urlNavigator->showFullPath()) { + text = url.toDisplayString(QUrl::PreferLocalFile); + } else { const KFilePlacesModel *placesModel = DolphinPlacesModelSingleton::instance().placesModel(); const QModelIndex closestIdx = placesModel->closestItem(url); @@@ -1009,7 -970,6 +1009,7 @@@ void DolphinMainWindow::slotAboutToShow const KUrlNavigator *urlNavigator = m_activeViewContainer->urlNavigatorInternalWithHistory(); int entries = 0; QMenu *menu = m_forwardAction->popupMenu(); + menu->clear(); for (int i = urlNavigator->historyIndex() - 1; i >= 0 && entries < MaxNumberOfNavigationentries; --i, ++entries) { QAction *action = urlNavigatorHistoryAction(urlNavigator, i, menu); menu->addAction(action); @@@ -1055,15 -1015,8 +1055,15 @@@ void DolphinMainWindow::invertSelection void DolphinMainWindow::toggleSplitView() { + QUrl newSplitViewUrl; + const KFileItemList list = m_activeViewContainer->view()->selectedItems(); + if (list.count() == 1) { + const KFileItem &item = list.first(); + newSplitViewUrl = DolphinView::openItemAsFolderUrl(item); + } + DolphinTabPage *tabPage = m_tabWidget->currentTabPage(); - tabPage->setSplitViewEnabled(!tabPage->splitViewEnabled(), WithAnimation); + tabPage->setSplitViewEnabled(!tabPage->splitViewEnabled(), WithAnimation, newSplitViewUrl); m_tabWidget->updateTabName(m_tabWidget->indexOf(tabPage)); updateViewActions(); } @@@ -1165,10 -1118,8 +1165,10 @@@ void DolphinMainWindow::replaceLocation // If the text field currently has focus and everything is selected, // pressing the keyboard shortcut returns the whole thing to breadcrumb mode + // and goes back to the view, just like how it was before this action was triggered the first time. if (navigator->isUrlEditable() && lineEdit->hasFocus() && lineEdit->selectedText() == lineEdit->text()) { navigator->setUrlEditable(false); + m_activeViewContainer->view()->setFocus(); } else { navigator->setUrlEditable(true); navigator->setFocus(); @@@ -1474,8 -1425,6 +1474,8 @@@ void DolphinMainWindow::slotWriteStateC newFileMenu()->setEnabled(isFolderWritable && m_activeViewContainer->url().scheme() != QLatin1String("trash")); // When the menu is disabled, actions in it are disabled later in the event loop, and we need to set the disabled reason after that. QTimer::singleShot(0, this, [this]() { + m_disabledActionNotifier->setDisabledReason(actionCollection()->action(QStringLiteral("create_file")), + i18nc("@info", "Cannot create new file: You do not have permission to create items in this folder.")); m_disabledActionNotifier->setDisabledReason(actionCollection()->action(QStringLiteral("create_dir")), i18nc("@info", "Cannot create new folder: You do not have permission to create items in this folder.")); }); @@@ -1511,7 -1460,7 +1511,7 @@@ void DolphinMainWindow::updateHamburger menu = new QMenu(this); hamburgerMenu->setMenu(menu); hamburgerMenu->hideActionsOf(ac->action(QStringLiteral("basic_actions"))->menu()); - hamburgerMenu->hideActionsOf(ac->action(QStringLiteral("zoom"))->menu()); + hamburgerMenu->hideActionsOf(qobject_cast(ac->action(QStringLiteral("zoom")))->popupMenu()); } else { menu->clear(); } @@@ -1562,18 -1511,16 +1562,19 @@@ // The third group contains actions to change what one sees in the view // and to change the more general UI. if (!toolBar()->isVisible() - || (!toolbarActions.contains(ac->action(QStringLiteral("icons"))) && !toolbarActions.contains(ac->action(QStringLiteral("compact"))) - && !toolbarActions.contains(ac->action(QStringLiteral("details"))) && !toolbarActions.contains(ac->action(QStringLiteral("view_mode"))))) { + || ((!toolbarActions.contains(ac->action(QStringLiteral("icons"))) && !toolbarActions.contains(ac->action(QStringLiteral("compact"))) + && !toolbarActions.contains(ac->action(QStringLiteral("details"))) && !toolbarActions.contains(ac->action(QStringLiteral("view_mode")))) + && !toolbarActions.contains(ac->action(QStringLiteral("view_settings"))))) { menu->addAction(ac->action(QStringLiteral("view_mode"))); } - menu->addAction(ac->action(QStringLiteral("show_hidden_files"))); - menu->addAction(ac->action(QStringLiteral("sort"))); - menu->addAction(ac->action(QStringLiteral("group"))); - menu->addAction(ac->action(QStringLiteral("additional_info"))); - if (!GeneralSettings::showStatusBar() || !GeneralSettings::showZoomSlider()) { - menu->addAction(ac->action(QStringLiteral("zoom"))); + if (!toolBar()->isVisible() || !toolbarActions.contains(ac->action(QStringLiteral("view_settings")))) { - menu->addAction(ac->action(QStringLiteral("show_hidden_files"))); - menu->addAction(ac->action(QStringLiteral("sort"))); - menu->addAction(ac->action(QStringLiteral("additional_info"))); - if (!GeneralSettings::showStatusBar() || !GeneralSettings::showZoomSlider()) { - menu->addAction(ac->action(QStringLiteral("zoom"))); - } ++ menu->addAction(ac->action(QStringLiteral("show_hidden_files"))); ++ menu->addAction(ac->action(QStringLiteral("sort"))); ++ menu->addAction(ac->action(QStringLiteral("group"))); ++ menu->addAction(ac->action(QStringLiteral("additional_info"))); ++ if (!GeneralSettings::showStatusBar() || !GeneralSettings::showZoomSlider()) { ++ menu->addAction(ac->action(QStringLiteral("zoom"))); ++ } } menu->addAction(ac->action(QStringLiteral("panels"))); @@@ -1597,8 -1544,6 +1598,8 @@@ void DolphinMainWindow::slotPlaceActiva // We can end up here if the user clicked a device in the Places Panel // which had been unmounted earlier, see https://bugs.kde.org/show_bug.cgi?id=161385. reloadView(); + + m_activeViewContainer->view()->setFocus(); // We always want the focus on the view after activating a place. } else { view->disableUrlNavigatorSelectionRequests(); changeUrl(url); @@@ -1619,6 -1564,9 +1620,6 @@@ void DolphinMainWindow::activeViewChang m_activeViewContainer = viewContainer; if (oldViewContainer) { - const QAction *toggleSearchAction = actionCollection()->action(QStringLiteral("toggle_search")); - toggleSearchAction->disconnect(oldViewContainer); - // Disconnect all signals between the old view container (container, // view and url navigator) and main window. oldViewContainer->disconnect(this); @@@ -1629,7 -1577,6 +1630,7 @@@ if (auto secondaryUrlNavigator = navigators->secondaryUrlNavigator()) { secondaryUrlNavigator->disconnect(this); } + oldViewContainer->disconnect(m_diskSpaceUsageMenu); // except the requestItemInfo so that on hover the information panel can still be updated connect(oldViewContainer->view(), &DolphinView::requestItemInfo, this, &DolphinMainWindow::requestItemInfo); @@@ -1651,17 -1598,9 +1652,17 @@@ updateViewActions(); updateGoActions(); updateSearchAction(); + connect(m_diskSpaceUsageMenu, + &DiskSpaceUsageMenu::showMessage, + viewContainer, + [viewContainer](const QString &message, KMessageWidget::MessageType messageType) { + viewContainer->showMessage(message, messageType); + }); + connect(m_diskSpaceUsageMenu, &DiskSpaceUsageMenu::showInstallationProgress, viewContainer, &DolphinViewContainer::showProgress); const QUrl url = viewContainer->url(); Q_EMIT urlChanged(url); + Q_EMIT selectionChanged(m_activeViewContainer->view()->selectedItems()); } void DolphinMainWindow::tabCountChanged(int count) @@@ -1727,11 -1666,7 +1728,11 @@@ void DolphinMainWindow::setViewsToHomeI { const QVector theViewContainers = viewContainers(); for (DolphinViewContainer *viewContainer : theViewContainers) { - if (viewContainer && viewContainer->url().toLocalFile().startsWith(mountPath)) { + if (!viewContainer) { + continue; + } + const auto viewPath = viewContainer->url().toLocalFile(); + if (viewPath.startsWith(mountPath + QLatin1String("/")) || viewPath == mountPath) { viewContainer->setUrl(QUrl::fromLocalFile(QDir::homePath())); } } @@@ -1743,7 -1678,7 +1744,7 @@@ void DolphinMainWindow::setupActions( auto hamburgerMenuAction = KStandardAction::hamburgerMenu(nullptr, nullptr, actionCollection()); // setup 'File' menu - m_newFileMenu = new DolphinNewFileMenu(nullptr, this); + m_newFileMenu = new DolphinNewFileMenu(nullptr, nullptr, this); actionCollection()->addAction(QStringLiteral("new_menu"), m_newFileMenu); QMenu *menu = m_newFileMenu->menu(); menu->setTitle(i18nc("@title:menu Create new folder, file, link, etc.", "Create New")); @@@ -1886,7 -1821,9 +1887,7 @@@ "This helps you " "find files and folders by opening a search bar. " "There you can enter search terms and specify settings to find the " - "items you are looking for.Use this help again on " - "the search bar so we can have a look at it while the settings are " - "explained.")); + "items you are looking for.")); // toggle_search acts as a copy of the main searchAction to be used mainly // in the toolbar, with no default shortcut attached, to avoid messing with @@@ -1898,13 -1835,6 +1899,13 @@@ toggleSearchAction->setToolTip(searchAction->toolTip()); toggleSearchAction->setWhatsThis(searchAction->whatsThis()); toggleSearchAction->setCheckable(true); + connect(toggleSearchAction, &QAction::triggered, this, [this](bool checked) { + if (checked) { + find(); + } else { + m_activeViewContainer->setSearchBarVisible(false); + } + }); QAction *toggleSelectionModeAction = actionCollection()->addAction(QStringLiteral("toggle_selection_mode")); // i18n: This action toggles a selection mode. @@@ -2106,12 -2036,6 +2107,12 @@@ compareFiles->setEnabled(false); connect(compareFiles, &QAction::triggered, this, &DolphinMainWindow::compareFiles); + QAction *manageDiskSpaceUsage = actionCollection()->addAction(QStringLiteral("manage_disk_space")); + manageDiskSpaceUsage->setText(i18nc("@action:inmenu Tools", "Manage Disk Space Usage")); + manageDiskSpaceUsage->setIcon(QIcon::fromTheme(QStringLiteral("filelight"))); + m_diskSpaceUsageMenu = new DiskSpaceUsageMenu{this}; + manageDiskSpaceUsage->setMenu(m_diskSpaceUsageMenu); + QAction *openPreferredSearchTool = actionCollection()->addAction(QStringLiteral("open_preferred_search_tool")); openPreferredSearchTool->setText(i18nc("@action:inmenu Tools", "Open Preferred Search Tool")); openPreferredSearchTool->setWhatsThis(xi18nc("@info:whatsthis", @@@ -2122,18 -2046,12 +2123,18 @@@ connect(openPreferredSearchTool, &QAction::triggered, this, &DolphinMainWindow::openPreferredSearchTool); if (KAuthorized::authorize(QStringLiteral("shell_access"))) { + // Get icon of user default terminal emulator application + const KConfigGroup group(KSharedConfig::openConfig(QStringLiteral("kdeglobals"), KConfig::SimpleConfig), QStringLiteral("General")); + const QString terminalDesktopFilename = group.readEntry("TerminalService"); + // Use utilities-terminal icon from theme if readEntry() has failed + const QString terminalIcon = terminalDesktopFilename.isEmpty() ? "utilities-terminal" : KDesktopFile(terminalDesktopFilename).readIcon(); + QAction *openTerminal = actionCollection()->addAction(QStringLiteral("open_terminal")); openTerminal->setText(i18nc("@action:inmenu Tools", "Open Terminal")); openTerminal->setWhatsThis(xi18nc("@info:whatsthis", "This opens a terminal application for the viewed location." "To learn more about terminals use the help features in the terminal application.")); - openTerminal->setIcon(QIcon::fromTheme(QStringLiteral("utilities-terminal"))); + openTerminal->setIcon(QIcon::fromTheme(terminalIcon)); actionCollection()->setDefaultShortcut(openTerminal, Qt::SHIFT | Qt::Key_F4); connect(openTerminal, &QAction::triggered, this, &DolphinMainWindow::openTerminal); @@@ -2143,7 -2061,7 +2144,7 @@@ openTerminalHere->setWhatsThis(xi18nc("@info:whatsthis", "This opens terminal applications for the selected items' locations." "To learn more about terminals use the help features in the terminal application.")); - openTerminalHere->setIcon(QIcon::fromTheme(QStringLiteral("utilities-terminal"))); + openTerminalHere->setIcon(QIcon::fromTheme(terminalIcon)); actionCollection()->setDefaultShortcut(openTerminalHere, Qt::SHIFT | Qt::ALT | Qt::Key_F4); connect(openTerminalHere, &QAction::triggered, this, &DolphinMainWindow::openTerminalHere); } @@@ -2172,6 -2090,14 +2173,6 @@@ &DolphinMainWindow::toggleShowMenuBar, Qt::QueuedConnection); - KToggleAction *showStatusBar = KStandardAction::showStatusbar(nullptr, nullptr, actionCollection()); - showStatusBar->setChecked(GeneralSettings::showStatusBar()); - connect(GeneralSettings::self(), &GeneralSettings::showStatusBarChanged, showStatusBar, &KToggleAction::setChecked); - connect(showStatusBar, &KToggleAction::triggered, this, [this](bool checked) { - GeneralSettings::setShowStatusBar(checked); - refreshViews(); - }); - KStandardAction::keyBindings(this, &DolphinMainWindow::slotKeyBindings, actionCollection()); KStandardAction::preferences(this, &DolphinMainWindow::editSettings, actionCollection()); @@@ -2241,7 -2167,7 +2242,7 @@@ QAction *openInSplitViewAction = actionCollection()->addAction(QStringLiteral("open_in_split_view")); openInSplitViewAction->setText(i18nc("@action:inmenu", "Open in Split View")); - openInSplitViewAction->setIcon(QIcon::fromTheme(QStringLiteral("view-right-new"))); + openInSplitViewAction->setIcon(QIcon::fromTheme(QStringLiteral("view-split-left-right"))); connect(openInSplitViewAction, &QAction::triggered, this, [this]() { openInSplitView(QUrl()); }); @@@ -2413,7 -2339,7 +2414,7 @@@ void DolphinMainWindow::setupDockWidget placesDock->setLocked(lock); placesDock->setObjectName(QStringLiteral("placesDock")); placesDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); - + m_placesPanel = new PlacesPanel(placesDock); m_placesPanel->setCustomContextMenuActions({lockLayoutAction}); placesDock->setWidget(m_placesPanel); @@@ -2518,7 -2444,7 +2519,7 @@@ void DolphinMainWindow::updateFileAndEd QAction *addToPlacesAction = col->action(QStringLiteral("add_to_places")); QAction *copyToOtherViewAction = col->action(QStringLiteral("copy_to_inactive_split_view")); QAction *moveToOtherViewAction = col->action(QStringLiteral("move_to_inactive_split_view")); - QAction *copyLocation = col->action(QString("copy_location")); + QAction *copyLocation = col->action(QStringLiteral("copy_location")); if (list.isEmpty()) { stateChanged(QStringLiteral("has_no_selection")); @@@ -2677,11 -2603,14 +2678,11 @@@ void DolphinMainWindow::connectViewSign connect(container, &DolphinViewContainer::showFilterBarChanged, this, &DolphinMainWindow::updateFilterBarAction); connect(container, &DolphinViewContainer::writeStateChanged, this, &DolphinMainWindow::slotWriteStateChanged); slotWriteStateChanged(container->view()->isFolderWritable()); - connect(container, &DolphinViewContainer::searchModeEnabledChanged, this, &DolphinMainWindow::updateSearchAction); + connect(container, &DolphinViewContainer::searchBarVisibilityChanged, 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); - // Make the toggled state of the selection mode actions visually follow the selection mode state of the view. auto toggleSelectionModeAction = actionCollection()->action(QStringLiteral("toggle_selection_mode")); toggleSelectionModeAction->setChecked(m_activeViewContainer->isSelectionModeEnabled()); @@@ -2736,16 -2665,12 +2737,16 @@@ void DolphinMainWindow::updateSplitActi m_splitViewAction->setText(i18nc("@action:intoolbar Close left view", "Close")); m_splitViewAction->setToolTip(i18nc("@info", "Close left view")); m_splitViewAction->setIcon(QIcon::fromTheme(QStringLiteral("view-left-close"))); + m_splitViewMenuAction->setText(i18nc("@action:inmenu Close left view", "Close Left View")); + popoutSplitAction->setText(i18nc("@action:intoolbar Move left view to a new window", "Pop out Left View")); popoutSplitAction->setToolTip(i18nc("@info", "Move left view to a new window")); } else { m_splitViewAction->setText(i18nc("@action:intoolbar Close right view", "Close")); m_splitViewAction->setToolTip(i18nc("@info", "Close right view")); m_splitViewAction->setIcon(QIcon::fromTheme(QStringLiteral("view-right-close"))); + m_splitViewMenuAction->setText(i18nc("@action:inmenu Close left view", "Close Right View")); + popoutSplitAction->setText(i18nc("@action:intoolbar Move right view to a new window", "Pop out Right View")); popoutSplitAction->setToolTip(i18nc("@info", "Move right view to a new window")); } @@@ -2757,9 -2682,8 +2758,9 @@@ } } else { m_splitViewAction->setText(i18nc("@action:intoolbar Split view", "Split")); + m_splitViewMenuAction->setText(m_splitViewAction->text()); m_splitViewAction->setToolTip(i18nc("@info", "Split view")); - m_splitViewAction->setIcon(QIcon::fromTheme(QStringLiteral("view-right-new"))); + m_splitViewAction->setIcon(QIcon::fromTheme(QStringLiteral("view-split-left-right"))); popoutSplitAction->setText(i18nc("@action:intoolbar Move view in focus to a new window", "Pop out")); popoutSplitAction->setEnabled(false); if (m_splitViewAction->menu()) { @@@ -2771,6 -2695,7 +2772,6 @@@ } // Update state from toolbar action - m_splitViewMenuAction->setText(m_splitViewAction->text()); m_splitViewMenuAction->setToolTip(m_splitViewAction->toolTip()); m_splitViewMenuAction->setIcon(m_splitViewAction->icon()); } @@@ -2788,12 -2713,6 +2789,12 @@@ void DolphinMainWindow::updateAllowedTo } } +void DolphinMainWindow::updateNavigatorsBackground() +{ + auto navigators = static_cast(actionCollection()->action(QStringLiteral("url_navigators"))); + navigators->setBackgroundEnabled(navigators->isInToolbar()); +} + bool DolphinMainWindow::isKompareInstalled() const { static bool initialized = false; @@@ -2980,7 -2899,6 +2981,7 @@@ void DolphinMainWindow::saveNewToolbarC m_tabWidget->currentTabPage()->insertNavigatorsWidget(navigators); } updateAllowedToolbarAreas(); + updateNavigatorsBackground(); (static_cast(actionCollection()->action(KStandardAction::name(KStandardAction::HamburgerMenu))))->hideActionsOf(toolBar()); } @@@ -3052,7 -2970,7 +3053,7 @@@ bool DolphinMainWindow::isItemVisibleIn void DolphinMainWindow::slotDoubleClickViewBackground(Qt::MouseButton button) { - if (button == Qt::MouseButton::LeftButton) { + if (button != Qt::MouseButton::LeftButton) { // only handle left mouse button for now return; } diff --combined src/dolphinpart.rc index 5f57f586a,13f0f2172..5dacc2b81 --- a/src/dolphinpart.rc +++ b/src/dolphinpart.rc @@@ -1,6 -1,6 +1,6 @@@ - + &Edit @@@ -23,6 -23,7 +23,7 @@@ &View + diff --combined src/dolphinui.rc index 3e6e4c463,bbbfb967f..b611b9170 --- a/src/dolphinui.rc +++ b/src/dolphinui.rc @@@ -1,6 -1,6 +1,6 @@@ - + @@@ -43,6 -43,7 +43,7 @@@ + @@@ -73,7 -74,6 +74,7 @@@ + @@@ -107,7 -107,10 +108,7 @@@ Main Toolbar - - - - + @@@ -123,8 -126,6 +124,8 @@@ + + diff --combined src/kitemviews/kfileitemmodel.cpp index 4386bca16,a6f90b9f5..603c16e0d --- a/src/kitemviews/kfileitemmodel.cpp +++ b/src/kitemviews/kfileitemmodel.cpp @@@ -20,9 -20,6 +20,9 @@@ #include #include +#ifndef QT_NO_ACCESSIBILITY +#include +#endif #include #include #include @@@ -37,11 -34,12 +37,12 @@@ Q_GLOBAL_STATIC(QRecursiveMutex, s_coll // #define KFILEITEMMODEL_DEBUG KFileItemModel::KFileItemModel(QObject *parent) - : KItemModelBase("text", parent) + : KItemModelBase("text", "none", parent) , m_dirLister(nullptr) , m_sortDirsFirst(true) , m_sortHiddenLast(false) , m_sortRole(NameRole) + , m_groupRole(NoRole) , m_sortingProgressPercent(-1) , m_roles() , m_itemData() @@@ -205,20 -203,13 +206,20 @@@ bool KFileItemModel::setData(int index return false; } - m_itemData[index]->values = currentValues; if (changedRoles.contains("text")) { QUrl url = m_itemData[index]->item.url(); + m_items.remove(url); url = url.adjusted(QUrl::RemoveFilename); url.setPath(url.path() + currentValues["text"].toString()); m_itemData[index]->item.setUrl(url); + m_items.insert(url, index); + + if (!changedRoles.contains("url")) { + changedRoles.insert("url"); + currentValues["url"] = url; + } } + m_itemData[index]->values = currentValues; emitItemsChangedAndTriggerResorting(KItemRangeList() << KItemRange(index, 1), changedRoles); @@@ -335,32 -326,16 +336,32 @@@ QMimeData *KFileItemModel::createMimeDa return data; } +namespace +{ +QString removeMarks(const QString &original) +{ + const auto normalized = original.normalized(QString::NormalizationForm_D); + QString res; + for (auto ch : normalized) { + if (!ch.isMark()) { + res.append(ch); + } + } + return res; +} +} + int KFileItemModel::indexForKeyboardSearch(const QString &text, int startFromIndex) const { + const auto noMarkText = removeMarks(text); startFromIndex = qMax(0, startFromIndex); for (int i = startFromIndex; i < count(); ++i) { - if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) { + if (removeMarks(fileItem(i).text()).startsWith(noMarkText, Qt::CaseInsensitive)) { return i; } } for (int i = 0; i < startFromIndex; ++i) { - if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) { + if (removeMarks(fileItem(i).text()).startsWith(noMarkText, Qt::CaseInsensitive)) { return i; } } @@@ -413,7 -388,17 +414,17 @@@ QList> KFileItemMo QElapsedTimer timer; timer.start(); #endif - switch (typeForRole(sortRole())) { + QByteArray role = groupRole(); + if (typeForRole(role) == NoRole) { + // Handle extra grouping information + if (m_groupExtraInfo == "followSort") { + role = sortRole(); + } + } + switch (typeForRole(role)) { + case NoRole: + m_groups.clear(); + break; case NameRole: m_groups = nameRoleGroups(); break; @@@ -447,7 -432,7 +458,7 @@@ m_groups = ratingRoleGroups(); break; default: - m_groups = genericStringRoleGroups(sortRole()); + m_groups = genericStringRoleGroups(role); break; } @@@ -912,6 -897,39 +923,39 @@@ void KFileItemModel::removeFilteredChil } } + KFileItemModel::RoleInfo KFileItemModel::roleInformation(const QByteArray &role) + { + static QHash information; + if (information.isEmpty()) { + int count = 0; + const RoleInfoMap *map = rolesInfoMap(count); + for (int i = 0; i < count; ++i) { + RoleInfo info; + info.role = map[i].role; + 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 + // menus tries to put the actions into sub menus otherwise. + info.group = QString(); + } + info.requiresBaloo = map[i].requiresBaloo; + info.requiresIndexer = map[i].requiresIndexer; + if (!map[i].tooltipTranslation.isEmpty()) { + info.tooltip = map[i].tooltipTranslation.toString(); + } else { + info.tooltip = QString(); + } + + information.insert(map[i].role, info); + } + } + + return information.value(role); + } + QList KFileItemModel::rolesInformation() { static QList rolesInfo; @@@ -920,24 -938,7 +964,7 @@@ const RoleInfoMap *map = rolesInfoMap(count); for (int i = 0; i < count; ++i) { if (map[i].roleType != NoRole) { - RoleInfo info; - info.role = map[i].role; - 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 - // menus tries to put the actions into sub menus otherwise. - info.group = QString(); - } - info.requiresBaloo = map[i].requiresBaloo; - info.requiresIndexer = map[i].requiresIndexer; - if (!map[i].tooltipTranslation.isEmpty()) { - info.tooltip = map[i].tooltipTranslation.toString(); - } else { - info.tooltip = QString(); - } + RoleInfo info = roleInformation(map[i].role); rolesInfo.append(info); } } @@@ -946,6 -947,15 +973,15 @@@ return rolesInfo; } + QList KFileItemModel::extraGroupingInformation() + { + static QList rolesInfo{ + {QByteArray("none"), kli18nc("@label", "No grouping").toString(), nullptr, nullptr, false, false}, + {QByteArray("followSort"), kli18nc("@label", "Follow sorting").toString(), nullptr, nullptr, false, false} + }; + return rolesInfo; + } + void KFileItemModel::onGroupedSortingChanged(bool current) { Q_UNUSED(current) @@@ -956,6 -966,13 +992,13 @@@ void KFileItemModel::onSortRoleChanged( { Q_UNUSED(previous) m_sortRole = typeForRole(current); + if (m_sortRole == NoRole) { + // Requested role not in list of roles. This could + // be used for indicating non-trivial sorting behavior + m_sortExtraInfo = current; + } else { + m_sortExtraInfo.clear(); + } if (!m_requestRole[m_sortRole]) { QSet newRoles = m_roles; @@@ -975,6 -992,36 +1018,36 @@@ void KFileItemModel::onSortOrderChanged resortAllItems(); } + void KFileItemModel::onGroupRoleChanged(const QByteArray ¤t, const QByteArray &previous, bool resortItems) + { + Q_UNUSED(previous) + m_groupRole = typeForRole(current); + if (m_groupRole == NoRole) { + // Requested role not in list of roles. This could + // be used for indicating non-trivial grouping behavior + m_groupExtraInfo = current; + } else { + m_groupExtraInfo.clear(); + } + + if (!m_requestRole[m_groupRole]) { + QSet newRoles = m_roles; + newRoles << current; + setRoles(newRoles); + } + + if (resortItems) { + resortAllItems(); + } + } + + void KFileItemModel::onGroupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous) + { + Q_UNUSED(current) + Q_UNUSED(previous) + resortAllItems(); + } + void KFileItemModel::loadSortingSettings() { using Choice = GeneralSettings::EnumSortingChoice; @@@ -997,6 -1044,7 +1070,7 @@@ // Workaround for bug https://bugreports.qt.io/browse/QTBUG-69361 // Force the clean state of QCollator in single thread to avoid thread safety problems in sort m_collator.compare(QString(), QString()); + ContentDisplaySettings::self(); } void KFileItemModel::resortAllItems() @@@ -1062,7 -1110,8 +1136,8 @@@ } Q_EMIT itemsMoved(KItemRange(firstMovedIndex, movedItemsCount), movedToIndexes); - } else if (groupedSorting()) { + } + if (groupedSorting()) { // The groups might have changed even if the order of the items has not. const QList> oldGroups = m_groups; m_groups.clear(); @@@ -1647,7 -1696,7 +1722,7 @@@ void KFileItemModel::removeItems(const QList KFileItemModel::createItemDataList(const QUrl &parentUrl, const KFileItemList &items) const { - if (m_sortRole == TypeRole) { + if (m_sortRole == TypeRole || m_groupRole == TypeRole) { // Try to resolve the MIME-types synchronously to prevent a reordering of // the items when sorting by type (per default MIME-types are resolved // asynchronously by KFileItemModelRolesUpdater). @@@ -1671,9 -1720,9 +1746,9 @@@ return itemDataList; } - void KFileItemModel::prepareItemsForSorting(QList &itemDataList) + void KFileItemModel::prepareItemsWithRole(QList &itemDataList, RoleType roleType) { - switch (m_sortRole) { + switch (roleType) { case ExtensionRole: case PermissionsRole: case OwnerRole: @@@ -1712,6 -1761,12 +1787,12 @@@ } } + void KFileItemModel::prepareItemsForSorting(QList &itemDataList) + { + prepareItemsWithRole(itemDataList, m_sortRole); + prepareItemsWithRole(itemDataList, m_groupRole); + } + int KFileItemModel::expandedParentsCount(const ItemData *data) { // The hash 'values' is only guaranteed to contain the key "expandedParentsCount" @@@ -2045,31 -2100,33 +2126,33 @@@ bool KFileItemModel::lessThan(const Ite } } - // Show hidden files and folders last - if (m_sortHiddenLast) { - const bool isHiddenA = a->item.isHidden(); - const bool isHiddenB = b->item.isHidden(); - if (isHiddenA && !isHiddenB) { - return false; - } else if (!isHiddenA && isHiddenB) { - return true; + result = groupRoleCompare(a, b, collator); + if (result == 0) { + // Show hidden files and folders last + if (m_sortHiddenLast) { + const bool isHiddenA = a->item.isHidden(); + const bool isHiddenB = b->item.isHidden(); + if (isHiddenA && !isHiddenB) { + return false; + } else if (!isHiddenA && isHiddenB) { + return true; + } } - } - - if (m_sortDirsFirst - || (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::ContentCount && m_sortRole == SizeRole)) { - const bool isDirA = a->item.isDir(); - const bool isDirB = b->item.isDir(); - if (isDirA && !isDirB) { - return true; - } else if (!isDirA && isDirB) { - return false; + if (m_sortDirsFirst || (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::ContentCount && m_sortRole == SizeRole)) { + const bool isDirA = a->item.isDir(); + const bool isDirB = b->item.isDir(); + if (isDirA && !isDirB) { + return true; + } else if (!isDirA && isDirB) { + return false; + } } + result = sortRoleCompare(a, b, collator); + result = (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0; + } else { + result = (groupOrder() == Qt::AscendingOrder) ? result < 0 : result > 0; } - - result = sortRoleCompare(a, b, collator); - - return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0; + return result; } void KFileItemModel::sort(const QList::iterator &begin, const QList::iterator &end) const @@@ -2260,29 -2317,114 +2343,131 @@@ int KFileItemModel::sortRoleCompare(con return QString::compare(itemA.url().url(), itemB.url().url(), Qt::CaseSensitive); } + int KFileItemModel::groupRoleCompare(const ItemData *a, const ItemData *b, const QCollator &collator) const + { + // Unlike sortRoleCompare, this function can and often will return 0. + int result = 0; + + ItemGroupInfo groupA, groupB; + switch (m_groupRole) { + case NoRole: + // Non-trivial grouping behavior might be handled there in the future. + return 0; + case NameRole: + groupA = nameRoleGroup(a, false); + groupB = nameRoleGroup(b, false); + break; + case SizeRole: + groupA = sizeRoleGroup(a, false); + groupB = sizeRoleGroup(b, false); + break; + case ModificationTimeRole: + groupA = timeRoleGroup( + [](const ItemData *item) { + return item->item.time(KFileItem::ModificationTime); + }, + a, + false); + groupB = timeRoleGroup( + [](const ItemData *item) { + return item->item.time(KFileItem::ModificationTime); + }, + b, + false); + break; + case CreationTimeRole: + groupA = timeRoleGroup( + [](const ItemData *item) { + return item->item.time(KFileItem::CreationTime); + }, + a, + false); + groupB = timeRoleGroup( + [](const ItemData *item) { + return item->item.time(KFileItem::CreationTime); + }, + b, + false); + break; + case AccessTimeRole: + groupA = timeRoleGroup( + [](const ItemData *item) { + return item->item.time(KFileItem::AccessTime); + }, + a, + false); + groupB = timeRoleGroup( + [](const ItemData *item) { + return item->item.time(KFileItem::AccessTime); + }, + b, + false); + break; + case DeletionTimeRole: + groupA = timeRoleGroup( + [](const ItemData *item) { + return item->values.value("deletiontime").toDateTime(); + }, + a, + false); + groupB = timeRoleGroup( + [](const ItemData *item) { + return item->values.value("deletiontime").toDateTime(); + }, + b, + false); + break; + case PermissionsRole: + groupA = permissionRoleGroup(a, false); + groupB = permissionRoleGroup(b, false); + break; + case RatingRole: + groupA = ratingRoleGroup(a, false); + groupB = ratingRoleGroup(b, false); + break; + case TypeRole: + groupA = typeRoleGroup(a); + groupB = typeRoleGroup(b); + break; + default: { + groupA = genericStringRoleGroup(groupRole(), a); + groupB = genericStringRoleGroup(groupRole(), b); + break; + } + } + if (groupA.comparable < groupB.comparable) { + result = -1; + } else if (groupA.comparable > groupB.comparable) { + result = 1; + } else { + result = stringCompare(groupA.text, groupB.text, collator); + } + return result; + } + int KFileItemModel::stringCompare(const QString &a, const QString &b, const QCollator &collator) const { QMutexLocker collatorLock(s_collatorMutex()); if (m_naturalSorting) { - return collator.compare(a, b); + // Split extension, taking into account it can be empty + constexpr QString::SectionFlags flags = QString::SectionSkipEmpty | QString::SectionIncludeLeadingSep; + + // Sort by baseName first + const QString aBaseName = a.section('.', 0, 0, flags); + const QString bBaseName = b.section('.', 0, 0, flags); + + const int res = collator.compare(aBaseName, bBaseName); + if (res != 0 || (aBaseName.length() == a.length() && bBaseName.length() == b.length())) { + return res; + } + + // sliced() has undefined behavior when pos < 0 or pos > size(). + Q_ASSERT(aBaseName.length() <= a.length() && aBaseName.length() >= 0); + Q_ASSERT(bBaseName.length() <= b.length() && bBaseName.length() >= 0); + + // baseNames were equal, sort by extension + return collator.compare(a.sliced(aBaseName.length()), b.sliced(bBaseName.length())); } const int result = QString::compare(a, b, collator.caseSensitivity()); @@@ -2296,128 -2438,122 +2481,122 @@@ return QString::compare(a, b, Qt::CaseSensitive); } - QList> KFileItemModel::nameRoleGroups() const + KFileItemModel::ItemGroupInfo KFileItemModel::nameRoleGroup(const ItemData *itemData, bool withString) const { - Q_ASSERT(!m_itemData.isEmpty()); - - const int maxIndex = count() - 1; - QList> groups; - - QString groupValue; + static bool oldWithString; + static ItemGroupInfo oldGroupInfo; + static QChar oldFirstChar; + ItemGroupInfo groupInfo; QChar firstChar; - for (int i = 0; i <= maxIndex; ++i) { - if (isChildItem(i)) { - continue; - } - const QString name = m_itemData.at(i)->item.text(); + const QString name = itemData->item.text(); - // Use the first character of the name as group indication - QChar newFirstChar = name.at(0).toUpper(); - if (newFirstChar == QLatin1Char('~') && name.length() > 1) { - newFirstChar = name.at(1).toUpper(); - } - - if (firstChar != newFirstChar) { - QString newGroupValue; - if (newFirstChar.isLetter()) { - if (m_collator.compare(newFirstChar, QChar(QLatin1Char('A'))) >= 0 && m_collator.compare(newFirstChar, QChar(QLatin1Char('Z'))) <= 0) { - // WARNING! Symbols based on latin 'Z' like 'Z' with acute are treated wrong as non Latin and put in a new group. - - // Try to find a matching group in the range 'A' to 'Z'. - static std::vector lettersAtoZ; - lettersAtoZ.reserve('Z' - 'A' + 1); - if (lettersAtoZ.empty()) { - for (char c = 'A'; c <= 'Z'; ++c) { - lettersAtoZ.push_back(QLatin1Char(c)); - } - } + QMutexLocker collatorLock(s_collatorMutex()); - auto localeAwareLessThan = [this](QChar c1, QChar c2) -> bool { - return m_collator.compare(c1, c2) < 0; - }; + // Use the first character of the name as group indication + firstChar = name.at(0).toUpper(); + + if (firstChar == oldFirstChar && withString == oldWithString) { + return oldGroupInfo; + } + if (firstChar == QLatin1Char('~') && name.length() > 1) { + firstChar = name.at(1).toUpper(); + } + if (firstChar.isLetter()) { + if (m_collator.compare(firstChar, QChar(QLatin1Char('A'))) >= 0 && m_collator.compare(firstChar, QChar(QLatin1Char('Z'))) <= 0) { + // WARNING! Symbols based on latin 'Z' like 'Z' with acute are treated wrong as non Latin and put in a new group. + + // Try to find a matching group in the range 'A' to 'Z'. + static std::vector lettersAtoZ; + lettersAtoZ.reserve('Z' - 'A' + 1); + if (lettersAtoZ.empty()) { + for (char c = 'A'; c <= 'Z'; ++c) { + lettersAtoZ.push_back(QLatin1Char(c)); + } + } - std::vector::iterator it = std::lower_bound(lettersAtoZ.begin(), lettersAtoZ.end(), newFirstChar, localeAwareLessThan); - if (it != lettersAtoZ.end()) { - if (localeAwareLessThan(newFirstChar, *it)) { - // newFirstChar belongs to the group preceding *it. - // Example: for an umlaut 'A' in the German locale, *it would be 'B' now. - --it; - } - newGroupValue = *it; - } + auto localeAwareLessThan = [this](QChar c1, QChar c2) -> bool { + return m_collator.compare(c1, c2) < 0; + }; - } else { - // Symbols from non Latin-based scripts - newGroupValue = newFirstChar; + std::vector::iterator it = std::lower_bound(lettersAtoZ.begin(), lettersAtoZ.end(), firstChar, localeAwareLessThan); + if (it != lettersAtoZ.end()) { + if (localeAwareLessThan(firstChar, *it)) { + // newFirstChar belongs to the group preceding *it. + // Example: for an umlaut 'A' in the German locale, *it would be 'B' now. + --it; } - } else if (newFirstChar >= QLatin1Char('0') && newFirstChar <= QLatin1Char('9')) { - // Apply group '0 - 9' for any name that starts with a digit - newGroupValue = i18nc("@title:group Groups that start with a digit", "0 - 9"); - } else { - newGroupValue = i18nc("@title:group", "Others"); + if (withString) { + groupInfo.text = *it; + } + groupInfo.comparable = (*it).unicode(); } - if (newGroupValue != groupValue) { - groupValue = newGroupValue; - groups.append(QPair(i, newGroupValue)); + } else { + // Symbols from non Latin-based scripts + if (withString) { + groupInfo.text = firstChar; } - - firstChar = newFirstChar; + groupInfo.comparable = firstChar.unicode(); + } + } else if (firstChar >= QLatin1Char('0') && firstChar <= QLatin1Char('9')) { + // Apply group '0 - 9' for any name that starts with a digit + if (withString) { + groupInfo.text = i18nc("@title:group Groups that start with a digit", "0 - 9"); } + groupInfo.comparable = (int)'0'; + } else { + if (withString) { + groupInfo.text = i18nc("@title:group", "Others"); + } + groupInfo.comparable = (int)'.'; } - return groups; + oldWithString = withString; + oldFirstChar = firstChar; + oldGroupInfo = groupInfo; + return groupInfo; } - QList> KFileItemModel::sizeRoleGroups() const + KFileItemModel::ItemGroupInfo KFileItemModel::sizeRoleGroup(const ItemData *itemData, bool withString) const { - Q_ASSERT(!m_itemData.isEmpty()); + ItemGroupInfo groupInfo; + KIO::filesize_t fileSize; - const int maxIndex = count() - 1; - QList> groups; + const KFileItem item = itemData->item; + fileSize = !item.isNull() ? item.size() : ~0U; - QString groupValue; - for (int i = 0; i <= maxIndex; ++i) { - if (isChildItem(i)) { - continue; - } - - const KFileItem &item = m_itemData.at(i)->item; - KIO::filesize_t fileSize = !item.isNull() ? item.size() : ~0U; - QString newGroupValue; - if (!item.isNull() && item.isDir()) { - if (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::ContentCount || m_sortDirsFirst) { - newGroupValue = i18nc("@title:group Size", "Folders"); - } else { - fileSize = m_itemData.at(i)->values.value("size").toULongLong(); - } - } - - if (newGroupValue.isEmpty()) { - if (fileSize < 5 * 1024 * 1024) { // < 5 MB - newGroupValue = i18nc("@title:group Size", "Small"); - } else if (fileSize < 10 * 1024 * 1024) { // < 10 MB - newGroupValue = i18nc("@title:group Size", "Medium"); - } else { - newGroupValue = i18nc("@title:group Size", "Big"); - } + groupInfo.comparable = -1; // None + if (!item.isNull() && item.isDir()) { + if (ContentDisplaySettings::directorySizeMode() != ContentDisplaySettings::EnumDirectorySizeMode::ContentSize) { + groupInfo.comparable = 0; // Folders + } else { + fileSize = itemData->values.value("size").toULongLong(); } - - if (newGroupValue != groupValue) { - groupValue = newGroupValue; - groups.append(QPair(i, newGroupValue)); + } + if (groupInfo.comparable < 0) { + if (fileSize < 5 * 1024 * 1024) { // < 5 MB + groupInfo.comparable = 1; // Small + } else if (fileSize < 10 * 1024 * 1024) { // < 10 MB + groupInfo.comparable = 2; // Medium + } else { + groupInfo.comparable = 3; // Big } } - return groups; + if (withString) { + char const *groupNames[] = {"Folders", "Small", "Medium", "Big"}; + groupInfo.text = i18nc("@title:group Size", groupNames[groupInfo.comparable]); + } + return groupInfo; } - QList> KFileItemModel::timeRoleGroups(const std::function &fileTimeCb) const + KFileItemModel::ItemGroupInfo + KFileItemModel::timeRoleGroup(const std::function &fileTimeCb, const ItemData *itemData, bool withString) const { - Q_ASSERT(!m_itemData.isEmpty()); - - const int maxIndex = count() - 1; - QList> groups; + static bool oldWithString; + static ItemGroupInfo oldGroupInfo; + static QDate oldFileDate; + ItemGroupInfo groupInfo; const QDate currentDate = QDate::currentDate(); @@@ -2439,16 -2575,52 +2618,52 @@@ const int daysDistance = fileDate.daysTo(currentDate); - QString newGroupValue; + if (fileDate == oldFileDate && withString == oldWithString) { + return oldGroupInfo; + } + // Simplified grouping algorithm, preserving dates + // but not taking "pretty printing" into account + if (currentDate.year() == fileDate.year() && currentDate.month() == fileDate.month()) { + if (daysDistance < 7) { + groupInfo.comparable = daysDistance; // Today, Yesterday and week days + } else if (daysDistance < 14) { + groupInfo.comparable = 10; // One Week Ago + } else if (daysDistance < 21) { + groupInfo.comparable = 20; // Two Weeks Ago + } else if (daysDistance < 28) { + groupInfo.comparable = 30; // Three Weeks Ago + } else { + groupInfo.comparable = 40; // Earlier This Month + } + } else { + const QDate lastMonthDate = currentDate.addMonths(-1); + if (lastMonthDate.year() == fileDate.year() && lastMonthDate.month() == fileDate.month()) { + if (daysDistance < 7) { + groupInfo.comparable = daysDistance; // Today, Yesterday and week days (Month, Year) + } else if (daysDistance < 14) { + groupInfo.comparable = 11; // One Week Ago (Month, Year) + } else if (daysDistance < 21) { + groupInfo.comparable = 21; // Two Weeks Ago (Month, Year) + } else if (daysDistance < 28) { + groupInfo.comparable = 31; // Three Weeks Ago (Month, Year) + } else { + groupInfo.comparable = 41; // Earlier on Month, Year + } + } else { + // The trick will fail for dates past April, 178956967 or before 1 AD. + groupInfo.comparable = 2147483647 - (fileDate.year() * 12 + fileDate.month() - 1); // Month, Year; newer < older + } + } + if (withString) { if (currentDate.year() == fileDate.year() && currentDate.month() == fileDate.month()) { switch (daysDistance / 7) { case 0: switch (daysDistance) { case 0: - newGroupValue = i18nc("@title:group Date", "Today"); + groupInfo.text = i18nc("@title:group Date", "Today"); break; case 1: - newGroupValue = i18nc("@title:group Date", "Yesterday"); + groupInfo.text = i18nc("@title:group Date", "Yesterday"); break; default: newGroupValue = locale.toString(fileTime, i18nc("@title:group Date: The week day name: dddd", "dddd")); @@@ -2456,21 -2628,21 +2671,21 @@@ "Can be used to script translation of \"dddd\"" "with context @title:group Date", "%1", - newGroupValue); + groupInfo.text); } break; case 1: - newGroupValue = i18nc("@title:group Date", "One Week Ago"); + groupInfo.text = i18nc("@title:group Date", "One Week Ago"); break; case 2: - newGroupValue = i18nc("@title:group Date", "Two Weeks Ago"); + groupInfo.text = i18nc("@title:group Date", "Two Weeks Ago"); break; case 3: - newGroupValue = i18nc("@title:group Date", "Three Weeks Ago"); + groupInfo.text = i18nc("@title:group Date", "Three Weeks Ago"); break; case 4: case 5: - newGroupValue = i18nc("@title:group Date", "Earlier this Month"); + groupInfo.text = i18nc("@title:group Date", "Earlier this Month"); break; default: Q_ASSERT(false); @@@ -2486,13 -2658,13 +2701,13 @@@ "part of the text that should not be formatted as a date", "'Yesterday' (MMMM, yyyy)"); const QString translatedFormat = format.toString(); - if (translatedFormat.count(QLatin1Char('\'')) == 2) { + if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) { newGroupValue = locale.toString(fileTime, translatedFormat); newGroupValue = i18nc( "Can be used to script translation of " "\"'Yesterday' (MMMM, yyyy)\" with context @title:group Date", "%1", - newGroupValue); + groupInfo.text); } else { qCWarning(DolphinDebug).nospace() << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; @@@ -2509,8 -2681,8 +2724,8 @@@ "Can be used to script translation of " "\"dddd (MMMM, yyyy)\" with context @title:group Date", "%1", - newGroupValue); - } else if (daysDistance <= 7 * 2) { + groupInfo.text); + } else if (daysDistance < 7 * 2) { const KLocalizedString format = ki18nc( "@title:group Date: " "MMMM is full month name in current locale, and yyyy is " @@@ -2518,20 -2690,20 +2733,20 @@@ "part of the text that should not be formatted as a date", "'One Week Ago' (MMMM, yyyy)"); const QString translatedFormat = format.toString(); - if (translatedFormat.count(QLatin1Char('\'')) == 2) { + if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) { newGroupValue = locale.toString(fileTime, translatedFormat); newGroupValue = i18nc( "Can be used to script translation of " "\"'One Week Ago' (MMMM, yyyy)\" with context @title:group Date", "%1", - newGroupValue); + groupInfo.text); } else { qCWarning(DolphinDebug).nospace() << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; const QString untranslatedFormat = format.toString({QLatin1String("en_US")}); newGroupValue = locale.toString(fileTime, untranslatedFormat); } - } else if (daysDistance <= 7 * 3) { + } else if (daysDistance < 7 * 3) { const KLocalizedString format = ki18nc( "@title:group Date: " "MMMM is full month name in current locale, and yyyy is " @@@ -2539,20 -2711,20 +2754,20 @@@ "part of the text that should not be formatted as a date", "'Two Weeks Ago' (MMMM, yyyy)"); const QString translatedFormat = format.toString(); - if (translatedFormat.count(QLatin1Char('\'')) == 2) { + if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) { newGroupValue = locale.toString(fileTime, translatedFormat); newGroupValue = i18nc( "Can be used to script translation of " "\"'Two Weeks Ago' (MMMM, yyyy)\" with context @title:group Date", "%1", - newGroupValue); + groupInfo.text); } else { qCWarning(DolphinDebug).nospace() << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; const QString untranslatedFormat = format.toString({QLatin1String("en_US")}); newGroupValue = locale.toString(fileTime, untranslatedFormat); } - } else if (daysDistance <= 7 * 4) { + } else if (daysDistance < 7 * 4) { const KLocalizedString format = ki18nc( "@title:group Date: " "MMMM is full month name in current locale, and yyyy is " @@@ -2560,13 -2732,13 +2775,13 @@@ "part of the text that should not be formatted as a date", "'Three Weeks Ago' (MMMM, yyyy)"); const QString translatedFormat = format.toString(); - if (translatedFormat.count(QLatin1Char('\'')) == 2) { + if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) { newGroupValue = locale.toString(fileTime, translatedFormat); newGroupValue = i18nc( "Can be used to script translation of " "\"'Three Weeks Ago' (MMMM, yyyy)\" with context @title:group Date", "%1", - newGroupValue); + groupInfo.text); } else { qCWarning(DolphinDebug).nospace() << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; @@@ -2581,13 -2753,13 +2796,13 @@@ "part of the text that should not be formatted as a date", "'Earlier on' MMMM, yyyy"); const QString translatedFormat = format.toString(); - if (translatedFormat.count(QLatin1Char('\'')) == 2) { + if (const int count = translatedFormat.count(QLatin1Char('\'')); count >= 2 && count % 2 == 0) { newGroupValue = locale.toString(fileTime, translatedFormat); newGroupValue = i18nc( "Can be used to script translation of " "\"'Earlier on' MMMM, yyyy\" with context @title:group Date", "%1", - newGroupValue); + groupInfo.text); } else { qCWarning(DolphinDebug).nospace() << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; @@@ -2605,134 -2777,245 +2820,245 @@@ "Can be used to script translation of " "\"MMMM, yyyy\" with context @title:group Date", "%1", - newGroupValue); + groupInfo.text); } } - - if (newGroupValue != groupValue) { - groupValue = newGroupValue; - groups.append(QPair(i, newGroupValue)); - } } - - return groups; + oldWithString = withString; + oldFileDate = fileDate; + oldGroupInfo = groupInfo; + return groupInfo; } - QList> KFileItemModel::permissionRoleGroups() const + KFileItemModel::ItemGroupInfo KFileItemModel::permissionRoleGroup(const ItemData *itemData, bool withString) const { - Q_ASSERT(!m_itemData.isEmpty()); + static bool oldWithString; + static ItemGroupInfo oldGroupInfo; + static QFileDevice::Permissions oldPermissions; + ItemGroupInfo groupInfo; - const int maxIndex = count() - 1; - QList> groups; - - QString permissionsString; - QString groupValue; - for (int i = 0; i <= maxIndex; ++i) { - if (isChildItem(i)) { - continue; - } - - const ItemData *itemData = m_itemData.at(i); - const QString newPermissionsString = itemData->values.value("permissions").toString(); - if (newPermissionsString == permissionsString) { - continue; - } - permissionsString = newPermissionsString; - - const QFileInfo info(itemData->item.url().toLocalFile()); + const QFileInfo info(itemData->item.url().toLocalFile()); + const QFileDevice::Permissions permissions = info.permissions(); + if (permissions == oldPermissions && withString == oldWithString) { + return oldGroupInfo; + } + groupInfo.comparable = (int)permissions; + if (withString) { // Set user string QString user; - if (info.permission(QFile::ReadUser)) { + if (permissions & QFile::ReadUser) { user = i18nc("@item:intext Access permission, concatenated", "Read, "); } - if (info.permission(QFile::WriteUser)) { + if (permissions & QFile::WriteUser) { user += i18nc("@item:intext Access permission, concatenated", "Write, "); } - if (info.permission(QFile::ExeUser)) { + if (permissions & QFile::ExeUser) { user += i18nc("@item:intext Access permission, concatenated", "Execute, "); } user = user.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : user.mid(0, user.length() - 2); // Set group string QString group; - if (info.permission(QFile::ReadGroup)) { + if (permissions & QFile::ReadGroup) { group = i18nc("@item:intext Access permission, concatenated", "Read, "); } - if (info.permission(QFile::WriteGroup)) { + if (permissions & QFile::WriteGroup) { group += i18nc("@item:intext Access permission, concatenated", "Write, "); } - if (info.permission(QFile::ExeGroup)) { + if (permissions & QFile::ExeGroup) { group += i18nc("@item:intext Access permission, concatenated", "Execute, "); } group = group.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : group.mid(0, group.length() - 2); // Set others string QString others; - if (info.permission(QFile::ReadOther)) { + if (permissions & QFile::ReadOther) { others = i18nc("@item:intext Access permission, concatenated", "Read, "); } - if (info.permission(QFile::WriteOther)) { + if (permissions & QFile::WriteOther) { others += i18nc("@item:intext Access permission, concatenated", "Write, "); } - if (info.permission(QFile::ExeOther)) { + if (permissions & QFile::ExeOther) { others += i18nc("@item:intext Access permission, concatenated", "Execute, "); } others = others.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : others.mid(0, others.length() - 2); + groupInfo.text = i18nc("@title:group Files and folders by permissions", "User: %1 | Group: %2 | Others: %3", user, group, others); + } + oldWithString = withString; + oldPermissions = permissions; + oldGroupInfo = groupInfo; + return groupInfo; + } + + KFileItemModel::ItemGroupInfo KFileItemModel::ratingRoleGroup(const ItemData *itemData, bool withString) const + { + ItemGroupInfo groupInfo; + groupInfo.comparable = itemData->values.value("rating", 0).toInt(); + if (withString) { + // Dolphin does not currently use string representation of star rating + // as stars are rendered as graphics in group headers. + groupInfo.text = i18nc("@item:intext Rated N (stars)", "Rated %i", QString::number(groupInfo.comparable)); + } + return groupInfo; + } + + KFileItemModel::ItemGroupInfo KFileItemModel::genericStringRoleGroup(const QByteArray &role, const ItemData *itemData) const + { + return {0, itemData->values.value(role).toString()}; + } + + QList> KFileItemModel::nameRoleGroups() const + { + Q_ASSERT(!m_itemData.isEmpty()); + + const int maxIndex = count() - 1; + QList> groups; + + ItemGroupInfo groupInfo; + for (int i = 0; i <= maxIndex; ++i) { + if (isChildItem(i)) { + continue; + } + + ItemGroupInfo newGroupInfo = nameRoleGroup(m_itemData.at(i)); - const QString newGroupValue = i18nc("@title:group Files and folders by permissions", "User: %1 | Group: %2 | Others: %3", user, group, others); - if (newGroupValue != groupValue) { - groupValue = newGroupValue; - groups.append(QPair(i, newGroupValue)); + if (newGroupInfo != groupInfo) { + groupInfo = newGroupInfo; + groups.append(QPair(i, newGroupInfo.text)); } } + return groups; + } + + QList> KFileItemModel::sizeRoleGroups() const + { + Q_ASSERT(!m_itemData.isEmpty()); + + const int maxIndex = count() - 1; + QList> groups; + + ItemGroupInfo groupInfo; + for (int i = 0; i <= maxIndex; ++i) { + if (isChildItem(i)) { + continue; + } + + ItemGroupInfo newGroupInfo = sizeRoleGroup(m_itemData.at(i)); + if (newGroupInfo != groupInfo) { + groupInfo = newGroupInfo; + groups.append(QPair(i, newGroupInfo.text)); + } + } return groups; } - QList> KFileItemModel::ratingRoleGroups() const + KFileItemModel::ItemGroupInfo KFileItemModel::typeRoleGroup(const ItemData *itemData) const + { + int priority = 0; + if (itemData->item.isDir() && m_sortDirsFirst) { + // Ensure folders stay first regardless of grouping order + if (groupOrder() == Qt::AscendingOrder) { + priority = -1; + } else { + priority = 1; + } + } + return {priority, itemData->values.value("type").toString()}; + } + + QList> KFileItemModel::timeRoleGroups(const std::function &fileTimeCb) const { Q_ASSERT(!m_itemData.isEmpty()); const int maxIndex = count() - 1; QList> groups; - int groupValue = -1; + ItemGroupInfo groupInfo; for (int i = 0; i <= maxIndex; ++i) { if (isChildItem(i)) { continue; } - const int newGroupValue = m_itemData.at(i)->values.value("rating", 0).toInt(); - if (newGroupValue != groupValue) { - groupValue = newGroupValue; - groups.append(QPair(i, newGroupValue)); + + ItemGroupInfo newGroupInfo = timeRoleGroup(fileTimeCb, m_itemData.at(i)); + + if (newGroupInfo != groupInfo) { + groupInfo = newGroupInfo; + groups.append(QPair(i, newGroupInfo.text)); } } + return groups; + } + QList> KFileItemModel::permissionRoleGroups() const + { + Q_ASSERT(!m_itemData.isEmpty()); + + const int maxIndex = count() - 1; + QList> groups; + + ItemGroupInfo groupInfo; + for (int i = 0; i <= maxIndex; ++i) { + if (isChildItem(i)) { + continue; + } + + ItemGroupInfo newGroupInfo = permissionRoleGroup(m_itemData.at(i)); + + if (newGroupInfo != groupInfo) { + groupInfo = newGroupInfo; + groups.append(QPair(i, newGroupInfo.text)); + } + } return groups; } - QList> KFileItemModel::genericStringRoleGroups(const QByteArray &role) const + QList> KFileItemModel::ratingRoleGroups() const { Q_ASSERT(!m_itemData.isEmpty()); const int maxIndex = count() - 1; QList> groups; - bool isFirstGroupValue = true; - QString groupValue; + ItemGroupInfo groupInfo; for (int i = 0; i <= maxIndex; ++i) { if (isChildItem(i)) { continue; } - const QString newGroupValue = m_itemData.at(i)->values.value(role).toString(); - if (newGroupValue != groupValue || isFirstGroupValue) { - groupValue = newGroupValue; - groups.append(QPair(i, newGroupValue)); - isFirstGroupValue = false; + + ItemGroupInfo newGroupInfo = ratingRoleGroup(m_itemData.at(i)); + + if (newGroupInfo != groupInfo) { + groupInfo = newGroupInfo; + // Using the numeric representation because Dolphin has a special + // case for drawing stars. + groups.append(QPair(i, newGroupInfo.comparable)); } } + return groups; + } + + QList> KFileItemModel::genericStringRoleGroups(const QByteArray &role) const + { + Q_ASSERT(!m_itemData.isEmpty()); + + const int maxIndex = count() - 1; + QList> groups; + ItemGroupInfo groupInfo; + for (int i = 0; i <= maxIndex; ++i) { + if (isChildItem(i)) { + continue; + } + + ItemGroupInfo newGroupInfo = genericStringRoleGroup(role, m_itemData.at(i)); + + if (newGroupInfo != groupInfo) { + groupInfo = newGroupInfo; + groups.append(QPair(i, newGroupInfo.text)); + } + } return groups; } @@@ -2769,7 -3052,7 +3095,7 @@@ const KFileItemModel::RoleInfoMap *KFil static const RoleInfoMap rolesInfoMap[] = { // clang-format off // | role | roleType | role translation | group translation | requires Baloo | requires indexer - { nullptr, NoRole, KLazyLocalizedString(), KLazyLocalizedString(), KLazyLocalizedString(), false, false }, + { nullptr, NoRole, kli18nc("@label", "None"), KLazyLocalizedString(), KLazyLocalizedString(), false, false }, { "text", NameRole, kli18nc("@label", "Name"), KLazyLocalizedString(), KLazyLocalizedString(), false, false }, { "size", SizeRole, kli18nc("@label", "Size"), KLazyLocalizedString(), KLazyLocalizedString(), false, false }, { "modificationtime", ModificationTimeRole, kli18nc("@label", "Modified"), KLazyLocalizedString(), kli18nc("@tooltip", "The date format can be selected in settings."), false, false }, @@@ -2799,7 -3082,6 +3125,7 @@@ { "releaseYear", ReleaseYearRole, kli18nc("@label", "Release Year"), kli18nc("@label", "Audio"), KLazyLocalizedString(), true, true }, { "aspectRatio", AspectRatioRole, kli18nc("@label", "Aspect Ratio"), kli18nc("@label", "Video"), KLazyLocalizedString(), true, true }, { "frameRate", FrameRateRole, kli18nc("@label", "Frame Rate"), kli18nc("@label", "Video"), KLazyLocalizedString(), true, true }, + { "duration", DurationRole, kli18nc("@label", "Duration"), kli18nc("@label", "Video"), KLazyLocalizedString(), true, true }, { "path", PathRole, kli18nc("@label", "Path"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false }, { "extension", ExtensionRole, kli18nc("@label", "File Extension"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false }, { "deletiontime", DeletionTimeRole, kli18nc("@label", "Deletion Time"), kli18nc("@label", "Other"), KLazyLocalizedString(), false, false }, diff --combined src/kitemviews/kfileitemmodel.h index 10be27128,980efe66b..13554d8c7 --- a/src/kitemviews/kfileitemmodel.h +++ b/src/kitemviews/kfileitemmodel.h @@@ -196,6 -196,13 +196,13 @@@ public bool requiresIndexer; }; + /** + * @return Provides static information for a role that is supported + * by KFileItemModel. Some roles can only be determined if + * Baloo is enabled and/or the Baloo indexing is enabled. + */ + static RoleInfo roleInformation(const QByteArray &role); + /** * @return Provides static information for all available roles that * are supported by KFileItemModel. Some roles can only be @@@ -204,6 -211,13 +211,13 @@@ */ static QList rolesInformation(); + /** + * @return Provides static information for all available grouping + * behaviors supported by KFileItemModel but not directly + * mapped to roles of KFileItemModel. + */ + static QList extraGroupingInformation(); + /** set to true to hide application/x-trash files */ void setShowTrashMime(bool show); @@@ -287,11 -301,13 +301,13 @@@ protected void onGroupedSortingChanged(bool current) override; void onSortRoleChanged(const QByteArray ¤t, const QByteArray &previous, bool resortItems = true) override; void onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous) override; + void onGroupRoleChanged(const QByteArray ¤t, const QByteArray &previous, bool resortItems = true) override; + void onGroupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous) override; private Q_SLOTS: /** - * Resorts all items dependent on the set sortRole(), sortOrder() - * and foldersFirst() settings. + * Resorts all items dependent on the set sortRole(), sortOrder(), + * groupRole(), groupOrder() and foldersFirst() settings. */ void resortAllItems(); @@@ -364,13 -380,18 +380,18 @@@ private QHash values; ItemData *parent; }; -- - enum RemoveItemsBehavior { - KeepItemData, - DeleteItemData, - DeleteItemDataIfUnfiltered ++ + struct ItemGroupInfo { + int comparable; + QString text; + + bool operator==(const ItemGroupInfo &other) const; + bool operator!=(const ItemGroupInfo &other) const; + bool operator<(const ItemGroupInfo &other) const; }; + enum RemoveItemsBehavior { KeepItemData, DeleteItemData, DeleteItemDataIfUnfiltered }; + void insertItems(QList &items); void removeItems(const KItemRangeList &itemRanges, RemoveItemsBehavior behavior); @@@ -382,6 -403,12 +403,12 @@@ */ QList createItemDataList(const QUrl &parentUrl, const KFileItemList &items) const; + /** + * Helper method for prepareItemsForSorting(). + * For a set role, fills 'values' of ItemData non-lazily. + */ + void prepareItemsWithRole(QList &itemDataList, RoleType roleType); + /** * Prepares the items for sorting. Normally, the hash 'values' in ItemData is filled * lazily to save time and memory, but for some sort roles, it is expected that the @@@ -449,13 -476,29 +476,29 @@@ */ int sortRoleCompare(const ItemData *a, const ItemData *b, const QCollator &collator) const; + /** + * Helper method for lessThan() and expandedParentsCountCompare(): Compares + * the passed item-data using m_groupRole as criteria. Both items must + * have the same parent item, otherwise the comparison will be wrong. + */ + int groupRoleCompare(const ItemData *a, const ItemData *b, const QCollator &collator) const; + int stringCompare(const QString &a, const QString &b, const QCollator &collator) const; + ItemGroupInfo nameRoleGroup(const ItemData *itemData, bool withString = true) const; + ItemGroupInfo sizeRoleGroup(const ItemData *itemData, bool withString = true) const; + ItemGroupInfo timeRoleGroup(const std::function &fileTimeCb, const ItemData *itemData, bool withString = true) const; + ItemGroupInfo permissionRoleGroup(const ItemData *itemData, bool withString = true) const; + ItemGroupInfo ratingRoleGroup(const ItemData *itemData, bool withString = true) const; + ItemGroupInfo typeRoleGroup(const ItemData *itemData) const; + ItemGroupInfo genericStringRoleGroup(const QByteArray &role, const ItemData *itemData) const; + QList> nameRoleGroups() const; QList> sizeRoleGroups() const; QList> timeRoleGroups(const std::function &fileTimeCb) const; QList> permissionRoleGroups() const; QList> ratingRoleGroups() const; + QList> typeRoleGroups() const; QList> genericStringRoleGroups(const QByteArray &typeForRole) const; /** @@@ -546,6 -589,10 +589,10 @@@ private bool m_sortHiddenLast; RoleType m_sortRole; + RoleType m_groupRole; + QByteArray m_sortExtraInfo; + QByteArray m_groupExtraInfo; + int m_sortingProgressPercent; // Value of directorySortingProgress() signal QSet m_roles; @@@ -592,9 -639,7 +639,9 @@@ inline bool KFileItemModel::isRoleValue inline bool KFileItemModel::nameLessThan(const ItemData *a, const ItemData *b) { - return a->item.text() < b->item.text(); + // Split extension, taking into account it can be empty + constexpr QString::SectionFlags flags = QString::SectionSkipEmpty | QString::SectionIncludeLeadingSep; + return a->item.text().section('.', 0, 0, flags) < b->item.text().section('.', 0, 0, flags); } inline bool KFileItemModel::isChildItem(int index) const @@@ -606,4 -651,14 +653,14 @@@ } } + inline bool KFileItemModel::ItemGroupInfo::operator==(const ItemGroupInfo &other) const + { + return comparable == other.comparable && text == other.text; + } + + inline bool KFileItemModel::ItemGroupInfo::operator!=(const ItemGroupInfo &other) const + { + return comparable != other.comparable || text != other.text; + } + #endif diff --combined src/kitemviews/kfileitemmodelrolesupdater.cpp index 497c13b52,318936e8a..44165dedc --- a/src/kitemviews/kfileitemmodelrolesupdater.cpp +++ b/src/kitemviews/kfileitemmodelrolesupdater.cpp @@@ -369,29 -369,20 +369,28 @@@ void KFileItemModelRolesUpdater::slotIt timer.start(); // Determine the sort role synchronously for as many items as possible. - if (m_resolvableRoles.contains(m_model->sortRole())) { - QList dirsWithAddedItems; - + if (m_resolvableRoles.contains(m_model->sortRole()) || m_resolvableRoles.contains(m_model->groupRole())) { ++ QList dirsWithAddedItems; int insertedCount = 0; for (const KItemRange &range : itemRanges) { const int lastIndex = insertedCount + range.index + range.count - 1; for (int i = insertedCount + range.index; i <= lastIndex; ++i) { + const auto fileItem = m_model->fileItem(i); + const auto fileItemParentFolderUrl = fileItem.url().adjusted(QUrl::RemoveFilename); + if (!dirsWithAddedItems.contains(fileItemParentFolderUrl)) { + dirsWithAddedItems.append(fileItemParentFolderUrl); + } if (timer.elapsed() < MaxBlockTimeout) { applySortRole(i); } else { - m_pendingSortRoleItems.insert(m_model->fileItem(i)); + m_pendingSortRoleItems.insert(fileItem); } } insertedCount += range.count; } + recountDirectoryItems(dirsWithAddedItems); + applySortProgressToModel(); // If there are still items whose sort role is unknown, check if the @@@ -448,25 -439,17 +447,25 @@@ void KFileItemModelRolesUpdater::slotIt m_directoryContentsCounter->stopWorker(); } } else { + QList dirsWithDeletedItems; // Only remove the items from m_finishedItems. They will be removed // from the other sets later on. QSet::iterator it = m_finishedItems.begin(); while (it != m_finishedItems.end()) { if (m_model->index(*it) < 0) { + // Get the folder path of the file. + const auto folderUrl = it->url().adjusted(QUrl::RemoveFilename); + if (!dirsWithDeletedItems.contains(folderUrl)) { + dirsWithDeletedItems.append(folderUrl); + } it = m_finishedItems.erase(it); } else { ++it; } } + recountDirectoryItems(dirsWithDeletedItems); + // Removed items won't have hover previews loaded anymore. for (const KItemRange &itemRange : itemRanges) { int index = itemRange.index; @@@ -568,14 -551,30 +567,14 @@@ void KFileItemModelRolesUpdater::slotGo return; } - QPixmap scaledPixmap = transformPreviewPixmap(pixmap); - QHash data = rolesData(item, index); - - const QStringList overlays = data["iconOverlays"].toStringList(); - // Strangely KFileItem::overlays() returns empty string-values, so - // we need to check first whether an overlay must be drawn at all. - if (!scaledPixmap.isNull()) { - for (const QString &overlay : overlays) { - if (!overlay.isEmpty()) { - // There is at least one overlay, draw all overlays above m_pixmap - // and cancel the check - const QSize size = scaledPixmap.size(); - scaledPixmap = KIconUtils::addOverlays(scaledPixmap, overlays).pixmap(size); - break; - } - } - } - - data.insert("iconPixmap", scaledPixmap); + data.insert("iconPixmap", transformPreviewPixmap(pixmap)); + data.insert("supportsSequencing", m_previewJob->handlesSequences()); disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); m_model->setData(index, data); connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); + Q_EMIT previewJobFinished(); // For unit testing m_finishedItems.insert(item); } @@@ -977,6 -976,13 +976,6 @@@ void KFileItemModelRolesUpdater::startP return; } - // PreviewJob internally caches items always with the size of - // 128 x 128 pixels or 256 x 256 pixels. A (slow) downscaling is done - // by PreviewJob if a smaller size is requested. For images KFileItemModelRolesUpdater must - // do a downscaling anyhow because of the frame, so in this case only the provided - // cache sizes are requested. - const QSize cacheSize = (m_iconSize.width() > 128) || (m_iconSize.height() > 128) ? QSize(256, 256) : QSize(128, 128); - // KIO::filePreview() will request the MIME-type of all passed items, which (in the // worst case) might block the application for several seconds. To prevent such // a blocking, we only pass items with known mime type to the preview job. @@@ -1005,7 -1011,7 +1004,7 @@@ } while (!m_pendingPreviewItems.isEmpty() && timer.elapsed() < MaxBlockTimeout); } - KIO::PreviewJob *job = new KIO::PreviewJob(itemSubSet, cacheSize, &m_enabledPlugins); + KIO::PreviewJob *job = new KIO::PreviewJob(itemSubSet, cacheSize(), &m_enabledPlugins); job->setDevicePixelRatio(m_devicePixelRatio); job->setIgnoreMaximumSize(itemSubSet.first().isLocalFile() && !itemSubSet.first().isSlow() && m_localFileSizePreviewLimit <= 0); if (job->uiDelegate()) { @@@ -1023,11 -1029,7 +1022,11 @@@ QPixmap KFileItemModelRolesUpdater::tra { QPixmap scaledPixmap = pixmap; - if (!pixmap.hasAlpha() && !pixmap.isNull() && m_iconSize.width() > KIconLoader::SizeSmallMedium && m_iconSize.height() > KIconLoader::SizeSmallMedium) { + if (pixmap.isNull()) { + return scaledPixmap; + } + + if (!pixmap.hasAlpha() && m_iconSize.width() > KIconLoader::SizeSmallMedium && m_iconSize.height() > KIconLoader::SizeSmallMedium) { if (m_enlargeSmallPreviews) { KPixmapModifier::applyFrame(scaledPixmap, m_iconSize); } else { @@@ -1055,7 -1057,7 +1054,7 @@@ KPixmapModifier::applyFrame(scaledPixmap, m_iconSize); } } - } else if (!pixmap.isNull()) { + } else { KPixmapModifier::scale(scaledPixmap, m_iconSize * m_devicePixelRatio); scaledPixmap.setDevicePixelRatio(m_devicePixelRatio); } @@@ -1063,16 -1065,6 +1062,16 @@@ return scaledPixmap; } +QSize KFileItemModelRolesUpdater::cacheSize() +{ + // PreviewJob internally caches items always with the size of + // 128 x 128 pixels or 256 x 256 pixels. A (slow) downscaling is done + // by PreviewJob if a smaller size is requested. For images KFileItemModelRolesUpdater must + // do a downscaling anyhow because of the frame, so in this case only the provided + // cache sizes are requested. + return (m_iconSize.width() > 128) || (m_iconSize.height() > 128) ? QSize(256, 256) : QSize(128, 128); +} + void KFileItemModelRolesUpdater::loadNextHoverSequencePreview() { if (m_hoverSequenceItem.isNull() || m_hoverSequencePreviewJob) { @@@ -1113,7 -1105,14 +1112,7 @@@ return; } - // PreviewJob internally caches items always with the size of - // 128 x 128 pixels or 256 x 256 pixels. A (slow) downscaling is done - // by PreviewJob if a smaller size is requested. For images KFileItemModelRolesUpdater must - // do a downscaling anyhow because of the frame, so in this case only the provided - // cache sizes are requested. - const QSize cacheSize = (m_iconSize.width() > 128) || (m_iconSize.height() > 128) ? QSize(256, 256) : QSize(128, 128); - - KIO::PreviewJob *job = new KIO::PreviewJob({m_hoverSequenceItem}, cacheSize, &m_enabledPlugins); + KIO::PreviewJob *job = new KIO::PreviewJob({m_hoverSequenceItem}, cacheSize(), &m_enabledPlugins); job->setSequenceIndex(loadSeqIdx); job->setIgnoreMaximumSize(m_hoverSequenceItem.isLocalFile() && !m_hoverSequenceItem.isSlow() && m_localFileSizePreviewLimit <= 0); @@@ -1218,13 -1217,13 +1217,13 @@@ void KFileItemModelRolesUpdater::applyS QHash data; const KFileItem item = m_model->fileItem(index); - if (m_model->sortRole() == "type") { + if (m_model->sortRole() == "type" || m_model->groupRole() == "type") { if (!item.isMimeTypeKnown()) { item.determineMimeType(); } data.insert("type", item.mimeComment()); - } else if (m_model->sortRole() == "size" && item.isLocalFile() && item.isDir()) { + } else if ((m_model->sortRole() == "size" || m_model->groupRole() == "size") && item.isLocalFile() && item.isDir()) { startDirectorySizeCounting(item, index); return; } else { @@@ -1288,20 -1287,13 +1287,20 @@@ bool KFileItemModelRolesUpdater::applyR void KFileItemModelRolesUpdater::startDirectorySizeCounting(const KFileItem &item, int index) { - if (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::None || !item.isLocalFile()) { + if (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::None) { + return; + } + + // Set any remote files to unknown size (-1). + if (!item.isLocalFile()) { + resetSizeData(index, -1); return; + } else { + resetSizeData(index); } if (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::ContentCount || item.isSlow()) { // fastpath no recursion necessary - auto data = m_model->data(index); if (data.value("size") == -2) { // means job already started @@@ -1321,33 -1313,28 +1320,33 @@@ connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); auto listJob = KIO::listDir(url, KIO::HideProgressInfo); - QObject::connect(listJob, &KIO::ListJob::entries, this, [this, item](const KJob * /*job*/, const KIO::UDSEntryList &list) { + + QObject::connect(listJob, &KIO::ListJob::entries, this, [this, item](const KJob *job, const KIO::UDSEntryList &list) { int index = m_model->index(item); if (index < 0) { return; } auto data = m_model->data(index); int origCount = data.value("count").toInt(); - int entryCount = origCount; + // Get the amount of processed items... + int entryCount = job->processedAmount(KJob::Bytes); + // ...and then remove the unwanted items from the amount. for (const KIO::UDSEntry &entry : list) { const auto name = entry.stringValue(KIO::UDSEntry::UDS_NAME); if (name == QStringLiteral("..") || name == QStringLiteral(".")) { + --entryCount; continue; } if (!m_model->showHiddenFiles() && name.startsWith(QLatin1Char('.'))) { + --entryCount; continue; } if (m_model->showDirectoriesOnly() && !entry.isDir()) { + --entryCount; continue; } - ++entryCount; } QHash newData; @@@ -1358,6 -1345,7 +1357,6 @@@ } if (origCount != entryCount) { - // count has changed newData.insert("count", entryCount); } @@@ -1540,27 -1528,4 +1539,27 @@@ void KFileItemModelRolesUpdater::trimHo } } +void KFileItemModelRolesUpdater::resetSizeData(const int index, const int size) +{ + disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); + auto data = m_model->data(index); + data.insert("size", size); + m_model->setData(index, data); + connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); +} + +void KFileItemModelRolesUpdater::recountDirectoryItems(const QList directories) +{ + for (const auto &dir : directories) { + auto index = m_model->index(dir); + if (index < 0) { + continue; + } + auto item = m_model->fileItem(index); + if (item.isDir()) { + startDirectorySizeCounting(item, index); + } + } +} + #include "moc_kfileitemmodelrolesupdater.cpp" diff --combined src/kitemviews/kfileitemmodelrolesupdater.h index 56e28ce72,df3a94226..73e42cf17 --- a/src/kitemviews/kfileitemmodelrolesupdater.h +++ b/src/kitemviews/kfileitemmodelrolesupdater.h @@@ -178,9 -178,6 +178,9 @@@ public */ void setHoverSequenceState(const QUrl &itemUrl, int seqIdx); +Q_SIGNALS: + void previewJobFinished(); // For unit testing + private Q_SLOTS: void slotItemsInserted(const KItemRangeList &itemRanges); void slotItemsRemoved(const KItemRangeList &itemRanges); @@@ -296,7 -293,6 +296,7 @@@ private * Transforms a raw preview image, applying scale and frame. * * @param pixmap A raw preview image from a PreviewJob. + * @param overlays the overlays to add to the pixmap * @return The scaled and decorated preview image. */ QPixmap transformPreviewPixmap(const QPixmap &pixmap); @@@ -319,6 -315,8 +319,8 @@@ /** * Resolves the sort role of the item and applies it to the model. + * Despite the name, this handles both sorting and grouping, as + * regrouping never happens without resorting at the same time. */ void applySortRole(int index); @@@ -340,12 -338,7 +342,12 @@@ void trimHoverSequenceLoadedItems(); + void resetSizeData(const int index, const int size = 0); + + void recountDirectoryItems(const QList directories); + private: + QSize cacheSize(); /** * enqueue directory size counting for KFileItem item at index */ diff --combined src/kitemviews/kitemlistview.cpp index 75d85be35,45f5851bf..82325cb19 --- a/src/kitemviews/kitemlistview.cpp +++ b/src/kitemviews/kitemlistview.cpp @@@ -8,16 -8,12 +8,16 @@@ #include "kitemlistview.h" +#ifndef QT_NO_ACCESSIBILITY +#include "accessibility/kitemlistcontaineraccessible.h" +#include "accessibility/kitemlistdelegateaccessible.h" +#include "accessibility/kitemlistviewaccessible.h" +#endif #include "dolphindebug.h" #include "kitemlistcontainer.h" #include "kitemlistcontroller.h" #include "kitemlistheader.h" #include "kitemlistselectionmanager.h" -#include "kitemlistviewaccessible.h" #include "kstandarditemlistwidget.h" #include "private/kitemlistheaderwidget.h" @@@ -25,8 -21,6 +25,8 @@@ #include "private/kitemlistsizehintresolver.h" #include "private/kitemlistviewlayouter.h" +#include + #include #include #include @@@ -106,7 -100,6 +106,7 @@@ KItemListView::KItemListView(QGraphicsW , m_header(nullptr) , m_headerWidget(nullptr) , m_indicatorAnimation(nullptr) + , m_statusBarOffset(0) , m_dropIndicator() , m_sizeHintResolver(nullptr) { @@@ -193,7 -186,7 +193,7 @@@ qreal KItemListView::scrollOffset() con qreal KItemListView::maximumScrollOffset() const { - return m_layouter->maximumScrollOffset(); + return m_layouter->maximumScrollOffset() + m_statusBarOffset; } void KItemListView::setItemOffset(qreal offset) @@@ -386,7 -379,7 +386,7 @@@ void KItemListView::setGeometry(const Q if (m_headerWidget->automaticColumnResizing()) { applyAutomaticColumnWidths(); } else { - const qreal requiredWidth = columnWidthsSum() + 2 * m_headerWidget->sidePadding(); + const qreal requiredWidth = m_headerWidget->leftPadding() + columnWidthsSum() + m_headerWidget->rightPadding(); const QSizeF dynamicItemSize(qMax(newSize.width(), requiredWidth), m_itemSize.height()); m_layouter->setItemSize(dynamicItemSize); } @@@ -398,12 -391,6 +398,12 @@@ doLayout(NoAnimation); } +qreal KItemListView::scrollSingleStep() const +{ + const QFontMetrics metrics(font()); + return metrics.height(); +} + qreal KItemListView::verticalPageStep() const { qreal headerHeight = 0; @@@ -425,7 -412,7 +425,7 @@@ std::optional KItemListView::itemA const KItemListWidget *widget = it.value(); const QPointF mappedPos = widget->mapFromItem(this, pos); - if (widget->contains(mappedPos) || widget->selectionRect().contains(mappedPos)) { + if (widget->contains(mappedPos)) { return it.key(); } } @@@ -542,7 -529,7 +542,7 @@@ QRectF KItemListView::itemContextRect(i const KItemListWidget *widget = m_visibleItems.value(index); if (widget) { - contextRect = widget->iconRect() | widget->textRect(); + contextRect = widget->selectionRectCore(); contextRect.translate(itemRect(index).topLeft()); } @@@ -561,9 -548,6 +561,9 @@@ void KItemListView::scrollToItem(int in const qreal headerHeight = m_headerWidget->size().height(); viewGeometry.adjust(0, headerHeight, 0, 0); } + if (m_statusBarOffset != 0) { + viewGeometry.adjust(0, 0, 0, -m_statusBarOffset); + } QRectF currentRect = itemRect(index); if (layoutDirection() == Qt::RightToLeft && scrollOrientation() == Qt::Horizontal) { @@@ -750,16 -734,11 +750,16 @@@ QPixmap KItemListView::createDragPixmap void KItemListView::editRole(int index, const QByteArray &role) { KStandardItemListWidget *widget = qobject_cast(m_visibleItems.value(index)); - if (!widget || m_editingRole) { + if (!widget) { + return; + } + if (m_editingRole || m_animation->isStarted(widget)) { + Q_EMIT widget->roleEditingCanceled(index, role, QVariant()); return; } m_editingRole = true; + m_controller->selectionManager()->setCurrentItem(index); widget->setEditedRole(role); connect(widget, &KItemListWidget::roleEditingCanceled, this, &KItemListView::slotRoleEditingCanceled); @@@ -843,16 -822,6 +843,16 @@@ void KItemListView::paint(QPainter *pai } } +void KItemListView::setStatusBarOffset(int offset) +{ + if (m_statusBarOffset != offset) { + m_statusBarOffset = offset; + if (m_layouter) { + m_layouter->setStatusBarOffset(offset); + } + } +} + QVariant KItemListView::itemChange(GraphicsItemChange change, const QVariant &value) { if (change == QGraphicsItem::ItemSceneHasChanged && scene()) { @@@ -1403,20 -1372,9 +1403,20 @@@ void KItemListView::slotItemsMoved(cons const int firstVisibleMovedIndex = qMax(firstVisibleIndex(), itemRange.index); const int lastVisibleMovedIndex = qMin(lastVisibleIndex(), itemRange.index + itemRange.count - 1); + /// Represents an item that was moved while being edited. + struct MovedEditedItem { + int movedToIndex; + QByteArray editedRole; + }; + std::optional movedEditedItem; for (int index = firstVisibleMovedIndex; index <= lastVisibleMovedIndex; ++index) { KItemListWidget *widget = m_visibleItems.value(index); if (widget) { + if (m_editingRole && !widget->editedRole().isEmpty()) { + movedEditedItem = {movedToIndexes[index - itemRange.index], widget->editedRole()}; + disconnectRoleEditingSignals(index); + m_editingRole = false; + } updateWidgetProperties(widget, index); initializeItemListWidget(widget); } @@@ -1424,10 -1382,6 +1424,10 @@@ doLayout(NoAnimation); updateSiblingsInformation(); + + if (movedEditedItem) { + editRole(movedEditedItem->movedToIndex, movedEditedItem->editedRole); + } } void KItemListView::slotItemsChanged(const KItemRangeList &itemRanges, const QSet &roles) @@@ -1461,7 -1415,13 +1461,7 @@@ updateVisibleGroupHeaders(); doLayout(NoAnimation); } - - QAccessibleTableModelChangeEvent ev(this, QAccessibleTableModelChangeEvent::DataChanged); - ev.setFirstRow(itemRange.index); - ev.setLastRow(itemRange.index + itemRange.count); - QAccessible::updateAccessibility(&ev); } - doLayout(NoAnimation); } @@@ -1521,8 -1481,30 +1521,28 @@@ void KItemListView::slotSortRoleChanged } } + void KItemListView::slotGroupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous) + { + Q_UNUSED(current) + Q_UNUSED(previous) + if (m_grouped) { + updateVisibleGroupHeaders(); + doLayout(NoAnimation); + } + } + + void KItemListView::slotGroupRoleChanged(const QByteArray ¤t, const QByteArray &previous) + { + Q_UNUSED(current) + Q_UNUSED(previous) + if (m_grouped) { + updateVisibleGroupHeaders(); + doLayout(NoAnimation); + } + } + void KItemListView::slotCurrentChanged(int current, int previous) { - Q_UNUSED(previous) - // In SingleSelection mode (e.g., in the Places Panel), the current item is // always the selected item. It is not necessary to highlight the current item then. if (m_controller->selectionBehavior() != KItemListController::SingleSelection) { @@@ -1536,39 -1518,23 +1556,39 @@@ currentWidget->setCurrent(true); } } - - QAccessibleEvent ev(this, QAccessible::Focus); - ev.setChild(current); - QAccessible::updateAccessibility(&ev); +#ifndef QT_NO_ACCESSIBILITY + if (current != previous && QAccessible::isActive()) { + static_cast(QAccessible::queryAccessibleInterface(this))->announceCurrentItem(); + } +#endif } void KItemListView::slotSelectionChanged(const KItemSet ¤t, const KItemSet &previous) { - Q_UNUSED(previous) - QHashIterator it(m_visibleItems); while (it.hasNext()) { it.next(); const int index = it.key(); KItemListWidget *widget = it.value(); - widget->setSelected(current.contains(index)); + const bool isSelected(current.contains(index)); + widget->setSelected(isSelected); + +#ifndef QT_NO_ACCESSIBILITY + if (!QAccessible::isActive()) { + continue; + } + // Let the screen reader announce "selected" or "not selected" for the active item. + const bool wasSelected(previous.contains(index)); + if (isSelected != wasSelected) { + QAccessibleEvent accessibleSelectionChangedEvent(this, QAccessible::SelectionAdd); + accessibleSelectionChangedEvent.setChild(index); + QAccessible::updateAccessibility(&accessibleSelectionChangedEvent); + } } +#else + } + Q_UNUSED(previous) +#endif } void KItemListView::slotAnimationFinished(QGraphicsWidget *widget, KItemListViewAnimation::AnimationType type) @@@ -1621,10 -1587,10 +1641,10 @@@ void KItemListView::slotRubberBandActiv curve.addCubicBezierSegment(QPointF(0.4, 0.0), QPointF(1.0, 1.0), QPointF(1.0, 1.0)); animation->setEasingCurve(curve); - connect(animation, &QVariantAnimation::valueChanged, this, [=](const QVariant &) { + connect(animation, &QVariantAnimation::valueChanged, this, [=, this](const QVariant &) { update(); }); - connect(animation, &QVariantAnimation::finished, this, [=]() { + connect(animation, &QVariantAnimation::finished, this, [=, this]() { m_rubberBandAnimations.removeAll(animation); delete animation; }); @@@ -1797,6 -1763,8 +1817,8 @@@ void KItemListView::setModel(KItemModel disconnect(m_model, &KItemModelBase::groupedSortingChanged, this, &KItemListView::slotGroupedSortingChanged); disconnect(m_model, &KItemModelBase::sortOrderChanged, this, &KItemListView::slotSortOrderChanged); disconnect(m_model, &KItemModelBase::sortRoleChanged, this, &KItemListView::slotSortRoleChanged); + disconnect(m_model, &KItemModelBase::groupOrderChanged, this, &KItemListView::slotGroupOrderChanged); + disconnect(m_model, &KItemModelBase::groupRoleChanged, this, &KItemListView::slotGroupRoleChanged); m_sizeHintResolver->itemsRemoved(KItemRangeList() << KItemRange(0, m_model->count())); } @@@ -1920,9 -1888,6 +1942,9 @@@ void KItemListView::doLayout(LayoutAnim if (animate) { if (m_animation->isStarted(widget, KItemListViewAnimation::MovingAnimation)) { + if (m_editingRole) { + Q_EMIT widget->roleEditingCanceled(widget->index(), QByteArray(), QVariant()); + } m_animation->start(widget, KItemListViewAnimation::MovingAnimation, newPos); applyNewPos = false; } @@@ -2242,7 -2207,7 +2264,7 @@@ void KItemListView::updateGroupHeaderFo const int groupIndex = groupIndexForItem(index); Q_ASSERT(groupIndex >= 0); groupHeader->setData(groups.at(groupIndex).second); - groupHeader->setRole(model()->sortRole()); + groupHeader->setRole(model()->groupRole()); groupHeader->setStyleOption(m_styleOption); groupHeader->setScrollOrientation(scrollOrientation()); groupHeader->setItemIndex(index); @@@ -2416,7 -2381,7 +2438,7 @@@ QHash KItemListView: void KItemListView::applyColumnWidthsFromHeader() { // Apply the new size to the layouter - const qreal requiredWidth = columnWidthsSum() + 2 * m_headerWidget->sidePadding(); + const qreal requiredWidth = m_headerWidget->leftPadding() + columnWidthsSum() + m_headerWidget->rightPadding(); const QSizeF dynamicItemSize(qMax(size().width(), requiredWidth), m_itemSize.height()); m_layouter->setItemSize(dynamicItemSize); @@@ -2433,7 -2398,7 +2455,7 @@@ void KItemListView::updateWidgetColumnW for (const QByteArray &role : std::as_const(m_visibleRoles)) { widget->setColumnWidth(role, m_headerWidget->columnWidth(role)); } - widget->setSidePadding(m_headerWidget->sidePadding()); + widget->setSidePadding(m_headerWidget->leftPadding(), m_headerWidget->rightPadding()); } void KItemListView::updatePreferredColumnWidths(const KItemRangeList &itemRanges) @@@ -2511,9 -2476,9 +2533,9 @@@ void KItemListView::applyAutomaticColum qreal firstColumnWidth = m_headerWidget->columnWidth(firstRole); QSizeF dynamicItemSize = m_itemSize; - qreal requiredWidth = columnWidthsSum() + 2 * m_headerWidget->sidePadding(); // Adding the padding a second time so we have the same padding - // symmetrically on both sides of the view. This improves UX, looks better and increases the chances of users figuring out that the padding - // area can be used for deselecting and dropping files. + qreal requiredWidth = m_headerWidget->leftPadding() + columnWidthsSum() + m_headerWidget->rightPadding(); + // By default we want the same padding symmetrically on both sides of the view. This improves UX, looks better and increases the chances of users figuring + // out that the padding area can be used for deselecting and dropping files. const qreal availableWidth = size().width(); if (requiredWidth < availableWidth) { // Stretch the first column to use the whole remaining width diff --combined src/kitemviews/kitemlistview.h index 1a4ff0df1,04d48bd47..1382405d0 --- a/src/kitemviews/kitemlistview.h +++ b/src/kitemviews/kitemlistview.h @@@ -175,11 -175,6 +175,11 @@@ public */ qreal verticalPageStep() const; + /** + * @return The line step which should be used for the scroll by mouse wheel. + */ + virtual qreal scrollSingleStep() const; + /** * @return Index of the item that is below the point \a pos. * The position is relative to the upper right of @@@ -307,12 -302,6 +307,12 @@@ void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; + /** + * Set the bottom offset for moving the view so that the small overlayed statusbar + * won't cover any items by accident. + */ + void setStatusBarOffset(int offset); + Q_SIGNALS: void scrollOrientationChanged(Qt::Orientation current, Qt::Orientation previous); void scrollOffsetChanged(qreal current, qreal previous); @@@ -441,6 -430,8 +441,8 @@@ protected Q_SLOTS virtual void slotGroupedSortingChanged(bool current); virtual void slotSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous); virtual void slotSortRoleChanged(const QByteArray ¤t, const QByteArray &previous); + virtual void slotGroupOrderChanged(Qt::SortOrder current, Qt::SortOrder previous); + virtual void slotGroupRoleChanged(const QByteArray ¤t, const QByteArray &previous); virtual void slotCurrentChanged(int current, int previous); virtual void slotSelectionChanged(const KItemSet ¤t, const KItemSet &previous); @@@ -563,8 -554,9 +565,9 @@@ private void recycleGroupHeaderForWidget(KItemListWidget *widget); /** - * Helper method for slotGroupedSortingChanged(), slotSortOrderChanged() - * and slotSortRoleChanged(): Iterates through all visible items and updates + * Helper method for slotGroupedSortingChanged(), slotSortOrderChanged(), + * slotSortRoleChanged(), slotGroupOrderChanged() and slotGroupRoleChanged(): + * Iterates through all visible items and updates * the group-header widgets. */ void updateVisibleGroupHeaders(); @@@ -787,8 -779,6 +790,8 @@@ private QPropertyAnimation *m_indicatorAnimation; + int m_statusBarOffset; + // When dragging items into the view where the sort-role of the model // is empty, a visual indicator should be shown during dragging where // the dropping will happen. This indicator is specified by an index @@@ -806,9 -796,7 +809,9 @@@ friend class KItemListController; friend class KItemListControllerTest; friend class KItemListViewAccessible; - friend class KItemListAccessibleCell; + friend class KItemListDelegateAccessible; + + friend class DolphinMainWindowTest; }; /** diff --combined src/settings/dolphin_directoryviewpropertysettings.kcfg index bae1f409f,7f34f7093..0044170a7 --- a/src/settings/dolphin_directoryviewpropertysettings.kcfg +++ b/src/settings/dolphin_directoryviewpropertysettings.kcfg @@@ -24,7 -24,7 +24,7 @@@ This option controls the style of the view. Currently supported values include icons (0), details (1) and column (2) views. - DolphinView::IconsView + DolphinView::IconsView @@@ -35,8 -35,8 +35,8 @@@ - When this option is enabled, the sorted items are categorized into groups. - false + When this option is enabled, the items are categorized into groups. + true @@@ -52,6 -52,19 +52,19 @@@ Qt::DescendingOrder + + + This option defines which attribute (text, size, date, etc.) grouping is performed on. + none + + + + + Qt::AscendingOrder + Qt::AscendingOrder + Qt::DescendingOrder + + true @@@ -77,11 -90,6 +90,11 @@@ The last time these properties were changed by the user. + + + false + + @@@ -89,5 -97,3 +102,3 @@@ - - diff --combined src/settings/dolphinsettingsdialog.cpp index 6410d9655,577dd23e2..1c8178651 --- a/src/settings/dolphinsettingsdialog.cpp +++ b/src/settings/dolphinsettingsdialog.cpp @@@ -21,7 -21,6 +21,7 @@@ #include #include #include +#include #include #include @@@ -38,13 -37,13 +38,13 @@@ DolphinSettingsDialog::DolphinSettingsD const QSize minSize = minimumSize(); setMinimumSize(QSize(540, minSize.height())); - setFaceType(List); + setFaceType(KRuntimePlatform::runtimePlatform().contains(QLatin1String("phone")) ? Tabbed : List); setWindowTitle(i18nc("@title:window", "Configure")); // Interface InterfaceSettingsPage *interfaceSettingsPage = new InterfaceSettingsPage(this); KPageWidgetItem *interfaceSettingsFrame = addPage(interfaceSettingsPage, i18nc("@title:group Interface settings", "Interface")); - interfaceSettingsFrame->setIcon(QIcon::fromTheme(QStringLiteral("system-file-manager"))); + interfaceSettingsFrame->setIcon(QIcon::fromTheme(QStringLiteral("org.kde.dolphin"))); connect(interfaceSettingsPage, &InterfaceSettingsPage::changed, this, &DolphinSettingsDialog::enableApply); // View @@@ -58,6 -57,7 +58,7 @@@ actions, {QStringLiteral("add_to_places"), QStringLiteral("sort"), + QStringLiteral("group"), QStringLiteral("view_mode"), QStringLiteral("open_in_new_tab"), QStringLiteral("open_in_new_window"), diff --combined src/tests/kfileitemmodeltest.cpp index 4c6db590d,ea05f840b..34ae2ba87 --- a/src/tests/kfileitemmodeltest.cpp +++ b/src/tests/kfileitemmodeltest.cpp @@@ -69,7 -69,6 +69,7 @@@ private Q_SLOTS void testMakeExpandedItemHidden(); void testRemoveFilteredExpandedItems(); void testSorting(); + void testNaturalSorting(); void testIndexForKeyboardSearch(); void testNameFilter(); void testEmptyPath(); @@@ -158,14 -157,14 +158,14 @@@ void KFileItemModelTest::testDefaultSor QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(m_model->count(), 3); - QCOMPARE(m_model->data(0).value("text").toString(), QString("a.txt")); - QCOMPARE(m_model->data(1).value("text").toString(), QString("b.txt")); - QCOMPARE(m_model->data(2).value("text").toString(), QString("c.txt")); + QCOMPARE(m_model->data(0).value("text").toString(), QStringLiteral("a.txt")); + QCOMPARE(m_model->data(1).value("text").toString(), QStringLiteral("b.txt")); + QCOMPARE(m_model->data(2).value("text").toString(), QStringLiteral("c.txt")); } void KFileItemModelTest::testDefaultGroupedSorting() { - QCOMPARE(m_model->groupedSorting(), false); + QCOMPARE(m_model->groupedSorting(), true); } void KFileItemModelTest::testNewItems() @@@ -263,8 -262,8 +263,8 @@@ void KFileItemModelTest::testSetData( QCOMPARE(itemsChangedSpy.count(), 1); values = m_model->data(0); - QCOMPARE(values.value("customRole1").toString(), QString("Test1")); - QCOMPARE(values.value("customRole2").toString(), QString("Test2")); + QCOMPARE(values.value("customRole1").toString(), QStringLiteral("Test1")); + QCOMPARE(values.value("customRole2").toString(), QStringLiteral("Test2")); QVERIFY(m_model->isConsistent()); } @@@ -1276,88 -1275,11 +1276,88 @@@ void KFileItemModelTest::testSorting( QCOMPARE(itemsMovedSpy.takeFirst().at(1).value>(), QList() << 2 << 4 << 5 << 3 << 0 << 1 << 6 << 7 << 9 << 8); } +void KFileItemModelTest::testNaturalSorting() +{ + QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); + QSignalSpy itemsMovedSpy(m_model, &KFileItemModel::itemsMoved); + QVERIFY(itemsMovedSpy.isValid()); + + m_model->setSortRole("text"); + m_model->setShowHiddenFiles(true); + + m_testDir->createFiles({".a", "a.txt", "b.txt", "a 1.txt", "a 2.txt", "a 10.txt", "a.tar", "b.tar"}); + + m_model->loadDirectory(m_testDir->url()); + QVERIFY(itemsInsertedSpy.wait()); + + // Sort by Name, ascending, natural sorting enabled + QCOMPARE(m_model->sortRole(), QByteArray("text")); + QCOMPARE(m_model->sortOrder(), Qt::AscendingOrder); + QCOMPARE(itemsInModel(), + QStringList() << ".a" + << "a.tar" + << "a.txt" + << "a 1.txt" + << "a 2.txt" + << "a 10.txt" + << "b.tar" + << "b.txt"); + + // Sort by Extension + m_model->setSortRole("extension"); + QCOMPARE(m_model->sortRole(), QByteArray("extension")); + QCOMPARE(itemsInModel(), + QStringList() << ".a" + << "a.tar" + << "b.tar" + << "a.txt" + << "a 1.txt" + << "a 2.txt" + << "a 10.txt" + << "b.txt"); + QCOMPARE(itemsMovedSpy.count(), 1); + QCOMPARE(itemsMovedSpy.first().at(0).value(), KItemRange(2, 5)); + QCOMPARE(itemsMovedSpy.takeFirst().at(1).value>(), QList() << 3 << 4 << 5 << 6 << 2); + + // Disable natural sorting, refresh directory for the change to take effect + m_model->m_naturalSorting = false; + m_model->refreshDirectory(m_model->directory()); + QVERIFY(itemsInsertedSpy.wait()); + + // Sort by Extension, ascending, natural sorting disabled + QCOMPARE(m_model->sortRole(), QByteArray("extension")); + QCOMPARE(itemsInModel(), + QStringList() << ".a" + << "a.tar" + << "b.tar" + << "a 1.txt" + << "a 10.txt" + << "a 2.txt" + << "a.txt" + << "b.txt"); + + // Sort by Name + m_model->setSortRole("text"); + QCOMPARE(m_model->sortRole(), QByteArray("text")); + QCOMPARE(itemsInModel(), + QStringList() << ".a" + << "a 1.txt" + << "a 10.txt" + << "a 2.txt" + << "a.tar" + << "a.txt" + << "b.tar" + << "b.txt"); + QCOMPARE(itemsMovedSpy.count(), 1); + QCOMPARE(itemsMovedSpy.first().at(0).value(), KItemRange(1, 6)); + QCOMPARE(itemsMovedSpy.takeFirst().at(1).value>(), QList() << 4 << 6 << 1 << 2 << 3 << 5); +} + void KFileItemModelTest::testIndexForKeyboardSearch() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); - m_testDir->createFiles({"a", "aa", "Image.jpg", "Image.png", "Text", "Text1", "Text2", "Text11"}); + m_testDir->createFiles({"a", "aa", "Image.jpg", "Image.png", "Text", "Text1", "Text2", "Text11", "U", "Ü", "Üu", "Ž"}); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); @@@ -1374,9 -1296,6 +1374,9 @@@ QCOMPARE(m_model->indexForKeyboardSearch("text1", 0), 5); QCOMPARE(m_model->indexForKeyboardSearch("text2", 0), 6); QCOMPARE(m_model->indexForKeyboardSearch("text11", 0), 7); + QCOMPARE(m_model->indexForKeyboardSearch("u", 0), 8); + QCOMPARE(m_model->indexForKeyboardSearch("üu", 0), 10); + QCOMPARE(m_model->indexForKeyboardSearch("ž", 0), 11); // Start a search somewhere in the middle QCOMPARE(m_model->indexForKeyboardSearch("a", 1), 1); @@@ -1403,12 -1322,6 +1403,12 @@@ QCOMPARE(m_model->indexForKeyboardSearch("TexT", 5), 5); QCOMPARE(m_model->indexForKeyboardSearch("IMAGE", 4), 2); + // Test searches that match items with marks + QCOMPARE(m_model->indexForKeyboardSearch("u", 9), 9); + QCOMPARE(m_model->indexForKeyboardSearch("u", 10), 10); + QCOMPARE(m_model->indexForKeyboardSearch("uu", 0), 10); + QCOMPARE(m_model->indexForKeyboardSearch("z", 0), 11); + // TODO: Maybe we should also test keyboard searches in directories which are not sorted by Name? } @@@ -2094,6 -2007,7 +2094,7 @@@ void KFileItemModelTest::testNameRoleGr m_testDir->createFiles({"b.txt", "c.txt", "d.txt", "e.txt"}); m_model->setGroupedSorting(true); + m_model->setGroupRole("text"); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInModel(), @@@ -2180,6 -2094,8 +2181,8 @@@ void KFileItemModelTest::testNameRoleGr m_testDir->createFiles({"a/b.txt", "a/c.txt", "d/e.txt", "d/f.txt"}); m_model->setGroupedSorting(true); + m_model->setGroupRole("text"); + m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInModel(), @@@ -2189,6 -2105,7 +2192,7 @@@ QList> expectedGroups; expectedGroups << QPair(0, QLatin1String("A")); expectedGroups << QPair(1, QLatin1String("D")); + QCOMPARE(m_model->groups(), expectedGroups); // Verify that expanding "a" and "d" will not change the groups (except for the index of "D"). diff --combined src/views/dolphinview.cpp index b702f5b13,abaed9c7c..5c961b47b --- a/src/views/dolphinview.cpp +++ b/src/views/dolphinview.cpp @@@ -12,9 -12,6 +12,9 @@@ #include "dolphinitemlistview.h" #include "dolphinnewfilemenuobserver.h" #include "draganddrophelper.h" +#ifndef QT_NO_ACCESSIBILITY +#include "kitemviews/accessibility/kitemlistviewaccessible.h" +#endif #include "kitemviews/kfileitemlistview.h" #include "kitemviews/kfileitemmodel.h" #include "kitemviews/kitemlistcontainer.h" @@@ -44,7 -41,6 +44,7 @@@ #include #include #include +#include #include #include #include @@@ -54,9 -50,6 +54,9 @@@ #include #include +#ifndef QT_NO_ACCESSIBILITY +#include +#endif #include #include #include @@@ -124,7 -117,8 +124,7 @@@ DolphinView::DolphinView(const QUrl &ur applyModeToView(); KItemListController *controller = new KItemListController(m_model, m_view, this); - const int delay = GeneralSettings::autoExpandFolders() ? 750 : -1; - controller->setAutoActivationDelay(delay); + controller->setAutoActivationEnabled(GeneralSettings::autoExpandFolders()); connect(controller, &KItemListController::doubleClickViewBackground, this, &DolphinView::doubleClickViewBackground); // The EnlargeSmallPreviews setting can only be changed after the model @@@ -137,10 -131,10 +137,10 @@@ m_view->setAccessibleParentsObject(m_container); #endif setFocusProxy(m_container); - connect(m_container->horizontalScrollBar(), &QScrollBar::valueChanged, this, [=] { + connect(m_container->horizontalScrollBar(), &QScrollBar::valueChanged, this, [this] { hideToolTip(); }); - connect(m_container->verticalScrollBar(), &QScrollBar::valueChanged, this, [=] { + connect(m_container->verticalScrollBar(), &QScrollBar::valueChanged, this, [this] { hideToolTip(); }); @@@ -205,7 -199,8 +205,7 @@@ connect(m_model, &KFileItemModel::directoryRedirection, this, &DolphinView::slotDirectoryRedirection); connect(m_model, &KFileItemModel::urlIsFileError, this, &DolphinView::urlIsFileError); connect(m_model, &KFileItemModel::fileItemsChanged, this, &DolphinView::fileItemsChanged); - // #473377: Use a QueuedConnection to avoid modifying KCoreDirLister before KCoreDirListerCache::deleteDir() returns. - connect(m_model, &KFileItemModel::currentDirectoryRemoved, this, &DolphinView::currentDirectoryRemoved, Qt::QueuedConnection); + connect(m_model, &KFileItemModel::currentDirectoryRemoved, this, &DolphinView::currentDirectoryRemoved); connect(this, &DolphinView::itemCountChanged, this, &DolphinView::updatePlaceholderLabel); @@@ -320,12 -315,6 +320,12 @@@ void DolphinView::setSelectionModeEnabl m_view->setEnabledSelectionToggles(DolphinItemListView::SelectionTogglesEnabled::FollowSetting); } m_container->controller()->setSelectionModeEnabled(enabled); +#ifndef QT_NO_ACCESSIBILITY + if (QAccessible::isActive()) { + auto accessibleViewInterface = static_cast(QAccessible::queryAccessibleInterface(m_view)); + accessibleViewInterface->announceSelectionModeEnabled(enabled); + } +#endif } bool DolphinView::selectionMode() const @@@ -387,8 -376,9 +387,8 @@@ void DolphinView::setGroupedSorting(boo ViewProperties props(viewPropertiesUrl()); props.setGroupedSorting(grouped); - props.save(); - m_container->controller()->model()->setGroupedSorting(grouped); + m_model->setGroupedSorting(grouped); Q_EMIT groupedSortingChanged(grouped); } @@@ -515,6 -505,42 +515,42 @@@ Qt::SortOrder DolphinView::sortOrder() return m_model->sortOrder(); } + void DolphinView::setGroupRole(const QByteArray &role) + { + if (role != groupRole()) { + ViewProperties props(viewPropertiesUrl()); + props.setGroupRole(role); + + KItemModelBase *model = m_container->controller()->model(); + model->setGroupRole(role); + + Q_EMIT groupRoleChanged(role); + } + } + + QByteArray DolphinView::groupRole() const + { + const KItemModelBase *model = m_container->controller()->model(); + return model->groupRole(); + } + + void DolphinView::setGroupOrder(Qt::SortOrder order) + { + if (groupOrder() != order) { + ViewProperties props(viewPropertiesUrl()); + props.setGroupOrder(order); + + m_model->setGroupOrder(order); + + Q_EMIT groupOrderChanged(order); + } + } + + Qt::SortOrder DolphinView::groupOrder() const + { + return m_model->groupOrder(); + } + void DolphinView::setSortFoldersFirst(bool foldersFirst) { if (sortFoldersFirst() != foldersFirst) { @@@ -578,7 -604,8 +614,7 @@@ void DolphinView::readSettings( m_view->readSettings(); applyViewProperties(); - const int delay = GeneralSettings::autoExpandFolders() ? 750 : -1; - m_container->controller()->setAutoActivationDelay(delay); + m_container->controller()->setAutoActivationEnabled(GeneralSettings::autoExpandFolders()); const int newZoomLevel = m_view->zoomLevel(); if (newZoomLevel != oldZoomLevel) { @@@ -767,23 -794,7 +803,23 @@@ void DolphinView::renameSelectedItems( } else { KIO::RenameFileDialog *dialog = new KIO::RenameFileDialog(items, this); - connect(dialog, &KIO::RenameFileDialog::renamingFinished, this, &DolphinView::slotRenameDialogRenamingFinished); + connect(dialog, &KIO::RenameFileDialog::renamingFinished, this, [this, items](const QList &urls) { + // The model may have already been updated, so it's possible that we don't find the old items. + for (int i = 0; i < items.count(); ++i) { + const int index = m_model->index(items[i]); + if (index >= 0) { + QHash data; + data.insert("text", urls[i].fileName()); + m_model->setData(index, data); + } + } + + forceUrlsSelection(urls.first(), urls); + updateSelectionState(); + }); + connect(dialog, &KIO::RenameFileDialog::error, this, [this](KJob *job) { + KMessageBox::error(this, job->errorString()); + }); dialog->open(); } @@@ -811,7 -822,7 +847,7 @@@ void DolphinView::deleteSelectedItems( using Iface = KIO::AskUserActionInterface; auto *trashJob = new KIO::DeleteOrTrashJob(list, Iface::Delete, Iface::DefaultConfirmation, this); - connect(trashJob, &KJob::result, this, &DolphinView::slotTrashFileFinished); + connect(trashJob, &KJob::result, this, &DolphinView::slotDeleteFileFinished); m_selectNextItem = true; trashJob->start(); } @@@ -844,12 -855,9 +880,12 @@@ void DolphinView::copySelectedItems(con KIO::CopyJob *job = KIO::copy(selection.urlList(), destinationUrl, KIO::DefaultFlags); KJobWidgets::setWindow(job, this); - connect(job, &KIO::DropJob::result, this, &DolphinView::slotJobResult); + connect(job, &KIO::CopyJob::result, this, &DolphinView::slotJobResult); connect(job, &KIO::CopyJob::copying, this, &DolphinView::slotItemCreatedFromJob); connect(job, &KIO::CopyJob::copyingDone, this, &DolphinView::slotItemCreatedFromJob); + connect(job, &KIO::CopyJob::warning, this, [this](KJob *job, const QString & /* warning */) { + Q_EMIT errorMessage(job->errorString(), job->error()); + }); KIO::FileUndoManager::self()->recordCopyJob(job); } @@@ -866,12 -874,9 +902,12 @@@ void DolphinView::moveSelectedItems(con KIO::CopyJob *job = KIO::move(selection.urlList(), destinationUrl, KIO::DefaultFlags); KJobWidgets::setWindow(job, this); - connect(job, &KIO::DropJob::result, this, &DolphinView::slotJobResult); + connect(job, &KIO::CopyJob::result, this, &DolphinView::slotJobResult); connect(job, &KIO::CopyJob::moving, this, &DolphinView::slotItemCreatedFromJob); connect(job, &KIO::CopyJob::copyingDone, this, &DolphinView::slotItemCreatedFromJob); + connect(job, &KIO::CopyJob::warning, this, [this](KJob *job, const QString & /*warning */) { + Q_EMIT errorMessage(job->errorString(), job->error()); + }); KIO::FileUndoManager::self()->recordCopyJob(job); } @@@ -933,9 -938,6 +969,9 @@@ void DolphinView::duplicateSelectedItem connect(job, &KIO::CopyJob::result, this, &DolphinView::slotJobResult); connect(job, &KIO::CopyJob::copyingDone, this, &DolphinView::slotItemCreatedFromJob); connect(job, &KIO::CopyJob::copyingLinkDone, this, &DolphinView::slotItemLinkCreatedFromJob); + connect(job, &KIO::CopyJob::warning, this, [this](KJob *job, const QString & /*warning*/) { + Q_EMIT errorMessage(job->errorString(), job->error()); + }); KIO::FileUndoManager::self()->recordCopyJob(job); } } @@@ -1182,9 -1184,6 +1218,9 @@@ void DolphinView::slotItemContextMenuRe if (m_selectionChangedTimer->isActive()) { emitSelectionChangedSignal(); } + if (m_twoClicksRenamingTimer->isActive()) { + abortTwoClicksRenaming(); + } const KFileItem item = m_model->fileItem(index); Q_EMIT requestContextMenu(pos.toPoint(), item, selectedItems(), url()); @@@ -1250,7 -1249,7 +1286,7 @@@ void DolphinView::slotHeaderContextMenu QAction *toggleSidePaddingAction = menu->addAction(i18nc("@action:inmenu", "Side Padding")); toggleSidePaddingAction->setCheckable(true); - toggleSidePaddingAction->setChecked(view->header()->sidePadding() > 0); + toggleSidePaddingAction->setChecked(layoutDirection() == Qt::LeftToRight ? view->header()->leftPadding() > 0 : view->header()->rightPadding() > 0); QAction *autoAdjustWidthsAction = menu->addAction(i18nc("@action:inmenu", "Automatic Column Widths")); autoAdjustWidthsAction->setCheckable(true); @@@ -1283,11 -1282,7 +1319,11 @@@ props.setHeaderColumnWidths(columnWidths); header->setAutomaticColumnResizing(false); } else if (action == toggleSidePaddingAction) { - header->setSidePadding(toggleSidePaddingAction->isChecked() ? 20 : 0); + if (toggleSidePaddingAction->isChecked()) { + header->setSidePadding(20, 20); + } else { + header->setSidePadding(0, 0); + } } else { // Show or hide the selected role const QByteArray selectedRole = action->data().toByteArray(); @@@ -1340,11 -1335,10 +1376,11 @@@ void DolphinView::slotHeaderColumnWidth props.setHeaderColumnWidths(columnWidths); } -void DolphinView::slotSidePaddingWidthChanged(qreal width) +void DolphinView::slotSidePaddingWidthChanged(qreal leftPaddingWidth, qreal rightPaddingWidth) { ViewProperties props(viewPropertiesUrl()); - DetailsModeSettings::setSidePadding(int(width)); + DetailsModeSettings::setLeftPadding(int(leftPaddingWidth)); + DetailsModeSettings::setRightPadding(int(rightPaddingWidth)); m_view->writeSettings(); } @@@ -1411,9 -1405,6 +1447,9 @@@ void DolphinView::dropUrls(const QUrl & connect(job, &KIO::DropJob::copyJobStarted, this, [this](const KIO::CopyJob *copyJob) { connect(copyJob, &KIO::CopyJob::copying, this, &DolphinView::slotItemCreatedFromJob); connect(copyJob, &KIO::CopyJob::moving, this, &DolphinView::slotItemCreatedFromJob); + connect(copyJob, &KIO::CopyJob::warning, this, [this](KJob *job, const QString & /*warning*/) { + Q_EMIT errorMessage(job->errorString(), job->error()); + }); connect(copyJob, &KIO::CopyJob::linking, this, [this](KIO::Job *job, const QString &src, const QUrl &dest) { Q_UNUSED(job) Q_UNUSED(src) @@@ -1787,6 -1778,7 +1823,6 @@@ void DolphinView::updateSelectionState( if (!selectedItems.isEmpty()) { selectionManager->beginAnchoredSelection(selectionManager->currentItem()); selectionManager->setSelectedItems(selectedItems); - selectionManager->endAnchoredSelection(); if (shouldScrollToCurrentItem) { m_view->scrollToItem(selectedItems.first()); } @@@ -1894,7 -1886,7 +1930,7 @@@ void DolphinView::selectNextItem( Q_ASSERT_X(false, "DolphinView", "Selecting the next item failed."); return; } - const auto lastSelectedIndex = m_model->index(selectedItems().last()); + const auto lastSelectedIndex = m_model->index(selectedItems().constLast()); if (lastSelectedIndex < 0) { Q_ASSERT_X(false, "DolphinView", "Selecting the next item failed."); return; @@@ -1905,7 -1897,6 +1941,7 @@@ } if (nextItem >= 0) { selectionManager->setSelected(nextItem, 1); + selectionManager->beginAnchoredSelection(nextItem); } m_selectNextItem = false; } @@@ -1913,18 -1904,15 +1949,18 @@@ void DolphinView::slotRenamingResult(KJob *job) { - if (job->error()) { + // Change model data after renaming has succeeded. On failure we do nothing. + // If there is already an item with the newUrl, the copyjob will open a dialog for it, and + // KFileItemModel will update the data when the dir lister signals that the file name has changed. + if (!job->error()) { KIO::CopyJob *copyJob = qobject_cast(job); Q_ASSERT(copyJob); const QUrl newUrl = copyJob->destUrl(); + const QUrl oldUrl = copyJob->srcUrls().at(0); const int index = m_model->index(newUrl); - if (index >= 0) { + if (m_model->index(oldUrl) == index) { QHash data; - const QUrl oldUrl = copyJob->srcUrls().at(0); - data.insert("text", oldUrl.fileName()); + data.insert("text", newUrl.fileName()); m_model->setData(index, data); } } @@@ -1958,7 -1946,6 +1994,7 @@@ void DolphinView::slotDirectoryLoadingC Q_EMIT directoryLoadingCompleted(); + applyDynamicView(); updatePlaceholderLabel(); updateWritableState(); } @@@ -2060,14 -2047,25 +2096,14 @@@ void DolphinView::slotRoleEditingFinish } #endif - const bool newNameExistsAlready = (m_model->index(newUrl) >= 0); - if (!newNameExistsAlready && m_model->index(oldUrl) == index) { - // Only change the data in the model if no item with the new name - // is in the model yet. If there is an item with the new name - // already, calling KIO::CopyJob will open a dialog - // asking for a new name, and KFileItemModel will update the - // data when the dir lister signals that the file name has changed. - QHash data; - data.insert(role, retVal.newName); - m_model->setData(index, data); - } - KIO::Job *job = KIO::moveAs(oldUrl, newUrl); KJobWidgets::setWindow(job, this); KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Rename, {oldUrl}, newUrl, job); job->uiDelegate()->setAutoErrorHandlingEnabled(true); - if (!newNameExistsAlready) { + if (m_model->index(newUrl) < 0) { forceUrlsSelection(newUrl, {newUrl}); + updateSelectionState(); // Only connect the result signal if there is no item with the new name // in the model yet, see bug 328262. @@@ -2154,6 -2152,18 +2190,18 @@@ void DolphinView::applyViewProperties(c Q_EMIT sortOrderChanged(sortOrder); } + const QByteArray groupRole = props.groupRole(); + if (groupRole != m_model->groupRole()) { + m_model->setGroupRole(groupRole); + Q_EMIT groupRoleChanged(groupRole); + } + + const Qt::SortOrder groupOrder = props.groupOrder(); + if (groupOrder != m_model->groupOrder()) { + m_model->setGroupOrder(groupOrder); + Q_EMIT groupOrderChanged(groupOrder); + } + const bool sortFoldersFirst = props.sortFoldersFirst(); if (sortFoldersFirst != m_model->sortDirectoriesFirst()) { m_model->setSortDirectoriesFirst(sortFoldersFirst); @@@ -2203,7 -2213,7 +2251,7 @@@ } else { header->setAutomaticColumnResizing(true); } - header->setSidePadding(DetailsModeSettings::sidePadding()); + header->setSidePadding(DetailsModeSettings::leftPadding(), DetailsModeSettings::rightPadding()); } m_view->endTransaction(); @@@ -2227,51 -2237,6 +2275,51 @@@ void DolphinView::applyModeToView( } } +void DolphinView::applyDynamicView() +{ + ViewProperties props(viewPropertiesUrl()); + /* return early if: + * - dynamic view is not enabled + * - the current view mode is already Icon View + * - dynamic view has previously changed the view mode + */ + if (!GeneralSettings::dynamicView() || m_mode == IconsView || props.dynamicViewPassed()) { + return; + } + + uint imageAndVideoCount = 0; + uint checkedItems = 0; + const uint totalItems = itemsCount(); + const KFileItemList itemList = items(); + bool applyDynamicView = false; + + for (const auto &file : itemList) { + ++checkedItems; + const QString type = file.mimetype().slice(0, 5); + + if (type == "image" || type == "video") { + ++imageAndVideoCount; + // if 2/3 or more of the items are images/videos, dynamic view should be applied + applyDynamicView = imageAndVideoCount >= (totalItems * 2 / 3); + if (applyDynamicView) { + break; + } + } else if (checkedItems - imageAndVideoCount > totalItems / 3) { + // if more than a third of the checked files are not media files, return + return; + } + } + + if (!applyDynamicView) { + return; + } + + props.setAutoSaveEnabled(!GeneralSettings::globalViewProps()); + props.setDynamicViewPassed(true); + props.setViewMode(IconsView); + applyViewProperties(props); +} + void DolphinView::pasteToUrl(const QUrl &url) { KIO::PasteJob *job = KIO::paste(QApplication::clipboard()->mimeData(), url); @@@ -2283,9 -2248,6 +2331,9 @@@ connect(job, &KIO::PasteJob::copyJobStarted, this, [this](const KIO::CopyJob *copyJob) { connect(copyJob, &KIO::CopyJob::copying, this, &DolphinView::slotItemCreatedFromJob); connect(copyJob, &KIO::CopyJob::moving, this, &DolphinView::slotItemCreatedFromJob); + connect(copyJob, &KIO::CopyJob::warning, this, [this](KJob *job, const QString & /*warning*/) { + Q_EMIT errorMessage(job->errorString(), job->error()); + }); connect(copyJob, &KIO::CopyJob::linking, this, [this](KIO::Job *job, const QString &src, const QUrl &dest) { Q_UNUSED(job) Q_UNUSED(src) @@@ -2347,22 -2309,6 +2395,22 @@@ bool DolphinView::isFolderWritable() co return m_isFolderWritable; } +int DolphinView::horizontalScrollBarHeight() const +{ + if (m_container && m_container->horizontalScrollBar() && m_container->horizontalScrollBar()->isVisible()) { + return m_container->horizontalScrollBar()->height(); + } + return 0; +} + +void DolphinView::setStatusBarOffset(int offset) +{ + KItemListView *view = m_container->controller()->view(); + if (view) { + view->setStatusBarOffset(offset); + } +} + QUrl DolphinView::viewPropertiesUrl() const { if (m_viewPropertiesContext.isEmpty()) { @@@ -2375,6 -2321,11 +2423,6 @@@ return url; } -void DolphinView::slotRenameDialogRenamingFinished(const QList &urls) -{ - forceUrlsSelection(urls.first(), urls); -} - void DolphinView::forceUrlsSelection(const QUrl ¤t, const QList &selected) { clearSelection(); @@@ -2420,22 -2371,12 +2468,22 @@@ void DolphinView::showLoadingPlaceholde { m_placeholderLabel->setText(i18n("Loading…")); m_placeholderLabel->setVisible(true); +#ifndef QT_NO_ACCESSIBILITY + if (QAccessible::isActive()) { + static_cast(QAccessible::queryAccessibleInterface(m_view))->announceNewlyLoadedLocation(m_placeholderLabel->text()); + } +#endif } void DolphinView::updatePlaceholderLabel() { m_showLoadingPlaceholderTimer->stop(); if (itemsCount() > 0) { +#ifndef QT_NO_ACCESSIBILITY + if (QAccessible::isActive()) { + static_cast(QAccessible::queryAccessibleInterface(m_view))->announceNewlyLoadedLocation(QString()); + } +#endif m_placeholderLabel->setVisible(false); return; } @@@ -2479,11 -2420,6 +2527,11 @@@ } m_placeholderLabel->setVisible(true); +#ifndef QT_NO_ACCESSIBILITY + if (QAccessible::isActive()) { + static_cast(QAccessible::queryAccessibleInterface(m_view))->announceNewlyLoadedLocation(m_placeholderLabel->text()); + } +#endif } bool DolphinView::tryShowNameToolTip(QHelpEvent *event) @@@ -2502,7 -2438,7 +2550,7 @@@ const KFileItem item = m_model->fileItem(index.value()); const QString text = item.text(); const QPoint pos = mapToGlobal(event->pos()); - QToolTip::showText(pos, text); + QToolTip::showText(pos, text, this); return true; } } diff --combined src/views/dolphinview.h index f491b6dd5,2c772ad90..d50c932df --- a/src/views/dolphinview.h +++ b/src/views/dolphinview.h @@@ -51,6 -51,8 +51,8 @@@ class QRegularExpression * - show hidden files * - show previews * - enable grouping + * - grouping order + * - grouping type */ class DOLPHIN_EXPORT DolphinView : public QWidget { @@@ -219,6 -221,20 +221,20 @@@ public void setSortOrder(Qt::SortOrder order); Qt::SortOrder sortOrder() const; + /** + * Updates the view properties of the current URL to the + * grouping given by \a role. + */ + void setGroupRole(const QByteArray &role); + QByteArray groupRole() const; + + /** + * Updates the view properties of the current URL to the + * sort order given by \a order. + */ + void setGroupOrder(Qt::SortOrder order); + Qt::SortOrder groupOrder() const; + /** Sets a separate sorting with folders first (true) or a mixed sorting of files and folders (false). */ void setSortFoldersFirst(bool foldersFirst); bool sortFoldersFirst() const; @@@ -357,18 -373,6 +373,18 @@@ */ bool isFolderWritable() const; + /** + * @returns the height of the scrollbar at the bottom of the view or zero if no such scroll bar is visible. + */ + int horizontalScrollBarHeight() const; + + /** + * Set the offset for any view items that small statusbar would otherwise + * cover. For example, in compact view this is used to make sure no + * item is covered by statusbar. + */ + void setStatusBarOffset(int offset); + public Q_SLOTS: void reload(); @@@ -534,6 -538,12 +550,12 @@@ Q_SIGNALS /** Is emitted if the sort order (ascending or descending) has been changed. */ void sortOrderChanged(Qt::SortOrder order); + /** Is emitted if the grouping by name, size or date has been changed. */ + void groupRoleChanged(const QByteArray &role); + + /** Is emitted if the group order (ascending or descending) has been changed. */ + void groupOrderChanged(Qt::SortOrder order); + /** * Is emitted if the sorting of files and folders (separate with folders * first or mixed) has been changed. @@@ -704,12 -714,13 +726,12 @@@ private Q_SLOTS void slotViewContextMenuRequested(const QPointF &pos); void slotHeaderContextMenuRequested(const QPointF &pos); void slotHeaderColumnWidthChangeFinished(const QByteArray &role, qreal current); - void slotSidePaddingWidthChanged(qreal width); + void slotSidePaddingWidthChanged(qreal leftPaddingWidth, qreal rightPaddingWidth); void slotItemHovered(int index); void slotItemUnhovered(int index); void slotItemDropEvent(int index, QGraphicsSceneDragDropEvent *event); void slotModelChanged(KItemModelBase *current, KItemModelBase *previous); void slotMouseButtonPressed(int itemIndex, Qt::MouseButtons buttons); - void slotRenameDialogRenamingFinished(const QList &urls); void slotSelectedItemTextPressed(int index); void slotItemCreatedFromJob(KIO::Job *, const QUrl &, const QUrl &to); void slotItemLinkCreatedFromJob(KIO::Job *, const QUrl &, const QString &, const QUrl &to); @@@ -867,11 -878,6 +889,11 @@@ private */ void applyModeToView(); + /** + * Changes the current view based on the content of the directory. + */ + void applyDynamicView(); + enum Selection { HasSelection, NoSelection }; /** * Helper method for DolphinView::requestStatusBarText(). diff --combined src/views/dolphinviewactionhandler.cpp index e504fd831,815b0f63e..14600c133 --- a/src/views/dolphinviewactionhandler.cpp +++ b/src/views/dolphinviewactionhandler.cpp @@@ -12,7 -12,6 +12,7 @@@ #include "selectionmode/actiontexthelper.h" #include "settings/viewpropertiesdialog.h" #include "views/zoomlevelinfo.h" +#include "views/zoomwidgetaction.h" #if HAVE_BALOO #include @@@ -34,6 -33,7 +34,7 @@@ DolphinViewActionHandler::DolphinViewAc , m_actionCollection(collection) , m_currentView(nullptr) , m_sortByActions() + , m_groupByActions() , m_visibleRoles() { Q_ASSERT(m_actionCollection); @@@ -59,6 -59,8 +60,8 @@@ void DolphinViewActionHandler::setCurre connect(view, &DolphinView::groupedSortingChanged, this, &DolphinViewActionHandler::slotGroupedSortingChanged); connect(view, &DolphinView::hiddenFilesShownChanged, this, &DolphinViewActionHandler::slotHiddenFilesShownChanged); connect(view, &DolphinView::sortRoleChanged, this, &DolphinViewActionHandler::slotSortRoleChanged); + connect(view, &DolphinView::groupRoleChanged, this, &DolphinViewActionHandler::slotGroupRoleChanged); + connect(view, &DolphinView::groupOrderChanged, this, &DolphinViewActionHandler::slotGroupOrderChanged); connect(view, &DolphinView::zoomLevelChanged, this, &DolphinViewActionHandler::slotZoomLevelChanged); connect(view, &DolphinView::writeStateChanged, this, &DolphinViewActionHandler::slotWriteStateChanged); slotWriteStateChanged(view->isFolderWritable()); @@@ -85,12 -87,6 +88,12 @@@ void DolphinViewActionHandler::createAc newDirAction->setEnabled(false); // Will be enabled in slotWriteStateChanged(bool) if the current URL is writable connect(newDirAction, &QAction::triggered, this, &DolphinViewActionHandler::createDirectoryTriggered); + QAction *newFileAction = m_actionCollection->addAction(QStringLiteral("create_file")); + newFileAction->setText(i18nc("@action", "Create File…")); + newFileAction->setIcon(QIcon::fromTheme(QStringLiteral("document-new"))); + newFileAction->setEnabled(false); // Will be enabled in slotWriteStateChanged(bool) if the current URL is writable + connect(newFileAction, &QAction::triggered, this, &DolphinViewActionHandler::createFileTriggered); + // File menu auto renameAction = KStandardAction::renameFile(this, &DolphinViewActionHandler::slotRename, m_actionCollection); @@@ -218,25 -214,12 +221,25 @@@ "view the contents of multiple folders in the same list.")); KSelectAction *viewModeActions = m_actionCollection->add(QStringLiteral("view_mode")); - viewModeActions->setText(i18nc("@action:intoolbar", "View Mode")); + viewModeActions->setText(i18nc("@action:intoolbar", "Change View Mode")); + viewModeActions->setWhatsThis(xi18nc("@info:whatsthis View Mode Toolbutton", "This cycles through all view modes.")); viewModeActions->addAction(iconsAction); viewModeActions->addAction(compactAction); viewModeActions->addAction(detailsAction); viewModeActions->setToolBarMode(KSelectAction::MenuMode); + viewModeActions->setToolButtonPopupMode(QToolButton::ToolButtonPopupMode::MenuButtonPopup); connect(viewModeActions, &KSelectAction::actionTriggered, this, &DolphinViewActionHandler::slotViewModeActionTriggered); + connect(viewModeActions, &KSelectAction::triggered, this, [this, viewModeActions, iconsAction, compactAction, detailsAction]() { + // Loop through the actions when button is clicked + const auto currentAction = viewModeActions->currentAction(); + if (currentAction == iconsAction) { + slotViewModeActionTriggered(compactAction); + } else if (currentAction == compactAction) { + slotViewModeActionTriggered(detailsAction); + } else if (currentAction == detailsAction) { + slotViewModeActionTriggered(iconsAction); + } + }); QAction *zoomInAction = KStandardAction::zoomIn(this, &DolphinViewActionHandler::zoomIn, m_actionCollection); zoomInAction->setWhatsThis(i18nc("@info:whatsthis zoom in", "This increases the icon size.")); @@@ -252,8 -235,13 +255,8 @@@ QAction *zoomOutAction = KStandardAction::zoomOut(this, &DolphinViewActionHandler::zoomOut, m_actionCollection); zoomOutAction->setWhatsThis(i18nc("@info:whatsthis zoom out", "This reduces the icon size.")); - KActionMenu *zoomMenu = m_actionCollection->add(QStringLiteral("zoom")); - zoomMenu->setText(i18nc("@action:inmenu menu of zoom actions", "Zoom")); - zoomMenu->setIcon(QIcon::fromTheme(QStringLiteral("zoom"))); - zoomMenu->setPopupMode(QToolButton::InstantPopup); - zoomMenu->addAction(zoomInAction); - zoomMenu->addAction(zoomResetAction); - zoomMenu->addAction(zoomOutAction); + ZoomWidgetAction *zoomWidgetAction = new ZoomWidgetAction(zoomInAction, zoomResetAction, zoomOutAction, m_actionCollection); + m_actionCollection->addAction(QStringLiteral("zoom"), zoomWidgetAction); KToggleAction *showPreview = m_actionCollection->add(QStringLiteral("show_preview")); showPreview->setText(i18nc("@action:intoolbar", "Show Previews")); @@@ -290,27 -278,60 +293,60 @@@ sortByActionMenu->addSeparator(); - QActionGroup *group = new QActionGroup(sortByActionMenu); - group->setExclusive(true); + QActionGroup *groupForSort = new QActionGroup(sortByActionMenu); + groupForSort->setExclusive(true); - KToggleAction *ascendingAction = m_actionCollection->add(QStringLiteral("ascending")); - ascendingAction->setActionGroup(group); - connect(ascendingAction, &QAction::triggered, this, [this] { + KToggleAction *sortAscendingAction = m_actionCollection->add(QStringLiteral("sort_ascending")); + sortAscendingAction->setActionGroup(groupForSort); + connect(sortAscendingAction, &QAction::triggered, this, [this] { m_currentView->setSortOrder(Qt::AscendingOrder); }); - KToggleAction *descendingAction = m_actionCollection->add(QStringLiteral("descending")); - descendingAction->setActionGroup(group); - connect(descendingAction, &QAction::triggered, this, [this] { + KToggleAction *sortDescendingAction = m_actionCollection->add(QStringLiteral("sort_descending")); + sortDescendingAction->setActionGroup(groupForSort); + connect(sortDescendingAction, &QAction::triggered, this, [this] { m_currentView->setSortOrder(Qt::DescendingOrder); }); - sortByActionMenu->addAction(ascendingAction); - sortByActionMenu->addAction(descendingAction); + sortByActionMenu->addAction(sortAscendingAction); + sortByActionMenu->addAction(sortDescendingAction); sortByActionMenu->addSeparator(); sortByActionMenu->addAction(sortFoldersFirst); sortByActionMenu->addAction(sortHiddenLast); + // View -> Group By + QActionGroup *groupByActionGroup = createFileItemRolesActionGroup(QStringLiteral("group_by_")); + + KActionMenu *groupByActionMenu = m_actionCollection->add(QStringLiteral("group")); + groupByActionMenu->setIcon(QIcon::fromTheme(QStringLiteral("view-group"))); + groupByActionMenu->setText(i18nc("@action:inmenu View", "Group By")); + groupByActionMenu->setPopupMode(QToolButton::InstantPopup); + + const auto groupByActionGroupActions = groupByActionGroup->actions(); + for (QAction *action : groupByActionGroupActions) { + groupByActionMenu->addAction(action); + } + + groupByActionMenu->addSeparator(); + + QActionGroup *groupForGroup = new QActionGroup(groupByActionMenu); + groupForGroup->setExclusive(true); + + KToggleAction *groupAscendingAction = m_actionCollection->add(QStringLiteral("group_ascending")); + groupAscendingAction->setActionGroup(groupForGroup); + connect(groupAscendingAction, &QAction::triggered, this, [this] { + m_currentView->setGroupOrder(Qt::AscendingOrder); + }); + + KToggleAction *groupDescendingAction = m_actionCollection->add(QStringLiteral("group_descending")); + groupDescendingAction->setActionGroup(groupForGroup); + connect(groupDescendingAction, &QAction::triggered, this, [this] { + m_currentView->setGroupOrder(Qt::DescendingOrder); + }); + + groupByActionMenu->addAction(groupAscendingAction); + groupByActionMenu->addAction(groupDescendingAction); + // View -> Additional Information QActionGroup *visibleRolesGroup = createFileItemRolesActionGroup(QStringLiteral("show_")); @@@ -354,37 -375,20 +390,40 @@@ "This opens a window " "in which all folder view properties can be adjusted.")); connect(adjustViewProps, &QAction::triggered, this, &DolphinViewActionHandler::slotAdjustViewProperties); + + // View settings: the dropdown menu contains various view-related actions + KActionMenu *viewSettings = m_actionCollection->add(QStringLiteral("view_settings")); + viewSettings->setText(i18nc("@action:intoolbar", "View Settings")); + viewSettings->setWhatsThis( + xi18nc("@info:whatsthis View Settings Toolbutton", "This cycles through all view modes. The dropdown menu contains various view-related actions.")); + const auto actions = viewModeActions->actions(); + for (QAction *action : actions) { + viewSettings->addAction(action); + } + viewSettings->addSeparator(); + viewSettings->addAction(zoomWidgetAction); + viewSettings->addAction(sortByActionMenu); + viewSettings->addAction(visibleRolesMenu); + viewSettings->addAction(showPreview); + viewSettings->addAction(showInGroups); + viewSettings->addAction(showHiddenFiles); + viewSettings->addAction(adjustViewProps); + viewSettings->setPopupMode(QToolButton::ToolButtonPopupMode::MenuButtonPopup); + connect(viewSettings, &KActionMenu::triggered, viewModeActions, &KSelectAction::triggered); } QActionGroup *DolphinViewActionHandler::createFileItemRolesActionGroup(const QString &groupPrefix) { const bool isSortGroup = (groupPrefix == QLatin1String("sort_by_")); - Q_ASSERT(isSortGroup || groupPrefix == QLatin1String("show_")); + const bool isGroupGroup = (groupPrefix == QLatin1String("group_by_")); + Q_ASSERT(isSortGroup || isGroupGroup || groupPrefix == QLatin1String("show_")); QActionGroup *rolesActionGroup = new QActionGroup(m_actionCollection); - rolesActionGroup->setExclusive(isSortGroup); + rolesActionGroup->setExclusive(isSortGroup || isGroupGroup); if (isSortGroup) { connect(rolesActionGroup, &QActionGroup::triggered, this, &DolphinViewActionHandler::slotSortTriggered); + } else if (isGroupGroup) { + connect(rolesActionGroup, &QActionGroup::triggered, this, &DolphinViewActionHandler::slotGroupTriggered); } else { connect(rolesActionGroup, &QActionGroup::triggered, this, &DolphinViewActionHandler::toggleVisibleRole); } @@@ -399,9 -403,13 +438,13 @@@ indexingEnabled = config.fileIndexingEnabled(); #endif - const QList rolesInfo = KFileItemModel::rolesInformation(); + QList rolesInfo = KFileItemModel::rolesInformation(); + if (isGroupGroup) { + rolesInfo += KFileItemModel::extraGroupingInformation(); + } + for (const KFileItemModel::RoleInfo &info : rolesInfo) { - if (!isSortGroup && info.role == "text") { + if (!isSortGroup && !isGroupGroup && info.role == "text") { // It should not be possible to hide the "text" role continue; } @@@ -419,9 -427,11 +462,11 @@@ groupMenu->setActionGroup(rolesActionGroup); groupMenuGroup = new QActionGroup(groupMenu); - groupMenuGroup->setExclusive(isSortGroup); + groupMenuGroup->setExclusive(isSortGroup || isGroupGroup); if (isSortGroup) { connect(groupMenuGroup, &QActionGroup::triggered, this, &DolphinViewActionHandler::slotSortTriggered); + } else if (isGroupGroup) { + connect(groupMenuGroup, &QActionGroup::triggered, this, &DolphinViewActionHandler::slotGroupTriggered); } else { connect(groupMenuGroup, &QActionGroup::triggered, this, &DolphinViewActionHandler::toggleVisibleRole); } @@@ -439,6 -449,8 +484,8 @@@ if (isSortGroup) { m_sortByActions.insert(info.role, action); + } else if (isGroupGroup) { + m_groupByActions.insert(info.role, action); } else { m_visibleRoles.insert(info.role, action); } @@@ -454,9 -466,6 +501,9 @@@ void DolphinViewActionHandler::slotView QAction *viewModeMenu = m_actionCollection->action(QStringLiteral("view_mode")); viewModeMenu->setIcon(action->icon()); + + QAction *viewSettingsAction = m_actionCollection->action(QStringLiteral("view_settings")); + viewSettingsAction->setIcon(action->icon()); } void DolphinViewActionHandler::slotRename() @@@ -535,9 -544,6 +582,9 @@@ void DolphinViewActionHandler::updateVi QAction *viewModeMenu = m_actionCollection->action(QStringLiteral("view_mode")); viewModeMenu->setIcon(viewModeAction->icon()); + + QAction *viewSettingsAction = m_actionCollection->action(QStringLiteral("view_settings")); + viewSettingsAction->setIcon(viewModeAction->icon()); } QAction *showPreviewAction = m_actionCollection->action(QStringLiteral("show_preview")); @@@ -549,6 -555,8 +596,8 @@@ slotVisibleRolesChanged(m_currentView->visibleRoles(), QList()); slotGroupedSortingChanged(m_currentView->groupedSorting()); slotSortRoleChanged(m_currentView->sortRole()); + slotGroupRoleChanged(m_currentView->groupRole()); + slotGroupOrderChanged(m_currentView->groupOrder()); slotZoomLevelChanged(m_currentView->zoomLevel(), -1); // Updates the "show_hidden_files" action state and icon @@@ -589,13 -597,22 +638,22 @@@ void DolphinViewActionHandler::toggleSo void DolphinViewActionHandler::slotSortOrderChanged(Qt::SortOrder order) { - QAction *descending = m_actionCollection->action(QStringLiteral("descending")); - QAction *ascending = m_actionCollection->action(QStringLiteral("ascending")); + QAction *descending = m_actionCollection->action(QStringLiteral("sort_descending")); + QAction *ascending = m_actionCollection->action(QStringLiteral("sort_ascending")); const bool sortDescending = (order == Qt::DescendingOrder); descending->setChecked(sortDescending); ascending->setChecked(!sortDescending); } + void DolphinViewActionHandler::slotGroupOrderChanged(Qt::SortOrder order) + { + QAction *descending = m_actionCollection->action(QStringLiteral("group_descending")); + QAction *ascending = m_actionCollection->action(QStringLiteral("group_ascending")); + const bool groupDescending = (order == Qt::DescendingOrder); + descending->setChecked(groupDescending); + ascending->setChecked(!groupDescending); + } + void DolphinViewActionHandler::slotSortFoldersFirstChanged(bool foldersFirst) { m_actionCollection->action(QStringLiteral("folders_first"))->setChecked(foldersFirst); @@@ -667,9 -684,7 +725,9 @@@ void DolphinViewActionHandler::slotHidd void DolphinViewActionHandler::slotWriteStateChanged(bool isFolderWritable) { - m_actionCollection->action(QStringLiteral("create_dir"))->setEnabled(isFolderWritable && KProtocolManager::supportsMakeDir(currentView()->url())); + const bool supportsMakeDir = KProtocolManager::supportsMakeDir(currentView()->url()); + m_actionCollection->action(QStringLiteral("create_dir"))->setEnabled(isFolderWritable && supportsMakeDir); + m_actionCollection->action(QStringLiteral("create_file"))->setEnabled(isFolderWritable); } KToggleAction *DolphinViewActionHandler::iconsModeAction() @@@ -717,8 -732,8 +775,8 @@@ void DolphinViewActionHandler::slotSort } } - QAction *descending = m_actionCollection->action(QStringLiteral("descending")); - QAction *ascending = m_actionCollection->action(QStringLiteral("ascending")); + QAction *descending = m_actionCollection->action(QStringLiteral("sort_descending")); + QAction *ascending = m_actionCollection->action(QStringLiteral("sort_ascending")); if (role == "text" || role == "type" || role == "extension" || role == "tags" || role == "comment") { descending->setText(i18nc("Sort descending", "Z-A")); @@@ -740,6 -755,50 +798,50 @@@ slotSortOrderChanged(m_currentView->sortOrder()); } + void DolphinViewActionHandler::slotGroupRoleChanged(const QByteArray &role) + { + KToggleAction *action = m_groupByActions.value(role); + if (action) { + action->setChecked(true); + + if (!action->icon().isNull()) { + QAction *groupByMenu = m_actionCollection->action(QStringLiteral("group")); + groupByMenu->setIcon(action->icon()); + } + } + + QAction *descending = m_actionCollection->action(QStringLiteral("group_descending")); + QAction *ascending = m_actionCollection->action(QStringLiteral("group_ascending")); + + if (role == "text" || role == "type" || role == "extension" || role == "tags" || role == "comment") { + descending->setText(i18nc("Group descending", "Z-A")); + ascending->setText(i18nc("Group ascending", "A-Z")); + } else if (role == "size") { + descending->setText(i18nc("Group descending", "Largest First")); + ascending->setText(i18nc("Group ascending", "Smallest First")); + } else if (role == "modificationtime" || role == "creationtime" || role == "accesstime") { + descending->setText(i18nc("Group descending", "Newest First")); + ascending->setText(i18nc("Group ascending", "Oldest First")); + } else if (role == "rating") { + descending->setText(i18nc("Group descending", "Highest First")); + ascending->setText(i18nc("Group ascending", "Lowest First")); + } else { + descending->setText(i18nc("Group descending", "Descending")); + ascending->setText(i18nc("Group ascending", "Ascending")); + } + + // Disable group order selector if grouping behavior does not support it + if (role == "none" || role == "followSort") { + descending->setEnabled(false); + ascending->setEnabled(false); + } else { + descending->setEnabled(true); + ascending->setEnabled(true); + } + + slotGroupOrderChanged(m_currentView->groupOrder()); + } + void DolphinViewActionHandler::slotZoomLevelChanged(int current, int previous) { Q_UNUSED(previous) @@@ -781,6 -840,32 +883,32 @@@ void DolphinViewActionHandler::slotSort m_currentView->setSortRole(role); } + void DolphinViewActionHandler::slotGroupTriggered(QAction *action) + { + // The radiobuttons of the "Group By"-menu are split between the main-menu + // and several sub-menus. Because of this they don't have a common + // action-group that assures an exclusive toggle-state between the main-menu + // actions and the sub-menu-actions. If an action gets checked, it must + // be assured that all other actions get unchecked, except the ascending/ + // descending actions + for (QAction *groupAction : std::as_const(m_groupByActions)) { + KActionMenu *actionMenu = qobject_cast(groupAction); + if (actionMenu) { + const auto actions = actionMenu->menu()->actions(); + for (QAction *subAction : actions) { + subAction->setChecked(false); + } + } else if (groupAction->actionGroup()) { + groupAction->setChecked(false); + } + } + action->setChecked(true); + + // Apply the activated sort-role to the view + const QByteArray role = action->data().toByteArray(); + m_currentView->setGroupRole(role); + } + void DolphinViewActionHandler::slotAdjustViewProperties() { Q_EMIT actionBeingHandled(); diff --combined src/views/dolphinviewactionhandler.h index 20d62cf64,93ed10e2b..a589be9ea --- a/src/views/dolphinviewactionhandler.h +++ b/src/views/dolphinviewactionhandler.h @@@ -87,13 -87,6 +87,13 @@@ Q_SIGNALS */ void createDirectoryTriggered(); + /** + * Emitted if the user requested creating a new file. + * The receiver of the signal (DolphinMainWindow or DolphinPart) invokes + * the method createFile of their KNewFileMenu instance. + */ + void createFileTriggered(); + /** Used to request either entering or leaving of selection mode */ void selectionModeChangeTriggered(bool enabled, SelectionMode::BottomBar::Contents bottomBarContents = SelectionMode::BottomBar::Contents::GeneralContents); @@@ -165,6 -158,16 +165,16 @@@ private Q_SLOTS */ void slotSortRoleChanged(const QByteArray &role); + /** + * Updates the state of the 'Group Ascending/Descending' action. + */ + void slotGroupOrderChanged(Qt::SortOrder order); + + /** + * Updates the state of the 'Group by' actions. + */ + void slotGroupRoleChanged(const QByteArray &role); + /** * Updates the state of the 'Zoom In' and 'Zoom Out' actions. */ @@@ -186,6 -189,11 +196,11 @@@ */ void slotVisibleRolesChanged(const QList ¤t, const QList &previous); + /** + * Changes the grouping of the current view. + */ + void slotGroupTriggered(QAction *); + /** * Switches between sorting by groups or not. */ @@@ -281,6 -289,7 +296,7 @@@ private DolphinView *m_currentView; QHash m_sortByActions; + QHash m_groupByActions; QHash m_visibleRoles; }; diff --combined src/views/viewproperties.cpp index 8bf3b2531,60c643062..9c03cf598 --- a/src/views/viewproperties.cpp +++ b/src/views/viewproperties.cpp @@@ -12,10 -12,8 +12,10 @@@ #include "dolphindebug.h" #include +#include #include +#include namespace { @@@ -33,74 -31,6 +33,74 @@@ const char CustomizedDetailsString[] = const char ViewPropertiesFileName[] = ".directory"; } +ViewPropertySettings *ViewProperties::loadProperties(const QString &folderPath) const +{ + const QString settingsFile = folderPath + QDir::separator() + ViewPropertiesFileName; + + KFileMetaData::UserMetaData metadata(folderPath); + if (!metadata.isSupported()) { + return new ViewPropertySettings(KSharedConfig::openConfig(settingsFile, KConfig::SimpleConfig)); + } + + std::unique_ptr tempFile(new QTemporaryFile()); + tempFile->setAutoRemove(false); + if (!tempFile->open()) { + qCWarning(DolphinDebug) << "Could not open temp file"; + return nullptr; + } + if (QFile::exists(settingsFile)) { + // copy settings to tempfile to load them separately + QFile::remove(tempFile->fileName()); + QFile::copy(settingsFile, tempFile->fileName()); + + auto config = KConfig(tempFile->fileName(), KConfig::SimpleConfig); + // ignore settings that are outside of dolphin scope + if (config.hasGroup("Dolphin") || config.hasGroup("Settings")) { + const auto groupList = config.groupList(); + for (const auto &group : groupList) { + if (group != QStringLiteral("Dolphin") && group != QStringLiteral("Settings")) { + config.deleteGroup(group); + } + } + return new ViewPropertySettings(KSharedConfig::openConfig(tempFile->fileName(), KConfig::SimpleConfig)); + + } else if (!config.groupList().isEmpty()) { + // clear temp file content + QFile::remove(tempFile->fileName()); + } + } + + // load from metadata + const QString viewPropertiesString = metadata.attribute(QStringLiteral("kde.fm.viewproperties#1")); + if (viewPropertiesString.isEmpty()) { + return nullptr; + } + // load view properties from xattr to temp file then loads into ViewPropertySettings + QFile outputFile(tempFile->fileName()); + outputFile.open(QIODevice::WriteOnly); + outputFile.write(viewPropertiesString.toUtf8()); + outputFile.close(); + return new ViewPropertySettings(KSharedConfig::openConfig(tempFile->fileName(), KConfig::SimpleConfig)); +} + +ViewPropertySettings *ViewProperties::defaultProperties() const +{ + auto props = loadProperties(destinationDir(QStringLiteral("global"))); + if (props == nullptr) { + qCWarning(DolphinDebug) << "Could not load default global viewproperties"; + QTemporaryFile tempFile; + tempFile.setAutoRemove(false); + if (!tempFile.open()) { + qCWarning(DolphinDebug) << "Could not open temp file"; + props = new ViewPropertySettings; + } else { + props = new ViewPropertySettings(KSharedConfig::openConfig(tempFile.fileName(), KConfig::SimpleConfig)); + } + } + + return props; +} + ViewProperties::ViewProperties(const QUrl &url) : m_changedProps(false) , m_autoSave(true) @@@ -160,22 -90,14 +160,22 @@@ m_filePath = destinationDir(QStringLiteral("remote")) + m_filePath; } - const QString file = m_filePath + QDir::separator() + ViewPropertiesFileName; - m_node = new ViewPropertySettings(KSharedConfig::openConfig(file)); + m_node = loadProperties(m_filePath); + + bool useDefaultSettings = useGlobalViewProps || + // If the props timestamp is too old, + // use default values instead. + (m_node != nullptr && (!useGlobalViewProps || useSearchView || useTrashView || useRecentDocumentsView || useDownloadsView) + && m_node->timestamp() < settings->viewPropsTimestamp()); + + if (m_node == nullptr) { + // no settings found for m_filepath, load defaults + m_node = defaultProperties(); + useDefaultSettings = true; + } - // If the .directory file does not exist or the timestamp is too old, - // use default values instead. - const bool useDefaultProps = (!useGlobalViewProps || useSearchView || useTrashView || useRecentDocumentsView || useDownloadsView) - && (!QFile::exists(file) || (m_node->timestamp() < settings->viewPropsTimestamp())); - if (useDefaultProps) { + // default values for special directories + if (useDefaultSettings) { if (useSearchView) { const QString path = url.path(); @@@ -210,6 -132,14 +210,6 @@@ setSortRole(QByteArrayLiteral("modificationtime")); } } else { - // The global view-properties act as default for directories without - // any view-property configuration. Constructing a ViewProperties - // instance for an empty QUrl ensures that the global view-properties - // are loaded. - QUrl emptyUrl; - ViewProperties defaultProps(emptyUrl); - setDirProperties(defaultProps); - m_changedProps = false; } } @@@ -242,11 -172,6 +242,11 @@@ ViewProperties::~ViewProperties( save(); } + if (!m_node->config()->name().endsWith(ViewPropertiesFileName)) { + // remove temp file + QFile::remove(m_node->config()->name()); + } + delete m_node; m_node = nullptr; } @@@ -330,6 -255,32 +330,32 @@@ Qt::SortOrder ViewProperties::sortOrder return static_cast(m_node->sortOrder()); } + void ViewProperties::setGroupRole(const QByteArray &role) + { + if (m_node->groupRole() != role) { + m_node->setGroupRole(role); + update(); + } + } + + QByteArray ViewProperties::groupRole() const + { + return m_node->groupRole().toLatin1(); + } + + void ViewProperties::setGroupOrder(Qt::SortOrder groupOrder) + { + if (m_node->groupOrder() != groupOrder) { + m_node->setGroupOrder(groupOrder); + update(); + } + } + + Qt::SortOrder ViewProperties::groupOrder() const + { + return static_cast(m_node->groupOrder()); + } + void ViewProperties::setSortFoldersFirst(bool foldersFirst) { if (m_node->sortFoldersFirst() != foldersFirst) { @@@ -356,19 -307,6 +382,19 @@@ bool ViewProperties::sortHiddenLast() c return m_node->sortHiddenLast(); } +void ViewProperties::setDynamicViewPassed(bool dynamicViewPassed) +{ + if (m_node->dynamicViewPassed() != dynamicViewPassed) { + m_node->setDynamicViewPassed(dynamicViewPassed); + update(); + } +} + +bool ViewProperties::dynamicViewPassed() const +{ + return m_node->dynamicViewPassed(); +} + void ViewProperties::setVisibleRoles(const QList &roles) { if (roles == visibleRoles()) { @@@ -474,6 -412,8 +500,8 @@@ void ViewProperties::setDirProperties(c setGroupedSorting(props.groupedSorting()); setSortRole(props.sortRole()); setSortOrder(props.sortOrder()); + setGroupRole(props.groupRole()); + setGroupOrder(props.groupOrder()); setSortFoldersFirst(props.sortFoldersFirst()); setSortHiddenLast(props.sortHiddenLast()); setVisibleRoles(props.visibleRoles()); @@@ -500,112 -440,17 +528,112 @@@ void ViewProperties::update( void ViewProperties::save() { qCDebug(DolphinDebug) << "Saving view-properties to" << m_filePath; + + auto cleanDotDirectoryFile = [this]() { + const QString settingsFile = m_filePath + QDir::separator() + ViewPropertiesFileName; + if (QFile::exists(settingsFile)) { + qCDebug(DolphinDebug) << "cleaning .directory" << settingsFile; + KConfig cfg(settingsFile, KConfig::OpenFlag::SimpleConfig); + const auto groupList = cfg.groupList(); + for (const auto &group : groupList) { + if (group == QStringLiteral("Dolphin") || group == QStringLiteral("Settings")) { + cfg.deleteGroup(group); + } + } + if (cfg.groupList().isEmpty()) { + QFile::remove(settingsFile); + } else if (cfg.isDirty()) { + cfg.sync(); + } + } + }; + + // ensures the destination dir exists, in case we don't write metadata directly on the folder + QDir destinationDir(m_filePath); + if (!destinationDir.exists() && !destinationDir.mkpath(m_filePath)) { + qCWarning(DolphinDebug) << "Could not create fake directory to store metadata"; + } + + KFileMetaData::UserMetaData metaData(m_filePath); + if (metaData.isSupported()) { + const auto metaDataKey = QStringLiteral("kde.fm.viewproperties#1"); + + const auto items = m_node->items(); + const auto defaultConfig = defaultProperties(); + bool allDefault = true; + for (const auto item : items) { + if (item->name() == "Timestamp") { + continue; + } + if (item->name() == "Version") { + if (m_node->version() != CurrentViewPropertiesVersion) { + allDefault = false; + break; + } else { + continue; + } + } + auto defaultItem = defaultConfig->findItem(item->name()); + if (!defaultItem || defaultItem->property() != item->property()) { + allDefault = false; + break; + } + } + + if (allDefault) { + if (metaData.hasAttribute(metaDataKey)) { + qCDebug(DolphinDebug) << "clearing extended attributes for " << m_filePath; + const auto result = metaData.setAttribute(metaDataKey, QString()); + if (result != KFileMetaData::UserMetaData::NoError) { + qCWarning(DolphinDebug) << "could not clear extended attributes for " << m_filePath << "error:" << result; + } + } + cleanDotDirectoryFile(); + return; + } + + // save config to disk + if (!m_node->save()) { + qCWarning(DolphinDebug) << "could not save viewproperties" << m_node->config()->name(); + return; + } + + QFile configFile(m_node->config()->name()); + if (!configFile.open(QIODevice::ReadOnly)) { + qCWarning(DolphinDebug) << "Could not open readonly config file" << m_node->config()->name(); + } else { + // load config from disk + const QString viewPropertiesString = configFile.readAll(); + + // save to xattr + const auto result = metaData.setAttribute(metaDataKey, viewPropertiesString); + if (result != KFileMetaData::UserMetaData::NoError) { + if (result == KFileMetaData::UserMetaData::NoSpace) { + // copy settings to dotDirectory file as fallback + if (!configFile.copy(m_filePath + QDir::separator() + ViewPropertiesFileName)) { + qCWarning(DolphinDebug) << "could not write viewproperties to .directory for dir " << m_filePath; + } + // free the space used by viewproperties from the file metadata + metaData.setAttribute(metaDataKey, ""); + } else { + qCWarning(DolphinDebug) << "could not save viewproperties to extended attributes for dir " << m_filePath << "error:" << result; + } + // keep .directory file + return; + } + cleanDotDirectoryFile(); + } + + m_changedProps = false; + return; + } + QDir dir; dir.mkpath(m_filePath); m_node->setVersion(CurrentViewPropertiesVersion); m_node->save(); - m_changedProps = false; -} -bool ViewProperties::exist() const -{ - const QString file = m_filePath + QDir::separator() + ViewPropertiesFileName; - return QFile::exists(file); + m_changedProps = false; } QString ViewProperties::destinationDir(const QString &subDir) const diff --combined src/views/viewproperties.h index bee1e7330,0c0452d7a..ac9ee4bb6 --- a/src/views/viewproperties.h +++ b/src/views/viewproperties.h @@@ -59,15 -59,18 +59,21 @@@ public void setSortOrder(Qt::SortOrder sortOrder); Qt::SortOrder sortOrder() const; + void setGroupRole(const QByteArray &role); + QByteArray groupRole() const; + + void setGroupOrder(Qt::SortOrder groupOrder); + Qt::SortOrder groupOrder() const; + void setSortFoldersFirst(bool foldersFirst); bool sortFoldersFirst() const; void setSortHiddenLast(bool hiddenLast); bool sortHiddenLast() const; + void setDynamicViewPassed(bool dynamicViewPassed); + bool dynamicViewPassed() const; + /** * Sets the additional information for the current set view-mode. * Note that the additional-info property is the only property where @@@ -111,6 -114,15 +117,6 @@@ */ void save(); - /** - * @return True if properties for the given URL exist: - * As soon as the properties for an URL have been saved with - * ViewProperties::save(), true will be returned. If false is - * returned, the default view-properties are used. - */ - bool exist() const; - -private: /** * Returns the destination directory path where the view * properties are stored. \a subDir specifies the used sub @@@ -118,7 -130,6 +124,7 @@@ */ QString destinationDir(const QString &subDir) const; +private: /** * Returns the view-mode prefix when storing additional properties for * a view-mode. @@@ -156,11 -167,6 +162,11 @@@ */ static QString directoryHashForUrl(const QUrl &url); + /** @returns a ViewPropertySettings object with properties loaded for the directory at @param filePath. Ownership is returned to the caller. */ + ViewPropertySettings *loadProperties(const QString &folderPath) const; + /** @returns a ViewPropertySettings object with the globally configured default values. Ownership is returned to the caller. */ + ViewPropertySettings *defaultProperties() const; + Q_DISABLE_COPY(ViewProperties) private: