#include <QApplication>
#include <QClipboard>
#include <QKeyEvent>
+#include <QAction>
DolphinContextMenu::DolphinContextMenu(DolphinMainWindow *parent,
const KFileItem &fileInfo,
{
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);
}
Q_ASSERT(m_context & TrashContext);
Q_ASSERT(m_context & ItemContext);
- addAction(QIcon::fromTheme("restoration"), i18nc("@action:inmenu", "Restore"), [this]() {
- QList<QUrl> 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<QUrl> 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()
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());
});
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]() {
}
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")));
}
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);
}
#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"
#include <KColorSchemeManager>
#include <KConfig>
#include <KConfigGui>
+#include <KDesktopFile>
#include <KDialogJobUiDelegate>
#include <KDualAction>
#include <KFileItemListProperties>
#include <KProtocolInfo>
#include <KProtocolManager>
#include <KRecentFilesAction>
+#include <KRuntimePlatform>
#include <KShell>
#include <KShortcutsDialog>
#include <KStandardAction>
, m_forwardAction(nullptr)
, m_splitViewAction(nullptr)
, m_splitViewMenuAction(nullptr)
+ , m_diskSpaceUsageMenu(nullptr)
, m_sessionSaveTimer(nullptr)
, m_sessionSaveWatcher(nullptr)
, m_sessionSaveScheduled(false)
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);
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();
if (firstRun) {
menuBar()->setVisible(false);
+
+ if (usePhoneUi) {
+ Q_ASSERT(qobject_cast<QDockWidget *>(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();
}
updateAllowedToolbarAreas();
+ updateNavigatorsBackground();
// enable middle-click on back/forward/up to open in a new tab
auto *middleClickEventFilter = new MiddleClickActionEventFilter(this);
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);
}
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)
}
if (url.isValid()) {
QString icon;
- if (m_activeViewContainer->isSearchModeEnabled()) {
+ if (isSearchUrl(url)) {
icon = QStringLiteral("folder-saved-search-symbolic");
} else {
icon = KIO::iconNameForUrl(url);
}
}
-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)
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();
}
}
+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();
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()
{
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);
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);
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();
}
// 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();
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."));
});
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<KToolBarPopupAction *>(ac->action(QStringLiteral("zoom")))->popupMenu());
} else {
menu->clear();
}
// 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")));
// 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);
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);
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);
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)
{
const QVector<DolphinViewContainer *> 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()));
}
}
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"));
"<para>This helps you "
"find files and folders by opening a <emphasis>search bar</emphasis>. "
"There you can enter search terms and specify settings to find the "
- "items you are looking for.</para><para>Use this help again on "
- "the search bar so we can have a look at it while the settings are "
- "explained.</para>"));
+ "items you are looking for.</para>"));
// 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
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.
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",
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",
"<para>This opens a <emphasis>terminal</emphasis> application for the viewed location.</para>"
"<para>To learn more about terminals use the help features in the terminal application.</para>"));
- 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);
openTerminalHere->setWhatsThis(xi18nc("@info:whatsthis",
"<para>This opens <emphasis>terminal</emphasis> applications for the selected items' locations.</para>"
"<para>To learn more about terminals use the help features in the terminal application.</para>"));
- 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);
}
&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());
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());
});
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);
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"));
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());
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"));
}
}
} 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()) {
}
// Update state from toolbar action
- m_splitViewMenuAction->setText(m_splitViewAction->text());
m_splitViewMenuAction->setToolTip(m_splitViewAction->toolTip());
m_splitViewMenuAction->setIcon(m_splitViewAction->icon());
}
}
}
+void DolphinMainWindow::updateNavigatorsBackground()
+{
+ auto navigators = static_cast<DolphinNavigatorsWidgetAction *>(actionCollection()->action(QStringLiteral("url_navigators")));
+ navigators->setBackgroundEnabled(navigators->isInToolbar());
+}
+
bool DolphinMainWindow::isKompareInstalled() const
{
static bool initialized = false;
m_tabWidget->currentTabPage()->insertNavigatorsWidget(navigators);
}
updateAllowedToolbarAreas();
+ updateNavigatorsBackground();
(static_cast<KHamburgerMenu *>(actionCollection()->action(KStandardAction::name(KStandardAction::HamburgerMenu))))->hideActionsOf(toolBar());
}
void DolphinMainWindow::slotDoubleClickViewBackground(Qt::MouseButton button)
{
- if (button == Qt::MouseButton::LeftButton) {
+ if (button != Qt::MouseButton::LeftButton) {
// only handle left mouse button for now
return;
}
<?xml version="1.0"?>
<!DOCTYPE gui SYSTEM "kpartgui.dtd">
-<gui name="dolphinpart" version="16" translationDomain="dolphin">
+<gui name="dolphinpart" version="18" translationDomain="dolphin">
<MenuBar>
<Menu name="edit"><text>&Edit</text>
<Action name="new_menu"/>
</Menu>
<Menu name="view"><text>&View</text>
<Action name="sort" />
+ <Action name="group" />
<Action name="additional_info" />
<Action name="show_preview" />
<Action name="show_in_groups" />
<?xml version="1.0"?>
<!DOCTYPE gui SYSTEM "kpartgui.dtd">
-<gui name="dolphin" version="41">
+<gui name="dolphin" version="46">
<MenuBar>
<Menu name="file">
<Action name="new_menu" />
<Action name="view_zoom_out"/>
<Separator/>
<Action name="sort" />
+ <Action name="group" />
<Action name="view_mode" />
<Action name="additional_info" />
<Action name="show_preview" />
<Action name="open_preferred_search_tool" />
<Action name="open_terminal" />
<Action name="open_terminal_here" />
+ <Action name="manage_disk_space" />
<Action name="compare_files" />
<Action name="change_remote_encoding" />
</Menu>
<text context="@title:menu">Main Toolbar</text>
<Action name="go_back" />
<Action name="go_forward" />
- <Separator name="separator_1" />
- <Action name="icons" />
- <Action name="compact" />
- <Action name="details" />
+ <Action name="view_settings" />
<Action name="url_navigators" />
<Action name="split_view" />
<Action name="split_stash" />
<Action priority="0" name="icons"/>
<Action priority="0" name="compact"/>
<Action priority="0" name="details"/>
+ <Action priority="0" name="view_mode" />
+ <Action priority="0" name="view_settings" />
<Action priority="0" name="view_zoom_in"/>
<Action priority="0" name="view_zoom_reset"/>
<Action priority="0" name="view_zoom_out"/>
#include <KLocalizedString>
#include <KUrlMimeData>
+#ifndef QT_NO_ACCESSIBILITY
+#include <QAccessible>
+#endif
#include <QElapsedTimer>
#include <QIcon>
#include <QMimeData>
// #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()
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);
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;
}
}
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;
m_groups = ratingRoleGroups();
break;
default:
- m_groups = genericStringRoleGroups(sortRole());
+ m_groups = genericStringRoleGroups(role);
break;
}
}
}
+ KFileItemModel::RoleInfo KFileItemModel::roleInformation(const QByteArray &role)
+ {
+ static QHash<QByteArray, RoleInfo> 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::RoleInfo> KFileItemModel::rolesInformation()
{
static QList<RoleInfo> rolesInfo;
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);
}
}
return rolesInfo;
}
+ QList<KFileItemModel::RoleInfo> KFileItemModel::extraGroupingInformation()
+ {
+ static QList<RoleInfo> 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)
{
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<QByteArray> newRoles = m_roles;
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<QByteArray> 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;
// 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()
}
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<QPair<int, QVariant>> oldGroups = m_groups;
m_groups.clear();
QList<KFileItemModel::ItemData *> 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).
return itemDataList;
}
- void KFileItemModel::prepareItemsForSorting(QList<ItemData *> &itemDataList)
+ void KFileItemModel::prepareItemsWithRole(QList<ItemData *> &itemDataList, RoleType roleType)
{
- switch (m_sortRole) {
+ switch (roleType) {
case ExtensionRole:
case PermissionsRole:
case OwnerRole:
}
}
+ void KFileItemModel::prepareItemsForSorting(QList<ItemData *> &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"
}
}
- // 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<KFileItemModel::ItemData *>::iterator &begin, const QList<KFileItemModel::ItemData *>::iterator &end) const
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());
return QString::compare(a, b, Qt::CaseSensitive);
}
- QList<QPair<int, QVariant>> KFileItemModel::nameRoleGroups() const
+ KFileItemModel::ItemGroupInfo KFileItemModel::nameRoleGroup(const ItemData *itemData, bool withString) const
{
- Q_ASSERT(!m_itemData.isEmpty());
-
- const int maxIndex = count() - 1;
- QList<QPair<int, QVariant>> 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<QChar> 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<QChar> lettersAtoZ;
+ lettersAtoZ.reserve('Z' - 'A' + 1);
+ if (lettersAtoZ.empty()) {
+ for (char c = 'A'; c <= 'Z'; ++c) {
+ lettersAtoZ.push_back(QLatin1Char(c));
+ }
+ }
- std::vector<QChar>::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<QChar>::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<int, QVariant>(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<QPair<int, QVariant>> 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<QPair<int, QVariant>> 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<int, QVariant>(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<QPair<int, QVariant>> KFileItemModel::timeRoleGroups(const std::function<QDateTime(const ItemData *)> &fileTimeCb) const
+ KFileItemModel::ItemGroupInfo
+ KFileItemModel::timeRoleGroup(const std::function<QDateTime(const ItemData *)> &fileTimeCb, const ItemData *itemData, bool withString) const
{
- Q_ASSERT(!m_itemData.isEmpty());
-
- const int maxIndex = count() - 1;
- QList<QPair<int, QVariant>> groups;
+ static bool oldWithString;
+ static ItemGroupInfo oldGroupInfo;
+ static QDate oldFileDate;
+ ItemGroupInfo groupInfo;
const QDate currentDate = QDate::currentDate();
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"));
"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);
"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";
"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 "
"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 "
"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 "
"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";
"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";
"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<int, QVariant>(i, newGroupValue));
- }
}
-
- return groups;
+ oldWithString = withString;
+ oldFileDate = fileDate;
+ oldGroupInfo = groupInfo;
+ return groupInfo;
}
- QList<QPair<int, QVariant>> 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<QPair<int, QVariant>> 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<QPair<int, QVariant>> KFileItemModel::nameRoleGroups() const
+ {
+ Q_ASSERT(!m_itemData.isEmpty());
+
+ const int maxIndex = count() - 1;
+ QList<QPair<int, QVariant>> 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<int, QVariant>(i, newGroupValue));
+ if (newGroupInfo != groupInfo) {
+ groupInfo = newGroupInfo;
+ groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
}
}
+ return groups;
+ }
+
+ QList<QPair<int, QVariant>> KFileItemModel::sizeRoleGroups() const
+ {
+ Q_ASSERT(!m_itemData.isEmpty());
+
+ const int maxIndex = count() - 1;
+ QList<QPair<int, QVariant>> 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<int, QVariant>(i, newGroupInfo.text));
+ }
+ }
return groups;
}
- QList<QPair<int, QVariant>> 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<QPair<int, QVariant>> KFileItemModel::timeRoleGroups(const std::function<QDateTime(const ItemData *)> &fileTimeCb) const
{
Q_ASSERT(!m_itemData.isEmpty());
const int maxIndex = count() - 1;
QList<QPair<int, QVariant>> 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<int, QVariant>(i, newGroupValue));
+
+ ItemGroupInfo newGroupInfo = timeRoleGroup(fileTimeCb, m_itemData.at(i));
+
+ if (newGroupInfo != groupInfo) {
+ groupInfo = newGroupInfo;
+ groups.append(QPair<int, QVariant>(i, newGroupInfo.text));
}
}
+ return groups;
+ }
+ QList<QPair<int, QVariant>> KFileItemModel::permissionRoleGroups() const
+ {
+ Q_ASSERT(!m_itemData.isEmpty());
+
+ const int maxIndex = count() - 1;
+ QList<QPair<int, QVariant>> 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<int, QVariant>(i, newGroupInfo.text));
+ }
+ }
return groups;
}
- QList<QPair<int, QVariant>> KFileItemModel::genericStringRoleGroups(const QByteArray &role) const
+ QList<QPair<int, QVariant>> KFileItemModel::ratingRoleGroups() const
{
Q_ASSERT(!m_itemData.isEmpty());
const int maxIndex = count() - 1;
QList<QPair<int, QVariant>> 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<int, QVariant>(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<int, QVariant>(i, newGroupInfo.comparable));
}
}
+ return groups;
+ }
+
+ QList<QPair<int, QVariant>> KFileItemModel::genericStringRoleGroups(const QByteArray &role) const
+ {
+ Q_ASSERT(!m_itemData.isEmpty());
+
+ const int maxIndex = count() - 1;
+ QList<QPair<int, QVariant>> 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<int, QVariant>(i, newGroupInfo.text));
+ }
+ }
return groups;
}
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 },
{ "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 },
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
*/
static QList<RoleInfo> rolesInformation();
+ /**
+ * @return Provides static information for all available grouping
+ * behaviors supported by KFileItemModel but not directly
+ * mapped to roles of KFileItemModel.
+ */
+ static QList<RoleInfo> extraGroupingInformation();
+
/** set to true to hide application/x-trash files */
void setShowTrashMime(bool show);
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();
QHash<QByteArray, QVariant> 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<ItemData *> &items);
void removeItems(const KItemRangeList &itemRanges, RemoveItemsBehavior behavior);
*/
QList<ItemData *> 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<ItemData *> &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
*/
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<QDateTime(const ItemData *)> &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<QPair<int, QVariant>> nameRoleGroups() const;
QList<QPair<int, QVariant>> sizeRoleGroups() const;
QList<QPair<int, QVariant>> timeRoleGroups(const std::function<QDateTime(const ItemData *)> &fileTimeCb) const;
QList<QPair<int, QVariant>> permissionRoleGroups() const;
QList<QPair<int, QVariant>> ratingRoleGroups() const;
+ QList<QPair<int, QVariant>> typeRoleGroups() const;
QList<QPair<int, QVariant>> genericStringRoleGroups(const QByteArray &typeForRole) const;
/**
bool m_sortHiddenLast;
RoleType m_sortRole;
+ RoleType m_groupRole;
+ QByteArray m_sortExtraInfo;
+ QByteArray m_groupExtraInfo;
+
int m_sortingProgressPercent; // Value of directorySortingProgress() signal
QSet<QByteArray> m_roles;
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
}
}
+ 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
timer.start();
// Determine the sort role synchronously for as many items as possible.
- if (m_resolvableRoles.contains(m_model->sortRole())) {
- QList<QUrl> dirsWithAddedItems;
-
+ if (m_resolvableRoles.contains(m_model->sortRole()) || m_resolvableRoles.contains(m_model->groupRole())) {
++ QList<QUrl> 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
m_directoryContentsCounter->stopWorker();
}
} else {
+ QList<QUrl> dirsWithDeletedItems;
// Only remove the items from m_finishedItems. They will be removed
// from the other sets later on.
QSet<KFileItem>::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;
return;
}
- QPixmap scaledPixmap = transformPreviewPixmap(pixmap);
-
QHash<QByteArray, QVariant> 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);
}
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.
} 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()) {
{
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 {
KPixmapModifier::applyFrame(scaledPixmap, m_iconSize);
}
}
- } else if (!pixmap.isNull()) {
+ } else {
KPixmapModifier::scale(scaledPixmap, m_iconSize * m_devicePixelRatio);
scaledPixmap.setDevicePixelRatio(m_devicePixelRatio);
}
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) {
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);
QHash<QByteArray, QVariant> 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 {
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
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<QByteArray, QVariant> newData;
}
if (origCount != entryCount) {
- // count has changed
newData.insert("count", entryCount);
}
}
}
+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<QUrl> 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"
*/
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);
* 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);
/**
* 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);
void trimHoverSequenceLoadedItems();
+ void resetSizeData(const int index, const int size = 0);
+
+ void recountDirectoryItems(const QList<QUrl> directories);
+
private:
+ QSize cacheSize();
/**
* enqueue directory size counting for KFileItem item at index
*/
#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"
#include "private/kitemlistsizehintresolver.h"
#include "private/kitemlistviewlayouter.h"
+#include <optional>
+
#include <QElapsedTimer>
#include <QGraphicsSceneMouseEvent>
#include <QGraphicsView>
, m_header(nullptr)
, m_headerWidget(nullptr)
, m_indicatorAnimation(nullptr)
+ , m_statusBarOffset(0)
, m_dropIndicator()
, m_sizeHintResolver(nullptr)
{
qreal KItemListView::maximumScrollOffset() const
{
- return m_layouter->maximumScrollOffset();
+ return m_layouter->maximumScrollOffset() + m_statusBarOffset;
}
void KItemListView::setItemOffset(qreal offset)
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);
}
doLayout(NoAnimation);
}
+qreal KItemListView::scrollSingleStep() const
+{
+ const QFontMetrics metrics(font());
+ return metrics.height();
+}
+
qreal KItemListView::verticalPageStep() const
{
qreal headerHeight = 0;
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();
}
}
const KItemListWidget *widget = m_visibleItems.value(index);
if (widget) {
- contextRect = widget->iconRect() | widget->textRect();
+ contextRect = widget->selectionRectCore();
contextRect.translate(itemRect(index).topLeft());
}
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) {
void KItemListView::editRole(int index, const QByteArray &role)
{
KStandardItemListWidget *widget = qobject_cast<KStandardItemListWidget *>(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);
}
}
+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()) {
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> 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);
}
doLayout(NoAnimation);
updateSiblingsInformation();
+
+ if (movedEditedItem) {
+ editRole(movedEditedItem->movedToIndex, movedEditedItem->editedRole);
+ }
}
void KItemListView::slotItemsChanged(const KItemRangeList &itemRanges, const QSet<QByteArray> &roles)
updateVisibleGroupHeaders();
doLayout(NoAnimation);
}
-
- QAccessibleTableModelChangeEvent ev(this, QAccessibleTableModelChangeEvent::DataChanged);
- ev.setFirstRow(itemRange.index);
- ev.setLastRow(itemRange.index + itemRange.count);
- QAccessible::updateAccessibility(&ev);
}
-
doLayout(NoAnimation);
}
}
}
+ 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) {
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<KItemListViewAccessible *>(QAccessible::queryAccessibleInterface(this))->announceCurrentItem();
+ }
+#endif
}
void KItemListView::slotSelectionChanged(const KItemSet ¤t, const KItemSet &previous)
{
- Q_UNUSED(previous)
-
QHashIterator<int, KItemListWidget *> 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)
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;
});
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()));
}
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;
}
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);
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);
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)
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
*/
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
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);
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);
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();
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
friend class KItemListController;
friend class KItemListControllerTest;
friend class KItemListViewAccessible;
- friend class KItemListAccessibleCell;
+ friend class KItemListDelegateAccessible;
+
+ friend class DolphinMainWindowTest;
};
/**
<entry name="ViewMode" type="Int" >
<label context="@label">View Mode</label>
<whatsthis context="@info:whatsthis">This option controls the style of the view. Currently supported values include icons (0), details (1) and column (2) views.</whatsthis>
- <default>DolphinView::IconsView</default>
+ <default code="true">DolphinView::IconsView</default>
</entry>
<entry name="PreviewsShown" type="Bool" >
<entry name="GroupedSorting" type="Bool" >
<label context="@label">Grouped Sorting</label>
- <whatsthis context="@info:whatsthis">When this option is enabled, the sorted items are categorized into groups.</whatsthis>
- <default>false</default>
+ <whatsthis context="@info:whatsthis">When this option is enabled, the items are categorized into groups.</whatsthis>
+ <default>true</default>
</entry>
<entry name="SortRole" type="String" >
<max code="true">Qt::DescendingOrder</max>
</entry>
+ <entry name="GroupRole" type="String" >
+ <label context="@label">Group files by</label>
+ <whatsthis context="@info:whatsthis">This option defines which attribute (text, size, date, etc.) grouping is performed on.</whatsthis>
+ <default>none</default>
+ </entry>
+
+ <entry name="GroupOrder" type="Int" >
+ <label context="@label">Order in which to group files</label>
+ <default code="true">Qt::AscendingOrder</default>
+ <min code="true">Qt::AscendingOrder</min>
+ <max code="true">Qt::DescendingOrder</max>
+ </entry>
+
<entry name="SortFoldersFirst" type="Bool" >
<label context="@label">Show folders first when sorting files and folders</label>
<default>true</default>
<whatsthis context="@info:whatsthis">The last time these properties were changed by the user.</whatsthis>
</entry>
+ <entry name="DynamicViewPassed" type="Bool">
+ <label context="@label">View mode changed once by dynamic view</label>
+ <default>false</default>
+ </entry>
+
<!-- Obsolete - replaced by VisibleRoles -->
<entry name="AdditionalInfo" type="StringList">
<label context="@label">Additional Information</label>
</entry>
</group>
</kcfg>
-
-
#include <KAuthorized>
#include <KLocalizedString>
#include <KMessageBox>
+#include <KRuntimePlatform>
#include <KWindowConfig>
#include <kwidgetsaddons_version.h>
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
actions,
{QStringLiteral("add_to_places"),
QStringLiteral("sort"),
+ QStringLiteral("group"),
QStringLiteral("view_mode"),
QStringLiteral("open_in_new_tab"),
QStringLiteral("open_in_new_window"),
void testMakeExpandedItemHidden();
void testRemoveFilteredExpandedItems();
void testSorting();
+ void testNaturalSorting();
void testIndexForKeyboardSearch();
void testNameFilter();
void testEmptyPath();
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()
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());
}
QCOMPARE(itemsMovedSpy.takeFirst().at(1).value<QList<int>>(), QList<int>() << 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>(), KItemRange(2, 5));
+ QCOMPARE(itemsMovedSpy.takeFirst().at(1).value<QList<int>>(), QList<int>() << 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>(), KItemRange(1, 6));
+ QCOMPARE(itemsMovedSpy.takeFirst().at(1).value<QList<int>>(), QList<int>() << 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());
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);
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?
}
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(),
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(),
QList<QPair<int, QVariant>> expectedGroups;
expectedGroups << QPair<int, QVariant>(0, QLatin1String("A"));
expectedGroups << QPair<int, QVariant>(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").
#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"
#include <KIO/Paste>
#include <KIO/PasteJob>
#include <KIO/RenameFileDialog>
+#include <KJob>
#include <KJobWidgets>
#include <KLocalizedString>
#include <KMessageBox>
#include <kwidgetsaddons_version.h>
#include <QAbstractItemView>
+#ifndef QT_NO_ACCESSIBILITY
+#include <QAccessible>
+#endif
#include <QActionGroup>
#include <QApplication>
#include <QClipboard>
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
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();
});
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);
m_view->setEnabledSelectionToggles(DolphinItemListView::SelectionTogglesEnabled::FollowSetting);
}
m_container->controller()->setSelectionModeEnabled(enabled);
+#ifndef QT_NO_ACCESSIBILITY
+ if (QAccessible::isActive()) {
+ auto accessibleViewInterface = static_cast<KItemListViewAccessible *>(QAccessible::queryAccessibleInterface(m_view));
+ accessibleViewInterface->announceSelectionModeEnabled(enabled);
+ }
+#endif
}
bool DolphinView::selectionMode() const
ViewProperties props(viewPropertiesUrl());
props.setGroupedSorting(grouped);
- props.save();
- m_container->controller()->model()->setGroupedSorting(grouped);
+ m_model->setGroupedSorting(grouped);
Q_EMIT groupedSortingChanged(grouped);
}
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) {
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) {
} 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<QUrl> &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<QByteArray, QVariant> 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();
}
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();
}
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);
}
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);
}
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);
}
}
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());
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);
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();
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();
}
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)
if (!selectedItems.isEmpty()) {
selectionManager->beginAnchoredSelection(selectionManager->currentItem());
selectionManager->setSelectedItems(selectedItems);
- selectionManager->endAnchoredSelection();
if (shouldScrollToCurrentItem) {
m_view->scrollToItem(selectedItems.first());
}
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;
}
if (nextItem >= 0) {
selectionManager->setSelected(nextItem, 1);
+ selectionManager->beginAnchoredSelection(nextItem);
}
m_selectNextItem = false;
}
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<KIO::CopyJob *>(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<QByteArray, QVariant> data;
- const QUrl oldUrl = copyJob->srcUrls().at(0);
- data.insert("text", oldUrl.fileName());
+ data.insert("text", newUrl.fileName());
m_model->setData(index, data);
}
}
Q_EMIT directoryLoadingCompleted();
+ applyDynamicView();
updatePlaceholderLabel();
updateWritableState();
}
}
#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<QByteArray, QVariant> 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.
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);
} else {
header->setAutomaticColumnResizing(true);
}
- header->setSidePadding(DetailsModeSettings::sidePadding());
+ header->setSidePadding(DetailsModeSettings::leftPadding(), DetailsModeSettings::rightPadding());
}
m_view->endTransaction();
}
}
+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);
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)
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()) {
return url;
}
-void DolphinView::slotRenameDialogRenamingFinished(const QList<QUrl> &urls)
-{
- forceUrlsSelection(urls.first(), urls);
-}
-
void DolphinView::forceUrlsSelection(const QUrl ¤t, const QList<QUrl> &selected)
{
clearSelection();
{
m_placeholderLabel->setText(i18n("Loading…"));
m_placeholderLabel->setVisible(true);
+#ifndef QT_NO_ACCESSIBILITY
+ if (QAccessible::isActive()) {
+ static_cast<KItemListViewAccessible *>(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<KItemListViewAccessible *>(QAccessible::queryAccessibleInterface(m_view))->announceNewlyLoadedLocation(QString());
+ }
+#endif
m_placeholderLabel->setVisible(false);
return;
}
}
m_placeholderLabel->setVisible(true);
+#ifndef QT_NO_ACCESSIBILITY
+ if (QAccessible::isActive()) {
+ static_cast<KItemListViewAccessible *>(QAccessible::queryAccessibleInterface(m_view))->announceNewlyLoadedLocation(m_placeholderLabel->text());
+ }
+#endif
}
bool DolphinView::tryShowNameToolTip(QHelpEvent *event)
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;
}
}
* - show hidden files
* - show previews
* - enable grouping
+ * - grouping order
+ * - grouping type
*/
class DOLPHIN_EXPORT DolphinView : public QWidget
{
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;
*/
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();
/** 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.
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<QUrl> &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);
*/
void applyModeToView();
+ /**
+ * Changes the current view based on the content of the directory.
+ */
+ void applyDynamicView();
+
enum Selection { HasSelection, NoSelection };
/**
* Helper method for DolphinView::requestStatusBarText().
#include "selectionmode/actiontexthelper.h"
#include "settings/viewpropertiesdialog.h"
#include "views/zoomlevelinfo.h"
+#include "views/zoomwidgetaction.h"
#if HAVE_BALOO
#include <Baloo/IndexerConfig>
, m_actionCollection(collection)
, m_currentView(nullptr)
, m_sortByActions()
+ , m_groupByActions()
, m_visibleRoles()
{
Q_ASSERT(m_actionCollection);
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());
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);
"view the contents of multiple folders in the same list.</para>"));
KSelectAction *viewModeActions = m_actionCollection->add<KSelectAction>(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."));
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<KActionMenu>(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<KToggleAction>(QStringLiteral("show_preview"));
showPreview->setText(i18nc("@action:intoolbar", "Show Previews"));
sortByActionMenu->addSeparator();
- QActionGroup *group = new QActionGroup(sortByActionMenu);
- group->setExclusive(true);
+ QActionGroup *groupForSort = new QActionGroup(sortByActionMenu);
+ groupForSort->setExclusive(true);
- KToggleAction *ascendingAction = m_actionCollection->add<KToggleAction>(QStringLiteral("ascending"));
- ascendingAction->setActionGroup(group);
- connect(ascendingAction, &QAction::triggered, this, [this] {
+ KToggleAction *sortAscendingAction = m_actionCollection->add<KToggleAction>(QStringLiteral("sort_ascending"));
+ sortAscendingAction->setActionGroup(groupForSort);
+ connect(sortAscendingAction, &QAction::triggered, this, [this] {
m_currentView->setSortOrder(Qt::AscendingOrder);
});
- KToggleAction *descendingAction = m_actionCollection->add<KToggleAction>(QStringLiteral("descending"));
- descendingAction->setActionGroup(group);
- connect(descendingAction, &QAction::triggered, this, [this] {
+ KToggleAction *sortDescendingAction = m_actionCollection->add<KToggleAction>(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<KActionMenu>(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<KToggleAction>(QStringLiteral("group_ascending"));
+ groupAscendingAction->setActionGroup(groupForGroup);
+ connect(groupAscendingAction, &QAction::triggered, this, [this] {
+ m_currentView->setGroupOrder(Qt::AscendingOrder);
+ });
+
+ KToggleAction *groupDescendingAction = m_actionCollection->add<KToggleAction>(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_"));
"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<KActionMenu>(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);
}
indexingEnabled = config.fileIndexingEnabled();
#endif
- const QList<KFileItemModel::RoleInfo> rolesInfo = KFileItemModel::rolesInformation();
+ QList<KFileItemModel::RoleInfo> 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;
}
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);
}
if (isSortGroup) {
m_sortByActions.insert(info.role, action);
+ } else if (isGroupGroup) {
+ m_groupByActions.insert(info.role, action);
} else {
m_visibleRoles.insert(info.role, action);
}
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()
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"));
slotVisibleRolesChanged(m_currentView->visibleRoles(), QList<QByteArray>());
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
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);
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()
}
}
- 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"));
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)
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<KActionMenu *>(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();
*/
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);
*/
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.
*/
*/
void slotVisibleRolesChanged(const QList<QByteArray> ¤t, const QList<QByteArray> &previous);
+ /**
+ * Changes the grouping of the current view.
+ */
+ void slotGroupTriggered(QAction *);
+
/**
* Switches between sorting by groups or not.
*/
DolphinView *m_currentView;
QHash<QByteArray, KToggleAction *> m_sortByActions;
+ QHash<QByteArray, KToggleAction *> m_groupByActions;
QHash<QByteArray, KToggleAction *> m_visibleRoles;
};
#include "dolphindebug.h"
#include <QCryptographicHash>
+#include <QTemporaryFile>
#include <KFileItem>
+#include <KFileMetaData/UserMetaData>
namespace
{
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<QTemporaryFile> 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)
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();
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;
}
}
save();
}
+ if (!m_node->config()->name().endsWith(ViewPropertiesFileName)) {
+ // remove temp file
+ QFile::remove(m_node->config()->name());
+ }
+
delete m_node;
m_node = nullptr;
}
return static_cast<Qt::SortOrder>(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<Qt::SortOrder>(m_node->groupOrder());
+ }
+
void ViewProperties::setSortFoldersFirst(bool foldersFirst)
{
if (m_node->sortFoldersFirst() != foldersFirst) {
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<QByteArray> &roles)
{
if (roles == visibleRoles()) {
setGroupedSorting(props.groupedSorting());
setSortRole(props.sortRole());
setSortOrder(props.sortOrder());
+ setGroupRole(props.groupRole());
+ setGroupOrder(props.groupOrder());
setSortFoldersFirst(props.sortFoldersFirst());
setSortHiddenLast(props.sortHiddenLast());
setVisibleRoles(props.visibleRoles());
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
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
*/
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
*/
QString destinationDir(const QString &subDir) const;
+private:
/**
* Returns the view-mode prefix when storing additional properties for
* a view-mode.
*/
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: