diff options
Diffstat (limited to 'src')
75 files changed, 1514 insertions, 369 deletions
diff --git a/src/animatedheightwidget.cpp b/src/animatedheightwidget.cpp index c89863b25..ed7e6bdc6 100644 --- a/src/animatedheightwidget.cpp +++ b/src/animatedheightwidget.cpp @@ -33,6 +33,11 @@ AnimatedHeightWidget::AnimatedHeightWidget(QWidget *parent) m_contentsContainerParent->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_contentsContainerParent->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_contentsContainerParent->setWidgetResizable(true); + // Prevent the internal scroll area from reacting to navigation keys and + // scrolling the contents (scrollbars are hidden but scrolling still changes + // the visible region). + m_contentsContainerParent->installEventFilter(this); + m_contentsContainerParent->viewport()->installEventFilter(this); // Disables manual scrolling, for example with mouse scrollwheel. m_contentsContainerParent->verticalScrollBar()->setEnabled(false); m_contentsContainerParent->horizontalScrollBar()->setEnabled(false); @@ -88,7 +93,6 @@ QWidget *AnimatedHeightWidget::prepareContentsContainer(QWidget *contentsContain contentsContainer->setParent(m_contentsContainerParent); m_contentsContainerParent->setWidget(contentsContainer); m_contentsContainerParent->setFocusProxy(contentsContainer); - contentsContainer->installEventFilter(this); return contentsContainer; } @@ -99,12 +103,20 @@ bool AnimatedHeightWidget::isAnimationRunning() const bool AnimatedHeightWidget::eventFilter(QObject *obj, QEvent *event) { - if (event->type() == QEvent::KeyPress) { + if ((obj == m_contentsContainerParent || obj == m_contentsContainerParent->viewport()) && event->type() == QEvent::KeyPress) { auto *keyEvent = static_cast<QKeyEvent *>(event); - // Ignore PageUp/PageDown to prevent QScrollArea (invisible scrollbar) from scrolling - if (keyEvent->key() == Qt::Key_PageUp || keyEvent->key() == Qt::Key_PageDown) { + // Ignore navigation keys to prevent the internal QScrollArea (with + // invisible scrollbars) from scrolling and visually moving child bars + // such as the search bar. + switch (keyEvent->key()) { + case Qt::Key_Up: + case Qt::Key_Down: + case Qt::Key_PageUp: + case Qt::Key_PageDown: keyEvent->accept(); return true; + default: + break; } } return QWidget::eventFilter(obj, event); diff --git a/src/animatedheightwidget.h b/src/animatedheightwidget.h index 3933df93f..92701bd25 100644 --- a/src/animatedheightwidget.h +++ b/src/animatedheightwidget.h @@ -57,8 +57,9 @@ protected: protected: /** - * Ignore PageUp/PageDown key events to prevent the internal QScrollArea - * (with an invisible scrollbar) from scrolling while editing inside child widgets. + * Ignore Up/Down/PageUp/PageDown key events for the internal QScrollArea + * (with an invisible scrollbar) to prevent it from scrolling and moving the + * visible region of contained widgets such as the search bar. */ bool eventFilter(QObject *obj, QEvent *event) override; diff --git a/src/dolphincontextmenu.cpp b/src/dolphincontextmenu.cpp index 6fb362039..796c723ed 100644 --- a/src/dolphincontextmenu.cpp +++ b/src/dolphincontextmenu.cpp @@ -121,6 +121,18 @@ bool DolphinContextMenu::eventFilter(QObject *object, QEvent *event) void DolphinContextMenu::addTrashContextMenu() { + // Insert 'Sort By' and 'View Mode' + if (ContextMenuSettings::showSortBy()) { + addAction(m_mainWindow->actionCollection()->action(QStringLiteral("sort"))); + } + if (ContextMenuSettings::showViewMode()) { + addAction(m_mainWindow->actionCollection()->action(QStringLiteral("view_mode"))); + } + + if (ContextMenuSettings::showSortBy() || ContextMenuSettings::showViewMode()) { + addSeparator(); + } + Q_ASSERT(m_context & TrashContext); QAction *emptyTrashAction = addAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18nc("@action:inmenu", "Empty Trash"), this, [this]() { @@ -132,17 +144,6 @@ void DolphinContextMenu::addTrashContextMenu() emptyTrashAction->setEnabled(!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(); auto *configureTrashAction = new QAction(QIcon::fromTheme(QStringLiteral("configure")), i18nc("@action:inmenu", "Configure Trash…"), this); @@ -267,6 +268,9 @@ void DolphinContextMenu::addItemContextMenu() Q_ASSERT(!m_fileInfo.isNull()); const KFileItemListProperties &selectedItemsProps = selectedItemsProperties(); + // This is updated live in DolphinMainWindow::slotSelectionChanged but there can + // be situations where there are no selected items. + m_fileItemActions->setItemListProperties(selectedItemsProps); if (m_selectedItems.count() == 1) { // single files @@ -529,7 +533,7 @@ void DolphinContextMenu::addAdditionalActions(const KFileItemListProperties &pro addSeparator(); QList<QAction *> additionalActions; - if (props.isLocal() && ContextMenuSettings::showOpenTerminal()) { + if (props.isLocal() && props.isDirectory() && ContextMenuSettings::showOpenTerminal()) { additionalActions << m_mainWindow->actionCollection()->action(QStringLiteral("open_terminal_here")); } m_fileItemActions->addActionsTo(this, KFileItemActions::MenuActionSource::All, additionalActions); diff --git a/src/dolphinmainwindow.cpp b/src/dolphinmainwindow.cpp index f276d88d4..0aff2299c 100644 --- a/src/dolphinmainwindow.cpp +++ b/src/dolphinmainwindow.cpp @@ -33,7 +33,6 @@ #endif #include "settings/dolphinsettingsdialog.h" #include "statusbar/diskspaceusagemenu.h" -#include "statusbar/dolphinstatusbar.h" #include "views/dolphinnewfilemenuobserver.h" #include "views/dolphinremoteencoding.h" #include "views/dolphinviewactionhandler.h" @@ -87,6 +86,7 @@ #include <QLineEdit> #include <QMenuBar> #include <QPushButton> +#include <QSharedPointer> #include <QShowEvent> #include <QStandardPaths> #include <QTimer> @@ -163,8 +163,6 @@ DolphinMainWindow::DolphinMainWindow() connect(undoManager, &KIO::FileUndoManager::redoAvailable, this, &DolphinMainWindow::slotRedoAvailable); connect(undoManager, &KIO::FileUndoManager::redoTextChanged, this, &DolphinMainWindow::slotRedoTextChanged); #endif - connect(undoManager, &KIO::FileUndoManager::jobRecordingStarted, this, &DolphinMainWindow::clearStatusBar); - connect(undoManager, &KIO::FileUndoManager::jobRecordingFinished, this, &DolphinMainWindow::showCommand); const bool firstRun = (GeneralSettings::version() < 200); if (firstRun) { @@ -186,7 +184,6 @@ DolphinMainWindow::DolphinMainWindow() setupActions(); 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); @@ -237,9 +234,8 @@ DolphinMainWindow::DolphinMainWindow() } } - const bool showMenu = !menuBar()->isHidden(); QAction *showMenuBarAction = actionCollection()->action(KStandardAction::name(KStandardAction::ShowMenubar)); - showMenuBarAction->setChecked(showMenu); // workaround for bug #171080 + menuBar()->setVisible(showMenuBarAction->isChecked()); auto hamburgerMenu = static_cast<KHamburgerMenu *>(actionCollection()->action(KStandardAction::name(KStandardAction::HamburgerMenu))); hamburgerMenu->setMenuBar(menuBar()); @@ -359,35 +355,6 @@ bool DolphinMainWindow::isActiveWindow() return window()->isActiveWindow(); } -void DolphinMainWindow::showCommand(CommandType command) -{ - DolphinStatusBar *statusBar = m_activeViewContainer->statusBar(); - switch (command) { - case KIO::FileUndoManager::Copy: - statusBar->setText(i18nc("@info:status", "Successfully copied.")); - break; - case KIO::FileUndoManager::Move: - statusBar->setText(i18nc("@info:status", "Successfully moved.")); - break; - case KIO::FileUndoManager::Link: - statusBar->setText(i18nc("@info:status", "Successfully linked.")); - break; - case KIO::FileUndoManager::Trash: - statusBar->setText(i18nc("@info:status", "Successfully moved to trash.")); - break; - case KIO::FileUndoManager::Rename: - statusBar->setText(i18nc("@info:status", "Successfully renamed.")); - break; - - case KIO::FileUndoManager::Mkdir: - statusBar->setText(i18nc("@info:status", "Created folder.")); - break; - - default: - break; - } -} - void DolphinMainWindow::pasteIntoFolder() { m_activeViewContainer->view()->pasteIntoFolder(); @@ -789,8 +756,10 @@ void DolphinMainWindow::slotSaveSession() 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(); + // Copy the config in the main thread so sync() can safely run in the worker. + QSharedPointer<KConfig> configCopy(config->copyTo(config->name())); + auto future = QtConcurrent::run([configCopy]() { + configCopy->sync(); }); m_sessionSaveWatcher->setFuture(future); } @@ -890,7 +859,6 @@ void DolphinMainWindow::slotUndoTextChanged(const QString &text) void DolphinMainWindow::undo() { - clearStatusBar(); KIO::FileUndoManager::self()->uiInterface()->setParentWidget(this); KIO::FileUndoManager::self()->undo(); } @@ -914,7 +882,6 @@ void DolphinMainWindow::slotRedoTextChanged(const QString &text) void DolphinMainWindow::redo() { - clearStatusBar(); KIO::FileUndoManager::self()->uiInterface()->setParentWidget(this); KIO::FileUndoManager::self()->redo(); } @@ -1080,8 +1047,6 @@ void DolphinMainWindow::slotSetSelectionMode(bool enabled, SelectionMode::Bottom void DolphinMainWindow::selectAll() { - clearStatusBar(); - // if the URL navigator is editable and focused, select the whole // URL instead of all items of the view @@ -1097,7 +1062,6 @@ void DolphinMainWindow::selectAll() void DolphinMainWindow::invertSelection() { - clearStatusBar(); m_activeViewContainer->view()->invertSelection(); } @@ -1169,9 +1133,8 @@ void DolphinMainWindow::moveToInactiveSplitView() void DolphinMainWindow::reloadView() { - clearStatusBar(); m_activeViewContainer->reload(); - m_activeViewContainer->statusBar()->updateSpaceInfo(); + Q_EMIT urlRefreshed(m_activeViewContainer->url()); } void DolphinMainWindow::stopLoading() @@ -1213,8 +1176,6 @@ void DolphinMainWindow::toggleFilterBar() void DolphinMainWindow::toggleEditLocation() { - clearStatusBar(); - QAction *action = actionCollection()->action(QStringLiteral("editable_location")); KUrlNavigator *urlNavigator = m_activeViewContainer->urlNavigator(); urlNavigator->setUrlEditable(action->isChecked()); @@ -1335,21 +1296,18 @@ void DolphinMainWindow::compareFiles() QUrl urlA = items.at(0).url(); QUrl urlB = items.at(1).url(); - QString command(QStringLiteral("kompare -c \"")); - command.append(urlA.toDisplayString(QUrl::PreferLocalFile)); - command.append("\" \""); - command.append(urlB.toDisplayString(QUrl::PreferLocalFile)); - command.append('\"'); + const QString program = QStringLiteral("kompare"); + const QStringList args({QStringLiteral("-c"), urlA.toDisplayString(QUrl::PreferLocalFile), urlB.toDisplayString(QUrl::PreferLocalFile)}); - KIO::CommandLauncherJob *job = new KIO::CommandLauncherJob(command, this); + KIO::CommandLauncherJob *job = new KIO::CommandLauncherJob(program, args, this); job->setDesktopName(QStringLiteral("org.kde.kompare")); job->start(); } void DolphinMainWindow::toggleShowMenuBar() { - const bool visible = menuBar()->isVisible(); - menuBar()->setVisible(!visible); + QAction *showMenuBarAction = actionCollection()->action(KStandardAction::name(KStandardAction::ShowMenubar)); + menuBar()->setVisible(showMenuBarAction->isChecked()); } QPointer<QAction> DolphinMainWindow::preferredSearchTool() @@ -2072,6 +2030,7 @@ void DolphinMainWindow::setupActions() connect(stashSplit, &QAction::triggered, this, &DolphinMainWindow::toggleSplitStash); QAction *redisplay = KStandardAction::redisplay(this, &DolphinMainWindow::reloadView, actionCollection()); + redisplay->setAutoRepeat(false); redisplay->setToolTip(i18nc("@info:tooltip", "Refresh view")); redisplay->setWhatsThis(xi18nc("@info:whatsthis refresh", "<para>This refreshes " @@ -2368,7 +2327,7 @@ void DolphinMainWindow::setupDockWidgets() connect(lockLayoutAction, &KDualAction::triggered, this, &DolphinMainWindow::togglePanelLockState); // Setup "Information" - DolphinDockWidget *infoDock = new DolphinDockWidget(i18nc("@title:window", "Information")); + DolphinDockWidget *infoDock = new DolphinDockWidget(i18nc("@title:window", "Information"), this); infoDock->setLocked(lock); infoDock->setObjectName(QStringLiteral("infoDock")); infoDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); @@ -2467,6 +2426,7 @@ void DolphinMainWindow::setupDockWidgets() addDockWidget(Qt::BottomDockWidgetArea, terminalDock); connect(this, &DolphinMainWindow::urlChanged, m_terminalPanel, &TerminalPanel::setUrl); + connect(this, &DolphinMainWindow::urlRefreshed, m_terminalPanel, &TerminalPanel::refreshUrl); if (GeneralSettings::version() < 200) { terminalDock->hide(); @@ -2794,11 +2754,6 @@ void DolphinMainWindow::refreshViews() Q_EMIT settingsChanged(); } -void DolphinMainWindow::clearStatusBar() -{ - m_activeViewContainer->statusBar()->resetToDefaultText(); -} - void DolphinMainWindow::connectViewSignals(DolphinViewContainer *container) { connect(container, &DolphinViewContainer::showFilterBarChanged, this, &DolphinMainWindow::updateFilterBarAction); diff --git a/src/dolphinmainwindow.h b/src/dolphinmainwindow.h index cd9f238aa..4b2b0c87b 100644 --- a/src/dolphinmainwindow.h +++ b/src/dolphinmainwindow.h @@ -246,6 +246,11 @@ Q_SIGNALS: void urlChanged(const QUrl &url); /** + * Is sent when the view is refreshed + */ + void urlRefreshed(const QUrl &url); + + /** * Is emitted if information of an item is requested to be shown e. g. in the panel. * If item is null, no item information request is pending. */ @@ -296,15 +301,13 @@ private Q_SLOTS: */ void refreshViews(); - void clearStatusBar(); - /** Updates the 'Create New...' sub menu. */ void updateNewMenu(); void createDirectory(); void createFile(); - /** Shows the error message in the status bar of the active view. */ + /** Shows the error message in a non-modal message box above the active view. */ void showErrorMessage(const QString &message); /** @@ -551,12 +554,6 @@ private Q_SLOTS: void showTarget(); /** - * Indicates in the statusbar that the execution of the command \a command - * has been finished. - */ - void showCommand(CommandType command); - - /** * If the URL can be listed, open it in the current view, otherwise * run it through KRun. */ @@ -765,8 +762,8 @@ private: private: /** * Implements a custom error handling for the undo manager. This - * assures that all errors are shown in the status bar of Dolphin - * instead as modal error dialog with an OK button. + * assures that all errors are shown as a message box above the view, + * and not as modal error dialogs with an OK button. */ class UndoUiInterface : public KIO::FileUndoManager::UiInterface { diff --git a/src/dolphinnavigatorswidgetaction.cpp b/src/dolphinnavigatorswidgetaction.cpp index c44b7e796..b16b4876b 100644 --- a/src/dolphinnavigatorswidgetaction.cpp +++ b/src/dolphinnavigatorswidgetaction.cpp @@ -15,6 +15,7 @@ #include <KService> #include <KIO/ApplicationLauncherJob> +#include <KIO/CommandLauncherJob> #include <QApplication> #include <QHBoxLayout> @@ -55,7 +56,7 @@ void DolphinNavigatorsWidgetAction::adjustSpacing() leadingSpacing = 0; } int trailingSpacing = (viewGeometries.globalXOfNavigatorsWidget + m_splitter->width()) - (viewGeometries.globalXOfPrimary + viewGeometries.widthOfPrimary); - if (trailingSpacing < 0 || emptyTrashButton(Primary)->isVisible() || networkFolderButton(Primary)->isVisible()) { + if (trailingSpacing < 0 || emptyTrashButton(Primary)->isVisible() || networkFolderButton(Primary)->isVisible() || kdeConnectButton(Primary)->isVisible()) { trailingSpacing = 0; } const int widthLeftForUrlNavigator = m_splitter->widget(0)->width() - leadingSpacing - trailingSpacing; @@ -81,7 +82,8 @@ void DolphinNavigatorsWidgetAction::adjustSpacing() spacing(Primary, Trailing)->setFixedWidth(0); trailingSpacing = (viewGeometries.globalXOfNavigatorsWidget + m_splitter->width()) - (viewGeometries.globalXOfSecondary + viewGeometries.widthOfSecondary); - if (trailingSpacing < 0 || emptyTrashButton(Secondary)->isVisible() || networkFolderButton(Secondary)->isVisible()) { + if (trailingSpacing < 0 || emptyTrashButton(Secondary)->isVisible() || networkFolderButton(Secondary)->isVisible() + || kdeConnectButton(Secondary)->isVisible()) { trailingSpacing = 0; } else { const int widthLeftForUrlNavigator2 = m_splitter->widget(1)->width() - trailingSpacing; @@ -213,6 +215,9 @@ QWidget *DolphinNavigatorsWidgetAction::createNavigatorWidget(Side side) const auto networkFolderButton = newNetworkFolderButton(urlNavigator, navigatorWidget); layout->addWidget(networkFolderButton); + auto kdeConnectButton = newKdeConnectButton(urlNavigator, navigatorWidget); + layout->addWidget(kdeConnectButton); + connect( urlNavigator, &KUrlNavigator::urlChanged, @@ -303,6 +308,35 @@ QPushButton *DolphinNavigatorsWidgetAction::newNetworkFolderButton(const Dolphin return networkFolderButton; } +QPushButton *DolphinNavigatorsWidgetAction::kdeConnectButton(DolphinNavigatorsWidgetAction::Side side) +{ + const int sideIndex = (side == Primary ? 0 : 1); + return m_splitter->widget(sideIndex)->findChild<QPushButton *>(QStringLiteral("KdeConnectButton")); +} + +QPushButton *DolphinNavigatorsWidgetAction::newKdeConnectButton(const DolphinUrlNavigator *urlNavigator, QWidget *parent) const +{ + auto kdeConnectButton = new QPushButton(QIcon::fromTheme(QStringLiteral("kdeconnect")), i18nc("@action:button", "Open KDE Connect"), parent); + kdeConnectButton->setObjectName(QStringLiteral("KdeConnectButton")); + kdeConnectButton->setFlat(true); + connect(kdeConnectButton, &QPushButton::clicked, urlNavigator, [kdeConnectButton, urlNavigator]() { + QStringList args; + if (const QString deviceId = urlNavigator->locationUrl().host(); !deviceId.isEmpty()) { + args << QStringLiteral("--device") << deviceId; + } + auto *job = new KIO::CommandLauncherJob(QStringLiteral("kdeconnect-app"), args); + job->setDesktopName(QStringLiteral("org.kde.kdeconnect.app")); + job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled)); + job->start(); + }); + kdeConnectButton->hide(); + connect(urlNavigator, &KUrlNavigator::urlChanged, kdeConnectButton, [kdeConnectButton, urlNavigator]() { + kdeConnectButton->setVisible(urlNavigator->locationUrl().scheme() == QLatin1String("kdeconnect") + && KService::serviceByDesktopName(QStringLiteral("org.kde.kdeconnect.app"))); + }); + return kdeConnectButton; +} + QWidget *DolphinNavigatorsWidgetAction::spacing(Side side, Position position) const { int sideIndex = (side == Primary ? 0 : 1); @@ -311,9 +345,9 @@ QWidget *DolphinNavigatorsWidgetAction::spacing(Side side, Position position) co return m_splitter->widget(sideIndex)->layout()->itemAt(0)->widget(); } if (side == Primary) { - return m_splitter->widget(sideIndex)->layout()->itemAt(4)->widget(); + return m_splitter->widget(sideIndex)->layout()->itemAt(5)->widget(); } - return m_splitter->widget(sideIndex)->layout()->itemAt(3)->widget(); + return m_splitter->widget(sideIndex)->layout()->itemAt(4)->widget(); } void DolphinNavigatorsWidgetAction::updateText() diff --git a/src/dolphinnavigatorswidgetaction.h b/src/dolphinnavigatorswidgetaction.h index c9ce4ece2..8395f0b91 100644 --- a/src/dolphinnavigatorswidgetaction.h +++ b/src/dolphinnavigatorswidgetaction.h @@ -145,6 +145,20 @@ private: */ QPushButton *newNetworkFolderButton(const DolphinUrlNavigator *urlNavigator, QWidget *parent) const; + /** + * Used to retrieve the kdeConnectButtons for the navigatorWidgets on + * both sides. + */ + QPushButton *kdeConnectButton(Side side); + + /** + * Creates a new add "Open KDE Connect" button. + * @param urlNavigator Only when this UrlNavigator shows a "kdeconnect" URL + * will the button be visible. + * @param parent The object that should be the button's parent. + */ + QPushButton *newKdeConnectButton(const DolphinUrlNavigator *urlNavigator, QWidget *parent) const; + enum Position { Leading, Trailing }; /** * Used to retrieve both the leading and trailing spacing for the navigatorWidgets diff --git a/src/dolphintabpage.cpp b/src/dolphintabpage.cpp index ca47130c0..7187162a4 100644 --- a/src/dolphintabpage.cpp +++ b/src/dolphintabpage.cpp @@ -38,8 +38,8 @@ DolphinTabPage::DolphinTabPage(const QUrl &primaryUrl, const QUrl &secondaryUrl, m_splitter = new DolphinTabPageSplitter(Qt::Horizontal, this); m_splitter->setChildrenCollapsible(false); connect(m_splitter, &QSplitter::splitterMoved, this, &DolphinTabPage::splitterMoved); - layout->addWidget(m_splitter, 1, 0); - layout->setRowStretch(1, 1); + layout->addWidget(m_splitter, 2, 0, 1, 2); + layout->setRowStretch(2, 1); // Create a new primary view m_primaryViewContainer = createViewContainer(primaryUrl); @@ -60,7 +60,13 @@ DolphinTabPage::DolphinTabPage(const QUrl &primaryUrl, const QUrl &secondaryUrl, m_secondaryViewContainer->show(); } + // DolphinView::setActive(true) calls setFocus() then emits activated(). + // activated() is connected to slotViewActivated() which toggles + // m_primaryViewActive — correct for user-initiated pane switches, but + // wrong here during construction. Disconnect to prevent the spurious toggle. + disconnectViewActivatedSignals(); m_primaryViewContainer->setActive(true); + connectViewActivatedSignals(); } bool DolphinTabPage::primaryViewActive() const @@ -259,6 +265,8 @@ void DolphinTabPage::insertNavigatorsWidget(DolphinNavigatorsWidgetAction *navig // We set a row minimum height, so the height does not visibly change whenever // navigatorsWidget is inserted which happens every time the current tab is changed. gridLayout->setRowMinimumHeight(0, navigatorsWidget->primaryUrlNavigator()->height()); + gridLayout->setRowMinimumHeight(1, 1); + gridLayout->addWidget(navigatorsWidget->requestWidget(this), 0, 0); if (!m_navigatorSeparator) { m_navigatorSeparator = std::make_unique<QFrame>(this); @@ -266,7 +274,7 @@ void DolphinTabPage::insertNavigatorsWidget(DolphinNavigatorsWidgetAction *navig m_navigatorSeparator->setFrameStyle(QFrame::HLine); m_navigatorSeparator->setFixedHeight(1); m_navigatorSeparator->setContentsMargins(0, 0, 0, 0); - gridLayout->addWidget(m_navigatorSeparator.get(), 1, 0, 0, -1); + gridLayout->addWidget(m_navigatorSeparator.get(), 1, 0); } } @@ -364,12 +372,22 @@ void DolphinTabPage::restoreState(const QByteArray &state) } stream >> m_primaryViewActive; + // DolphinView::setActive(true) calls setFocus() then emits activated(). + // activated() is connected to slotViewActivated() which toggles + // m_primaryViewActive — correct for user-initiated pane switches, but + // wrong here during session restore. Disconnect to prevent the spurious toggle. + disconnectViewActivatedSignals(); if (m_primaryViewActive) { + if (m_splitViewEnabled) { + m_secondaryViewContainer->setActive(false); + } m_primaryViewContainer->setActive(true); } else { Q_ASSERT(m_splitViewEnabled); + m_primaryViewContainer->setActive(false); m_secondaryViewContainer->setActive(true); } + connectViewActivatedSignals(); QByteArray splitterState; stream >> splitterState; @@ -393,8 +411,16 @@ void DolphinTabPage::setActive(bool active) // we should bypass changing active view in split mode m_active = !m_splitViewEnabled; } - // we want view to fire activated when goes from false to true + // DolphinView::setActive(true) calls setFocus() then emits activated(). + // activated() is connected to slotViewActivated() which toggles + // m_primaryViewActive — correct for user-initiated pane switches, but + // wrong here during tab switch. Disconnect to prevent the spurious toggle. + disconnectViewActivatedSignals(); + if (active && m_splitViewEnabled) { + inactiveViewContainer()->setActive(false); + } activeViewContainer()->setActive(active); + connectViewActivatedSignals(); } void DolphinTabPage::setCustomLabel(const QString &label) @@ -518,6 +544,22 @@ void DolphinTabPage::switchActiveView() } } +void DolphinTabPage::connectViewActivatedSignals() +{ + connect(m_primaryViewContainer->view(), &DolphinView::activated, this, &DolphinTabPage::slotViewActivated); + if (m_secondaryViewContainer) { + connect(m_secondaryViewContainer->view(), &DolphinView::activated, this, &DolphinTabPage::slotViewActivated); + } +} + +void DolphinTabPage::disconnectViewActivatedSignals() +{ + disconnect(m_primaryViewContainer->view(), &DolphinView::activated, this, &DolphinTabPage::slotViewActivated); + if (m_secondaryViewContainer) { + disconnect(m_secondaryViewContainer->view(), &DolphinView::activated, this, &DolphinTabPage::slotViewActivated); + } +} + DolphinViewContainer *DolphinTabPage::createViewContainer(const QUrl &url) const { DolphinViewContainer *container = new DolphinViewContainer(url, m_splitter); diff --git a/src/dolphintabpage.h b/src/dolphintabpage.h index b6135d360..421496ce9 100644 --- a/src/dolphintabpage.h +++ b/src/dolphintabpage.h @@ -204,6 +204,15 @@ private: */ void startExpandViewAnimation(DolphinViewContainer *expandingContainer); + /** + * Connect/disconnect DolphinView::activated → slotViewActivated for all + * current view containers. Used to suppress the signal during programmatic + * activation (tab switch, construction, session restore) so that + * slotViewActivated does not spuriously toggle m_primaryViewActive. + */ + void connectViewActivatedSignals(); + void disconnectViewActivatedSignals(); + private: DolphinTabPageSplitter *m_splitter; diff --git a/src/dolphinviewcontainer.cpp b/src/dolphinviewcontainer.cpp index 5c054eab8..968919b63 100644 --- a/src/dolphinviewcontainer.cpp +++ b/src/dolphinviewcontainer.cpp @@ -110,6 +110,8 @@ DolphinViewContainer::DolphinViewContainer(const QUrl &url, QWidget *parent) m_filterBar->setVisible(GeneralSettings::filterBar(), WithoutAnimation); connect(m_filterBar, &FilterBar::filterChanged, this, &DolphinViewContainer::setNameFilter); + connect(m_filterBar, &FilterBar::filterModeChanged, this, &DolphinViewContainer::setFilterMode); + connect(m_filterBar, &FilterBar::caseSensitiveChanged, this, &DolphinViewContainer::setFilterCaseSensitive); connect(m_filterBar, &FilterBar::closeRequest, this, &DolphinViewContainer::closeFilterBar); connect(m_filterBar, &FilterBar::focusViewRequest, this, &DolphinViewContainer::requestFocus); @@ -147,10 +149,10 @@ DolphinViewContainer::DolphinViewContainer(const QUrl &url, QWidget *parent) m_statusBar->setZoomLevel(m_view->zoomLevel()); connect(m_view, &DolphinView::urlChanged, m_statusBar, &DolphinStatusBar::setUrl); connect(m_view, &DolphinView::zoomLevelChanged, m_statusBar, &DolphinStatusBar::setZoomLevel); - connect(m_view, &DolphinView::infoMessage, m_statusBar, &DolphinStatusBar::setText); - connect(m_view, &DolphinView::operationCompletedMessage, m_statusBar, &DolphinStatusBar::setText); + connect(m_view, &DolphinView::infoMessage, m_statusBar, &DolphinStatusBar::setTemporaryRichText); + connect(m_view, &DolphinView::operationCompletedMessage, m_statusBar, &DolphinStatusBar::setTemporaryRichText); + connect(m_view, &DolphinView::showTypeAheadFeedback, m_statusBar, &DolphinStatusBar::setTemporaryRichText); connect(m_view, &DolphinView::statusBarTextChanged, m_statusBar, &DolphinStatusBar::setDefaultText); - connect(m_view, &DolphinView::statusBarTextChanged, m_statusBar, &DolphinStatusBar::resetToDefaultText); connect(m_view, &DolphinView::directoryLoadingProgress, m_statusBar, [this](int percent) { m_statusBar->showProgress(i18nc("@info:progress", "Loading folder…"), percent); }); @@ -208,6 +210,10 @@ DolphinViewContainer::DolphinViewContainer(const QUrl &url, QWidget *parent) connect(placesModel, &KFilePlacesModel::rowsRemoved, this, &DolphinViewContainer::slotPlacesModelChanged); QApplication::instance()->installEventFilter(this); + + // Update the view with the current state of the filter bar (from the state config) + m_view->setFilterMode(m_filterBar->filterMode()); + m_view->setFilterCaseSensitive(m_filterBar->isCaseSensitive()); } DolphinViewContainer::~DolphinViewContainer() = default; @@ -240,16 +246,6 @@ void DolphinViewContainer::setGrabFocusOnUrlChange(bool grabFocus) m_grabFocusOnUrlChange = grabFocus; } -const DolphinStatusBar *DolphinViewContainer::statusBar() const -{ - return m_statusBar; -} - -DolphinStatusBar *DolphinViewContainer::statusBar() -{ - return m_statusBar; -} - const DolphinUrlNavigator *DolphinViewContainer::urlNavigator() const { return m_urlNavigatorConnected; @@ -333,7 +329,7 @@ void DolphinViewContainer::setSearchBarVisible(bool visible) { if (!visible) { if (isSearchBarVisible()) { - m_searchBar->setVisible(false, WithAnimation); + m_searchBar->setVisible(false, WithAnimation, Search::Bar::HideBehavior::KeepCurrentUrl); } return; } @@ -567,6 +563,7 @@ void DolphinViewContainer::reload() { view()->reload(); m_messageWidget->hide(); + m_statusBar->updateSpaceInfo(); } QString DolphinViewContainer::captionWindowTitle() const @@ -699,7 +696,7 @@ void DolphinViewContainer::slotDirectoryLoadingCompleted() if (isSearchUrl(url()) && m_view->itemsCount() == 0) { // The dir lister has been completed on a Baloo-URI and no items have been found. Instead // of showing the default status bar information ("0 items") a more helpful information is given: - m_statusBar->setText(i18nc("@info:status", "No items found.")); + m_statusBar->setDefaultText(i18nc("@info:status", "No items found.")); } else { updateStatusBar(); } @@ -723,7 +720,6 @@ void DolphinViewContainer::slotDirectoryLoadingCompleted() void DolphinViewContainer::slotDirectoryLoadingCanceled() { m_statusBar->showProgress(QString(), 100); - m_statusBar->setText(QString()); } void DolphinViewContainer::slotUrlIsFileError(const QUrl &url) @@ -842,9 +838,9 @@ void DolphinViewContainer::slotItemsActivated(const KFileItemList &items) void DolphinViewContainer::showItemInfo(const KFileItem &item) { if (item.isNull()) { - m_statusBar->resetToDefaultText(); + m_statusBar->setHoveredItemText(QString()); } else { - m_statusBar->setText(item.getStatusBarInfo()); + m_statusBar->setHoveredItemText(item.getStatusBarInfo()); } } @@ -867,6 +863,18 @@ void DolphinViewContainer::setNameFilter(const QString &nameFilter) delayedStatusBarUpdate(); } +void DolphinViewContainer::setFilterMode(const KFileItemModelFilter::FilterMode mode) +{ + m_view->setFilterMode(mode); + delayedStatusBarUpdate(); +} + +void DolphinViewContainer::setFilterCaseSensitive(const bool caseSensitive) +{ + m_view->setFilterCaseSensitive(caseSensitive); + delayedStatusBarUpdate(); +} + void DolphinViewContainer::activate() { setActive(true); diff --git a/src/dolphinviewcontainer.h b/src/dolphinviewcontainer.h index 3829a4f78..9c6dbb611 100644 --- a/src/dolphinviewcontainer.h +++ b/src/dolphinviewcontainer.h @@ -88,9 +88,6 @@ public: */ void setGrabFocusOnUrlChange(bool grabFocus); - const DolphinStatusBar *statusBar() const; - DolphinStatusBar *statusBar(); - /** * @return An UrlNavigator that is controlling this view * or nullptr if there is none. @@ -372,6 +369,16 @@ private Q_SLOTS: void setNameFilter(const QString &nameFilter); /** + * Set the filtering mode of the filter. + */ + void setFilterMode(const KFileItemModelFilter::FilterMode mode); + + /** + * Enable or disable the case sensitive filtering. + */ + void setFilterCaseSensitive(const bool caseSensitive); + + /** * Marks the view container as active * (see DolphinViewContainer::setActive()). */ diff --git a/src/filterbar/filterbar.cpp b/src/filterbar/filterbar.cpp index 6cb8d5e2a..08bff24fc 100644 --- a/src/filterbar/filterbar.cpp +++ b/src/filterbar/filterbar.cpp @@ -2,18 +2,25 @@ * SPDX-FileCopyrightText: 2006-2010 Peter Penz <[email protected]> * SPDX-FileCopyrightText: 2006 Gregor Kališnik <[email protected]> * SPDX-FileCopyrightText: 2012 Stuart Citrin <[email protected]> + * SPDX-FileCopyrightText: 2026 Alessio Bonfiglio <[email protected]> * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "filterbar.h" +#include <KConfigGroup> #include <KLocalizedString> +#include <KSharedConfig> +#include <KColorScheme> +#include <QAction> #include <QApplication> +#include <QComboBox> #include <QHBoxLayout> #include <QKeyEvent> #include <QLineEdit> +#include <QPalette> #include <QToolButton> FilterBar::FilterBar(QWidget *parent) @@ -35,8 +42,35 @@ FilterBar::FilterBar(QWidget *parent) m_filterInput->setClearButtonEnabled(true); m_filterInput->setPlaceholderText(i18n("Filter…")); connect(m_filterInput, &QLineEdit::textChanged, this, &FilterBar::filterChanged); + connect(m_filterInput, &QLineEdit::textChanged, this, &FilterBar::updateInvalidPatternView); setFocusProxy(m_filterInput); + m_invalidPatternAction = new QAction(m_filterInput); + m_invalidPatternAction->setCheckable(false); + m_invalidPatternAction->setIcon(QIcon::fromTheme(QStringLiteral("error-symbolic"))); + m_invalidPatternAction->setToolTip(i18n("Invalid expression")); + m_filterInput->addAction(m_invalidPatternAction, QLineEdit::TrailingPosition); + m_invalidPatternAction->setVisible(false); + + // Create case sensitive button + m_caseSensitiveButton = new QToolButton(contentsContainer); + m_caseSensitiveButton->setAutoRaise(true); + m_caseSensitiveButton->setCheckable(true); + m_caseSensitiveButton->setIcon(QIcon::fromTheme(QStringLiteral("format-text-superscript"), QIcon::fromTheme(QStringLiteral("format-text-bold")))); + m_caseSensitiveButton->setToolTip(i18nc("@info:tooltip", "Match case")); + connect(m_caseSensitiveButton, &QToolButton::toggled, this, &FilterBar::caseSensitiveChanged); + connect(m_caseSensitiveButton, &QToolButton::toggled, this, &FilterBar::updateInvalidPatternView); + + // Create filter mode combobox + m_filterModeComboBox = new QComboBox(contentsContainer); + m_filterModeComboBox->addItem(i18nc("@item:inlistbox", "Plain Text"), KFileItemModelFilter::FilterMode::PlainText); + m_filterModeComboBox->addItem(i18nc("@item:inlistbox", "Glob Pattern"), KFileItemModelFilter::FilterMode::Glob); + m_filterModeComboBox->addItem(i18nc("@item:inlistbox", "Regular Expression"), KFileItemModelFilter::FilterMode::Regex); + connect(m_filterModeComboBox, &QComboBox::currentIndexChanged, this, [this](int index) { + Q_EMIT filterModeChanged(m_filterModeComboBox->itemData(index).value<KFileItemModelFilter::FilterMode>()); + }); + connect(m_filterModeComboBox, &QComboBox::currentIndexChanged, this, &FilterBar::updateInvalidPatternView); + // Create close button QToolButton *closeButton = new QToolButton(contentsContainer); closeButton->setAutoRaise(true); @@ -49,13 +83,25 @@ FilterBar::FilterBar(QWidget *parent) hLayout->setContentsMargins(0, 0, 0, 0); hLayout->addWidget(m_lockButton); hLayout->addWidget(m_filterInput); + hLayout->addWidget(m_caseSensitiveButton); + hLayout->addWidget(m_filterModeComboBox); hLayout->addWidget(closeButton); - setTabOrder(m_lockButton, closeButton); - setTabOrder(closeButton, m_filterInput); + setTabOrder({m_lockButton, m_caseSensitiveButton, m_filterModeComboBox, closeButton, m_filterInput}); + + KConfigGroup filterBarConfig(KSharedConfig::openStateConfig(), QStringLiteral("FilterBar")); + bool caseSensitiveEnabled = filterBarConfig.readEntry("caseSensitive", false); + int filterModeComboBoxIndex = filterBarConfig.readEntry("filterMode", m_filterModeComboBox->findData(KFileItemModelFilter::FilterMode::Glob)); + m_caseSensitiveButton->setChecked(caseSensitiveEnabled); + m_filterModeComboBox->setCurrentIndex(filterModeComboBoxIndex); } -FilterBar::~FilterBar() = default; +FilterBar::~FilterBar() +{ + KConfigGroup filterBarConfig(KSharedConfig::openStateConfig(), QStringLiteral("FilterBar")); + filterBarConfig.writeEntry("caseSensitive", this->m_caseSensitiveButton->isChecked()); + filterBarConfig.writeEntry("filterMode", this->m_filterModeComboBox->currentIndex()); +} void FilterBar::closeFilterBar() { @@ -71,6 +117,16 @@ void FilterBar::selectAll() m_filterInput->selectAll(); } +KFileItemModelFilter::FilterMode FilterBar::filterMode() const +{ + return m_filterModeComboBox->itemData(m_filterModeComboBox->currentIndex()).value<KFileItemModelFilter::FilterMode>(); +} + +bool FilterBar::isCaseSensitive() const +{ + return m_caseSensitiveButton->isChecked(); +} + void FilterBar::clear() { m_filterInput->clear(); @@ -93,6 +149,39 @@ void FilterBar::slotToggleLockButton(bool checked) } } +void FilterBar::updateInvalidPatternView() +{ + bool valid = true; + + KFileItemModelFilter::FilterMode current_filter_mode = filterMode(); + if (current_filter_mode != KFileItemModelFilter::FilterMode::PlainText) { + QRegularExpression regExp = QRegularExpression(); + QString pattern = m_filterInput->text(); + + QRegularExpression::PatternOptions options = + m_caseSensitiveButton->isChecked() ? QRegularExpression::NoPatternOption : QRegularExpression::CaseInsensitiveOption; + if (current_filter_mode == KFileItemModelFilter::FilterMode::Regex) { + regExp.setPattern(pattern); + regExp.setPatternOptions(options); + } else if (current_filter_mode == KFileItemModelFilter::FilterMode::Glob) { + regExp.setPattern(QRegularExpression::wildcardToRegularExpression(pattern, QRegularExpression::UnanchoredWildcardConversion)); + regExp.setPatternOptions(options); + } + + valid = regExp.isValid(); + } + + if (valid) { + m_filterInput->setPalette(QPalette()); + m_invalidPatternAction->setVisible(false); + } else { + auto pal = m_filterInput->palette(); + KColorScheme::adjustBackground(pal, KColorScheme::NegativeBackground); + m_filterInput->setPalette(pal); + m_invalidPatternAction->setVisible(true); + } +} + void FilterBar::showEvent(QShowEvent *event) { if (!event->spontaneous()) { @@ -137,7 +226,10 @@ void FilterBar::keyPressEvent(QKeyEvent *event) int FilterBar::preferredHeight() const { - return std::max(m_filterInput->sizeHint().height(), m_lockButton->sizeHint().height()); + return std::max({m_filterInput->sizeHint().height(), + m_lockButton->sizeHint().height(), + m_caseSensitiveButton->sizeHint().height(), + m_filterModeComboBox->sizeHint().height()}); } #include "moc_filterbar.cpp" diff --git a/src/filterbar/filterbar.h b/src/filterbar/filterbar.h index 1424f4cb8..924c6dba3 100644 --- a/src/filterbar/filterbar.h +++ b/src/filterbar/filterbar.h @@ -10,9 +10,12 @@ #define FILTERBAR_H #include "animatedheightwidget.h" +#include "kitemviews/private/kfileitemmodelfilter.h" class QLineEdit; class QToolButton; +class QComboBox; +class QAction; /** * @brief Provides an input field for filtering the currently shown items. @@ -35,6 +38,9 @@ public: */ void selectAll(); + KFileItemModelFilter::FilterMode filterMode() const; + bool isCaseSensitive() const; + public Q_SLOTS: /** Clears the input field. */ void clear(); @@ -51,6 +57,16 @@ Q_SIGNALS: void filterChanged(const QString &nameFilter); /** + * Emitted when the case sensitive mode has been changed + */ + void caseSensitiveChanged(bool caseSensitive); + + /** + * Emitted when the filter mode has been changed + */ + void filterModeChanged(KFileItemModelFilter::FilterMode mode); + + /** * Emitted as soon as the filterbar should get closed. */ void closeRequest(); @@ -70,6 +86,12 @@ protected: private: QLineEdit *m_filterInput; QToolButton *m_lockButton; + QToolButton *m_caseSensitiveButton; + QComboBox *m_filterModeComboBox; + QAction *m_invalidPatternAction; + + /** Enable or disable the alterative view for when a pattern is invalid */ + void updateInvalidPatternView(); }; #endif diff --git a/src/global.cpp b/src/global.cpp index c91046efb..b32549102 100644 --- a/src/global.cpp +++ b/src/global.cpp @@ -162,7 +162,11 @@ QVector<QPair<QSharedPointer<OrgKdeDolphinMainWindowInterface>, QStringList>> Do QSharedPointer<OrgKdeDolphinMainWindowInterface> interface( new OrgKdeDolphinMainWindowInterface(service, QStringLiteral("/dolphin/Dolphin_1"), QDBusConnection::sessionBus())); if (interface->isValid() && !interface->lastError().isValid()) { - dolphinInterfaces.append(qMakePair(interface, QStringList())); + auto isActiveWindowReply = interface->isActiveWindow(); + isActiveWindowReply.waitForFinished(); + if (!isActiveWindowReply.isError()) { + dolphinInterfaces.append(qMakePair(interface, QStringList())); + } } } } diff --git a/src/itemactions/hidefileitemaction.json b/src/itemactions/hidefileitemaction.json index 6e6174016..2397a93ca 100644 --- a/src/itemactions/hidefileitemaction.json +++ b/src/itemactions/hidefileitemaction.json @@ -10,6 +10,7 @@ "Name[ca@valencia]": "Oculta", "Name[ca]": "Oculta", "Name[cs]": "Skrýt", + "Name[da]": "Skjul", "Name[de]": "Ausblenden", "Name[es]": "Ocultar", "Name[eu]": "Ezkutatu", @@ -34,6 +35,7 @@ "Name[pt_BR]": "Ocultar", "Name[ro]": "Ascunde", "Name[ru]": "Скрыть", + "Name[sk]": "Skryť", "Name[sl]": "Skrij", "Name[sv]": "Dölj", "Name[tr]": "Gizle", diff --git a/src/itemactions/movetonewfolderitemaction.json b/src/itemactions/movetonewfolderitemaction.json index 6c27e343e..fa0432990 100644 --- a/src/itemactions/movetonewfolderitemaction.json +++ b/src/itemactions/movetonewfolderitemaction.json @@ -44,6 +44,7 @@ "Name[ro]": "Mută în dosar nou", "Name[ru]": "Переместить в новую папку", "Name[sa]": "New Folder इत्यत्र गच्छन्तु", + "Name[sk]": "Presunúť do nového priečinka", "Name[sl]": "Premakni v novo mapo", "Name[sv]": "Flytta till ny katalog", "Name[ta]": "புதிய அடைவிற்கு நகர்த்து", diff --git a/src/itemactions/setfoldericonitemaction.json b/src/itemactions/setfoldericonitemaction.json index 4d385aec8..5809a454c 100644 --- a/src/itemactions/setfoldericonitemaction.json +++ b/src/itemactions/setfoldericonitemaction.json @@ -10,6 +10,7 @@ "Name[ca@valencia]": "Establix la icona de carpeta", "Name[ca]": "Estableix la icona de carpeta", "Name[cs]": "Nastavit ikonu složky", + "Name[da]": "Indstil ikonfarve", "Name[de]": "Ordnersymbol festlegen", "Name[es]": "Definir icono de carpeta", "Name[eu]": "Ezarri karpetaren ikonoa", @@ -35,6 +36,7 @@ "Name[pt_BR]": "Definir ícone da pasta", "Name[ro]": "Stabilește pictograma dosarului", "Name[ru]": "Задать значок папки", + "Name[sk]": "Nastaviť ikonu priečinka", "Name[sl]": "Nastavi ikono mape", "Name[sv]": "Ange katalogikon", "Name[tr]": "Klasör Simgesi Ayarla", diff --git a/src/kitemviews/kfileitemmodel.cpp b/src/kitemviews/kfileitemmodel.cpp index c8bac0b9d..72c3d1410 100644 --- a/src/kitemviews/kfileitemmodel.cpp +++ b/src/kitemviews/kfileitemmodel.cpp @@ -31,12 +31,237 @@ #include <QRecursiveMutex> #include <QTimer> #include <QWidget> +#include <QtCore/qcompare.h> +#include <algorithm> #include <klazylocalizedstring.h> Q_GLOBAL_STATIC(QRecursiveMutex, s_collatorMutex) // #define KFILEITEMMODEL_DEBUG +namespace +{ +bool isAsciiDigit(QChar c) +{ + return c >= QLatin1Char('0') && c <= QLatin1Char('9'); +} + +Qt::strong_ordering orderingFromInt(int result) +{ + if (result < 0) { + return Qt::strong_ordering::less; + } + + if (result > 0) { + return Qt::strong_ordering::greater; + } + + return Qt::strong_ordering::equivalent; +} + +int orderingToInt(Qt::strong_ordering ordering) +{ + if (ordering < 0) { + return -1; + } + + if (ordering > 0) { + return 1; + } + + return 0; +} + +Qt::strong_ordering compareDigitStrings(const QString &a, const QString &b) +{ + int firstSignificantA = 0; + while (firstSignificantA < a.length() && a.at(firstSignificantA) == QLatin1Char('0')) { + ++firstSignificantA; + } + + int firstSignificantB = 0; + while (firstSignificantB < b.length() && b.at(firstSignificantB) == QLatin1Char('0')) { + ++firstSignificantB; + } + + const int significantLengthA = a.length() - firstSignificantA; + const int significantLengthB = b.length() - firstSignificantB; + if (significantLengthA != significantLengthB) { + return significantLengthA < significantLengthB ? Qt::strong_ordering::less : Qt::strong_ordering::greater; + } + + for (int i = 0; i < significantLengthA; ++i) { + const QChar digitA = a.at(firstSignificantA + i); + const QChar digitB = b.at(firstSignificantB + i); + if (digitA != digitB) { + return digitA < digitB ? Qt::strong_ordering::less : Qt::strong_ordering::greater; + } + } + + return Qt::strong_ordering::equivalent; +} + +Qt::strong_ordering compareFractionalDigitStrings(const QString &a, const QString &b) +{ + const int length = std::max(a.length(), b.length()); + for (int i = 0; i < length; ++i) { + const QChar digitA = i < a.length() ? a.at(i) : QLatin1Char('0'); + const QChar digitB = i < b.length() ? b.at(i) : QLatin1Char('0'); + if (digitA != digitB) { + return digitA < digitB ? Qt::strong_ordering::less : Qt::strong_ordering::greater; + } + } + + return Qt::strong_ordering::equivalent; +} + +int findDigitRunEnd(const QString &text, int start) +{ + int end = start; + while (end < text.length() && isAsciiDigit(text.at(end))) { + ++end; + } + + return end; +} + +int countNumericChainSegments(const QString &text, int start, int *chainEnd) +{ + int end = findDigitRunEnd(text, start); + int segmentCount = 1; + + while (end + 1 < text.length() && text.at(end) == QLatin1Char('.') && isAsciiDigit(text.at(end + 1))) { + end = findDigitRunEnd(text, end + 1); + ++segmentCount; + } + + if (chainEnd) { + *chainEnd = end; + } + + return segmentCount; +} + +Qt::strong_ordering compareNumericChains(const QString &a, int startA, int endA, int segmentCountA, const QString &b, int startB, int endB, int segmentCountB) +{ + if (segmentCountA == 2 && segmentCountB == 2) { + const int dotA = findDigitRunEnd(a, startA); + const int dotB = findDigitRunEnd(b, startB); + + const Qt::strong_ordering integerResult = compareDigitStrings(a.mid(startA, dotA - startA), b.mid(startB, dotB - startB)); + if (integerResult != 0) { + return integerResult; + } + + return compareFractionalDigitStrings(a.mid(dotA + 1, endA - dotA - 1), b.mid(dotB + 1, endB - dotB - 1)); + } + + int segmentStartA = startA; + int segmentStartB = startB; + + while (true) { + const int segmentEndA = findDigitRunEnd(a, segmentStartA); + const int segmentEndB = findDigitRunEnd(b, segmentStartB); + + const Qt::strong_ordering segmentResult = + compareDigitStrings(a.mid(segmentStartA, segmentEndA - segmentStartA), b.mid(segmentStartB, segmentEndB - segmentStartB)); + if (segmentResult != 0) { + return segmentResult; + } + + const bool hasNextSegmentA = segmentEndA < endA; + const bool hasNextSegmentB = segmentEndB < endB; + if (!hasNextSegmentA || !hasNextSegmentB) { + if (hasNextSegmentA != hasNextSegmentB) { + return hasNextSegmentA ? Qt::strong_ordering::greater : Qt::strong_ordering::less; + } + + return Qt::strong_ordering::equivalent; + } + + segmentStartA = segmentEndA + 1; + segmentStartB = segmentEndB + 1; + } +} + +int findExtensionSeparator(const QString &text) +{ + for (int i = text.length() - 1; i > 0; --i) { + if (text.at(i) != QLatin1Char('.')) { + continue; + } + + if (isAsciiDigit(text.at(i - 1)) && i + 1 < text.length() && isAsciiDigit(text.at(i + 1))) { + continue; + } + + return i; + } + + return -1; +} + +Qt::strong_ordering decimalAwareNaturalCompare(const QString &a, const QString &b, const QCollator &collator) +{ + bool comparedNumericTokens = false; + int indexA = 0; + int indexB = 0; + + while (indexA < a.length() && indexB < b.length()) { + if (isAsciiDigit(a.at(indexA)) && isAsciiDigit(b.at(indexB))) { + comparedNumericTokens = true; + int chainEndA = indexA; + const int segmentCountA = countNumericChainSegments(a, indexA, &chainEndA); + int chainEndB = indexB; + const int segmentCountB = countNumericChainSegments(b, indexB, &chainEndB); + + const Qt::strong_ordering numericResult = compareNumericChains(a, indexA, chainEndA, segmentCountA, b, indexB, chainEndB, segmentCountB); + indexA = chainEndA; + indexB = chainEndB; + if (numericResult != 0) { + return numericResult; + } + + continue; + } + + int textEndA = indexA; + while (textEndA < a.length() && !isAsciiDigit(a.at(textEndA))) { + ++textEndA; + } + + int textEndB = indexB; + while (textEndB < b.length() && !isAsciiDigit(b.at(textEndB))) { + ++textEndB; + } + + const Qt::strong_ordering textResult = orderingFromInt(collator.compare(a.mid(indexA, textEndA - indexA), b.mid(indexB, textEndB - indexB))); + if (textResult != 0) { + return orderingFromInt(collator.compare(a.mid(indexA), b.mid(indexB))); + } + + indexA = textEndA; + indexB = textEndB; + } + + const Qt::strong_ordering remainderResult = orderingFromInt(collator.compare(a.mid(indexA), b.mid(indexB))); + if (remainderResult != 0) { + return remainderResult; + } + + if (!comparedNumericTokens) { + return Qt::strong_ordering::equivalent; + } + + const Qt::strong_ordering result = orderingFromInt(QString::compare(a, b, collator.caseSensitivity())); + if (result != 0 || collator.caseSensitivity() == Qt::CaseSensitive) { + return result; + } + + return orderingFromInt(QString::compare(a, b, Qt::CaseSensitive)); +} +} + KFileItemModel::KFileItemModel(QObject *parent) : KItemModelBase("text", parent) , m_dirLister(nullptr) @@ -817,6 +1042,34 @@ QStringList KFileItemModel::mimeTypeFilters() const return m_filter.mimeTypes(); } +void KFileItemModel::setFilterMode(KFileItemModelFilter::FilterMode mode) +{ + if (m_filter.filterMode() != mode) { + dispatchPendingItemsToInsert(); + m_filter.setFilterMode(mode); + applyFilters(); + } +} + +KFileItemModelFilter::FilterMode KFileItemModel::filterMode() const +{ + return m_filter.filterMode(); +} + +void KFileItemModel::setFilterCaseSensitive(bool caseSensitive) +{ + if (m_filter.isCaseSensitive() != caseSensitive) { + dispatchPendingItemsToInsert(); + m_filter.setCaseSensitive(caseSensitive); + applyFilters(); + } +} + +bool KFileItemModel::isFilterCaseSensitive() const +{ + return m_filter.isCaseSensitive(); +} + void KFileItemModel::setExcludeMimeTypeFilter(const QStringList &filters) { if (m_filter.excludeMimeTypes() != filters) { @@ -1921,7 +2174,14 @@ QHash<QByteArray, QVariant> KFileItemModel::retrieveData(const KFileItem &item, } if (m_requestRole[NameRole]) { - data.insert(sharedValue("text"), item.text()); + QString displayName = item.text(); + if (ContentDisplaySettings::hideFileExtensions() && !isDir) { + const int dotIndex = displayName.lastIndexOf(QLatin1Char('.')); + if (dotIndex > 0) { + displayName = displayName.left(dotIndex); + } + } + data.insert(sharedValue("text"), displayName); } if (m_requestRole[ExtensionRole] && !isDir) { @@ -2309,24 +2569,18 @@ int KFileItemModel::stringCompare(const QString &a, const QString &b, const QCol QMutexLocker collatorLock(s_collatorMutex()); if (m_naturalSorting) { - // Split extension, taking into account it can be empty - constexpr QString::SectionFlags flags = QString::SectionSkipEmpty | QString::SectionIncludeLeadingSep; + const int aExtensionSeparator = findExtensionSeparator(a); + const int bExtensionSeparator = findExtensionSeparator(b); + const int aBaseNameLength = aExtensionSeparator < 0 ? a.length() : aExtensionSeparator; + const int bBaseNameLength = bExtensionSeparator < 0 ? b.length() : bExtensionSeparator; - // 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())) { + const int res = orderingToInt(decimalAwareNaturalCompare(a.left(aBaseNameLength), b.left(bBaseNameLength), collator)); + if (res != 0 || (aExtensionSeparator < 0 && bExtensionSeparator < 0)) { 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())); + return orderingToInt(decimalAwareNaturalCompare(a.mid(aBaseNameLength), b.mid(bBaseNameLength), collator)); } const int result = QString::compare(a, b, collator.caseSensitivity()); diff --git a/src/kitemviews/kfileitemmodel.h b/src/kitemviews/kfileitemmodel.h index 3749b0c1b..2a6e710c3 100644 --- a/src/kitemviews/kfileitemmodel.h +++ b/src/kitemviews/kfileitemmodel.h @@ -184,6 +184,12 @@ public: void setMimeTypeFilters(const QStringList &filters); QStringList mimeTypeFilters() const; + void setFilterMode(KFileItemModelFilter::FilterMode mode); + KFileItemModelFilter::FilterMode filterMode() const; + + void setFilterCaseSensitive(bool caseSensitive); + bool isFilterCaseSensitive() const; + void setExcludeMimeTypeFilter(const QStringList &filters); QStringList excludeMimeTypeFilter() const; diff --git a/src/kitemviews/kfileitemmodelrolesupdater.cpp b/src/kitemviews/kfileitemmodelrolesupdater.cpp index 31459d8d1..0f4816424 100644 --- a/src/kitemviews/kfileitemmodelrolesupdater.cpp +++ b/src/kitemviews/kfileitemmodelrolesupdater.cpp @@ -973,8 +973,6 @@ void KFileItemModelRolesUpdater::startPreviewJob() const KFileItemList items = m_pendingPreviewItems; m_pendingPreviewItems.clear(); - const KFileItem &referenceItem = items.first(); - KIO::PreviewJob *job = new KIO::PreviewJob(items, cacheSize(), &m_enabledPlugins); job->setDevicePixelRatio(m_devicePixelRatio); if (job->uiDelegate()) { diff --git a/src/kitemviews/kitemlistcontroller.cpp b/src/kitemviews/kitemlistcontroller.cpp index 54e95e1d1..e87ed3c18 100644 --- a/src/kitemviews/kitemlistcontroller.cpp +++ b/src/kitemviews/kitemlistcontroller.cpp @@ -547,6 +547,7 @@ void KItemListController::slotChangeCurrentItem(const QString &text, bool search m_view->scrollToItem(index, KItemListView::ViewItemPosition::Beginning); *found = true; } + Q_EMIT typeAheadUsed(text, index >= 0 ? std::make_optional<int>(index) : std::nullopt); } void KItemListController::slotAutoActivationTimeout() @@ -1493,8 +1494,7 @@ KItemListWidget *KItemListController::widgetForDropPos(const QPointF &pos) const const auto widgets = m_view->visibleItemListWidgets(); for (KItemListWidget *widget : widgets) { const QPointF mappedPos = widget->mapFromItem(m_view, pos); - const QRectF highlightRect = m_view->highlightEntireRow() ? widget->selectionRectFull() : widget->selectionRectCore(); - if (highlightRect.contains(mappedPos)) { + if (widget->selectionRectCore().contains(mappedPos)) { return widget; } } @@ -1763,6 +1763,7 @@ bool KItemListController::onPress(const QPointF &pos, const Qt::KeyboardModifier case SingleSelection: m_selectionManager->setSelected(m_pressedIndex.value()); + Q_FALLTHROUGH(); case MultiSelection: if (controlPressed && !shiftPressed && leftClick) { diff --git a/src/kitemviews/kitemlistcontroller.h b/src/kitemviews/kitemlistcontroller.h index 48a518610..6379acbd8 100644 --- a/src/kitemviews/kitemlistcontroller.h +++ b/src/kitemviews/kitemlistcontroller.h @@ -190,6 +190,13 @@ Q_SIGNALS: void aboveItemDropEvent(int index, QGraphicsSceneDragDropEvent *event); /** + * Emits the keys the user typed for searching so they can be displayed back to the user. + * @param typedString A string of basic interpretation of key presses e.g. "qwert". + * @param foundIndex The index of the item that was marked as current in response to this search. + */ + void typeAheadUsed(const QString &typedString, std::optional<int> foundIndex); + + /** * Is emitted if the Escape key is pressed. */ void escapePressed(); diff --git a/src/kitemviews/kitemlistview.cpp b/src/kitemviews/kitemlistview.cpp index b780e3ff4..452567f05 100644 --- a/src/kitemviews/kitemlistview.cpp +++ b/src/kitemviews/kitemlistview.cpp @@ -117,6 +117,7 @@ KItemListView::KItemListView(QGraphicsWidget *parent) m_animation = new KItemListViewAnimation(this); connect(m_animation, &KItemListViewAnimation::finished, this, &KItemListView::slotAnimationFinished); + connect(m_animation, &KItemListViewAnimation::started, this, &KItemListView::slotAnimationStarted); m_rubberBand = new KItemListRubberBand(this); connect(m_rubberBand, &KItemListRubberBand::activationChanged, this, &KItemListView::slotRubberBandActivationChanged); @@ -132,7 +133,7 @@ KItemListView::KItemListView(QGraphicsWidget *parent) } update(); }); - connect(m_tapAndHoldIndicator, &KItemListRubberBand::endPositionChanged, this, [this]() { + connect(m_tapAndHoldIndicator, &KItemListRubberBand::endPositionChanged, this, [this](const QPointF &, const QPointF &) { if (m_tapAndHoldIndicator->isActive()) { update(); } @@ -758,8 +759,7 @@ void KItemListView::editRole(int index, const QByteArray &role) if (!widget) { return; } - if (m_editingRole || m_animation->isStarted(widget)) { - Q_EMIT widget->roleEditingCanceled(index, role, QVariant()); + if (widget->editedRole() == role) { return; } @@ -771,7 +771,7 @@ void KItemListView::editRole(int index, const QByteArray &role) connect(widget, &KItemListWidget::roleEditingCanceled, this, &KItemListView::slotRoleEditingCanceled); connect(widget, &KItemListWidget::roleEditingFinished, this, &KItemListView::slotRoleEditingFinished); - connect(this, &KItemListView::scrollOffsetChanged, widget, &KStandardItemListWidget::finishRoleEditing); + connect(this, &KItemListView::scrollOffsetChanged, widget, &KStandardItemListWidget::updateRoleEditorGeometry); } void KItemListView::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) @@ -1092,6 +1092,20 @@ bool KItemListView::event(QEvent *event) return true; break; + case QEvent::GraphicsSceneMousePress: + case QEvent::GraphicsSceneMouseDoubleClick: + case QEvent::GraphicsSceneContextMenu: + if (m_editingRole) { + for (KItemListWidget *widget : std::as_const(m_visibleItems)) { + auto *standardWidget = qobject_cast<KStandardItemListWidget *>(widget); + if (standardWidget && !standardWidget->isVisible() && !standardWidget->editedRole().isEmpty()) { + standardWidget->finishRoleEditing(); + break; + } + } + } + [[fallthrough]]; + default: // Forward all other events to the controller and handle them there if (!m_editingRole && m_controller && m_controller->processEvent(event, transform())) { @@ -1577,6 +1591,13 @@ void KItemListView::slotSelectionChanged(const KItemSet ¤t, const KItemSet #endif } +void KItemListView::slotAnimationStarted(QGraphicsWidget *widget, KItemListViewAnimation::AnimationType /* type */, const QVariant & /* endValue */) +{ + KStandardItemListWidget *listWidget = qobject_cast<KStandardItemListWidget *>(widget); + Q_ASSERT(widget); + listWidget->cancelRoleEditing(); +} + void KItemListView::slotAnimationFinished(QGraphicsWidget *widget, KItemListViewAnimation::AnimationType type) { KItemListWidget *itemListWidget = qobject_cast<KItemListWidget *>(widget); @@ -1926,9 +1947,6 @@ void KItemListView::doLayout(LayoutAnimationHint hint, int changedIndex, int cha 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; } @@ -2022,6 +2040,10 @@ QList<int> KItemListView::recycleInvisibleItems(int firstVisibleIndex, int lastV const bool invisible = (index < firstVisibleIndex) || (index > lastVisibleIndex); if (invisible) { + if (!widget->editedRole().isEmpty()) { + widget->setVisible(false); + continue; + } if (m_animation->isStarted(widget)) { if (hint == NoAnimation) { // Stopping the animation will call KItemListView::slotAnimationFinished() @@ -2804,7 +2826,7 @@ bool KItemListView::hasSiblingSuccessor(int index) const void KItemListView::disconnectRoleEditingSignals(int index) { - KStandardItemListWidget *widget = qobject_cast<KStandardItemListWidget *>(m_visibleItems.value(index)); + KItemListWidget *widget = m_visibleItems.value(index); if (!widget) { return; } diff --git a/src/kitemviews/kitemlistview.h b/src/kitemviews/kitemlistview.h index 415710e02..c8ab796a9 100644 --- a/src/kitemviews/kitemlistview.h +++ b/src/kitemviews/kitemlistview.h @@ -446,6 +446,7 @@ protected Q_SLOTS: private Q_SLOTS: void slotAnimationFinished(QGraphicsWidget *widget, KItemListViewAnimation::AnimationType type); + void slotAnimationStarted(QGraphicsWidget *widget, KItemListViewAnimation::AnimationType type, const QVariant &endValue); void slotRubberBandPosChanged(); void slotRubberBandActivationChanged(bool active); diff --git a/src/kitemviews/kitemlistwidget.cpp b/src/kitemviews/kitemlistwidget.cpp index 089211716..2d94c4303 100644 --- a/src/kitemviews/kitemlistwidget.cpp +++ b/src/kitemviews/kitemlistwidget.cpp @@ -121,9 +121,13 @@ void KItemListWidget::paint(QPainter *painter, const QStyleOptionGraphicsItem *o painter->fillRect(backgroundRect, backgroundColor); } + const QStyle::State selectedState(m_selected ? QStyle::State_Selected : QStyle::State(0)); if ((m_selected || m_current) && m_editedRole.isEmpty()) { const QStyle::State activeState(isActiveWindow() && widget->hasFocus() ? QStyle::State_Active : 0); - drawItemStyleOption(painter, widget, activeState | QStyle::State_Enabled | QStyle::State_Selected | QStyle::State_Item); + // Only pass State_Selected when the item is actually selected, not just when + // it has keyboard focus (m_current), to avoid styles drawing a persistent + // selection highlight on focused but unselected items. + drawItemStyleOption(painter, widget, activeState | selectedState | QStyle::State_Enabled | QStyle::State_Item); } if (m_hoverOpacity > 0.0) { @@ -135,7 +139,7 @@ void KItemListWidget::paint(QPainter *painter, const QStyleOptionGraphicsItem *o QPainter pixmapPainter(m_hoverCache); const QStyle::State activeState(isActiveWindow() && widget->hasFocus() ? QStyle::State_Active | QStyle::State_Enabled : 0); - drawItemStyleOption(&pixmapPainter, widget, activeState | QStyle::State_MouseOver | QStyle::State_Item); + drawItemStyleOption(&pixmapPainter, widget, activeState | selectedState | QStyle::State_MouseOver | QStyle::State_Item); } const qreal opacity = painter->opacity(); @@ -231,6 +235,7 @@ void KItemListWidget::setSelected(bool selected) m_selectionToggle->setChecked(selected); } selectedChanged(selected); + clearHoverCache(); update(); } } @@ -617,50 +622,69 @@ void KItemListWidget::setPressed(bool enabled) void KItemListWidget::drawItemStyleOption(QPainter *painter, QWidget *widget, QStyle::State styleState) { + painter->save(); QStyleOptionViewItem viewItemOption; - constexpr int roundness = 5; // From Breeze style. - constexpr qreal penWidth = 1.25; initStyleOption(&viewItemOption); viewItemOption.state = styleState; viewItemOption.viewItemPosition = QStyleOptionViewItem::OnlyOne; viewItemOption.showDecorationSelected = true; viewItemOption.rect = selectionRectFull().toRect(); - QPainterPath path; - const qreal adjustment = 0.5 * penWidth; // Use same adjustments as Breeze strokedRect uses, to snap to pixelGrid. - path.addRoundedRect(selectionRectFull().adjusted(adjustment, adjustment, -adjustment, -adjustment), roundness, roundness); - QColor backgroundColor{widget->palette().color(QPalette::Accent)}; - painter->setRenderHint(QPainter::Antialiasing); - bool current = m_current && styleState & QStyle::State_Active; - - // Background item, alpha values are from - // https://invent.kde.org/plasma/libplasma/-/blob/master/src/desktoptheme/breeze/widgets/viewitem.svg - backgroundColor.setAlphaF(0.0); + const bool current = m_current && styleState & QStyle::State_Active; - if (m_clickHighlighted) { - backgroundColor.setAlphaF(1.0); - } else { - if (m_selected && m_hovered) { - backgroundColor.setAlphaF(0.40); - } else if (m_selected) { - backgroundColor.setAlphaF(0.32); - } else if (m_hovered) { - backgroundColor = widget->palette().color(QPalette::Text); - backgroundColor.setAlphaF(0.06); + // TODO: Remove this check after Plasma 6.8 release + // See: https://invent.kde.org/plasma/breeze/-/merge_requests/595 + if (style()->name() == QStringLiteral("breeze")) { + QColor backgroundColor{widget->palette().color(QPalette::Active, QPalette::Highlight)}; + backgroundColor.setAlphaF(0.0); + if (m_clickHighlighted) { + backgroundColor.setAlphaF(1.0); + } else { + if (m_selected && m_hovered) { + backgroundColor.setAlphaF(0.85); + } else if (m_selected) { + backgroundColor.setAlphaF(0.70); + } else if (m_hovered) { + backgroundColor = widget->palette().color(QPalette::Text); + backgroundColor.setAlphaF(0.12); + } } - } + painter->setRenderHint(QPainter::Antialiasing); + constexpr int roundness = 5; // From Breeze style. + constexpr qreal penWidth = 1.25; + QPainterPath path; + const qreal adjustment = 0.5 * penWidth; // Use same adjustments as Breeze strokedRect uses, to snap to pixelGrid. + path.addRoundedRect(selectionRectFull().adjusted(adjustment, adjustment, -adjustment, -adjustment), roundness, roundness); + painter->fillPath(path, backgroundColor); - painter->fillPath(path, backgroundColor); + // Focus decoration + if (current) { + QColor focusColor{widget->palette().color(QPalette::Active, QPalette::Highlight)}; + // Set the pen color lighter or darker depending on background color + focusColor = m_styleOption.palette.color(QPalette::Base).lightnessF() > 0.5 ? focusColor.darker(110) : focusColor.lighter(110); + focusColor.setAlphaF(m_selected || m_hovered ? 1.0 : 0.8); + QPen pen{focusColor, penWidth}; + pen.setCosmetic(true); + painter->strokePath(path, pen); + } + } else { + style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &viewItemOption, painter, widget); - // Focus decoration - if (current) { - QColor focusColor{widget->palette().color(QPalette::Accent)}; - focusColor = m_styleOption.palette.color(QPalette::Base).lightnessF() > 0.5 ? focusColor.darker(110) : focusColor.lighter(110); - focusColor.setAlphaF(m_selected || m_hovered ? 1.0 : 0.8); - // Set the pen color lighter or darker depending on background color - QPen pen{focusColor, penWidth}; - pen.setCosmetic(true); - painter->strokePath(path, pen); + // Focus decoration + if (current) { + QStyleOptionFocusRect focusRectOption; + initStyleOption(&focusRectOption); + focusRectOption.state = QStyle::State_HasFocus; + if (m_selected && widget->hasFocus()) { + focusRectOption.state = QStyle::State_HasFocus | QStyle::State_Selected; + } + if (m_hovered) { + focusRectOption.state |= QStyle::State_MouseOver; + } + focusRectOption.rect = viewItemOption.rect; + style()->drawPrimitive(QStyle::PE_FrameFocusRect, &focusRectOption, painter, widget); + } } + painter->restore(); } #include "moc_kitemlistwidget.cpp" diff --git a/src/kitemviews/kstandarditemlistwidget.cpp b/src/kitemviews/kstandarditemlistwidget.cpp index 494473c62..ddbb8b6a1 100644 --- a/src/kitemviews/kstandarditemlistwidget.cpp +++ b/src/kitemviews/kstandarditemlistwidget.cpp @@ -854,7 +854,7 @@ void KStandardItemListWidget::editedRoleChanged(const QByteArray ¤t, const { Q_UNUSED(previous) - QGraphicsView *parent = scene()->views()[0]; + QGraphicsView *parent = !scene() || scene()->views().isEmpty() ? nullptr : scene()->views()[0]; if (current.isEmpty() || !parent || current != "text") { if (m_roleEditor) { Q_EMIT roleEditingCanceled(index(), current, data().value(current)); @@ -891,16 +891,7 @@ void KStandardItemListWidget::editedRoleChanged(const QByteArray ¤t, const connect(m_roleEditor, &KItemListRoleEditor::roleEditingCanceled, this, &KStandardItemListWidget::slotRoleEditingCanceled); connect(m_roleEditor, &KItemListRoleEditor::roleEditingFinished, this, &KStandardItemListWidget::slotRoleEditingFinished); - // Adjust the geometry of the editor - QRectF rect = roleEditingRect(current); - const int frameWidth = m_roleEditor->frameWidth(); - rect.adjust(-frameWidth, -frameWidth, frameWidth, frameWidth); - rect.translate(pos()); - if (rect.right() > parent->width()) { - rect.setWidth(parent->width() - rect.left()); - } - m_roleEditor->setGeometry(rect.toRect()); - m_roleEditor->autoAdjustSize(); + updateRoleEditorGeometry(); m_roleEditor->show(); m_roleEditor->setFocus(); setHovered(false); @@ -932,6 +923,13 @@ void KStandardItemListWidget::showEvent(QShowEvent *event) { KItemListWidget::showEvent(event); + if (m_roleEditor) { + m_roleEditor->setFinishedSignalBlocked(false); + updateRoleEditorGeometry(); + m_roleEditor->show(); + m_roleEditor->setFocus(); + } + // Listen to changes of the clipboard to mark the item as cut/uncut KFileItemClipboard *clipboard = KFileItemClipboard::instance(); @@ -943,6 +941,11 @@ void KStandardItemListWidget::showEvent(QShowEvent *event) void KStandardItemListWidget::hideEvent(QHideEvent *event) { + if (m_roleEditor) { + m_roleEditor->setFinishedSignalBlocked(true); + m_roleEditor->hide(); + } + disconnect(KFileItemClipboard::instance(), &KFileItemClipboard::cutItemsChanged, this, &KStandardItemListWidget::slotCutItemsChanged); KItemListWidget::hideEvent(event); @@ -964,6 +967,31 @@ void KStandardItemListWidget::finishRoleEditing() } } +void KStandardItemListWidget::cancelRoleEditing() +{ + if (!editedRole().isEmpty() && m_roleEditor) { + slotRoleEditingCanceled(editedRole(), KIO::encodeFileName(m_roleEditor->toPlainText())); + } +} + +void KStandardItemListWidget::updateRoleEditorGeometry() +{ + if (!m_roleEditor || editedRole().isEmpty() || !scene() || scene()->views().isEmpty()) { + return; + } + + auto *parent = scene()->views()[0]; + QRectF rect = roleEditingRect(editedRole()); + const int frameWidth = m_roleEditor->frameWidth(); + rect.adjust(-frameWidth, -frameWidth, frameWidth, frameWidth); + rect.translate(pos()); + if (rect.right() > parent->width()) { + rect.setWidth(parent->width() - rect.left()); + } + m_roleEditor->setGeometry(rect.toRect()); + m_roleEditor->autoAdjustSize(); +} + void KStandardItemListWidget::slotCutItemsChanged() { const QUrl itemUrl = data().value("url").toUrl(); diff --git a/src/kitemviews/kstandarditemlistwidget.h b/src/kitemviews/kstandarditemlistwidget.h index 9e6fff935..e0a32b745 100644 --- a/src/kitemviews/kstandarditemlistwidget.h +++ b/src/kitemviews/kstandarditemlistwidget.h @@ -196,6 +196,8 @@ protected: public Q_SLOTS: void finishRoleEditing(); + void cancelRoleEditing(); + void updateRoleEditorGeometry(); private Q_SLOTS: void slotCutItemsChanged(); diff --git a/src/kitemviews/private/kfileitemmodelfilter.cpp b/src/kitemviews/private/kfileitemmodelfilter.cpp index 45c62e7ca..48d2f6276 100644 --- a/src/kitemviews/private/kfileitemmodelfilter.cpp +++ b/src/kitemviews/private/kfileitemmodelfilter.cpp @@ -13,7 +13,8 @@ #include <KFileItem> KFileItemModelFilter::KFileItemModelFilter() - : m_useRegExp(false) + : m_filterMode(Glob) + , m_caseSensitive(false) , m_regExp(nullptr) , m_lowerCasePattern() , m_pattern() @@ -31,16 +32,29 @@ void KFileItemModelFilter::setPattern(const QString &filter) m_pattern = filter; m_lowerCasePattern = filter.toLower(); - if (filter.contains(QLatin1Char('*')) || filter.contains(QLatin1Char('?')) || filter.contains(QLatin1Char('['))) { - if (!m_regExp) { - m_regExp = new QRegularExpression(); - m_regExp->setPatternOptions(QRegularExpression::CaseInsensitiveOption); - } - m_regExp->setPattern(QRegularExpression::wildcardToRegularExpression(filter)); - m_useRegExp = m_regExp->isValid(); - } else { - m_useRegExp = false; - } + updateFilter(); +} + +void KFileItemModelFilter::setFilterMode(FilterMode mode) +{ + m_filterMode = mode; + updateFilter(); +} + +KFileItemModelFilter::FilterMode KFileItemModelFilter::filterMode() const +{ + return m_filterMode; +} + +void KFileItemModelFilter::setCaseSensitive(bool caseSensitive) +{ + m_caseSensitive = caseSensitive; + updateFilter(); +} + +bool KFileItemModelFilter::isCaseSensitive() const +{ + return m_caseSensitive; } QString KFileItemModelFilter::pattern() const @@ -48,6 +62,26 @@ QString KFileItemModelFilter::pattern() const return m_pattern; } +void KFileItemModelFilter::updateFilter() +{ + if (m_filterMode == PlainText) { + return; + } + + if (!m_regExp) { + m_regExp = new QRegularExpression(); + } + + QRegularExpression::PatternOptions options = m_caseSensitive ? QRegularExpression::NoPatternOption : QRegularExpression::CaseInsensitiveOption; + if (m_filterMode == Regex) { + m_regExp->setPattern(m_pattern); + m_regExp->setPatternOptions(options); + } else if (m_filterMode == Glob) { + m_regExp->setPattern(QRegularExpression::wildcardToRegularExpression(m_pattern, QRegularExpression::UnanchoredWildcardConversion)); + m_regExp->setPatternOptions(options); + } +} + void KFileItemModelFilter::setMimeTypes(const QStringList &types) { m_mimeTypes = types; @@ -98,8 +132,10 @@ bool KFileItemModelFilter::matches(const KFileItem &item) const bool KFileItemModelFilter::matchesPattern(const KFileItem &item) const { - if (m_useRegExp) { - return m_regExp->match(item.text()).hasMatch(); + if (m_filterMode == Glob || m_filterMode == Regex) { + return m_regExp->isValid() && m_regExp->match(item.text()).hasMatch(); + } else if (m_caseSensitive) { + return item.text().contains(m_pattern); } else { return item.text().toLower().contains(m_lowerCasePattern); } diff --git a/src/kitemviews/private/kfileitemmodelfilter.h b/src/kitemviews/private/kfileitemmodelfilter.h index ce6cbeebb..9d93d42cc 100644 --- a/src/kitemviews/private/kfileitemmodelfilter.h +++ b/src/kitemviews/private/kfileitemmodelfilter.h @@ -28,16 +28,36 @@ public: KFileItemModelFilter(); virtual ~KFileItemModelFilter(); + /** Filtering modes of KFileItemModelFilter */ + enum FilterMode { + /** Substring matching. */ + PlainText = 0, + /** Matching with glob, default. */ + Glob, + /** Matching with regex. */ + Regex + }; + /** * Sets the pattern that is used for a comparison with the item - * in KFileItemModelFilter::matches(). Per default the pattern - * defines a sub-string. As soon as the pattern contains at least - * a '*', '?' or '[' the pattern represents a regular expression. + * in KFileItemModelFilter::matches(). */ void setPattern(const QString &pattern); QString pattern() const; /** + * Sets the filtering mode used in KFileItemModelFilter::matches(). + */ + void setFilterMode(FilterMode mode); + FilterMode filterMode() const; + + /** + * Enable or disable the case sensitive filtering. + */ + void setCaseSensitive(bool caseSensitive); + bool isCaseSensitive() const; + + /** * Set the list of mimetypes that are used for comparison with the * item in KFileItemModelFilter::matchesMimeType. */ @@ -73,8 +93,14 @@ private: */ bool matchesType(const KFileItem &item) const; - bool m_useRegExp; // If true, m_regExp is used for filtering, - // otherwise m_lowerCaseFilter is used. + /** + * Instantiate and configure m_regExp according to m_filterMode and m_caseSensitive. + */ + void updateFilter(); + + FilterMode m_filterMode; // The current filtering mode. + bool m_caseSensitive; // If true the matching will be case sensitive. + QRegularExpression *m_regExp; QString m_lowerCasePattern; // Lowercase version of m_filter for // faster comparison in matches(). diff --git a/src/kitemviews/private/kitemlistroleeditor.cpp b/src/kitemviews/private/kitemlistroleeditor.cpp index 6105d604f..cec9bb98b 100644 --- a/src/kitemviews/private/kitemlistroleeditor.cpp +++ b/src/kitemviews/private/kitemlistroleeditor.cpp @@ -64,6 +64,11 @@ bool KItemListRoleEditor::event(QEvent *event) return KTextEdit::event(event); } +void KItemListRoleEditor::setFinishedSignalBlocked(bool blocked) +{ + m_blockFinishedSignal = blocked; +} + void KItemListRoleEditor::keyPressEvent(QKeyEvent *event) { switch (event->key()) { @@ -141,17 +146,6 @@ void KItemListRoleEditor::autoAdjustSize() const auto originalSize = size(); auto newSize = originalSize; - document()->adjustSize(); - const qreal requiredWidth = document()->size().width(); - const qreal availableWidth = size().width() - frameBorder; - if (requiredWidth > availableWidth) { - qreal newWidth = requiredWidth + frameBorder; - if (parentWidget() && pos().x() + newWidth > parentWidget()->width()) { - newWidth = parentWidget()->width() - pos().x(); - } - newSize.setWidth(newWidth); - } - const qreal requiredHeight = document()->size().height(); const qreal availableHeight = size().height() - frameBorder; if (requiredHeight > availableHeight) { diff --git a/src/kitemviews/private/kitemlistroleeditor.h b/src/kitemviews/private/kitemlistroleeditor.h index eb8a9cb5e..3956380cd 100644 --- a/src/kitemviews/private/kitemlistroleeditor.h +++ b/src/kitemviews/private/kitemlistroleeditor.h @@ -45,6 +45,7 @@ public: QByteArray role() const; void setAllowUpDownKeyChainEdit(bool allowChainEdit); + void setFinishedSignalBlocked(bool blocked); bool eventFilter(QObject *watched, QEvent *event) override; Q_SIGNALS: diff --git a/src/kitemviews/private/kitemlistsmoothscroller.cpp b/src/kitemviews/private/kitemlistsmoothscroller.cpp index b7d0a8cd4..915b6b3d5 100644 --- a/src/kitemviews/private/kitemlistsmoothscroller.cpp +++ b/src/kitemviews/private/kitemlistsmoothscroller.cpp @@ -214,6 +214,7 @@ void KItemListSmoothScroller::handleWheelEvent(QWheelEvent *event) QWheelEvent *copy = event->clone(); QApplication::sendEvent(m_scrollBar, copy); event->setAccepted(copy->isAccepted()); + delete copy; m_smoothScrolling = previous; } diff --git a/src/kitemviews/private/kitemlistviewanimation.cpp b/src/kitemviews/private/kitemlistviewanimation.cpp index 2ea884461..db98713f7 100644 --- a/src/kitemviews/private/kitemlistviewanimation.cpp +++ b/src/kitemviews/private/kitemlistviewanimation.cpp @@ -143,6 +143,8 @@ void KItemListViewAnimation::start(QGraphicsWidget *widget, AnimationType type, m_animation[type].insert(widget, propertyAnim); propertyAnim->start(); + + Q_EMIT started(widget, type, endValue); } void KItemListViewAnimation::stop(QGraphicsWidget *widget, AnimationType type) diff --git a/src/kitemviews/private/kitemlistviewanimation.h b/src/kitemviews/private/kitemlistviewanimation.h index 821566161..a23715a8a 100644 --- a/src/kitemviews/private/kitemlistviewanimation.h +++ b/src/kitemviews/private/kitemlistviewanimation.h @@ -74,6 +74,7 @@ public: Q_SIGNALS: void finished(QGraphicsWidget *widget, KItemListViewAnimation::AnimationType type); + void started(QGraphicsWidget *widget, AnimationType type, const QVariant &endValue); private Q_SLOTS: void slotFinished(); diff --git a/src/kitemviews/private/kpixmapmodifier.cpp b/src/kitemviews/private/kpixmapmodifier.cpp index bf316b880..96aea26d4 100644 --- a/src/kitemviews/private/kpixmapmodifier.cpp +++ b/src/kitemviews/private/kpixmapmodifier.cpp @@ -15,6 +15,8 @@ #include "kpixmapmodifier.h" +#include "dolphin_iconsmodesettings.h" + #include <QGuiApplication> #include <QImage> #include <QPainter> @@ -281,7 +283,10 @@ void KPixmapModifier::scale(QPixmap &pixmap, const QSize &scaledSize) return; } qreal dpr = pixmap.devicePixelRatio(); - pixmap = pixmap.scaled(scaledSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); + const Qt::TransformationMode mode = IconsModeSettings::usePixelatedScaling() + ? Qt::FastTransformation + : Qt::SmoothTransformation; + pixmap = pixmap.scaled(scaledSize, Qt::KeepAspectRatio, mode); pixmap.setDevicePixelRatio(dpr); } diff --git a/src/main.cpp b/src/main.cpp index eeb01854b..1ef608193 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -232,14 +232,16 @@ int main(int argc, char **argv) } // Only restore session if: - // 1. Not explicitly opening a new instance - // 2. The "remember state" setting is enabled or session restoration after + // 1. The "remember state" setting is enabled or session restoration after // reboot is in use + // 2. This is the first instance or restoring multiple instances with + // session restoration after reboot enabled // 3. There is a session available to restore - if (!parser.isSet(QStringLiteral("new-window")) && (app.isSessionRestored() || GeneralSettings::rememberOpenedTabs())) { + if (app.isSessionRestored() || GeneralSettings::rememberOpenedTabs()) { // Get saved state data for the last-closed Dolphin instance const QString serviceName = QStringLiteral("org.kde.dolphin-%1").arg(QCoreApplication::applicationPid()); - if (Dolphin::dolphinGuiInstances(serviceName).size() > 0) { + const auto instancesCount = Dolphin::dolphinGuiInstances(serviceName).size(); + if (instancesCount == 1 || (app.isSessionRestored() && instancesCount > 0)) { const QString className = KXmlGuiWindow::classNameOfToplevel(1); if (className == QLatin1String("DolphinMainWindow")) { mainWindow->restore(1); diff --git a/src/org.kde.dolphin.appdata.xml b/src/org.kde.dolphin.appdata.xml index 8df4c91a8..156c5cfe8 100644 --- a/src/org.kde.dolphin.appdata.xml +++ b/src/org.kde.dolphin.appdata.xml @@ -96,6 +96,7 @@ <summary xml:lang="ja">ファイルを管理</summary> <summary xml:lang="ka">მართეთ თქვენი ფაილები</summary> <summary xml:lang="ko">내 파일 관리</summary> + <summary xml:lang="lt">Tvarkyti failus</summary> <summary xml:lang="lv">Pārvaldiet savas datnes</summary> <summary xml:lang="nb">Behandle filene dine</summary> <summary xml:lang="nl">Uw bestanden beheren</summary> @@ -107,6 +108,7 @@ <summary xml:lang="ro">Gestionați-vă fișierele</summary> <summary xml:lang="ru">Управление файлами</summary> <summary xml:lang="sa">स्वसञ्चिकाः प्रबन्धयन्तु</summary> + <summary xml:lang="sk">Správa vašich súborov</summary> <summary xml:lang="sl">Upravljajte svoje datoteke</summary> <summary xml:lang="sv">Hantera dina filer</summary> <summary xml:lang="ta">கோப்புகளை நிர்வகியுங்கள்</summary> @@ -290,6 +292,7 @@ <p xml:lang="ja">Dolphin は、多くのインターネット上のクラウドサービスや他のリモートマシンのファイルやフォルダを、あたかもデスクトップ上にあるかのように表示することができます。</p> <p xml:lang="ka">Dolphin-ს ფაილებისა და საქაღალდეების ჩვენება ბევრი ღრუბლოვანი სერვისიდან და სხვა დაშორებული მანქანებიდან ისე შეუძლია, თითქოს ისინი თქვენს სამუშაო მაგიდაზე იყვნენ.</p> <p xml:lang="ko">Dolphin은 인터넷 클라우드 서비스나 원격지에 있는 파일이나 폴더를 내 데스크톱에 있는 것처럼 표시할 수 있습니다.</p> + <p xml:lang="lt">Dolphin gali atvaizduoti failus ir aplankus iš daugelio internetinių debesijos paslaugų bei kitų nuotolinių kompiuterių taip, lyg jie būtų čia pat, jūsų darbalaukyje.</p> <p xml:lang="lv">„Dolphin“ var parādīt datnes un mapes no daudziem internta mākoņdatošanas pakalpojumu sniedzējiem it kā tās atrastos jūsu datorā.</p> <p xml:lang="nb">Dolphin kan vise filer og mapper fra mange skytjenester og andre eksterne maskiner, som om de var lokale filer og mapper.</p> <p xml:lang="nl">Dolphin kan bestanden en mappen uit vele Internet-cloudservices en andere machines op afstand tonen alsof ze direct op uw bureaublad staan.</p> @@ -352,7 +355,7 @@ <p xml:lang="sl">Dolphin ima tudi integriran terminal, ki vam omogoča zagon ukazov v trenutni mapi. Zmogljivosti Dolphina lahko razširite še močnimi vtičniki, ki ga prilagodijo vašemu delovnemu toku. Lahko uporabite git integracijski vtičnik za interakcijo s skladišči git ali vtičnik Nextcloud za sinhronizacijo datotek v spletu in še veliko več.</p> <p xml:lang="sv">Dolphin levereras också med en integrerad terminal som låter dig köra kommandon i den aktuella katalogen. Du kan utöka Dolphins funktioner ytterligare med kraftfulla insticksprogram för att anpassa programmet till ditt arbetsflöde. Du kan använda insticksprogrammet git-integrering för att komma åt git-arkiv, eller insticksprogrammet Nextcloud för att synkronisera filer på nätet, med mera.</p> <p xml:lang="ta">தற்போதைய அடைவில் நீங்கள் விரும்பும் கட்டளைகளை இயக்க உதவும் உள்ளமைந்த முனையத்தை டால்பின் கொண்டுள்ளது. பல ஆற்றல்மிக்க செருகுநிரல்களைக் கொண்டு நீங்கள் டால்பினின் இயலுமைகளை உங்கள் தேவைக்கேற்ப மேம்படுத்தலாம். உதாரணத்துக்கு git ஒருங்கிணைப்பு செருகுநிரலைக் கொண்டு git repo-களை நீங்கள் கையாளலாம், அல்லது Nextcloud செருகுநிரலைக் கொண்டு உங்கள் கோப்புகளை இணையத்தில் ஒத்திசைக்கலாம்.</p> - <p xml:lang="tr">Dolphin ayrıca mevcut klasörde komutları çalıştırmanıza izin veren tümleşik bir uçbirim ile birlikte gelir. İş akışınıza uyarlamak için güçlü eklentilerle Dolphin’in yeteneklerini daha da genişletebilirsiniz. Git bütünleştirme eklentisini git depolarıyla etkileşime girmek veya Nextcloud eklentisini dosyalarınızı çevrimiçi olarak eşzamanlamak ve çok daha fazlasını için kullanabilirsiniz.</p> + <p xml:lang="tr">Dolphin ayrıca mevcut klasörde komutları çalıştırmanıza izin veren tümleşik bir uçbirim ile birlikte gelir. İş akışınıza uyarlamak için güçlü eklentilerle Dolphin’in yeteneklerini daha da genişletebilirsiniz. Git bütünleştirme eklentisini git depolarıyla etkileşime girmek veya Nextcloud eklentisini dosyalarınızı çevrim içi olarak eşzamanlamak ve çok daha fazlasını için kullanabilirsiniz.</p> <p xml:lang="uk">Також до Dolphin вбудовано термінал, за допомогою якого ви можете виконувати команди у поточній теці. Ви можете розширити можливості Dolphin за допомогою потужних додатків з метою пристосування програми до ваших робочих процедур. Ви можете скористатися додатком інтеграції із git для роботи зі сховищами git або додатком Nextcloud для синхронізації ваших файлів зі сховищами даних в інтернеті тощо.</p> <p xml:lang="vi">Dolphin còn đi kèm với một dòng lệnh tích hợp, cho phép bạn chạy lệnh ở thư mục hiện tại. Bạn thậm chí có thể mở rộng khả năng của Dolphin thêm nữa bằng các phần cài cắm mạnh mẽ để đáp ứng với cách làm việc của bạn. Bạn có thể dùng phần cài cắm tích hợp git để tương tác với các kho git, hay phần cài cắm Nextcloud để đồng bộ trực tuyến các tệp của bạn, và còn nhiều nữa.</p> <p xml:lang="zh-CN">Dolphin 还整合了命令行终端,可以在当前文件夹中执行命令行指令。您还可以通过插件来进一步增强 Dolphin 的功能,适应您的使用习惯。例如您可以使用 git 整合插件来与 git 源代码仓库进行交互,或者使用 Nextcloud 插件来在线同步文件等。</p> @@ -451,6 +454,7 @@ <caption xml:lang="ja">Dolphin の組み込みターミナル</caption> <caption xml:lang="ka">Dolphin-ის ჩადგმული ტერმინალი</caption> <caption xml:lang="ko">Dolphin에 내장된 터미널</caption> + <caption xml:lang="lt">Dolphin programoje įtaisytas terminalas</caption> <caption xml:lang="lv">„Dolphin“ iegultais terminālis</caption> <caption xml:lang="my">လင်းပိုင်တွင် မြှပ်သွင်း-တာမီနယ်</caption> <caption xml:lang="nb">Innebygd terminal i Dolphin</caption> @@ -504,6 +508,7 @@ <caption xml:lang="ja">Dolphin では、ファイルマネージャを思いどおりに設定することができます。</caption> <caption xml:lang="ka">Dolphin-ი საშუალებას გაძლევთ ფაილების მმართველი ზუსტად ისე მოირგოთ, როგორც თქვენ გნებვთ</caption> <caption xml:lang="ko">Dolphin은 사용하고 싶은 대로 설정할 수 있습니다</caption> + <caption xml:lang="lt">Dolphin jums leidžia konfigūruoti failų tvarkytuvę būtent taip, kaip pageidaujate</caption> <caption xml:lang="lv">„Dolphin“ ir pēc jūsu vēlmēm pielāgojams datņu pārvaldnieks</caption> <caption xml:lang="my">လင်းပိုင်သည် ဖိုင်လ်မန်နေဂျာကို သင်ကြိုက်သလို ပြင်ဆင်ခွင့်ပေးထားသည်</caption> <caption xml:lang="nb">Dolphin lar deg sette opp filbehandleren akkurat slik du vil</caption> @@ -534,6 +539,8 @@ </provides> <launchable type="desktop-id">org.kde.dolphin.desktop</launchable> <releases> + <release version="26.04.1" date="2026-05-07"/> + <release version="26.04.0" date="2026-04-16"/> <release version="25.12.3" date="2026-03-05"/> <release version="25.12.2" date="2026-02-05"/> <release version="25.12.1" date="2026-01-08"/> diff --git a/src/org.kde.dolphin.desktop b/src/org.kde.dolphin.desktop index b74480aac..8c41e6288 100755 --- a/src/org.kde.dolphin.desktop +++ b/src/org.kde.dolphin.desktop @@ -132,6 +132,7 @@ Comment[bg]=Управление на файловете Comment[ca]=Gestioneu els fitxers Comment[ca@valencia]=Gestioneu els fitxers Comment[cs]=Spravujte své soubory +Comment[da]=Adminstrér dine filer Comment[de]=Verwalten Sie Ihre Dateien Comment[es]=Gestión de archivos Comment[eu]=Kudeatu zure fitxategiak @@ -157,6 +158,7 @@ Comment[pt]=Gerir os seus ficheiros Comment[pt_BR]=Gerencie seus arquivos Comment[ro]=Gestionați-vă fișierele Comment[ru]=Управление файлами +Comment[sk]=Spravujte svoje súbory Comment[sl]=Upravlja vaše datoteke Comment[sv]=Hantera filer Comment[tr]=Dosyalarınızı yönetin diff --git a/src/panels/information/informationpanel.cpp b/src/panels/information/informationpanel.cpp index 03e048ae7..5f613c520 100644 --- a/src/panels/information/informationpanel.cpp +++ b/src/panels/information/informationpanel.cpp @@ -49,6 +49,7 @@ InformationPanel::~InformationPanel() void InformationPanel::setSelection(const KFileItemList &selection) { m_selection = selection; + m_hoveredItem = KFileItem(); // Selection supersedes any stale hover if (!isVisible()) { return; @@ -83,14 +84,12 @@ void InformationPanel::requestDelayedItemInfo(const KFileItem &item) return; } - if (item.isNull()) { - m_hoveredItem = KFileItem(); - return; - } - cancelRequest(); - - m_isSelectionActive = false; + if (!item.isNull()) { + m_isSelectionActive = false; + } else { + m_isSelectionActive = !m_selection.isEmpty(); + } m_hoveredItem = item; m_infoTimer->start(); @@ -229,35 +228,36 @@ void InformationPanel::showItemInfo() cancelRequest(); //qDebug() << "showItemInfo" << m_fileItem; - if (m_hoveredItem.isNull() && (m_selection.count() > 1)) { + bool canShowHoverItem = !m_isSelectionActive && !m_hoveredItem.isNull() && InformationPanelSettings::showHovered(); + if (m_selection.count() > 1 && !canShowHoverItem) { // The information for a selection of items should be shown m_content->showItems(m_selection); - } else { - // The information for exactly one item should be shown - KFileItem item; + return; + } - if (!m_isSelectionActive && !m_hoveredItem.isNull() && InformationPanelSettings::showHovered()) { - item = m_hoveredItem; - } else if (m_isSelectionActive && !m_selection.isEmpty()) { - Q_ASSERT(m_selection.count() == 1); - item = m_selection.first(); - } + // The information for exactly one item should be shown + KFileItem item; + if (canShowHoverItem) { + item = m_hoveredItem; + } else if (!m_selection.isEmpty()) { + Q_ASSERT(m_selection.count() == 1); + item = m_selection.first(); + } - if (!item.isNull()) { - m_shownUrl = item.url(); - m_content->showItem(item); - return; - } + if (!item.isNull()) { + m_shownUrl = item.url(); + m_content->showItem(item); + return; + } - // No item is hovered and no selection has been done: provide - // an item for the currently shown directory. - m_shownUrl = url(); - m_folderStatJob = KIO::stat(m_shownUrl, KIO::StatJob::SourceSide, KIO::StatDefaultDetails | KIO::StatRecursiveSize, KIO::HideProgressInfo); - if (m_folderStatJob->uiDelegate()) { - KJobWidgets::setWindow(m_folderStatJob, this); - } - connect(m_folderStatJob, &KIO::Job::result, this, &InformationPanel::slotFolderStatFinished); + // No item is hovered and no selection has been done: provide + // an item for the currently shown directory. + m_shownUrl = url(); + m_folderStatJob = KIO::stat(m_shownUrl, KIO::StatJob::SourceSide, KIO::StatDefaultDetails | KIO::StatRecursiveSize, KIO::HideProgressInfo); + if (m_folderStatJob->uiDelegate()) { + KJobWidgets::setWindow(m_folderStatJob, this); } + connect(m_folderStatJob, &KIO::Job::result, this, &InformationPanel::slotFolderStatFinished); } void InformationPanel::slotFolderStatFinished(KJob *job) diff --git a/src/panels/information/mediawidget.cpp b/src/panels/information/mediawidget.cpp index 20366445a..54a6d7fe7 100644 --- a/src/panels/information/mediawidget.cpp +++ b/src/panels/information/mediawidget.cpp @@ -50,6 +50,7 @@ public: SeekSlider(Qt::Orientation orientation, QWidget *parent = nullptr) : QSlider(orientation, parent) { + grabGesture(Qt::TapAndHoldGesture); } protected: @@ -132,6 +133,15 @@ protected: QSlider::keyPressEvent(event); } } + + bool event(QEvent *event) override + { + if (event->type() == QEvent::Gesture) { + event->ignore(); + return true; + } + return QSlider::event(event); + } }; MediaWidget::MediaWidget(QWidget *parent) @@ -292,8 +302,8 @@ void MediaWidget::initPlayer() { if (!m_player) { initLayout(); - m_player = new QMediaPlayer; - m_player->setAudioOutput(new QAudioOutput); + m_player = new QMediaPlayer(this); + m_player->setAudioOutput(new QAudioOutput(m_player)); m_videoWidget = new EmbeddedVideoPlayer(this); m_videoWidget->setCursor(Qt::PointingHandCursor); diff --git a/src/panels/information/pixmapviewer.cpp b/src/panels/information/pixmapviewer.cpp index 9ac9bd253..4f3f9e82f 100644 --- a/src/panels/information/pixmapviewer.cpp +++ b/src/panels/information/pixmapviewer.cpp @@ -54,6 +54,7 @@ void PixmapViewer::setPixmap(const QPixmap &pixmap) void PixmapViewer::setSizeHint(const QSize &size) { if (m_animatedImage && size != m_sizeHint) { + m_animatedImage->setScaledSize(QSize()); m_animatedImage->stop(); } @@ -104,10 +105,12 @@ void PixmapViewer::updateAnimatedImageFrame() Q_ASSERT(m_animatedImage); m_pixmap = m_animatedImage->currentPixmap(); - if (m_pixmap.width() > m_sizeHint.width() || m_pixmap.height() > m_sizeHint.height()) { - m_pixmap = m_pixmap.scaled(m_sizeHint, Qt::KeepAspectRatio); + const auto physicalSize = m_sizeHint * devicePixelRatio(); + if (m_pixmap.width() > physicalSize.width() || m_pixmap.height() > physicalSize.height()) { + m_pixmap = m_pixmap.scaled(physicalSize, Qt::KeepAspectRatio); m_animatedImage->setScaledSize(m_pixmap.size()); } + m_pixmap.setDevicePixelRatio(devicePixelRatio()); update(); } diff --git a/src/panels/panel.cpp b/src/panels/panel.cpp index e8250c62b..94f80f7e7 100644 --- a/src/panels/panel.cpp +++ b/src/panels/panel.cpp @@ -54,6 +54,15 @@ void Panel::setUrl(const QUrl &url) } } +void Panel::refreshUrl(const QUrl &url) +{ + const QUrl oldUrl = m_url; + m_url = url; + if (!urlChanged()) { + m_url = oldUrl; + } +} + void Panel::readSettings() { } diff --git a/src/panels/panel.h b/src/panels/panel.h index 5f1fabb4a..23136a4b9 100644 --- a/src/panels/panel.h +++ b/src/panels/panel.h @@ -46,6 +46,12 @@ public Q_SLOTS: void setUrl(const QUrl &url); /** + * This is invoked whenever the folder being displayed in the + * active Dolphin view is refreshed. + */ + void refreshUrl(const QUrl &url); + + /** * Refreshes the view to get synchronized with the settings. */ virtual void readSettings(); diff --git a/src/panels/terminal/terminalpanel.cpp b/src/panels/terminal/terminalpanel.cpp index a357b31ac..2ed6cd33e 100644 --- a/src/panels/terminal/terminalpanel.cpp +++ b/src/panels/terminal/terminalpanel.cpp @@ -145,9 +145,10 @@ bool TerminalPanel::urlChanged() return false; } - const bool sendInput = m_terminal && !hasProgramRunning() && isVisible() && m_syncUrl; + const bool sendInput = m_terminal && !hasProgramRunning() && isVisible() && m_syncUrl && url() != m_url; if (sendInput) { changeDir(url()); + m_url = url(); } return true; diff --git a/src/panels/terminal/terminalpanel.h b/src/panels/terminal/terminalpanel.h index 0aaf921c0..7f4e9e36c 100644 --- a/src/panels/terminal/terminalpanel.h +++ b/src/panels/terminal/terminalpanel.h @@ -86,6 +86,7 @@ private: private: bool m_clearTerminal; bool m_syncUrl; + QUrl m_url; KIO::StatJob *m_mostLocalUrlJob; QVBoxLayout *m_layout; diff --git a/src/search/bar.cpp b/src/search/bar.cpp index 851eef942..7e31cb0f2 100644 --- a/src/search/bar.cpp +++ b/src/search/bar.cpp @@ -187,11 +187,13 @@ void Bar::selectAll() m_searchTermEditor->selectAll(); } -void Bar::setVisible(bool visible, Animated animated) +void Bar::setVisible(bool visible, Animated animated, HideBehavior hideBehavior) { if (!visible) { m_startSearchTimer->stop(); - Q_EMIT urlChangeRequested(m_searchConfiguration->searchPath()); + if (hideBehavior == HideBehavior::RestoreUrl) { + Q_EMIT urlChangeRequested(m_searchConfiguration->searchPath()); + } if (isAncestorOf(QApplication::focusWidget())) { Q_EMIT focusViewRequest(); } diff --git a/src/search/bar.h b/src/search/bar.h index 969335232..1017d813f 100644 --- a/src/search/bar.h +++ b/src/search/bar.h @@ -51,6 +51,19 @@ class Bar : public AnimatedHeightWidget, public UpdatableStateInterface Q_OBJECT public: + enum class HideBehavior { + /** + * When hiding the bar, request that the view switches back to a non-search URL (the search path). + * This is the behavior when the user explicitly quits searching. + */ + RestoreUrl, + /** + * When hiding the bar, do not request any URL change. + * This is used when the UI is hidden automatically because the view navigated elsewhere already. + */ + KeepCurrentUrl, + }; + /** * @brief Constructs a Search::Bar with an initial state matching @p dolphinQuery and with parent @p parent. */ @@ -80,7 +93,7 @@ public: * be properly un/checked. * @see AnimatedHeightWidget::setVisible(). */ - void setVisible(bool visible, Animated animated); + void setVisible(bool visible, Animated animated, HideBehavior hideBehavior = HideBehavior::RestoreUrl); /** * @returns false, when the search UI has not yet been changed to search for anything specific. For example when no search term has been entered yet. diff --git a/src/search/dolphinquery.cpp b/src/search/dolphinquery.cpp index 4b7627846..2db574673 100644 --- a/src/search/dolphinquery.cpp +++ b/src/search/dolphinquery.cpp @@ -225,7 +225,7 @@ QUrl DolphinQuery::toUrl() const query.setSearchString(balooQueryStrings.join(QLatin1Char(' '))); - return query.toSearchUrl(QUrl::toPercentEncoding(title())); + return query.toSearchUrl(title()); } #endif diff --git a/src/search/popup.cpp b/src/search/popup.cpp index 2c4b38fa5..66dc4f7e4 100644 --- a/src/search/popup.cpp +++ b/src/search/popup.cpp @@ -341,6 +341,10 @@ void Popup::slotKFindButtonClicked() if (kFind) { auto *job = new KIO::ApplicationLauncherJob(kFind); job->setUrls({m_searchConfiguration->searchPath()}); + + // must hide the parent pop, so the focus switches correctly + hide(); + job->start(); return; } @@ -351,7 +355,7 @@ void Popup::slotKFindButtonClicked() #else auto packageInstaller = new DolphinPackageInstaller( KFIND_PACKAGE_NAME, - QUrl("appstream://org.kde.kfind.desktop"), + QUrl("appstream://org.kde.kfind"), []() { return KService::serviceByDesktopName(kFindDesktopName); }, diff --git a/src/search/selectors/dateselector.cpp b/src/search/selectors/dateselector.cpp index 70e563614..a7f4463e5 100644 --- a/src/search/selectors/dateselector.cpp +++ b/src/search/selectors/dateselector.cpp @@ -6,6 +6,7 @@ #include "dateselector.h" +#include "../chip.h" #include "../dolphinquery.h" #include <KDatePicker> @@ -19,7 +20,11 @@ Search::DateSelector::DateSelector(std::shared_ptr<const DolphinQuery> dolphinQu : QToolButton{parent} , UpdatableStateInterface{dolphinQuery} , m_datePickerPopup{ - new KDatePickerPopup{KDatePickerPopup::NoDate | KDatePickerPopup::DatePicker | KDatePickerPopup::Words, dolphinQuery->modifiedSinceDate(), this}} + new KDatePickerPopup{/* When in a Chip, we don't add the KDatePickerPopup::NoDate option because it would allow removing the Chip unexpectedly. */ + qobject_cast<ChipBase *>(parent) ? KDatePickerPopup::DatePicker | KDatePickerPopup::Words + : KDatePickerPopup::NoDate | KDatePickerPopup::DatePicker | KDatePickerPopup::Words, + dolphinQuery->modifiedSinceDate(), + this}} { setToolButtonStyle(Qt::ToolButtonTextBesideIcon); setPopupMode(QToolButton::InstantPopup); diff --git a/src/search/selectors/filetypeselector.cpp b/src/search/selectors/filetypeselector.cpp index acf5680e2..6c5bbc5d7 100644 --- a/src/search/selectors/filetypeselector.cpp +++ b/src/search/selectors/filetypeselector.cpp @@ -6,6 +6,7 @@ #include "filetypeselector.h" +#include "../chip.h" #include "../dolphinquery.h" #include <KFileMetaData/TypeInfo> @@ -20,7 +21,9 @@ FileTypeSelector::FileTypeSelector(std::shared_ptr<const DolphinQuery> dolphinQu for (KFileMetaData::Type::Type type = KFileMetaData::Type::FirstType; type <= KFileMetaData::Type::LastType; type = KFileMetaData::Type::Type(type + 1)) { switch (type) { case KFileMetaData::Type::Empty: - addItem(/** No icon for the empty state */ i18nc("@item:inlistbox", "Any Type"), type); + if (!qobject_cast<ChipBase *>(parent)) { // When in a Chip, we don't add the "Any Type" option because it would unexpectedly remove the Chip. + addItem(/* No icon for the empty state */ i18nc("@item:inlistbox", "Any Type"), type); + } continue; case KFileMetaData::Type::Archive: addItem(QIcon::fromTheme(QStringLiteral("package-x-generic")), KFileMetaData::TypeInfo{type}.displayName(), type); diff --git a/src/search/selectors/minimumratingselector.cpp b/src/search/selectors/minimumratingselector.cpp index 386f525db..b41561f6a 100644 --- a/src/search/selectors/minimumratingselector.cpp +++ b/src/search/selectors/minimumratingselector.cpp @@ -7,6 +7,7 @@ #include "minimumratingselector.h" +#include "../chip.h" #include "../dolphinquery.h" #include <KLocalizedString> @@ -17,7 +18,9 @@ MinimumRatingSelector::MinimumRatingSelector(std::shared_ptr<const DolphinQuery> : QComboBox{parent} , UpdatableStateInterface{dolphinQuery} { - addItem(/** No icon for the empty state */ i18nc("@item:inlistbox", "Any Rating"), 0); + if (!qobject_cast<ChipBase *>(parent)) { // When in a Chip, we don't add the "Any Rating" option because it would unexpectedly remove the Chip. + addItem(/* No icon for the empty state */ i18nc("@item:inlistbox", "Any Rating"), 0); + } addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox", "1 or more"), 2); addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox", "2 or more"), 4); addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox", "3 or more"), 6); diff --git a/src/search/selectors/tagsselector.cpp b/src/search/selectors/tagsselector.cpp index 6ad74af91..57ce02e2f 100644 --- a/src/search/selectors/tagsselector.cpp +++ b/src/search/selectors/tagsselector.cpp @@ -7,6 +7,7 @@ #include "tagsselector.h" +#include "../chip.h" #include "../dolphinquery.h" #include <KCoreDirLister> @@ -146,6 +147,9 @@ void TagsSelector::updateMenu(const std::shared_ptr<const DolphinQuery> &dolphin QAction *tagAction = new QAction{QIcon::fromTheme(QStringLiteral("tag")), tag, menu()}; tagAction->setCheckable(true); tagAction->setChecked(dolphinQuery->requiredTags().contains(tag)); + tagAction->setEnabled(/* When in a Chip, at least one tags needs to stay checked or the Chip will unexepectedly remove itself. */ + !tagAction->isChecked() || dolphinQuery->requiredTags().size() != 1 || !qobject_cast<ChipBase *>(parent())); + connect(tagAction, &QAction::triggered, this, [this, tag, onlyOneTagExists](bool checked) { QStringList requiredTags = m_searchConfiguration->requiredTags(); if (checked == requiredTags.contains(tag)) { @@ -160,11 +164,19 @@ void TagsSelector::updateMenu(const std::shared_ptr<const DolphinQuery> &dolphin searchConfigurationCopy.setRequiredTags(requiredTags); Q_EMIT configurationChanged(searchConfigurationCopy); + if (qobject_cast<ChipBase *>(parent())) { + auto tagActions = menu()->actions(); + for (auto tagAction : tagActions) { + tagAction->setEnabled(/* When in a Chip, at least one tags needs to stay checked or the Chip will unexepectedly remove itself. */ + !tagAction->isChecked() || searchConfigurationCopy.requiredTags().size() != 1); + } + } if (!onlyOneTagExists) { // Keep the menu open to allow easier tag multi-selection. menu()->show(); } }); + menu()->addAction(tagAction); } if (menuWasVisible) { diff --git a/src/settings/dolphin_contentdisplaysettings.kcfg b/src/settings/dolphin_contentdisplaysettings.kcfg index 6e8f8e9d5..dd10d09e8 100644 --- a/src/settings/dolphin_contentdisplaysettings.kcfg +++ b/src/settings/dolphin_contentdisplaysettings.kcfg @@ -46,5 +46,9 @@ </choices> <default>Middle</default> </entry> + <entry name="HideFileExtensions" type="Bool"> + <label>Hide file name extensions</label> + <default>false</default> + </entry> </group> </kcfg> diff --git a/src/settings/dolphin_directoryviewpropertysettings.kcfg b/src/settings/dolphin_directoryviewpropertysettings.kcfg index bae1f409f..3b23918ef 100644 --- a/src/settings/dolphin_directoryviewpropertysettings.kcfg +++ b/src/settings/dolphin_directoryviewpropertysettings.kcfg @@ -27,6 +27,12 @@ <default code="true">DolphinView::IconsView</default> </entry> + <entry name="ZoomLevel" type="Int" > + <label context="@label">Zoom Level</label> + <whatsthis context="@info:whatsthis">Zoom level of the folder.</whatsthis> + <default>-1</default> + </entry> + <entry name="PreviewsShown" type="Bool" > <label context="@label">Previews shown</label> <whatsthis context="@info:whatsthis">When this option is enabled, a preview of the file content is shown as an icon.</whatsthis> diff --git a/src/settings/dolphin_iconsmodesettings.kcfg b/src/settings/dolphin_iconsmodesettings.kcfg index 7948af75d..d4e3ca8d5 100644 --- a/src/settings/dolphin_iconsmodesettings.kcfg +++ b/src/settings/dolphin_iconsmodesettings.kcfg @@ -31,5 +31,9 @@ <label>Maximum textlines (0 means unlimited)</label> <default>3</default> </entry> + <entry name="UsePixelatedScaling" type="Bool"> + <label>Use nearest-neighbor scaling for icons (better for pixel art)</label> + <default>false</default> + </entry> </group> </kcfg> diff --git a/src/settings/trash/trashsettingspage.cpp b/src/settings/trash/trashsettingspage.cpp index b19571338..7d61a49ce 100644 --- a/src/settings/trash/trashsettingspage.cpp +++ b/src/settings/trash/trashsettingspage.cpp @@ -15,11 +15,7 @@ TrashSettingsPage::TrashSettingsPage(QWidget *parent) : SettingsPageBase(parent) { - QFormLayout *topLayout = new QFormLayout(this); - - m_kcm = KCModuleLoader::loadModule(KPluginMetaData(QStringLiteral("kcm_trash"))); - - topLayout->addRow(m_kcm->widget()); + m_kcm = KCModuleLoader::loadModule(KPluginMetaData(QStringLiteral("kcm_trash")), this); loadSettings(); diff --git a/src/settings/viewmodes/contentdisplaytab.cpp b/src/settings/viewmodes/contentdisplaytab.cpp index 747c3ff20..7b854182e 100644 --- a/src/settings/viewmodes/contentdisplaytab.cpp +++ b/src/settings/viewmodes/contentdisplaytab.cpp @@ -10,6 +10,7 @@ #include <KLocalizedString> #include <QButtonGroup> +#include <QCheckBox> #include <QFormLayout> #include <QHBoxLayout> #include <QRadioButton> @@ -31,6 +32,7 @@ ContentDisplayTab::ContentDisplayTab(QWidget *parent) , m_useCombinedPermissions(nullptr) , m_elideMiddle(nullptr) , m_elideEnding(nullptr) + , m_hideFileExtensions(nullptr) { QFormLayout *topLayout = new QFormLayout(this); topLayout->setFormAlignment(Qt::AlignHCenter); @@ -114,6 +116,9 @@ ContentDisplayTab::ContentDisplayTab(QWidget *parent) elidingModeGroup->addButton(m_elideMiddle); elidingModeGroup->addButton(m_elideEnding); + m_hideFileExtensions = new QCheckBox(i18nc("@option:check", "Hide file name extensions")); + topLayout->addRow(QString(), m_hideFileExtensions); + #ifndef Q_OS_WIN connect(m_recursiveDirectorySizeLimit, &QSpinBox::valueChanged, this, &SettingsPageBase::changed); connect(m_numberOfItems, &QRadioButton::toggled, this, &SettingsPageBase::changed); @@ -133,6 +138,7 @@ ContentDisplayTab::ContentDisplayTab(QWidget *parent) connect(m_caseSensitiveSorting, &QRadioButton::toggled, this, &SettingsPageBase::changed); connect(m_elideMiddle, &QRadioButton::toggled, this, &SettingsPageBase::changed); connect(m_elideEnding, &QRadioButton::toggled, this, &SettingsPageBase::changed); + connect(m_hideFileExtensions, &QCheckBox::toggled, this, &SettingsPageBase::changed); loadSettings(); } @@ -166,6 +172,7 @@ void ContentDisplayTab::applySettings() } else if (m_elideEnding->isChecked()) { settings->setElidingMode(ContentDisplaySettings::ElidingMode::Right); } + settings->setHideFileExtensions(m_hideFileExtensions->isChecked()); settings->save(); GeneralSettings::self()->save(); @@ -188,6 +195,7 @@ void ContentDisplayTab::loadSettings() loadSortingChoiceSettings(); m_elideMiddle->setChecked(settings->elidingMode() == ContentDisplaySettings::ElidingMode::Middle); m_elideEnding->setChecked(settings->elidingMode() == ContentDisplaySettings::ElidingMode::Right); + m_hideFileExtensions->setChecked(settings->hideFileExtensions()); } void ContentDisplayTab::setSortingChoiceValue() diff --git a/src/settings/viewmodes/contentdisplaytab.h b/src/settings/viewmodes/contentdisplaytab.h index 2bae5a87c..6d673b0f6 100644 --- a/src/settings/viewmodes/contentdisplaytab.h +++ b/src/settings/viewmodes/contentdisplaytab.h @@ -10,6 +10,7 @@ #include "dolphin_generalsettings.h" #include "settings/settingspagebase.h" +class QCheckBox; class QRadioButton; class QSpinBox; @@ -45,6 +46,7 @@ private: QRadioButton *m_useCombinedPermissions; QRadioButton *m_elideMiddle; QRadioButton *m_elideEnding; + QCheckBox *m_hideFileExtensions; }; #endif // GENERALTAB_H diff --git a/src/settings/viewmodes/viewsettingstab.cpp b/src/settings/viewmodes/viewsettingstab.cpp index 3593f23dd..12a8c11cb 100644 --- a/src/settings/viewmodes/viewsettingstab.cpp +++ b/src/settings/viewmodes/viewsettingstab.cpp @@ -37,6 +37,7 @@ ViewSettingsTab::ViewSettingsTab(Mode mode, QWidget *parent) , m_widthBox(nullptr) , m_maxLinesBox(nullptr) , m_expandableFolders(nullptr) + , m_pixelatedScaling(nullptr) { QFormLayout *topLayout = new QFormLayout(this); topLayout->setFormAlignment(Qt::AlignHCenter); @@ -93,6 +94,9 @@ ViewSettingsTab::ViewSettingsTab(Mode mode, QWidget *parent) m_maxLinesBox->addItem(i18nc("@item:inlistbox Maximum lines", "4")); m_maxLinesBox->addItem(i18nc("@item:inlistbox Maximum lines", "5")); topLayout->addRow(i18nc("@label:listbox", "Maximum lines:"), m_maxLinesBox); + + m_pixelatedScaling = new QCheckBox(i18nc("@option:check", "Use pixelated thumbnail scaling")); + topLayout->addRow(QString(), m_pixelatedScaling); break; } case CompactMode: { @@ -135,6 +139,7 @@ ViewSettingsTab::ViewSettingsTab(Mode mode, QWidget *parent) case IconsMode: connect(m_widthBox, &QComboBox::currentIndexChanged, this, &ViewSettingsTab::changed); connect(m_maxLinesBox, &QComboBox::currentIndexChanged, this, &ViewSettingsTab::changed); + connect(m_pixelatedScaling, &QCheckBox::toggled, this, &ViewSettingsTab::changed); break; case CompactMode: connect(m_widthBox, &QComboBox::currentIndexChanged, this, &ViewSettingsTab::changed); @@ -173,6 +178,7 @@ void ViewSettingsTab::applySettings() case IconsMode: IconsModeSettings::setTextWidthIndex(m_widthBox->currentIndex()); IconsModeSettings::setMaximumTextLines(m_maxLinesBox->currentIndex()); + IconsModeSettings::setUsePixelatedScaling(m_pixelatedScaling->isChecked()); IconsModeSettings::self()->save(); break; case CompactMode: @@ -235,6 +241,7 @@ void ViewSettingsTab::loadSettings() case IconsMode: m_widthBox->setCurrentIndex(IconsModeSettings::textWidthIndex()); m_maxLinesBox->setCurrentIndex(IconsModeSettings::maximumTextLines()); + m_pixelatedScaling->setChecked(IconsModeSettings::usePixelatedScaling()); break; case CompactMode: m_widthBox->setCurrentIndex(CompactModeSettings::maximumTextWidthIndex()); diff --git a/src/settings/viewmodes/viewsettingstab.h b/src/settings/viewmodes/viewsettingstab.h index 6ea7a3bf3..2f2e75180 100644 --- a/src/settings/viewmodes/viewsettingstab.h +++ b/src/settings/viewmodes/viewsettingstab.h @@ -55,6 +55,7 @@ private: QRadioButton *m_iconAndNameOnly; QCheckBox *m_expandableFolders; QLabel *m_fontWarningLabel; + QCheckBox *m_pixelatedScaling; }; #endif
\ No newline at end of file diff --git a/src/statusbar/dolphinstatusbar.cpp b/src/statusbar/dolphinstatusbar.cpp index 5d7e8e674..4b2bb6f0f 100644 --- a/src/statusbar/dolphinstatusbar.cpp +++ b/src/statusbar/dolphinstatusbar.cpp @@ -24,17 +24,24 @@ #include <QProgressBar> #include <QSlider> #include <QStyleOption> +#include <QTextDocumentFragment> #include <QTimer> #include <QToolButton> +#include <chrono> + +using namespace std::chrono_literals; + namespace { -const int UpdateDelay = 50; +constexpr std::chrono::milliseconds minimumTimeBetweenTextChanges = 50ms; +constexpr std::chrono::seconds temporaryRichTextTimeout = 1s; } DolphinStatusBar::DolphinStatusBar(QWidget *parent) : AnimatedHeightWidget(parent) - , m_text() + , m_temporaryRichText() + , m_hoveredItemText() , m_defaultText() , m_label(nullptr) , m_zoomLabel(nullptr) @@ -44,8 +51,8 @@ DolphinStatusBar::DolphinStatusBar(QWidget *parent) , m_stopButton(nullptr) , m_progress(100) , m_showProgressBarTimer(nullptr) - , m_delayUpdateTimer(nullptr) - , m_textTimestamp() + , m_clearTemporaryRichTextTimer(nullptr) + , m_updateLabelTextTimer(nullptr) { setProperty("_breeze_statusbar_separator", true); @@ -53,7 +60,7 @@ DolphinStatusBar::DolphinStatusBar(QWidget *parent) contentsContainer->setContentsMargins(0, 0, 0, 0); // Initialize text label - m_label = new KSqueezedTextLabel(m_text, contentsContainer); + m_label = new KSqueezedTextLabel{contentsContainer}; m_label->setTextFormat(Qt::PlainText); m_label->setTextInteractionFlags(Qt::TextBrowserInteraction | Qt::TextSelectableByKeyboard); // for accessibility but also to allow copy-pasting this text. @@ -101,11 +108,16 @@ DolphinStatusBar::DolphinStatusBar(QWidget *parent) m_showProgressBarTimer->setSingleShot(true); connect(m_showProgressBarTimer, &QTimer::timeout, this, &DolphinStatusBar::updateProgressInfo); - // initialize text updater delay timer - m_delayUpdateTimer = new QTimer(this); - m_delayUpdateTimer->setInterval(UpdateDelay); - m_delayUpdateTimer->setSingleShot(true); - connect(m_delayUpdateTimer, &QTimer::timeout, this, &DolphinStatusBar::updateLabelText); + // initialize timers for delayed replacing which text is shown + m_clearTemporaryRichTextTimer = new QTimer(this); + m_clearTemporaryRichTextTimer->setInterval(temporaryRichTextTimeout); + m_clearTemporaryRichTextTimer->setSingleShot(true); + connect(m_clearTemporaryRichTextTimer, &QTimer::timeout, this, &DolphinStatusBar::clearTemporaryRichText); + + m_updateLabelTextTimer = new QTimer(this); + m_updateLabelTextTimer->setInterval(minimumTimeBetweenTextChanges); + m_updateLabelTextTimer->setSingleShot(true); + connect(m_updateLabelTextTimer, &QTimer::timeout, this, &DolphinStatusBar::updateLabelText); // Initialize top layout and size policies const int fontHeight = QFontMetrics(m_label->font()).height(); @@ -153,24 +165,6 @@ DolphinStatusBar::DolphinStatusBar(QWidget *parent) DolphinStatusBar::~DolphinStatusBar() = default; -void DolphinStatusBar::setText(const QString &text) -{ - if (m_text == text) { - return; - } - - m_textTimestamp = QTime::currentTime(); - - m_text = text; - // will update status bar text in 50ms - m_delayUpdateTimer->start(); -} - -QString DolphinStatusBar::text() const -{ - return m_text; -} - void DolphinStatusBar::showProgress(const QString ¤tlyRunningTaskTitle, int progressPercent, CancelLoading cancelLoading) { m_cancelLoading = cancelLoading; @@ -215,27 +209,28 @@ int DolphinStatusBar::progress() const return m_progress; } -void DolphinStatusBar::resetToDefaultText() +void DolphinStatusBar::setTemporaryRichText(const QString &temporaryRichText) { - m_text.clear(); - - QTime currentTime; - if (currentTime.msecsTo(m_textTimestamp) < UpdateDelay) { - m_delayUpdateTimer->start(); - } else { - updateLabelText(); + if (m_temporaryRichText == temporaryRichText) { + return; } + + m_temporaryRichText = temporaryRichText; + updateLabelText(); // Show the text instantly because we only show it temporarily anyway. + m_clearTemporaryRichTextTimer->start(); } -void DolphinStatusBar::setDefaultText(const QString &text) +void DolphinStatusBar::setHoveredItemText(const QString &hoveredItemText) { - m_defaultText = text; - updateLabelText(); + m_hoveredItemText = hoveredItemText; + m_updateLabelTextTimer->start(); } -QString DolphinStatusBar::defaultText() const +void DolphinStatusBar::setDefaultText(const QString &text) { - return m_defaultText; + m_defaultText = text; + m_hoveredItemText.clear(); // We want to show the new default text instead of whatever was hovered. + m_updateLabelTextTimer->start(); } void DolphinStatusBar::setUrl(const QUrl &url) @@ -280,7 +275,9 @@ void DolphinStatusBar::updateWidthToContent() QStyleOptionSlider opt; opt.initFrom(this); opt.orientation = Qt::Vertical; - const QSize labelSize = QFontMetrics(font()).size(Qt::TextSingleLine, m_label->fullText()); + const QSize labelSize = m_label->textFormat() == Qt::PlainText + ? QFontMetrics(font()).size(Qt::TextSingleLine, m_label->fullText()) + : QFontMetrics(font()).size(Qt::TextSingleLine, QTextDocumentFragment::fromHtml(m_label->fullText()).toPlainText()); // Make sure minimum height takes clipping into account. setMinimumHeight(m_label->height() + clippingAmount()); const int scrollbarWidth = style()->pixelMetric(QStyle::PM_ScrollBarExtent, &opt, this); @@ -389,10 +386,26 @@ void DolphinStatusBar::updateProgressInfo() updateWidthToContent(); } +void DolphinStatusBar::clearTemporaryRichText() +{ + if (m_clearTemporaryRichTextTimer->isActive()) { + return; + } + m_temporaryRichText.clear(); + m_updateLabelTextTimer->start(); +} + void DolphinStatusBar::updateLabelText() { - const QString text = m_text.isEmpty() ? m_defaultText : m_text; - m_label->setText(text); + if (!m_temporaryRichText.isEmpty()) { + m_label->setTextFormat(Qt::RichText); + m_label->setTextElideMode(Qt::ElideNone); + m_label->setText(m_temporaryRichText); + } else { + m_label->setTextFormat(Qt::PlainText); + m_label->setTextElideMode(Qt::ElideMiddle); + m_label->setText(m_hoveredItemText.isEmpty() ? m_defaultText : m_hoveredItemText); + } updateWidthToContent(); } diff --git a/src/statusbar/dolphinstatusbar.h b/src/statusbar/dolphinstatusbar.h index b4ddcd95e..232a4ba07 100644 --- a/src/statusbar/dolphinstatusbar.h +++ b/src/statusbar/dolphinstatusbar.h @@ -11,8 +11,6 @@ #include <KMessageWidget> -#include <QTime> - class QUrl; class StatusBarSpaceInfo; class QLabel; @@ -37,8 +35,6 @@ public: explicit DolphinStatusBar(QWidget *parent); ~DolphinStatusBar() override; - QString text() const; - enum class CancelLoading { Allowed, Disallowed @@ -62,18 +58,21 @@ public: int progress() const; /** - * Replaces the text set by setText() by the text that - * has been set by setDefaultText(). DolphinStatusBar::text() - * will return an empty string after the reset has been done. + * Sets a text that is shown with priority as a Qt::RichText for a short amount of time. */ - void resetToDefaultText(); - + void setTemporaryRichText(const QString &temporaryRichText); + /** + * Sets a text describing the hovered item. This text is immediately shown if no m_temporaryRichText is currently shown. + * When no item is hovered, call this method with an empty string so the m_defaultText is shown. + * @see setTemporaryRichText() + * @see setDefaultText() + */ + void setHoveredItemText(const QString &hoveredItemText); /** - * Sets the default text, which is shown if the status bar - * is rest by DolphinStatusBar::resetToDefaultText(). + * Sets the default text. This text is immediately shown if no m_temporaryRichText is currently shown. + * @see setTemporaryRichText() */ void setDefaultText(const QString &text); - QString defaultText() const; QUrl url() const; int zoomLevel() const; @@ -105,7 +104,6 @@ public: int clippingAmount() const; public Q_SLOTS: - void setText(const QString &text); void setUrl(const QUrl &url); void setZoomLevel(int zoomLevel); @@ -147,6 +145,12 @@ private Q_SLOTS: void updateProgressInfo(); /** + * Replaces the text set by setTemporaryRichText() by the text set by setHoveredItemText() or setDefaultText(). + * Is only called when m_clearTemporaryRichTextTimer times out. + */ + void clearTemporaryRichText(); + + /** * Updates the text for m_label and does an eliding in * case if the text does not fit into the available width. */ @@ -173,7 +177,11 @@ private: int preferredHeight() const override; private: - QString m_text; + /** @see setTemporaryRichText() */ + QString m_temporaryRichText; + /** @see setHoveredItemText() */ + QString m_hoveredItemText; + /** @see setDefaultText() */ QString m_defaultText; KSqueezedTextLabel *m_label; QLabel *m_zoomLabel; @@ -188,8 +196,10 @@ private: int m_progress; QTimer *m_showProgressBarTimer; - QTimer *m_delayUpdateTimer; - QTime m_textTimestamp; + /** Clears the temporary rich text from the status bar and shows a non-temporary text instead. */ + QTimer *m_clearTemporaryRichTextTimer; + /** Very frequent updates to the status bar text look ugly. Most updates go through this timer to avoid this. */ + QTimer *m_updateLabelTextTimer; QHBoxLayout *m_topLayout; }; diff --git a/src/tests/dolphinmainwindowtest.cpp b/src/tests/dolphinmainwindowtest.cpp index f68f07cbc..21e48f246 100644 --- a/src/tests/dolphinmainwindowtest.cpp +++ b/src/tests/dolphinmainwindowtest.cpp @@ -16,9 +16,11 @@ #include "kitemviews/kitemlistcontroller.h" #include "kitemviews/kitemlistselectionmanager.h" #include "kitemviews/kitemlistwidget.h" +#include "settings/viewmodes/viewmodesettings.h" #include "testdir.h" #include "views/dolphinitemlistview.h" #include "views/viewproperties.h" +#include "views/zoomlevelinfo.h" #include <KActionCollection> #include <KConfig> @@ -69,6 +71,7 @@ private Q_SLOTS: void testThumbnailAfterRename(); void testViewModeAfterDynamicView(); void testActivationAndTabTitleAfterRenameOpeningFolder(); + void testActiveViewAfterTabSwitchWithSplitView(); void cleanupTestCase(); private: @@ -935,7 +938,7 @@ void DolphinMainWindowTest::testAccessibilityTree() // after going forwards which is probably not intended. } } - QCOMPARE_GE(testedObjectsSizeAfterTraversingForwards, 11); // The test did not reach many objects while using the Tab key to move through Dolphin. Did the + QCOMPARE_GE(testedObjectsSizeAfterTraversingForwards, 10); // The test did not reach many objects while using the Tab key to move through Dolphin. Did the // test run correctly? } @@ -1077,6 +1080,7 @@ void DolphinMainWindowTest::testThumbnailAfterRename() void DolphinMainWindowTest::testViewModeAfterDynamicView() { GeneralSettings *settings = GeneralSettings::self(); + settings->setGlobalViewProps(true); settings->setDynamicView(true); settings->save(); @@ -1144,6 +1148,9 @@ void DolphinMainWindowTest::testViewModeAfterDynamicView() QCOMPARE(view->m_mode, DolphinView::DetailsView); QVERIFY(!ViewProperties(view->viewPropertiesUrl()).dynamicViewPassed()); + // store parent current zoom level + const int parentZoomLevel = view->zoomLevel(); + // go to child folder and make sure view mode change to "Details" is permanent m_mainWindow->openFiles({testDirUrl + "/a"}, false); view->m_model->loadDirectory(QUrl(testDirUrl + "/a")); @@ -1151,6 +1158,60 @@ void DolphinMainWindowTest::testViewModeAfterDynamicView() QVERIFY(modelDirectoryLoadingCompletedSpy.wait()); QCOMPARE(view->m_mode, DolphinView::DetailsView); QVERIFY(ViewProperties(view->viewPropertiesUrl()).dynamicViewPassed()); + + // still on child, change view zoom level + const int childZoomLevel = view->zoomLevel() + 2; + view->setZoomLevel(childZoomLevel); + + // go back to parent folder and check for zoom level + m_mainWindow->actionCollection()->action(KStandardAction::name(KStandardAction::Back))->trigger(); + view->m_model->loadDirectory(testDir->url()); + view->setUrl(testDir->url()); + QVERIFY(modelDirectoryLoadingCompletedSpy.wait()); + QCOMPARE(view->zoomLevel(), parentZoomLevel); + QVERIFY(!ViewProperties(view->viewPropertiesUrl()).dynamicViewPassed()); + + // go to child and check if zoom level is permanent + m_mainWindow->openFiles({testDirUrl + "/a"}, false); + view->m_model->loadDirectory(QUrl(testDirUrl + "/a")); + view->setUrl(QUrl(testDirUrl + "/a")); + QVERIFY(modelDirectoryLoadingCompletedSpy.wait()); + QCOMPARE(view->zoomLevel(), childZoomLevel); + QVERIFY(ViewProperties(view->viewPropertiesUrl()).dynamicViewPassed()); + + // test for global views + settings->setGlobalViewProps(true); + settings->save(); + QVERIFY(GeneralSettings::globalViewProps()); + + // go back to parent folder and set zoom level + m_mainWindow->actionCollection()->action(KStandardAction::name(KStandardAction::Back))->trigger(); + view->m_model->loadDirectory(testDir->url()); + view->setUrl(testDir->url()); + QVERIFY(modelDirectoryLoadingCompletedSpy.wait()); + + // zoom isn't changed + QCOMPARE(view->zoomLevel(), childZoomLevel); + + // change the zoom + view->setZoomLevel(parentZoomLevel + 1); + QCOMPARE(view->zoomLevel(), parentZoomLevel + 1); + QVERIFY(!ViewProperties(view->viewPropertiesUrl()).dynamicViewPassed()); + + // go to child and check if zoom level remains the same + m_mainWindow->openFiles({testDirUrl + "/a"}, false); + view->m_model->loadDirectory(QUrl(testDirUrl + "/a")); + view->setUrl(QUrl(testDirUrl + "/a")); + QVERIFY(modelDirectoryLoadingCompletedSpy.wait()); + + ViewModeSettings modeDefaultSettings{DolphinView::IconsView}; + auto defaultPreviewIconSize = modeDefaultSettings.previewSize(); + auto defaultPreviewZoom = ZoomLevelInfo::zoomLevelForIconSize(QSize(defaultPreviewIconSize, defaultPreviewIconSize)); + // dynamic view works + QCOMPARE(view->m_mode, DolphinView::IconsView); + QCOMPARE(view->zoomLevel(), defaultPreviewZoom); + // that's the global settings, no dynamicViewPassed saved + QVERIFY(!ViewProperties(view->viewPropertiesUrl()).dynamicViewPassed()); } void DolphinMainWindowTest::testActivationAndTabTitleAfterRenameOpeningFolder() @@ -1232,6 +1293,53 @@ void DolphinMainWindowTest::testActivationAndTabTitleAfterRenameOpeningFolder() QCOMPARE(tabWidget->tabText(1), expectedNewTab1Title); } +// Test that switching tabs does not spuriously toggle which split-view pane is active. +// Regression test for the bug where DolphinTabPage::setActive(true) during tab switch +// caused DolphinView::activated() to reach slotViewActivated(), which toggled +// m_primaryViewActive and connected MainWindow signals to the wrong view container. +void DolphinMainWindowTest::testActiveViewAfterTabSwitchWithSplitView() +{ + m_mainWindow->openDirectories({QUrl::fromLocalFile(QDir::homePath())}, false); + m_mainWindow->show(); + QVERIFY(QTest::qWaitForWindowExposed(m_mainWindow.data())); + QVERIFY(m_mainWindow->isVisible()); + + auto tabWidget = m_mainWindow->findChild<DolphinTabWidget *>("tabWidget"); + QVERIFY(tabWidget); + + // Enable split view on the first tab. After this, the secondary (right) pane + // becomes active via slotViewActivated(), so primaryViewActive() is false. + m_mainWindow->actionCollection()->action(QStringLiteral("split_view"))->trigger(); + QVERIFY(tabWidget->currentTabPage()->splitViewEnabled()); + QVERIFY(!tabWidget->currentTabPage()->primaryViewActive()); + auto firstTabPage = tabWidget->currentTabPage(); + auto firstTabSecondary = firstTabPage->secondaryViewContainer(); + QVERIFY(firstTabSecondary->isActive()); + + // Open a second tab and switch to it. + tabWidget->openNewActivatedTab(QUrl::fromLocalFile(QDir::homePath())); + QCOMPARE(tabWidget->count(), 2); + QCOMPARE(tabWidget->currentIndex(), 1); + + // Spy on activeViewChanged to count emissions during the tab switch back. + QSignalSpy activeViewChangedSpy(tabWidget, &DolphinTabWidget::activeViewChanged); + + // Switch back to the first tab. + tabWidget->setCurrentIndex(0); + QCOMPARE(tabWidget->currentTabPage(), firstTabPage); + + // activeViewChanged must be emitted exactly once — by currentTabChanged itself. + // A spurious second emission would indicate slotViewActivated() fired during + // the programmatic setActive(true) and toggled m_primaryViewActive. + QCOMPARE(activeViewChangedSpy.count(), 1); + + // The secondary pane must still be the designated active one. + QVERIFY(!firstTabPage->primaryViewActive()); + QCOMPARE(firstTabPage->activeViewContainer(), firstTabSecondary); + QVERIFY(firstTabSecondary->isActive()); + QVERIFY(!firstTabPage->primaryViewContainer()->isActive()); +} + void DolphinMainWindowTest::cleanupTestCase() { m_mainWindow->showNormal(); diff --git a/src/tests/kfileitemmodeltest.cpp b/src/tests/kfileitemmodeltest.cpp index b8e0e9893..f61bc246f 100644 --- a/src/tests/kfileitemmodeltest.cpp +++ b/src/tests/kfileitemmodeltest.cpp @@ -70,6 +70,8 @@ private Q_SLOTS: void testRemoveFilteredExpandedItems(); void testSorting(); void testNaturalSorting(); + void testStringCompare_data(); + void testStringCompare(); void testIndexForKeyboardSearch(); void testNameFilter(); void testEmptyPath(); @@ -94,6 +96,7 @@ private Q_SLOTS: void testInsertAfterExpand(); void testCurrentDirRemoved(); void testSizeSortingAfterRefresh(); + void testFilterModesAndCaseSensitivity(); private: QStringList itemsInModel() const; @@ -1356,6 +1359,41 @@ void KFileItemModelTest::testNaturalSorting() QCOMPARE(itemsMovedSpy.takeFirst().at(1).value<QList<int>>(), QList<int>() << 4 << 6 << 1 << 2 << 3 << 5); } +void KFileItemModelTest::testStringCompare_data() +{ + QTest::addColumn<QString>("left"); + QTest::addColumn<QString>("right"); + QTest::addColumn<bool>("naturalSorting"); + QTest::addColumn<int>("expectedResult"); + + QTest::newRow("decimal") << QStringLiteral("0.09") << QStringLiteral("0.1") << true << -1; + QTest::newRow("version-patch") << QStringLiteral("v1.2.3") << QStringLiteral("v1.2.10") << true << -1; + QTest::newRow("version-patch-reversed") << QStringLiteral("v1.2.10") << QStringLiteral("v1.2.3") << true << 1; + QTest::newRow("version-minor") << QStringLiteral("v1.2.10") << QStringLiteral("v1.10.1") << true << -1; + QTest::newRow("numeric-basename-before-extension") << QStringLiteral("1.09.txt") << QStringLiteral("1.1.txt") << true << -1; + QTest::newRow("leading-dot-is-not-decimal") << QStringLiteral(".1") << QStringLiteral(".09") << true << -1; + QTest::newRow("natural-sorting-disabled") << QStringLiteral("v1.2.10") << QStringLiteral("v1.2.3") << false << -1; +} + +void KFileItemModelTest::testStringCompare() +{ + QFETCH(QString, left); + QFETCH(QString, right); + QFETCH(bool, naturalSorting); + QFETCH(int, expectedResult); + + QCollator collator; + collator.setNumericMode(true); + + m_model->m_naturalSorting = naturalSorting; + + const int result = m_model->stringCompare(left, right, collator); + QCOMPARE(result < 0 ? -1 : result > 0 ? 1 : 0, expectedResult); + + const int reverseResult = m_model->stringCompare(right, left, collator); + QCOMPARE(reverseResult < 0 ? -1 : reverseResult > 0 ? 1 : 0, -expectedResult); +} + void KFileItemModelTest::testIndexForKeyboardSearch() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); @@ -2712,6 +2750,118 @@ void KFileItemModelTest::testCurrentDirRemoved() QCOMPARE(m_model->count(), 0); } +void KFileItemModelTest::testFilterModesAndCaseSensitivity() +{ + QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); + + m_testDir->createFiles({"1_abc.txt", "2_aBc.txt", "3_a[b]c.txt", "4_test.txt", "5_test.cpp"}); + + m_model->loadDirectory(m_testDir->url()); + QVERIFY(itemsInsertedSpy.wait()); + + m_model->setSortRole("text"); + QCOMPARE(itemsInModel(), + QStringList() << "1_abc.txt" + << "2_aBc.txt" + << "3_a[b]c.txt" + << "4_test.txt" + << "5_test.cpp"); + + // Glob + case insensitive + // no need to set the these options here as they are the default ones + m_model->setNameFilter("*a?c*"); + QCOMPARE(itemsInModel(), QStringList() << "1_abc.txt" << "2_aBc.txt"); + + m_model->setNameFilter("*.txt"); + QCOMPARE(itemsInModel(), QStringList() << "1_abc.txt" << "2_aBc.txt" << "3_a[b]c.txt" << "4_test.txt"); + + m_model->setNameFilter("*a*c*"); + QCOMPARE(itemsInModel(), QStringList() << "1_abc.txt" << "2_aBc.txt" << "3_a[b]c.txt"); + + // Glob + case sensitive + m_model->setFilterCaseSensitive(true); + + m_model->setNameFilter("*bc*"); + QCOMPARE(itemsInModel(), QStringList() << "1_abc.txt"); + + m_model->setNameFilter("*Bc*"); + QCOMPARE(itemsInModel(), QStringList() << "2_aBc.txt"); + + m_model->setNameFilter("*.TXT"); + QCOMPARE(itemsInModel(), QStringList()); + + // PlainText + case insensitive + m_model->setFilterMode(KFileItemModelFilter::PlainText); + m_model->setFilterCaseSensitive(false); + + m_model->setNameFilter("bc"); + QCOMPARE(itemsInModel(), QStringList() << "1_abc.txt" << "2_aBc.txt"); + + m_model->setNameFilter("[b]"); + QCOMPARE(itemsInModel(), QStringList() << "3_a[b]c.txt"); + + // PlainText + case sensitive + m_model->setFilterCaseSensitive(true); + + m_model->setNameFilter("bc"); + QCOMPARE(itemsInModel(), QStringList() << "1_abc.txt"); + + m_model->setNameFilter("Bc"); + QCOMPARE(itemsInModel(), QStringList() << "2_aBc.txt"); + + m_model->setNameFilter("[b]"); + QCOMPARE(itemsInModel(), QStringList() << "3_a[b]c.txt"); + + // Regex + case insensitive + m_model->setFilterMode(KFileItemModelFilter::Regex); + m_model->setFilterCaseSensitive(false); + + m_model->setNameFilter("a.c"); + QCOMPARE(itemsInModel(), QStringList() << "1_abc.txt" << "2_aBc.txt"); + + m_model->setNameFilter("a[a-z]c"); + QCOMPARE(itemsInModel(), QStringList() << "1_abc.txt" << "2_aBc.txt"); + + m_model->setNameFilter("a.*c"); + QCOMPARE(itemsInModel(), QStringList() << "1_abc.txt" << "2_aBc.txt" << "3_a[b]c.txt"); + + m_model->setNameFilter("\\.txt$"); + QCOMPARE(itemsInModel(), QStringList() << "1_abc.txt" << "2_aBc.txt" << "3_a[b]c.txt" << "4_test.txt"); + + // Regex + case sensitive + m_model->setFilterCaseSensitive(true); + + m_model->setNameFilter("a[a-z]c"); + QCOMPARE(itemsInModel(), QStringList() << "1_abc.txt"); + + m_model->setNameFilter("a[A-Z]c"); + QCOMPARE(itemsInModel(), QStringList() << "2_aBc.txt"); + + m_model->setNameFilter("a\\[b\\]c"); + QCOMPARE(itemsInModel(), QStringList() << "3_a[b]c.txt"); + + // Invalid Glob + m_model->setFilterMode(KFileItemModelFilter::Glob); + + m_model->setNameFilter("a["); + QCOMPARE(itemsInModel(), QStringList()); + + // Invalid Regex + m_model->setFilterMode(KFileItemModelFilter::Regex); + + m_model->setNameFilter("(abc"); + QCOMPARE(itemsInModel(), QStringList()); + + // Clear filter, very that all the items reappear + m_model->setNameFilter(QString()); + QCOMPARE(itemsInModel(), + QStringList() << "1_abc.txt" + << "2_aBc.txt" + << "3_a[b]c.txt" + << "4_test.txt" + << "5_test.cpp"); +} + QStringList KFileItemModelTest::itemsInModel() const { QStringList items; diff --git a/src/tests/testdir.cpp b/src/tests/testdir.cpp index 30ca6632e..453a29784 100644 --- a/src/tests/testdir.cpp +++ b/src/tests/testdir.cpp @@ -48,7 +48,9 @@ void TestDir::createFile(const QString &path, const QByteArray &data, const QDat makePathAbsoluteAndCreateParents(absolutePath); QFile f(absolutePath); - f.open(QIODevice::WriteOnly); + if (!f.open(QIODevice::WriteOnly)) { + qFatal() << "could not open" << absolutePath; + } f.write(data); f.close(); diff --git a/src/userfeedback/settingsdatasource.cpp b/src/userfeedback/settingsdatasource.cpp index c517793ad..6e34dbcda 100644 --- a/src/userfeedback/settingsdatasource.cpp +++ b/src/userfeedback/settingsdatasource.cpp @@ -31,22 +31,16 @@ QString SettingsDataSource::description() const QVariant SettingsDataSource::data() { - if (!m_mainWindow) { - // This assumes there is only one DolphinMainWindow per process. - const auto topLevelWidgets = QApplication::topLevelWidgets(); - for (const auto widget : topLevelWidgets) { - if (qobject_cast<DolphinMainWindow *>(widget)) { - m_mainWindow = static_cast<DolphinMainWindow *>(widget); - break; - } - } - } - QVariantMap map; - if (m_mainWindow) { - map.insert(QStringLiteral("informationPanelEnabled"), m_mainWindow->isInformationPanelEnabled()); - map.insert(QStringLiteral("foldersPanelEnabled"), m_mainWindow->isFoldersPanelEnabled()); + // This assumes there is only one DolphinMainWindow per process. + const auto topLevelWidgets = QApplication::topLevelWidgets(); + for (const auto widget : topLevelWidgets) { + if (auto mainWindow = qobject_cast<DolphinMainWindow *>(widget)) { + map.insert(QStringLiteral("informationPanelEnabled"), mainWindow->isInformationPanelEnabled()); + map.insert(QStringLiteral("foldersPanelEnabled"), mainWindow->isFoldersPanelEnabled()); + break; + } } map.insert(QStringLiteral("tooltipsEnabled"), GeneralSettings::showToolTips()); diff --git a/src/userfeedback/settingsdatasource.h b/src/userfeedback/settingsdatasource.h index 9804c78a7..bd1cec015 100644 --- a/src/userfeedback/settingsdatasource.h +++ b/src/userfeedback/settingsdatasource.h @@ -19,9 +19,6 @@ public: QString name() const override; QString description() const override; QVariant data() override; - -private: - DolphinMainWindow *m_mainWindow = nullptr; }; #endif // SETTINGSDATASOURCE_H diff --git a/src/views/dolphinitemlistview.cpp b/src/views/dolphinitemlistview.cpp index f9e710a31..53d1c6c34 100644 --- a/src/views/dolphinitemlistview.cpp +++ b/src/views/dolphinitemlistview.cpp @@ -45,13 +45,22 @@ void DolphinItemListView::setZoomLevel(int level) m_zoomLevel = level; + const bool useGlobalViewProps = GeneralSettings::globalViewProps(); ViewModeSettings settings(itemLayout()); + if (previewsShown()) { - const int previewSize = ZoomLevelInfo::iconSizeForZoomLevel(level); - settings.setPreviewSize(previewSize); + m_previewSize = ZoomLevelInfo::iconSizeForZoomLevel(level); + // Only update the icon size settings if we're using global view props + // to prevent inconsistent state on zoom level changes + if (useGlobalViewProps) { + settings.setPreviewSize(m_previewSize); + } } else { - const int iconSize = ZoomLevelInfo::iconSizeForZoomLevel(level); - settings.setIconSize(iconSize); + // Same as above + m_iconSize = ZoomLevelInfo::iconSizeForZoomLevel(level); + if (useGlobalViewProps) { + settings.setIconSize(m_iconSize); + } } updateGridSize(); @@ -167,9 +176,11 @@ void DolphinItemListView::updateFont() void DolphinItemListView::updateGridSize() { const ViewModeSettings settings(itemLayout()); + const bool useGlobalViewProps = GeneralSettings::globalViewProps(); // Calculate the size of the icon - const int iconSize = previewsShown() ? settings.previewSize() : settings.iconSize(); + // Only use zoom stored in settings if we're using global view props + const int iconSize = useGlobalViewProps ? (previewsShown() ? settings.previewSize() : settings.iconSize()) : (previewsShown() ? m_previewSize : m_iconSize); m_zoomLevel = ZoomLevelInfo::zoomLevelForIconSize(QSize(iconSize, iconSize)); KItemListStyleOption option = styleOption(); @@ -190,7 +201,7 @@ void DolphinItemListView::updateGridSize() // 9 is the average char width for 10pt Noto Sans, making fontFactor =1 // by each pixel the font gets larger the factor increases by 1/9 auto fontFactor = option.fontMetrics.averageCharWidth() / 9.0; - itemWidth = 48 + IconsModeSettings::textWidthIndex() * 64 * fontFactor * zoomFactor; + itemWidth = 16 + IconsModeSettings::textWidthIndex() * 64 * fontFactor * zoomFactor; if (itemWidth < iconSize + padding * 2 * zoomFactor) { itemWidth = iconSize + padding * 2 * zoomFactor; diff --git a/src/views/dolphinitemlistview.h b/src/views/dolphinitemlistview.h index 0483c0644..7ac7a4fad 100644 --- a/src/views/dolphinitemlistview.h +++ b/src/views/dolphinitemlistview.h @@ -62,6 +62,8 @@ private: private: int m_zoomLevel; + int m_previewSize = 0; + int m_iconSize = 0; }; #endif diff --git a/src/views/dolphinview.cpp b/src/views/dolphinview.cpp index b01833d69..3c973618c 100644 --- a/src/views/dolphinview.cpp +++ b/src/views/dolphinview.cpp @@ -130,6 +130,25 @@ DolphinView::DolphinView(const QUrl &url, QWidget *parent) KItemListController *controller = new KItemListController(m_model, m_view, this); controller->setAutoActivationEnabled(GeneralSettings::autoExpandFolders()); connect(controller, &KItemListController::doubleClickViewBackground, this, &DolphinView::doubleClickViewBackground); + connect(controller, &KItemListController::typeAheadUsed, this, [this](const QString &typedString, std::optional<int> foundIndex) { + if (foundIndex.has_value()) { + const KFileItem item = m_model->fileItem(foundIndex.value()); + if (item.isNull()) { + return; + } + const KColorScheme colorScheme = KColorScheme(QPalette::Normal, KColorScheme::Tooltip); + const QColor autoCompleteTextColor = colorScheme.foreground(KColorScheme::InactiveText).color(); + + Q_EMIT showTypeAheadFeedback(QStringLiteral("%1<font color=\"%2\">%3</font>") + .arg(typedString.toHtmlEscaped()) + .arg(autoCompleteTextColor.name()) + .arg(item.name().toHtmlEscaped().mid(typedString.size()))); + } else { + const KColorScheme colorScheme = KColorScheme(QPalette::Normal, KColorScheme::Tooltip); + const QColor noMatchTextColor = colorScheme.foreground(KColorScheme::NegativeText).color(); + Q_EMIT showTypeAheadFeedback(QStringLiteral("<font color=\"%1\">%2</font>").arg(noMatchTextColor.name()).arg(typedString.toHtmlEscaped())); + } + }); // The EnlargeSmallPreviews setting can only be changed after the model // has been set in the view by KItemListController. @@ -479,9 +498,16 @@ void DolphinView::selectItems(const QRegularExpression ®exp, bool enabled) void DolphinView::setZoomLevel(int level) { + if (m_defaultZoomLevel < 0) { + updateDefaultZoomLevel(); + } + const int oldZoomLevel = zoomLevel(); m_view->setZoomLevel(level); if (zoomLevel() != oldZoomLevel) { + ViewProperties props(viewPropertiesUrl()); + props.setZoomLevel(level == m_defaultZoomLevel ? -1 : level); + hideToolTip(); Q_EMIT zoomLevelChanged(zoomLevel(), oldZoomLevel); } @@ -632,6 +658,26 @@ QStringList DolphinView::mimeTypeFilters() const return m_model->mimeTypeFilters(); } +void DolphinView::setFilterMode(KFileItemModelFilter::FilterMode mode) +{ + m_model->setFilterMode(mode); +} + +KFileItemModelFilter::FilterMode DolphinView::filterMode() const +{ + return m_model->filterMode(); +} + +void DolphinView::setFilterCaseSensitive(bool caseSensitive) +{ + m_model->setFilterCaseSensitive(caseSensitive); +} + +bool DolphinView::isFilterCaseSensitive() const +{ + return m_model->isFilterCaseSensitive(); +} + void DolphinView::requestStatusBarText() { if (m_statJobForStatusBarText) { @@ -1798,10 +1844,10 @@ Qt::SortOrder DolphinView::defaultSortOrderForRole(const QByteArray &role) void DolphinView::resetZoomLevel() { - ViewModeSettings settings{m_mode}; - const int userDefaultIconSize = settings.iconSize(); - - setZoomLevel(ZoomLevelInfo::zoomLevelForIconSize(QSize(userDefaultIconSize, userDefaultIconSize))); + if (m_defaultZoomLevel < 0) { + updateDefaultZoomLevel(); + } + setZoomLevel(m_defaultZoomLevel); } void DolphinView::selectFileOnceAvailable(const QUrl &url, const std::function<bool()> &condition) @@ -2312,11 +2358,16 @@ void DolphinView::applyViewProperties(const ViewProperties &props) { m_view->beginTransaction(); + // Caches old zoom level change for signal emitting + int zoomLevelChangeFrom = -1; + const Mode mode = props.viewMode(); if (m_mode != mode) { const Mode previousMode = m_mode; m_mode = mode; + updateDefaultZoomLevel(); + // Changing the mode might result in changing // the zoom level. Remember the old zoom level so // that zoomLevelChanged() can get emitted. @@ -2326,7 +2377,7 @@ void DolphinView::applyViewProperties(const ViewProperties &props) Q_EMIT modeChanged(m_mode, previousMode); if (m_view->zoomLevel() != oldZoomLevel) { - Q_EMIT zoomLevelChanged(m_view->zoomLevel(), oldZoomLevel); + zoomLevelChangeFrom = oldZoomLevel; } } @@ -2383,10 +2434,28 @@ void DolphinView::applyViewProperties(const ViewProperties &props) // Changing the preview-state might result in a changed zoom-level if (oldZoomLevel != zoomLevel()) { - Q_EMIT zoomLevelChanged(zoomLevel(), oldZoomLevel); + zoomLevelChangeFrom = oldZoomLevel; + } + } + + // Only check for folder zoom changes if we're using local view props + if (!GeneralSettings::globalViewProps()) { + const int propZoomLevel = props.zoomLevel(); + if (m_defaultZoomLevel < 0) { + updateDefaultZoomLevel(); + } + + const int nextZoomLevel = propZoomLevel < 0 ? m_defaultZoomLevel : propZoomLevel; + if (nextZoomLevel != m_view->zoomLevel()) { + zoomLevelChangeFrom = m_view->zoomLevel(); + m_view->setZoomLevel(nextZoomLevel); } } + if (zoomLevelChangeFrom > -1) { + Q_EMIT zoomLevelChanged(m_view->zoomLevel(), zoomLevelChangeFrom); + } + KItemListView *itemListView = m_container->controller()->view(); if (itemListView->isHeaderVisible()) { KItemListHeader *header = itemListView->header(); @@ -2409,6 +2478,14 @@ void DolphinView::applyViewProperties(const ViewProperties &props) m_view->endTransaction(); } +void DolphinView::updateDefaultZoomLevel() +{ + ViewModeSettings settings{m_mode}; + const int userDefaultIconSize = settings.iconSize(); + + m_defaultZoomLevel = ZoomLevelInfo::zoomLevelForIconSize(QSize(userDefaultIconSize, userDefaultIconSize)); +} + void DolphinView::applyModeToView() { switch (m_mode) { @@ -2694,6 +2771,12 @@ void DolphinView::updatePlaceholderLabel() m_placeholderLabel->setText(i18n("No MTP-compatible devices found")); } else if (m_url.scheme() == QLatin1String("afc") && m_url.path() == QLatin1String("/")) { m_placeholderLabel->setText(i18n("No Apple devices found")); + } else if (m_url.scheme() == QLatin1String("kdeconnect") && m_url.path() == QLatin1String("/")) { + if (m_url.host().isEmpty()) { + m_placeholderLabel->setText(i18n("No KDE Connect devices found")); + } else { + m_placeholderLabel->setText(i18n("No storage found on this device")); + } } else if (m_url.scheme() == QLatin1String("bluetooth")) { m_placeholderLabel->setText(i18n("No Bluetooth devices found")); } else { diff --git a/src/views/dolphinview.h b/src/views/dolphinview.h index e3f83979c..9b068f92e 100644 --- a/src/views/dolphinview.h +++ b/src/views/dolphinview.h @@ -10,6 +10,7 @@ #include "dolphin_export.h" #include "dolphintabwidget.h" +#include "kitemviews/private/kfileitemmodelfilter.h" #include "tooltips/tooltipmanager.h" #include "config-dolphin.h" @@ -277,6 +278,18 @@ public: QStringList mimeTypeFilters() const; /** + * Sets the filtering mode of the currently used nameFilter. + */ + void setFilterMode(KFileItemModelFilter::FilterMode mode); + KFileItemModelFilter::FilterMode filterMode() const; + + /** + * Enables or disable the caseSensitive matching of the currently used nameFilter. + */ + void setFilterCaseSensitive(bool caseSensitive); + bool isFilterCaseSensitive() const; + + /** * Tells the view to generate an updated status bar text. The result * is returned through the statusBarTextChanged(QString statusBarText) signal. * It will carry a textual representation of the state of the current @@ -622,6 +635,11 @@ Q_SIGNALS: void operationCompletedMessage(const QString &msg); /** + * Is emitted so the \a typeAheadFeedback is displayed to the user. Beware: \a typeAheadFeedback is HTML-escaped rich text. + */ + void showTypeAheadFeedback(const QString &typeAheadFeedback); + + /** * Is emitted after DolphinView::setUrl() has been invoked and * the current directory is loaded. If this signal is emitted, * it is assured that the view contains already the correct root @@ -791,6 +809,11 @@ private Q_SLOTS: void updateSortFoldersFirst(bool foldersFirst); /** + * Cache out zoom mode changes to prevent constant Settings requests + */ + void updateDefaultZoomLevel(); + + /** * Updates the view properties of the current URL to the * sorting of hidden files given by \a hiddenLast. */ @@ -1007,6 +1030,9 @@ private: // resolution scroll wheels) int m_controlWheelAccumulatedDelta; + // Cached out default zoom level after view mode changes + int m_defaultZoomLevel = -1; + QList<QUrl> m_selectedUrls; // Used for making the view to remember selections after F5 and file operations bool m_clearSelectionBeforeSelectingNewItems; bool m_markFirstNewlySelectedItemAsCurrent; diff --git a/src/views/viewproperties.cpp b/src/views/viewproperties.cpp index 03ab2bd58..3d33a805b 100644 --- a/src/views/viewproperties.cpp +++ b/src/views/viewproperties.cpp @@ -229,6 +229,7 @@ ViewProperties::ViewProperties(const QUrl &url) } else { m_changedProps = false; } + setZoomLevel(-1); } if (m_node->version() < CurrentViewPropertiesVersion) { @@ -269,6 +270,19 @@ ViewProperties::~ViewProperties() m_node = nullptr; } +void ViewProperties::setZoomLevel(int zoomLevel) +{ + if (m_node->zoomLevel() != zoomLevel) { + m_node->setZoomLevel(zoomLevel); + update(); + } +} + +int ViewProperties::zoomLevel() const +{ + return m_node->zoomLevel(); +} + void ViewProperties::setViewMode(DolphinView::Mode mode) { if (m_node->viewMode() != mode) { @@ -487,6 +501,7 @@ QList<int> ViewProperties::headerColumnWidths() const void ViewProperties::setDirProperties(const ViewProperties &props) { setViewMode(props.viewMode()); + setZoomLevel(props.zoomLevel()); setPreviewsShown(props.previewsShown()); setHiddenFilesShown(props.hiddenFilesShown()); setGroupedSorting(props.groupedSorting()); diff --git a/src/views/viewproperties.h b/src/views/viewproperties.h index bee1e7330..622a1c87e 100644 --- a/src/views/viewproperties.h +++ b/src/views/viewproperties.h @@ -44,6 +44,10 @@ public: void setViewMode(DolphinView::Mode mode); DolphinView::Mode viewMode() const; + void setZoomLevel(int zoomLevel); + /// -1 is the default zoom + int zoomLevel() const; + void setPreviewsShown(bool show); bool previewsShown() const; diff --git a/src/views/zoomwidgetaction.cpp b/src/views/zoomwidgetaction.cpp index 431ac4f62..2260d9754 100644 --- a/src/views/zoomwidgetaction.cpp +++ b/src/views/zoomwidgetaction.cpp @@ -61,9 +61,13 @@ ZoomWidgetAction::ZoomWidgetAction(QAction *zoomInAction, QAction *zoomResetActi { // This is a property that KXMLGui reads to determine whether this action // should be included in the shortcut configuration UI + // they are already added to the action collection setProperty("isShortcutConfigurable", false); setPopupMode(InstantPopup); popupMenu()->addActions({zoomInAction, zoomResetAction, zoomOutAction}); + + // but this should be accessible through command bar + setMenu(popupMenu()); } bool ZoomWidgetAction::eventFilter(QObject *object, QEvent *event) |
