diff --git a/src/dbusinterface.cpp b/src/dbusinterface.cpp index 15016d2e0..fd2d229a2 100644 --- a/src/dbusinterface.cpp +++ b/src/dbusinterface.cpp @@ -1,73 +1,73 @@ /* * Copyright 2015 Ashish Bansal * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "dbusinterface.h" #include "global.h" #include "dolphin_generalsettings.h" #include #include #include #include #include DBusInterface::DBusInterface() : QObject() { QDBusConnection::sessionBus().registerObject(QStringLiteral("/org/freedesktop/FileManager1"), this, QDBusConnection::ExportScriptableContents | QDBusConnection::ExportAdaptors); QDBusConnection::sessionBus().interface()->registerService(QStringLiteral("org.freedesktop.FileManager1"), QDBusConnectionInterface::QueueService); } void DBusInterface::ShowFolders(const QStringList& uriList, const QString& startUpId) { - Q_UNUSED(startUpId); + Q_UNUSED(startUpId) const QList urls = Dolphin::validateUris(uriList); if (urls.isEmpty()) { return; } const auto serviceName = QStringLiteral("org.kde.dolphin-%1").arg(QCoreApplication::applicationPid()); if(!Dolphin::attachToExistingInstance(urls, false, GeneralSettings::splitView(), serviceName)) { Dolphin::openNewWindow(urls); } } void DBusInterface::ShowItems(const QStringList& uriList, const QString& startUpId) { - Q_UNUSED(startUpId); + Q_UNUSED(startUpId) const QList urls = Dolphin::validateUris(uriList); if (urls.isEmpty()) { return; } const auto serviceName = QStringLiteral("org.kde.dolphin-%1").arg(QCoreApplication::applicationPid()); if(!Dolphin::attachToExistingInstance(urls, true, GeneralSettings::splitView(), serviceName)) { Dolphin::openNewWindow(urls, nullptr, Dolphin::OpenNewWindowFlag::Select); }; } void DBusInterface::ShowItemProperties(const QStringList& uriList, const QString& startUpId) { - Q_UNUSED(startUpId); + Q_UNUSED(startUpId) const QList urls = Dolphin::validateUris(uriList); if (!urls.isEmpty()) { KPropertiesDialog::showDialog(urls); } } diff --git a/src/dolphintabpage.cpp b/src/dolphintabpage.cpp index 6dae730eb..88b4b726e 100644 --- a/src/dolphintabpage.cpp +++ b/src/dolphintabpage.cpp @@ -1,381 +1,381 @@ /*************************************************************************** * Copyright (C) 2014 by Emmanuel Pescosta * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "dolphintabpage.h" #include "dolphin_generalsettings.h" #include "dolphinviewcontainer.h" #include #include DolphinTabPage::DolphinTabPage(const QUrl &primaryUrl, const QUrl &secondaryUrl, QWidget* parent) : QWidget(parent), m_primaryViewActive(true), m_splitViewEnabled(false), m_active(true) { QVBoxLayout* layout = new QVBoxLayout(this); layout->setSpacing(0); layout->setContentsMargins(0, 0, 0, 0); m_splitter = new QSplitter(Qt::Horizontal, this); m_splitter->setChildrenCollapsible(false); layout->addWidget(m_splitter); // Create a new primary view m_primaryViewContainer = createViewContainer(primaryUrl); connect(m_primaryViewContainer->view(), &DolphinView::urlChanged, this, &DolphinTabPage::activeViewUrlChanged); connect(m_primaryViewContainer->view(), &DolphinView::redirection, this, &DolphinTabPage::slotViewUrlRedirection); m_splitter->addWidget(m_primaryViewContainer); m_primaryViewContainer->show(); if (secondaryUrl.isValid() || GeneralSettings::splitView()) { // Provide a secondary view, if the given secondary url is valid or if the // startup settings are set this way (use the url of the primary view). m_splitViewEnabled = true; const QUrl& url = secondaryUrl.isValid() ? secondaryUrl : primaryUrl; m_secondaryViewContainer = createViewContainer(url); m_splitter->addWidget(m_secondaryViewContainer); m_secondaryViewContainer->show(); } m_primaryViewContainer->setActive(true); } bool DolphinTabPage::primaryViewActive() const { return m_primaryViewActive; } bool DolphinTabPage::splitViewEnabled() const { return m_splitViewEnabled; } void DolphinTabPage::setSplitViewEnabled(bool enabled, const QUrl &secondaryUrl) { if (m_splitViewEnabled != enabled) { m_splitViewEnabled = enabled; if (enabled) { const QUrl& url = (secondaryUrl.isEmpty()) ? m_primaryViewContainer->url() : secondaryUrl; m_secondaryViewContainer = createViewContainer(url); const bool placesSelectorVisible = m_primaryViewContainer->urlNavigator()->isPlacesSelectorVisible(); m_secondaryViewContainer->urlNavigator()->setPlacesSelectorVisible(placesSelectorVisible); m_splitter->addWidget(m_secondaryViewContainer); m_secondaryViewContainer->show(); m_secondaryViewContainer->setActive(true); } else { DolphinViewContainer* view; if (GeneralSettings::closeActiveSplitView()) { view = activeViewContainer(); if (m_primaryViewActive) { // If the primary view is active, we have to swap the pointers // because the secondary view will be the new primary view. qSwap(m_primaryViewContainer, m_secondaryViewContainer); m_primaryViewActive = false; } } else { view = m_primaryViewActive ? m_secondaryViewContainer : m_primaryViewContainer; if (!m_primaryViewActive) { // If the secondary view is active, we have to swap the pointers // because the secondary view will be the new primary view. qSwap(m_primaryViewContainer, m_secondaryViewContainer); m_primaryViewActive = true; } } m_primaryViewContainer->setActive(true); view->close(); view->deleteLater(); } } } DolphinViewContainer* DolphinTabPage::primaryViewContainer() const { return m_primaryViewContainer; } DolphinViewContainer* DolphinTabPage::secondaryViewContainer() const { return m_secondaryViewContainer; } DolphinViewContainer* DolphinTabPage::activeViewContainer() const { return m_primaryViewActive ? m_primaryViewContainer : m_secondaryViewContainer; } KFileItemList DolphinTabPage::selectedItems() const { KFileItemList items = m_primaryViewContainer->view()->selectedItems(); if (m_splitViewEnabled) { items += m_secondaryViewContainer->view()->selectedItems(); } return items; } int DolphinTabPage::selectedItemsCount() const { int selectedItemsCount = m_primaryViewContainer->view()->selectedItemsCount(); if (m_splitViewEnabled) { selectedItemsCount += m_secondaryViewContainer->view()->selectedItemsCount(); } return selectedItemsCount; } void DolphinTabPage::markUrlsAsSelected(const QList& urls) { m_primaryViewContainer->view()->markUrlsAsSelected(urls); if (m_splitViewEnabled) { m_secondaryViewContainer->view()->markUrlsAsSelected(urls); } } void DolphinTabPage::markUrlAsCurrent(const QUrl& url) { m_primaryViewContainer->view()->markUrlAsCurrent(url); if (m_splitViewEnabled) { m_secondaryViewContainer->view()->markUrlAsCurrent(url); } } void DolphinTabPage::setPlacesSelectorVisible(bool visible) { m_primaryViewContainer->urlNavigator()->setPlacesSelectorVisible(visible); if (m_splitViewEnabled) { m_secondaryViewContainer->urlNavigator()->setPlacesSelectorVisible(visible); } } void DolphinTabPage::refreshViews() { m_primaryViewContainer->readSettings(); if (m_splitViewEnabled) { m_secondaryViewContainer->readSettings(); } } QByteArray DolphinTabPage::saveState() const { QByteArray state; QDataStream stream(&state, QIODevice::WriteOnly); stream << quint32(2); // Tab state version stream << m_splitViewEnabled; stream << m_primaryViewContainer->url(); stream << m_primaryViewContainer->urlNavigator()->isUrlEditable(); m_primaryViewContainer->view()->saveState(stream); if (m_splitViewEnabled) { stream << m_secondaryViewContainer->url(); stream << m_secondaryViewContainer->urlNavigator()->isUrlEditable(); m_secondaryViewContainer->view()->saveState(stream); } stream << m_primaryViewActive; stream << m_splitter->saveState(); return state; } void DolphinTabPage::restoreState(const QByteArray& state) { if (state.isEmpty()) { return; } QByteArray sd = state; QDataStream stream(&sd, QIODevice::ReadOnly); // Read the version number of the tab state and check if the version is supported. quint32 version = 0; stream >> version; if (version != 2) { // The version of the tab state isn't supported, we can't restore it. return; } bool isSplitViewEnabled = false; stream >> isSplitViewEnabled; setSplitViewEnabled(isSplitViewEnabled); QUrl primaryUrl; stream >> primaryUrl; m_primaryViewContainer->setUrl(primaryUrl); bool primaryUrlEditable; stream >> primaryUrlEditable; m_primaryViewContainer->urlNavigator()->setUrlEditable(primaryUrlEditable); m_primaryViewContainer->view()->restoreState(stream); if (isSplitViewEnabled) { QUrl secondaryUrl; stream >> secondaryUrl; m_secondaryViewContainer->setUrl(secondaryUrl); bool secondaryUrlEditable; stream >> secondaryUrlEditable; m_secondaryViewContainer->urlNavigator()->setUrlEditable(secondaryUrlEditable); m_secondaryViewContainer->view()->restoreState(stream); } stream >> m_primaryViewActive; if (m_primaryViewActive) { m_primaryViewContainer->setActive(true); } else { Q_ASSERT(m_splitViewEnabled); m_secondaryViewContainer->setActive(true); } QByteArray splitterState; stream >> splitterState; m_splitter->restoreState(splitterState); } void DolphinTabPage::restoreStateV1(const QByteArray& state) { if (state.isEmpty()) { return; } QByteArray sd = state; QDataStream stream(&sd, QIODevice::ReadOnly); bool isSplitViewEnabled = false; stream >> isSplitViewEnabled; setSplitViewEnabled(isSplitViewEnabled); QUrl primaryUrl; stream >> primaryUrl; m_primaryViewContainer->setUrl(primaryUrl); bool primaryUrlEditable; stream >> primaryUrlEditable; m_primaryViewContainer->urlNavigator()->setUrlEditable(primaryUrlEditable); if (isSplitViewEnabled) { QUrl secondaryUrl; stream >> secondaryUrl; m_secondaryViewContainer->setUrl(secondaryUrl); bool secondaryUrlEditable; stream >> secondaryUrlEditable; m_secondaryViewContainer->urlNavigator()->setUrlEditable(secondaryUrlEditable); } stream >> m_primaryViewActive; if (m_primaryViewActive) { m_primaryViewContainer->setActive(true); } else { Q_ASSERT(m_splitViewEnabled); m_secondaryViewContainer->setActive(true); } QByteArray splitterState; stream >> splitterState; m_splitter->restoreState(splitterState); } void DolphinTabPage::setActive(bool active) { if (active) { m_active = active; } else { // 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 activeViewContainer()->setActive(active); } void DolphinTabPage::slotViewActivated() { const DolphinView* oldActiveView = activeViewContainer()->view(); // Set the view, which was active before, to inactive // and update the active view type, if tab is active if (m_active) { if (m_splitViewEnabled) { activeViewContainer()->setActive(false); m_primaryViewActive = !m_primaryViewActive; } else { m_primaryViewActive = true; if (m_secondaryViewContainer) { m_secondaryViewContainer->setActive(false); } } } const DolphinView* newActiveView = activeViewContainer()->view(); if (newActiveView == oldActiveView) { return; } disconnect(oldActiveView, &DolphinView::urlChanged, this, &DolphinTabPage::activeViewUrlChanged); disconnect(oldActiveView, &DolphinView::redirection, this, &DolphinTabPage::slotViewUrlRedirection); connect(newActiveView, &DolphinView::urlChanged, this, &DolphinTabPage::activeViewUrlChanged); connect(newActiveView, &DolphinView::redirection, this, &DolphinTabPage::slotViewUrlRedirection); emit activeViewChanged(activeViewContainer()); emit activeViewUrlChanged(activeViewContainer()->url()); } void DolphinTabPage::slotViewUrlRedirection(const QUrl& oldUrl, const QUrl& newUrl) { - Q_UNUSED(oldUrl); + Q_UNUSED(oldUrl) emit activeViewUrlChanged(newUrl); } void DolphinTabPage::switchActiveView() { if (!m_splitViewEnabled) { return; } if (m_primaryViewActive) { m_secondaryViewContainer->setActive(true); } else { m_primaryViewContainer->setActive(true); } } DolphinViewContainer* DolphinTabPage::createViewContainer(const QUrl& url) const { DolphinViewContainer* container = new DolphinViewContainer(url, m_splitter); container->setActive(false); const DolphinView* view = container->view(); connect(view, &DolphinView::activated, this, &DolphinTabPage::slotViewActivated); connect(view, &DolphinView::toggleActiveViewRequested, this, &DolphinTabPage::switchActiveView); return container; } diff --git a/src/dolphinviewcontainer.cpp b/src/dolphinviewcontainer.cpp index 41706288c..a7587a91c 100644 --- a/src/dolphinviewcontainer.cpp +++ b/src/dolphinviewcontainer.cpp @@ -1,810 +1,810 @@ /*************************************************************************** * Copyright (C) 2007 by Peter Penz * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "dolphinviewcontainer.h" #include "dolphin_generalsettings.h" #include "dolphinplacesmodelsingleton.h" #include "dolphindebug.h" #include "filterbar/filterbar.h" #include "global.h" #include "search/dolphinsearchbox.h" #include "statusbar/dolphinstatusbar.h" #include "trash/dolphintrash.h" #include "views/viewmodecontroller.h" #include "views/viewproperties.h" #ifdef HAVE_KACTIVITIES #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include DolphinViewContainer::DolphinViewContainer(const QUrl& url, QWidget* parent) : QWidget(parent), m_topLayout(nullptr), m_navigatorWidget(nullptr), m_urlNavigator(nullptr), m_emptyTrashButton(nullptr), m_searchBox(nullptr), m_searchModeEnabled(false), m_messageWidget(nullptr), m_view(nullptr), m_filterBar(nullptr), m_statusBar(nullptr), m_statusBarTimer(nullptr), m_statusBarTimestamp(), m_autoGrabFocus(true) #ifdef HAVE_KACTIVITIES , m_activityResourceInstance(nullptr) #endif { hide(); m_topLayout = new QVBoxLayout(this); m_topLayout->setSpacing(0); m_topLayout->setContentsMargins(0, 0, 0, 0); m_navigatorWidget = new QWidget(this); QHBoxLayout* navigatorLayout = new QHBoxLayout(m_navigatorWidget); navigatorLayout->setSpacing(0); navigatorLayout->setContentsMargins(0, 0, 0, 0); m_navigatorWidget->setWhatsThis(xi18nc("@info:whatsthis location bar", "This line describes the location of the files and folders " "displayed below.The name of the currently viewed " "folder can be read at the very right. To the left of it is the " "name of the folder that contains it. The whole line is called " "the path to the current location because " "following these folders from left to right leads here." "The path is displayed on the location bar " "which is more powerful than one would expect. To learn more " "about the basic and advanced features of the location bar " "click here. " "This will open the dedicated page in the Handbook.")); m_urlNavigator = new KUrlNavigator(DolphinPlacesModelSingleton::instance().placesModel(), url, this); connect(m_urlNavigator, &KUrlNavigator::activated, this, &DolphinViewContainer::activate); connect(m_urlNavigator->editor(), &KUrlComboBox::completionModeChanged, this, &DolphinViewContainer::saveUrlCompletionMode); const GeneralSettings* settings = GeneralSettings::self(); m_urlNavigator->setUrlEditable(settings->editableUrl()); m_urlNavigator->setShowFullPath(settings->showFullPath()); m_urlNavigator->setHomeUrl(Dolphin::homeUrl()); KUrlComboBox* editor = m_urlNavigator->editor(); editor->setCompletionMode(KCompletion::CompletionMode(settings->urlCompletionMode())); m_emptyTrashButton = new QPushButton(QIcon::fromTheme(QStringLiteral("user-trash")), i18nc("@action:button", "Empty Trash"), this); m_emptyTrashButton->setFlat(true); connect(m_emptyTrashButton, &QPushButton::clicked, this, [this]() { Trash::empty(this); }); connect(&Trash::instance(), &Trash::emptinessChanged, m_emptyTrashButton, &QPushButton::setDisabled); m_emptyTrashButton->setDisabled(Trash::isEmpty()); m_emptyTrashButton->hide(); m_searchBox = new DolphinSearchBox(this); m_searchBox->hide(); connect(m_searchBox, &DolphinSearchBox::activated, this, &DolphinViewContainer::activate); connect(m_searchBox, &DolphinSearchBox::closeRequest, this, &DolphinViewContainer::closeSearchBox); connect(m_searchBox, &DolphinSearchBox::searchRequest, this, &DolphinViewContainer::startSearching); connect(m_searchBox, &DolphinSearchBox::returnPressed, this, &DolphinViewContainer::requestFocus); m_searchBox->setWhatsThis(xi18nc("@info:whatsthis findbar", "This helps you find files and folders. Enter a " "search term and specify search settings with the " "buttons at the bottom:Filename/Content: " "Does the item you are looking for contain the search terms " "within its filename or its contents?The contents of images, " "audio files and videos will not be searched." "From Here/Everywhere: Do you want to search in this " "folder and its sub-folders or everywhere?" "More Options: Click this to search by media type, access " "time or rating.More Search Tools: Install other " "means to find an item.")); m_messageWidget = new KMessageWidget(this); m_messageWidget->setCloseButtonVisible(true); m_messageWidget->hide(); #ifndef Q_OS_WIN if (getuid() == 0) { // We must be logged in as the root user; show a big scary warning showMessage(i18n("Running Dolphin as root can be dangerous. Please be careful."), Warning); } #endif // Initialize filter bar m_filterBar = new FilterBar(this); m_filterBar->setVisible(settings->filterBar()); connect(m_filterBar, &FilterBar::filterChanged, this, &DolphinViewContainer::setNameFilter); connect(m_filterBar, &FilterBar::closeRequest, this, &DolphinViewContainer::closeFilterBar); connect(m_filterBar, &FilterBar::focusViewRequest, this, &DolphinViewContainer::requestFocus); // Initialize the main view m_view = new DolphinView(url, this); connect(m_view, &DolphinView::urlChanged, m_filterBar, &FilterBar::slotUrlChanged); connect(m_view, &DolphinView::urlChanged, m_urlNavigator, &KUrlNavigator::setLocationUrl); connect(m_view, &DolphinView::urlChanged, m_messageWidget, &KMessageWidget::hide); connect(m_view, &DolphinView::writeStateChanged, this, &DolphinViewContainer::writeStateChanged); connect(m_view, &DolphinView::requestItemInfo, this, &DolphinViewContainer::showItemInfo); connect(m_view, &DolphinView::itemActivated, this, &DolphinViewContainer::slotItemActivated); connect(m_view, &DolphinView::itemsActivated, this, &DolphinViewContainer::slotItemsActivated); connect(m_view, &DolphinView::redirection, this, &DolphinViewContainer::redirect); connect(m_view, &DolphinView::directoryLoadingStarted, this, &DolphinViewContainer::slotDirectoryLoadingStarted); connect(m_view, &DolphinView::directoryLoadingCompleted, this, &DolphinViewContainer::slotDirectoryLoadingCompleted); connect(m_view, &DolphinView::directoryLoadingCanceled, this, &DolphinViewContainer::slotDirectoryLoadingCanceled); connect(m_view, &DolphinView::itemCountChanged, this, &DolphinViewContainer::delayedStatusBarUpdate); connect(m_view, &DolphinView::directoryLoadingProgress, this, &DolphinViewContainer::updateDirectoryLoadingProgress); connect(m_view, &DolphinView::directorySortingProgress, this, &DolphinViewContainer::updateDirectorySortingProgress); connect(m_view, &DolphinView::selectionChanged, this, &DolphinViewContainer::delayedStatusBarUpdate); connect(m_view, &DolphinView::errorMessage, this, &DolphinViewContainer::showErrorMessage); connect(m_view, &DolphinView::urlIsFileError, this, &DolphinViewContainer::slotUrlIsFileError); connect(m_view, &DolphinView::activated, this, &DolphinViewContainer::activate); connect(m_urlNavigator, &KUrlNavigator::urlAboutToBeChanged, this, &DolphinViewContainer::slotUrlNavigatorLocationAboutToBeChanged); connect(m_urlNavigator, &KUrlNavigator::urlChanged, this, &DolphinViewContainer::slotUrlNavigatorLocationChanged); connect(m_urlNavigator, &KUrlNavigator::urlSelectionRequested, this, &DolphinViewContainer::slotUrlSelectionRequested); connect(m_urlNavigator, &KUrlNavigator::returnPressed, this, &DolphinViewContainer::slotReturnPressed); connect(m_urlNavigator, &KUrlNavigator::urlsDropped, this, [=](const QUrl &destination, QDropEvent *event) { m_view->dropUrls(destination, event, m_urlNavigator->dropWidget()); }); connect(m_view, &DolphinView::directoryLoadingCompleted, this, [this]() { m_emptyTrashButton->setVisible(m_view->url().scheme() == QLatin1String("trash")); }); // Initialize status bar m_statusBar = new DolphinStatusBar(this); m_statusBar->setUrl(m_view->url()); 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_statusBar, &DolphinStatusBar::stopPressed, this, &DolphinViewContainer::stopDirectoryLoading); connect(m_statusBar, &DolphinStatusBar::zoomLevelChanged, this, &DolphinViewContainer::slotStatusBarZoomLevelChanged); m_statusBarTimer = new QTimer(this); m_statusBarTimer->setSingleShot(true); m_statusBarTimer->setInterval(300); connect(m_statusBarTimer, &QTimer::timeout, this, &DolphinViewContainer::updateStatusBar); KIO::FileUndoManager* undoManager = KIO::FileUndoManager::self(); connect(undoManager, &KIO::FileUndoManager::jobRecordingFinished, this, &DolphinViewContainer::delayedStatusBarUpdate); navigatorLayout->addWidget(m_urlNavigator); navigatorLayout->addWidget(m_emptyTrashButton); m_topLayout->addWidget(m_navigatorWidget); m_topLayout->addWidget(m_searchBox); m_topLayout->addWidget(m_messageWidget); m_topLayout->addWidget(m_view); m_topLayout->addWidget(m_filterBar); m_topLayout->addWidget(m_statusBar); setSearchModeEnabled(isSearchUrl(url)); // Initialize kactivities resource instance #ifdef HAVE_KACTIVITIES m_activityResourceInstance = new KActivities::ResourceInstance(window()->winId(), url); m_activityResourceInstance->setParent(this); #endif } DolphinViewContainer::~DolphinViewContainer() { } QUrl DolphinViewContainer::url() const { return m_view->url(); } void DolphinViewContainer::setActive(bool active) { m_searchBox->setActive(active); m_urlNavigator->setActive(active); m_view->setActive(active); #ifdef HAVE_KACTIVITIES if (active) { m_activityResourceInstance->notifyFocusedIn(); } else { m_activityResourceInstance->notifyFocusedOut(); } #endif } bool DolphinViewContainer::isActive() const { Q_ASSERT(m_view->isActive() == m_urlNavigator->isActive()); return m_view->isActive(); } void DolphinViewContainer::setAutoGrabFocus(bool grab) { m_autoGrabFocus = grab; } bool DolphinViewContainer::autoGrabFocus() const { return m_autoGrabFocus; } QString DolphinViewContainer::currentSearchText() const { return m_searchBox->text(); } const DolphinStatusBar* DolphinViewContainer::statusBar() const { return m_statusBar; } DolphinStatusBar* DolphinViewContainer::statusBar() { return m_statusBar; } const KUrlNavigator* DolphinViewContainer::urlNavigator() const { return m_urlNavigator; } KUrlNavigator* DolphinViewContainer::urlNavigator() { return m_urlNavigator; } const DolphinView* DolphinViewContainer::view() const { return m_view; } DolphinView* DolphinViewContainer::view() { return m_view; } void DolphinViewContainer::showMessage(const QString& msg, MessageType type) { if (msg.isEmpty()) { return; } m_messageWidget->setText(msg); // TODO: wrap at arbitrary character positions once QLabel can do this // https://bugreports.qt.io/browse/QTBUG-1276 m_messageWidget->setWordWrap(true); switch (type) { case Information: m_messageWidget->setMessageType(KMessageWidget::Information); break; case Warning: m_messageWidget->setMessageType(KMessageWidget::Warning); break; case Error: m_messageWidget->setMessageType(KMessageWidget::Error); break; default: Q_ASSERT(false); break; } m_messageWidget->setWordWrap(false); const int unwrappedWidth = m_messageWidget->sizeHint().width(); m_messageWidget->setWordWrap(unwrappedWidth > size().width()); if (m_messageWidget->isVisible()) { m_messageWidget->hide(); } m_messageWidget->animatedShow(); } void DolphinViewContainer::readSettings() { if (GeneralSettings::modifiedStartupSettings()) { // The startup settings should only get applied if they have been // modified by the user. Otherwise keep the (possibly) different current // settings of the URL navigator and the filterbar. m_urlNavigator->setUrlEditable(GeneralSettings::editableUrl()); m_urlNavigator->setShowFullPath(GeneralSettings::showFullPath()); m_urlNavigator->setHomeUrl(Dolphin::homeUrl()); setFilterBarVisible(GeneralSettings::filterBar()); } m_view->readSettings(); m_statusBar->readSettings(); } bool DolphinViewContainer::isFilterBarVisible() const { return m_filterBar->isVisible(); } void DolphinViewContainer::setSearchModeEnabled(bool enabled) { m_searchBox->setVisible(enabled); m_navigatorWidget->setVisible(!enabled); if (enabled) { const QUrl& locationUrl = m_urlNavigator->locationUrl(); m_searchBox->fromSearchUrl(locationUrl); } if (enabled == isSearchModeEnabled()) { if (enabled && !m_searchBox->hasFocus()) { m_searchBox->setFocus(); m_searchBox->selectAll(); } return; } if (!enabled) { m_view->setViewPropertiesContext(QString()); // Restore the URL for the URL navigator. If Dolphin has been // started with a search-URL, the home URL is used as fallback. QUrl url = m_searchBox->searchPath(); if (url.isEmpty() || !url.isValid() || isSearchUrl(url)) { url = Dolphin::homeUrl(); } m_urlNavigator->setLocationUrl(url); } m_searchModeEnabled = enabled; emit searchModeEnabledChanged(enabled); } bool DolphinViewContainer::isSearchModeEnabled() const { return m_searchModeEnabled; } QString DolphinViewContainer::placesText() const { QString text; if (isSearchModeEnabled()) { text = i18n("Search for %1 in %2", m_searchBox->text(), m_searchBox->searchPath().fileName()); } else { text = url().adjusted(QUrl::StripTrailingSlash).fileName(); if (text.isEmpty()) { text = url().host(); } if (text.isEmpty()) { text = url().scheme(); } } return text; } void DolphinViewContainer::reload() { view()->reload(); m_messageWidget->hide(); } QString DolphinViewContainer::caption() const { if (GeneralSettings::showFullPathInTitlebar()) { if (!url().isLocalFile()) { return url().adjusted(QUrl::StripTrailingSlash).toString(); } return url().adjusted(QUrl::StripTrailingSlash).path(); } KFilePlacesModel *placesModel = DolphinPlacesModelSingleton::instance().placesModel(); const auto& matchedPlaces = placesModel->match(placesModel->index(0,0), KFilePlacesModel::UrlRole, QUrl(url().adjusted(QUrl::StripTrailingSlash).toString(QUrl::FullyEncoded).append("/?")), 1, Qt::MatchRegExp); if (!matchedPlaces.isEmpty()) { return placesModel->text(matchedPlaces.first()); } if (isSearchModeEnabled()) { if (currentSearchText().isEmpty()){ return i18n("Search"); } else { return i18n("Search for %1", currentSearchText()); } } if (!url().isLocalFile()) { QUrl adjustedUrl = url().adjusted(QUrl::StripTrailingSlash); QString caption; if (!adjustedUrl.fileName().isEmpty()) { caption = adjustedUrl.fileName(); } else if (!adjustedUrl.path().isEmpty() && adjustedUrl.path() != "/") { caption = adjustedUrl.path(); } else if (!adjustedUrl.host().isEmpty()) { caption = adjustedUrl.host(); } else { caption = adjustedUrl.toString(); } return caption; } QString fileName = url().adjusted(QUrl::StripTrailingSlash).fileName(); if (fileName.isEmpty()) { fileName = '/'; } return fileName; } void DolphinViewContainer::setUrl(const QUrl& newUrl) { if (newUrl != m_urlNavigator->locationUrl()) { m_urlNavigator->setLocationUrl(newUrl); } #ifdef HAVE_KACTIVITIES m_activityResourceInstance->setUri(newUrl); #endif } void DolphinViewContainer::setFilterBarVisible(bool visible) { Q_ASSERT(m_filterBar); if (visible) { m_filterBar->show(); m_filterBar->setFocus(); m_filterBar->selectAll(); } else { closeFilterBar(); } } void DolphinViewContainer::delayedStatusBarUpdate() { if (m_statusBarTimer->isActive() && (m_statusBarTimestamp.elapsed() > 2000)) { // No update of the statusbar has been done during the last 2 seconds, // although an update has been requested. Trigger an immediate update. m_statusBarTimer->stop(); updateStatusBar(); } else { // Invoke updateStatusBar() with a small delay. This assures that // when a lot of delayedStatusBarUpdates() are done in a short time, // no bottleneck is given. m_statusBarTimer->start(); } } void DolphinViewContainer::updateStatusBar() { m_statusBarTimestamp.start(); const QString text = m_view->statusBarText(); m_statusBar->setDefaultText(text); m_statusBar->resetToDefaultText(); } void DolphinViewContainer::updateDirectoryLoadingProgress(int percent) { if (m_statusBar->progressText().isEmpty()) { m_statusBar->setProgressText(i18nc("@info:progress", "Loading folder...")); } m_statusBar->setProgress(percent); } void DolphinViewContainer::updateDirectorySortingProgress(int percent) { if (m_statusBar->progressText().isEmpty()) { m_statusBar->setProgressText(i18nc("@info:progress", "Sorting...")); } m_statusBar->setProgress(percent); } void DolphinViewContainer::slotDirectoryLoadingStarted() { if (isSearchUrl(url())) { // Search KIO-slaves usually don't provide any progress information. Give // a hint to the user that a searching is done: updateStatusBar(); m_statusBar->setProgressText(i18nc("@info", "Searching...")); m_statusBar->setProgress(-1); } else { // Trigger an undetermined progress indication. The progress // information in percent will be triggered by the percent() signal // of the directory lister later. m_statusBar->setProgressText(QString()); updateDirectoryLoadingProgress(-1); } } void DolphinViewContainer::slotDirectoryLoadingCompleted() { if (!m_statusBar->progressText().isEmpty()) { m_statusBar->setProgressText(QString()); m_statusBar->setProgress(100); } 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.")); } else { updateStatusBar(); } } void DolphinViewContainer::slotDirectoryLoadingCanceled() { if (!m_statusBar->progressText().isEmpty()) { m_statusBar->setProgressText(QString()); m_statusBar->setProgress(100); } m_statusBar->setText(QString()); } void DolphinViewContainer::slotUrlIsFileError(const QUrl& url) { const KFileItem item(url); // Find out if the file can be opened in the view (for example, this is the // case if the file is an archive). The mime type must be known for that. item.determineMimeType(); const QUrl& folderUrl = DolphinView::openItemAsFolderUrl(item, true); if (!folderUrl.isEmpty()) { setUrl(folderUrl); } else { slotItemActivated(item); } } void DolphinViewContainer::slotItemActivated(const KFileItem& item) { // It is possible to activate items on inactive views by // drag & drop operations. Assure that activating an item always // results in an active view. m_view->setActive(true); const QUrl& url = DolphinView::openItemAsFolderUrl(item, GeneralSettings::browseThroughArchives()); if (!url.isEmpty()) { setUrl(url); return; } KRun *run = new KRun(item.targetUrl(), this); run->setShowScriptExecutionPrompt(true); } void DolphinViewContainer::slotItemsActivated(const KFileItemList& items) { Q_ASSERT(items.count() >= 2); KFileItemActions fileItemActions(this); fileItemActions.runPreferredApplications(items, QString()); } void DolphinViewContainer::showItemInfo(const KFileItem& item) { if (item.isNull()) { m_statusBar->resetToDefaultText(); } else { m_statusBar->setText(item.getStatusBarInfo()); } } void DolphinViewContainer::closeFilterBar() { m_filterBar->closeFilterBar(); m_view->setFocus(); emit showFilterBarChanged(false); } void DolphinViewContainer::setNameFilter(const QString& nameFilter) { m_view->setNameFilter(nameFilter); delayedStatusBarUpdate(); } void DolphinViewContainer::activate() { setActive(true); } void DolphinViewContainer::slotUrlNavigatorLocationAboutToBeChanged(const QUrl&) { saveViewState(); } void DolphinViewContainer::slotUrlNavigatorLocationChanged(const QUrl& url) { slotReturnPressed(); if (KProtocolManager::supportsListing(url)) { setSearchModeEnabled(isSearchUrl(url)); m_view->setUrl(url); tryRestoreViewState(); if (m_autoGrabFocus && isActive() && !isSearchUrl(url)) { // When an URL has been entered, the view should get the focus. // The focus must be requested asynchronously, as changing the URL might create // a new view widget. QTimer::singleShot(0, this, &DolphinViewContainer::requestFocus); } } else if (KProtocolManager::isSourceProtocol(url)) { QString app = QStringLiteral("konqueror"); if (url.scheme().startsWith(QLatin1String("http"))) { showMessage(i18nc("@info:status", // krazy:exclude=qmethods "Dolphin does not support web pages, the web browser has been launched"), Information); const KConfigGroup config(KSharedConfig::openConfig(QStringLiteral("kdeglobals")), "General"); const QString browser = config.readEntry("BrowserApplication"); if (!browser.isEmpty()) { app = browser; if (app.startsWith('!')) { // a literal command has been configured, remove the '!' prefix app.remove(0, 1); } } } else { showMessage(i18nc("@info:status", "Protocol not supported by Dolphin, Konqueror has been launched"), Information); } const QString secureUrl = KShell::quoteArg(url.toDisplayString(QUrl::PreferLocalFile)); const QString command = app + ' ' + secureUrl; KRun::runCommand(command, app, app, this); } else { showMessage(i18nc("@info:status", "Invalid protocol"), Error); } } void DolphinViewContainer::slotUrlSelectionRequested(const QUrl& url) { m_view->markUrlsAsSelected({url}); m_view->markUrlAsCurrent(url); // makes the item scroll into view } void DolphinViewContainer::redirect(const QUrl& oldUrl, const QUrl& newUrl) { - Q_UNUSED(oldUrl); + Q_UNUSED(oldUrl) const bool block = m_urlNavigator->signalsBlocked(); m_urlNavigator->blockSignals(true); // Assure that the location state is reset for redirection URLs. This // allows to skip redirection URLs when going back or forward in the // URL history. m_urlNavigator->saveLocationState(QByteArray()); m_urlNavigator->setLocationUrl(newUrl); setSearchModeEnabled(isSearchUrl(newUrl)); m_urlNavigator->blockSignals(block); } void DolphinViewContainer::requestFocus() { m_view->setFocus(); } void DolphinViewContainer::saveUrlCompletionMode(KCompletion::CompletionMode completion) { GeneralSettings::setUrlCompletionMode(completion); } void DolphinViewContainer::slotReturnPressed() { if (!GeneralSettings::editableUrl()) { m_urlNavigator->setUrlEditable(false); } } void DolphinViewContainer::startSearching() { const QUrl url = m_searchBox->urlForSearching(); if (url.isValid() && !url.isEmpty()) { m_view->setViewPropertiesContext(QStringLiteral("search")); m_urlNavigator->setLocationUrl(url); } } void DolphinViewContainer::closeSearchBox() { setSearchModeEnabled(false); } void DolphinViewContainer::stopDirectoryLoading() { m_view->stopLoading(); m_statusBar->setProgress(100); } void DolphinViewContainer::slotStatusBarZoomLevelChanged(int zoomLevel) { m_view->setZoomLevel(zoomLevel); } void DolphinViewContainer::showErrorMessage(const QString& msg) { showMessage(msg, Error); } bool DolphinViewContainer::isSearchUrl(const QUrl& url) const { return url.scheme().contains(QLatin1String("search")); } void DolphinViewContainer::saveViewState() { QByteArray locationState; QDataStream stream(&locationState, QIODevice::WriteOnly); m_view->saveState(stream); m_urlNavigator->saveLocationState(locationState); } void DolphinViewContainer::tryRestoreViewState() { QByteArray locationState = m_urlNavigator->locationState(); if (!locationState.isEmpty()) { QDataStream stream(&locationState, QIODevice::ReadOnly); m_view->restoreState(stream); } } diff --git a/src/kitemviews/kfileitemlistview.cpp b/src/kitemviews/kfileitemlistview.cpp index b1d3da643..80d85aa2e 100644 --- a/src/kitemviews/kfileitemlistview.cpp +++ b/src/kitemviews/kfileitemlistview.cpp @@ -1,423 +1,423 @@ /*************************************************************************** * Copyright (C) 2011 by Peter Penz * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kfileitemlistview.h" #include "kfileitemlistwidget.h" #include "kfileitemmodel.h" #include "kfileitemmodelrolesupdater.h" #include "private/kpixmapmodifier.h" #include #include #include #include #include // #define KFILEITEMLISTVIEW_DEBUG namespace { // If the visible index range changes, KFileItemModelRolesUpdater is not // informed immediately, but with a short delay. This ensures that scrolling // always feels smooth and is not interrupted by icon loading (which can be // quite expensive if a disk access is required to determine the final icon). const int ShortInterval = 50; // If the icon size changes, a longer delay is used. This prevents that // the expensive re-generation of all previews is triggered repeatedly when // changing the zoom level. const int LongInterval = 300; } KFileItemListView::KFileItemListView(QGraphicsWidget* parent) : KStandardItemListView(parent), m_modelRolesUpdater(nullptr), m_updateVisibleIndexRangeTimer(nullptr), m_updateIconSizeTimer(nullptr) { setAcceptDrops(true); setScrollOrientation(Qt::Vertical); m_updateVisibleIndexRangeTimer = new QTimer(this); m_updateVisibleIndexRangeTimer->setSingleShot(true); m_updateVisibleIndexRangeTimer->setInterval(ShortInterval); connect(m_updateVisibleIndexRangeTimer, &QTimer::timeout, this, &KFileItemListView::updateVisibleIndexRange); m_updateIconSizeTimer = new QTimer(this); m_updateIconSizeTimer->setSingleShot(true); m_updateIconSizeTimer->setInterval(LongInterval); connect(m_updateIconSizeTimer, &QTimer::timeout, this, &KFileItemListView::updateIconSize); setVisibleRoles({"text"}); } KFileItemListView::~KFileItemListView() { } void KFileItemListView::setPreviewsShown(bool show) { if (!m_modelRolesUpdater) { return; } if (m_modelRolesUpdater->previewsShown() != show) { beginTransaction(); m_modelRolesUpdater->setPreviewsShown(show); onPreviewsShownChanged(show); endTransaction(); } } bool KFileItemListView::previewsShown() const { return m_modelRolesUpdater ? m_modelRolesUpdater->previewsShown() : false; } void KFileItemListView::setEnlargeSmallPreviews(bool enlarge) { if (m_modelRolesUpdater) { m_modelRolesUpdater->setEnlargeSmallPreviews(enlarge); } } bool KFileItemListView::enlargeSmallPreviews() const { return m_modelRolesUpdater ? m_modelRolesUpdater->enlargeSmallPreviews() : false; } void KFileItemListView::setEnabledPlugins(const QStringList& list) { if (m_modelRolesUpdater) { m_modelRolesUpdater->setEnabledPlugins(list); } } QStringList KFileItemListView::enabledPlugins() const { return m_modelRolesUpdater ? m_modelRolesUpdater->enabledPlugins() : QStringList(); } QPixmap KFileItemListView::createDragPixmap(const KItemSet& indexes) const { if (!model()) { return QPixmap(); } const int itemCount = indexes.count(); Q_ASSERT(itemCount > 0); if (itemCount == 1) { return KItemListView::createDragPixmap(indexes); } // If more than one item is dragged, align the items inside a // rectangular grid. The maximum grid size is limited to 5 x 5 items. int xCount; int size; if (itemCount > 16) { xCount = 5; size = KIconLoader::SizeSmall; } else if (itemCount > 9) { xCount = 4; size = KIconLoader::SizeSmallMedium; } else { xCount = 3; size = KIconLoader::SizeMedium; } if (itemCount < xCount) { xCount = itemCount; } int yCount = itemCount / xCount; if (itemCount % xCount != 0) { ++yCount; } if (yCount > xCount) { yCount = xCount; } const qreal dpr = scene()->views()[0]->devicePixelRatio(); // Draw the selected items into the grid cells. QPixmap dragPixmap(QSize(xCount * size + xCount, yCount * size + yCount) * dpr); dragPixmap.setDevicePixelRatio(dpr); dragPixmap.fill(Qt::transparent); QPainter painter(&dragPixmap); int x = 0; int y = 0; for (int index : indexes) { QPixmap pixmap = model()->data(index).value("iconPixmap").value(); if (pixmap.isNull()) { QIcon icon = QIcon::fromTheme(model()->data(index).value("iconName").toString()); pixmap = icon.pixmap(size, size); } else { KPixmapModifier::scale(pixmap, QSize(size, size) * dpr); } painter.drawPixmap(x, y, pixmap); x += size + 1; if (x >= dragPixmap.width()) { x = 0; y += size + 1; } if (y >= dragPixmap.height()) { break; } } return dragPixmap; } KItemListWidgetCreatorBase* KFileItemListView::defaultWidgetCreator() const { return new KItemListWidgetCreator(); } void KFileItemListView::initializeItemListWidget(KItemListWidget* item) { KStandardItemListView::initializeItemListWidget(item); // Make sure that the item has an icon. QHash data = item->data(); if (!data.contains("iconName") && data["iconPixmap"].value().isNull()) { Q_ASSERT(qobject_cast(model())); KFileItemModel* fileItemModel = static_cast(model()); const KFileItem fileItem = fileItemModel->fileItem(item->index()); data.insert("iconName", fileItem.iconName()); item->setData(data, {"iconName"}); } } void KFileItemListView::onPreviewsShownChanged(bool shown) { - Q_UNUSED(shown); + Q_UNUSED(shown) } void KFileItemListView::onItemLayoutChanged(ItemLayout current, ItemLayout previous) { KStandardItemListView::onItemLayoutChanged(current, previous); triggerVisibleIndexRangeUpdate(); } void KFileItemListView::onModelChanged(KItemModelBase* current, KItemModelBase* previous) { Q_ASSERT(qobject_cast(current)); KStandardItemListView::onModelChanged(current, previous); delete m_modelRolesUpdater; m_modelRolesUpdater = nullptr; if (current) { m_modelRolesUpdater = new KFileItemModelRolesUpdater(static_cast(current), this); m_modelRolesUpdater->setIconSize(availableIconSize()); applyRolesToModel(); } } void KFileItemListView::onScrollOrientationChanged(Qt::Orientation current, Qt::Orientation previous) { KStandardItemListView::onScrollOrientationChanged(current, previous); triggerVisibleIndexRangeUpdate(); } void KFileItemListView::onItemSizeChanged(const QSizeF& current, const QSizeF& previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) triggerVisibleIndexRangeUpdate(); } void KFileItemListView::onScrollOffsetChanged(qreal current, qreal previous) { KStandardItemListView::onScrollOffsetChanged(current, previous); triggerVisibleIndexRangeUpdate(); } void KFileItemListView::onVisibleRolesChanged(const QList& current, const QList& previous) { KStandardItemListView::onVisibleRolesChanged(current, previous); applyRolesToModel(); } void KFileItemListView::onStyleOptionChanged(const KItemListStyleOption& current, const KItemListStyleOption& previous) { KStandardItemListView::onStyleOptionChanged(current, previous); triggerIconSizeUpdate(); } void KFileItemListView::onSupportsItemExpandingChanged(bool supportsExpanding) { applyRolesToModel(); KStandardItemListView::onSupportsItemExpandingChanged(supportsExpanding); triggerVisibleIndexRangeUpdate(); } void KFileItemListView::onTransactionBegin() { if (m_modelRolesUpdater) { m_modelRolesUpdater->setPaused(true); } } void KFileItemListView::onTransactionEnd() { if (!m_modelRolesUpdater) { return; } // Only unpause the model-roles-updater if no timer is active. If one // timer is still active the model-roles-updater will be unpaused later as // soon as the timer has been exceeded. const bool timerActive = m_updateVisibleIndexRangeTimer->isActive() || m_updateIconSizeTimer->isActive(); if (!timerActive) { m_modelRolesUpdater->setPaused(false); } } void KFileItemListView::resizeEvent(QGraphicsSceneResizeEvent* event) { KStandardItemListView::resizeEvent(event); triggerVisibleIndexRangeUpdate(); } void KFileItemListView::slotItemsRemoved(const KItemRangeList& itemRanges) { KStandardItemListView::slotItemsRemoved(itemRanges); } void KFileItemListView::slotSortRoleChanged(const QByteArray& current, const QByteArray& previous) { const QByteArray sortRole = model()->sortRole(); if (!visibleRoles().contains(sortRole)) { applyRolesToModel(); } KStandardItemListView::slotSortRoleChanged(current, previous); } void KFileItemListView::triggerVisibleIndexRangeUpdate() { if (!model()) { return; } m_modelRolesUpdater->setPaused(true); // If the icon size has been changed recently, wait until // m_updateIconSizeTimer expires. if (!m_updateIconSizeTimer->isActive()) { m_updateVisibleIndexRangeTimer->start(); } } void KFileItemListView::updateVisibleIndexRange() { if (!m_modelRolesUpdater) { return; } const int index = firstVisibleIndex(); const int count = lastVisibleIndex() - index + 1; m_modelRolesUpdater->setMaximumVisibleItems(maximumVisibleItems()); m_modelRolesUpdater->setVisibleIndexRange(index, count); m_modelRolesUpdater->setPaused(isTransactionActive()); } void KFileItemListView::triggerIconSizeUpdate() { if (!model()) { return; } m_modelRolesUpdater->setPaused(true); m_updateIconSizeTimer->start(); // The visible index range will be updated when m_updateIconSizeTimer expires. // Stop m_updateVisibleIndexRangeTimer to prevent an expensive re-generation // of all previews (note that the user might change the icon size again soon). m_updateVisibleIndexRangeTimer->stop(); } void KFileItemListView::updateIconSize() { if (!m_modelRolesUpdater) { return; } m_modelRolesUpdater->setIconSize(availableIconSize()); // Update the visible index range (which has most likely changed after the // icon size change) before unpausing m_modelRolesUpdater. const int index = firstVisibleIndex(); const int count = lastVisibleIndex() - index + 1; m_modelRolesUpdater->setVisibleIndexRange(index, count); m_modelRolesUpdater->setPaused(isTransactionActive()); } void KFileItemListView::applyRolesToModel() { if (!model()) { return; } Q_ASSERT(qobject_cast(model())); KFileItemModel* fileItemModel = static_cast(model()); // KFileItemModel does not distinct between "visible" and "invisible" roles. // Add all roles that are mandatory for having a working KFileItemListView: QSet roles = visibleRoles().toSet(); roles.insert("iconPixmap"); roles.insert("iconName"); roles.insert("text"); roles.insert("isDir"); roles.insert("isLink"); roles.insert("isHidden"); if (supportsItemExpanding()) { roles.insert("isExpanded"); roles.insert("isExpandable"); roles.insert("expandedParentsCount"); } // Assure that the role that is used for sorting will be determined roles.insert(fileItemModel->sortRole()); fileItemModel->setRoles(roles); m_modelRolesUpdater->setRoles(roles); } QSize KFileItemListView::availableIconSize() const { const KItemListStyleOption& option = styleOption(); const int iconSize = option.iconSize; if (itemLayout() == IconsLayout) { const int maxIconWidth = itemSize().width() - 2 * option.padding; return QSize(maxIconWidth, iconSize); } return QSize(iconSize, iconSize); } diff --git a/src/kitemviews/kfileitemmodel.cpp b/src/kitemviews/kfileitemmodel.cpp index c11aae3d2..51ffb2140 100644 --- a/src/kitemviews/kfileitemmodel.cpp +++ b/src/kitemviews/kfileitemmodel.cpp @@ -1,2432 +1,2432 @@ /***************************************************************************** * Copyright (C) 2011 by Peter Penz * * Copyright (C) 2013 by Frank Reininghaus * * Copyright (C) 2013 by Emmanuel Pescosta * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * *****************************************************************************/ #include "kfileitemmodel.h" #include "dolphin_generalsettings.h" #include "dolphindebug.h" #include "private/kfileitemmodeldirlister.h" #include "private/kfileitemmodelsortalgorithm.h" #include #include #include #include #include #include // #define KFILEITEMMODEL_DEBUG KFileItemModel::KFileItemModel(QObject* parent) : KItemModelBase("text", parent), m_dirLister(nullptr), m_sortDirsFirst(true), m_sortRole(NameRole), m_sortingProgressPercent(-1), m_roles(), m_itemData(), m_items(), m_filter(), m_filteredItems(), m_requestRole(), m_maximumUpdateIntervalTimer(nullptr), m_resortAllItemsTimer(nullptr), m_pendingItemsToInsert(), m_groups(), m_expandedDirs(), m_urlsToExpand() { m_collator.setNumericMode(true); loadSortingSettings(); m_dirLister = new KFileItemModelDirLister(this); m_dirLister->setDelayedMimeTypes(true); const QWidget* parentWidget = qobject_cast(parent); if (parentWidget) { m_dirLister->setMainWindow(parentWidget->window()); } connect(m_dirLister, &KFileItemModelDirLister::started, this, &KFileItemModel::directoryLoadingStarted); connect(m_dirLister, QOverload<>::of(&KCoreDirLister::canceled), this, &KFileItemModel::slotCanceled); connect(m_dirLister, QOverload::of(&KCoreDirLister::completed), this, &KFileItemModel::slotCompleted); connect(m_dirLister, &KFileItemModelDirLister::itemsAdded, this, &KFileItemModel::slotItemsAdded); connect(m_dirLister, &KFileItemModelDirLister::itemsDeleted, this, &KFileItemModel::slotItemsDeleted); connect(m_dirLister, &KFileItemModelDirLister::refreshItems, this, &KFileItemModel::slotRefreshItems); connect(m_dirLister, QOverload<>::of(&KCoreDirLister::clear), this, &KFileItemModel::slotClear); connect(m_dirLister, &KFileItemModelDirLister::infoMessage, this, &KFileItemModel::infoMessage); connect(m_dirLister, &KFileItemModelDirLister::errorMessage, this, &KFileItemModel::errorMessage); connect(m_dirLister, &KFileItemModelDirLister::percent, this, &KFileItemModel::directoryLoadingProgress); connect(m_dirLister, QOverload::of(&KCoreDirLister::redirection), this, &KFileItemModel::directoryRedirection); connect(m_dirLister, &KFileItemModelDirLister::urlIsFileError, this, &KFileItemModel::urlIsFileError); // Apply default roles that should be determined resetRoles(); m_requestRole[NameRole] = true; m_requestRole[IsDirRole] = true; m_requestRole[IsLinkRole] = true; m_roles.insert("text"); m_roles.insert("isDir"); m_roles.insert("isLink"); m_roles.insert("isHidden"); // For slow KIO-slaves like used for searching it makes sense to show results periodically even // before the completed() or canceled() signal has been emitted. m_maximumUpdateIntervalTimer = new QTimer(this); m_maximumUpdateIntervalTimer->setInterval(2000); m_maximumUpdateIntervalTimer->setSingleShot(true); connect(m_maximumUpdateIntervalTimer, &QTimer::timeout, this, &KFileItemModel::dispatchPendingItemsToInsert); // When changing the value of an item which represents the sort-role a resorting must be // triggered. Especially in combination with KFileItemModelRolesUpdater this might be done // for a lot of items within a quite small timeslot. To prevent expensive resortings the // resorting is postponed until the timer has been exceeded. m_resortAllItemsTimer = new QTimer(this); m_resortAllItemsTimer->setInterval(500); m_resortAllItemsTimer->setSingleShot(true); connect(m_resortAllItemsTimer, &QTimer::timeout, this, &KFileItemModel::resortAllItems); connect(GeneralSettings::self(), &GeneralSettings::sortingChoiceChanged, this, &KFileItemModel::slotSortingChoiceChanged); } KFileItemModel::~KFileItemModel() { qDeleteAll(m_itemData); qDeleteAll(m_filteredItems); qDeleteAll(m_pendingItemsToInsert); } void KFileItemModel::loadDirectory(const QUrl &url) { m_dirLister->openUrl(url); } void KFileItemModel::refreshDirectory(const QUrl &url) { // Refresh all expanded directories first (Bug 295300) QHashIterator expandedDirs(m_expandedDirs); while (expandedDirs.hasNext()) { expandedDirs.next(); m_dirLister->openUrl(expandedDirs.value(), KDirLister::Reload); } m_dirLister->openUrl(url, KDirLister::Reload); } QUrl KFileItemModel::directory() const { return m_dirLister->url(); } void KFileItemModel::cancelDirectoryLoading() { m_dirLister->stop(); } int KFileItemModel::count() const { return m_itemData.count(); } QHash KFileItemModel::data(int index) const { if (index >= 0 && index < count()) { ItemData* data = m_itemData.at(index); if (data->values.isEmpty()) { data->values = retrieveData(data->item, data->parent); } return data->values; } return QHash(); } bool KFileItemModel::setData(int index, const QHash& values) { if (index < 0 || index >= count()) { return false; } QHash currentValues = data(index); // Determine which roles have been changed QSet changedRoles; QHashIterator it(values); while (it.hasNext()) { it.next(); const QByteArray role = sharedValue(it.key()); const QVariant value = it.value(); if (currentValues[role] != value) { currentValues[role] = value; changedRoles.insert(role); } } if (changedRoles.isEmpty()) { return false; } m_itemData[index]->values = currentValues; if (changedRoles.contains("text")) { QUrl url = m_itemData[index]->item.url(); url = url.adjusted(QUrl::RemoveFilename); url.setPath(url.path() + currentValues["text"].toString()); m_itemData[index]->item.setUrl(url); } emitItemsChangedAndTriggerResorting(KItemRangeList() << KItemRange(index, 1), changedRoles); return true; } void KFileItemModel::setSortDirectoriesFirst(bool dirsFirst) { if (dirsFirst != m_sortDirsFirst) { m_sortDirsFirst = dirsFirst; resortAllItems(); } } bool KFileItemModel::sortDirectoriesFirst() const { return m_sortDirsFirst; } void KFileItemModel::setShowHiddenFiles(bool show) { m_dirLister->setShowingDotFiles(show); m_dirLister->emitChanges(); if (show) { dispatchPendingItemsToInsert(); } } bool KFileItemModel::showHiddenFiles() const { return m_dirLister->showingDotFiles(); } void KFileItemModel::setShowDirectoriesOnly(bool enabled) { m_dirLister->setDirOnlyMode(enabled); } bool KFileItemModel::showDirectoriesOnly() const { return m_dirLister->dirOnlyMode(); } QMimeData* KFileItemModel::createMimeData(const KItemSet& indexes) const { QMimeData* data = new QMimeData(); // The following code has been taken from KDirModel::mimeData() // (kdelibs/kio/kio/kdirmodel.cpp) // Copyright (C) 2006 David Faure QList urls; QList mostLocalUrls; const ItemData* lastAddedItem = nullptr; for (int index : indexes) { const ItemData* itemData = m_itemData.at(index); const ItemData* parent = itemData->parent; while (parent && parent != lastAddedItem) { parent = parent->parent; } if (parent && parent == lastAddedItem) { // A parent of 'itemData' has been added already. continue; } lastAddedItem = itemData; const KFileItem& item = itemData->item; if (!item.isNull()) { urls << item.url(); bool isLocal; mostLocalUrls << item.mostLocalUrl(isLocal); } } KUrlMimeData::setUrls(urls, mostLocalUrls, data); return data; } int KFileItemModel::indexForKeyboardSearch(const QString& text, int startFromIndex) const { startFromIndex = qMax(0, startFromIndex); for (int i = startFromIndex; i < count(); ++i) { if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) { return i; } } for (int i = 0; i < startFromIndex; ++i) { if (fileItem(i).text().startsWith(text, Qt::CaseInsensitive)) { return i; } } return -1; } bool KFileItemModel::supportsDropping(int index) const { const KFileItem item = fileItem(index); return !item.isNull() && (item.isDir() || item.isDesktopFile()); } QString KFileItemModel::roleDescription(const QByteArray& role) const { static QHash description; if (description.isEmpty()) { int count = 0; const RoleInfoMap* map = rolesInfoMap(count); for (int i = 0; i < count; ++i) { if (!map[i].roleTranslation) { continue; } description.insert(map[i].role, i18nc(map[i].roleTranslationContext, map[i].roleTranslation)); } } return description.value(role); } QList > KFileItemModel::groups() const { if (!m_itemData.isEmpty() && m_groups.isEmpty()) { #ifdef KFILEITEMMODEL_DEBUG QElapsedTimer timer; timer.start(); #endif switch (typeForRole(sortRole())) { case NameRole: m_groups = nameRoleGroups(); break; case SizeRole: m_groups = sizeRoleGroups(); break; case ModificationTimeRole: m_groups = timeRoleGroups([](const ItemData *item) { return item->item.time(KFileItem::ModificationTime); }); break; case CreationTimeRole: m_groups = timeRoleGroups([](const ItemData *item) { return item->item.time(KFileItem::CreationTime); }); break; case AccessTimeRole: m_groups = timeRoleGroups([](const ItemData *item) { return item->item.time(KFileItem::AccessTime); }); break; case DeletionTimeRole: m_groups = timeRoleGroups([](const ItemData *item) { return item->values.value("deletiontime").toDateTime(); }); break; case PermissionsRole: m_groups = permissionRoleGroups(); break; case RatingRole: m_groups = ratingRoleGroups(); break; default: m_groups = genericStringRoleGroups(sortRole()); break; } #ifdef KFILEITEMMODEL_DEBUG qCDebug(DolphinDebug) << "[TIME] Calculating groups for" << count() << "items:" << timer.elapsed(); #endif } return m_groups; } KFileItem KFileItemModel::fileItem(int index) const { if (index >= 0 && index < count()) { return m_itemData.at(index)->item; } return KFileItem(); } KFileItem KFileItemModel::fileItem(const QUrl &url) const { const int indexForUrl = index(url); if (indexForUrl >= 0) { return m_itemData.at(indexForUrl)->item; } return KFileItem(); } int KFileItemModel::index(const KFileItem& item) const { return index(item.url()); } int KFileItemModel::index(const QUrl& url) const { const QUrl urlToFind = url.adjusted(QUrl::StripTrailingSlash); const int itemCount = m_itemData.count(); int itemsInHash = m_items.count(); int index = m_items.value(urlToFind, -1); while (index < 0 && itemsInHash < itemCount) { // Not all URLs are stored yet in m_items. We grow m_items until either // urlToFind is found, or all URLs have been stored in m_items. // Note that we do not add the URLs to m_items one by one, but in // larger blocks. After each block, we check if urlToFind is in // m_items. We could in principle compare urlToFind with each URL while // we are going through m_itemData, but comparing two QUrls will, // unlike calling qHash for the URLs, trigger a parsing of the URLs // which costs both CPU cycles and memory. const int blockSize = 1000; const int currentBlockEnd = qMin(itemsInHash + blockSize, itemCount); for (int i = itemsInHash; i < currentBlockEnd; ++i) { const QUrl nextUrl = m_itemData.at(i)->item.url(); m_items.insert(nextUrl, i); } itemsInHash = currentBlockEnd; index = m_items.value(urlToFind, -1); } if (index < 0) { // The item could not be found, even though all items from m_itemData // should be in m_items now. We print some diagnostic information which // might help to find the cause of the problem, but only once. This // prevents that obtaining and printing the debugging information // wastes CPU cycles and floods the shell or .xsession-errors. static bool printDebugInfo = true; if (m_items.count() != m_itemData.count() && printDebugInfo) { printDebugInfo = false; qCWarning(DolphinDebug) << "The model is in an inconsistent state."; qCWarning(DolphinDebug) << "m_items.count() ==" << m_items.count(); qCWarning(DolphinDebug) << "m_itemData.count() ==" << m_itemData.count(); // Check if there are multiple items with the same URL. QMultiHash indexesForUrl; for (int i = 0; i < m_itemData.count(); ++i) { indexesForUrl.insert(m_itemData.at(i)->item.url(), i); } foreach (const QUrl& url, indexesForUrl.uniqueKeys()) { if (indexesForUrl.count(url) > 1) { qCWarning(DolphinDebug) << "Multiple items found with the URL" << url; auto it = indexesForUrl.find(url); while (it != indexesForUrl.end() && it.key() == url) { const ItemData* data = m_itemData.at(it.value()); qCWarning(DolphinDebug) << "index" << it.value() << ":" << data->item; if (data->parent) { qCWarning(DolphinDebug) << "parent" << data->parent->item; } ++it; } } } } } return index; } KFileItem KFileItemModel::rootItem() const { return m_dirLister->rootItem(); } void KFileItemModel::clear() { slotClear(); } void KFileItemModel::setRoles(const QSet& roles) { if (m_roles == roles) { return; } const QSet changedRoles = (roles - m_roles) + (m_roles - roles); m_roles = roles; if (count() > 0) { const bool supportedExpanding = m_requestRole[ExpandedParentsCountRole]; const bool willSupportExpanding = roles.contains("expandedParentsCount"); if (supportedExpanding && !willSupportExpanding) { // No expanding is supported anymore. Take care to delete all items that have an expansion level // that is not 0 (and hence are part of an expanded item). removeExpandedItems(); } } m_groups.clear(); resetRoles(); QSetIterator it(roles); while (it.hasNext()) { const QByteArray& role = it.next(); m_requestRole[typeForRole(role)] = true; } if (count() > 0) { // Update m_data with the changed requested roles const int maxIndex = count() - 1; for (int i = 0; i <= maxIndex; ++i) { m_itemData[i]->values = retrieveData(m_itemData.at(i)->item, m_itemData.at(i)->parent); } emit itemsChanged(KItemRangeList() << KItemRange(0, count()), changedRoles); } // Clear the 'values' of all filtered items. They will be re-populated with the // correct roles the next time 'values' will be accessed via data(int). QHash::iterator filteredIt = m_filteredItems.begin(); const QHash::iterator filteredEnd = m_filteredItems.end(); while (filteredIt != filteredEnd) { (*filteredIt)->values.clear(); ++filteredIt; } } QSet KFileItemModel::roles() const { return m_roles; } bool KFileItemModel::setExpanded(int index, bool expanded) { if (!isExpandable(index) || isExpanded(index) == expanded) { return false; } QHash values; values.insert(sharedValue("isExpanded"), expanded); if (!setData(index, values)) { return false; } const KFileItem item = m_itemData.at(index)->item; const QUrl url = item.url(); const QUrl targetUrl = item.targetUrl(); if (expanded) { m_expandedDirs.insert(targetUrl, url); m_dirLister->openUrl(url, KDirLister::Keep); const QVariantList previouslyExpandedChildren = m_itemData.at(index)->values.value("previouslyExpandedChildren").value(); foreach (const QVariant& var, previouslyExpandedChildren) { m_urlsToExpand.insert(var.toUrl()); } } else { // Note that there might be (indirect) children of the folder which is to be collapsed in // m_pendingItemsToInsert. To prevent that they will be inserted into the model later, // possibly without a parent, which might result in a crash, we insert all pending items // right now. All new items which would be without a parent will then be removed. dispatchPendingItemsToInsert(); // Check if the index of the collapsed folder has changed. If that is the case, then items // were inserted before the collapsed folder, and its index needs to be updated. if (m_itemData.at(index)->item != item) { index = this->index(item); } m_expandedDirs.remove(targetUrl); m_dirLister->stop(url); const int parentLevel = expandedParentsCount(index); const int itemCount = m_itemData.count(); const int firstChildIndex = index + 1; QVariantList expandedChildren; int childIndex = firstChildIndex; while (childIndex < itemCount && expandedParentsCount(childIndex) > parentLevel) { ItemData* itemData = m_itemData.at(childIndex); if (itemData->values.value("isExpanded").toBool()) { const QUrl targetUrl = itemData->item.targetUrl(); const QUrl url = itemData->item.url(); m_expandedDirs.remove(targetUrl); m_dirLister->stop(url); // TODO: try to unit-test this, see https://bugs.kde.org/show_bug.cgi?id=332102#c11 expandedChildren.append(targetUrl); } ++childIndex; } const int childrenCount = childIndex - firstChildIndex; removeFilteredChildren(KItemRangeList() << KItemRange(index, 1 + childrenCount)); removeItems(KItemRangeList() << KItemRange(firstChildIndex, childrenCount), DeleteItemData); m_itemData.at(index)->values.insert("previouslyExpandedChildren", expandedChildren); } return true; } bool KFileItemModel::isExpanded(int index) const { if (index >= 0 && index < count()) { return m_itemData.at(index)->values.value("isExpanded").toBool(); } return false; } bool KFileItemModel::isExpandable(int index) const { if (index >= 0 && index < count()) { // Call data (instead of accessing m_itemData directly) // to ensure that the value is initialized. return data(index).value("isExpandable").toBool(); } return false; } int KFileItemModel::expandedParentsCount(int index) const { if (index >= 0 && index < count()) { return expandedParentsCount(m_itemData.at(index)); } return 0; } QSet KFileItemModel::expandedDirectories() const { QSet result; const auto dirs = m_expandedDirs; for (const auto &dir : dirs) { result.insert(dir); } return result; } void KFileItemModel::restoreExpandedDirectories(const QSet &urls) { m_urlsToExpand = urls; } void KFileItemModel::expandParentDirectories(const QUrl &url) { // Assure that each sub-path of the URL that should be // expanded is added to m_urlsToExpand. KDirLister // does not care whether the parent-URL has already been // expanded. QUrl urlToExpand = m_dirLister->url(); const int pos = urlToExpand.path().length(); // first subdir can be empty, if m_dirLister->url().path() does not end with '/' // this happens if baseUrl is not root but a home directory, see FoldersPanel, // so using QString::SkipEmptyParts const QStringList subDirs = url.path().mid(pos).split(QDir::separator(), QString::SkipEmptyParts); for (int i = 0; i < subDirs.count() - 1; ++i) { QString path = urlToExpand.path(); if (!path.endsWith(QLatin1Char('/'))) { path.append(QLatin1Char('/')); } urlToExpand.setPath(path + subDirs.at(i)); m_urlsToExpand.insert(urlToExpand); } // KDirLister::open() must called at least once to trigger an initial // loading. The pending URLs that must be restored are handled // in slotCompleted(). QSetIterator it2(m_urlsToExpand); while (it2.hasNext()) { const int idx = index(it2.next()); if (idx >= 0 && !isExpanded(idx)) { setExpanded(idx, true); break; } } } void KFileItemModel::setNameFilter(const QString& nameFilter) { if (m_filter.pattern() != nameFilter) { dispatchPendingItemsToInsert(); m_filter.setPattern(nameFilter); applyFilters(); } } QString KFileItemModel::nameFilter() const { return m_filter.pattern(); } void KFileItemModel::setMimeTypeFilters(const QStringList& filters) { if (m_filter.mimeTypes() != filters) { dispatchPendingItemsToInsert(); m_filter.setMimeTypes(filters); applyFilters(); } } QStringList KFileItemModel::mimeTypeFilters() const { return m_filter.mimeTypes(); } void KFileItemModel::applyFilters() { // Check which shown items from m_itemData must get // hidden and hence moved to m_filteredItems. QVector newFilteredIndexes; const int itemCount = m_itemData.count(); for (int index = 0; index < itemCount; ++index) { ItemData* itemData = m_itemData.at(index); // Only filter non-expanded items as child items may never // exist without a parent item if (!itemData->values.value("isExpanded").toBool()) { const KFileItem item = itemData->item; if (!m_filter.matches(item)) { newFilteredIndexes.append(index); m_filteredItems.insert(item, itemData); } } } const KItemRangeList removedRanges = KItemRangeList::fromSortedContainer(newFilteredIndexes); removeItems(removedRanges, KeepItemData); // Check which hidden items from m_filteredItems should // get visible again and hence removed from m_filteredItems. QList newVisibleItems; QHash::iterator it = m_filteredItems.begin(); while (it != m_filteredItems.end()) { if (m_filter.matches(it.key())) { newVisibleItems.append(it.value()); it = m_filteredItems.erase(it); } else { ++it; } } insertItems(newVisibleItems); } void KFileItemModel::removeFilteredChildren(const KItemRangeList& itemRanges) { if (m_filteredItems.isEmpty() || !m_requestRole[ExpandedParentsCountRole]) { // There are either no filtered items, or it is not possible to expand // folders -> there cannot be any filtered children. return; } QSet parents; foreach (const KItemRange& range, itemRanges) { for (int index = range.index; index < range.index + range.count; ++index) { parents.insert(m_itemData.at(index)); } } QHash::iterator it = m_filteredItems.begin(); while (it != m_filteredItems.end()) { if (parents.contains(it.value()->parent)) { delete it.value(); it = m_filteredItems.erase(it); } else { ++it; } } } QList KFileItemModel::rolesInformation() { static QList rolesInfo; if (rolesInfo.isEmpty()) { int count = 0; const RoleInfoMap* map = rolesInfoMap(count); for (int i = 0; i < count; ++i) { if (map[i].roleType != NoRole) { RoleInfo info; info.role = map[i].role; info.translation = i18nc(map[i].roleTranslationContext, map[i].roleTranslation); if (map[i].groupTranslation) { info.group = i18nc(map[i].groupTranslationContext, map[i].groupTranslation); } else { // For top level roles, groupTranslation is 0. We must make sure that // info.group is an empty string then because the code that generates // menus tries to put the actions into sub menus otherwise. info.group = QString(); } info.requiresBaloo = map[i].requiresBaloo; info.requiresIndexer = map[i].requiresIndexer; rolesInfo.append(info); } } } return rolesInfo; } void KFileItemModel::onGroupedSortingChanged(bool current) { - Q_UNUSED(current); + Q_UNUSED(current) m_groups.clear(); } void KFileItemModel::onSortRoleChanged(const QByteArray& current, const QByteArray& previous, bool resortItems) { - Q_UNUSED(previous); + Q_UNUSED(previous) m_sortRole = typeForRole(current); if (!m_requestRole[m_sortRole]) { QSet newRoles = m_roles; newRoles << current; setRoles(newRoles); } if (resortItems) { resortAllItems(); } } void KFileItemModel::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) resortAllItems(); } void KFileItemModel::loadSortingSettings() { using Choice = GeneralSettings::EnumSortingChoice; switch (GeneralSettings::sortingChoice()) { case Choice::NaturalSorting: m_naturalSorting = true; m_collator.setCaseSensitivity(Qt::CaseInsensitive); break; case Choice::CaseSensitiveSorting: m_naturalSorting = false; m_collator.setCaseSensitivity(Qt::CaseSensitive); break; case Choice::CaseInsensitiveSorting: m_naturalSorting = false; m_collator.setCaseSensitivity(Qt::CaseInsensitive); break; default: Q_UNREACHABLE(); } // Workaround for bug https://bugreports.qt.io/browse/QTBUG-69361 // Force the clean state of QCollator in single thread to avoid thread safety problems in sort m_collator.compare(QString(), QString()); } void KFileItemModel::resortAllItems() { m_resortAllItemsTimer->stop(); const int itemCount = count(); if (itemCount <= 0) { return; } #ifdef KFILEITEMMODEL_DEBUG QElapsedTimer timer; timer.start(); qCDebug(DolphinDebug) << "==========================================================="; qCDebug(DolphinDebug) << "Resorting" << itemCount << "items"; #endif // Remember the order of the current URLs so // that it can be determined which indexes have // been moved because of the resorting. QList oldUrls; oldUrls.reserve(itemCount); foreach (const ItemData* itemData, m_itemData) { oldUrls.append(itemData->item.url()); } m_items.clear(); m_items.reserve(itemCount); // Resort the items sort(m_itemData.begin(), m_itemData.end()); for (int i = 0; i < itemCount; ++i) { m_items.insert(m_itemData.at(i)->item.url(), i); } // Determine the first index that has been moved. int firstMovedIndex = 0; while (firstMovedIndex < itemCount && firstMovedIndex == m_items.value(oldUrls.at(firstMovedIndex))) { ++firstMovedIndex; } const bool itemsHaveMoved = firstMovedIndex < itemCount; if (itemsHaveMoved) { m_groups.clear(); int lastMovedIndex = itemCount - 1; while (lastMovedIndex > firstMovedIndex && lastMovedIndex == m_items.value(oldUrls.at(lastMovedIndex))) { --lastMovedIndex; } Q_ASSERT(firstMovedIndex <= lastMovedIndex); // Create a list movedToIndexes, which has the property that // movedToIndexes[i] is the new index of the item with the old index // firstMovedIndex + i. const int movedItemsCount = lastMovedIndex - firstMovedIndex + 1; QList movedToIndexes; movedToIndexes.reserve(movedItemsCount); for (int i = firstMovedIndex; i <= lastMovedIndex; ++i) { const int newIndex = m_items.value(oldUrls.at(i)); movedToIndexes.append(newIndex); } emit itemsMoved(KItemRange(firstMovedIndex, movedItemsCount), movedToIndexes); } else if (groupedSorting()) { // The groups might have changed even if the order of the items has not. const QList > oldGroups = m_groups; m_groups.clear(); if (groups() != oldGroups) { emit groupsChanged(); } } #ifdef KFILEITEMMODEL_DEBUG qCDebug(DolphinDebug) << "[TIME] Resorting of" << itemCount << "items:" << timer.elapsed(); #endif } void KFileItemModel::slotCompleted() { m_maximumUpdateIntervalTimer->stop(); dispatchPendingItemsToInsert(); if (!m_urlsToExpand.isEmpty()) { // Try to find a URL that can be expanded. // Note that the parent folder must be expanded before any of its subfolders become visible. // Therefore, some URLs in m_restoredExpandedUrls might not be visible yet // -> we expand the first visible URL we find in m_restoredExpandedUrls. foreach (const QUrl& url, m_urlsToExpand) { const int indexForUrl = index(url); if (indexForUrl >= 0) { m_urlsToExpand.remove(url); if (setExpanded(indexForUrl, true)) { // The dir lister has been triggered. This slot will be called // again after the directory has been expanded. return; } } } // None of the URLs in m_restoredExpandedUrls could be found in the model. This can happen // if these URLs have been deleted in the meantime. m_urlsToExpand.clear(); } emit directoryLoadingCompleted(); } void KFileItemModel::slotCanceled() { m_maximumUpdateIntervalTimer->stop(); dispatchPendingItemsToInsert(); emit directoryLoadingCanceled(); } void KFileItemModel::slotItemsAdded(const QUrl &directoryUrl, const KFileItemList& items) { Q_ASSERT(!items.isEmpty()); QUrl parentUrl; if (m_expandedDirs.contains(directoryUrl)) { parentUrl = m_expandedDirs.value(directoryUrl); } else { parentUrl = directoryUrl.adjusted(QUrl::StripTrailingSlash); } if (m_requestRole[ExpandedParentsCountRole]) { // If the expanding of items is enabled, the call // dirLister->openUrl(url, KDirLister::Keep) in KFileItemModel::setExpanded() // might result in emitting the same items twice due to the Keep-parameter. // This case happens if an item gets expanded, collapsed and expanded again // before the items could be loaded for the first expansion. if (index(items.first().url()) >= 0) { // The items are already part of the model. return; } if (directoryUrl != directory()) { // To be able to compare whether the new items may be inserted as children // of a parent item the pending items must be added to the model first. dispatchPendingItemsToInsert(); } // KDirLister keeps the children of items that got expanded once even if // they got collapsed again with KFileItemModel::setExpanded(false). So it must be // checked whether the parent for new items is still expanded. const int parentIndex = index(parentUrl); if (parentIndex >= 0 && !m_itemData[parentIndex]->values.value("isExpanded").toBool()) { // The parent is not expanded. return; } } QList itemDataList = createItemDataList(parentUrl, items); if (!m_filter.hasSetFilters()) { m_pendingItemsToInsert.append(itemDataList); } else { // The name or type filter is active. Hide filtered items // before inserting them into the model and remember // the filtered items in m_filteredItems. foreach (ItemData* itemData, itemDataList) { if (m_filter.matches(itemData->item)) { m_pendingItemsToInsert.append(itemData); } else { m_filteredItems.insert(itemData->item, itemData); } } } if (!m_maximumUpdateIntervalTimer->isActive()) { // Assure that items get dispatched if no completed() or canceled() signal is // emitted during the maximum update interval. m_maximumUpdateIntervalTimer->start(); } } void KFileItemModel::slotItemsDeleted(const KFileItemList& items) { dispatchPendingItemsToInsert(); QVector indexesToRemove; indexesToRemove.reserve(items.count()); foreach (const KFileItem& item, items) { const int indexForItem = index(item); if (indexForItem >= 0) { indexesToRemove.append(indexForItem); } else { // Probably the item has been filtered. QHash::iterator it = m_filteredItems.find(item); if (it != m_filteredItems.end()) { delete it.value(); m_filteredItems.erase(it); } } } std::sort(indexesToRemove.begin(), indexesToRemove.end()); if (m_requestRole[ExpandedParentsCountRole] && !m_expandedDirs.isEmpty()) { // Assure that removing a parent item also results in removing all children QVector indexesToRemoveWithChildren; indexesToRemoveWithChildren.reserve(m_itemData.count()); const int itemCount = m_itemData.count(); foreach (int index, indexesToRemove) { indexesToRemoveWithChildren.append(index); const int parentLevel = expandedParentsCount(index); int childIndex = index + 1; while (childIndex < itemCount && expandedParentsCount(childIndex) > parentLevel) { indexesToRemoveWithChildren.append(childIndex); ++childIndex; } } indexesToRemove = indexesToRemoveWithChildren; } const KItemRangeList itemRanges = KItemRangeList::fromSortedContainer(indexesToRemove); removeFilteredChildren(itemRanges); removeItems(itemRanges, DeleteItemData); } void KFileItemModel::slotRefreshItems(const QList >& items) { Q_ASSERT(!items.isEmpty()); #ifdef KFILEITEMMODEL_DEBUG qCDebug(DolphinDebug) << "Refreshing" << items.count() << "items"; #endif // Get the indexes of all items that have been refreshed QList indexes; indexes.reserve(items.count()); QSet changedRoles; QListIterator > it(items); while (it.hasNext()) { const QPair& itemPair = it.next(); const KFileItem& oldItem = itemPair.first; const KFileItem& newItem = itemPair.second; const int indexForItem = index(oldItem); if (indexForItem >= 0) { m_itemData[indexForItem]->item = newItem; // Keep old values as long as possible if they could not retrieved synchronously yet. // The update of the values will be done asynchronously by KFileItemModelRolesUpdater. QHashIterator it(retrieveData(newItem, m_itemData.at(indexForItem)->parent)); QHash& values = m_itemData[indexForItem]->values; while (it.hasNext()) { it.next(); const QByteArray& role = it.key(); if (values.value(role) != it.value()) { values.insert(role, it.value()); changedRoles.insert(role); } } m_items.remove(oldItem.url()); m_items.insert(newItem.url(), indexForItem); indexes.append(indexForItem); } else { // Check if 'oldItem' is one of the filtered items. QHash::iterator it = m_filteredItems.find(oldItem); if (it != m_filteredItems.end()) { ItemData* itemData = it.value(); itemData->item = newItem; // The data stored in 'values' might have changed. Therefore, we clear // 'values' and re-populate it the next time it is requested via data(int). itemData->values.clear(); m_filteredItems.erase(it); m_filteredItems.insert(newItem, itemData); } } } // If the changed items have been created recently, they might not be in m_items yet. // In that case, the list 'indexes' might be empty. if (indexes.isEmpty()) { return; } // Extract the item-ranges out of the changed indexes std::sort(indexes.begin(), indexes.end()); const KItemRangeList itemRangeList = KItemRangeList::fromSortedContainer(indexes); emitItemsChangedAndTriggerResorting(itemRangeList, changedRoles); } void KFileItemModel::slotClear() { #ifdef KFILEITEMMODEL_DEBUG qCDebug(DolphinDebug) << "Clearing all items"; #endif qDeleteAll(m_filteredItems); m_filteredItems.clear(); m_groups.clear(); m_maximumUpdateIntervalTimer->stop(); m_resortAllItemsTimer->stop(); qDeleteAll(m_pendingItemsToInsert); m_pendingItemsToInsert.clear(); const int removedCount = m_itemData.count(); if (removedCount > 0) { qDeleteAll(m_itemData); m_itemData.clear(); m_items.clear(); emit itemsRemoved(KItemRangeList() << KItemRange(0, removedCount)); } m_expandedDirs.clear(); } void KFileItemModel::slotSortingChoiceChanged() { loadSortingSettings(); resortAllItems(); } void KFileItemModel::dispatchPendingItemsToInsert() { if (!m_pendingItemsToInsert.isEmpty()) { insertItems(m_pendingItemsToInsert); m_pendingItemsToInsert.clear(); } } void KFileItemModel::insertItems(QList& newItems) { if (newItems.isEmpty()) { return; } #ifdef KFILEITEMMODEL_DEBUG QElapsedTimer timer; timer.start(); qCDebug(DolphinDebug) << "==========================================================="; qCDebug(DolphinDebug) << "Inserting" << newItems.count() << "items"; #endif m_groups.clear(); prepareItemsForSorting(newItems); if (m_sortRole == NameRole && m_naturalSorting) { // Natural sorting of items can be very slow. However, it becomes much // faster if the input sequence is already mostly sorted. Therefore, we // first sort 'newItems' according to the QStrings returned by // KFileItem::text() using QString::operator<(), which is quite fast. parallelMergeSort(newItems.begin(), newItems.end(), nameLessThan, QThread::idealThreadCount()); } sort(newItems.begin(), newItems.end()); #ifdef KFILEITEMMODEL_DEBUG qCDebug(DolphinDebug) << "[TIME] Sorting:" << timer.elapsed(); #endif KItemRangeList itemRanges; const int existingItemCount = m_itemData.count(); const int newItemCount = newItems.count(); const int totalItemCount = existingItemCount + newItemCount; if (existingItemCount == 0) { // Optimization for the common special case that there are no // items in the model yet. Happens, e.g., when entering a folder. m_itemData = newItems; itemRanges << KItemRange(0, newItemCount); } else { m_itemData.reserve(totalItemCount); for (int i = existingItemCount; i < totalItemCount; ++i) { m_itemData.append(nullptr); } // We build the new list m_itemData in reverse order to minimize // the number of moves and guarantee O(N) complexity. int targetIndex = totalItemCount - 1; int sourceIndexExistingItems = existingItemCount - 1; int sourceIndexNewItems = newItemCount - 1; int rangeCount = 0; while (sourceIndexNewItems >= 0) { ItemData* newItem = newItems.at(sourceIndexNewItems); if (sourceIndexExistingItems >= 0 && lessThan(newItem, m_itemData.at(sourceIndexExistingItems), m_collator)) { // Move an existing item to its new position. If any new items // are behind it, push the item range to itemRanges. if (rangeCount > 0) { itemRanges << KItemRange(sourceIndexExistingItems + 1, rangeCount); rangeCount = 0; } m_itemData[targetIndex] = m_itemData.at(sourceIndexExistingItems); --sourceIndexExistingItems; } else { // Insert a new item into the list. ++rangeCount; m_itemData[targetIndex] = newItem; --sourceIndexNewItems; } --targetIndex; } // Push the final item range to itemRanges. if (rangeCount > 0) { itemRanges << KItemRange(sourceIndexExistingItems + 1, rangeCount); } // Note that itemRanges is still sorted in reverse order. std::reverse(itemRanges.begin(), itemRanges.end()); } // The indexes in m_items are not correct anymore. Therefore, we clear m_items. // It will be re-populated with the updated indices if index(const QUrl&) is called. m_items.clear(); emit itemsInserted(itemRanges); #ifdef KFILEITEMMODEL_DEBUG qCDebug(DolphinDebug) << "[TIME] Inserting of" << newItems.count() << "items:" << timer.elapsed(); #endif } void KFileItemModel::removeItems(const KItemRangeList& itemRanges, RemoveItemsBehavior behavior) { if (itemRanges.isEmpty()) { return; } m_groups.clear(); // Step 1: Remove the items from m_itemData, and free the ItemData. int removedItemsCount = 0; foreach (const KItemRange& range, itemRanges) { removedItemsCount += range.count; for (int index = range.index; index < range.index + range.count; ++index) { if (behavior == DeleteItemData) { delete m_itemData.at(index); } m_itemData[index] = nullptr; } } // Step 2: Remove the ItemData pointers from the list m_itemData. int target = itemRanges.at(0).index; int source = itemRanges.at(0).index + itemRanges.at(0).count; int nextRange = 1; const int oldItemDataCount = m_itemData.count(); while (source < oldItemDataCount) { m_itemData[target] = m_itemData[source]; ++target; ++source; if (nextRange < itemRanges.count() && source == itemRanges.at(nextRange).index) { // Skip the items in the next removed range. source += itemRanges.at(nextRange).count; ++nextRange; } } m_itemData.erase(m_itemData.end() - removedItemsCount, m_itemData.end()); // The indexes in m_items are not correct anymore. Therefore, we clear m_items. // It will be re-populated with the updated indices if index(const QUrl&) is called. m_items.clear(); emit itemsRemoved(itemRanges); } QList KFileItemModel::createItemDataList(const QUrl& parentUrl, const KFileItemList& items) const { if (m_sortRole == TypeRole) { // Try to resolve the MIME-types synchronously to prevent a reordering of // the items when sorting by type (per default MIME-types are resolved // asynchronously by KFileItemModelRolesUpdater). determineMimeTypes(items, 200); } const int parentIndex = index(parentUrl); ItemData* parentItem = parentIndex < 0 ? nullptr : m_itemData.at(parentIndex); QList itemDataList; itemDataList.reserve(items.count()); foreach (const KFileItem& item, items) { ItemData* itemData = new ItemData(); itemData->item = item; itemData->parent = parentItem; itemDataList.append(itemData); } return itemDataList; } void KFileItemModel::prepareItemsForSorting(QList& itemDataList) { switch (m_sortRole) { case PermissionsRole: case OwnerRole: case GroupRole: case DestinationRole: case PathRole: case DeletionTimeRole: // These roles can be determined with retrieveData, and they have to be stored // in the QHash "values" for the sorting. foreach (ItemData* itemData, itemDataList) { if (itemData->values.isEmpty()) { itemData->values = retrieveData(itemData->item, itemData->parent); } } break; case TypeRole: // At least store the data including the file type for items with known MIME type. foreach (ItemData* itemData, itemDataList) { if (itemData->values.isEmpty()) { const KFileItem item = itemData->item; if (item.isDir() || item.isMimeTypeKnown()) { itemData->values = retrieveData(itemData->item, itemData->parent); } } } break; default: // The other roles are either resolved by KFileItemModelRolesUpdater // (this includes the SizeRole for directories), or they do not need // to be stored in the QHash "values" for sorting because the data can // be retrieved directly from the KFileItem (NameRole, SizeRole for files, // DateRole). break; } } int KFileItemModel::expandedParentsCount(const ItemData* data) { // The hash 'values' is only guaranteed to contain the key "expandedParentsCount" // if the corresponding item is expanded, and it is not a top-level item. const ItemData* parent = data->parent; if (parent) { if (parent->parent) { Q_ASSERT(parent->values.contains("expandedParentsCount")); return parent->values.value("expandedParentsCount").toInt() + 1; } else { return 1; } } else { return 0; } } void KFileItemModel::removeExpandedItems() { QVector indexesToRemove; const int maxIndex = m_itemData.count() - 1; for (int i = 0; i <= maxIndex; ++i) { const ItemData* itemData = m_itemData.at(i); if (itemData->parent) { indexesToRemove.append(i); } } removeItems(KItemRangeList::fromSortedContainer(indexesToRemove), DeleteItemData); m_expandedDirs.clear(); // Also remove all filtered items which have a parent. QHash::iterator it = m_filteredItems.begin(); const QHash::iterator end = m_filteredItems.end(); while (it != end) { if (it.value()->parent) { delete it.value(); it = m_filteredItems.erase(it); } else { ++it; } } } void KFileItemModel::emitItemsChangedAndTriggerResorting(const KItemRangeList& itemRanges, const QSet& changedRoles) { emit itemsChanged(itemRanges, changedRoles); // Trigger a resorting if necessary. Note that this can happen even if the sort // role has not changed at all because the file name can be used as a fallback. if (changedRoles.contains(sortRole()) || changedRoles.contains(roleForType(NameRole))) { foreach (const KItemRange& range, itemRanges) { bool needsResorting = false; const int first = range.index; const int last = range.index + range.count - 1; // Resorting the model is necessary if // (a) The first item in the range is "lessThan" its predecessor, // (b) the successor of the last item is "lessThan" the last item, or // (c) the internal order of the items in the range is incorrect. if (first > 0 && lessThan(m_itemData.at(first), m_itemData.at(first - 1), m_collator)) { needsResorting = true; } else if (last < count() - 1 && lessThan(m_itemData.at(last + 1), m_itemData.at(last), m_collator)) { needsResorting = true; } else { for (int index = first; index < last; ++index) { if (lessThan(m_itemData.at(index + 1), m_itemData.at(index), m_collator)) { needsResorting = true; break; } } } if (needsResorting) { m_resortAllItemsTimer->start(); return; } } } if (groupedSorting() && changedRoles.contains(sortRole())) { // The position is still correct, but the groups might have changed // if the changed item is either the first or the last item in a // group. // In principle, we could try to find out if the item really is the // first or last one in its group and then update the groups // (possibly with a delayed timer to make sure that we don't // re-calculate the groups very often if items are updated one by // one), but starting m_resortAllItemsTimer is easier. m_resortAllItemsTimer->start(); } } void KFileItemModel::resetRoles() { for (int i = 0; i < RolesCount; ++i) { m_requestRole[i] = false; } } KFileItemModel::RoleType KFileItemModel::typeForRole(const QByteArray& role) const { static QHash roles; if (roles.isEmpty()) { // Insert user visible roles that can be accessed with // KFileItemModel::roleInformation() int count = 0; const RoleInfoMap* map = rolesInfoMap(count); for (int i = 0; i < count; ++i) { roles.insert(map[i].role, map[i].roleType); } // Insert internal roles (take care to synchronize the implementation // with KFileItemModel::roleForType() in case if a change is done). roles.insert("isDir", IsDirRole); roles.insert("isLink", IsLinkRole); roles.insert("isHidden", IsHiddenRole); roles.insert("isExpanded", IsExpandedRole); roles.insert("isExpandable", IsExpandableRole); roles.insert("expandedParentsCount", ExpandedParentsCountRole); Q_ASSERT(roles.count() == RolesCount); } return roles.value(role, NoRole); } QByteArray KFileItemModel::roleForType(RoleType roleType) const { static QHash roles; if (roles.isEmpty()) { // Insert user visible roles that can be accessed with // KFileItemModel::roleInformation() int count = 0; const RoleInfoMap* map = rolesInfoMap(count); for (int i = 0; i < count; ++i) { roles.insert(map[i].roleType, map[i].role); } // Insert internal roles (take care to synchronize the implementation // with KFileItemModel::typeForRole() in case if a change is done). roles.insert(IsDirRole, "isDir"); roles.insert(IsLinkRole, "isLink"); roles.insert(IsHiddenRole, "isHidden"); roles.insert(IsExpandedRole, "isExpanded"); roles.insert(IsExpandableRole, "isExpandable"); roles.insert(ExpandedParentsCountRole, "expandedParentsCount"); Q_ASSERT(roles.count() == RolesCount); }; return roles.value(roleType); } QHash KFileItemModel::retrieveData(const KFileItem& item, const ItemData* parent) const { // It is important to insert only roles that are fast to retrieve. E.g. // KFileItem::iconName() can be very expensive if the MIME-type is unknown // and hence will be retrieved asynchronously by KFileItemModelRolesUpdater. QHash data; data.insert(sharedValue("url"), item.url()); const bool isDir = item.isDir(); if (m_requestRole[IsDirRole] && isDir) { data.insert(sharedValue("isDir"), true); } if (m_requestRole[IsLinkRole] && item.isLink()) { data.insert(sharedValue("isLink"), true); } if (m_requestRole[IsHiddenRole]) { data.insert(sharedValue("isHidden"), item.isHidden()); } if (m_requestRole[NameRole]) { data.insert(sharedValue("text"), item.text()); } if (m_requestRole[SizeRole] && !isDir) { data.insert(sharedValue("size"), item.size()); } if (m_requestRole[ModificationTimeRole]) { // Don't use KFileItem::timeString() or KFileItem::time() as this is too expensive when // having several thousands of items. Instead read the raw number from UDSEntry directly // and the formatting of the date-time will be done on-demand by the view when the date will be shown. const long long dateTime = item.entry().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1); data.insert(sharedValue("modificationtime"), dateTime); } if (m_requestRole[CreationTimeRole]) { // Don't use KFileItem::timeString() or KFileItem::time() as this is too expensive when // having several thousands of items. Instead read the raw number from UDSEntry directly // and the formatting of the date-time will be done on-demand by the view when the date will be shown. const long long dateTime = item.entry().numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1); data.insert(sharedValue("creationtime"), dateTime); } if (m_requestRole[AccessTimeRole]) { // Don't use KFileItem::timeString() or KFileItem::time() as this is too expensive when // having several thousands of items. Instead read the raw number from UDSEntry directly // and the formatting of the date-time will be done on-demand by the view when the date will be shown. const long long dateTime = item.entry().numberValue(KIO::UDSEntry::UDS_ACCESS_TIME, -1); data.insert(sharedValue("accesstime"), dateTime); } if (m_requestRole[PermissionsRole]) { data.insert(sharedValue("permissions"), item.permissionsString()); } if (m_requestRole[OwnerRole]) { data.insert(sharedValue("owner"), item.user()); } if (m_requestRole[GroupRole]) { data.insert(sharedValue("group"), item.group()); } if (m_requestRole[DestinationRole]) { QString destination = item.linkDest(); if (destination.isEmpty()) { destination = QLatin1Char('-'); } data.insert(sharedValue("destination"), destination); } if (m_requestRole[PathRole]) { QString path; if (item.url().scheme() == QLatin1String("trash")) { path = item.entry().stringValue(KIO::UDSEntry::UDS_EXTRA); } else { // For performance reasons cache the home-path in a static QString // (see QDir::homePath() for more details) static QString homePath; if (homePath.isEmpty()) { homePath = QDir::homePath(); } path = item.localPath(); if (path.startsWith(homePath)) { path.replace(0, homePath.length(), QLatin1Char('~')); } } const int index = path.lastIndexOf(item.text()); path = path.mid(0, index - 1); data.insert(sharedValue("path"), path); } if (m_requestRole[DeletionTimeRole]) { QDateTime deletionTime; if (item.url().scheme() == QLatin1String("trash")) { deletionTime = QDateTime::fromString(item.entry().stringValue(KIO::UDSEntry::UDS_EXTRA + 1), Qt::ISODate); } data.insert(sharedValue("deletiontime"), deletionTime); } if (m_requestRole[IsExpandableRole] && isDir) { data.insert(sharedValue("isExpandable"), true); } if (m_requestRole[ExpandedParentsCountRole]) { if (parent) { const int level = expandedParentsCount(parent) + 1; data.insert(sharedValue("expandedParentsCount"), level); } } if (item.isMimeTypeKnown()) { data.insert(sharedValue("iconName"), item.iconName()); if (m_requestRole[TypeRole]) { data.insert(sharedValue("type"), item.mimeComment()); } } else if (m_requestRole[TypeRole] && isDir) { static const QString folderMimeType = item.mimeComment(); data.insert(sharedValue("type"), folderMimeType); } return data; } bool KFileItemModel::lessThan(const ItemData* a, const ItemData* b, const QCollator& collator) const { int result = 0; if (a->parent != b->parent) { const int expansionLevelA = expandedParentsCount(a); const int expansionLevelB = expandedParentsCount(b); // If b has a higher expansion level than a, check if a is a parent // of b, and make sure that both expansion levels are equal otherwise. for (int i = expansionLevelB; i > expansionLevelA; --i) { if (b->parent == a) { return true; } b = b->parent; } // If a has a higher expansion level than a, check if b is a parent // of a, and make sure that both expansion levels are equal otherwise. for (int i = expansionLevelA; i > expansionLevelB; --i) { if (a->parent == b) { return false; } a = a->parent; } Q_ASSERT(expandedParentsCount(a) == expandedParentsCount(b)); // Compare the last parents of a and b which are different. while (a->parent != b->parent) { a = a->parent; b = b->parent; } } if (m_sortDirsFirst || m_sortRole == SizeRole) { const bool isDirA = a->item.isDir(); const bool isDirB = b->item.isDir(); if (isDirA && !isDirB) { return true; } else if (!isDirA && isDirB) { return false; } } result = sortRoleCompare(a, b, collator); return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0; } void KFileItemModel::sort(const QList::iterator &begin, const QList::iterator &end) const { auto lambdaLessThan = [&] (const KFileItemModel::ItemData* a, const KFileItemModel::ItemData* b) { return lessThan(a, b, m_collator); }; if (m_sortRole == NameRole) { // Sorting by name can be expensive, in particular if natural sorting is // enabled. Use all CPU cores to speed up the sorting process. static const int numberOfThreads = QThread::idealThreadCount(); parallelMergeSort(begin, end, lambdaLessThan, numberOfThreads); } else { // Sorting by other roles is quite fast. Use only one thread to prevent // problems caused by non-reentrant comparison functions, see // https://bugs.kde.org/show_bug.cgi?id=312679 mergeSort(begin, end, lambdaLessThan); } } int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b, const QCollator& collator) const { const KFileItem& itemA = a->item; const KFileItem& itemB = b->item; int result = 0; switch (m_sortRole) { case NameRole: // The name role is handled as default fallback after the switch break; case SizeRole: { if (itemA.isDir()) { // See "if (m_sortFoldersFirst || m_sortRole == SizeRole)" in KFileItemModel::lessThan(): Q_ASSERT(itemB.isDir()); const QVariant valueA = a->values.value("size"); const QVariant valueB = b->values.value("size"); if (valueA.isNull() && valueB.isNull()) { result = 0; } else if (valueA.isNull()) { result = -1; } else if (valueB.isNull()) { result = +1; } else { result = valueA.toInt() - valueB.toInt(); } } else { // See "if (m_sortFoldersFirst || m_sortRole == SizeRole)" in KFileItemModel::lessThan(): Q_ASSERT(!itemB.isDir()); const KIO::filesize_t sizeA = itemA.size(); const KIO::filesize_t sizeB = itemB.size(); if (sizeA > sizeB) { result = +1; } else if (sizeA < sizeB) { result = -1; } else { result = 0; } } break; } case ModificationTimeRole: { const long long dateTimeA = itemA.entry().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1); const long long dateTimeB = itemB.entry().numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1); if (dateTimeA < dateTimeB) { result = -1; } else if (dateTimeA > dateTimeB) { result = +1; } break; } case CreationTimeRole: { const long long dateTimeA = itemA.entry().numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1); const long long dateTimeB = itemB.entry().numberValue(KIO::UDSEntry::UDS_CREATION_TIME, -1); if (dateTimeA < dateTimeB) { result = -1; } else if (dateTimeA > dateTimeB) { result = +1; } break; } case DeletionTimeRole: { const QDateTime dateTimeA = a->values.value("deletiontime").toDateTime(); const QDateTime dateTimeB = b->values.value("deletiontime").toDateTime(); if (dateTimeA < dateTimeB) { result = -1; } else if (dateTimeA > dateTimeB) { result = +1; } break; } case RatingRole: case WidthRole: case HeightRole: case WordCountRole: case LineCountRole: case TrackRole: case ReleaseYearRole: { result = a->values.value(roleForType(m_sortRole)).toInt() - b->values.value(roleForType(m_sortRole)).toInt(); break; } default: { const QByteArray role = roleForType(m_sortRole); const QString roleValueA = a->values.value(role).toString(); const QString roleValueB = b->values.value(role).toString(); if (!roleValueA.isEmpty() && roleValueB.isEmpty()) { result = -1; } else if (roleValueA.isEmpty() && !roleValueB.isEmpty()) { result = +1; } else { result = QString::compare(roleValueA, roleValueB); } break; } } if (result != 0) { // The current sort role was sufficient to define an order return result; } // Fallback #1: Compare the text of the items result = stringCompare(itemA.text(), itemB.text(), collator); if (result != 0) { return result; } // Fallback #2: KFileItem::text() may not be unique in case UDS_DISPLAY_NAME is used result = stringCompare(itemA.name(), itemB.name(), collator); if (result != 0) { return result; } // Fallback #3: It must be assured that the sort order is always unique even if two values have been // equal. In this case a comparison of the URL is done which is unique in all cases // within KDirLister. return QString::compare(itemA.url().url(), itemB.url().url(), Qt::CaseSensitive); } int KFileItemModel::stringCompare(const QString& a, const QString& b, const QCollator& collator) const { if (m_naturalSorting) { return collator.compare(a, b); } const int result = QString::compare(a, b, collator.caseSensitivity()); if (result != 0 || collator.caseSensitivity() == Qt::CaseSensitive) { // Only return the result, if the strings are not equal. If they are equal by a case insensitive // comparison, still a deterministic sort order is required. A case sensitive // comparison is done as fallback. return result; } return QString::compare(a, b, Qt::CaseSensitive); } QList > KFileItemModel::nameRoleGroups() const { Q_ASSERT(!m_itemData.isEmpty()); const int maxIndex = count() - 1; QList > groups; QString groupValue; QChar firstChar; for (int i = 0; i <= maxIndex; ++i) { if (isChildItem(i)) { continue; } const QString name = m_itemData.at(i)->item.text(); // Use the first character of the name as group indication QChar newFirstChar = name.at(0).toUpper(); if (newFirstChar == QLatin1Char('~') && name.length() > 1) { newFirstChar = name.at(1).toUpper(); } if (firstChar != newFirstChar) { QString newGroupValue; if (newFirstChar.isLetter()) { if (m_collator.compare(newFirstChar, QChar(QLatin1Char('A'))) >= 0 && m_collator.compare(newFirstChar, QChar(QLatin1Char('Z'))) <= 0) { // WARNING! Symbols based on latin 'Z' like 'Z' with acute are treated wrong as non Latin and put in a new group. // Try to find a matching group in the range 'A' to 'Z'. static std::vector lettersAtoZ; lettersAtoZ.reserve('Z' - 'A' + 1); if (lettersAtoZ.empty()) { for (char c = 'A'; c <= 'Z'; ++c) { lettersAtoZ.push_back(QLatin1Char(c)); } } auto localeAwareLessThan = [this](QChar c1, QChar c2) -> bool { return m_collator.compare(c1, c2) < 0; }; std::vector::iterator it = std::lower_bound(lettersAtoZ.begin(), lettersAtoZ.end(), newFirstChar, localeAwareLessThan); if (it != lettersAtoZ.end()) { if (localeAwareLessThan(newFirstChar, *it)) { // newFirstChar belongs to the group preceding *it. // Example: for an umlaut 'A' in the German locale, *it would be 'B' now. --it; } newGroupValue = *it; } } else { // Symbols from non Latin-based scripts newGroupValue = newFirstChar; } } else if (newFirstChar >= QLatin1Char('0') && newFirstChar <= QLatin1Char('9')) { // Apply group '0 - 9' for any name that starts with a digit newGroupValue = i18nc("@title:group Groups that start with a digit", "0 - 9"); } else { newGroupValue = i18nc("@title:group", "Others"); } if (newGroupValue != groupValue) { groupValue = newGroupValue; groups.append(QPair(i, newGroupValue)); } firstChar = newFirstChar; } } return groups; } QList > KFileItemModel::sizeRoleGroups() const { Q_ASSERT(!m_itemData.isEmpty()); const int maxIndex = count() - 1; QList > groups; QString groupValue; for (int i = 0; i <= maxIndex; ++i) { if (isChildItem(i)) { continue; } const KFileItem& item = m_itemData.at(i)->item; const KIO::filesize_t fileSize = !item.isNull() ? item.size() : ~0U; QString newGroupValue; if (!item.isNull() && item.isDir()) { newGroupValue = i18nc("@title:group Size", "Folders"); } else if (fileSize < 5 * 1024 * 1024) { newGroupValue = i18nc("@title:group Size", "Small"); } else if (fileSize < 10 * 1024 * 1024) { newGroupValue = i18nc("@title:group Size", "Medium"); } else { newGroupValue = i18nc("@title:group Size", "Big"); } if (newGroupValue != groupValue) { groupValue = newGroupValue; groups.append(QPair(i, newGroupValue)); } } return groups; } QList > KFileItemModel::timeRoleGroups(const std::function &fileTimeCb) const { Q_ASSERT(!m_itemData.isEmpty()); const int maxIndex = count() - 1; QList > groups; const QDate currentDate = QDate::currentDate(); QDate previousFileDate; QString groupValue; for (int i = 0; i <= maxIndex; ++i) { if (isChildItem(i)) { continue; } const QDateTime fileTime = fileTimeCb(m_itemData.at(i)); const QDate fileDate = fileTime.date(); if (fileDate == previousFileDate) { // The current item is in the same group as the previous item continue; } previousFileDate = fileDate; const int daysDistance = fileDate.daysTo(currentDate); QString newGroupValue; if (currentDate.year() == fileDate.year() && currentDate.month() == fileDate.month()) { switch (daysDistance / 7) { case 0: switch (daysDistance) { case 0: newGroupValue = i18nc("@title:group Date", "Today"); break; case 1: newGroupValue = i18nc("@title:group Date", "Yesterday"); break; default: newGroupValue = fileTime.toString( i18nc("@title:group Date: The week day name: dddd", "dddd")); newGroupValue = i18nc("Can be used to script translation of \"dddd\"" "with context @title:group Date", "%1", newGroupValue); } break; case 1: newGroupValue = i18nc("@title:group Date", "One Week Ago"); break; case 2: newGroupValue = i18nc("@title:group Date", "Two Weeks Ago"); break; case 3: newGroupValue = i18nc("@title:group Date", "Three Weeks Ago"); break; case 4: case 5: newGroupValue = i18nc("@title:group Date", "Earlier this Month"); break; default: Q_ASSERT(false); } } else { const QDate lastMonthDate = currentDate.addMonths(-1); if (lastMonthDate.year() == fileDate.year() && lastMonthDate.month() == fileDate.month()) { if (daysDistance == 1) { const KLocalizedString format = ki18nc("@title:group Date: " "MMMM is full month name in current locale, and yyyy is " "full year number", "'Yesterday' (MMMM, yyyy)"); const QString translatedFormat = format.toString(); if (translatedFormat.count(QLatin1Char('\'')) == 2) { newGroupValue = fileTime.toString(translatedFormat); newGroupValue = i18nc("Can be used to script translation of " "\"'Yesterday' (MMMM, yyyy)\" with context @title:group Date", "%1", newGroupValue); } else { qCWarning(DolphinDebug).nospace() << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; const QString untranslatedFormat = format.toString({ QLatin1String("en_US") }); newGroupValue = fileTime.toString(untranslatedFormat); } } else if (daysDistance <= 7) { newGroupValue = fileTime.toString(i18nc("@title:group Date: " "The week day name: dddd, MMMM is full month name " "in current locale, and yyyy is full year number", "dddd (MMMM, yyyy)")); newGroupValue = i18nc("Can be used to script translation of " "\"dddd (MMMM, yyyy)\" with context @title:group Date", "%1", newGroupValue); } else if (daysDistance <= 7 * 2) { const KLocalizedString format = ki18nc("@title:group Date: " "MMMM is full month name in current locale, and yyyy is " "full year number", "'One Week Ago' (MMMM, yyyy)"); const QString translatedFormat = format.toString(); if (translatedFormat.count(QLatin1Char('\'')) == 2) { newGroupValue = fileTime.toString(translatedFormat); newGroupValue = i18nc("Can be used to script translation of " "\"'One Week Ago' (MMMM, yyyy)\" with context @title:group Date", "%1", newGroupValue); } else { qCWarning(DolphinDebug).nospace() << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; const QString untranslatedFormat = format.toString({ QLatin1String("en_US") }); newGroupValue = fileTime.toString(untranslatedFormat); } } else if (daysDistance <= 7 * 3) { const KLocalizedString format = ki18nc("@title:group Date: " "MMMM is full month name in current locale, and yyyy is " "full year number", "'Two Weeks Ago' (MMMM, yyyy)"); const QString translatedFormat = format.toString(); if (translatedFormat.count(QLatin1Char('\'')) == 2) { newGroupValue = fileTime.toString(translatedFormat); newGroupValue = i18nc("Can be used to script translation of " "\"'Two Weeks Ago' (MMMM, yyyy)\" with context @title:group Date", "%1", newGroupValue); } else { qCWarning(DolphinDebug).nospace() << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; const QString untranslatedFormat = format.toString({ QLatin1String("en_US") }); newGroupValue = fileTime.toString(untranslatedFormat); } } else if (daysDistance <= 7 * 4) { const KLocalizedString format = ki18nc("@title:group Date: " "MMMM is full month name in current locale, and yyyy is " "full year number", "'Three Weeks Ago' (MMMM, yyyy)"); const QString translatedFormat = format.toString(); if (translatedFormat.count(QLatin1Char('\'')) == 2) { newGroupValue = fileTime.toString(translatedFormat); newGroupValue = i18nc("Can be used to script translation of " "\"'Three Weeks Ago' (MMMM, yyyy)\" with context @title:group Date", "%1", newGroupValue); } else { qCWarning(DolphinDebug).nospace() << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; const QString untranslatedFormat = format.toString({ QLatin1String("en_US") }); newGroupValue = fileTime.toString(untranslatedFormat); } } else { const KLocalizedString format = ki18nc("@title:group Date: " "MMMM is full month name in current locale, and yyyy is " "full year number", "'Earlier on' MMMM, yyyy"); const QString translatedFormat = format.toString(); if (translatedFormat.count(QLatin1Char('\'')) == 2) { newGroupValue = fileTime.toString(translatedFormat); newGroupValue = i18nc("Can be used to script translation of " "\"'Earlier on' MMMM, yyyy\" with context @title:group Date", "%1", newGroupValue); } else { qCWarning(DolphinDebug).nospace() << "A wrong translation was found: " << translatedFormat << ". Please file a bug report at bugs.kde.org"; const QString untranslatedFormat = format.toString({ QLatin1String("en_US") }); newGroupValue = fileTime.toString(untranslatedFormat); } } } else { newGroupValue = fileTime.toString(i18nc("@title:group " "The month and year: MMMM is full month name in current locale, " "and yyyy is full year number", "MMMM, yyyy")); newGroupValue = i18nc("Can be used to script translation of " "\"MMMM, yyyy\" with context @title:group Date", "%1", newGroupValue); } } if (newGroupValue != groupValue) { groupValue = newGroupValue; groups.append(QPair(i, newGroupValue)); } } return groups; } QList > KFileItemModel::permissionRoleGroups() const { Q_ASSERT(!m_itemData.isEmpty()); const int maxIndex = count() - 1; QList > groups; QString permissionsString; QString groupValue; for (int i = 0; i <= maxIndex; ++i) { if (isChildItem(i)) { continue; } const ItemData* itemData = m_itemData.at(i); const QString newPermissionsString = itemData->values.value("permissions").toString(); if (newPermissionsString == permissionsString) { continue; } permissionsString = newPermissionsString; const QFileInfo info(itemData->item.url().toLocalFile()); // Set user string QString user; if (info.permission(QFile::ReadUser)) { user = i18nc("@item:intext Access permission, concatenated", "Read, "); } if (info.permission(QFile::WriteUser)) { user += i18nc("@item:intext Access permission, concatenated", "Write, "); } if (info.permission(QFile::ExeUser)) { user += i18nc("@item:intext Access permission, concatenated", "Execute, "); } user = user.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : user.mid(0, user.count() - 2); // Set group string QString group; if (info.permission(QFile::ReadGroup)) { group = i18nc("@item:intext Access permission, concatenated", "Read, "); } if (info.permission(QFile::WriteGroup)) { group += i18nc("@item:intext Access permission, concatenated", "Write, "); } if (info.permission(QFile::ExeGroup)) { group += i18nc("@item:intext Access permission, concatenated", "Execute, "); } group = group.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : group.mid(0, group.count() - 2); // Set others string QString others; if (info.permission(QFile::ReadOther)) { others = i18nc("@item:intext Access permission, concatenated", "Read, "); } if (info.permission(QFile::WriteOther)) { others += i18nc("@item:intext Access permission, concatenated", "Write, "); } if (info.permission(QFile::ExeOther)) { others += i18nc("@item:intext Access permission, concatenated", "Execute, "); } others = others.isEmpty() ? i18nc("@item:intext Access permission, concatenated", "Forbidden") : others.mid(0, others.count() - 2); const QString newGroupValue = i18nc("@title:group Files and folders by permissions", "User: %1 | Group: %2 | Others: %3", user, group, others); if (newGroupValue != groupValue) { groupValue = newGroupValue; groups.append(QPair(i, newGroupValue)); } } return groups; } QList > KFileItemModel::ratingRoleGroups() const { Q_ASSERT(!m_itemData.isEmpty()); const int maxIndex = count() - 1; QList > groups; int groupValue = -1; for (int i = 0; i <= maxIndex; ++i) { if (isChildItem(i)) { continue; } const int newGroupValue = m_itemData.at(i)->values.value("rating", 0).toInt(); if (newGroupValue != groupValue) { groupValue = newGroupValue; groups.append(QPair(i, newGroupValue)); } } return groups; } QList > KFileItemModel::genericStringRoleGroups(const QByteArray& role) const { Q_ASSERT(!m_itemData.isEmpty()); const int maxIndex = count() - 1; QList > groups; bool isFirstGroupValue = true; QString groupValue; for (int i = 0; i <= maxIndex; ++i) { if (isChildItem(i)) { continue; } const QString newGroupValue = m_itemData.at(i)->values.value(role).toString(); if (newGroupValue != groupValue || isFirstGroupValue) { groupValue = newGroupValue; groups.append(QPair(i, newGroupValue)); isFirstGroupValue = false; } } return groups; } void KFileItemModel::emitSortProgress(int resolvedCount) { // Be tolerant against a resolvedCount with a wrong range. // Although there should not be a case where KFileItemModelRolesUpdater // (= caller) provides a wrong range, it is important to emit // a useful progress information even if there is an unexpected // implementation issue. const int itemCount = count(); if (resolvedCount >= itemCount) { m_sortingProgressPercent = -1; if (m_resortAllItemsTimer->isActive()) { m_resortAllItemsTimer->stop(); resortAllItems(); } emit directorySortingProgress(100); } else if (itemCount > 0) { resolvedCount = qBound(0, resolvedCount, itemCount); const int progress = resolvedCount * 100 / itemCount; if (m_sortingProgressPercent != progress) { m_sortingProgressPercent = progress; emit directorySortingProgress(progress); } } } const KFileItemModel::RoleInfoMap* KFileItemModel::rolesInfoMap(int& count) { static const RoleInfoMap rolesInfoMap[] = { // | role | roleType | role translation | group translation | requires Baloo | requires indexer { nullptr, NoRole, nullptr, nullptr, nullptr, nullptr, false, false }, { "text", NameRole, I18N_NOOP2_NOSTRIP("@label", "Name"), nullptr, nullptr, false, false }, { "size", SizeRole, I18N_NOOP2_NOSTRIP("@label", "Size"), nullptr, nullptr, false, false }, { "modificationtime", ModificationTimeRole, I18N_NOOP2_NOSTRIP("@label", "Modified"), nullptr, nullptr, false, false }, { "creationtime", CreationTimeRole, I18N_NOOP2_NOSTRIP("@label", "Created"), nullptr, nullptr, false, false }, { "accesstime", AccessTimeRole, I18N_NOOP2_NOSTRIP("@label", "Accessed"), nullptr, nullptr, false, false }, { "type", TypeRole, I18N_NOOP2_NOSTRIP("@label", "Type"), nullptr, nullptr, false, false }, { "rating", RatingRole, I18N_NOOP2_NOSTRIP("@label", "Rating"), nullptr, nullptr, true, false }, { "tags", TagsRole, I18N_NOOP2_NOSTRIP("@label", "Tags"), nullptr, nullptr, true, false }, { "comment", CommentRole, I18N_NOOP2_NOSTRIP("@label", "Comment"), nullptr, nullptr, true, false }, { "title", TitleRole, I18N_NOOP2_NOSTRIP("@label", "Title"), I18N_NOOP2_NOSTRIP("@label", "Document"), true, true }, { "wordCount", WordCountRole, I18N_NOOP2_NOSTRIP("@label", "Word Count"), I18N_NOOP2_NOSTRIP("@label", "Document"), true, true }, { "lineCount", LineCountRole, I18N_NOOP2_NOSTRIP("@label", "Line Count"), I18N_NOOP2_NOSTRIP("@label", "Document"), true, true }, { "imageDateTime", ImageDateTimeRole, I18N_NOOP2_NOSTRIP("@label", "Date Photographed"), I18N_NOOP2_NOSTRIP("@label", "Image"), true, true }, { "width", WidthRole, I18N_NOOP2_NOSTRIP("@label", "Width"), I18N_NOOP2_NOSTRIP("@label", "Image"), true, true }, { "height", HeightRole, I18N_NOOP2_NOSTRIP("@label", "Height"), I18N_NOOP2_NOSTRIP("@label", "Image"), true, true }, { "orientation", OrientationRole, I18N_NOOP2_NOSTRIP("@label", "Orientation"), I18N_NOOP2_NOSTRIP("@label", "Image"), true, true }, { "artist", ArtistRole, I18N_NOOP2_NOSTRIP("@label", "Artist"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true }, { "genre", GenreRole, I18N_NOOP2_NOSTRIP("@label", "Genre"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true }, { "album", AlbumRole, I18N_NOOP2_NOSTRIP("@label", "Album"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true }, { "duration", DurationRole, I18N_NOOP2_NOSTRIP("@label", "Duration"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true }, { "bitrate", BitrateRole, I18N_NOOP2_NOSTRIP("@label", "Bitrate"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true }, { "track", TrackRole, I18N_NOOP2_NOSTRIP("@label", "Track"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true }, { "releaseYear", ReleaseYearRole, I18N_NOOP2_NOSTRIP("@label", "Release Year"), I18N_NOOP2_NOSTRIP("@label", "Audio"), true, true }, { "aspectRatio", AspectRatioRole, I18N_NOOP2_NOSTRIP("@label", "Aspect Ratio"), I18N_NOOP2_NOSTRIP("@label", "Video"), true, true }, { "frameRate", FrameRateRole, I18N_NOOP2_NOSTRIP("@label", "Frame Rate"), I18N_NOOP2_NOSTRIP("@label", "Video"), true, true }, { "path", PathRole, I18N_NOOP2_NOSTRIP("@label", "Path"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, { "deletiontime", DeletionTimeRole, I18N_NOOP2_NOSTRIP("@label", "Deletion Time"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, { "destination", DestinationRole, I18N_NOOP2_NOSTRIP("@label", "Link Destination"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, { "originUrl", OriginUrlRole, I18N_NOOP2_NOSTRIP("@label", "Downloaded From"), I18N_NOOP2_NOSTRIP("@label", "Other"), true, false }, { "permissions", PermissionsRole, I18N_NOOP2_NOSTRIP("@label", "Permissions"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, { "owner", OwnerRole, I18N_NOOP2_NOSTRIP("@label", "Owner"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, { "group", GroupRole, I18N_NOOP2_NOSTRIP("@label", "User Group"), I18N_NOOP2_NOSTRIP("@label", "Other"), false, false }, }; count = sizeof(rolesInfoMap) / sizeof(RoleInfoMap); return rolesInfoMap; } void KFileItemModel::determineMimeTypes(const KFileItemList& items, int timeout) { QElapsedTimer timer; timer.start(); foreach (const KFileItem& item, items) { // krazy:exclude=foreach // Only determine mime types for files here. For directories, // KFileItem::determineMimeType() reads the .directory file inside to // load the icon, but this is not necessary at all if we just need the // type. Some special code for setting the correct mime type for // directories is in retrieveData(). if (!item.isDir()) { item.determineMimeType(); } if (timer.elapsed() > timeout) { // Don't block the user interface, let the remaining items // be resolved asynchronously. return; } } } QByteArray KFileItemModel::sharedValue(const QByteArray& value) { static QSet pool; const QSet::const_iterator it = pool.constFind(value); if (it != pool.constEnd()) { return *it; } else { pool.insert(value); return value; } } bool KFileItemModel::isConsistent() const { // m_items may contain less items than m_itemData because m_items // is populated lazily, see KFileItemModel::index(const QUrl& url). if (m_items.count() > m_itemData.count()) { return false; } for (int i = 0, iMax = count(); i < iMax; ++i) { // Check if m_items and m_itemData are consistent. const KFileItem item = fileItem(i); if (item.isNull()) { qCWarning(DolphinDebug) << "Item" << i << "is null"; return false; } const int itemIndex = index(item); if (itemIndex != i) { qCWarning(DolphinDebug) << "Item" << i << "has a wrong index:" << itemIndex; return false; } // Check if the items are sorted correctly. if (i > 0 && !lessThan(m_itemData.at(i - 1), m_itemData.at(i), m_collator)) { qCWarning(DolphinDebug) << "The order of items" << i - 1 << "and" << i << "is wrong:" << fileItem(i - 1) << fileItem(i); return false; } // Check if all parent-child relationships are consistent. const ItemData* data = m_itemData.at(i); const ItemData* parent = data->parent; if (parent) { if (expandedParentsCount(data) != expandedParentsCount(parent) + 1) { qCWarning(DolphinDebug) << "expandedParentsCount is inconsistent for parent" << parent->item << "and child" << data->item; return false; } const int parentIndex = index(parent->item); if (parentIndex >= i) { qCWarning(DolphinDebug) << "Index" << parentIndex << "of parent" << parent->item << "is not smaller than index" << i << "of child" << data->item; return false; } } } return true; } diff --git a/src/kitemviews/kfileitemmodelrolesupdater.cpp b/src/kitemviews/kfileitemmodelrolesupdater.cpp index 6e0103c79..bf2c84c40 100644 --- a/src/kitemviews/kfileitemmodelrolesupdater.cpp +++ b/src/kitemviews/kfileitemmodelrolesupdater.cpp @@ -1,1195 +1,1195 @@ /*************************************************************************** * Copyright (C) 2011 by Peter Penz * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kfileitemmodelrolesupdater.h" #include "kfileitemmodel.h" #include "private/kdirectorycontentscounter.h" #include "private/kpixmapmodifier.h" #include #include #include #include #include #include #include #include #include #ifdef HAVE_BALOO #include "private/kbaloorolesprovider.h" #include #include #endif #include #include #include #include // #define KFILEITEMMODELROLESUPDATER_DEBUG namespace { // Maximum time in ms that the KFileItemModelRolesUpdater // may perform a blocking operation const int MaxBlockTimeout = 200; // If the number of items is smaller than ResolveAllItemsLimit, // the roles of all items will be resolved. const int ResolveAllItemsLimit = 500; // Not only the visible area, but up to ReadAheadPages before and after // this area will be resolved. const int ReadAheadPages = 5; } KFileItemModelRolesUpdater::KFileItemModelRolesUpdater(KFileItemModel* model, QObject* parent) : QObject(parent), m_state(Idle), m_previewChangedDuringPausing(false), m_iconSizeChangedDuringPausing(false), m_rolesChangedDuringPausing(false), m_previewShown(false), m_enlargeSmallPreviews(true), m_clearPreviews(false), m_finishedItems(), m_model(model), m_iconSize(), m_firstVisibleIndex(0), m_lastVisibleIndex(-1), m_maximumVisibleItems(50), m_roles(), m_resolvableRoles(), m_enabledPlugins(), m_pendingSortRoleItems(), m_pendingIndexes(), m_pendingPreviewItems(), m_previewJob(), m_recentlyChangedItemsTimer(nullptr), m_recentlyChangedItems(), m_changedItems(), m_directoryContentsCounter(nullptr) #ifdef HAVE_BALOO , m_balooFileMonitor(nullptr) #endif { Q_ASSERT(model); const KConfigGroup globalConfig(KSharedConfig::openConfig(), "PreviewSettings"); m_enabledPlugins = globalConfig.readEntry("Plugins", KIO::PreviewJob::defaultPlugins()); connect(m_model, &KFileItemModel::itemsInserted, this, &KFileItemModelRolesUpdater::slotItemsInserted); connect(m_model, &KFileItemModel::itemsRemoved, this, &KFileItemModelRolesUpdater::slotItemsRemoved); connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); connect(m_model, &KFileItemModel::itemsMoved, this, &KFileItemModelRolesUpdater::slotItemsMoved); connect(m_model, &KFileItemModel::sortRoleChanged, this, &KFileItemModelRolesUpdater::slotSortRoleChanged); // Use a timer to prevent that each call of slotItemsChanged() results in a synchronous // resolving of the roles. Postpone the resolving until no update has been done for 1 second. m_recentlyChangedItemsTimer = new QTimer(this); m_recentlyChangedItemsTimer->setInterval(1000); m_recentlyChangedItemsTimer->setSingleShot(true); connect(m_recentlyChangedItemsTimer, &QTimer::timeout, this, &KFileItemModelRolesUpdater::resolveRecentlyChangedItems); m_resolvableRoles.insert("size"); m_resolvableRoles.insert("type"); m_resolvableRoles.insert("isExpandable"); #ifdef HAVE_BALOO m_resolvableRoles += KBalooRolesProvider::instance().roles(); #endif m_directoryContentsCounter = new KDirectoryContentsCounter(m_model, this); connect(m_directoryContentsCounter, &KDirectoryContentsCounter::result, this, &KFileItemModelRolesUpdater::slotDirectoryContentsCountReceived); auto plugins = KPluginLoader::instantiatePlugins(QStringLiteral("kf5/overlayicon"), nullptr, qApp); foreach (QObject *it, plugins) { auto plugin = qobject_cast(it); if (plugin) { m_overlayIconsPlugin.append(plugin); connect(plugin, &KOverlayIconPlugin::overlaysChanged, this, &KFileItemModelRolesUpdater::slotOverlaysChanged); } else { // not our/valid plugin, so delete the created object it->deleteLater(); } } } KFileItemModelRolesUpdater::~KFileItemModelRolesUpdater() { killPreviewJob(); } void KFileItemModelRolesUpdater::setIconSize(const QSize& size) { if (size != m_iconSize) { m_iconSize = size; if (m_state == Paused) { m_iconSizeChangedDuringPausing = true; } else if (m_previewShown) { // An icon size change requires the regenerating of // all previews m_finishedItems.clear(); startUpdating(); } } } QSize KFileItemModelRolesUpdater::iconSize() const { return m_iconSize; } void KFileItemModelRolesUpdater::setVisibleIndexRange(int index, int count) { if (index < 0) { index = 0; } if (count < 0) { count = 0; } if (index == m_firstVisibleIndex && count == m_lastVisibleIndex - m_firstVisibleIndex + 1) { // The range has not been changed return; } m_firstVisibleIndex = index; m_lastVisibleIndex = qMin(index + count - 1, m_model->count() - 1); startUpdating(); } void KFileItemModelRolesUpdater::setMaximumVisibleItems(int count) { m_maximumVisibleItems = count; } void KFileItemModelRolesUpdater::setPreviewsShown(bool show) { if (show == m_previewShown) { return; } m_previewShown = show; if (!show) { m_clearPreviews = true; } updateAllPreviews(); } bool KFileItemModelRolesUpdater::previewsShown() const { return m_previewShown; } void KFileItemModelRolesUpdater::setEnlargeSmallPreviews(bool enlarge) { if (enlarge != m_enlargeSmallPreviews) { m_enlargeSmallPreviews = enlarge; if (m_previewShown) { updateAllPreviews(); } } } bool KFileItemModelRolesUpdater::enlargeSmallPreviews() const { return m_enlargeSmallPreviews; } void KFileItemModelRolesUpdater::setEnabledPlugins(const QStringList& list) { if (m_enabledPlugins != list) { m_enabledPlugins = list; if (m_previewShown) { updateAllPreviews(); } } } void KFileItemModelRolesUpdater::setPaused(bool paused) { if (paused == (m_state == Paused)) { return; } if (paused) { m_state = Paused; killPreviewJob(); } else { const bool updatePreviews = (m_iconSizeChangedDuringPausing && m_previewShown) || m_previewChangedDuringPausing; const bool resolveAll = updatePreviews || m_rolesChangedDuringPausing; if (resolveAll) { m_finishedItems.clear(); } m_iconSizeChangedDuringPausing = false; m_previewChangedDuringPausing = false; m_rolesChangedDuringPausing = false; if (!m_pendingSortRoleItems.isEmpty()) { m_state = ResolvingSortRole; resolveNextSortRole(); } else { m_state = Idle; } startUpdating(); } } void KFileItemModelRolesUpdater::setRoles(const QSet& roles) { if (m_roles != roles) { m_roles = roles; #ifdef HAVE_BALOO // Check whether there is at least one role that must be resolved // with the help of Baloo. If this is the case, a (quite expensive) // resolving will be done in KFileItemModelRolesUpdater::rolesData() and // the role gets watched for changes. const KBalooRolesProvider& rolesProvider = KBalooRolesProvider::instance(); bool hasBalooRole = false; QSetIterator it(roles); while (it.hasNext()) { const QByteArray& role = it.next(); if (rolesProvider.roles().contains(role)) { hasBalooRole = true; break; } } if (hasBalooRole && m_balooConfig.fileIndexingEnabled() && !m_balooFileMonitor) { m_balooFileMonitor = new Baloo::FileMonitor(this); connect(m_balooFileMonitor, &Baloo::FileMonitor::fileMetaDataChanged, this, &KFileItemModelRolesUpdater::applyChangedBalooRoles); } else if (!hasBalooRole && m_balooFileMonitor) { delete m_balooFileMonitor; m_balooFileMonitor = nullptr; } #endif if (m_state == Paused) { m_rolesChangedDuringPausing = true; } else { startUpdating(); } } } QSet KFileItemModelRolesUpdater::roles() const { return m_roles; } bool KFileItemModelRolesUpdater::isPaused() const { return m_state == Paused; } QStringList KFileItemModelRolesUpdater::enabledPlugins() const { return m_enabledPlugins; } void KFileItemModelRolesUpdater::slotItemsInserted(const KItemRangeList& itemRanges) { QElapsedTimer timer; timer.start(); // Determine the sort role synchronously for as many items as possible. if (m_resolvableRoles.contains(m_model->sortRole())) { int insertedCount = 0; foreach (const KItemRange& range, itemRanges) { const int lastIndex = insertedCount + range.index + range.count - 1; for (int i = insertedCount + range.index; i <= lastIndex; ++i) { if (timer.elapsed() < MaxBlockTimeout) { applySortRole(i); } else { m_pendingSortRoleItems.insert(m_model->fileItem(i)); } } insertedCount += range.count; } applySortProgressToModel(); // If there are still items whose sort role is unknown, check if the // asynchronous determination of the sort role is already in progress, // and start it if that is not the case. if (!m_pendingSortRoleItems.isEmpty() && m_state != ResolvingSortRole) { killPreviewJob(); m_state = ResolvingSortRole; resolveNextSortRole(); } } startUpdating(); } void KFileItemModelRolesUpdater::slotItemsRemoved(const KItemRangeList& itemRanges) { - Q_UNUSED(itemRanges); + Q_UNUSED(itemRanges) const bool allItemsRemoved = (m_model->count() == 0); #ifdef HAVE_BALOO if (m_balooFileMonitor) { // Don't let the FileWatcher watch for removed items if (allItemsRemoved) { m_balooFileMonitor->clear(); } else { QStringList newFileList; foreach (const QString& file, m_balooFileMonitor->files()) { if (m_model->index(QUrl::fromLocalFile(file)) >= 0) { newFileList.append(file); } } m_balooFileMonitor->setFiles(newFileList); } } #endif if (allItemsRemoved) { m_state = Idle; m_finishedItems.clear(); m_pendingSortRoleItems.clear(); m_pendingIndexes.clear(); m_pendingPreviewItems.clear(); m_recentlyChangedItems.clear(); m_recentlyChangedItemsTimer->stop(); m_changedItems.clear(); killPreviewJob(); } else { // Only remove the items from m_finishedItems. They will be removed // from the other sets later on. QSet::iterator it = m_finishedItems.begin(); while (it != m_finishedItems.end()) { if (m_model->index(*it) < 0) { it = m_finishedItems.erase(it); } else { ++it; } } // The visible items might have changed. startUpdating(); } } void KFileItemModelRolesUpdater::slotItemsMoved(const KItemRange& itemRange, const QList &movedToIndexes) { - Q_UNUSED(itemRange); - Q_UNUSED(movedToIndexes); + Q_UNUSED(itemRange) + Q_UNUSED(movedToIndexes) // The visible items might have changed. startUpdating(); } void KFileItemModelRolesUpdater::slotItemsChanged(const KItemRangeList& itemRanges, const QSet& roles) { - Q_UNUSED(roles); + Q_UNUSED(roles) // Find out if slotItemsChanged() has been done recently. If that is the // case, resolving the roles is postponed until a timer has exceeded // to prevent expensive repeated updates if files are updated frequently. const bool itemsChangedRecently = m_recentlyChangedItemsTimer->isActive(); QSet& targetSet = itemsChangedRecently ? m_recentlyChangedItems : m_changedItems; foreach (const KItemRange& itemRange, itemRanges) { int index = itemRange.index; for (int count = itemRange.count; count > 0; --count) { const KFileItem item = m_model->fileItem(index); targetSet.insert(item); ++index; } } m_recentlyChangedItemsTimer->start(); if (!itemsChangedRecently) { updateChangedItems(); } } void KFileItemModelRolesUpdater::slotSortRoleChanged(const QByteArray& current, const QByteArray& previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) if (m_resolvableRoles.contains(current)) { m_pendingSortRoleItems.clear(); m_finishedItems.clear(); const int count = m_model->count(); QElapsedTimer timer; timer.start(); // Determine the sort role synchronously for as many items as possible. for (int index = 0; index < count; ++index) { if (timer.elapsed() < MaxBlockTimeout) { applySortRole(index); } else { m_pendingSortRoleItems.insert(m_model->fileItem(index)); } } applySortProgressToModel(); if (!m_pendingSortRoleItems.isEmpty()) { // Trigger the asynchronous determination of the sort role. killPreviewJob(); m_state = ResolvingSortRole; resolveNextSortRole(); } } else { m_state = Idle; m_pendingSortRoleItems.clear(); applySortProgressToModel(); } } void KFileItemModelRolesUpdater::slotGotPreview(const KFileItem& item, const QPixmap& pixmap) { if (m_state != PreviewJobRunning) { return; } m_changedItems.remove(item); const int index = m_model->index(item); if (index < 0) { return; } QPixmap scaledPixmap = pixmap; if (!pixmap.hasAlpha() && m_iconSize.width() > KIconLoader::SizeSmallMedium && m_iconSize.height() > KIconLoader::SizeSmallMedium) { if (m_enlargeSmallPreviews) { KPixmapModifier::applyFrame(scaledPixmap, m_iconSize); } else { // Assure that small previews don't get enlarged. Instead they // should be shown centered within the frame. const QSize contentSize = KPixmapModifier::sizeInsideFrame(m_iconSize); const bool enlargingRequired = scaledPixmap.width() < contentSize.width() && scaledPixmap.height() < contentSize.height(); if (enlargingRequired) { QSize frameSize = scaledPixmap.size() / scaledPixmap.devicePixelRatio(); frameSize.scale(m_iconSize, Qt::KeepAspectRatio); QPixmap largeFrame(frameSize); largeFrame.fill(Qt::transparent); KPixmapModifier::applyFrame(largeFrame, frameSize); QPainter painter(&largeFrame); painter.drawPixmap((largeFrame.width() - scaledPixmap.width() / scaledPixmap.devicePixelRatio()) / 2, (largeFrame.height() - scaledPixmap.height() / scaledPixmap.devicePixelRatio()) / 2, scaledPixmap); scaledPixmap = largeFrame; } else { // The image must be shrunk as it is too large to fit into // the available icon size KPixmapModifier::applyFrame(scaledPixmap, m_iconSize); } } } else { KPixmapModifier::scale(scaledPixmap, m_iconSize * qApp->devicePixelRatio()); scaledPixmap.setDevicePixelRatio(qApp->devicePixelRatio()); } QHash data = rolesData(item); const QStringList overlays = data["iconOverlays"].toStringList(); // Strangely KFileItem::overlays() returns empty string-values, so // we need to check first whether an overlay must be drawn at all. // It is more efficient to do it here, as KIconLoader::drawOverlays() // assumes that an overlay will be drawn and has some additional // setup time. foreach (const QString& overlay, overlays) { if (!overlay.isEmpty()) { // There is at least one overlay, draw all overlays above m_pixmap // and cancel the check KIconLoader::global()->drawOverlays(overlays, scaledPixmap, KIconLoader::Desktop); break; } } data.insert("iconPixmap", scaledPixmap); disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); m_model->setData(index, data); connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); m_finishedItems.insert(item); } void KFileItemModelRolesUpdater::slotPreviewFailed(const KFileItem& item) { if (m_state != PreviewJobRunning) { return; } m_changedItems.remove(item); const int index = m_model->index(item); if (index >= 0) { QHash data; data.insert("iconPixmap", QPixmap()); disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); m_model->setData(index, data); connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); applyResolvedRoles(index, ResolveAll); m_finishedItems.insert(item); } } void KFileItemModelRolesUpdater::slotPreviewJobFinished() { m_previewJob = nullptr; if (m_state != PreviewJobRunning) { return; } m_state = Idle; if (!m_pendingPreviewItems.isEmpty()) { startPreviewJob(); } else { if (!m_changedItems.isEmpty()) { updateChangedItems(); } } } void KFileItemModelRolesUpdater::resolveNextSortRole() { if (m_state != ResolvingSortRole) { return; } QSet::iterator it = m_pendingSortRoleItems.begin(); while (it != m_pendingSortRoleItems.end()) { const KFileItem item = *it; const int index = m_model->index(item); // Continue if the sort role has already been determined for the // item, and the item has not been changed recently. if (!m_changedItems.contains(item) && m_model->data(index).contains(m_model->sortRole())) { it = m_pendingSortRoleItems.erase(it); continue; } applySortRole(index); m_pendingSortRoleItems.erase(it); break; } if (!m_pendingSortRoleItems.isEmpty()) { applySortProgressToModel(); QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextSortRole); } else { m_state = Idle; // Prevent that we try to update the items twice. disconnect(m_model, &KFileItemModel::itemsMoved, this, &KFileItemModelRolesUpdater::slotItemsMoved); applySortProgressToModel(); connect(m_model, &KFileItemModel::itemsMoved, this, &KFileItemModelRolesUpdater::slotItemsMoved); startUpdating(); } } void KFileItemModelRolesUpdater::resolveNextPendingRoles() { if (m_state != ResolvingAllRoles) { return; } while (!m_pendingIndexes.isEmpty()) { const int index = m_pendingIndexes.takeFirst(); const KFileItem item = m_model->fileItem(index); if (m_finishedItems.contains(item)) { continue; } applyResolvedRoles(index, ResolveAll); m_finishedItems.insert(item); m_changedItems.remove(item); break; } if (!m_pendingIndexes.isEmpty()) { QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextPendingRoles); } else { m_state = Idle; if (m_clearPreviews) { // Only go through the list if there are items which might still have previews. if (m_finishedItems.count() != m_model->count()) { QHash data; data.insert("iconPixmap", QPixmap()); disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); for (int index = 0; index <= m_model->count(); ++index) { if (m_model->data(index).contains("iconPixmap")) { m_model->setData(index, data); } } connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); } m_clearPreviews = false; } if (!m_changedItems.isEmpty()) { updateChangedItems(); } } } void KFileItemModelRolesUpdater::resolveRecentlyChangedItems() { m_changedItems += m_recentlyChangedItems; m_recentlyChangedItems.clear(); updateChangedItems(); } void KFileItemModelRolesUpdater::applyChangedBalooRoles(const QString& file) { #ifdef HAVE_BALOO const KFileItem item = m_model->fileItem(QUrl::fromLocalFile(file)); if (item.isNull()) { // itemUrl is not in the model anymore, probably because // the corresponding file has been deleted in the meantime. return; } applyChangedBalooRolesForItem(item); #else - Q_UNUSED(file); + Q_UNUSED(file) #endif } void KFileItemModelRolesUpdater::applyChangedBalooRolesForItem(const KFileItem &item) { #ifdef HAVE_BALOO Baloo::File file(item.localPath()); file.load(); const KBalooRolesProvider& rolesProvider = KBalooRolesProvider::instance(); QHash data; foreach (const QByteArray& role, rolesProvider.roles()) { // Overwrite all the role values with an empty QVariant, because the roles // provider doesn't overwrite it when the property value list is empty. // See bug 322348 data.insert(role, QVariant()); } QHashIterator it(rolesProvider.roleValues(file, m_roles)); while (it.hasNext()) { it.next(); data.insert(it.key(), it.value()); } disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); const int index = m_model->index(item); m_model->setData(index, data); connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); #else #ifndef Q_CC_MSVC - Q_UNUSED(item); + Q_UNUSED(item) #endif #endif } void KFileItemModelRolesUpdater::slotDirectoryContentsCountReceived(const QString& path, int count) { const bool getSizeRole = m_roles.contains("size"); const bool getIsExpandableRole = m_roles.contains("isExpandable"); if (getSizeRole || getIsExpandableRole) { const int index = m_model->index(QUrl::fromLocalFile(path)); if (index >= 0) { QHash data; if (getSizeRole) { data.insert("size", count); } if (getIsExpandableRole) { data.insert("isExpandable", count > 0); } disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); m_model->setData(index, data); connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); } } } void KFileItemModelRolesUpdater::startUpdating() { if (m_state == Paused) { return; } if (m_finishedItems.count() == m_model->count()) { // All roles have been resolved already. m_state = Idle; return; } // Terminate all updates that are currently active. killPreviewJob(); m_pendingIndexes.clear(); QElapsedTimer timer; timer.start(); // Determine the icons for the visible items synchronously. updateVisibleIcons(); // A detailed update of the items in and near the visible area // only makes sense if sorting is finished. if (m_state == ResolvingSortRole) { return; } // Start the preview job or the asynchronous resolving of all roles. QList indexes = indexesToResolve(); if (m_previewShown) { m_pendingPreviewItems.clear(); m_pendingPreviewItems.reserve(indexes.count()); foreach (int index, indexes) { const KFileItem item = m_model->fileItem(index); if (!m_finishedItems.contains(item)) { m_pendingPreviewItems.append(item); } } startPreviewJob(); } else { m_pendingIndexes = indexes; // Trigger the asynchronous resolving of all roles. m_state = ResolvingAllRoles; QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextPendingRoles); } } void KFileItemModelRolesUpdater::updateVisibleIcons() { int lastVisibleIndex = m_lastVisibleIndex; if (lastVisibleIndex <= 0) { // Guess a reasonable value for the last visible index if the view // has not told us about the real value yet. lastVisibleIndex = qMin(m_firstVisibleIndex + m_maximumVisibleItems, m_model->count() - 1); if (lastVisibleIndex <= 0) { lastVisibleIndex = qMin(200, m_model->count() - 1); } } QElapsedTimer timer; timer.start(); // Try to determine the final icons for all visible items. int index; for (index = m_firstVisibleIndex; index <= lastVisibleIndex && timer.elapsed() < MaxBlockTimeout; ++index) { applyResolvedRoles(index, ResolveFast); } // KFileItemListView::initializeItemListWidget(KItemListWidget*) will load // preliminary icons (i.e., without mime type determination) for the // remaining items. } void KFileItemModelRolesUpdater::startPreviewJob() { m_state = PreviewJobRunning; if (m_pendingPreviewItems.isEmpty()) { QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::slotPreviewJobFinished); return; } // PreviewJob internally caches items always with the size of // 128 x 128 pixels or 256 x 256 pixels. A (slow) downscaling is done // by PreviewJob if a smaller size is requested. For images KFileItemModelRolesUpdater must // do a downscaling anyhow because of the frame, so in this case only the provided // cache sizes are requested. const QSize cacheSize = (m_iconSize.width() > 128) || (m_iconSize.height() > 128) ? QSize(256, 256) : QSize(128, 128); // KIO::filePreview() will request the MIME-type of all passed items, which (in the // worst case) might block the application for several seconds. To prevent such // a blocking, we only pass items with known mime type to the preview job. const int count = m_pendingPreviewItems.count(); KFileItemList itemSubSet; itemSubSet.reserve(count); if (m_pendingPreviewItems.first().isMimeTypeKnown()) { // Some mime types are known already, probably because they were // determined when loading the icons for the visible items. Start // a preview job for all items at the beginning of the list which // have a known mime type. do { itemSubSet.append(m_pendingPreviewItems.takeFirst()); } while (!m_pendingPreviewItems.isEmpty() && m_pendingPreviewItems.first().isMimeTypeKnown()); } else { // Determine mime types for MaxBlockTimeout ms, and start a preview // job for the corresponding items. QElapsedTimer timer; timer.start(); do { const KFileItem item = m_pendingPreviewItems.takeFirst(); item.determineMimeType(); itemSubSet.append(item); } while (!m_pendingPreviewItems.isEmpty() && timer.elapsed() < MaxBlockTimeout); } KIO::PreviewJob* job = new KIO::PreviewJob(itemSubSet, cacheSize, &m_enabledPlugins); job->setIgnoreMaximumSize(itemSubSet.first().isLocalFile()); if (job->uiDelegate()) { KJobWidgets::setWindow(job, qApp->activeWindow()); } connect(job, &KIO::PreviewJob::gotPreview, this, &KFileItemModelRolesUpdater::slotGotPreview); connect(job, &KIO::PreviewJob::failed, this, &KFileItemModelRolesUpdater::slotPreviewFailed); connect(job, &KIO::PreviewJob::finished, this, &KFileItemModelRolesUpdater::slotPreviewJobFinished); m_previewJob = job; } void KFileItemModelRolesUpdater::updateChangedItems() { if (m_state == Paused) { return; } if (m_changedItems.isEmpty()) { return; } m_finishedItems -= m_changedItems; if (m_resolvableRoles.contains(m_model->sortRole())) { m_pendingSortRoleItems += m_changedItems; if (m_state != ResolvingSortRole) { // Stop the preview job if necessary, and trigger the // asynchronous determination of the sort role. killPreviewJob(); m_state = ResolvingSortRole; QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextSortRole); } return; } QList visibleChangedIndexes; QList invisibleChangedIndexes; foreach (const KFileItem& item, m_changedItems) { const int index = m_model->index(item); if (index < 0) { m_changedItems.remove(item); continue; } if (index >= m_firstVisibleIndex && index <= m_lastVisibleIndex) { visibleChangedIndexes.append(index); } else { invisibleChangedIndexes.append(index); } } std::sort(visibleChangedIndexes.begin(), visibleChangedIndexes.end()); if (m_previewShown) { foreach (int index, visibleChangedIndexes) { m_pendingPreviewItems.append(m_model->fileItem(index)); } foreach (int index, invisibleChangedIndexes) { m_pendingPreviewItems.append(m_model->fileItem(index)); } if (!m_previewJob) { startPreviewJob(); } } else { const bool resolvingInProgress = !m_pendingIndexes.isEmpty(); m_pendingIndexes = visibleChangedIndexes + m_pendingIndexes + invisibleChangedIndexes; if (!resolvingInProgress) { // Trigger the asynchronous resolving of the changed roles. m_state = ResolvingAllRoles; QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextPendingRoles); } } } void KFileItemModelRolesUpdater::applySortRole(int index) { QHash data; const KFileItem item = m_model->fileItem(index); if (m_model->sortRole() == "type") { if (!item.isMimeTypeKnown()) { item.determineMimeType(); } data.insert("type", item.mimeComment()); } else if (m_model->sortRole() == "size" && item.isLocalFile() && item.isDir()) { const QString path = item.localPath(); data.insert("size", m_directoryContentsCounter->countDirectoryContentsSynchronously(path)); } else { // Probably the sort role is a baloo role - just determine all roles. data = rolesData(item); } disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); m_model->setData(index, data); connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); } void KFileItemModelRolesUpdater::applySortProgressToModel() { // Inform the model about the progress of the resolved items, // so that it can give an indication when the sorting has been finished. const int resolvedCount = m_model->count() - m_pendingSortRoleItems.count(); m_model->emitSortProgress(resolvedCount); } bool KFileItemModelRolesUpdater::applyResolvedRoles(int index, ResolveHint hint) { const KFileItem item = m_model->fileItem(index); const bool resolveAll = (hint == ResolveAll); bool iconChanged = false; if (!item.isMimeTypeKnown() || !item.isFinalIconKnown()) { item.determineMimeType(); iconChanged = true; } else if (!m_model->data(index).contains("iconName")) { iconChanged = true; } if (iconChanged || resolveAll || m_clearPreviews) { if (index < 0) { return false; } QHash data; if (resolveAll) { data = rolesData(item); } data.insert("iconName", item.iconName()); if (m_clearPreviews) { data.insert("iconPixmap", QPixmap()); } disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); m_model->setData(index, data); connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); return true; } return false; } QHash KFileItemModelRolesUpdater::rolesData(const KFileItem& item) { QHash data; const bool getSizeRole = m_roles.contains("size"); const bool getIsExpandableRole = m_roles.contains("isExpandable"); if ((getSizeRole || getIsExpandableRole) && item.isDir()) { if (item.isLocalFile()) { // Tell m_directoryContentsCounter that we want to count the items // inside the directory. The result will be received in slotDirectoryContentsCountReceived. const QString path = item.localPath(); m_directoryContentsCounter->addDirectory(path); } else if (getSizeRole) { data.insert("size", -1); // -1 indicates an unknown number of items } } if (m_roles.contains("type")) { data.insert("type", item.mimeComment()); } QStringList overlays = item.overlays(); foreach(KOverlayIconPlugin *it, m_overlayIconsPlugin) { overlays.append(it->getOverlays(item.url())); } data.insert("iconOverlays", overlays); #ifdef HAVE_BALOO if (m_balooFileMonitor) { m_balooFileMonitor->addFile(item.localPath()); applyChangedBalooRolesForItem(item); } #endif return data; } void KFileItemModelRolesUpdater::slotOverlaysChanged(const QUrl& url, const QStringList &) { const KFileItem item = m_model->fileItem(url); if (item.isNull()) { return; } const int index = m_model->index(item); QHash data = m_model->data(index); QStringList overlays = item.overlays(); foreach (KOverlayIconPlugin *it, m_overlayIconsPlugin) { overlays.append(it->getOverlays(url)); } data.insert("iconOverlays", overlays); m_model->setData(index, data); } void KFileItemModelRolesUpdater::updateAllPreviews() { if (m_state == Paused) { m_previewChangedDuringPausing = true; } else { m_finishedItems.clear(); startUpdating(); } } void KFileItemModelRolesUpdater::killPreviewJob() { if (m_previewJob) { disconnect(m_previewJob, &KIO::PreviewJob::gotPreview, this, &KFileItemModelRolesUpdater::slotGotPreview); disconnect(m_previewJob, &KIO::PreviewJob::failed, this, &KFileItemModelRolesUpdater::slotPreviewFailed); disconnect(m_previewJob, &KIO::PreviewJob::finished, this, &KFileItemModelRolesUpdater::slotPreviewJobFinished); m_previewJob->kill(); m_previewJob = nullptr; m_pendingPreviewItems.clear(); } } QList KFileItemModelRolesUpdater::indexesToResolve() const { const int count = m_model->count(); QList result; result.reserve(ResolveAllItemsLimit); // Add visible items. for (int i = m_firstVisibleIndex; i <= m_lastVisibleIndex; ++i) { result.append(i); } // We need a reasonable upper limit for number of items to resolve after // and before the visible range. m_maximumVisibleItems can be quite large // when using Compact View. const int readAheadItems = qMin(ReadAheadPages * m_maximumVisibleItems, ResolveAllItemsLimit / 2); // Add items after the visible range. const int endExtendedVisibleRange = qMin(m_lastVisibleIndex + readAheadItems, count - 1); for (int i = m_lastVisibleIndex + 1; i <= endExtendedVisibleRange; ++i) { result.append(i); } // Add items before the visible range in reverse order. const int beginExtendedVisibleRange = qMax(0, m_firstVisibleIndex - readAheadItems); for (int i = m_firstVisibleIndex - 1; i >= beginExtendedVisibleRange; --i) { result.append(i); } // Add items on the last page. const int beginLastPage = qMax(qMin(endExtendedVisibleRange + 1, count - 1), count - m_maximumVisibleItems); for (int i = beginLastPage; i < count; ++i) { result.append(i); } // Add items on the first page. const int endFirstPage = qMin(qMax(beginExtendedVisibleRange - 1, 0), m_maximumVisibleItems); for (int i = 0; i <= endFirstPage; ++i) { result.append(i); } // Continue adding items until ResolveAllItemsLimit is reached. int remainingItems = ResolveAllItemsLimit - result.count(); for (int i = endExtendedVisibleRange + 1; i < beginLastPage && remainingItems > 0; ++i) { result.append(i); --remainingItems; } for (int i = beginExtendedVisibleRange - 1; i > endFirstPage && remainingItems > 0; --i) { result.append(i); --remainingItems; } return result; } diff --git a/src/kitemviews/kitemlistcontainer.cpp b/src/kitemviews/kitemlistcontainer.cpp index f308667b3..2deba5466 100644 --- a/src/kitemviews/kitemlistcontainer.cpp +++ b/src/kitemviews/kitemlistcontainer.cpp @@ -1,406 +1,406 @@ /*************************************************************************** * Copyright (C) 2011 by Peter Penz * * * * Based on the Itemviews NG project from Trolltech Labs: * * http://qt.gitorious.org/qt-labs/itemviews-ng * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kitemlistcontainer.h" #include "kitemlistcontroller.h" #include "kitemlistview.h" #include "private/kitemlistsmoothscroller.h" #include #include #include #include #include /** * Replaces the default viewport of KItemListContainer by a * non-scrollable viewport. The scrolling is done in an optimized * way by KItemListView internally. */ class KItemListContainerViewport : public QGraphicsView { Q_OBJECT public: KItemListContainerViewport(QGraphicsScene* scene, QWidget* parent); protected: void wheelEvent(QWheelEvent* event) override; }; KItemListContainerViewport::KItemListContainerViewport(QGraphicsScene* scene, QWidget* parent) : QGraphicsView(scene, parent) { setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setViewportMargins(0, 0, 0, 0); setFrameShape(QFrame::NoFrame); } void KItemListContainerViewport::wheelEvent(QWheelEvent* event) { // Assure that the wheel-event gets forwarded to the parent // and not handled at all by QGraphicsView. event->ignore(); } KItemListContainer::KItemListContainer(KItemListController* controller, QWidget* parent) : QAbstractScrollArea(parent), m_controller(controller), m_horizontalSmoothScroller(nullptr), m_verticalSmoothScroller(nullptr) { Q_ASSERT(controller); controller->setParent(this); QGraphicsView* graphicsView = new KItemListContainerViewport(new QGraphicsScene(this), this); setViewport(graphicsView); m_horizontalSmoothScroller = new KItemListSmoothScroller(horizontalScrollBar(), this); m_verticalSmoothScroller = new KItemListSmoothScroller(verticalScrollBar(), this); if (controller->model()) { slotModelChanged(controller->model(), nullptr); } if (controller->view()) { slotViewChanged(controller->view(), nullptr); } connect(controller, &KItemListController::modelChanged, this, &KItemListContainer::slotModelChanged); connect(controller, &KItemListController::viewChanged, this, &KItemListContainer::slotViewChanged); } KItemListContainer::~KItemListContainer() { // Don't rely on the QObject-order to delete the controller, otherwise // the QGraphicsScene might get deleted before the view. delete m_controller; m_controller = nullptr; } KItemListController* KItemListContainer::controller() const { return m_controller; } void KItemListContainer::setEnabledFrame(bool enable) { QGraphicsView* graphicsView = qobject_cast(viewport()); if (enable) { setFrameShape(QFrame::StyledPanel); graphicsView->setPalette(palette()); graphicsView->viewport()->setAutoFillBackground(true); } else { setFrameShape(QFrame::NoFrame); // Make the background of the container transparent and apply the window-text color // to the text color, so that enough contrast is given for all color // schemes QPalette p = graphicsView->palette(); p.setColor(QPalette::Active, QPalette::Text, p.color(QPalette::Active, QPalette::WindowText)); p.setColor(QPalette::Inactive, QPalette::Text, p.color(QPalette::Inactive, QPalette::WindowText)); p.setColor(QPalette::Disabled, QPalette::Text, p.color(QPalette::Disabled, QPalette::WindowText)); graphicsView->setPalette(p); graphicsView->viewport()->setAutoFillBackground(false); } } bool KItemListContainer::enabledFrame() const { const QGraphicsView* graphicsView = qobject_cast(viewport()); return graphicsView->autoFillBackground(); } void KItemListContainer::keyPressEvent(QKeyEvent* event) { // TODO: We should find a better way to handle the key press events in the view. // The reasons why we need this hack are: // 1. Without reimplementing keyPressEvent() here, the event would not reach the QGraphicsView. // 2. By default, the KItemListView does not have the keyboard focus in the QGraphicsScene, so // simply sending the event to the QGraphicsView which is the KItemListContainer's viewport // does not work. KItemListView* view = m_controller->view(); if (view) { QApplication::sendEvent(view, event); } } void KItemListContainer::showEvent(QShowEvent* event) { QAbstractScrollArea::showEvent(event); updateGeometries(); } void KItemListContainer::resizeEvent(QResizeEvent* event) { QAbstractScrollArea::resizeEvent(event); updateGeometries(); } void KItemListContainer::scrollContentsBy(int dx, int dy) { m_horizontalSmoothScroller->scrollContentsBy(dx); m_verticalSmoothScroller->scrollContentsBy(dy); } void KItemListContainer::wheelEvent(QWheelEvent* event) { if (event->modifiers().testFlag(Qt::ControlModifier)) { event->ignore(); return; } KItemListView* view = m_controller->view(); if (!view) { event->ignore(); return; } const bool scrollHorizontally = (event->orientation() == Qt::Horizontal) || (event->orientation() == Qt::Vertical && !verticalScrollBar()->isVisible()); KItemListSmoothScroller* smoothScroller = scrollHorizontally ? m_horizontalSmoothScroller : m_verticalSmoothScroller; smoothScroller->handleWheelEvent(event); } void KItemListContainer::slotScrollOrientationChanged(Qt::Orientation current, Qt::Orientation previous) { - Q_UNUSED(previous); + Q_UNUSED(previous) updateSmoothScrollers(current); } void KItemListContainer::slotModelChanged(KItemModelBase* current, KItemModelBase* previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) } void KItemListContainer::slotViewChanged(KItemListView* current, KItemListView* previous) { QGraphicsScene* scene = static_cast(viewport())->scene(); if (previous) { scene->removeItem(previous); disconnect(previous, &KItemListView::scrollOrientationChanged, this, &KItemListContainer::slotScrollOrientationChanged); disconnect(previous, &KItemListView::scrollOffsetChanged, this, &KItemListContainer::updateScrollOffsetScrollBar); disconnect(previous, &KItemListView::maximumScrollOffsetChanged, this, &KItemListContainer::updateScrollOffsetScrollBar); disconnect(previous, &KItemListView::itemOffsetChanged, this, &KItemListContainer::updateItemOffsetScrollBar); disconnect(previous, &KItemListView::maximumItemOffsetChanged, this, &KItemListContainer::updateItemOffsetScrollBar); disconnect(previous, &KItemListView::scrollTo, this, &KItemListContainer::scrollTo); m_horizontalSmoothScroller->setTargetObject(nullptr); m_verticalSmoothScroller->setTargetObject(nullptr); } if (current) { scene->addItem(current); connect(current, &KItemListView::scrollOrientationChanged, this, &KItemListContainer::slotScrollOrientationChanged); connect(current, &KItemListView::scrollOffsetChanged, this, &KItemListContainer::updateScrollOffsetScrollBar); connect(current, &KItemListView::maximumScrollOffsetChanged, this, &KItemListContainer::updateScrollOffsetScrollBar); connect(current, &KItemListView::itemOffsetChanged, this, &KItemListContainer::updateItemOffsetScrollBar); connect(current, &KItemListView::maximumItemOffsetChanged, this, &KItemListContainer::updateItemOffsetScrollBar); connect(current, &KItemListView::scrollTo, this, &KItemListContainer::scrollTo); m_horizontalSmoothScroller->setTargetObject(current); m_verticalSmoothScroller->setTargetObject(current); updateSmoothScrollers(current->scrollOrientation()); } } void KItemListContainer::scrollTo(qreal offset) { const KItemListView* view = m_controller->view(); if (view) { if (view->scrollOrientation() == Qt::Vertical) { m_verticalSmoothScroller->scrollTo(offset); } else { m_horizontalSmoothScroller->scrollTo(offset); } } } void KItemListContainer::updateScrollOffsetScrollBar() { const KItemListView* view = m_controller->view(); if (!view) { return; } KItemListSmoothScroller* smoothScroller = nullptr; QScrollBar* scrollOffsetScrollBar = nullptr; int singleStep = 0; int pageStep = 0; int maximum = 0; if (view->scrollOrientation() == Qt::Vertical) { smoothScroller = m_verticalSmoothScroller; scrollOffsetScrollBar = verticalScrollBar(); singleStep = view->itemSizeHint().height(); // We cannot use view->size().height() because this height might // include the header widget, which is not part of the scrolled area. pageStep = view->verticalPageStep(); // However, the total height of the view must be considered for the // maximum value of the scroll bar. Note that the view's scrollOffset() // refers to the offset of the top part of the view, which might be // hidden behind the header. maximum = qMax(0, int(view->maximumScrollOffset() - view->size().height())); } else { smoothScroller = m_horizontalSmoothScroller; scrollOffsetScrollBar = horizontalScrollBar(); singleStep = view->itemSize().width(); pageStep = view->size().width(); maximum = qMax(0, int(view->maximumScrollOffset() - view->size().width())); } const int value = view->scrollOffset(); if (smoothScroller->requestScrollBarUpdate(maximum)) { const bool updatePolicy = (scrollOffsetScrollBar->maximum() > 0 && maximum == 0) || horizontalScrollBarPolicy() == Qt::ScrollBarAlwaysOn; scrollOffsetScrollBar->setSingleStep(singleStep); scrollOffsetScrollBar->setPageStep(pageStep); scrollOffsetScrollBar->setMinimum(0); scrollOffsetScrollBar->setMaximum(maximum); scrollOffsetScrollBar->setValue(value); if (updatePolicy) { // Prevent a potential endless layout loop (see bug #293318). updateScrollOffsetScrollBarPolicy(); } } } void KItemListContainer::updateItemOffsetScrollBar() { const KItemListView* view = m_controller->view(); if (!view) { return; } KItemListSmoothScroller* smoothScroller = nullptr; QScrollBar* itemOffsetScrollBar = nullptr; int singleStep = 0; int pageStep = 0; if (view->scrollOrientation() == Qt::Vertical) { smoothScroller = m_horizontalSmoothScroller; itemOffsetScrollBar = horizontalScrollBar(); singleStep = view->size().width() / 10; pageStep = view->size().width(); } else { smoothScroller = m_verticalSmoothScroller; itemOffsetScrollBar = verticalScrollBar(); singleStep = view->size().height() / 10; pageStep = view->size().height(); } const int value = view->itemOffset(); const int maximum = qMax(0, int(view->maximumItemOffset()) - pageStep); if (smoothScroller->requestScrollBarUpdate(maximum)) { itemOffsetScrollBar->setSingleStep(singleStep); itemOffsetScrollBar->setPageStep(pageStep); itemOffsetScrollBar->setMinimum(0); itemOffsetScrollBar->setMaximum(maximum); itemOffsetScrollBar->setValue(value); } } void KItemListContainer::updateGeometries() { QRect rect = geometry(); int extra = frameWidth() * 2; QStyleOption option; option.initFrom(this); int scrollbarSpacing = 0; if (style()->styleHint(QStyle::SH_ScrollView_FrameOnlyAroundContents, &option, this)) { scrollbarSpacing = style()->pixelMetric(QStyle::PM_ScrollView_ScrollBarSpacing, &option, this); } const int widthDec = verticalScrollBar()->isVisible() ? extra + scrollbarSpacing + style()->pixelMetric(QStyle::PM_ScrollBarExtent, &option, this) : extra; const int heightDec = horizontalScrollBar()->isVisible() ? extra + scrollbarSpacing + style()->pixelMetric(QStyle::PM_ScrollBarExtent, &option, this) : extra; const QRectF newGeometry(0, 0, rect.width() - widthDec, rect.height() - heightDec); if (m_controller->view()->geometry() != newGeometry) { m_controller->view()->setGeometry(newGeometry); // Get the real geometry of the view again since the scrollbars // visibilities and the view geometry may have changed in re-layout. static_cast(viewport())->scene()->setSceneRect(m_controller->view()->geometry()); static_cast(viewport())->viewport()->setGeometry(m_controller->view()->geometry().toRect()); updateScrollOffsetScrollBar(); updateItemOffsetScrollBar(); } } void KItemListContainer::updateSmoothScrollers(Qt::Orientation orientation) { if (orientation == Qt::Vertical) { m_verticalSmoothScroller->setPropertyName("scrollOffset"); m_horizontalSmoothScroller->setPropertyName("itemOffset"); } else { m_horizontalSmoothScroller->setPropertyName("scrollOffset"); m_verticalSmoothScroller->setPropertyName("itemOffset"); } } void KItemListContainer::updateScrollOffsetScrollBarPolicy() { const KItemListView* view = m_controller->view(); Q_ASSERT(view); const bool vertical = (view->scrollOrientation() == Qt::Vertical); QStyleOption option; option.initFrom(this); const int scrollBarInc = style()->pixelMetric(QStyle::PM_ScrollBarExtent, &option, this); QSizeF newViewSize = m_controller->view()->size(); if (vertical) { newViewSize.rwidth() += scrollBarInc; } else { newViewSize.rheight() += scrollBarInc; } const Qt::ScrollBarPolicy policy = view->scrollBarRequired(newViewSize) ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAsNeeded; if (vertical) { setVerticalScrollBarPolicy(policy); } else { setHorizontalScrollBarPolicy(policy); } } #include "kitemlistcontainer.moc" diff --git a/src/kitemviews/kitemlistcontroller.cpp b/src/kitemviews/kitemlistcontroller.cpp index 8cc9529ef..a80869dfc 100644 --- a/src/kitemviews/kitemlistcontroller.cpp +++ b/src/kitemviews/kitemlistcontroller.cpp @@ -1,1356 +1,1356 @@ /*************************************************************************** * Copyright (C) 2011 by Peter Penz * * Copyright (C) 2012 by Frank Reininghaus * * * * Based on the Itemviews NG project from Trolltech Labs: * * http://qt.gitorious.org/qt-labs/itemviews-ng * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kitemlistcontroller.h" #include "kitemlistselectionmanager.h" #include "kitemlistview.h" #include "private/kitemlistkeyboardsearchmanager.h" #include "private/kitemlistrubberband.h" #include "views/draganddrophelper.h" #include #include #include #include #include #include #include #include KItemListController::KItemListController(KItemModelBase* model, KItemListView* view, QObject* parent) : QObject(parent), m_singleClickActivationEnforced(false), m_selectionTogglePressed(false), m_clearSelectionIfItemsAreNotDragged(false), m_selectionBehavior(NoSelection), m_autoActivationBehavior(ActivationAndExpansion), m_mouseDoubleClickAction(ActivateItemOnly), m_model(nullptr), m_view(nullptr), m_selectionManager(new KItemListSelectionManager(this)), m_keyboardManager(new KItemListKeyboardSearchManager(this)), m_pressedIndex(-1), m_pressedMousePos(), m_autoActivationTimer(nullptr), m_oldSelection(), m_keyboardAnchorIndex(-1), m_keyboardAnchorPos(0) { connect(m_keyboardManager, &KItemListKeyboardSearchManager::changeCurrentItem, this, &KItemListController::slotChangeCurrentItem); connect(m_selectionManager, &KItemListSelectionManager::currentChanged, m_keyboardManager, &KItemListKeyboardSearchManager::slotCurrentChanged); connect(m_selectionManager, &KItemListSelectionManager::selectionChanged, m_keyboardManager, &KItemListKeyboardSearchManager::slotSelectionChanged); m_autoActivationTimer = new QTimer(this); m_autoActivationTimer->setSingleShot(true); m_autoActivationTimer->setInterval(-1); connect(m_autoActivationTimer, &QTimer::timeout, this, &KItemListController::slotAutoActivationTimeout); setModel(model); setView(view); } KItemListController::~KItemListController() { setView(nullptr); Q_ASSERT(!m_view); setModel(nullptr); Q_ASSERT(!m_model); } void KItemListController::setModel(KItemModelBase* model) { if (m_model == model) { return; } KItemModelBase* oldModel = m_model; if (oldModel) { oldModel->deleteLater(); } m_model = model; if (m_model) { m_model->setParent(this); } if (m_view) { m_view->setModel(m_model); } m_selectionManager->setModel(m_model); emit modelChanged(m_model, oldModel); } KItemModelBase* KItemListController::model() const { return m_model; } KItemListSelectionManager* KItemListController::selectionManager() const { return m_selectionManager; } void KItemListController::setView(KItemListView* view) { if (m_view == view) { return; } KItemListView* oldView = m_view; if (oldView) { disconnect(oldView, &KItemListView::scrollOffsetChanged, this, &KItemListController::slotViewScrollOffsetChanged); oldView->deleteLater(); } m_view = view; if (m_view) { m_view->setParent(this); m_view->setController(this); m_view->setModel(m_model); connect(m_view, &KItemListView::scrollOffsetChanged, this, &KItemListController::slotViewScrollOffsetChanged); updateExtendedSelectionRegion(); } emit viewChanged(m_view, oldView); } KItemListView* KItemListController::view() const { return m_view; } void KItemListController::setSelectionBehavior(SelectionBehavior behavior) { m_selectionBehavior = behavior; updateExtendedSelectionRegion(); } KItemListController::SelectionBehavior KItemListController::selectionBehavior() const { return m_selectionBehavior; } void KItemListController::setAutoActivationBehavior(AutoActivationBehavior behavior) { m_autoActivationBehavior = behavior; } KItemListController::AutoActivationBehavior KItemListController::autoActivationBehavior() const { return m_autoActivationBehavior; } void KItemListController::setMouseDoubleClickAction(MouseDoubleClickAction action) { m_mouseDoubleClickAction = action; } KItemListController::MouseDoubleClickAction KItemListController::mouseDoubleClickAction() const { return m_mouseDoubleClickAction; } int KItemListController::indexCloseToMousePressedPosition() const { QHashIterator it(m_view->m_visibleGroups); while (it.hasNext()) { it.next(); KItemListGroupHeader *groupHeader = it.value(); const QPointF mappedToGroup = groupHeader->mapFromItem(nullptr, m_pressedMousePos); if (groupHeader->contains(mappedToGroup)) { return it.key()->index(); } } return -1; } void KItemListController::setAutoActivationDelay(int delay) { m_autoActivationTimer->setInterval(delay); } int KItemListController::autoActivationDelay() const { return m_autoActivationTimer->interval(); } void KItemListController::setSingleClickActivationEnforced(bool singleClick) { m_singleClickActivationEnforced = singleClick; } bool KItemListController::singleClickActivationEnforced() const { return m_singleClickActivationEnforced; } bool KItemListController::keyPressEvent(QKeyEvent* event) { int index = m_selectionManager->currentItem(); int key = event->key(); // Handle the expanding/collapsing of items if (m_view->supportsItemExpanding() && m_model->isExpandable(index)) { if (key == Qt::Key_Right) { if (m_model->setExpanded(index, true)) { return true; } } else if (key == Qt::Key_Left) { if (m_model->setExpanded(index, false)) { return true; } } } const bool shiftPressed = event->modifiers() & Qt::ShiftModifier; const bool controlPressed = event->modifiers() & Qt::ControlModifier; const bool shiftOrControlPressed = shiftPressed || controlPressed; const bool navigationPressed = key == Qt::Key_Home || key == Qt::Key_End || key == Qt::Key_PageUp || key == Qt::Key_PageDown || key == Qt::Key_Up || key == Qt::Key_Down || key == Qt::Key_Left || key == Qt::Key_Right; const int itemCount = m_model->count(); // For horizontal scroll orientation, transform // the arrow keys to simplify the event handling. if (m_view->scrollOrientation() == Qt::Horizontal) { switch (key) { case Qt::Key_Up: key = Qt::Key_Left; break; case Qt::Key_Down: key = Qt::Key_Right; break; case Qt::Key_Left: key = Qt::Key_Up; break; case Qt::Key_Right: key = Qt::Key_Down; break; default: break; } } const bool selectSingleItem = m_selectionBehavior != NoSelection && itemCount == 1 && navigationPressed; if (selectSingleItem) { const int current = m_selectionManager->currentItem(); m_selectionManager->setSelected(current); return true; } switch (key) { case Qt::Key_Home: index = 0; m_keyboardAnchorIndex = index; m_keyboardAnchorPos = keyboardAnchorPos(index); break; case Qt::Key_End: index = itemCount - 1; m_keyboardAnchorIndex = index; m_keyboardAnchorPos = keyboardAnchorPos(index); break; case Qt::Key_Left: if (index > 0) { const int expandedParentsCount = m_model->expandedParentsCount(index); if (expandedParentsCount == 0) { --index; } else { // Go to the parent of the current item. do { --index; } while (index > 0 && m_model->expandedParentsCount(index) == expandedParentsCount); } m_keyboardAnchorIndex = index; m_keyboardAnchorPos = keyboardAnchorPos(index); } break; case Qt::Key_Right: if (index < itemCount - 1) { ++index; m_keyboardAnchorIndex = index; m_keyboardAnchorPos = keyboardAnchorPos(index); } break; case Qt::Key_Up: updateKeyboardAnchor(); index = previousRowIndex(index); break; case Qt::Key_Down: updateKeyboardAnchor(); index = nextRowIndex(index); break; case Qt::Key_PageUp: if (m_view->scrollOrientation() == Qt::Horizontal) { // The new current index should correspond to the first item in the current column. int newIndex = qMax(index - 1, 0); while (newIndex != index && m_view->itemRect(newIndex).topLeft().y() < m_view->itemRect(index).topLeft().y()) { index = newIndex; newIndex = qMax(index - 1, 0); } m_keyboardAnchorIndex = index; m_keyboardAnchorPos = keyboardAnchorPos(index); } else { const qreal currentItemBottom = m_view->itemRect(index).bottomLeft().y(); const qreal height = m_view->geometry().height(); // The new current item should be the first item in the current // column whose itemRect's top coordinate is larger than targetY. const qreal targetY = currentItemBottom - height; updateKeyboardAnchor(); int newIndex = previousRowIndex(index); do { index = newIndex; updateKeyboardAnchor(); newIndex = previousRowIndex(index); } while (m_view->itemRect(newIndex).topLeft().y() > targetY && newIndex != index); } break; case Qt::Key_PageDown: if (m_view->scrollOrientation() == Qt::Horizontal) { // The new current index should correspond to the last item in the current column. int newIndex = qMin(index + 1, m_model->count() - 1); while (newIndex != index && m_view->itemRect(newIndex).topLeft().y() > m_view->itemRect(index).topLeft().y()) { index = newIndex; newIndex = qMin(index + 1, m_model->count() - 1); } m_keyboardAnchorIndex = index; m_keyboardAnchorPos = keyboardAnchorPos(index); } else { const qreal currentItemTop = m_view->itemRect(index).topLeft().y(); const qreal height = m_view->geometry().height(); // The new current item should be the last item in the current // column whose itemRect's bottom coordinate is smaller than targetY. const qreal targetY = currentItemTop + height; updateKeyboardAnchor(); int newIndex = nextRowIndex(index); do { index = newIndex; updateKeyboardAnchor(); newIndex = nextRowIndex(index); } while (m_view->itemRect(newIndex).bottomLeft().y() < targetY && newIndex != index); } break; case Qt::Key_Enter: case Qt::Key_Return: { const KItemSet selectedItems = m_selectionManager->selectedItems(); if (selectedItems.count() >= 2) { emit itemsActivated(selectedItems); } else if (selectedItems.count() == 1) { emit itemActivated(selectedItems.first()); } else { emit itemActivated(index); } break; } case Qt::Key_Menu: { // Emit the signal itemContextMenuRequested() in case if at least one // item is selected. Otherwise the signal viewContextMenuRequested() will be emitted. const KItemSet selectedItems = m_selectionManager->selectedItems(); int index = -1; if (selectedItems.count() >= 2) { const int currentItemIndex = m_selectionManager->currentItem(); index = selectedItems.contains(currentItemIndex) ? currentItemIndex : selectedItems.first(); } else if (selectedItems.count() == 1) { index = selectedItems.first(); } if (index >= 0) { const QRectF contextRect = m_view->itemContextRect(index); const QPointF pos(m_view->scene()->views().first()->mapToGlobal(contextRect.bottomRight().toPoint())); emit itemContextMenuRequested(index, pos); } else { emit viewContextMenuRequested(QCursor::pos()); } break; } case Qt::Key_Escape: if (m_selectionBehavior != SingleSelection) { m_selectionManager->clearSelection(); } m_keyboardManager->cancelSearch(); emit escapePressed(); break; case Qt::Key_Space: if (m_selectionBehavior == MultiSelection) { if (controlPressed) { // Toggle the selection state of the current item. m_selectionManager->endAnchoredSelection(); m_selectionManager->setSelected(index, 1, KItemListSelectionManager::Toggle); m_selectionManager->beginAnchoredSelection(index); break; } else { // Select the current item if it is not selected yet. const int current = m_selectionManager->currentItem(); if (!m_selectionManager->isSelected(current)) { m_selectionManager->setSelected(current); break; } } } Q_FALLTHROUGH(); // fall through to the default case and add the Space to the current search string. default: m_keyboardManager->addKeys(event->text()); // Make sure unconsumed events get propagated up the chain. #302329 event->ignore(); return false; } if (m_selectionManager->currentItem() != index) { switch (m_selectionBehavior) { case NoSelection: m_selectionManager->setCurrentItem(index); break; case SingleSelection: m_selectionManager->setCurrentItem(index); m_selectionManager->clearSelection(); m_selectionManager->setSelected(index, 1); break; case MultiSelection: if (controlPressed) { m_selectionManager->endAnchoredSelection(); } m_selectionManager->setCurrentItem(index); if (!shiftOrControlPressed) { m_selectionManager->clearSelection(); m_selectionManager->setSelected(index, 1); } if (!shiftPressed) { m_selectionManager->beginAnchoredSelection(index); } break; } } if (navigationPressed) { m_view->scrollToItem(index); } return true; } void KItemListController::slotChangeCurrentItem(const QString& text, bool searchFromNextItem) { if (!m_model || m_model->count() == 0) { return; } int index; if (searchFromNextItem) { const int currentIndex = m_selectionManager->currentItem(); index = m_model->indexForKeyboardSearch(text, (currentIndex + 1) % m_model->count()); } else { index = m_model->indexForKeyboardSearch(text, 0); } if (index >= 0) { m_selectionManager->setCurrentItem(index); if (m_selectionBehavior != NoSelection) { m_selectionManager->replaceSelection(index); m_selectionManager->beginAnchoredSelection(index); } m_view->scrollToItem(index); } } void KItemListController::slotAutoActivationTimeout() { if (!m_model || !m_view) { return; } const int index = m_autoActivationTimer->property("index").toInt(); if (index < 0 || index >= m_model->count()) { return; } /* m_view->isUnderMouse() fixes a bug in the Folder-View-Panel and in the * Places-Panel. * * Bug: When you drag a file onto a Folder-View-Item or a Places-Item and * then move away before the auto-activation timeout triggers, than the * item still becomes activated/expanded. * * See Bug 293200 and 305783 */ if (m_model->supportsDropping(index) && m_view->isUnderMouse()) { if (m_view->supportsItemExpanding() && m_model->isExpandable(index)) { const bool expanded = m_model->isExpanded(index); m_model->setExpanded(index, !expanded); } else if (m_autoActivationBehavior != ExpansionOnly) { emit itemActivated(index); } } } bool KItemListController::inputMethodEvent(QInputMethodEvent* event) { - Q_UNUSED(event); + Q_UNUSED(event) return false; } bool KItemListController::mousePressEvent(QGraphicsSceneMouseEvent* event, const QTransform& transform) { if (!m_view) { return false; } m_pressedMousePos = transform.map(event->pos()); m_pressedIndex = m_view->itemAt(m_pressedMousePos); emit mouseButtonPressed(m_pressedIndex, event->buttons()); if (event->buttons() & (Qt::BackButton | Qt::ForwardButton)) { // Do not select items when clicking the back/forward buttons, see // https://bugs.kde.org/show_bug.cgi?id=327412. return true; } if (m_view->isAboveExpansionToggle(m_pressedIndex, m_pressedMousePos)) { m_selectionManager->endAnchoredSelection(); m_selectionManager->setCurrentItem(m_pressedIndex); m_selectionManager->beginAnchoredSelection(m_pressedIndex); return true; } m_selectionTogglePressed = m_view->isAboveSelectionToggle(m_pressedIndex, m_pressedMousePos); if (m_selectionTogglePressed) { m_selectionManager->setSelected(m_pressedIndex, 1, KItemListSelectionManager::Toggle); // The previous anchored selection has been finished already in // KItemListSelectionManager::setSelected(). We can safely change // the current item and start a new anchored selection now. m_selectionManager->setCurrentItem(m_pressedIndex); m_selectionManager->beginAnchoredSelection(m_pressedIndex); return true; } const bool shiftPressed = event->modifiers() & Qt::ShiftModifier; const bool controlPressed = event->modifiers() & Qt::ControlModifier; // The previous selection is cleared if either // 1. The selection mode is SingleSelection, or // 2. the selection mode is MultiSelection, and *none* of the following conditions are met: // a) Shift or Control are pressed. // b) The clicked item is selected already. In that case, the user might want to: // - start dragging multiple items, or // - open the context menu and perform an action for all selected items. const bool shiftOrControlPressed = shiftPressed || controlPressed; const bool pressedItemAlreadySelected = m_pressedIndex >= 0 && m_selectionManager->isSelected(m_pressedIndex); const bool clearSelection = m_selectionBehavior == SingleSelection || (!shiftOrControlPressed && !pressedItemAlreadySelected); if (clearSelection) { m_selectionManager->clearSelection(); } else if (pressedItemAlreadySelected && !shiftOrControlPressed && (event->buttons() & Qt::LeftButton)) { // The user might want to start dragging multiple items, but if he clicks the item // in order to trigger it instead, the other selected items must be deselected. // However, we do not know yet what the user is going to do. // -> remember that the user pressed an item which had been selected already and // clear the selection in mouseReleaseEvent(), unless the items are dragged. m_clearSelectionIfItemsAreNotDragged = true; if (m_selectionManager->selectedItems().count() == 1 && m_view->isAboveText(m_pressedIndex, m_pressedMousePos)) { emit selectedItemTextPressed(m_pressedIndex); } } if (!shiftPressed) { // Finish the anchored selection before the current index is changed m_selectionManager->endAnchoredSelection(); } if (event->buttons() & Qt::RightButton) { // Stop rubber band from persisting after right-clicks KItemListRubberBand* rubberBand = m_view->rubberBand(); if (rubberBand->isActive()) { disconnect(rubberBand, &KItemListRubberBand::endPositionChanged, this, &KItemListController::slotRubberBandChanged); rubberBand->setActive(false); m_view->setAutoScroll(false); } } if (m_pressedIndex >= 0) { m_selectionManager->setCurrentItem(m_pressedIndex); switch (m_selectionBehavior) { case NoSelection: break; case SingleSelection: m_selectionManager->setSelected(m_pressedIndex); break; case MultiSelection: if (controlPressed && !shiftPressed) { m_selectionManager->setSelected(m_pressedIndex, 1, KItemListSelectionManager::Toggle); m_selectionManager->beginAnchoredSelection(m_pressedIndex); } else if (!shiftPressed || !m_selectionManager->isAnchoredSelectionActive()) { // Select the pressed item and start a new anchored selection m_selectionManager->setSelected(m_pressedIndex, 1, KItemListSelectionManager::Select); m_selectionManager->beginAnchoredSelection(m_pressedIndex); } break; default: Q_ASSERT(false); break; } if (event->buttons() & Qt::RightButton) { emit itemContextMenuRequested(m_pressedIndex, event->screenPos()); } return true; } if (event->buttons() & Qt::RightButton) { const QRectF headerBounds = m_view->headerBoundaries(); if (headerBounds.contains(event->pos())) { emit headerContextMenuRequested(event->screenPos()); } else { emit viewContextMenuRequested(event->screenPos()); } return true; } if (m_selectionBehavior == MultiSelection) { QPointF startPos = m_pressedMousePos; if (m_view->scrollOrientation() == Qt::Vertical) { startPos.ry() += m_view->scrollOffset(); if (m_view->itemSize().width() < 0) { // Use a special rubberband for views that have only one column and // expand the rubberband to use the whole width of the view. startPos.setX(0); } } else { startPos.rx() += m_view->scrollOffset(); } m_oldSelection = m_selectionManager->selectedItems(); KItemListRubberBand* rubberBand = m_view->rubberBand(); rubberBand->setStartPosition(startPos); rubberBand->setEndPosition(startPos); rubberBand->setActive(true); connect(rubberBand, &KItemListRubberBand::endPositionChanged, this, &KItemListController::slotRubberBandChanged); m_view->setAutoScroll(true); } return false; } bool KItemListController::mouseMoveEvent(QGraphicsSceneMouseEvent* event, const QTransform& transform) { if (!m_view) { return false; } if (m_pressedIndex >= 0) { // Check whether a dragging should be started if (event->buttons() & Qt::LeftButton) { const QPointF pos = transform.map(event->pos()); if ((pos - m_pressedMousePos).manhattanLength() >= QApplication::startDragDistance()) { if (!m_selectionManager->isSelected(m_pressedIndex)) { // Always assure that the dragged item gets selected. Usually this is already // done on the mouse-press event, but when using the selection-toggle on a // selected item the dragged item is not selected yet. m_selectionManager->setSelected(m_pressedIndex, 1, KItemListSelectionManager::Toggle); } else { // A selected item has been clicked to drag all selected items // -> the selection should not be cleared when the mouse button is released. m_clearSelectionIfItemsAreNotDragged = false; } startDragging(); } } } else { KItemListRubberBand* rubberBand = m_view->rubberBand(); if (rubberBand->isActive()) { QPointF endPos = transform.map(event->pos()); // Update the current item. const int newCurrent = m_view->itemAt(endPos); if (newCurrent >= 0) { // It's expected that the new current index is also the new anchor (bug 163451). m_selectionManager->endAnchoredSelection(); m_selectionManager->setCurrentItem(newCurrent); m_selectionManager->beginAnchoredSelection(newCurrent); } if (m_view->scrollOrientation() == Qt::Vertical) { endPos.ry() += m_view->scrollOffset(); if (m_view->itemSize().width() < 0) { // Use a special rubberband for views that have only one column and // expand the rubberband to use the whole width of the view. endPos.setX(m_view->size().width()); } } else { endPos.rx() += m_view->scrollOffset(); } rubberBand->setEndPosition(endPos); } } return false; } bool KItemListController::mouseReleaseEvent(QGraphicsSceneMouseEvent* event, const QTransform& transform) { if (!m_view) { return false; } emit mouseButtonReleased(m_pressedIndex, event->buttons()); const bool isAboveSelectionToggle = m_view->isAboveSelectionToggle(m_pressedIndex, m_pressedMousePos); if (isAboveSelectionToggle) { m_selectionTogglePressed = false; return true; } if (!isAboveSelectionToggle && m_selectionTogglePressed) { m_selectionManager->setSelected(m_pressedIndex, 1, KItemListSelectionManager::Toggle); m_selectionTogglePressed = false; return true; } const bool shiftOrControlPressed = event->modifiers() & Qt::ShiftModifier || event->modifiers() & Qt::ControlModifier; KItemListRubberBand* rubberBand = m_view->rubberBand(); if (rubberBand->isActive()) { disconnect(rubberBand, &KItemListRubberBand::endPositionChanged, this, &KItemListController::slotRubberBandChanged); rubberBand->setActive(false); m_oldSelection.clear(); m_view->setAutoScroll(false); } const QPointF pos = transform.map(event->pos()); const int index = m_view->itemAt(pos); if (index >= 0 && index == m_pressedIndex) { // The release event is done above the same item as the press event if (m_clearSelectionIfItemsAreNotDragged) { // A selected item has been clicked, but no drag operation has been started // -> clear the rest of the selection. m_selectionManager->clearSelection(); m_selectionManager->setSelected(m_pressedIndex, 1, KItemListSelectionManager::Select); m_selectionManager->beginAnchoredSelection(m_pressedIndex); } if (event->button() & Qt::LeftButton) { bool emitItemActivated = true; if (m_view->isAboveExpansionToggle(index, pos)) { const bool expanded = m_model->isExpanded(index); m_model->setExpanded(index, !expanded); emit itemExpansionToggleClicked(index); emitItemActivated = false; } else if (shiftOrControlPressed) { // The mouse click should only update the selection, not trigger the item emitItemActivated = false; } else if (!(m_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick) || m_singleClickActivationEnforced)) { emitItemActivated = false; } if (emitItemActivated) { emit itemActivated(index); } } else if (event->button() & Qt::MidButton) { emit itemMiddleClicked(index); } } m_pressedMousePos = QPointF(); m_pressedIndex = -1; m_clearSelectionIfItemsAreNotDragged = false; return false; } bool KItemListController::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* event, const QTransform& transform) { const QPointF pos = transform.map(event->pos()); const int index = m_view->itemAt(pos); // Expand item if desired - See Bug 295573 if (m_mouseDoubleClickAction != ActivateItemOnly) { if (m_view && m_model && m_view->supportsItemExpanding() && m_model->isExpandable(index)) { const bool expanded = m_model->isExpanded(index); m_model->setExpanded(index, !expanded); } } if (event->button() & Qt::RightButton) { m_selectionManager->clearSelection(); if (index >= 0) { m_selectionManager->setSelected(index); emit itemContextMenuRequested(index, event->screenPos()); } else { const QRectF headerBounds = m_view->headerBoundaries(); if (headerBounds.contains(event->pos())) { emit headerContextMenuRequested(event->screenPos()); } else { emit viewContextMenuRequested(event->screenPos()); } } return true; } bool emitItemActivated = !(m_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick) || m_singleClickActivationEnforced) && (event->button() & Qt::LeftButton) && index >= 0 && index < m_model->count(); if (emitItemActivated) { emit itemActivated(index); } return false; } bool KItemListController::dragEnterEvent(QGraphicsSceneDragDropEvent* event, const QTransform& transform) { - Q_UNUSED(event); - Q_UNUSED(transform); + Q_UNUSED(event) + Q_UNUSED(transform) DragAndDropHelper::clearUrlListMatchesUrlCache(); return false; } bool KItemListController::dragLeaveEvent(QGraphicsSceneDragDropEvent* event, const QTransform& transform) { - Q_UNUSED(event); - Q_UNUSED(transform); + Q_UNUSED(event) + Q_UNUSED(transform) m_autoActivationTimer->stop(); m_view->setAutoScroll(false); m_view->hideDropIndicator(); KItemListWidget* widget = hoveredWidget(); if (widget) { widget->setHovered(false); emit itemUnhovered(widget->index()); } return false; } bool KItemListController::dragMoveEvent(QGraphicsSceneDragDropEvent* event, const QTransform& transform) { if (!m_model || !m_view) { return false; } QUrl hoveredDir = m_model->directory(); KItemListWidget* oldHoveredWidget = hoveredWidget(); const QPointF pos = transform.map(event->pos()); KItemListWidget* newHoveredWidget = widgetForPos(pos); if (oldHoveredWidget != newHoveredWidget) { m_autoActivationTimer->stop(); if (oldHoveredWidget) { oldHoveredWidget->setHovered(false); emit itemUnhovered(oldHoveredWidget->index()); } } if (newHoveredWidget) { bool droppingBetweenItems = false; if (m_model->sortRole().isEmpty()) { // The model supports inserting items between other items. droppingBetweenItems = (m_view->showDropIndicator(pos) >= 0); } const int index = newHoveredWidget->index(); if (m_model->isDir(index)) { hoveredDir = m_model->url(index); } if (!droppingBetweenItems) { if (m_model->supportsDropping(index)) { // Something has been dragged on an item. m_view->hideDropIndicator(); if (!newHoveredWidget->isHovered()) { newHoveredWidget->setHovered(true); emit itemHovered(index); } if (!m_autoActivationTimer->isActive() && m_autoActivationTimer->interval() >= 0) { m_autoActivationTimer->setProperty("index", index); m_autoActivationTimer->start(); } } } else { m_autoActivationTimer->stop(); if (newHoveredWidget && newHoveredWidget->isHovered()) { newHoveredWidget->setHovered(false); emit itemUnhovered(index); } } } else { m_view->hideDropIndicator(); } event->setAccepted(!DragAndDropHelper::urlListMatchesUrl(event->mimeData()->urls(), hoveredDir)); return false; } bool KItemListController::dropEvent(QGraphicsSceneDragDropEvent* event, const QTransform& transform) { if (!m_view) { return false; } m_autoActivationTimer->stop(); m_view->setAutoScroll(false); const QPointF pos = transform.map(event->pos()); int dropAboveIndex = -1; if (m_model->sortRole().isEmpty()) { // The model supports inserting of items between other items. dropAboveIndex = m_view->showDropIndicator(pos); } if (dropAboveIndex >= 0) { // Something has been dropped between two items. m_view->hideDropIndicator(); emit aboveItemDropEvent(dropAboveIndex, event); } else if (!event->mimeData()->hasFormat(m_model->blacklistItemDropEventMimeType())) { // Something has been dropped on an item or on an empty part of the view. emit itemDropEvent(m_view->itemAt(pos), event); } QAccessibleEvent accessibilityEvent(view(), QAccessible::DragDropEnd); QAccessible::updateAccessibility(&accessibilityEvent); return true; } bool KItemListController::hoverEnterEvent(QGraphicsSceneHoverEvent* event, const QTransform& transform) { - Q_UNUSED(event); - Q_UNUSED(transform); + Q_UNUSED(event) + Q_UNUSED(transform) return false; } bool KItemListController::hoverMoveEvent(QGraphicsSceneHoverEvent* event, const QTransform& transform) { - Q_UNUSED(transform); + Q_UNUSED(transform) if (!m_model || !m_view) { return false; } KItemListWidget* oldHoveredWidget = hoveredWidget(); const QPointF pos = transform.map(event->pos()); KItemListWidget* newHoveredWidget = widgetForPos(pos); if (oldHoveredWidget != newHoveredWidget) { if (oldHoveredWidget) { oldHoveredWidget->setHovered(false); emit itemUnhovered(oldHoveredWidget->index()); } if (newHoveredWidget) { newHoveredWidget->setHovered(true); const QPointF mappedPos = newHoveredWidget->mapFromItem(m_view, pos); newHoveredWidget->setHoverPosition(mappedPos); emit itemHovered(newHoveredWidget->index()); } } else if (oldHoveredWidget) { const QPointF mappedPos = oldHoveredWidget->mapFromItem(m_view, pos); oldHoveredWidget->setHoverPosition(mappedPos); } return false; } bool KItemListController::hoverLeaveEvent(QGraphicsSceneHoverEvent* event, const QTransform& transform) { - Q_UNUSED(event); - Q_UNUSED(transform); + Q_UNUSED(event) + Q_UNUSED(transform) if (!m_model || !m_view) { return false; } foreach (KItemListWidget* widget, m_view->visibleItemListWidgets()) { if (widget->isHovered()) { widget->setHovered(false); emit itemUnhovered(widget->index()); } } return false; } bool KItemListController::wheelEvent(QGraphicsSceneWheelEvent* event, const QTransform& transform) { - Q_UNUSED(event); - Q_UNUSED(transform); + Q_UNUSED(event) + Q_UNUSED(transform) return false; } bool KItemListController::resizeEvent(QGraphicsSceneResizeEvent* event, const QTransform& transform) { - Q_UNUSED(event); - Q_UNUSED(transform); + Q_UNUSED(event) + Q_UNUSED(transform) return false; } bool KItemListController::processEvent(QEvent* event, const QTransform& transform) { if (!event) { return false; } switch (event->type()) { case QEvent::KeyPress: return keyPressEvent(static_cast(event)); case QEvent::InputMethod: return inputMethodEvent(static_cast(event)); case QEvent::GraphicsSceneMousePress: return mousePressEvent(static_cast(event), QTransform()); case QEvent::GraphicsSceneMouseMove: return mouseMoveEvent(static_cast(event), QTransform()); case QEvent::GraphicsSceneMouseRelease: return mouseReleaseEvent(static_cast(event), QTransform()); case QEvent::GraphicsSceneMouseDoubleClick: return mouseDoubleClickEvent(static_cast(event), QTransform()); case QEvent::GraphicsSceneWheel: return wheelEvent(static_cast(event), QTransform()); case QEvent::GraphicsSceneDragEnter: return dragEnterEvent(static_cast(event), QTransform()); case QEvent::GraphicsSceneDragLeave: return dragLeaveEvent(static_cast(event), QTransform()); case QEvent::GraphicsSceneDragMove: return dragMoveEvent(static_cast(event), QTransform()); case QEvent::GraphicsSceneDrop: return dropEvent(static_cast(event), QTransform()); case QEvent::GraphicsSceneHoverEnter: return hoverEnterEvent(static_cast(event), QTransform()); case QEvent::GraphicsSceneHoverMove: return hoverMoveEvent(static_cast(event), QTransform()); case QEvent::GraphicsSceneHoverLeave: return hoverLeaveEvent(static_cast(event), QTransform()); case QEvent::GraphicsSceneResize: return resizeEvent(static_cast(event), transform); default: break; } return false; } void KItemListController::slotViewScrollOffsetChanged(qreal current, qreal previous) { if (!m_view) { return; } KItemListRubberBand* rubberBand = m_view->rubberBand(); if (rubberBand->isActive()) { const qreal diff = current - previous; // TODO: Ideally just QCursor::pos() should be used as // new end-position but it seems there is no easy way // to have something like QWidget::mapFromGlobal() for QGraphicsWidget // (... or I just missed an easy way to do the mapping) QPointF endPos = rubberBand->endPosition(); if (m_view->scrollOrientation() == Qt::Vertical) { endPos.ry() += diff; } else { endPos.rx() += diff; } rubberBand->setEndPosition(endPos); } } void KItemListController::slotRubberBandChanged() { if (!m_view || !m_model || m_model->count() <= 0) { return; } const KItemListRubberBand* rubberBand = m_view->rubberBand(); const QPointF startPos = rubberBand->startPosition(); const QPointF endPos = rubberBand->endPosition(); QRectF rubberBandRect = QRectF(startPos, endPos).normalized(); const bool scrollVertical = (m_view->scrollOrientation() == Qt::Vertical); if (scrollVertical) { rubberBandRect.translate(0, -m_view->scrollOffset()); } else { rubberBandRect.translate(-m_view->scrollOffset(), 0); } if (!m_oldSelection.isEmpty()) { // Clear the old selection that was available before the rubberband has // been activated in case if no Shift- or Control-key are pressed const bool shiftOrControlPressed = QApplication::keyboardModifiers() & Qt::ShiftModifier || QApplication::keyboardModifiers() & Qt::ControlModifier; if (!shiftOrControlPressed) { m_oldSelection.clear(); } } KItemSet selectedItems; // Select all visible items that intersect with the rubberband foreach (const KItemListWidget* widget, m_view->visibleItemListWidgets()) { const int index = widget->index(); const QRectF widgetRect = m_view->itemRect(index); if (widgetRect.intersects(rubberBandRect)) { const QRectF iconRect = widget->iconRect().translated(widgetRect.topLeft()); const QRectF textRect = widget->textRect().translated(widgetRect.topLeft()); if (iconRect.intersects(rubberBandRect) || textRect.intersects(rubberBandRect)) { selectedItems.insert(index); } } } // Select all invisible items that intersect with the rubberband. Instead of // iterating all items only the area which might be touched by the rubberband // will be checked. const bool increaseIndex = scrollVertical ? startPos.y() > endPos.y(): startPos.x() > endPos.x(); int index = increaseIndex ? m_view->lastVisibleIndex() + 1 : m_view->firstVisibleIndex() - 1; bool selectionFinished = false; do { const QRectF widgetRect = m_view->itemRect(index); if (widgetRect.intersects(rubberBandRect)) { selectedItems.insert(index); } if (increaseIndex) { ++index; selectionFinished = (index >= m_model->count()) || ( scrollVertical && widgetRect.top() > rubberBandRect.bottom()) || (!scrollVertical && widgetRect.left() > rubberBandRect.right()); } else { --index; selectionFinished = (index < 0) || ( scrollVertical && widgetRect.bottom() < rubberBandRect.top()) || (!scrollVertical && widgetRect.right() < rubberBandRect.left()); } } while (!selectionFinished); if (QApplication::keyboardModifiers() & Qt::ControlModifier) { // If Control is pressed, the selection state of all items in the rubberband is toggled. // Therefore, the new selection contains: // 1. All previously selected items which are not inside the rubberband, and // 2. all items inside the rubberband which have not been selected previously. m_selectionManager->setSelectedItems(m_oldSelection ^ selectedItems); } else { m_selectionManager->setSelectedItems(selectedItems + m_oldSelection); } } void KItemListController::startDragging() { if (!m_view || !m_model) { return; } const KItemSet selectedItems = m_selectionManager->selectedItems(); if (selectedItems.isEmpty()) { return; } QMimeData* data = m_model->createMimeData(selectedItems); if (!data) { return; } // The created drag object will be owned and deleted // by QApplication::activeWindow(). QDrag* drag = new QDrag(QApplication::activeWindow()); drag->setMimeData(data); const QPixmap pixmap = m_view->createDragPixmap(selectedItems); drag->setPixmap(pixmap); const QPoint hotSpot((pixmap.width() / pixmap.devicePixelRatio()) / 2, 0); drag->setHotSpot(hotSpot); drag->exec(Qt::MoveAction | Qt::CopyAction | Qt::LinkAction, Qt::CopyAction); QAccessibleEvent accessibilityEvent(view(), QAccessible::DragDropStart); QAccessible::updateAccessibility(&accessibilityEvent); } KItemListWidget* KItemListController::hoveredWidget() const { Q_ASSERT(m_view); foreach (KItemListWidget* widget, m_view->visibleItemListWidgets()) { if (widget->isHovered()) { return widget; } } return nullptr; } KItemListWidget* KItemListController::widgetForPos(const QPointF& pos) const { Q_ASSERT(m_view); foreach (KItemListWidget* widget, m_view->visibleItemListWidgets()) { const QPointF mappedPos = widget->mapFromItem(m_view, pos); const bool hovered = widget->contains(mappedPos) && !widget->expansionToggleRect().contains(mappedPos); if (hovered) { return widget; } } return nullptr; } void KItemListController::updateKeyboardAnchor() { const bool validAnchor = m_keyboardAnchorIndex >= 0 && m_keyboardAnchorIndex < m_model->count() && keyboardAnchorPos(m_keyboardAnchorIndex) == m_keyboardAnchorPos; if (!validAnchor) { const int index = m_selectionManager->currentItem(); m_keyboardAnchorIndex = index; m_keyboardAnchorPos = keyboardAnchorPos(index); } } int KItemListController::nextRowIndex(int index) const { if (m_keyboardAnchorIndex < 0) { return index; } const int maxIndex = m_model->count() - 1; if (index == maxIndex) { return index; } // Calculate the index of the last column inside the row of the current index int lastColumnIndex = index; while (keyboardAnchorPos(lastColumnIndex + 1) > keyboardAnchorPos(lastColumnIndex)) { ++lastColumnIndex; if (lastColumnIndex >= maxIndex) { return index; } } // Based on the last column index go to the next row and calculate the nearest index // that is below the current index int nextRowIndex = lastColumnIndex + 1; int searchIndex = nextRowIndex; qreal minDiff = qAbs(m_keyboardAnchorPos - keyboardAnchorPos(nextRowIndex)); while (searchIndex < maxIndex && keyboardAnchorPos(searchIndex + 1) > keyboardAnchorPos(searchIndex)) { ++searchIndex; const qreal searchDiff = qAbs(m_keyboardAnchorPos - keyboardAnchorPos(searchIndex)); if (searchDiff < minDiff) { minDiff = searchDiff; nextRowIndex = searchIndex; } } return nextRowIndex; } int KItemListController::previousRowIndex(int index) const { if (m_keyboardAnchorIndex < 0 || index == 0) { return index; } // Calculate the index of the first column inside the row of the current index int firstColumnIndex = index; while (keyboardAnchorPos(firstColumnIndex - 1) < keyboardAnchorPos(firstColumnIndex)) { --firstColumnIndex; if (firstColumnIndex <= 0) { return index; } } // Based on the first column index go to the previous row and calculate the nearest index // that is above the current index int previousRowIndex = firstColumnIndex - 1; int searchIndex = previousRowIndex; qreal minDiff = qAbs(m_keyboardAnchorPos - keyboardAnchorPos(previousRowIndex)); while (searchIndex > 0 && keyboardAnchorPos(searchIndex - 1) < keyboardAnchorPos(searchIndex)) { --searchIndex; const qreal searchDiff = qAbs(m_keyboardAnchorPos - keyboardAnchorPos(searchIndex)); if (searchDiff < minDiff) { minDiff = searchDiff; previousRowIndex = searchIndex; } } return previousRowIndex; } qreal KItemListController::keyboardAnchorPos(int index) const { const QRectF itemRect = m_view->itemRect(index); if (!itemRect.isEmpty()) { return (m_view->scrollOrientation() == Qt::Vertical) ? itemRect.x() : itemRect.y(); } return 0; } void KItemListController::updateExtendedSelectionRegion() { if (m_view) { const bool extend = (m_selectionBehavior != MultiSelection); KItemListStyleOption option = m_view->styleOption(); if (option.extendedSelectionRegion != extend) { option.extendedSelectionRegion = extend; m_view->setStyleOption(option); } } } diff --git a/src/kitemviews/kitemlistgroupheader.cpp b/src/kitemviews/kitemlistgroupheader.cpp index 06a32484a..9689180d1 100644 --- a/src/kitemviews/kitemlistgroupheader.cpp +++ b/src/kitemviews/kitemlistgroupheader.cpp @@ -1,238 +1,238 @@ /*************************************************************************** * Copyright (C) 2011 by Peter Penz * * * * Based on the Itemviews NG project from Trolltech Labs: * * http://qt.gitorious.org/qt-labs/itemviews-ng * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kstandarditemlistgroupheader.h" #include "kitemlistview.h" #include #include #include KItemListGroupHeader::KItemListGroupHeader(QGraphicsWidget* parent) : QGraphicsWidget(parent, nullptr), m_dirtyCache(true), m_role(), m_data(), m_styleOption(), m_scrollOrientation(Qt::Vertical), m_itemIndex(-1), m_separatorColor(), m_roleColor(), m_roleBounds() { } KItemListGroupHeader::~KItemListGroupHeader() { } void KItemListGroupHeader::setRole(const QByteArray& role) { if (m_role != role) { const QByteArray previous = m_role; m_role = role; update(); roleChanged(role, previous); } } QByteArray KItemListGroupHeader::role() const { return m_role; } void KItemListGroupHeader::setData(const QVariant& data) { if (m_data != data) { const QVariant previous = m_data; m_data = data; update(); dataChanged(data, previous); } } QVariant KItemListGroupHeader::data() const { return m_data; } void KItemListGroupHeader::setStyleOption(const KItemListStyleOption& option) { if (m_styleOption == option) { return; } const KItemListStyleOption previous = m_styleOption; m_styleOption = option; m_dirtyCache = true; styleOptionChanged(option, previous); } const KItemListStyleOption& KItemListGroupHeader::styleOption() const { return m_styleOption; } void KItemListGroupHeader::setScrollOrientation(Qt::Orientation orientation) { if (m_scrollOrientation != orientation) { const Qt::Orientation previous = m_scrollOrientation; m_scrollOrientation = orientation; if (orientation == Qt::Vertical) { m_dirtyCache = true; } scrollOrientationChanged(orientation, previous); } } void KItemListGroupHeader::setItemIndex(int index) { if (m_itemIndex != index) { const int previous = m_itemIndex; m_itemIndex = index; m_dirtyCache = true; itemIndexChanged(m_itemIndex, previous); } } int KItemListGroupHeader::itemIndex() const { return m_itemIndex; } Qt::Orientation KItemListGroupHeader::scrollOrientation() const { return m_scrollOrientation; } void KItemListGroupHeader::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) { - Q_UNUSED(painter); - Q_UNUSED(option); - Q_UNUSED(widget); + Q_UNUSED(painter) + Q_UNUSED(option) + Q_UNUSED(widget) if (m_dirtyCache) { updateCache(); } paintSeparator(painter, m_separatorColor); paintRole(painter, m_roleBounds, m_roleColor); } void KItemListGroupHeader::roleChanged(const QByteArray& current, const QByteArray& previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) } void KItemListGroupHeader::dataChanged(const QVariant& current, const QVariant& previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) } void KItemListGroupHeader::styleOptionChanged(const KItemListStyleOption& current, const KItemListStyleOption& previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) } void KItemListGroupHeader::scrollOrientationChanged(Qt::Orientation current, Qt::Orientation previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) } void KItemListGroupHeader::itemIndexChanged(int current, int previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) } void KItemListGroupHeader::resizeEvent(QGraphicsSceneResizeEvent* event) { QGraphicsWidget::resizeEvent(event); if (event->oldSize().height() != event->newSize().height()) { m_dirtyCache = true; } } void KItemListGroupHeader::updateCache() { Q_ASSERT(m_dirtyCache); // Calculate the role- and line-color. No alphablending is used for // performance reasons. const QColor c1 = textColor(); const QColor c2 = baseColor(); m_separatorColor = mixedColor(c1, c2, 10); m_roleColor = mixedColor(c1, c2, 60); const int padding = qMax(1, m_styleOption.padding); const int horizontalMargin = qMax(2, m_styleOption.horizontalMargin); const QFontMetrics fontMetrics(m_styleOption.font); const qreal roleHeight = fontMetrics.height(); const int y = (m_scrollOrientation == Qt::Vertical) ? padding : horizontalMargin; m_roleBounds = QRectF(horizontalMargin + padding, y, size().width() - 2 * padding - horizontalMargin, roleHeight); m_dirtyCache = false; } QColor KItemListGroupHeader::mixedColor(const QColor& c1, const QColor& c2, int c1Percent) { Q_ASSERT(c1Percent >= 0 && c1Percent <= 100); const int c2Percent = 100 - c1Percent; return QColor((c1.red() * c1Percent + c2.red() * c2Percent) / 100, (c1.green() * c1Percent + c2.green() * c2Percent) / 100, (c1.blue() * c1Percent + c2.blue() * c2Percent) / 100); } QPalette::ColorRole KItemListGroupHeader::normalTextColorRole() const { return QPalette::Text; } QPalette::ColorRole KItemListGroupHeader::normalBaseColorRole() const { return QPalette::Window; } QColor KItemListGroupHeader::textColor() const { const QPalette::ColorGroup group = isActiveWindow() ? QPalette::Active : QPalette::Inactive; return styleOption().palette.color(group, normalTextColorRole()); } QColor KItemListGroupHeader::baseColor() const { const QPalette::ColorGroup group = isActiveWindow() ? QPalette::Active : QPalette::Inactive; return styleOption().palette.color(group, normalBaseColorRole()); } diff --git a/src/kitemviews/kitemlistview.cpp b/src/kitemviews/kitemlistview.cpp index db258eb39..53682461a 100644 --- a/src/kitemviews/kitemlistview.cpp +++ b/src/kitemviews/kitemlistview.cpp @@ -1,2744 +1,2744 @@ /*************************************************************************** * Copyright (C) 2011 by Peter Penz * * * * Based on the Itemviews NG project from Trolltech Labs: * * http://qt.gitorious.org/qt-labs/itemviews-ng * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kitemlistview.h" #include "dolphindebug.h" #include "kitemlistcontainer.h" #include "kitemlistcontroller.h" #include "kitemlistheader.h" #include "kitemlistselectionmanager.h" #include "kitemlistviewaccessible.h" #include "kstandarditemlistwidget.h" #include "private/kitemlistheaderwidget.h" #include "private/kitemlistrubberband.h" #include "private/kitemlistsizehintresolver.h" #include "private/kitemlistviewlayouter.h" #include #include #include #include #include namespace { // Time in ms until reaching the autoscroll margin triggers // an initial autoscrolling const int InitialAutoScrollDelay = 700; // Delay in ms for triggering the next autoscroll const int RepeatingAutoScrollDelay = 1000 / 60; } #ifndef QT_NO_ACCESSIBILITY QAccessibleInterface* accessibleInterfaceFactory(const QString& key, QObject* object) { Q_UNUSED(key) if (KItemListContainer* container = qobject_cast(object)) { return new KItemListContainerAccessible(container); } else if (KItemListView* view = qobject_cast(object)) { return new KItemListViewAccessible(view); } return nullptr; } #endif KItemListView::KItemListView(QGraphicsWidget* parent) : QGraphicsWidget(parent), m_enabledSelectionToggles(false), m_grouped(false), m_supportsItemExpanding(false), m_editingRole(false), m_activeTransactions(0), m_endTransactionAnimationHint(Animation), m_itemSize(), m_controller(nullptr), m_model(nullptr), m_visibleRoles(), m_widgetCreator(nullptr), m_groupHeaderCreator(nullptr), m_styleOption(), m_visibleItems(), m_visibleGroups(), m_visibleCells(), m_sizeHintResolver(nullptr), m_layouter(nullptr), m_animation(nullptr), m_layoutTimer(nullptr), m_oldScrollOffset(0), m_oldMaximumScrollOffset(0), m_oldItemOffset(0), m_oldMaximumItemOffset(0), m_skipAutoScrollForRubberBand(false), m_rubberBand(nullptr), m_mousePos(), m_autoScrollIncrement(0), m_autoScrollTimer(nullptr), m_header(nullptr), m_headerWidget(nullptr), m_dropIndicator() { setAcceptHoverEvents(true); m_sizeHintResolver = new KItemListSizeHintResolver(this); m_layouter = new KItemListViewLayouter(m_sizeHintResolver, this); m_animation = new KItemListViewAnimation(this); connect(m_animation, &KItemListViewAnimation::finished, this, &KItemListView::slotAnimationFinished); m_layoutTimer = new QTimer(this); m_layoutTimer->setInterval(300); m_layoutTimer->setSingleShot(true); connect(m_layoutTimer, &QTimer::timeout, this, &KItemListView::slotLayoutTimerFinished); m_rubberBand = new KItemListRubberBand(this); connect(m_rubberBand, &KItemListRubberBand::activationChanged, this, &KItemListView::slotRubberBandActivationChanged); m_headerWidget = new KItemListHeaderWidget(this); m_headerWidget->setVisible(false); m_header = new KItemListHeader(this); #ifndef QT_NO_ACCESSIBILITY QAccessible::installFactory(accessibleInterfaceFactory); #endif } KItemListView::~KItemListView() { // The group headers are children of the widgets created by // widgetCreator(). So it is mandatory to delete the group headers // first. delete m_groupHeaderCreator; m_groupHeaderCreator = nullptr; delete m_widgetCreator; m_widgetCreator = nullptr; delete m_sizeHintResolver; m_sizeHintResolver = nullptr; } void KItemListView::setScrollOffset(qreal offset) { if (offset < 0) { offset = 0; } const qreal previousOffset = m_layouter->scrollOffset(); if (offset == previousOffset) { return; } m_layouter->setScrollOffset(offset); m_animation->setScrollOffset(offset); // Don't check whether the m_layoutTimer is active: Changing the // scroll offset must always trigger a synchronous layout, otherwise // the smooth-scrolling might get jerky. doLayout(NoAnimation); onScrollOffsetChanged(offset, previousOffset); } qreal KItemListView::scrollOffset() const { return m_layouter->scrollOffset(); } qreal KItemListView::maximumScrollOffset() const { return m_layouter->maximumScrollOffset(); } void KItemListView::setItemOffset(qreal offset) { if (m_layouter->itemOffset() == offset) { return; } m_layouter->setItemOffset(offset); if (m_headerWidget->isVisible()) { m_headerWidget->setOffset(offset); } // Don't check whether the m_layoutTimer is active: Changing the // item offset must always trigger a synchronous layout, otherwise // the smooth-scrolling might get jerky. doLayout(NoAnimation); } qreal KItemListView::itemOffset() const { return m_layouter->itemOffset(); } qreal KItemListView::maximumItemOffset() const { return m_layouter->maximumItemOffset(); } int KItemListView::maximumVisibleItems() const { return m_layouter->maximumVisibleItems(); } void KItemListView::setVisibleRoles(const QList& roles) { const QList previousRoles = m_visibleRoles; m_visibleRoles = roles; onVisibleRolesChanged(roles, previousRoles); m_sizeHintResolver->clearCache(); m_layouter->markAsDirty(); if (m_itemSize.isEmpty()) { m_headerWidget->setColumns(roles); updatePreferredColumnWidths(); if (!m_headerWidget->automaticColumnResizing()) { // The column-width of new roles are still 0. Apply the preferred // column-width as default with. foreach (const QByteArray& role, m_visibleRoles) { if (m_headerWidget->columnWidth(role) == 0) { const qreal width = m_headerWidget->preferredColumnWidth(role); m_headerWidget->setColumnWidth(role, width); } } applyColumnWidthsFromHeader(); } } const bool alternateBackgroundsChanged = m_itemSize.isEmpty() && ((roles.count() > 1 && previousRoles.count() <= 1) || (roles.count() <= 1 && previousRoles.count() > 1)); QHashIterator it(m_visibleItems); while (it.hasNext()) { it.next(); KItemListWidget* widget = it.value(); widget->setVisibleRoles(roles); if (alternateBackgroundsChanged) { updateAlternateBackgroundForWidget(widget); } } doLayout(NoAnimation); } QList KItemListView::visibleRoles() const { return m_visibleRoles; } void KItemListView::setAutoScroll(bool enabled) { if (enabled && !m_autoScrollTimer) { m_autoScrollTimer = new QTimer(this); m_autoScrollTimer->setSingleShot(true); connect(m_autoScrollTimer, &QTimer::timeout, this, &KItemListView::triggerAutoScrolling); m_autoScrollTimer->start(InitialAutoScrollDelay); } else if (!enabled && m_autoScrollTimer) { delete m_autoScrollTimer; m_autoScrollTimer = nullptr; } } bool KItemListView::autoScroll() const { return m_autoScrollTimer != nullptr; } void KItemListView::setEnabledSelectionToggles(bool enabled) { if (m_enabledSelectionToggles != enabled) { m_enabledSelectionToggles = enabled; QHashIterator it(m_visibleItems); while (it.hasNext()) { it.next(); it.value()->setEnabledSelectionToggle(enabled); } } } bool KItemListView::enabledSelectionToggles() const { return m_enabledSelectionToggles; } KItemListController* KItemListView::controller() const { return m_controller; } KItemModelBase* KItemListView::model() const { return m_model; } void KItemListView::setWidgetCreator(KItemListWidgetCreatorBase* widgetCreator) { delete m_widgetCreator; m_widgetCreator = widgetCreator; } KItemListWidgetCreatorBase* KItemListView::widgetCreator() const { if (!m_widgetCreator) { m_widgetCreator = defaultWidgetCreator(); } return m_widgetCreator; } void KItemListView::setGroupHeaderCreator(KItemListGroupHeaderCreatorBase* groupHeaderCreator) { delete m_groupHeaderCreator; m_groupHeaderCreator = groupHeaderCreator; } KItemListGroupHeaderCreatorBase* KItemListView::groupHeaderCreator() const { if (!m_groupHeaderCreator) { m_groupHeaderCreator = defaultGroupHeaderCreator(); } return m_groupHeaderCreator; } QSizeF KItemListView::itemSize() const { return m_itemSize; } QSizeF KItemListView::itemSizeHint() const { return m_sizeHintResolver->minSizeHint(); } const KItemListStyleOption& KItemListView::styleOption() const { return m_styleOption; } void KItemListView::setGeometry(const QRectF& rect) { QGraphicsWidget::setGeometry(rect); if (!m_model) { return; } const QSizeF newSize = rect.size(); if (m_itemSize.isEmpty()) { m_headerWidget->resize(rect.width(), m_headerWidget->size().height()); if (m_headerWidget->automaticColumnResizing()) { applyAutomaticColumnWidths(); } else { const qreal requiredWidth = columnWidthsSum(); const QSizeF dynamicItemSize(qMax(newSize.width(), requiredWidth), m_itemSize.height()); m_layouter->setItemSize(dynamicItemSize); } // Triggering a synchronous layout is fine from a performance point of view, // as with dynamic item sizes no moving animation must be done. m_layouter->setSize(newSize); doLayout(NoAnimation); } else { const bool animate = !changesItemGridLayout(newSize, m_layouter->itemSize(), m_layouter->itemMargin()); m_layouter->setSize(newSize); if (animate) { // Trigger an asynchronous relayout with m_layoutTimer to prevent // performance bottlenecks. If the timer is exceeded, an animated layout // will be triggered. if (!m_layoutTimer->isActive()) { m_layoutTimer->start(); } } else { m_layoutTimer->stop(); doLayout(NoAnimation); } } } qreal KItemListView::verticalPageStep() const { qreal headerHeight = 0; if (m_headerWidget->isVisible()) { headerHeight = m_headerWidget->size().height(); } return size().height() - headerHeight; } int KItemListView::itemAt(const QPointF& pos) const { QHashIterator it(m_visibleItems); while (it.hasNext()) { it.next(); const KItemListWidget* widget = it.value(); const QPointF mappedPos = widget->mapFromItem(this, pos); if (widget->contains(mappedPos)) { return it.key(); } } return -1; } bool KItemListView::isAboveSelectionToggle(int index, const QPointF& pos) const { if (!m_enabledSelectionToggles) { return false; } const KItemListWidget* widget = m_visibleItems.value(index); if (widget) { const QRectF selectionToggleRect = widget->selectionToggleRect(); if (!selectionToggleRect.isEmpty()) { const QPointF mappedPos = widget->mapFromItem(this, pos); return selectionToggleRect.contains(mappedPos); } } return false; } bool KItemListView::isAboveExpansionToggle(int index, const QPointF& pos) const { const KItemListWidget* widget = m_visibleItems.value(index); if (widget) { const QRectF expansionToggleRect = widget->expansionToggleRect(); if (!expansionToggleRect.isEmpty()) { const QPointF mappedPos = widget->mapFromItem(this, pos); return expansionToggleRect.contains(mappedPos); } } return false; } bool KItemListView::isAboveText(int index, const QPointF &pos) const { const KItemListWidget* widget = m_visibleItems.value(index); if (widget) { const QRectF &textRect = widget->textRect(); if (!textRect.isEmpty()) { const QPointF mappedPos = widget->mapFromItem(this, pos); return textRect.contains(mappedPos); } } return false; } int KItemListView::firstVisibleIndex() const { return m_layouter->firstVisibleIndex(); } int KItemListView::lastVisibleIndex() const { return m_layouter->lastVisibleIndex(); } void KItemListView::calculateItemSizeHints(QVector& logicalHeightHints, qreal& logicalWidthHint) const { widgetCreator()->calculateItemSizeHints(logicalHeightHints, logicalWidthHint, this); } void KItemListView::setSupportsItemExpanding(bool supportsExpanding) { if (m_supportsItemExpanding != supportsExpanding) { m_supportsItemExpanding = supportsExpanding; updateSiblingsInformation(); onSupportsItemExpandingChanged(supportsExpanding); } } bool KItemListView::supportsItemExpanding() const { return m_supportsItemExpanding; } QRectF KItemListView::itemRect(int index) const { return m_layouter->itemRect(index); } QRectF KItemListView::itemContextRect(int index) const { QRectF contextRect; const KItemListWidget* widget = m_visibleItems.value(index); if (widget) { contextRect = widget->iconRect() | widget->textRect(); contextRect.translate(itemRect(index).topLeft()); } return contextRect; } void KItemListView::scrollToItem(int index) { QRectF viewGeometry = geometry(); if (m_headerWidget->isVisible()) { const qreal headerHeight = m_headerWidget->size().height(); viewGeometry.adjust(0, headerHeight, 0, 0); } QRectF currentRect = itemRect(index); // Fix for Bug 311099 - View the underscore when using Ctrl + PagDown currentRect.adjust(-m_styleOption.horizontalMargin, -m_styleOption.verticalMargin, m_styleOption.horizontalMargin, m_styleOption.verticalMargin); if (!viewGeometry.contains(currentRect)) { qreal newOffset = scrollOffset(); if (scrollOrientation() == Qt::Vertical) { if (currentRect.top() < viewGeometry.top()) { newOffset += currentRect.top() - viewGeometry.top(); } else if (currentRect.bottom() > viewGeometry.bottom()) { newOffset += currentRect.bottom() - viewGeometry.bottom(); } } else { if (currentRect.left() < viewGeometry.left()) { newOffset += currentRect.left() - viewGeometry.left(); } else if (currentRect.right() > viewGeometry.right()) { newOffset += currentRect.right() - viewGeometry.right(); } } if (newOffset != scrollOffset()) { emit scrollTo(newOffset); } } } void KItemListView::beginTransaction() { ++m_activeTransactions; if (m_activeTransactions == 1) { onTransactionBegin(); } } void KItemListView::endTransaction() { --m_activeTransactions; if (m_activeTransactions < 0) { m_activeTransactions = 0; qCWarning(DolphinDebug) << "Mismatch between beginTransaction()/endTransaction()"; } if (m_activeTransactions == 0) { onTransactionEnd(); doLayout(m_endTransactionAnimationHint); m_endTransactionAnimationHint = Animation; } } bool KItemListView::isTransactionActive() const { return m_activeTransactions > 0; } void KItemListView::setHeaderVisible(bool visible) { if (visible && !m_headerWidget->isVisible()) { QStyleOptionHeader option; const QSize headerSize = style()->sizeFromContents(QStyle::CT_HeaderSection, &option, QSize()); m_headerWidget->setPos(0, 0); m_headerWidget->resize(size().width(), headerSize.height()); m_headerWidget->setModel(m_model); m_headerWidget->setColumns(m_visibleRoles); m_headerWidget->setZValue(1); connect(m_headerWidget, &KItemListHeaderWidget::columnWidthChanged, this, &KItemListView::slotHeaderColumnWidthChanged); connect(m_headerWidget, &KItemListHeaderWidget::columnMoved, this, &KItemListView::slotHeaderColumnMoved); connect(m_headerWidget, &KItemListHeaderWidget::sortOrderChanged, this, &KItemListView::sortOrderChanged); connect(m_headerWidget, &KItemListHeaderWidget::sortRoleChanged, this, &KItemListView::sortRoleChanged); m_layouter->setHeaderHeight(headerSize.height()); m_headerWidget->setVisible(true); } else if (!visible && m_headerWidget->isVisible()) { disconnect(m_headerWidget, &KItemListHeaderWidget::columnWidthChanged, this, &KItemListView::slotHeaderColumnWidthChanged); disconnect(m_headerWidget, &KItemListHeaderWidget::columnMoved, this, &KItemListView::slotHeaderColumnMoved); disconnect(m_headerWidget, &KItemListHeaderWidget::sortOrderChanged, this, &KItemListView::sortOrderChanged); disconnect(m_headerWidget, &KItemListHeaderWidget::sortRoleChanged, this, &KItemListView::sortRoleChanged); m_layouter->setHeaderHeight(0); m_headerWidget->setVisible(false); } } bool KItemListView::isHeaderVisible() const { return m_headerWidget->isVisible(); } KItemListHeader* KItemListView::header() const { return m_header; } QPixmap KItemListView::createDragPixmap(const KItemSet& indexes) const { QPixmap pixmap; if (indexes.count() == 1) { KItemListWidget* item = m_visibleItems.value(indexes.first()); QGraphicsView* graphicsView = scene()->views()[0]; if (item && graphicsView) { pixmap = item->createDragPixmap(nullptr, graphicsView); } } else { // TODO: Not implemented yet. Probably extend the interface // from KItemListWidget::createDragPixmap() to return a pixmap // that can be used for multiple indexes. } return pixmap; } void KItemListView::editRole(int index, const QByteArray& role) { KStandardItemListWidget* widget = qobject_cast(m_visibleItems.value(index)); if (!widget || m_editingRole) { return; } m_editingRole = true; widget->setEditedRole(role); connect(widget, &KItemListWidget::roleEditingCanceled, this, &KItemListView::slotRoleEditingCanceled); connect(widget, &KItemListWidget::roleEditingFinished, this, &KItemListView::slotRoleEditingFinished); connect(this, &KItemListView::scrollOffsetChanged, widget, &KStandardItemListWidget::finishRoleEditing); } void KItemListView::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) { QGraphicsWidget::paint(painter, option, widget); if (m_rubberBand->isActive()) { QRectF rubberBandRect = QRectF(m_rubberBand->startPosition(), m_rubberBand->endPosition()).normalized(); const QPointF topLeft = rubberBandRect.topLeft(); if (scrollOrientation() == Qt::Vertical) { rubberBandRect.moveTo(topLeft.x(), topLeft.y() - scrollOffset()); } else { rubberBandRect.moveTo(topLeft.x() - scrollOffset(), topLeft.y()); } QStyleOptionRubberBand opt; initStyleOption(&opt); opt.shape = QRubberBand::Rectangle; opt.opaque = false; opt.rect = rubberBandRect.toRect(); style()->drawControl(QStyle::CE_RubberBand, &opt, painter); } if (!m_dropIndicator.isEmpty()) { const QRectF r = m_dropIndicator.toRect(); QColor color = palette().brush(QPalette::Normal, QPalette::Highlight).color(); painter->setPen(color); // TODO: The following implementation works only for a vertical scroll-orientation // and assumes a height of the m_draggingInsertIndicator of 1. Q_ASSERT(r.height() == 1); painter->drawLine(r.left() + 1, r.top(), r.right() - 1, r.top()); color.setAlpha(128); painter->setPen(color); painter->drawRect(r.left(), r.top() - 1, r.width() - 1, 2); } } QVariant KItemListView::itemChange(GraphicsItemChange change, const QVariant &value) { if (change == QGraphicsItem::ItemSceneHasChanged && scene()) { if (!scene()->views().isEmpty()) { m_styleOption.palette = scene()->views().at(0)->palette(); } } return QGraphicsItem::itemChange(change, value); } void KItemListView::setItemSize(const QSizeF& size) { const QSizeF previousSize = m_itemSize; if (size == previousSize) { return; } // Skip animations when the number of rows or columns // are changed in the grid layout. Although the animation // engine can handle this usecase, it looks obtrusive. const bool animate = !changesItemGridLayout(m_layouter->size(), size, m_layouter->itemMargin()); const bool alternateBackgroundsChanged = (m_visibleRoles.count() > 1) && (( m_itemSize.isEmpty() && !size.isEmpty()) || (!m_itemSize.isEmpty() && size.isEmpty())); m_itemSize = size; if (alternateBackgroundsChanged) { // For an empty item size alternate backgrounds are drawn if more than // one role is shown. Assure that the backgrounds for visible items are // updated when changing the size in this context. updateAlternateBackgrounds(); } if (size.isEmpty()) { if (m_headerWidget->automaticColumnResizing()) { updatePreferredColumnWidths(); } else { // Only apply the changed height and respect the header widths // set by the user const qreal currentWidth = m_layouter->itemSize().width(); const QSizeF newSize(currentWidth, size.height()); m_layouter->setItemSize(newSize); } } else { m_layouter->setItemSize(size); } m_sizeHintResolver->clearCache(); doLayout(animate ? Animation : NoAnimation); onItemSizeChanged(size, previousSize); } void KItemListView::setStyleOption(const KItemListStyleOption& option) { if (m_styleOption == option) { return; } const KItemListStyleOption previousOption = m_styleOption; m_styleOption = option; bool animate = true; const QSizeF margin(option.horizontalMargin, option.verticalMargin); if (margin != m_layouter->itemMargin()) { // Skip animations when the number of rows or columns // are changed in the grid layout. Although the animation // engine can handle this usecase, it looks obtrusive. animate = !changesItemGridLayout(m_layouter->size(), m_layouter->itemSize(), margin); m_layouter->setItemMargin(margin); } if (m_grouped) { updateGroupHeaderHeight(); } if (animate && (previousOption.maxTextLines != option.maxTextLines || previousOption.maxTextWidth != option.maxTextWidth)) { // Animating a change of the maximum text size just results in expensive // temporary eliding and clipping operations and does not look good visually. animate = false; } QHashIterator it(m_visibleItems); while (it.hasNext()) { it.next(); it.value()->setStyleOption(option); } m_sizeHintResolver->clearCache(); m_layouter->markAsDirty(); doLayout(animate ? Animation : NoAnimation); if (m_itemSize.isEmpty()) { updatePreferredColumnWidths(); } onStyleOptionChanged(option, previousOption); } void KItemListView::setScrollOrientation(Qt::Orientation orientation) { const Qt::Orientation previousOrientation = m_layouter->scrollOrientation(); if (orientation == previousOrientation) { return; } m_layouter->setScrollOrientation(orientation); m_animation->setScrollOrientation(orientation); m_sizeHintResolver->clearCache(); if (m_grouped) { QMutableHashIterator it (m_visibleGroups); while (it.hasNext()) { it.next(); it.value()->setScrollOrientation(orientation); } updateGroupHeaderHeight(); } doLayout(NoAnimation); onScrollOrientationChanged(orientation, previousOrientation); emit scrollOrientationChanged(orientation, previousOrientation); } Qt::Orientation KItemListView::scrollOrientation() const { return m_layouter->scrollOrientation(); } KItemListWidgetCreatorBase* KItemListView::defaultWidgetCreator() const { return nullptr; } KItemListGroupHeaderCreatorBase* KItemListView::defaultGroupHeaderCreator() const { return nullptr; } void KItemListView::initializeItemListWidget(KItemListWidget* item) { - Q_UNUSED(item); + Q_UNUSED(item) } bool KItemListView::itemSizeHintUpdateRequired(const QSet& changedRoles) const { - Q_UNUSED(changedRoles); + Q_UNUSED(changedRoles) return true; } void KItemListView::onControllerChanged(KItemListController* current, KItemListController* previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) } void KItemListView::onModelChanged(KItemModelBase* current, KItemModelBase* previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) } void KItemListView::onScrollOrientationChanged(Qt::Orientation current, Qt::Orientation previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) } void KItemListView::onItemSizeChanged(const QSizeF& current, const QSizeF& previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) } void KItemListView::onScrollOffsetChanged(qreal current, qreal previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) } void KItemListView::onVisibleRolesChanged(const QList& current, const QList& previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) } void KItemListView::onStyleOptionChanged(const KItemListStyleOption& current, const KItemListStyleOption& previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) } void KItemListView::onSupportsItemExpandingChanged(bool supportsExpanding) { - Q_UNUSED(supportsExpanding); + Q_UNUSED(supportsExpanding) } void KItemListView::onTransactionBegin() { } void KItemListView::onTransactionEnd() { } bool KItemListView::event(QEvent* event) { switch (event->type()) { case QEvent::PaletteChange: updatePalette(); break; case QEvent::FontChange: updateFont(); break; default: // Forward all other events to the controller and handle them there if (!m_editingRole && m_controller && m_controller->processEvent(event, transform())) { event->accept(); return true; } } return QGraphicsWidget::event(event); } void KItemListView::mousePressEvent(QGraphicsSceneMouseEvent* event) { m_mousePos = transform().map(event->pos()); event->accept(); } void KItemListView::mouseMoveEvent(QGraphicsSceneMouseEvent* event) { QGraphicsWidget::mouseMoveEvent(event); m_mousePos = transform().map(event->pos()); if (m_autoScrollTimer && !m_autoScrollTimer->isActive()) { m_autoScrollTimer->start(InitialAutoScrollDelay); } } void KItemListView::dragEnterEvent(QGraphicsSceneDragDropEvent* event) { event->setAccepted(true); setAutoScroll(true); } void KItemListView::dragMoveEvent(QGraphicsSceneDragDropEvent* event) { QGraphicsWidget::dragMoveEvent(event); m_mousePos = transform().map(event->pos()); if (m_autoScrollTimer && !m_autoScrollTimer->isActive()) { m_autoScrollTimer->start(InitialAutoScrollDelay); } } void KItemListView::dragLeaveEvent(QGraphicsSceneDragDropEvent* event) { QGraphicsWidget::dragLeaveEvent(event); setAutoScroll(false); } void KItemListView::dropEvent(QGraphicsSceneDragDropEvent* event) { QGraphicsWidget::dropEvent(event); setAutoScroll(false); } QList KItemListView::visibleItemListWidgets() const { return m_visibleItems.values(); } void KItemListView::updateFont() { if (scene() && !scene()->views().isEmpty()) { KItemListStyleOption option = styleOption(); option.font = scene()->views().first()->font(); option.fontMetrics = QFontMetrics(option.font); setStyleOption(option); } } void KItemListView::updatePalette() { if (scene() && !scene()->views().isEmpty()) { KItemListStyleOption option = styleOption(); option.palette = scene()->views().first()->palette(); setStyleOption(option); } } void KItemListView::slotItemsInserted(const KItemRangeList& itemRanges) { if (m_itemSize.isEmpty()) { updatePreferredColumnWidths(itemRanges); } const bool hasMultipleRanges = (itemRanges.count() > 1); if (hasMultipleRanges) { beginTransaction(); } m_layouter->markAsDirty(); m_sizeHintResolver->itemsInserted(itemRanges); int previouslyInsertedCount = 0; foreach (const KItemRange& range, itemRanges) { // range.index is related to the model before anything has been inserted. // As in each loop the current item-range gets inserted the index must // be increased by the already previously inserted items. const int index = range.index + previouslyInsertedCount; const int count = range.count; if (index < 0 || count <= 0) { qCWarning(DolphinDebug) << "Invalid item range (index:" << index << ", count:" << count << ")"; continue; } previouslyInsertedCount += count; // Determine which visible items must be moved QList itemsToMove; QHashIterator it(m_visibleItems); while (it.hasNext()) { it.next(); const int visibleItemIndex = it.key(); if (visibleItemIndex >= index) { itemsToMove.append(visibleItemIndex); } } // Update the indexes of all KItemListWidget instances that are located // after the inserted items. It is important to adjust the indexes in the order // from the highest index to the lowest index to prevent overlaps when setting the new index. std::sort(itemsToMove.begin(), itemsToMove.end()); for (int i = itemsToMove.count() - 1; i >= 0; --i) { KItemListWidget* widget = m_visibleItems.value(itemsToMove[i]); Q_ASSERT(widget); const int newIndex = widget->index() + count; if (hasMultipleRanges) { setWidgetIndex(widget, newIndex); } else { // Try to animate the moving of the item moveWidgetToIndex(widget, newIndex); } } if (m_model->count() == count && m_activeTransactions == 0) { // Check whether a scrollbar is required to show the inserted items. In this case // the size of the layouter will be decreased before calling doLayout(): This prevents // an unnecessary temporary animation due to the geometry change of the inserted scrollbar. const bool verticalScrollOrientation = (scrollOrientation() == Qt::Vertical); const bool decreaseLayouterSize = ( verticalScrollOrientation && maximumScrollOffset() > size().height()) || (!verticalScrollOrientation && maximumScrollOffset() > size().width()); if (decreaseLayouterSize) { const int scrollBarExtent = style()->pixelMetric(QStyle::PM_ScrollBarExtent); int scrollbarSpacing = 0; if (style()->styleHint(QStyle::SH_ScrollView_FrameOnlyAroundContents)) { scrollbarSpacing = style()->pixelMetric(QStyle::PM_ScrollView_ScrollBarSpacing); } QSizeF layouterSize = m_layouter->size(); if (verticalScrollOrientation) { layouterSize.rwidth() -= scrollBarExtent + scrollbarSpacing; } else { layouterSize.rheight() -= scrollBarExtent + scrollbarSpacing; } m_layouter->setSize(layouterSize); } } if (!hasMultipleRanges) { doLayout(animateChangedItemCount(count) ? Animation : NoAnimation, index, count); updateSiblingsInformation(); } } if (m_controller) { m_controller->selectionManager()->itemsInserted(itemRanges); } if (hasMultipleRanges) { m_endTransactionAnimationHint = NoAnimation; endTransaction(); updateSiblingsInformation(); } if (m_grouped && (hasMultipleRanges || itemRanges.first().count < m_model->count())) { // In case if items of the same group have been inserted before an item that // currently represents the first item of the group, the group header of // this item must be removed. updateVisibleGroupHeaders(); } if (useAlternateBackgrounds()) { updateAlternateBackgrounds(); } } void KItemListView::slotItemsRemoved(const KItemRangeList& itemRanges) { if (m_itemSize.isEmpty()) { // Don't pass the item-range: The preferred column-widths of // all items must be adjusted when removing items. updatePreferredColumnWidths(); } const bool hasMultipleRanges = (itemRanges.count() > 1); if (hasMultipleRanges) { beginTransaction(); } m_layouter->markAsDirty(); m_sizeHintResolver->itemsRemoved(itemRanges); for (int i = itemRanges.count() - 1; i >= 0; --i) { const KItemRange& range = itemRanges[i]; const int index = range.index; const int count = range.count; if (index < 0 || count <= 0) { qCWarning(DolphinDebug) << "Invalid item range (index:" << index << ", count:" << count << ")"; continue; } const int firstRemovedIndex = index; const int lastRemovedIndex = index + count - 1; // Remember which items have to be moved because they are behind the removed range. QVector itemsToMove; // Remove all KItemListWidget instances that got deleted foreach (KItemListWidget* widget, m_visibleItems) { const int i = widget->index(); if (i < firstRemovedIndex) { continue; } else if (i > lastRemovedIndex) { itemsToMove.append(i); continue; } m_animation->stop(widget); // Stopping the animation might lead to recycling the widget if // it is invisible (see slotAnimationFinished()). // Check again whether it is still visible: if (!m_visibleItems.contains(i)) { continue; } if (m_model->count() == 0 || hasMultipleRanges || !animateChangedItemCount(count)) { // Remove the widget without animation recycleWidget(widget); } else { // Animate the removing of the items. Special case: When removing an item there // is no valid model index available anymore. For the // remove-animation the item gets removed from m_visibleItems but the widget // will stay alive until the animation has been finished and will // be recycled (deleted) in KItemListView::slotAnimationFinished(). m_visibleItems.remove(i); widget->setIndex(-1); m_animation->start(widget, KItemListViewAnimation::DeleteAnimation); } } // Update the indexes of all KItemListWidget instances that are located // after the deleted items. It is important to update them in ascending // order to prevent overlaps when setting the new index. std::sort(itemsToMove.begin(), itemsToMove.end()); foreach (int i, itemsToMove) { KItemListWidget* widget = m_visibleItems.value(i); Q_ASSERT(widget); const int newIndex = i - count; if (hasMultipleRanges) { setWidgetIndex(widget, newIndex); } else { // Try to animate the moving of the item moveWidgetToIndex(widget, newIndex); } } if (!hasMultipleRanges) { // The decrease-layout-size optimization in KItemListView::slotItemsInserted() // assumes an updated geometry. If items are removed during an active transaction, // the transaction will be temporary deactivated so that doLayout() triggers a // geometry update if necessary. const int activeTransactions = m_activeTransactions; m_activeTransactions = 0; doLayout(animateChangedItemCount(count) ? Animation : NoAnimation, index, -count); m_activeTransactions = activeTransactions; updateSiblingsInformation(); } } if (m_controller) { m_controller->selectionManager()->itemsRemoved(itemRanges); } if (hasMultipleRanges) { m_endTransactionAnimationHint = NoAnimation; endTransaction(); updateSiblingsInformation(); } if (m_grouped && (hasMultipleRanges || m_model->count() > 0)) { // In case if the first item of a group has been removed, the group header // must be applied to the next visible item. updateVisibleGroupHeaders(); } if (useAlternateBackgrounds()) { updateAlternateBackgrounds(); } } void KItemListView::slotItemsMoved(const KItemRange& itemRange, const QList& movedToIndexes) { m_sizeHintResolver->itemsMoved(itemRange, movedToIndexes); m_layouter->markAsDirty(); if (m_controller) { m_controller->selectionManager()->itemsMoved(itemRange, movedToIndexes); } const int firstVisibleMovedIndex = qMax(firstVisibleIndex(), itemRange.index); const int lastVisibleMovedIndex = qMin(lastVisibleIndex(), itemRange.index + itemRange.count - 1); for (int index = firstVisibleMovedIndex; index <= lastVisibleMovedIndex; ++index) { KItemListWidget* widget = m_visibleItems.value(index); if (widget) { updateWidgetProperties(widget, index); initializeItemListWidget(widget); } } doLayout(NoAnimation); updateSiblingsInformation(); } void KItemListView::slotItemsChanged(const KItemRangeList& itemRanges, const QSet& roles) { const bool updateSizeHints = itemSizeHintUpdateRequired(roles); if (updateSizeHints && m_itemSize.isEmpty()) { updatePreferredColumnWidths(itemRanges); } foreach (const KItemRange& itemRange, itemRanges) { const int index = itemRange.index; const int count = itemRange.count; if (updateSizeHints) { m_sizeHintResolver->itemsChanged(index, count, roles); m_layouter->markAsDirty(); if (!m_layoutTimer->isActive()) { m_layoutTimer->start(); } } // Apply the changed roles to the visible item-widgets const int lastIndex = index + count - 1; for (int i = index; i <= lastIndex; ++i) { KItemListWidget* widget = m_visibleItems.value(i); if (widget) { widget->setData(m_model->data(i), roles); } } if (m_grouped && roles.contains(m_model->sortRole())) { // The sort-role has been changed which might result // in modified group headers updateVisibleGroupHeaders(); doLayout(NoAnimation); } QAccessibleTableModelChangeEvent ev(this, QAccessibleTableModelChangeEvent::DataChanged); ev.setFirstRow(itemRange.index); ev.setLastRow(itemRange.index + itemRange.count); QAccessible::updateAccessibility(&ev); } } void KItemListView::slotGroupsChanged() { updateVisibleGroupHeaders(); doLayout(NoAnimation); updateSiblingsInformation(); } void KItemListView::slotGroupedSortingChanged(bool current) { m_grouped = current; m_layouter->markAsDirty(); if (m_grouped) { updateGroupHeaderHeight(); } else { // Clear all visible headers. Note that the QHashIterator takes a copy of // m_visibleGroups. Therefore, it remains valid even if items are removed // from m_visibleGroups in recycleGroupHeaderForWidget(). QHashIterator it(m_visibleGroups); while (it.hasNext()) { it.next(); recycleGroupHeaderForWidget(it.key()); } Q_ASSERT(m_visibleGroups.isEmpty()); } if (useAlternateBackgrounds()) { // Changing the group mode requires to update the alternate backgrounds // as with the enabled group mode the altering is done on base of the first // group item. updateAlternateBackgrounds(); } updateSiblingsInformation(); doLayout(NoAnimation); } void KItemListView::slotSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) if (m_grouped) { updateVisibleGroupHeaders(); doLayout(NoAnimation); } } void KItemListView::slotSortRoleChanged(const QByteArray& current, const QByteArray& previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) if (m_grouped) { updateVisibleGroupHeaders(); doLayout(NoAnimation); } } void KItemListView::slotCurrentChanged(int current, int previous) { - Q_UNUSED(previous); + Q_UNUSED(previous) // In SingleSelection mode (e.g., in the Places Panel), the current item is // always the selected item. It is not necessary to highlight the current item then. if (m_controller->selectionBehavior() != KItemListController::SingleSelection) { KItemListWidget* previousWidget = m_visibleItems.value(previous, nullptr); if (previousWidget) { previousWidget->setCurrent(false); } KItemListWidget* currentWidget = m_visibleItems.value(current, nullptr); if (currentWidget) { currentWidget->setCurrent(true); } } QAccessibleEvent ev(this, QAccessible::Focus); ev.setChild(current); QAccessible::updateAccessibility(&ev); } void KItemListView::slotSelectionChanged(const KItemSet& current, const KItemSet& previous) { - Q_UNUSED(previous); + Q_UNUSED(previous) QHashIterator it(m_visibleItems); while (it.hasNext()) { it.next(); const int index = it.key(); KItemListWidget* widget = it.value(); widget->setSelected(current.contains(index)); } } void KItemListView::slotAnimationFinished(QGraphicsWidget* widget, KItemListViewAnimation::AnimationType type) { KItemListWidget* itemListWidget = qobject_cast(widget); Q_ASSERT(itemListWidget); switch (type) { case KItemListViewAnimation::DeleteAnimation: { // As we recycle the widget in this case it is important to assure that no // other animation has been started. This is a convention in KItemListView and // not a requirement defined by KItemListViewAnimation. Q_ASSERT(!m_animation->isStarted(itemListWidget)); // All KItemListWidgets that are animated by the DeleteAnimation are not maintained // by m_visibleWidgets and must be deleted manually after the animation has // been finished. recycleGroupHeaderForWidget(itemListWidget); widgetCreator()->recycle(itemListWidget); break; } case KItemListViewAnimation::CreateAnimation: case KItemListViewAnimation::MovingAnimation: case KItemListViewAnimation::ResizeAnimation: { const int index = itemListWidget->index(); const bool invisible = (index < m_layouter->firstVisibleIndex()) || (index > m_layouter->lastVisibleIndex()); if (invisible && !m_animation->isStarted(itemListWidget)) { recycleWidget(itemListWidget); } break; } default: break; } } void KItemListView::slotLayoutTimerFinished() { m_layouter->setSize(geometry().size()); doLayout(Animation); } void KItemListView::slotRubberBandPosChanged() { update(); } void KItemListView::slotRubberBandActivationChanged(bool active) { if (active) { connect(m_rubberBand, &KItemListRubberBand::startPositionChanged, this, &KItemListView::slotRubberBandPosChanged); connect(m_rubberBand, &KItemListRubberBand::endPositionChanged, this, &KItemListView::slotRubberBandPosChanged); m_skipAutoScrollForRubberBand = true; } else { disconnect(m_rubberBand, &KItemListRubberBand::startPositionChanged, this, &KItemListView::slotRubberBandPosChanged); disconnect(m_rubberBand, &KItemListRubberBand::endPositionChanged, this, &KItemListView::slotRubberBandPosChanged); m_skipAutoScrollForRubberBand = false; } update(); } void KItemListView::slotHeaderColumnWidthChanged(const QByteArray& role, qreal currentWidth, qreal previousWidth) { - Q_UNUSED(role); - Q_UNUSED(currentWidth); - Q_UNUSED(previousWidth); + Q_UNUSED(role) + Q_UNUSED(currentWidth) + Q_UNUSED(previousWidth) m_headerWidget->setAutomaticColumnResizing(false); applyColumnWidthsFromHeader(); doLayout(NoAnimation); } void KItemListView::slotHeaderColumnMoved(const QByteArray& role, int currentIndex, int previousIndex) { Q_ASSERT(m_visibleRoles[previousIndex] == role); const QList previous = m_visibleRoles; QList current = m_visibleRoles; current.removeAt(previousIndex); current.insert(currentIndex, role); setVisibleRoles(current); emit visibleRolesChanged(current, previous); } void KItemListView::triggerAutoScrolling() { if (!m_autoScrollTimer) { return; } int pos = 0; int visibleSize = 0; if (scrollOrientation() == Qt::Vertical) { pos = m_mousePos.y(); visibleSize = size().height(); } else { pos = m_mousePos.x(); visibleSize = size().width(); } if (m_autoScrollTimer->interval() == InitialAutoScrollDelay) { m_autoScrollIncrement = 0; } m_autoScrollIncrement = calculateAutoScrollingIncrement(pos, visibleSize, m_autoScrollIncrement); if (m_autoScrollIncrement == 0) { // The mouse position is not above an autoscroll margin (the autoscroll timer // will be restarted in mouseMoveEvent()) m_autoScrollTimer->stop(); return; } if (m_rubberBand->isActive() && m_skipAutoScrollForRubberBand) { // If a rubberband selection is ongoing the autoscrolling may only get triggered // if the direction of the rubberband is similar to the autoscroll direction. This // prevents that starting to create a rubberband within the autoscroll margins starts // an autoscrolling. const qreal minDiff = 4; // Ignore any autoscrolling if the rubberband is very small const qreal diff = (scrollOrientation() == Qt::Vertical) ? m_rubberBand->endPosition().y() - m_rubberBand->startPosition().y() : m_rubberBand->endPosition().x() - m_rubberBand->startPosition().x(); if (qAbs(diff) < minDiff || (m_autoScrollIncrement < 0 && diff > 0) || (m_autoScrollIncrement > 0 && diff < 0)) { // The rubberband direction is different from the scroll direction (e.g. the rubberband has // been moved up although the autoscroll direction might be down) m_autoScrollTimer->stop(); return; } } // As soon as the autoscrolling has been triggered at least once despite having an active rubberband, // the autoscrolling may not get skipped anymore until a new rubberband is created m_skipAutoScrollForRubberBand = false; const qreal maxVisibleOffset = qMax(qreal(0), maximumScrollOffset() - visibleSize); const qreal newScrollOffset = qMin(scrollOffset() + m_autoScrollIncrement, maxVisibleOffset); setScrollOffset(newScrollOffset); // Trigger the autoscroll timer which will periodically call // triggerAutoScrolling() m_autoScrollTimer->start(RepeatingAutoScrollDelay); } void KItemListView::slotGeometryOfGroupHeaderParentChanged() { KItemListWidget* widget = qobject_cast(sender()); Q_ASSERT(widget); KItemListGroupHeader* groupHeader = m_visibleGroups.value(widget); Q_ASSERT(groupHeader); updateGroupHeaderLayout(widget); } void KItemListView::slotRoleEditingCanceled(int index, const QByteArray& role, const QVariant& value) { disconnectRoleEditingSignals(index); emit roleEditingCanceled(index, role, value); m_editingRole = false; } void KItemListView::slotRoleEditingFinished(int index, const QByteArray& role, const QVariant& value) { disconnectRoleEditingSignals(index); emit roleEditingFinished(index, role, value); m_editingRole = false; } void KItemListView::setController(KItemListController* controller) { if (m_controller != controller) { KItemListController* previous = m_controller; if (previous) { KItemListSelectionManager* selectionManager = previous->selectionManager(); disconnect(selectionManager, &KItemListSelectionManager::currentChanged, this, &KItemListView::slotCurrentChanged); disconnect(selectionManager, &KItemListSelectionManager::selectionChanged, this, &KItemListView::slotSelectionChanged); } m_controller = controller; if (controller) { KItemListSelectionManager* selectionManager = controller->selectionManager(); connect(selectionManager, &KItemListSelectionManager::currentChanged, this, &KItemListView::slotCurrentChanged); connect(selectionManager, &KItemListSelectionManager::selectionChanged, this, &KItemListView::slotSelectionChanged); } onControllerChanged(controller, previous); } } void KItemListView::setModel(KItemModelBase* model) { if (m_model == model) { return; } KItemModelBase* previous = m_model; if (m_model) { disconnect(m_model, &KItemModelBase::itemsChanged, this, &KItemListView::slotItemsChanged); disconnect(m_model, &KItemModelBase::itemsInserted, this, &KItemListView::slotItemsInserted); disconnect(m_model, &KItemModelBase::itemsRemoved, this, &KItemListView::slotItemsRemoved); disconnect(m_model, &KItemModelBase::itemsMoved, this, &KItemListView::slotItemsMoved); disconnect(m_model, &KItemModelBase::groupsChanged, this, &KItemListView::slotGroupsChanged); disconnect(m_model, &KItemModelBase::groupedSortingChanged, this, &KItemListView::slotGroupedSortingChanged); disconnect(m_model, &KItemModelBase::sortOrderChanged, this, &KItemListView::slotSortOrderChanged); disconnect(m_model, &KItemModelBase::sortRoleChanged, this, &KItemListView::slotSortRoleChanged); m_sizeHintResolver->itemsRemoved(KItemRangeList() << KItemRange(0, m_model->count())); } m_model = model; m_layouter->setModel(model); m_grouped = model->groupedSorting(); if (m_model) { connect(m_model, &KItemModelBase::itemsChanged, this, &KItemListView::slotItemsChanged); connect(m_model, &KItemModelBase::itemsInserted, this, &KItemListView::slotItemsInserted); connect(m_model, &KItemModelBase::itemsRemoved, this, &KItemListView::slotItemsRemoved); connect(m_model, &KItemModelBase::itemsMoved, this, &KItemListView::slotItemsMoved); connect(m_model, &KItemModelBase::groupsChanged, this, &KItemListView::slotGroupsChanged); connect(m_model, &KItemModelBase::groupedSortingChanged, this, &KItemListView::slotGroupedSortingChanged); connect(m_model, &KItemModelBase::sortOrderChanged, this, &KItemListView::slotSortOrderChanged); connect(m_model, &KItemModelBase::sortRoleChanged, this, &KItemListView::slotSortRoleChanged); const int itemCount = m_model->count(); if (itemCount > 0) { slotItemsInserted(KItemRangeList() << KItemRange(0, itemCount)); } } onModelChanged(model, previous); } KItemListRubberBand* KItemListView::rubberBand() const { return m_rubberBand; } void KItemListView::doLayout(LayoutAnimationHint hint, int changedIndex, int changedCount) { if (m_layoutTimer->isActive()) { m_layoutTimer->stop(); } if (m_activeTransactions > 0) { if (hint == NoAnimation) { // As soon as at least one property change should be done without animation, // the whole transaction will be marked as not animated. m_endTransactionAnimationHint = NoAnimation; } return; } if (!m_model || m_model->count() < 0) { return; } int firstVisibleIndex = m_layouter->firstVisibleIndex(); if (firstVisibleIndex < 0) { emitOffsetChanges(); return; } // Do a sanity check of the scroll-offset property: When properties of the itemlist-view have been changed // it might be possible that the maximum offset got changed too. Assure that the full visible range // is still shown if the maximum offset got decreased. const qreal visibleOffsetRange = (scrollOrientation() == Qt::Horizontal) ? size().width() : size().height(); const qreal maxOffsetToShowFullRange = maximumScrollOffset() - visibleOffsetRange; if (scrollOffset() > maxOffsetToShowFullRange) { m_layouter->setScrollOffset(qMax(qreal(0), maxOffsetToShowFullRange)); firstVisibleIndex = m_layouter->firstVisibleIndex(); } const int lastVisibleIndex = m_layouter->lastVisibleIndex(); int firstSibblingIndex = -1; int lastSibblingIndex = -1; const bool supportsExpanding = supportsItemExpanding(); QList reusableItems = recycleInvisibleItems(firstVisibleIndex, lastVisibleIndex, hint); // Assure that for each visible item a KItemListWidget is available. KItemListWidget // instances from invisible items are reused. If no reusable items are // found then new KItemListWidget instances get created. const bool animate = (hint == Animation); for (int i = firstVisibleIndex; i <= lastVisibleIndex; ++i) { bool applyNewPos = true; bool wasHidden = false; const QRectF itemBounds = m_layouter->itemRect(i); const QPointF newPos = itemBounds.topLeft(); KItemListWidget* widget = m_visibleItems.value(i); if (!widget) { wasHidden = true; if (!reusableItems.isEmpty()) { // Reuse a KItemListWidget instance from an invisible item const int oldIndex = reusableItems.takeLast(); widget = m_visibleItems.value(oldIndex); setWidgetIndex(widget, i); updateWidgetProperties(widget, i); initializeItemListWidget(widget); } else { // No reusable KItemListWidget instance is available, create a new one widget = createWidget(i); } widget->resize(itemBounds.size()); if (animate && changedCount < 0) { // Items have been deleted. if (i >= changedIndex) { // The item is located behind the removed range. Move the // created item to the imaginary old position outside the // view. It will get animated to the new position later. const int previousIndex = i - changedCount; const QRectF itemRect = m_layouter->itemRect(previousIndex); if (itemRect.isEmpty()) { const QPointF invisibleOldPos = (scrollOrientation() == Qt::Vertical) ? QPointF(0, size().height()) : QPointF(size().width(), 0); widget->setPos(invisibleOldPos); } else { widget->setPos(itemRect.topLeft()); } applyNewPos = false; } } if (supportsExpanding && changedCount == 0) { if (firstSibblingIndex < 0) { firstSibblingIndex = i; } lastSibblingIndex = i; } } if (animate) { if (m_animation->isStarted(widget, KItemListViewAnimation::MovingAnimation)) { m_animation->start(widget, KItemListViewAnimation::MovingAnimation, newPos); applyNewPos = false; } const bool itemsRemoved = (changedCount < 0); const bool itemsInserted = (changedCount > 0); if (itemsRemoved && (i >= changedIndex)) { // The item is located after the removed items. Animate the moving of the position. applyNewPos = !moveWidget(widget, newPos); } else if (itemsInserted && i >= changedIndex) { // The item is located after the first inserted item if (i <= changedIndex + changedCount - 1) { // The item is an inserted item. Animate the appearing of the item. // For performance reasons no animation is done when changedCount is equal // to all available items. if (changedCount < m_model->count()) { m_animation->start(widget, KItemListViewAnimation::CreateAnimation); } } else if (!m_animation->isStarted(widget, KItemListViewAnimation::CreateAnimation)) { // The item was already there before, so animate the moving of the position. // No moving animation is done if the item is animated by a create animation: This // prevents a "move animation mess" when inserting several ranges in parallel. applyNewPos = !moveWidget(widget, newPos); } } else if (!itemsRemoved && !itemsInserted && !wasHidden) { // The size of the view might have been changed. Animate the moving of the position. applyNewPos = !moveWidget(widget, newPos); } } else { m_animation->stop(widget); } if (applyNewPos) { widget->setPos(newPos); } Q_ASSERT(widget->index() == i); widget->setVisible(true); if (widget->size() != itemBounds.size()) { // Resize the widget for the item to the changed size. if (animate) { // If a dynamic item size is used then no animation is done in the direction // of the dynamic size. if (m_itemSize.width() <= 0) { // The width is dynamic, apply the new width without animation. widget->resize(itemBounds.width(), widget->size().height()); } else if (m_itemSize.height() <= 0) { // The height is dynamic, apply the new height without animation. widget->resize(widget->size().width(), itemBounds.height()); } m_animation->start(widget, KItemListViewAnimation::ResizeAnimation, itemBounds.size()); } else { widget->resize(itemBounds.size()); } } // Updating the cell-information must be done as last step: The decision whether the // moving-animation should be started at all is based on the previous cell-information. const Cell cell(m_layouter->itemColumn(i), m_layouter->itemRow(i)); m_visibleCells.insert(i, cell); } // Delete invisible KItemListWidget instances that have not been reused foreach (int index, reusableItems) { recycleWidget(m_visibleItems.value(index)); } if (supportsExpanding && firstSibblingIndex >= 0) { Q_ASSERT(lastSibblingIndex >= 0); updateSiblingsInformation(firstSibblingIndex, lastSibblingIndex); } if (m_grouped) { // Update the layout of all visible group headers QHashIterator it(m_visibleGroups); while (it.hasNext()) { it.next(); updateGroupHeaderLayout(it.key()); } } emitOffsetChanges(); } QList KItemListView::recycleInvisibleItems(int firstVisibleIndex, int lastVisibleIndex, LayoutAnimationHint hint) { // Determine all items that are completely invisible and might be // reused for items that just got (at least partly) visible. If the // animation hint is set to 'Animation' items that do e.g. an animated // moving of their position are not marked as invisible: This assures // that a scrolling inside the view can be done without breaking an animation. QList items; QHashIterator it(m_visibleItems); while (it.hasNext()) { it.next(); KItemListWidget* widget = it.value(); const int index = widget->index(); const bool invisible = (index < firstVisibleIndex) || (index > lastVisibleIndex); if (invisible) { if (m_animation->isStarted(widget)) { if (hint == NoAnimation) { // Stopping the animation will call KItemListView::slotAnimationFinished() // and the widget will be recycled if necessary there. m_animation->stop(widget); } } else { widget->setVisible(false); items.append(index); if (m_grouped) { recycleGroupHeaderForWidget(widget); } } } } return items; } bool KItemListView::moveWidget(KItemListWidget* widget,const QPointF& newPos) { if (widget->pos() == newPos) { return false; } bool startMovingAnim = false; if (m_itemSize.isEmpty()) { // The items are not aligned in a grid but either as columns or rows. startMovingAnim = true; } else { // When having a grid the moving-animation should only be started, if it is done within // one row in the vertical scroll-orientation or one column in the horizontal scroll-orientation. // Otherwise instead of a moving-animation a create-animation on the new position will be used // instead. This is done to prevent overlapping (and confusing) moving-animations. const int index = widget->index(); const Cell cell = m_visibleCells.value(index); if (cell.column >= 0 && cell.row >= 0) { if (scrollOrientation() == Qt::Vertical) { startMovingAnim = (cell.row == m_layouter->itemRow(index)); } else { startMovingAnim = (cell.column == m_layouter->itemColumn(index)); } } } if (startMovingAnim) { m_animation->start(widget, KItemListViewAnimation::MovingAnimation, newPos); return true; } m_animation->stop(widget); m_animation->start(widget, KItemListViewAnimation::CreateAnimation); return false; } void KItemListView::emitOffsetChanges() { const qreal newScrollOffset = m_layouter->scrollOffset(); if (m_oldScrollOffset != newScrollOffset) { emit scrollOffsetChanged(newScrollOffset, m_oldScrollOffset); m_oldScrollOffset = newScrollOffset; } const qreal newMaximumScrollOffset = m_layouter->maximumScrollOffset(); if (m_oldMaximumScrollOffset != newMaximumScrollOffset) { emit maximumScrollOffsetChanged(newMaximumScrollOffset, m_oldMaximumScrollOffset); m_oldMaximumScrollOffset = newMaximumScrollOffset; } const qreal newItemOffset = m_layouter->itemOffset(); if (m_oldItemOffset != newItemOffset) { emit itemOffsetChanged(newItemOffset, m_oldItemOffset); m_oldItemOffset = newItemOffset; } const qreal newMaximumItemOffset = m_layouter->maximumItemOffset(); if (m_oldMaximumItemOffset != newMaximumItemOffset) { emit maximumItemOffsetChanged(newMaximumItemOffset, m_oldMaximumItemOffset); m_oldMaximumItemOffset = newMaximumItemOffset; } } KItemListWidget* KItemListView::createWidget(int index) { KItemListWidget* widget = widgetCreator()->create(this); widget->setFlag(QGraphicsItem::ItemStacksBehindParent); m_visibleItems.insert(index, widget); m_visibleCells.insert(index, Cell()); updateWidgetProperties(widget, index); initializeItemListWidget(widget); return widget; } void KItemListView::recycleWidget(KItemListWidget* widget) { if (m_grouped) { recycleGroupHeaderForWidget(widget); } const int index = widget->index(); m_visibleItems.remove(index); m_visibleCells.remove(index); widgetCreator()->recycle(widget); } void KItemListView::setWidgetIndex(KItemListWidget* widget, int index) { const int oldIndex = widget->index(); m_visibleItems.remove(oldIndex); m_visibleCells.remove(oldIndex); m_visibleItems.insert(index, widget); m_visibleCells.insert(index, Cell()); widget->setIndex(index); } void KItemListView::moveWidgetToIndex(KItemListWidget* widget, int index) { const int oldIndex = widget->index(); const Cell oldCell = m_visibleCells.value(oldIndex); setWidgetIndex(widget, index); const Cell newCell(m_layouter->itemColumn(index), m_layouter->itemRow(index)); const bool vertical = (scrollOrientation() == Qt::Vertical); const bool updateCell = (vertical && oldCell.row == newCell.row) || (!vertical && oldCell.column == newCell.column); if (updateCell) { m_visibleCells.insert(index, newCell); } } void KItemListView::setLayouterSize(const QSizeF& size, SizeType sizeType) { switch (sizeType) { case LayouterSize: m_layouter->setSize(size); break; case ItemSize: m_layouter->setItemSize(size); break; default: break; } } void KItemListView::updateWidgetProperties(KItemListWidget* widget, int index) { widget->setVisibleRoles(m_visibleRoles); updateWidgetColumnWidths(widget); widget->setStyleOption(m_styleOption); const KItemListSelectionManager* selectionManager = m_controller->selectionManager(); // In SingleSelection mode (e.g., in the Places Panel), the current item is // always the selected item. It is not necessary to highlight the current item then. if (m_controller->selectionBehavior() != KItemListController::SingleSelection) { widget->setCurrent(index == selectionManager->currentItem()); } widget->setSelected(selectionManager->isSelected(index)); widget->setHovered(false); widget->setEnabledSelectionToggle(enabledSelectionToggles()); widget->setIndex(index); widget->setData(m_model->data(index)); widget->setSiblingsInformation(QBitArray()); updateAlternateBackgroundForWidget(widget); if (m_grouped) { updateGroupHeaderForWidget(widget); } } void KItemListView::updateGroupHeaderForWidget(KItemListWidget* widget) { Q_ASSERT(m_grouped); const int index = widget->index(); if (!m_layouter->isFirstGroupItem(index)) { // The widget does not represent the first item of a group // and hence requires no header recycleGroupHeaderForWidget(widget); return; } const QList > groups = model()->groups(); if (groups.isEmpty() || !groupHeaderCreator()) { return; } KItemListGroupHeader* groupHeader = m_visibleGroups.value(widget); if (!groupHeader) { groupHeader = groupHeaderCreator()->create(this); groupHeader->setParentItem(widget); m_visibleGroups.insert(widget, groupHeader); connect(widget, &KItemListWidget::geometryChanged, this, &KItemListView::slotGeometryOfGroupHeaderParentChanged); } Q_ASSERT(groupHeader->parentItem() == widget); const int groupIndex = groupIndexForItem(index); Q_ASSERT(groupIndex >= 0); groupHeader->setData(groups.at(groupIndex).second); groupHeader->setRole(model()->sortRole()); groupHeader->setStyleOption(m_styleOption); groupHeader->setScrollOrientation(scrollOrientation()); groupHeader->setItemIndex(index); groupHeader->show(); } void KItemListView::updateGroupHeaderLayout(KItemListWidget* widget) { KItemListGroupHeader* groupHeader = m_visibleGroups.value(widget); Q_ASSERT(groupHeader); const int index = widget->index(); const QRectF groupHeaderRect = m_layouter->groupHeaderRect(index); const QRectF itemRect = m_layouter->itemRect(index); // The group-header is a child of the itemlist widget. Translate the // group header position to the relative position. if (scrollOrientation() == Qt::Vertical) { // In the vertical scroll orientation the group header should always span // the whole width no matter which temporary position the parent widget // has. In this case the x-position and width will be adjusted manually. const qreal x = -widget->x() - itemOffset(); const qreal width = maximumItemOffset(); groupHeader->setPos(x, -groupHeaderRect.height()); groupHeader->resize(width, groupHeaderRect.size().height()); } else { groupHeader->setPos(groupHeaderRect.x() - itemRect.x(), -widget->y()); groupHeader->resize(groupHeaderRect.size()); } } void KItemListView::recycleGroupHeaderForWidget(KItemListWidget* widget) { KItemListGroupHeader* header = m_visibleGroups.value(widget); if (header) { header->setParentItem(nullptr); groupHeaderCreator()->recycle(header); m_visibleGroups.remove(widget); disconnect(widget, &KItemListWidget::geometryChanged, this, &KItemListView::slotGeometryOfGroupHeaderParentChanged); } } void KItemListView::updateVisibleGroupHeaders() { Q_ASSERT(m_grouped); m_layouter->markAsDirty(); QHashIterator it(m_visibleItems); while (it.hasNext()) { it.next(); updateGroupHeaderForWidget(it.value()); } } int KItemListView::groupIndexForItem(int index) const { Q_ASSERT(m_grouped); const QList > groups = model()->groups(); if (groups.isEmpty()) { return -1; } int min = 0; int max = groups.count() - 1; int mid = 0; do { mid = (min + max) / 2; if (index > groups[mid].first) { min = mid + 1; } else { max = mid - 1; } } while (groups[mid].first != index && min <= max); if (min > max) { while (groups[mid].first > index && mid > 0) { --mid; } } return mid; } void KItemListView::updateAlternateBackgrounds() { QHashIterator it(m_visibleItems); while (it.hasNext()) { it.next(); updateAlternateBackgroundForWidget(it.value()); } } void KItemListView::updateAlternateBackgroundForWidget(KItemListWidget* widget) { bool enabled = useAlternateBackgrounds(); if (enabled) { const int index = widget->index(); enabled = (index & 0x1) > 0; if (m_grouped) { const int groupIndex = groupIndexForItem(index); if (groupIndex >= 0) { const QList > groups = model()->groups(); const int indexOfFirstGroupItem = groups[groupIndex].first; const int relativeIndex = index - indexOfFirstGroupItem; enabled = (relativeIndex & 0x1) > 0; } } } widget->setAlternateBackground(enabled); } bool KItemListView::useAlternateBackgrounds() const { return m_itemSize.isEmpty() && m_visibleRoles.count() > 1; } QHash KItemListView::preferredColumnWidths(const KItemRangeList& itemRanges) const { QElapsedTimer timer; timer.start(); QHash widths; // Calculate the minimum width for each column that is required // to show the headline unclipped. const QFontMetricsF fontMetrics(m_headerWidget->font()); const int gripMargin = m_headerWidget->style()->pixelMetric(QStyle::PM_HeaderGripMargin); const int headerMargin = m_headerWidget->style()->pixelMetric(QStyle::PM_HeaderMargin); foreach (const QByteArray& visibleRole, visibleRoles()) { const QString headerText = m_model->roleDescription(visibleRole); const qreal headerWidth = fontMetrics.width(headerText) + gripMargin + headerMargin * 2; widths.insert(visibleRole, headerWidth); } // Calculate the preferred column withs for each item and ignore values // smaller than the width for showing the headline unclipped. const KItemListWidgetCreatorBase* creator = widgetCreator(); int calculatedItemCount = 0; bool maxTimeExceeded = false; foreach (const KItemRange& itemRange, itemRanges) { const int startIndex = itemRange.index; const int endIndex = startIndex + itemRange.count - 1; for (int i = startIndex; i <= endIndex; ++i) { foreach (const QByteArray& visibleRole, visibleRoles()) { qreal maxWidth = widths.value(visibleRole, 0); const qreal width = creator->preferredRoleColumnWidth(visibleRole, i, this); maxWidth = qMax(width, maxWidth); widths.insert(visibleRole, maxWidth); } if (calculatedItemCount > 100 && timer.elapsed() > 200) { // When having several thousands of items calculating the sizes can get // very expensive. We accept a possibly too small role-size in favour // of having no blocking user interface. maxTimeExceeded = true; break; } ++calculatedItemCount; } if (maxTimeExceeded) { break; } } return widths; } void KItemListView::applyColumnWidthsFromHeader() { // Apply the new size to the layouter const qreal requiredWidth = columnWidthsSum(); const QSizeF dynamicItemSize(qMax(size().width(), requiredWidth), m_itemSize.height()); m_layouter->setItemSize(dynamicItemSize); // Update the role sizes for all visible widgets QHashIterator it(m_visibleItems); while (it.hasNext()) { it.next(); updateWidgetColumnWidths(it.value()); } } void KItemListView::updateWidgetColumnWidths(KItemListWidget* widget) { foreach (const QByteArray& role, m_visibleRoles) { widget->setColumnWidth(role, m_headerWidget->columnWidth(role)); } } void KItemListView::updatePreferredColumnWidths(const KItemRangeList& itemRanges) { Q_ASSERT(m_itemSize.isEmpty()); const int itemCount = m_model->count(); int rangesItemCount = 0; foreach (const KItemRange& range, itemRanges) { rangesItemCount += range.count; } if (itemCount == rangesItemCount) { const QHash preferredWidths = preferredColumnWidths(itemRanges); foreach (const QByteArray& role, m_visibleRoles) { m_headerWidget->setPreferredColumnWidth(role, preferredWidths.value(role)); } } else { // Only a sub range of the roles need to be determined. // The chances are good that the widths of the sub ranges // already fit into the available widths and hence no // expensive update might be required. bool changed = false; const QHash updatedWidths = preferredColumnWidths(itemRanges); QHashIterator it(updatedWidths); while (it.hasNext()) { it.next(); const QByteArray& role = it.key(); const qreal updatedWidth = it.value(); const qreal currentWidth = m_headerWidget->preferredColumnWidth(role); if (updatedWidth > currentWidth) { m_headerWidget->setPreferredColumnWidth(role, updatedWidth); changed = true; } } if (!changed) { // All the updated sizes are smaller than the current sizes and no change // of the stretched roles-widths is required return; } } if (m_headerWidget->automaticColumnResizing()) { applyAutomaticColumnWidths(); } } void KItemListView::updatePreferredColumnWidths() { if (m_model) { updatePreferredColumnWidths(KItemRangeList() << KItemRange(0, m_model->count())); } } void KItemListView::applyAutomaticColumnWidths() { Q_ASSERT(m_itemSize.isEmpty()); Q_ASSERT(m_headerWidget->automaticColumnResizing()); if (m_visibleRoles.isEmpty()) { return; } // Calculate the maximum size of an item by considering the // visible role sizes and apply them to the layouter. If the // size does not use the available view-size the size of the // first role will get stretched. foreach (const QByteArray& role, m_visibleRoles) { const qreal preferredWidth = m_headerWidget->preferredColumnWidth(role); m_headerWidget->setColumnWidth(role, preferredWidth); } const QByteArray firstRole = m_visibleRoles.first(); qreal firstColumnWidth = m_headerWidget->columnWidth(firstRole); QSizeF dynamicItemSize = m_itemSize; qreal requiredWidth = columnWidthsSum(); const qreal availableWidth = size().width(); if (requiredWidth < availableWidth) { // Stretch the first column to use the whole remaining width firstColumnWidth += availableWidth - requiredWidth; m_headerWidget->setColumnWidth(firstRole, firstColumnWidth); } else if (requiredWidth > availableWidth && m_visibleRoles.count() > 1) { // Shrink the first column to be able to show as much other // columns as possible qreal shrinkedFirstColumnWidth = firstColumnWidth - requiredWidth + availableWidth; // TODO: A proper calculation of the minimum width depends on the implementation // of KItemListWidget. Probably a kind of minimum size-hint should be introduced // later. const qreal minWidth = qMin(firstColumnWidth, qreal(m_styleOption.iconSize * 2 + 200)); if (shrinkedFirstColumnWidth < minWidth) { shrinkedFirstColumnWidth = minWidth; } m_headerWidget->setColumnWidth(firstRole, shrinkedFirstColumnWidth); requiredWidth -= firstColumnWidth - shrinkedFirstColumnWidth; } dynamicItemSize.rwidth() = qMax(requiredWidth, availableWidth); m_layouter->setItemSize(dynamicItemSize); // Update the role sizes for all visible widgets QHashIterator it(m_visibleItems); while (it.hasNext()) { it.next(); updateWidgetColumnWidths(it.value()); } } qreal KItemListView::columnWidthsSum() const { qreal widthsSum = 0; foreach (const QByteArray& role, m_visibleRoles) { widthsSum += m_headerWidget->columnWidth(role); } return widthsSum; } QRectF KItemListView::headerBoundaries() const { return m_headerWidget->isVisible() ? m_headerWidget->geometry() : QRectF(); } bool KItemListView::changesItemGridLayout(const QSizeF& newGridSize, const QSizeF& newItemSize, const QSizeF& newItemMargin) const { if (newItemSize.isEmpty() || newGridSize.isEmpty()) { return false; } if (m_layouter->scrollOrientation() == Qt::Vertical) { const qreal itemWidth = m_layouter->itemSize().width(); if (itemWidth > 0) { const int newColumnCount = itemsPerSize(newGridSize.width(), newItemSize.width(), newItemMargin.width()); if (m_model->count() > newColumnCount) { const int oldColumnCount = itemsPerSize(m_layouter->size().width(), itemWidth, m_layouter->itemMargin().width()); return oldColumnCount != newColumnCount; } } } else { const qreal itemHeight = m_layouter->itemSize().height(); if (itemHeight > 0) { const int newRowCount = itemsPerSize(newGridSize.height(), newItemSize.height(), newItemMargin.height()); if (m_model->count() > newRowCount) { const int oldRowCount = itemsPerSize(m_layouter->size().height(), itemHeight, m_layouter->itemMargin().height()); return oldRowCount != newRowCount; } } } return false; } bool KItemListView::animateChangedItemCount(int changedItemCount) const { if (m_itemSize.isEmpty()) { // We have only columns or only rows, but no grid: An animation is usually // welcome when inserting or removing items. return !supportsItemExpanding(); } if (m_layouter->size().isEmpty() || m_layouter->itemSize().isEmpty()) { return false; } const int maximum = (scrollOrientation() == Qt::Vertical) ? m_layouter->size().width() / m_layouter->itemSize().width() : m_layouter->size().height() / m_layouter->itemSize().height(); // Only animate if up to 2/3 of a row or column are inserted or removed return changedItemCount <= maximum * 2 / 3; } bool KItemListView::scrollBarRequired(const QSizeF& size) const { const QSizeF oldSize = m_layouter->size(); m_layouter->setSize(size); const qreal maxOffset = m_layouter->maximumScrollOffset(); m_layouter->setSize(oldSize); return m_layouter->scrollOrientation() == Qt::Vertical ? maxOffset > size.height() : maxOffset > size.width(); } int KItemListView::showDropIndicator(const QPointF& pos) { QHashIterator it(m_visibleItems); while (it.hasNext()) { it.next(); const KItemListWidget* widget = it.value(); const QPointF mappedPos = widget->mapFromItem(this, pos); const QRectF rect = itemRect(widget->index()); if (mappedPos.y() >= 0 && mappedPos.y() <= rect.height()) { if (m_model->supportsDropping(widget->index())) { // Keep 30% of the rectangle as the gap instead of always having a fixed gap const int gap = qMax(qreal(4.0), qreal(0.3) * rect.height()); if (mappedPos.y() >= gap && mappedPos.y() <= rect.height() - gap) { return -1; } } const bool isAboveItem = (mappedPos.y () < rect.height() / 2); const qreal y = isAboveItem ? rect.top() : rect.bottom(); const QRectF draggingInsertIndicator(rect.left(), y, rect.width(), 1); if (m_dropIndicator != draggingInsertIndicator) { m_dropIndicator = draggingInsertIndicator; update(); } int index = widget->index(); if (!isAboveItem) { ++index; } return index; } } const QRectF firstItemRect = itemRect(firstVisibleIndex()); return (pos.y() <= firstItemRect.top()) ? 0 : -1; } void KItemListView::hideDropIndicator() { if (!m_dropIndicator.isNull()) { m_dropIndicator = QRectF(); update(); } } void KItemListView::updateGroupHeaderHeight() { qreal groupHeaderHeight = m_styleOption.fontMetrics.height(); qreal groupHeaderMargin = 0; if (scrollOrientation() == Qt::Horizontal) { // The vertical margin above and below the header should be // equal to the horizontal margin, not the vertical margin // from m_styleOption. groupHeaderHeight += 2 * m_styleOption.horizontalMargin; groupHeaderMargin = m_styleOption.horizontalMargin; } else if (m_itemSize.isEmpty()){ groupHeaderHeight += 4 * m_styleOption.padding; groupHeaderMargin = m_styleOption.iconSize / 2; } else { groupHeaderHeight += 2 * m_styleOption.padding + m_styleOption.verticalMargin; groupHeaderMargin = m_styleOption.iconSize / 4; } m_layouter->setGroupHeaderHeight(groupHeaderHeight); m_layouter->setGroupHeaderMargin(groupHeaderMargin); updateVisibleGroupHeaders(); } void KItemListView::updateSiblingsInformation(int firstIndex, int lastIndex) { if (!supportsItemExpanding() || !m_model) { return; } if (firstIndex < 0 || lastIndex < 0) { firstIndex = m_layouter->firstVisibleIndex(); lastIndex = m_layouter->lastVisibleIndex(); } else { const bool isRangeVisible = (firstIndex <= m_layouter->lastVisibleIndex() && lastIndex >= m_layouter->firstVisibleIndex()); if (!isRangeVisible) { return; } } int previousParents = 0; QBitArray previousSiblings; // The rootIndex describes the first index where the siblings get // calculated from. For the calculation the upper most parent item // is required. For performance reasons it is checked first whether // the visible items before or after the current range already // contain a siblings information which can be used as base. int rootIndex = firstIndex; KItemListWidget* widget = m_visibleItems.value(firstIndex - 1); if (!widget) { // There is no visible widget before the range, check whether there // is one after the range: widget = m_visibleItems.value(lastIndex + 1); if (widget) { // The sibling information of the widget may only be used if // all items of the range have the same number of parents. const int parents = m_model->expandedParentsCount(lastIndex + 1); for (int i = lastIndex; i >= firstIndex; --i) { if (m_model->expandedParentsCount(i) != parents) { widget = nullptr; break; } } } } if (widget) { // Performance optimization: Use the sibling information of the visible // widget beside the given range. previousSiblings = widget->siblingsInformation(); if (previousSiblings.isEmpty()) { return; } previousParents = previousSiblings.count() - 1; previousSiblings.truncate(previousParents); } else { // Potentially slow path: Go back to the upper most parent of firstIndex // to be able to calculate the initial value for the siblings. while (rootIndex > 0 && m_model->expandedParentsCount(rootIndex) > 0) { --rootIndex; } } Q_ASSERT(previousParents >= 0); for (int i = rootIndex; i <= lastIndex; ++i) { // Update the parent-siblings in case if the current item represents // a child or an upper parent. const int currentParents = m_model->expandedParentsCount(i); Q_ASSERT(currentParents >= 0); if (previousParents < currentParents) { previousParents = currentParents; previousSiblings.resize(currentParents); previousSiblings.setBit(currentParents - 1, hasSiblingSuccessor(i - 1)); } else if (previousParents > currentParents) { previousParents = currentParents; previousSiblings.truncate(currentParents); } if (i >= firstIndex) { // The index represents a visible item. Apply the parent-siblings // and update the sibling of the current item. KItemListWidget* widget = m_visibleItems.value(i); if (!widget) { continue; } QBitArray siblings = previousSiblings; siblings.resize(siblings.count() + 1); siblings.setBit(siblings.count() - 1, hasSiblingSuccessor(i)); widget->setSiblingsInformation(siblings); } } } bool KItemListView::hasSiblingSuccessor(int index) const { bool hasSuccessor = false; const int parentsCount = m_model->expandedParentsCount(index); int successorIndex = index + 1; // Search the next sibling const int itemCount = m_model->count(); while (successorIndex < itemCount) { const int currentParentsCount = m_model->expandedParentsCount(successorIndex); if (currentParentsCount == parentsCount) { hasSuccessor = true; break; } else if (currentParentsCount < parentsCount) { break; } ++successorIndex; } if (m_grouped && hasSuccessor) { // If the sibling is part of another group, don't mark it as // successor as the group header is between the sibling connections. for (int i = index + 1; i <= successorIndex; ++i) { if (m_layouter->isFirstGroupItem(i)) { hasSuccessor = false; break; } } } return hasSuccessor; } void KItemListView::disconnectRoleEditingSignals(int index) { KStandardItemListWidget* widget = qobject_cast(m_visibleItems.value(index)); if (!widget) { return; } disconnect(widget, &KItemListWidget::roleEditingCanceled, this, nullptr); disconnect(widget, &KItemListWidget::roleEditingFinished, this, nullptr); disconnect(this, &KItemListView::scrollOffsetChanged, widget, nullptr); } int KItemListView::calculateAutoScrollingIncrement(int pos, int range, int oldInc) { int inc = 0; const int minSpeed = 4; const int maxSpeed = 128; const int speedLimiter = 96; const int autoScrollBorder = 64; // Limit the increment that is allowed to be added in comparison to 'oldInc'. // This assures that the autoscrolling speed grows gradually. const int incLimiter = 1; if (pos < autoScrollBorder) { inc = -minSpeed + qAbs(pos - autoScrollBorder) * (pos - autoScrollBorder) / speedLimiter; inc = qMax(inc, -maxSpeed); inc = qMax(inc, oldInc - incLimiter); } else if (pos > range - autoScrollBorder) { inc = minSpeed + qAbs(pos - range + autoScrollBorder) * (pos - range + autoScrollBorder) / speedLimiter; inc = qMin(inc, maxSpeed); inc = qMin(inc, oldInc + incLimiter); } return inc; } int KItemListView::itemsPerSize(qreal size, qreal itemSize, qreal itemMargin) { const qreal availableSize = size - itemMargin; const int count = availableSize / (itemSize + itemMargin); return count; } KItemListCreatorBase::~KItemListCreatorBase() { qDeleteAll(m_recycleableWidgets); qDeleteAll(m_createdWidgets); } void KItemListCreatorBase::addCreatedWidget(QGraphicsWidget* widget) { m_createdWidgets.insert(widget); } void KItemListCreatorBase::pushRecycleableWidget(QGraphicsWidget* widget) { Q_ASSERT(m_createdWidgets.contains(widget)); m_createdWidgets.remove(widget); if (m_recycleableWidgets.count() < 100) { m_recycleableWidgets.append(widget); widget->setVisible(false); } else { delete widget; } } QGraphicsWidget* KItemListCreatorBase::popRecycleableWidget() { if (m_recycleableWidgets.isEmpty()) { return nullptr; } QGraphicsWidget* widget = m_recycleableWidgets.takeLast(); m_createdWidgets.insert(widget); return widget; } KItemListWidgetCreatorBase::~KItemListWidgetCreatorBase() { } void KItemListWidgetCreatorBase::recycle(KItemListWidget* widget) { widget->setParentItem(nullptr); widget->setOpacity(1.0); pushRecycleableWidget(widget); } KItemListGroupHeaderCreatorBase::~KItemListGroupHeaderCreatorBase() { } void KItemListGroupHeaderCreatorBase::recycle(KItemListGroupHeader* header) { header->setOpacity(1.0); pushRecycleableWidget(header); } diff --git a/src/kitemviews/kitemlistviewaccessible.cpp b/src/kitemviews/kitemlistviewaccessible.cpp index 8fe8a6ef8..0188408a7 100644 --- a/src/kitemviews/kitemlistviewaccessible.cpp +++ b/src/kitemviews/kitemlistviewaccessible.cpp @@ -1,479 +1,479 @@ /*************************************************************************** * Copyright (C) 2012 by Amandeep Singh * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #ifndef QT_NO_ACCESSIBILITY #include "kitemlistviewaccessible.h" #include "kitemlistcontainer.h" #include "kitemlistcontroller.h" #include "kitemlistselectionmanager.h" #include "kitemlistview.h" #include "private/kitemlistviewlayouter.h" #include #include KItemListView* KItemListViewAccessible::view() const { return qobject_cast(object()); } KItemListViewAccessible::KItemListViewAccessible(KItemListView* view_) : QAccessibleObject(view_) { Q_ASSERT(view()); m_cells.resize(childCount()); } KItemListViewAccessible::~KItemListViewAccessible() { foreach (AccessibleIdWrapper idWrapper, m_cells) { if (idWrapper.isValid) { QAccessible::deleteAccessibleInterface(idWrapper.id); } } } void* KItemListViewAccessible::interface_cast(QAccessible::InterfaceType type) { if (type == QAccessible::TableInterface) { return static_cast(this); } return nullptr; } void KItemListViewAccessible::modelReset() { } QAccessibleInterface* KItemListViewAccessible::cell(int index) const { if (index < 0 || index >= view()->model()->count()) { return nullptr; } if (m_cells.size() <= index) { m_cells.resize(childCount()); } Q_ASSERT(index < m_cells.size()); AccessibleIdWrapper idWrapper = m_cells.at(index); if (!idWrapper.isValid) { idWrapper.id = QAccessible::registerAccessibleInterface(new KItemListAccessibleCell(view(), index)); idWrapper.isValid = true; m_cells.insert(index, idWrapper); } return QAccessible::accessibleInterface(idWrapper.id); } QAccessibleInterface* KItemListViewAccessible::cellAt(int row, int column) const { return cell(columnCount() * row + column); } QAccessibleInterface* KItemListViewAccessible::caption() const { return nullptr; } QString KItemListViewAccessible::columnDescription(int) const { return QString(); } int KItemListViewAccessible::columnCount() const { return view()->m_layouter->columnCount(); } int KItemListViewAccessible::rowCount() const { if (columnCount() <= 0) { return 0; } int itemCount = view()->model()->count(); int rowCount = itemCount / columnCount(); if (rowCount <= 0) { return 0; } if (itemCount % columnCount()) { ++rowCount; } return rowCount; } int KItemListViewAccessible::selectedCellCount() const { return view()->controller()->selectionManager()->selectedItems().count(); } int KItemListViewAccessible::selectedColumnCount() const { return 0; } int KItemListViewAccessible::selectedRowCount() const { return 0; } QString KItemListViewAccessible::rowDescription(int) const { return QString(); } QList KItemListViewAccessible::selectedCells() const { QList cells; const auto items = view()->controller()->selectionManager()->selectedItems(); cells.reserve(items.count()); for (int index : items) { cells.append(cell(index)); } return cells; } QList KItemListViewAccessible::selectedColumns() const { return QList(); } QList KItemListViewAccessible::selectedRows() const { return QList(); } QAccessibleInterface* KItemListViewAccessible::summary() const { return nullptr; } bool KItemListViewAccessible::isColumnSelected(int) const { return false; } bool KItemListViewAccessible::isRowSelected(int) const { return false; } bool KItemListViewAccessible::selectRow(int) { return true; } bool KItemListViewAccessible::selectColumn(int) { return true; } bool KItemListViewAccessible::unselectRow(int) { return true; } bool KItemListViewAccessible::unselectColumn(int) { return true; } void KItemListViewAccessible::modelChange(QAccessibleTableModelChangeEvent* /*event*/) {} QAccessible::Role KItemListViewAccessible::role() const { return QAccessible::Table; } QAccessible::State KItemListViewAccessible::state() const { QAccessible::State s; return s; } QAccessibleInterface* KItemListViewAccessible::childAt(int x, int y) const { const QPointF point = QPointF(x, y); int itemIndex = view()->itemAt(view()->mapFromScene(point)); return child(itemIndex); } QAccessibleInterface* KItemListViewAccessible::parent() const { // FIXME: return KItemListContainerAccessible here return nullptr; } int KItemListViewAccessible::childCount() const { return view()->model()->count(); } int KItemListViewAccessible::indexOfChild(const QAccessibleInterface* interface) const { const KItemListAccessibleCell* widget = static_cast(interface); return widget->index(); } QString KItemListViewAccessible::text(QAccessible::Text) const { return QString(); } QRect KItemListViewAccessible::rect() const { if (!view()->isVisible()) { return QRect(); } const QGraphicsScene* scene = view()->scene(); if (scene) { const QPoint origin = scene->views().at(0)->mapToGlobal(QPoint(0, 0)); const QRect viewRect = view()->geometry().toRect(); return viewRect.translated(origin); } else { return QRect(); } } QAccessibleInterface* KItemListViewAccessible::child(int index) const { if (index >= 0 && index < childCount()) { return cell(index); } return nullptr; } KItemListViewAccessible::AccessibleIdWrapper::AccessibleIdWrapper() : isValid(false), id(0) { } // Table Cell KItemListAccessibleCell::KItemListAccessibleCell(KItemListView* view, int index) : m_view(view), m_index(index) { Q_ASSERT(index >= 0 && index < view->model()->count()); } void* KItemListAccessibleCell::interface_cast(QAccessible::InterfaceType type) { if (type == QAccessible::TableCellInterface) { return static_cast(this); } return nullptr; } int KItemListAccessibleCell::columnExtent() const { return 1; } int KItemListAccessibleCell::rowExtent() const { return 1; } QList KItemListAccessibleCell::rowHeaderCells() const { return QList(); } QList KItemListAccessibleCell::columnHeaderCells() const { return QList(); } int KItemListAccessibleCell::columnIndex() const { return m_view->m_layouter->itemColumn(m_index); } int KItemListAccessibleCell::rowIndex() const { return m_view->m_layouter->itemRow(m_index); } bool KItemListAccessibleCell::isSelected() const { return m_view->controller()->selectionManager()->isSelected(m_index); } QAccessibleInterface* KItemListAccessibleCell::table() const { return QAccessible::queryAccessibleInterface(m_view); } QAccessible::Role KItemListAccessibleCell::role() const { return QAccessible::Cell; } QAccessible::State KItemListAccessibleCell::state() const { QAccessible::State state; state.selectable = true; if (isSelected()) { state.selected = true; } state.focusable = true; if (m_view->controller()->selectionManager()->currentItem() == m_index) { state.focused = true; } if (m_view->controller()->selectionBehavior() == KItemListController::MultiSelection) { state.multiSelectable = true; } if (m_view->model()->isExpandable(m_index)) { if (m_view->model()->isExpanded(m_index)) { state.expanded = true; } else { state.collapsed = true; } } return state; } bool KItemListAccessibleCell::isExpandable() const { return m_view->model()->isExpandable(m_index); } QRect KItemListAccessibleCell::rect() const { QRect rect = m_view->itemRect(m_index).toRect(); if (rect.isNull()) { return QRect(); } rect.translate(m_view->mapToScene(QPointF(0.0, 0.0)).toPoint()); rect.translate(m_view->scene()->views()[0]->mapToGlobal(QPoint(0, 0))); return rect; } QString KItemListAccessibleCell::text(QAccessible::Text t) const { switch (t) { case QAccessible::Name: { const QHash data = m_view->model()->data(m_index); return data["text"].toString(); } default: break; } return QString(); } void KItemListAccessibleCell::setText(QAccessible::Text, const QString&) { } QAccessibleInterface* KItemListAccessibleCell::child(int) const { return nullptr; } bool KItemListAccessibleCell::isValid() const { return m_view && (m_index >= 0) && (m_index < m_view->model()->count()); } QAccessibleInterface* KItemListAccessibleCell::childAt(int, int) const { return nullptr; } int KItemListAccessibleCell::childCount() const { return 0; } int KItemListAccessibleCell::indexOfChild(const QAccessibleInterface* child) const { - Q_UNUSED(child); + Q_UNUSED(child) return -1; } QAccessibleInterface* KItemListAccessibleCell::parent() const { return QAccessible::queryAccessibleInterface(m_view); } int KItemListAccessibleCell::index() const { return m_index; } QObject* KItemListAccessibleCell::object() const { return nullptr; } // Container Interface KItemListContainerAccessible::KItemListContainerAccessible(KItemListContainer* container) : QAccessibleWidget(container) { } KItemListContainerAccessible::~KItemListContainerAccessible() { } int KItemListContainerAccessible::childCount() const { return 1; } int KItemListContainerAccessible::indexOfChild(const QAccessibleInterface* child) const { if (child->object() == container()->controller()->view()) { return 0; } return -1; } QAccessibleInterface* KItemListContainerAccessible::child(int index) const { if (index == 0) { return QAccessible::queryAccessibleInterface(container()->controller()->view()); } return nullptr; } const KItemListContainer* KItemListContainerAccessible::container() const { return qobject_cast(object()); } #endif // QT_NO_ACCESSIBILITY diff --git a/src/kitemviews/kitemlistwidget.cpp b/src/kitemviews/kitemlistwidget.cpp index e8482216d..c33335ae6 100644 --- a/src/kitemviews/kitemlistwidget.cpp +++ b/src/kitemviews/kitemlistwidget.cpp @@ -1,527 +1,527 @@ /*************************************************************************** * Copyright (C) 2011 by Peter Penz * * * * Based on the Itemviews NG project from Trolltech Labs: * * http://qt.gitorious.org/qt-labs/itemviews-ng * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kitemlistwidget.h" #include "kitemlistview.h" #include "private/kitemlistselectiontoggle.h" #include #include #include #include KItemListWidgetInformant::KItemListWidgetInformant() { } KItemListWidgetInformant::~KItemListWidgetInformant() { } KItemListWidget::KItemListWidget(KItemListWidgetInformant* informant, QGraphicsItem* parent) : QGraphicsWidget(parent, nullptr), m_informant(informant), m_index(-1), m_selected(false), m_current(false), m_hovered(false), m_alternateBackground(false), m_enabledSelectionToggle(false), m_data(), m_visibleRoles(), m_columnWidths(), m_styleOption(), m_siblingsInfo(), m_hoverOpacity(0), m_hoverCache(nullptr), m_hoverAnimation(nullptr), m_selectionToggle(nullptr), m_editedRole() { } KItemListWidget::~KItemListWidget() { clearHoverCache(); } void KItemListWidget::setIndex(int index) { if (m_index != index) { delete m_selectionToggle; m_selectionToggle = nullptr; if (m_hoverAnimation) { m_hoverAnimation->stop(); m_hoverOpacity = 0; } clearHoverCache(); m_index = index; } } int KItemListWidget::index() const { return m_index; } void KItemListWidget::setData(const QHash& data, const QSet& roles) { clearHoverCache(); if (roles.isEmpty()) { m_data = data; dataChanged(m_data); } else { foreach (const QByteArray& role, roles) { m_data[role] = data[role]; } dataChanged(m_data, roles); } update(); } QHash KItemListWidget::data() const { return m_data; } void KItemListWidget::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) { - Q_UNUSED(option); + Q_UNUSED(option) if (m_alternateBackground) { const QColor backgroundColor = m_styleOption.palette.color(QPalette::AlternateBase); const QRectF backgroundRect(0, 0, size().width(), size().height()); painter->fillRect(backgroundRect, backgroundColor); } if (m_selected && m_editedRole.isEmpty()) { const QStyle::State activeState(isActiveWindow() ? QStyle::State_Active : 0); drawItemStyleOption(painter, widget, activeState | QStyle::State_Enabled | QStyle::State_Selected | QStyle::State_Item); } if (m_current && m_editedRole.isEmpty()) { QStyleOptionFocusRect focusRectOption; initStyleOption(&focusRectOption); focusRectOption.rect = textFocusRect().toRect(); focusRectOption.state = QStyle::State_Enabled | QStyle::State_Item | QStyle::State_KeyboardFocusChange; if (m_selected) { focusRectOption.state |= QStyle::State_Selected; } style()->drawPrimitive(QStyle::PE_FrameFocusRect, &focusRectOption, painter, widget); } if (m_hoverOpacity > 0.0) { if (!m_hoverCache) { // Initialize the m_hoverCache pixmap to improve the drawing performance // when fading the hover background m_hoverCache = new QPixmap(size().toSize()); m_hoverCache->fill(Qt::transparent); QPainter pixmapPainter(m_hoverCache); const QStyle::State activeState(isActiveWindow() ? QStyle::State_Active : 0); drawItemStyleOption(&pixmapPainter, widget, activeState | QStyle::State_Enabled | QStyle::State_MouseOver | QStyle::State_Item); } const qreal opacity = painter->opacity(); painter->setOpacity(m_hoverOpacity * opacity); painter->drawPixmap(0, 0, *m_hoverCache); painter->setOpacity(opacity); } } void KItemListWidget::setVisibleRoles(const QList& roles) { const QList previousRoles = m_visibleRoles; m_visibleRoles = roles; visibleRolesChanged(roles, previousRoles); update(); } QList KItemListWidget::visibleRoles() const { return m_visibleRoles; } void KItemListWidget::setColumnWidth(const QByteArray& role, qreal width) { const qreal previousWidth = m_columnWidths.value(role); if (previousWidth != width) { m_columnWidths.insert(role, width); columnWidthChanged(role, width, previousWidth); update(); } } qreal KItemListWidget::columnWidth(const QByteArray& role) const { return m_columnWidths.value(role); } void KItemListWidget::setStyleOption(const KItemListStyleOption& option) { if (m_styleOption == option) { return; } const KItemListStyleOption previous = m_styleOption; clearHoverCache(); m_styleOption = option; styleOptionChanged(option, previous); update(); } const KItemListStyleOption& KItemListWidget::styleOption() const { return m_styleOption; } void KItemListWidget::setSelected(bool selected) { if (m_selected != selected) { m_selected = selected; if (m_selectionToggle) { m_selectionToggle->setChecked(selected); } selectedChanged(selected); update(); } } bool KItemListWidget::isSelected() const { return m_selected; } void KItemListWidget::setCurrent(bool current) { if (m_current != current) { m_current = current; currentChanged(current); update(); } } bool KItemListWidget::isCurrent() const { return m_current; } void KItemListWidget::setHovered(bool hovered) { if (hovered == m_hovered) { return; } m_hovered = hovered; if (!m_hoverAnimation) { m_hoverAnimation = new QPropertyAnimation(this, "hoverOpacity", this); const int duration = style()->styleHint(QStyle::SH_Widget_Animate) ? 200 : 1; m_hoverAnimation->setDuration(duration); connect(m_hoverAnimation, &QPropertyAnimation::finished, this, &KItemListWidget::slotHoverAnimationFinished); } m_hoverAnimation->stop(); if (hovered) { const qreal startValue = qMax(hoverOpacity(), qreal(0.1)); m_hoverAnimation->setStartValue(startValue); m_hoverAnimation->setEndValue(1.0); if (m_enabledSelectionToggle && !(QApplication::mouseButtons() & Qt::LeftButton)) { initializeSelectionToggle(); } } else { m_hoverAnimation->setStartValue(hoverOpacity()); m_hoverAnimation->setEndValue(0.0); } m_hoverAnimation->start(); hoveredChanged(hovered); update(); } bool KItemListWidget::isHovered() const { return m_hovered; } void KItemListWidget::setHoverPosition(const QPointF& pos) { if (m_selectionToggle) { m_selectionToggle->setHovered(selectionToggleRect().contains(pos)); } } void KItemListWidget::setAlternateBackground(bool enable) { if (m_alternateBackground != enable) { m_alternateBackground = enable; alternateBackgroundChanged(enable); update(); } } bool KItemListWidget::alternateBackground() const { return m_alternateBackground; } void KItemListWidget::setEnabledSelectionToggle(bool enable) { if (m_enabledSelectionToggle != enable) { m_enabledSelectionToggle = enable; update(); } } bool KItemListWidget::enabledSelectionToggle() const { return m_enabledSelectionToggle; } void KItemListWidget::setSiblingsInformation(const QBitArray& siblings) { const QBitArray previous = m_siblingsInfo; m_siblingsInfo = siblings; siblingsInformationChanged(m_siblingsInfo, previous); update(); } QBitArray KItemListWidget::siblingsInformation() const { return m_siblingsInfo; } void KItemListWidget::setEditedRole(const QByteArray& role) { if (m_editedRole != role) { const QByteArray previous = m_editedRole; m_editedRole = role; editedRoleChanged(role, previous); } } QByteArray KItemListWidget::editedRole() const { return m_editedRole; } bool KItemListWidget::contains(const QPointF& point) const { if (!QGraphicsWidget::contains(point)) { return false; } return iconRect().contains(point) || textRect().contains(point) || expansionToggleRect().contains(point) || selectionToggleRect().contains(point); } QRectF KItemListWidget::textFocusRect() const { return textRect(); } QRectF KItemListWidget::selectionToggleRect() const { return QRectF(); } QRectF KItemListWidget::expansionToggleRect() const { return QRectF(); } QPixmap KItemListWidget::createDragPixmap(const QStyleOptionGraphicsItem* option, QWidget* widget) { QPixmap pixmap(size().toSize() * widget->devicePixelRatio()); pixmap.setDevicePixelRatio(widget->devicePixelRatio()); pixmap.fill(Qt::transparent); QPainter painter(&pixmap); const bool oldAlternateBackground = m_alternateBackground; const bool wasSelected = m_selected; const bool wasHovered = m_hovered; setAlternateBackground(false); setSelected(false); setHovered(false); paint(&painter, option, widget); setAlternateBackground(oldAlternateBackground); setSelected(wasSelected); setHovered(wasHovered); return pixmap; } void KItemListWidget::dataChanged(const QHash& current, const QSet& roles) { - Q_UNUSED(current); - Q_UNUSED(roles); + Q_UNUSED(current) + Q_UNUSED(roles) } void KItemListWidget::visibleRolesChanged(const QList& current, const QList& previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) } void KItemListWidget::columnWidthChanged(const QByteArray& role, qreal current, qreal previous) { - Q_UNUSED(role); - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(role) + Q_UNUSED(current) + Q_UNUSED(previous) } void KItemListWidget::styleOptionChanged(const KItemListStyleOption& current, const KItemListStyleOption& previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) } void KItemListWidget::currentChanged(bool current) { - Q_UNUSED(current); + Q_UNUSED(current) } void KItemListWidget::selectedChanged(bool selected) { - Q_UNUSED(selected); + Q_UNUSED(selected) } void KItemListWidget::hoveredChanged(bool hovered) { - Q_UNUSED(hovered); + Q_UNUSED(hovered) } void KItemListWidget::alternateBackgroundChanged(bool enabled) { - Q_UNUSED(enabled); + Q_UNUSED(enabled) } void KItemListWidget::siblingsInformationChanged(const QBitArray& current, const QBitArray& previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) } void KItemListWidget::editedRoleChanged(const QByteArray& current, const QByteArray& previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) } void KItemListWidget::resizeEvent(QGraphicsSceneResizeEvent* event) { QGraphicsWidget::resizeEvent(event); clearHoverCache(); if (m_selectionToggle) { const QRectF& toggleRect = selectionToggleRect(); m_selectionToggle->setPos(toggleRect.topLeft()); m_selectionToggle->resize(toggleRect.size()); } } qreal KItemListWidget::hoverOpacity() const { return m_hoverOpacity; } void KItemListWidget::slotHoverAnimationFinished() { if (!m_hovered && m_selectionToggle) { m_selectionToggle->deleteLater(); m_selectionToggle = nullptr; } } void KItemListWidget::initializeSelectionToggle() { Q_ASSERT(m_enabledSelectionToggle); if (!m_selectionToggle) { m_selectionToggle = new KItemListSelectionToggle(this); } const QRectF toggleRect = selectionToggleRect(); m_selectionToggle->setPos(toggleRect.topLeft()); m_selectionToggle->resize(toggleRect.size()); m_selectionToggle->setChecked(isSelected()); } void KItemListWidget::setHoverOpacity(qreal opacity) { m_hoverOpacity = opacity; if (m_selectionToggle) { m_selectionToggle->setOpacity(opacity); } if (m_hoverOpacity <= 0.0) { delete m_hoverCache; m_hoverCache = nullptr; } update(); } void KItemListWidget::clearHoverCache() { delete m_hoverCache; m_hoverCache = nullptr; } void KItemListWidget::drawItemStyleOption(QPainter* painter, QWidget* widget, QStyle::State styleState) { QStyleOptionViewItem viewItemOption; initStyleOption(&viewItemOption); viewItemOption.state = styleState; viewItemOption.viewItemPosition = QStyleOptionViewItem::OnlyOne; viewItemOption.showDecorationSelected = true; viewItemOption.rect = selectionRect().toRect(); style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &viewItemOption, painter, widget); } diff --git a/src/kitemviews/kitemmodelbase.cpp b/src/kitemviews/kitemmodelbase.cpp index 7f7877555..9cbcbf3c0 100644 --- a/src/kitemviews/kitemmodelbase.cpp +++ b/src/kitemviews/kitemmodelbase.cpp @@ -1,181 +1,181 @@ /*************************************************************************** * Copyright (C) 2011 by Peter Penz * * * * Based on the Itemviews NG project from Trolltech Labs: * * http://qt.gitorious.org/qt-labs/itemviews-ng * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kitemmodelbase.h" KItemModelBase::KItemModelBase(QObject* parent) : QObject(parent), m_groupedSorting(false), m_sortRole(), m_sortOrder(Qt::AscendingOrder) { } KItemModelBase::KItemModelBase(const QByteArray& sortRole, QObject* parent) : QObject(parent), m_groupedSorting(false), m_sortRole(sortRole), m_sortOrder(Qt::AscendingOrder) { } KItemModelBase::~KItemModelBase() { } bool KItemModelBase::setData(int index, const QHash &values) { - Q_UNUSED(index); - Q_UNUSED(values); + Q_UNUSED(index) + Q_UNUSED(values) return false; } void KItemModelBase::setGroupedSorting(bool grouped) { if (m_groupedSorting != grouped) { m_groupedSorting = grouped; onGroupedSortingChanged(grouped); emit groupedSortingChanged(grouped); } } bool KItemModelBase::groupedSorting() const { return m_groupedSorting; } void KItemModelBase::setSortRole(const QByteArray& role, bool resortItems) { if (role != m_sortRole) { const QByteArray previous = m_sortRole; m_sortRole = role; onSortRoleChanged(role, previous, resortItems); emit sortRoleChanged(role, previous); } } QByteArray KItemModelBase::sortRole() const { return m_sortRole; } void KItemModelBase::setSortOrder(Qt::SortOrder order) { if (order != m_sortOrder) { const Qt::SortOrder previous = m_sortOrder; m_sortOrder = order; onSortOrderChanged(order, previous); emit sortOrderChanged(order, previous); } } QString KItemModelBase::roleDescription(const QByteArray& role) const { return role; } QList > KItemModelBase::groups() const { return QList >(); } bool KItemModelBase::setExpanded(int index, bool expanded) { - Q_UNUSED(index); - Q_UNUSED(expanded); + Q_UNUSED(index) + Q_UNUSED(expanded) return false; } bool KItemModelBase::isExpanded(int index) const { - Q_UNUSED(index); + Q_UNUSED(index) return false; } bool KItemModelBase::isExpandable(int index) const { - Q_UNUSED(index); + Q_UNUSED(index) return false; } int KItemModelBase::expandedParentsCount(int index) const { - Q_UNUSED(index); + Q_UNUSED(index) return 0; } QMimeData* KItemModelBase::createMimeData(const KItemSet& indexes) const { - Q_UNUSED(indexes); + Q_UNUSED(indexes) return nullptr; } int KItemModelBase::indexForKeyboardSearch(const QString& text, int startFromIndex) const { - Q_UNUSED(text); - Q_UNUSED(startFromIndex); + Q_UNUSED(text) + Q_UNUSED(startFromIndex) return -1; } bool KItemModelBase::supportsDropping(int index) const { - Q_UNUSED(index); + Q_UNUSED(index) return false; } QString KItemModelBase::blacklistItemDropEventMimeType() const { return QStringLiteral("application/x-dolphin-blacklist-drop"); } void KItemModelBase::onGroupedSortingChanged(bool current) { - Q_UNUSED(current); + Q_UNUSED(current) } void KItemModelBase::onSortRoleChanged(const QByteArray& current, const QByteArray& previous, bool resortItems) { - Q_UNUSED(current); - Q_UNUSED(previous); - Q_UNUSED(resortItems); + Q_UNUSED(current) + Q_UNUSED(previous) + Q_UNUSED(resortItems) } void KItemModelBase::onSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) } QUrl KItemModelBase::url(int index) const { return data(index).value("url").toUrl(); } bool KItemModelBase::isDir(int index) const { return data(index).value("isDir").toBool(); } QUrl KItemModelBase::directory() const { return QUrl(); } diff --git a/src/kitemviews/kstandarditem.cpp b/src/kitemviews/kstandarditem.cpp index b3bbcaa41..b0673236f 100644 --- a/src/kitemviews/kstandarditem.cpp +++ b/src/kitemviews/kstandarditem.cpp @@ -1,142 +1,142 @@ /*************************************************************************** * Copyright (C) 2012 by Peter Penz * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kstandarditem.h" #include "kstandarditemmodel.h" KStandardItem::KStandardItem(KStandardItem* parent) : QObject(parent), m_model(nullptr), m_data() { } KStandardItem::KStandardItem(const QString& text, KStandardItem* parent) : QObject(parent), m_model(nullptr), m_data() { setText(text); } KStandardItem::KStandardItem(const QString& icon, const QString& text, KStandardItem* parent) : QObject(parent), m_model(nullptr), m_data() { setIcon(icon); setText(text); } KStandardItem::~KStandardItem() { } void KStandardItem::setText(const QString& text) { setDataValue("text", text); } QString KStandardItem::text() const { return m_data["text"].toString(); } void KStandardItem::setIcon(const QString& icon) { setDataValue("iconName", icon); } QString KStandardItem::icon() const { return m_data["iconName"].toString(); } void KStandardItem::setIconOverlays(const QStringList& overlays) { setDataValue("iconOverlays", overlays); } QStringList KStandardItem::iconOverlays() const { return m_data["iconOverlays"].toStringList(); } void KStandardItem::setGroup(const QString& group) { setDataValue("group", group); } QString KStandardItem::group() const { return m_data["group"].toString(); } void KStandardItem::setDataValue(const QByteArray& role, const QVariant& value) { const QVariant previous = m_data.value(role); if (previous == value) { return; } m_data.insert(role, value); onDataValueChanged(role, value, previous); if (m_model) { const int index = m_model->index(this); QSet changedRoles; changedRoles.insert(role); m_model->onItemChanged(index, changedRoles); emit m_model->itemsChanged(KItemRangeList() << KItemRange(index, 1), changedRoles); } } QVariant KStandardItem::dataValue(const QByteArray& role) const { return m_data[role]; } void KStandardItem::setData(const QHash& values) { const QHash previous = m_data; m_data = values; onDataChanged(values, previous); } QHash KStandardItem::data() const { return m_data; } void KStandardItem::onDataValueChanged(const QByteArray& role, const QVariant& current, const QVariant& previous) { - Q_UNUSED(role); - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(role) + Q_UNUSED(current) + Q_UNUSED(previous) } void KStandardItem::onDataChanged(const QHash& current, const QHash& previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) } diff --git a/src/kitemviews/kstandarditemlistgroupheader.cpp b/src/kitemviews/kstandarditemlistgroupheader.cpp index c7997faf1..015362e3f 100644 --- a/src/kitemviews/kstandarditemlistgroupheader.cpp +++ b/src/kitemviews/kstandarditemlistgroupheader.cpp @@ -1,124 +1,124 @@ /*************************************************************************** * Copyright (C) 2011 by Peter Penz * * * * Based on the Itemviews NG project from Trolltech Labs: * * http://qt.gitorious.org/qt-labs/itemviews-ng * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kstandarditemlistgroupheader.h" #include #include KStandardItemListGroupHeader::KStandardItemListGroupHeader(QGraphicsWidget* parent) : KItemListGroupHeader(parent), m_dirtyCache(true), m_text(), m_pixmap() { m_text.setTextFormat(Qt::PlainText); m_text.setPerformanceHint(QStaticText::AggressiveCaching); } KStandardItemListGroupHeader::~KStandardItemListGroupHeader() { } void KStandardItemListGroupHeader::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) { if (m_dirtyCache) { updateCache(); } KItemListGroupHeader::paint(painter, option, widget); } void KStandardItemListGroupHeader::paintRole(QPainter* painter, const QRectF& roleBounds, const QColor& color) { if (m_pixmap.isNull()) { painter->setPen(color); painter->drawStaticText(roleBounds.topLeft(), m_text); } else { painter->drawPixmap(roleBounds.topLeft(), m_pixmap); } } void KStandardItemListGroupHeader::paintSeparator(QPainter* painter, const QColor& color) { if (itemIndex() == 0) { // No top- or left-line should be drawn for the first group-header return; } painter->setPen(color); if (scrollOrientation() == Qt::Horizontal) { painter->drawLine(0, 0, 0, size().height() - 1); } else { painter->drawLine(0, 0, size().width() - 1, 0); } } void KStandardItemListGroupHeader::roleChanged(const QByteArray ¤t, const QByteArray &previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) m_dirtyCache = true; } void KStandardItemListGroupHeader::dataChanged(const QVariant& current, const QVariant& previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) m_dirtyCache = true; } void KStandardItemListGroupHeader::resizeEvent(QGraphicsSceneResizeEvent* event) { QGraphicsWidget::resizeEvent(event); m_dirtyCache = true; } void KStandardItemListGroupHeader::updateCache() { Q_ASSERT(m_dirtyCache); m_dirtyCache = false; const qreal maxWidth = size().width() - 4 * styleOption().padding; if (role() == "rating") { m_text.setText(QString()); const qreal height = styleOption().fontMetrics.ascent(); const QSizeF pixmapSize(qMin(height * 5, maxWidth), height); m_pixmap = QPixmap(pixmapSize.toSize()); m_pixmap.fill(Qt::transparent); QPainter painter(&m_pixmap); const QRect rect(0, 0, m_pixmap.width(), m_pixmap.height()); const int rating = data().toInt(); KRatingPainter::paintRating(&painter, rect, Qt::AlignJustify | Qt::AlignVCenter, rating); } else { m_pixmap = QPixmap(); QFontMetricsF fontMetrics(font()); const QString text = fontMetrics.elidedText(data().toString(), Qt::ElideRight, maxWidth); m_text.setText(text); } } diff --git a/src/kitemviews/kstandarditemlistview.cpp b/src/kitemviews/kstandarditemlistview.cpp index 929ee1da8..097f681c5 100644 --- a/src/kitemviews/kstandarditemlistview.cpp +++ b/src/kitemviews/kstandarditemlistview.cpp @@ -1,173 +1,173 @@ /*************************************************************************** * Copyright (C) 2012 by Peter Penz * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kstandarditemlistview.h" #include "kstandarditemlistwidget.h" #include KStandardItemListView::KStandardItemListView(QGraphicsWidget* parent) : KItemListView(parent), m_itemLayout(DetailsLayout) { setAcceptDrops(true); setScrollOrientation(Qt::Vertical); setVisibleRoles({"text"}); } KStandardItemListView::~KStandardItemListView() { } void KStandardItemListView::setItemLayout(ItemLayout layout) { if (m_itemLayout == layout) { return; } beginTransaction(); const ItemLayout previous = m_itemLayout; m_itemLayout = layout; setSupportsItemExpanding(itemLayoutSupportsItemExpanding(layout)); setScrollOrientation(layout == CompactLayout ? Qt::Horizontal : Qt::Vertical); onItemLayoutChanged(layout, previous); endTransaction(); } KStandardItemListView::ItemLayout KStandardItemListView::itemLayout() const { return m_itemLayout; } KItemListWidgetCreatorBase* KStandardItemListView::defaultWidgetCreator() const { return new KItemListWidgetCreator(); } KItemListGroupHeaderCreatorBase* KStandardItemListView::defaultGroupHeaderCreator() const { return new KItemListGroupHeaderCreator(); } void KStandardItemListView::initializeItemListWidget(KItemListWidget* item) { KStandardItemListWidget* standardItemListWidget = qobject_cast(item); Q_ASSERT(standardItemListWidget); switch (itemLayout()) { case IconsLayout: standardItemListWidget->setLayout(KStandardItemListWidget::IconsLayout); break; case CompactLayout: standardItemListWidget->setLayout(KStandardItemListWidget::CompactLayout); break; case DetailsLayout: standardItemListWidget->setLayout(KStandardItemListWidget::DetailsLayout); break; default: Q_ASSERT(false); break; } standardItemListWidget->setSupportsItemExpanding(supportsItemExpanding()); } bool KStandardItemListView::itemSizeHintUpdateRequired(const QSet& changedRoles) const { // The only thing that can modify the item's size hint is the amount of space // needed to display the text for the visible roles. // Even if the icons have a different size they are always aligned within // the area defined by KItemStyleOption.iconSize and hence result in no // change of the item-size. foreach (const QByteArray& role, visibleRoles()) { if (changedRoles.contains(role)) { return true; } } return false; } bool KStandardItemListView::itemLayoutSupportsItemExpanding(ItemLayout layout) const { return layout == DetailsLayout; } void KStandardItemListView::onItemLayoutChanged(ItemLayout current, ItemLayout previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) updateLayoutOfVisibleItems(); } void KStandardItemListView::onScrollOrientationChanged(Qt::Orientation current, Qt::Orientation previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) updateLayoutOfVisibleItems(); } void KStandardItemListView::onSupportsItemExpandingChanged(bool supportsExpanding) { - Q_UNUSED(supportsExpanding); + Q_UNUSED(supportsExpanding) updateLayoutOfVisibleItems(); } void KStandardItemListView::polishEvent() { switch (m_itemLayout) { case IconsLayout: applyDefaultStyleOption(style()->pixelMetric(QStyle::PM_LargeIconSize), 2, 4, 8); break; case CompactLayout: applyDefaultStyleOption(style()->pixelMetric(QStyle::PM_SmallIconSize), 2, 8, 0); break; case DetailsLayout: applyDefaultStyleOption(style()->pixelMetric(QStyle::PM_SmallIconSize), 2, 0, 0); break; default: Q_ASSERT(false); break; } QGraphicsWidget::polishEvent(); } void KStandardItemListView::applyDefaultStyleOption(int iconSize, int padding, int horizontalMargin, int verticalMargin) { KItemListStyleOption option = styleOption(); if (option.iconSize < 0) { option.iconSize = iconSize; } if (option.padding < 0) { option.padding = padding; } if (option.horizontalMargin < 0) { option.horizontalMargin = horizontalMargin; } if (option.verticalMargin < 0) { option.verticalMargin = verticalMargin; } setStyleOption(option); } void KStandardItemListView::updateLayoutOfVisibleItems() { if (model()) { foreach (KItemListWidget* widget, visibleItemListWidgets()) { initializeItemListWidget(widget); } } } diff --git a/src/kitemviews/kstandarditemlistwidget.cpp b/src/kitemviews/kstandarditemlistwidget.cpp index c963b7196..4bbbf17a8 100644 --- a/src/kitemviews/kstandarditemlistwidget.cpp +++ b/src/kitemviews/kstandarditemlistwidget.cpp @@ -1,1526 +1,1526 @@ /*************************************************************************** * Copyright (C) 2012 by Peter Penz * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kstandarditemlistwidget.h" #include "kfileitemlistview.h" #include "kfileitemmodel.h" #include "private/kfileitemclipboard.h" #include "private/kitemlistroleeditor.h" #include "private/kpixmapmodifier.h" #include #include #include #include #include #include #include #include #include #include // #define KSTANDARDITEMLISTWIDGET_DEBUG KStandardItemListWidgetInformant::KStandardItemListWidgetInformant() : KItemListWidgetInformant() { } KStandardItemListWidgetInformant::~KStandardItemListWidgetInformant() { } void KStandardItemListWidgetInformant::calculateItemSizeHints(QVector& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const { switch (static_cast(view)->itemLayout()) { case KStandardItemListView::IconsLayout: calculateIconsLayoutItemSizeHints(logicalHeightHints, logicalWidthHint, view); break; case KStandardItemListView::CompactLayout: calculateCompactLayoutItemSizeHints(logicalHeightHints, logicalWidthHint, view); break; case KStandardItemListView::DetailsLayout: calculateDetailsLayoutItemSizeHints(logicalHeightHints, logicalWidthHint, view); break; default: Q_ASSERT(false); break; } } qreal KStandardItemListWidgetInformant::preferredRoleColumnWidth(const QByteArray& role, int index, const KItemListView* view) const { const QHash values = view->model()->data(index); const KItemListStyleOption& option = view->styleOption(); const QString text = roleText(role, values); qreal width = KStandardItemListWidget::columnPadding(option); const QFontMetrics& normalFontMetrics = option.fontMetrics; const QFontMetrics linkFontMetrics(customizedFontForLinks(option.font)); if (role == "rating") { width += KStandardItemListWidget::preferredRatingSize(option).width(); } else { // If current item is a link, we use the customized link font metrics instead of the normal font metrics. const QFontMetrics& fontMetrics = itemIsLink(index, view) ? linkFontMetrics : normalFontMetrics; width += fontMetrics.width(text); if (role == "text") { if (view->supportsItemExpanding()) { // Increase the width by the expansion-toggle and the current expansion level const int expandedParentsCount = values.value("expandedParentsCount", 0).toInt(); const qreal height = option.padding * 2 + qMax(option.iconSize, fontMetrics.height()); width += (expandedParentsCount + 1) * height; } // Increase the width by the required space for the icon width += option.padding * 2 + option.iconSize; } } return width; } QString KStandardItemListWidgetInformant::itemText(int index, const KItemListView* view) const { return view->model()->data(index).value("text").toString(); } bool KStandardItemListWidgetInformant::itemIsLink(int index, const KItemListView* view) const { - Q_UNUSED(index); - Q_UNUSED(view); + Q_UNUSED(index) + Q_UNUSED(view) return false; } QString KStandardItemListWidgetInformant::roleText(const QByteArray& role, const QHash& values) const { if (role == "rating") { // Always use an empty text, as the rating is shown by the image m_rating. return QString(); } return values.value(role).toString(); } QFont KStandardItemListWidgetInformant::customizedFontForLinks(const QFont& baseFont) const { return baseFont; } void KStandardItemListWidgetInformant::calculateIconsLayoutItemSizeHints(QVector& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const { const KItemListStyleOption& option = view->styleOption(); const QFont& normalFont = option.font; const int additionalRolesCount = qMax(view->visibleRoles().count() - 1, 0); const qreal itemWidth = view->itemSize().width(); const qreal maxWidth = itemWidth - 2 * option.padding; const qreal additionalRolesSpacing = additionalRolesCount * option.fontMetrics.lineSpacing(); const qreal spacingAndIconHeight = option.iconSize + option.padding * 3; const QFont linkFont = customizedFontForLinks(normalFont); QTextOption textOption(Qt::AlignHCenter); textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); for (int index = 0; index < logicalHeightHints.count(); ++index) { if (logicalHeightHints.at(index) > 0.0) { continue; } // If the current item is a link, we use the customized link font instead of the normal font. const QFont& font = itemIsLink(index, view) ? linkFont : normalFont; const QString& text = KStringHandler::preProcessWrap(itemText(index, view)); // Calculate the number of lines required for wrapping the name qreal textHeight = 0; QTextLayout layout(text, font); layout.setTextOption(textOption); layout.beginLayout(); QTextLine line; int lineCount = 0; while ((line = layout.createLine()).isValid()) { line.setLineWidth(maxWidth); line.naturalTextWidth(); textHeight += line.height(); ++lineCount; if (lineCount == option.maxTextLines) { break; } } layout.endLayout(); // Add one line for each additional information textHeight += additionalRolesSpacing; logicalHeightHints[index] = textHeight + spacingAndIconHeight; } logicalWidthHint = itemWidth; } void KStandardItemListWidgetInformant::calculateCompactLayoutItemSizeHints(QVector& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const { const KItemListStyleOption& option = view->styleOption(); const QFontMetrics& normalFontMetrics = option.fontMetrics; const int additionalRolesCount = qMax(view->visibleRoles().count() - 1, 0); const QList& visibleRoles = view->visibleRoles(); const bool showOnlyTextRole = (visibleRoles.count() == 1) && (visibleRoles.first() == "text"); const qreal maxWidth = option.maxTextWidth; const qreal paddingAndIconWidth = option.padding * 4 + option.iconSize; const qreal height = option.padding * 2 + qMax(option.iconSize, (1 + additionalRolesCount) * normalFontMetrics.lineSpacing()); const QFontMetrics linkFontMetrics(customizedFontForLinks(option.font)); for (int index = 0; index < logicalHeightHints.count(); ++index) { if (logicalHeightHints.at(index) > 0.0) { continue; } // If the current item is a link, we use the customized link font metrics instead of the normal font metrics. const QFontMetrics& fontMetrics = itemIsLink(index, view) ? linkFontMetrics : normalFontMetrics; // For each row exactly one role is shown. Calculate the maximum required width that is necessary // to show all roles without horizontal clipping. qreal maximumRequiredWidth = 0.0; if (showOnlyTextRole) { maximumRequiredWidth = fontMetrics.width(itemText(index, view)); } else { const QHash& values = view->model()->data(index); foreach (const QByteArray& role, visibleRoles) { const QString& text = roleText(role, values); const qreal requiredWidth = fontMetrics.width(text); maximumRequiredWidth = qMax(maximumRequiredWidth, requiredWidth); } } qreal width = paddingAndIconWidth + maximumRequiredWidth; if (maxWidth > 0 && width > maxWidth) { width = maxWidth; } logicalHeightHints[index] = width; } logicalWidthHint = height; } void KStandardItemListWidgetInformant::calculateDetailsLayoutItemSizeHints(QVector& logicalHeightHints, qreal& logicalWidthHint, const KItemListView* view) const { const KItemListStyleOption& option = view->styleOption(); const qreal height = option.padding * 2 + qMax(option.iconSize, option.fontMetrics.height()); logicalHeightHints.fill(height); logicalWidthHint = -1.0; } KStandardItemListWidget::KStandardItemListWidget(KItemListWidgetInformant* informant, QGraphicsItem* parent) : KItemListWidget(informant, parent), m_isCut(false), m_isHidden(false), m_customizedFont(), m_customizedFontMetrics(m_customizedFont), m_isExpandable(false), m_supportsItemExpanding(false), m_dirtyLayout(true), m_dirtyContent(true), m_dirtyContentRoles(), m_layout(IconsLayout), m_pixmapPos(), m_pixmap(), m_scaledPixmapSize(), m_iconRect(), m_hoverPixmap(), m_textInfo(), m_textRect(), m_sortedVisibleRoles(), m_expansionArea(), m_customTextColor(), m_additionalInfoTextColor(), m_overlay(), m_rating(), m_roleEditor(nullptr), m_oldRoleEditor(nullptr) { } KStandardItemListWidget::~KStandardItemListWidget() { qDeleteAll(m_textInfo); m_textInfo.clear(); if (m_roleEditor) { m_roleEditor->deleteLater(); } if (m_oldRoleEditor) { m_oldRoleEditor->deleteLater(); } } void KStandardItemListWidget::setLayout(Layout layout) { if (m_layout != layout) { m_layout = layout; m_dirtyLayout = true; updateAdditionalInfoTextColor(); update(); } } KStandardItemListWidget::Layout KStandardItemListWidget::layout() const { return m_layout; } void KStandardItemListWidget::setSupportsItemExpanding(bool supportsItemExpanding) { if (m_supportsItemExpanding != supportsItemExpanding) { m_supportsItemExpanding = supportsItemExpanding; m_dirtyLayout = true; update(); } } bool KStandardItemListWidget::supportsItemExpanding() const { return m_supportsItemExpanding; } void KStandardItemListWidget::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) { const_cast(this)->triggerCacheRefreshing(); KItemListWidget::paint(painter, option, widget); if (!m_expansionArea.isEmpty()) { drawSiblingsInformation(painter); } const KItemListStyleOption& itemListStyleOption = styleOption(); if (isHovered()) { if (hoverOpacity() < 1.0) { /* * Linear interpolation between m_pixmap and m_hoverPixmap. * * Note that this cannot be achieved by painting m_hoverPixmap over * m_pixmap, even if the opacities are adjusted. For details see * https://git.reviewboard.kde.org/r/109614/ */ // Paint pixmap1 so that pixmap1 = m_pixmap * (1.0 - hoverOpacity()) QPixmap pixmap1(m_pixmap.size()); pixmap1.setDevicePixelRatio(m_pixmap.devicePixelRatio()); pixmap1.fill(Qt::transparent); { QPainter p(&pixmap1); p.setOpacity(1.0 - hoverOpacity()); p.drawPixmap(0, 0, m_pixmap); } // Paint pixmap2 so that pixmap2 = m_hoverPixmap * hoverOpacity() QPixmap pixmap2(pixmap1.size()); pixmap2.setDevicePixelRatio(pixmap1.devicePixelRatio()); pixmap2.fill(Qt::transparent); { QPainter p(&pixmap2); p.setOpacity(hoverOpacity()); p.drawPixmap(0, 0, m_hoverPixmap); } // Paint pixmap2 on pixmap1 using CompositionMode_Plus // Now pixmap1 = pixmap2 + m_pixmap * (1.0 - hoverOpacity()) // = m_hoverPixmap * hoverOpacity() + m_pixmap * (1.0 - hoverOpacity()) { QPainter p(&pixmap1); p.setCompositionMode(QPainter::CompositionMode_Plus); p.drawPixmap(0, 0, pixmap2); } // Finally paint pixmap1 on the widget drawPixmap(painter, pixmap1); } else { drawPixmap(painter, m_hoverPixmap); } } else { drawPixmap(painter, m_pixmap); } painter->setFont(m_customizedFont); painter->setPen(textColor()); const TextInfo* textInfo = m_textInfo.value("text"); if (!textInfo) { // It seems that we can end up here even if m_textInfo does not contain // the key "text", see bug 306167. According to triggerCacheRefreshing(), // this can only happen if the index is negative. This can happen when // the item is about to be removed, see KItemListView::slotItemsRemoved(). // TODO: try to reproduce the crash and find a better fix. return; } painter->drawStaticText(textInfo->pos, textInfo->staticText); bool clipAdditionalInfoBounds = false; if (m_supportsItemExpanding) { // Prevent a possible overlapping of the additional-information texts // with the icon. This can happen if the user has minimized the width // of the name-column to a very small value. const qreal minX = m_pixmapPos.x() + m_pixmap.width() + 4 * itemListStyleOption.padding; if (textInfo->pos.x() + columnWidth("text") > minX) { clipAdditionalInfoBounds = true; painter->save(); painter->setClipRect(minX, 0, size().width() - minX, size().height(), Qt::IntersectClip); } } painter->setPen(m_additionalInfoTextColor); painter->setFont(m_customizedFont); for (int i = 1; i < m_sortedVisibleRoles.count(); ++i) { const TextInfo* textInfo = m_textInfo.value(m_sortedVisibleRoles[i]); painter->drawStaticText(textInfo->pos, textInfo->staticText); } if (!m_rating.isNull()) { const TextInfo* ratingTextInfo = m_textInfo.value("rating"); QPointF pos = ratingTextInfo->pos; const Qt::Alignment align = ratingTextInfo->staticText.textOption().alignment(); if (align & Qt::AlignHCenter) { pos.rx() += (size().width() - m_rating.width()) / 2 - 2; } painter->drawPixmap(pos, m_rating); } if (clipAdditionalInfoBounds) { painter->restore(); } #ifdef KSTANDARDITEMLISTWIDGET_DEBUG painter->setBrush(Qt::NoBrush); painter->setPen(Qt::green); painter->drawRect(m_iconRect); painter->setPen(Qt::blue); painter->drawRect(m_textRect); painter->setPen(Qt::red); painter->drawText(QPointF(0, m_customizedFontMetrics.height()), QString::number(index())); painter->drawRect(rect()); #endif } QRectF KStandardItemListWidget::iconRect() const { const_cast(this)->triggerCacheRefreshing(); return m_iconRect; } QRectF KStandardItemListWidget::textRect() const { const_cast(this)->triggerCacheRefreshing(); return m_textRect; } QRectF KStandardItemListWidget::textFocusRect() const { // In the compact- and details-layout a larger textRect() is returned to be aligned // with the iconRect(). This is useful to have a larger selection/hover-area // when having a quite large icon size but only one line of text. Still the // focus rectangle should be shown as narrow as possible around the text. const_cast(this)->triggerCacheRefreshing(); switch (m_layout) { case CompactLayout: { QRectF rect = m_textRect; const TextInfo* topText = m_textInfo.value(m_sortedVisibleRoles.first()); const TextInfo* bottomText = m_textInfo.value(m_sortedVisibleRoles.last()); rect.setTop(topText->pos.y()); rect.setBottom(bottomText->pos.y() + bottomText->staticText.size().height()); return rect; } case DetailsLayout: { QRectF rect = m_textRect; const TextInfo* textInfo = m_textInfo.value(m_sortedVisibleRoles.first()); rect.setTop(textInfo->pos.y()); rect.setBottom(textInfo->pos.y() + textInfo->staticText.size().height()); const KItemListStyleOption& option = styleOption(); if (option.extendedSelectionRegion) { const QString text = textInfo->staticText.text(); rect.setWidth(m_customizedFontMetrics.width(text) + 2 * option.padding); } return rect; } default: break; } return m_textRect; } QRectF KStandardItemListWidget::selectionRect() const { const_cast(this)->triggerCacheRefreshing(); switch (m_layout) { case IconsLayout: return m_textRect; case CompactLayout: case DetailsLayout: { const int padding = styleOption().padding; QRectF adjustedIconRect = iconRect().adjusted(-padding, -padding, padding, padding); return adjustedIconRect | m_textRect; } default: Q_ASSERT(false); break; } return m_textRect; } QRectF KStandardItemListWidget::expansionToggleRect() const { const_cast(this)->triggerCacheRefreshing(); return m_isExpandable ? m_expansionArea : QRectF(); } QRectF KStandardItemListWidget::selectionToggleRect() const { const_cast(this)->triggerCacheRefreshing(); const int iconHeight = styleOption().iconSize; int toggleSize = KIconLoader::SizeSmall; if (iconHeight >= KIconLoader::SizeEnormous) { toggleSize = KIconLoader::SizeMedium; } else if (iconHeight >= KIconLoader::SizeLarge) { toggleSize = KIconLoader::SizeSmallMedium; } QPointF pos = iconRect().topLeft(); // If the selection toggle has a very small distance to the // widget borders, the size of the selection toggle will get // increased to prevent an accidental clicking of the item // when trying to hit the toggle. const int widgetHeight = size().height(); const int widgetWidth = size().width(); const int minMargin = 2; if (toggleSize + minMargin * 2 >= widgetHeight) { pos.rx() -= (widgetHeight - toggleSize) / 2; toggleSize = widgetHeight; pos.setY(0); } if (toggleSize + minMargin * 2 >= widgetWidth) { pos.ry() -= (widgetWidth - toggleSize) / 2; toggleSize = widgetWidth; pos.setX(0); } return QRectF(pos, QSizeF(toggleSize, toggleSize)); } QPixmap KStandardItemListWidget::createDragPixmap(const QStyleOptionGraphicsItem* option, QWidget* widget) { QPixmap pixmap = KItemListWidget::createDragPixmap(option, widget); if (m_layout != DetailsLayout) { return pixmap; } // Only return the content of the text-column as pixmap const int leftClip = m_pixmapPos.x(); const TextInfo* textInfo = m_textInfo.value("text"); const int rightClip = textInfo->pos.x() + textInfo->staticText.size().width() + 2 * styleOption().padding; QPixmap clippedPixmap(rightClip - leftClip + 1, pixmap.height()); clippedPixmap.fill(Qt::transparent); QPainter painter(&clippedPixmap); painter.drawPixmap(-leftClip, 0, pixmap); return clippedPixmap; } KItemListWidgetInformant* KStandardItemListWidget::createInformant() { return new KStandardItemListWidgetInformant(); } void KStandardItemListWidget::invalidateCache() { m_dirtyLayout = true; m_dirtyContent = true; } void KStandardItemListWidget::refreshCache() { } bool KStandardItemListWidget::isRoleRightAligned(const QByteArray& role) const { - Q_UNUSED(role); + Q_UNUSED(role) return false; } bool KStandardItemListWidget::isHidden() const { return false; } QFont KStandardItemListWidget::customizedFont(const QFont& baseFont) const { return baseFont; } QPalette::ColorRole KStandardItemListWidget::normalTextColorRole() const { return QPalette::Text; } void KStandardItemListWidget::setTextColor(const QColor& color) { if (color != m_customTextColor) { m_customTextColor = color; updateAdditionalInfoTextColor(); update(); } } QColor KStandardItemListWidget::textColor() const { if (!isSelected()) { if (m_isHidden) { return m_additionalInfoTextColor; } else if (m_customTextColor.isValid()) { return m_customTextColor; } } const QPalette::ColorGroup group = isActiveWindow() ? QPalette::Active : QPalette::Inactive; const QPalette::ColorRole role = isSelected() ? QPalette::HighlightedText : normalTextColorRole(); return styleOption().palette.color(group, role); } void KStandardItemListWidget::setOverlay(const QPixmap& overlay) { m_overlay = overlay; m_dirtyContent = true; update(); } QPixmap KStandardItemListWidget::overlay() const { return m_overlay; } QString KStandardItemListWidget::roleText(const QByteArray& role, const QHash& values) const { return static_cast(informant())->roleText(role, values); } void KStandardItemListWidget::dataChanged(const QHash& current, const QSet& roles) { - Q_UNUSED(current); + Q_UNUSED(current) m_dirtyContent = true; QSet dirtyRoles; if (roles.isEmpty()) { dirtyRoles = visibleRoles().toSet(); } else { dirtyRoles = roles; } // The URL might have changed (i.e., if the sort order of the items has // been changed). Therefore, the "is cut" state must be updated. KFileItemClipboard* clipboard = KFileItemClipboard::instance(); const QUrl itemUrl = data().value("url").toUrl(); m_isCut = clipboard->isCut(itemUrl); // The icon-state might depend from other roles and hence is // marked as dirty whenever a role has been changed dirtyRoles.insert("iconPixmap"); dirtyRoles.insert("iconName"); QSetIterator it(dirtyRoles); while (it.hasNext()) { const QByteArray& role = it.next(); m_dirtyContentRoles.insert(role); } } void KStandardItemListWidget::visibleRolesChanged(const QList& current, const QList& previous) { - Q_UNUSED(previous); + Q_UNUSED(previous) m_sortedVisibleRoles = current; m_dirtyLayout = true; } void KStandardItemListWidget::columnWidthChanged(const QByteArray& role, qreal current, qreal previous) { - Q_UNUSED(role); - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(role) + Q_UNUSED(current) + Q_UNUSED(previous) m_dirtyLayout = true; } void KStandardItemListWidget::styleOptionChanged(const KItemListStyleOption& current, const KItemListStyleOption& previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) updateAdditionalInfoTextColor(); m_dirtyLayout = true; } void KStandardItemListWidget::hoveredChanged(bool hovered) { - Q_UNUSED(hovered); + Q_UNUSED(hovered) m_dirtyLayout = true; } void KStandardItemListWidget::selectedChanged(bool selected) { - Q_UNUSED(selected); + Q_UNUSED(selected) updateAdditionalInfoTextColor(); m_dirtyContent = true; } void KStandardItemListWidget::siblingsInformationChanged(const QBitArray& current, const QBitArray& previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) m_dirtyLayout = true; } int KStandardItemListWidget::selectionLength(const QString& text) const { return text.length(); } void KStandardItemListWidget::editedRoleChanged(const QByteArray& current, const QByteArray& previous) { - Q_UNUSED(previous); + Q_UNUSED(previous) QGraphicsView* parent = scene()->views()[0]; if (current.isEmpty() || !parent || current != "text") { if (m_roleEditor) { emit roleEditingCanceled(index(), current, data().value(current)); disconnect(m_roleEditor, &KItemListRoleEditor::roleEditingCanceled, this, &KStandardItemListWidget::slotRoleEditingCanceled); disconnect(m_roleEditor, &KItemListRoleEditor::roleEditingFinished, this, &KStandardItemListWidget::slotRoleEditingFinished); if (m_oldRoleEditor) { m_oldRoleEditor->deleteLater(); } m_oldRoleEditor = m_roleEditor; m_roleEditor->hide(); m_roleEditor = nullptr; } return; } Q_ASSERT(!m_roleEditor); const TextInfo* textInfo = m_textInfo.value("text"); m_roleEditor = new KItemListRoleEditor(parent); m_roleEditor->setRole(current); m_roleEditor->setFont(styleOption().font); const QString text = data().value(current).toString(); m_roleEditor->setPlainText(text); QTextOption textOption = textInfo->staticText.textOption(); m_roleEditor->document()->setDefaultTextOption(textOption); const int textSelectionLength = selectionLength(text); if (textSelectionLength > 0) { QTextCursor cursor = m_roleEditor->textCursor(); cursor.movePosition(QTextCursor::StartOfBlock); cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, textSelectionLength); m_roleEditor->setTextCursor(cursor); } 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->show(); m_roleEditor->setFocus(); } void KStandardItemListWidget::resizeEvent(QGraphicsSceneResizeEvent* event) { if (m_roleEditor) { setEditedRole(QByteArray()); Q_ASSERT(!m_roleEditor); } KItemListWidget::resizeEvent(event); m_dirtyLayout = true; } void KStandardItemListWidget::showEvent(QShowEvent* event) { KItemListWidget::showEvent(event); // Listen to changes of the clipboard to mark the item as cut/uncut KFileItemClipboard* clipboard = KFileItemClipboard::instance(); const QUrl itemUrl = data().value("url").toUrl(); m_isCut = clipboard->isCut(itemUrl); connect(clipboard, &KFileItemClipboard::cutItemsChanged, this, &KStandardItemListWidget::slotCutItemsChanged); } void KStandardItemListWidget::hideEvent(QHideEvent* event) { disconnect(KFileItemClipboard::instance(), &KFileItemClipboard::cutItemsChanged, this, &KStandardItemListWidget::slotCutItemsChanged); KItemListWidget::hideEvent(event); } bool KStandardItemListWidget::event(QEvent *event) { if (event->type() == QEvent::WindowDeactivate || event->type() == QEvent::WindowActivate || event->type() == QEvent::PaletteChange) { m_dirtyContent = true; } return KItemListWidget::event(event); } void KStandardItemListWidget::finishRoleEditing() { if (!editedRole().isEmpty() && m_roleEditor) { slotRoleEditingFinished(editedRole(), KIO::encodeFileName(m_roleEditor->toPlainText())); } } void KStandardItemListWidget::slotCutItemsChanged() { const QUrl itemUrl = data().value("url").toUrl(); const bool isCut = KFileItemClipboard::instance()->isCut(itemUrl); if (m_isCut != isCut) { m_isCut = isCut; m_pixmap = QPixmap(); m_dirtyContent = true; update(); } } void KStandardItemListWidget::slotRoleEditingCanceled(const QByteArray& role, const QVariant& value) { closeRoleEditor(); emit roleEditingCanceled(index(), role, value); setEditedRole(QByteArray()); } void KStandardItemListWidget::slotRoleEditingFinished(const QByteArray& role, const QVariant& value) { closeRoleEditor(); emit roleEditingFinished(index(), role, value); setEditedRole(QByteArray()); } void KStandardItemListWidget::triggerCacheRefreshing() { if ((!m_dirtyContent && !m_dirtyLayout) || index() < 0) { return; } refreshCache(); const QHash values = data(); m_isExpandable = m_supportsItemExpanding && values["isExpandable"].toBool(); m_isHidden = isHidden(); m_customizedFont = customizedFont(styleOption().font); m_customizedFontMetrics = QFontMetrics(m_customizedFont); updateExpansionArea(); updateTextsCache(); updatePixmapCache(); m_dirtyLayout = false; m_dirtyContent = false; m_dirtyContentRoles.clear(); } void KStandardItemListWidget::updateExpansionArea() { if (m_supportsItemExpanding) { const QHash values = data(); const int expandedParentsCount = values.value("expandedParentsCount", 0).toInt(); if (expandedParentsCount >= 0) { const KItemListStyleOption& option = styleOption(); const qreal widgetHeight = size().height(); const qreal inc = (widgetHeight - option.iconSize) / 2; const qreal x = expandedParentsCount * widgetHeight + inc; const qreal y = inc; m_expansionArea = QRectF(x, y, option.iconSize, option.iconSize); return; } } m_expansionArea = QRectF(); } void KStandardItemListWidget::updatePixmapCache() { // Precondition: Requires already updated m_textPos values to calculate // the remaining height when the alignment is vertical. const QSizeF widgetSize = size(); const bool iconOnTop = (m_layout == IconsLayout); const KItemListStyleOption& option = styleOption(); const qreal padding = option.padding; const int maxIconWidth = iconOnTop ? widgetSize.width() - 2 * padding : option.iconSize; const int maxIconHeight = option.iconSize; const QHash values = data(); bool updatePixmap = (m_pixmap.width() != maxIconWidth || m_pixmap.height() != maxIconHeight); if (!updatePixmap && m_dirtyContent) { updatePixmap = m_dirtyContentRoles.isEmpty() || m_dirtyContentRoles.contains("iconPixmap") || m_dirtyContentRoles.contains("iconName") || m_dirtyContentRoles.contains("iconOverlays"); } if (updatePixmap) { m_pixmap = values["iconPixmap"].value(); if (m_pixmap.isNull()) { // Use the icon that fits to the MIME-type QString iconName = values["iconName"].toString(); if (iconName.isEmpty()) { // The icon-name has not been not resolved by KFileItemModelRolesUpdater, // use a generic icon as fallback iconName = QStringLiteral("unknown"); } const QStringList overlays = values["iconOverlays"].toStringList(); m_pixmap = pixmapForIcon(iconName, overlays, maxIconHeight, m_layout != IconsLayout && isActiveWindow() && isSelected() ? QIcon::Selected : QIcon::Normal); } else if (m_pixmap.width() / m_pixmap.devicePixelRatio() != maxIconWidth || m_pixmap.height() / m_pixmap.devicePixelRatio() != maxIconHeight) { // A custom pixmap has been applied. Assure that the pixmap // is scaled to the maximum available size. KPixmapModifier::scale(m_pixmap, QSize(maxIconWidth, maxIconHeight) * qApp->devicePixelRatio()); } if (m_isCut) { KIconEffect* effect = KIconLoader::global()->iconEffect(); m_pixmap = effect->apply(m_pixmap, KIconLoader::Desktop, KIconLoader::DisabledState); } if (m_isHidden) { KIconEffect::semiTransparent(m_pixmap); } if (m_layout == IconsLayout && isSelected()) { const QColor color = palette().brush(QPalette::Normal, QPalette::Highlight).color(); QImage image = m_pixmap.toImage(); KIconEffect::colorize(image, color, 0.8f); m_pixmap = QPixmap::fromImage(image); } } if (!m_overlay.isNull()) { QPainter painter(&m_pixmap); painter.drawPixmap(0, (m_pixmap.height() - m_overlay.height()) / m_pixmap.devicePixelRatio(), m_overlay); } int scaledIconSize = 0; if (iconOnTop) { const TextInfo* textInfo = m_textInfo.value("text"); scaledIconSize = static_cast(textInfo->pos.y() - 2 * padding); } else { const int textRowsCount = (m_layout == CompactLayout) ? visibleRoles().count() : 1; const qreal requiredTextHeight = textRowsCount * m_customizedFontMetrics.height(); scaledIconSize = (requiredTextHeight < maxIconHeight) ? widgetSize.height() - 2 * padding : maxIconHeight; } const int maxScaledIconWidth = iconOnTop ? widgetSize.width() - 2 * padding : scaledIconSize; const int maxScaledIconHeight = scaledIconSize; m_scaledPixmapSize = m_pixmap.size(); m_scaledPixmapSize.scale(maxScaledIconWidth * qApp->devicePixelRatio(), maxScaledIconHeight * qApp->devicePixelRatio(), Qt::KeepAspectRatio); m_scaledPixmapSize = m_scaledPixmapSize / qApp->devicePixelRatio(); if (iconOnTop) { // Center horizontally and align on bottom within the icon-area m_pixmapPos.setX((widgetSize.width() - m_scaledPixmapSize.width()) / 2.0); m_pixmapPos.setY(padding + scaledIconSize - m_scaledPixmapSize.height()); } else { // Center horizontally and vertically within the icon-area const TextInfo* textInfo = m_textInfo.value("text"); m_pixmapPos.setX(textInfo->pos.x() - 2.0 * padding - (scaledIconSize + m_scaledPixmapSize.width()) / 2.0); // Derive icon's vertical center from the center of the text frame, including // any necessary adjustment if the font's midline is offset from the frame center const qreal midlineShift = m_customizedFontMetrics.height() / 2.0 - m_customizedFontMetrics.descent() - m_customizedFontMetrics.capHeight() / 2.0; m_pixmapPos.setY(m_textRect.center().y() + midlineShift - m_scaledPixmapSize.height() / 2.0); } m_iconRect = QRectF(m_pixmapPos, QSizeF(m_scaledPixmapSize)); // Prepare the pixmap that is used when the item gets hovered if (isHovered()) { m_hoverPixmap = m_pixmap; KIconEffect* effect = KIconLoader::global()->iconEffect(); // In the KIconLoader terminology, active = hover. if (effect->hasEffect(KIconLoader::Desktop, KIconLoader::ActiveState)) { m_hoverPixmap = effect->apply(m_pixmap, KIconLoader::Desktop, KIconLoader::ActiveState); } else { m_hoverPixmap = m_pixmap; } } else if (hoverOpacity() <= 0.0) { // No hover animation is ongoing. Clear m_hoverPixmap to save memory. m_hoverPixmap = QPixmap(); } } void KStandardItemListWidget::updateTextsCache() { QTextOption textOption; switch (m_layout) { case IconsLayout: textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); textOption.setAlignment(Qt::AlignHCenter); break; case CompactLayout: case DetailsLayout: textOption.setAlignment(Qt::AlignLeft); textOption.setWrapMode(QTextOption::NoWrap); break; default: Q_ASSERT(false); break; } qDeleteAll(m_textInfo); m_textInfo.clear(); for (int i = 0; i < m_sortedVisibleRoles.count(); ++i) { TextInfo* textInfo = new TextInfo(); textInfo->staticText.setTextFormat(Qt::PlainText); textInfo->staticText.setPerformanceHint(QStaticText::AggressiveCaching); textInfo->staticText.setTextOption(textOption); m_textInfo.insert(m_sortedVisibleRoles[i], textInfo); } switch (m_layout) { case IconsLayout: updateIconsLayoutTextCache(); break; case CompactLayout: updateCompactLayoutTextCache(); break; case DetailsLayout: updateDetailsLayoutTextCache(); break; default: Q_ASSERT(false); break; } const TextInfo* ratingTextInfo = m_textInfo.value("rating"); if (ratingTextInfo) { // The text of the rating-role has been set to empty to get // replaced by a rating-image showing the rating as stars. const KItemListStyleOption& option = styleOption(); QSizeF ratingSize = preferredRatingSize(option); const qreal availableWidth = (m_layout == DetailsLayout) ? columnWidth("rating") - columnPadding(option) : size().width(); if (ratingSize.width() > availableWidth) { ratingSize.rwidth() = availableWidth; } const qreal dpr = qApp->devicePixelRatio(); m_rating = QPixmap(ratingSize.toSize() * dpr); m_rating.setDevicePixelRatio(dpr); m_rating.fill(Qt::transparent); QPainter painter(&m_rating); const QRect rect(QPoint(0, 0), ratingSize.toSize()); const int rating = data().value("rating").toInt(); KRatingPainter::paintRating(&painter, rect, Qt::AlignJustify | Qt::AlignVCenter, rating); } else if (!m_rating.isNull()) { m_rating = QPixmap(); } } void KStandardItemListWidget::updateIconsLayoutTextCache() { // +------+ // | Icon | // +------+ // // Name role that // might get wrapped above // several lines. // Additional role 1 // Additional role 2 const QHash values = data(); const KItemListStyleOption& option = styleOption(); const qreal padding = option.padding; const qreal maxWidth = size().width() - 2 * padding; const qreal widgetHeight = size().height(); const qreal lineSpacing = m_customizedFontMetrics.lineSpacing(); // Initialize properties for the "text" role. It will be used as anchor // for initializing the position of the other roles. TextInfo* nameTextInfo = m_textInfo.value("text"); const QString nameText = KStringHandler::preProcessWrap(values["text"].toString()); nameTextInfo->staticText.setText(nameText); // Calculate the number of lines required for the name and the required width qreal nameWidth = 0; qreal nameHeight = 0; QTextLine line; QTextLayout layout(nameTextInfo->staticText.text(), m_customizedFont); layout.setTextOption(nameTextInfo->staticText.textOption()); layout.beginLayout(); int nameLineIndex = 0; while ((line = layout.createLine()).isValid()) { line.setLineWidth(maxWidth); nameWidth = qMax(nameWidth, line.naturalTextWidth()); nameHeight += line.height(); ++nameLineIndex; if (nameLineIndex == option.maxTextLines) { // The maximum number of textlines has been reached. If this is // the case provide an elided text if necessary. const int textLength = line.textStart() + line.textLength(); if (textLength < nameText.length()) { // Elide the last line of the text qreal elidingWidth = maxWidth; qreal lastLineWidth; do { QString lastTextLine = nameText.mid(line.textStart()); lastTextLine = m_customizedFontMetrics.elidedText(lastTextLine, Qt::ElideMiddle, elidingWidth); const QString elidedText = nameText.left(line.textStart()) + lastTextLine; nameTextInfo->staticText.setText(elidedText); lastLineWidth = m_customizedFontMetrics.boundingRect(lastTextLine).width(); // We do the text eliding in a loop with decreasing width (1 px / iteration) // to avoid problems related to different width calculation code paths // within Qt. (see bug 337104) elidingWidth -= 1.0; } while (lastLineWidth > maxWidth); nameWidth = qMax(nameWidth, lastLineWidth); } break; } } layout.endLayout(); // Use one line for each additional information const int additionalRolesCount = qMax(visibleRoles().count() - 1, 0); nameTextInfo->staticText.setTextWidth(maxWidth); nameTextInfo->pos = QPointF(padding, widgetHeight - nameHeight - additionalRolesCount * lineSpacing - padding); m_textRect = QRectF(padding + (maxWidth - nameWidth) / 2, nameTextInfo->pos.y(), nameWidth, nameHeight); // Calculate the position for each additional information qreal y = nameTextInfo->pos.y() + nameHeight; foreach (const QByteArray& role, m_sortedVisibleRoles) { if (role == "text") { continue; } const QString text = roleText(role, values); TextInfo* textInfo = m_textInfo.value(role); textInfo->staticText.setText(text); qreal requiredWidth = 0; QTextLayout layout(text, m_customizedFont); QTextOption textOption; textOption.setWrapMode(QTextOption::NoWrap); layout.setTextOption(textOption); layout.beginLayout(); QTextLine textLine = layout.createLine(); if (textLine.isValid()) { textLine.setLineWidth(maxWidth); requiredWidth = textLine.naturalTextWidth(); if (requiredWidth > maxWidth) { const QString elidedText = m_customizedFontMetrics.elidedText(text, Qt::ElideMiddle, maxWidth); textInfo->staticText.setText(elidedText); requiredWidth = m_customizedFontMetrics.width(elidedText); } else if (role == "rating") { // Use the width of the rating pixmap, because the rating text is empty. requiredWidth = m_rating.width(); } } layout.endLayout(); textInfo->pos = QPointF(padding, y); textInfo->staticText.setTextWidth(maxWidth); const QRectF textRect(padding + (maxWidth - requiredWidth) / 2, y, requiredWidth, lineSpacing); m_textRect |= textRect; y += lineSpacing; } // Add a padding to the text rectangle m_textRect.adjust(-padding, -padding, padding, padding); } void KStandardItemListWidget::updateCompactLayoutTextCache() { // +------+ Name role // | Icon | Additional role 1 // +------+ Additional role 2 const QHash values = data(); const KItemListStyleOption& option = styleOption(); const qreal widgetHeight = size().height(); const qreal lineSpacing = m_customizedFontMetrics.lineSpacing(); const qreal textLinesHeight = qMax(visibleRoles().count(), 1) * lineSpacing; const int scaledIconSize = (textLinesHeight < option.iconSize) ? widgetHeight - 2 * option.padding : option.iconSize; qreal maximumRequiredTextWidth = 0; const qreal x = option.padding * 3 + scaledIconSize; qreal y = qRound((widgetHeight - textLinesHeight) / 2); const qreal maxWidth = size().width() - x - option.padding; foreach (const QByteArray& role, m_sortedVisibleRoles) { const QString text = roleText(role, values); TextInfo* textInfo = m_textInfo.value(role); textInfo->staticText.setText(text); qreal requiredWidth = m_customizedFontMetrics.width(text); if (requiredWidth > maxWidth) { requiredWidth = maxWidth; const QString elidedText = m_customizedFontMetrics.elidedText(text, Qt::ElideMiddle, maxWidth); textInfo->staticText.setText(elidedText); } textInfo->pos = QPointF(x, y); textInfo->staticText.setTextWidth(maxWidth); maximumRequiredTextWidth = qMax(maximumRequiredTextWidth, requiredWidth); y += lineSpacing; } m_textRect = QRectF(x - 2 * option.padding, 0, maximumRequiredTextWidth + 3 * option.padding, widgetHeight); } void KStandardItemListWidget::updateDetailsLayoutTextCache() { // Precondition: Requires already updated m_expansionArea // to determine the left position. // +------+ // | Icon | Name role Additional role 1 Additional role 2 // +------+ m_textRect = QRectF(); const KItemListStyleOption& option = styleOption(); const QHash values = data(); const qreal widgetHeight = size().height(); const int scaledIconSize = widgetHeight - 2 * option.padding; const int fontHeight = m_customizedFontMetrics.height(); const qreal columnWidthInc = columnPadding(option); qreal firstColumnInc = scaledIconSize; if (m_supportsItemExpanding) { firstColumnInc += (m_expansionArea.left() + m_expansionArea.right() + widgetHeight) / 2; } else { firstColumnInc += option.padding; } qreal x = firstColumnInc; const qreal y = qMax(qreal(option.padding), (widgetHeight - fontHeight) / 2); foreach (const QByteArray& role, m_sortedVisibleRoles) { QString text = roleText(role, values); // Elide the text in case it does not fit into the available column-width qreal requiredWidth = m_customizedFontMetrics.width(text); const qreal roleWidth = columnWidth(role); qreal availableTextWidth = roleWidth - columnWidthInc; const bool isTextRole = (role == "text"); if (isTextRole) { availableTextWidth -= firstColumnInc; } if (requiredWidth > availableTextWidth) { text = m_customizedFontMetrics.elidedText(text, Qt::ElideMiddle, availableTextWidth); requiredWidth = m_customizedFontMetrics.width(text); } TextInfo* textInfo = m_textInfo.value(role); textInfo->staticText.setText(text); textInfo->pos = QPointF(x + columnWidthInc / 2, y); x += roleWidth; if (isTextRole) { const qreal textWidth = option.extendedSelectionRegion ? size().width() - textInfo->pos.x() : requiredWidth + 2 * option.padding; m_textRect = QRectF(textInfo->pos.x() - 2 * option.padding, 0, textWidth + option.padding, size().height()); // The column after the name should always be aligned on the same x-position independent // from the expansion-level shown in the name column x -= firstColumnInc; } else if (isRoleRightAligned(role)) { textInfo->pos.rx() += roleWidth - requiredWidth - columnWidthInc; } } } void KStandardItemListWidget::updateAdditionalInfoTextColor() { QColor c1; if (m_customTextColor.isValid()) { c1 = m_customTextColor; } else if (isSelected() && m_layout != DetailsLayout) { c1 = styleOption().palette.highlightedText().color(); } else { c1 = styleOption().palette.text().color(); } // For the color of the additional info the inactive text color // is not used as this might lead to unreadable text for some color schemes. Instead // the text color c1 is slightly mixed with the background color. const QColor c2 = styleOption().palette.base().color(); const int p1 = 70; const int p2 = 100 - p1; m_additionalInfoTextColor = QColor((c1.red() * p1 + c2.red() * p2) / 100, (c1.green() * p1 + c2.green() * p2) / 100, (c1.blue() * p1 + c2.blue() * p2) / 100); } void KStandardItemListWidget::drawPixmap(QPainter* painter, const QPixmap& pixmap) { if (m_scaledPixmapSize != pixmap.size() / pixmap.devicePixelRatio()) { QPixmap scaledPixmap = pixmap; KPixmapModifier::scale(scaledPixmap, m_scaledPixmapSize * qApp->devicePixelRatio()); scaledPixmap.setDevicePixelRatio(qApp->devicePixelRatio()); painter->drawPixmap(m_pixmapPos, scaledPixmap); #ifdef KSTANDARDITEMLISTWIDGET_DEBUG painter->setPen(Qt::blue); painter->drawRect(QRectF(m_pixmapPos, QSizeF(m_scaledPixmapSize))); #endif } else { painter->drawPixmap(m_pixmapPos, pixmap); } } void KStandardItemListWidget::drawSiblingsInformation(QPainter* painter) { const int siblingSize = size().height(); const int x = (m_expansionArea.left() + m_expansionArea.right() - siblingSize) / 2; QRect siblingRect(x, 0, siblingSize, siblingSize); QStyleOption option; option.palette.setColor(QPalette::Text, option.palette.color(normalTextColorRole())); bool isItemSibling = true; const QBitArray siblings = siblingsInformation(); for (int i = siblings.count() - 1; i >= 0; --i) { option.rect = siblingRect; option.state = siblings.at(i) ? QStyle::State_Sibling : QStyle::State_None; if (isItemSibling) { option.state |= QStyle::State_Item; if (m_isExpandable) { option.state |= QStyle::State_Children; } if (data().value("isExpanded").toBool()) { option.state |= QStyle::State_Open; } isItemSibling = false; } style()->drawPrimitive(QStyle::PE_IndicatorBranch, &option, painter); siblingRect.translate(-siblingRect.width(), 0); } } QRectF KStandardItemListWidget::roleEditingRect(const QByteArray& role) const { const TextInfo* textInfo = m_textInfo.value(role); if (!textInfo) { return QRectF(); } QRectF rect(textInfo->pos, textInfo->staticText.size()); if (m_layout == DetailsLayout) { rect.setWidth(columnWidth(role) - rect.x()); } return rect; } void KStandardItemListWidget::closeRoleEditor() { disconnect(m_roleEditor, &KItemListRoleEditor::roleEditingCanceled, this, &KStandardItemListWidget::slotRoleEditingCanceled); disconnect(m_roleEditor, &KItemListRoleEditor::roleEditingFinished, this, &KStandardItemListWidget::slotRoleEditingFinished); if (m_roleEditor->hasFocus()) { // If the editing was not ended by a FocusOut event, we have // to transfer the keyboard focus back to the KItemListContainer. scene()->views()[0]->parentWidget()->setFocus(); } if (m_oldRoleEditor) { m_oldRoleEditor->deleteLater(); } m_oldRoleEditor = m_roleEditor; m_roleEditor->hide(); m_roleEditor = nullptr; } QPixmap KStandardItemListWidget::pixmapForIcon(const QString& name, const QStringList& overlays, int size, QIcon::Mode mode) { static const QIcon fallbackIcon = QIcon::fromTheme(QStringLiteral("unknown")); size *= qApp->devicePixelRatio(); const QString key = "KStandardItemListWidget:" % name % ":" % overlays.join(QLatin1Char(':')) % ":" % QString::number(size) % ":" % QString::number(mode); QPixmap pixmap; if (!QPixmapCache::find(key, pixmap)) { const QIcon icon = QIcon::fromTheme(name, fallbackIcon); pixmap = icon.pixmap(size / qApp->devicePixelRatio(), size / qApp->devicePixelRatio(), mode); if (pixmap.width() != size || pixmap.height() != size) { KPixmapModifier::scale(pixmap, QSize(size, size)); } // Strangely KFileItem::overlays() returns empty string-values, so // we need to check first whether an overlay must be drawn at all. // It is more efficient to do it here, as KIconLoader::drawOverlays() // assumes that an overlay will be drawn and has some additional // setup time. foreach (const QString& overlay, overlays) { if (!overlay.isEmpty()) { int state = KIconLoader::DefaultState; switch (mode) { case QIcon::Normal: break; case QIcon::Active: state = KIconLoader::ActiveState; break; case QIcon::Disabled: state = KIconLoader::DisabledState; break; case QIcon::Selected: state = KIconLoader::SelectedState; break; } // There is at least one overlay, draw all overlays above m_pixmap // and cancel the check KIconLoader::global()->drawOverlays(overlays, pixmap, KIconLoader::Desktop, state); break; } } QPixmapCache::insert(key, pixmap); } pixmap.setDevicePixelRatio(qApp->devicePixelRatio()); return pixmap; } QSizeF KStandardItemListWidget::preferredRatingSize(const KItemListStyleOption& option) { const qreal height = option.fontMetrics.ascent(); return QSizeF(height * 5, height); } qreal KStandardItemListWidget::columnPadding(const KItemListStyleOption& option) { return option.padding * 6; } diff --git a/src/kitemviews/kstandarditemmodel.cpp b/src/kitemviews/kstandarditemmodel.cpp index d7307c0bd..c40050c7e 100644 --- a/src/kitemviews/kstandarditemmodel.cpp +++ b/src/kitemviews/kstandarditemmodel.cpp @@ -1,238 +1,238 @@ /*************************************************************************** * Copyright (C) 2012 by Peter Penz * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kstandarditemmodel.h" #include "kstandarditem.h" KStandardItemModel::KStandardItemModel(QObject* parent) : KItemModelBase(parent), m_items(), m_indexesForItems() { } KStandardItemModel::~KStandardItemModel() { qDeleteAll(m_items); m_items.clear(); m_indexesForItems.clear(); } void KStandardItemModel::insertItem(int index, KStandardItem* item) { if (index < 0 || index > count() || !item) { delete item; return; } if (!m_indexesForItems.contains(item)) { item->m_model = this; m_items.insert(index, item); m_indexesForItems.insert(item, index); // Inserting an item requires to update the indexes // afterwards from m_indexesForItems. for (int i = index + 1; i < m_items.count(); ++i) { m_indexesForItems.insert(m_items[i], i); } // TODO: no hierarchical items are handled yet onItemInserted(index); emit itemsInserted(KItemRangeList() << KItemRange(index, 1)); } } void KStandardItemModel::changeItem(int index, KStandardItem* item) { if (index < 0 || index >= count() || !item) { delete item; return; } item->m_model = this; QSet changedRoles; KStandardItem* oldItem = m_items[index]; const QHash oldData = oldItem->data(); const QHash newData = item->data(); // Determine which roles have been changed QHashIterator it(oldData); while (it.hasNext()) { it.next(); const QByteArray role = it.key(); const QVariant oldValue = it.value(); if (newData.contains(role) && newData.value(role) != oldValue) { changedRoles.insert(role); } } m_indexesForItems.remove(oldItem); delete oldItem; oldItem = nullptr; m_items[index] = item; m_indexesForItems.insert(item, index); onItemChanged(index, changedRoles); emit itemsChanged(KItemRangeList() << KItemRange(index, 1), changedRoles); } void KStandardItemModel::removeItem(int index) { if (index >= 0 && index < count()) { KStandardItem* item = m_items[index]; m_indexesForItems.remove(item); m_items.removeAt(index); // Removing an item requires to update the indexes // afterwards from m_indexesForItems. for (int i = index; i < m_items.count(); ++i) { m_indexesForItems.insert(m_items[i], i); } onItemRemoved(index, item); item->deleteLater(); item = nullptr; emit itemsRemoved(KItemRangeList() << KItemRange(index, 1)); // TODO: no hierarchical items are handled yet } } void KStandardItemModel::clear() { int size = m_items.size(); m_items.clear(); m_indexesForItems.clear(); emit itemsRemoved(KItemRangeList() << KItemRange(0, size)); } KStandardItem* KStandardItemModel::item(int index) const { if (index < 0 || index >= m_items.count()) { return nullptr; } return m_items[index]; } int KStandardItemModel::index(const KStandardItem* item) const { return m_indexesForItems.value(item, -1); } void KStandardItemModel::appendItem(KStandardItem *item) { insertItem(m_items.count(), item); } int KStandardItemModel::count() const { return m_items.count(); } QHash KStandardItemModel::data(int index) const { if (index >= 0 && index < count()) { const KStandardItem* item = m_items[index]; if (item) { return item->data(); } } return QHash(); } bool KStandardItemModel::setData(int index, const QHash& values) { - Q_UNUSED(values); + Q_UNUSED(values) if (index < 0 || index >= count()) { return false; } return true; } QMimeData* KStandardItemModel::createMimeData(const KItemSet& indexes) const { - Q_UNUSED(indexes); + Q_UNUSED(indexes) return nullptr; } int KStandardItemModel::indexForKeyboardSearch(const QString& text, int startFromIndex) const { - Q_UNUSED(text); - Q_UNUSED(startFromIndex); + Q_UNUSED(text) + Q_UNUSED(startFromIndex) return -1; } bool KStandardItemModel::supportsDropping(int index) const { - Q_UNUSED(index); + Q_UNUSED(index) return false; } QString KStandardItemModel::roleDescription(const QByteArray& role) const { - Q_UNUSED(role); + Q_UNUSED(role) return QString(); } QList > KStandardItemModel::groups() const { QList > groups; const QByteArray role = sortRole().isEmpty() ? "group" : sortRole(); bool isFirstGroupValue = true; QString groupValue; const int maxIndex = count() - 1; for (int i = 0; i <= maxIndex; ++i) { const QString newGroupValue = m_items.at(i)->dataValue(role).toString(); if (newGroupValue != groupValue || isFirstGroupValue) { groupValue = newGroupValue; groups.append(QPair(i, newGroupValue)); isFirstGroupValue = false; } } return groups; } void KStandardItemModel::onItemInserted(int index) { - Q_UNUSED(index); + Q_UNUSED(index) } void KStandardItemModel::onItemChanged(int index, const QSet& changedRoles) { - Q_UNUSED(index); - Q_UNUSED(changedRoles); + Q_UNUSED(index) + Q_UNUSED(changedRoles) } void KStandardItemModel::onItemRemoved(int index, KStandardItem* removedItem) { - Q_UNUSED(index); - Q_UNUSED(removedItem); + Q_UNUSED(index) + Q_UNUSED(removedItem) } diff --git a/src/kitemviews/private/kitemlistheaderwidget.cpp b/src/kitemviews/private/kitemlistheaderwidget.cpp index 93d1389aa..9263a7de2 100644 --- a/src/kitemviews/private/kitemlistheaderwidget.cpp +++ b/src/kitemviews/private/kitemlistheaderwidget.cpp @@ -1,575 +1,575 @@ /*************************************************************************** * Copyright (C) 2011 by Peter Penz * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kitemlistheaderwidget.h" #include "kitemviews/kitemmodelbase.h" #include #include #include #include KItemListHeaderWidget::KItemListHeaderWidget(QGraphicsWidget* parent) : QGraphicsWidget(parent), m_automaticColumnResizing(true), m_model(nullptr), m_offset(0), m_columns(), m_columnWidths(), m_preferredColumnWidths(), m_hoveredRoleIndex(-1), m_pressedRoleIndex(-1), m_roleOperation(NoRoleOperation), m_pressedMousePos(), m_movingRole() { m_movingRole.x = 0; m_movingRole.xDec = 0; m_movingRole.index = -1; setAcceptHoverEvents(true); } KItemListHeaderWidget::~KItemListHeaderWidget() { } void KItemListHeaderWidget::setModel(KItemModelBase* model) { if (m_model == model) { return; } if (m_model) { disconnect(m_model, &KItemModelBase::sortRoleChanged, this, &KItemListHeaderWidget::slotSortRoleChanged); disconnect(m_model, &KItemModelBase::sortOrderChanged, this, &KItemListHeaderWidget::slotSortOrderChanged); } m_model = model; if (m_model) { connect(m_model, &KItemModelBase::sortRoleChanged, this, &KItemListHeaderWidget::slotSortRoleChanged); connect(m_model, &KItemModelBase::sortOrderChanged, this, &KItemListHeaderWidget::slotSortOrderChanged); } } KItemModelBase* KItemListHeaderWidget::model() const { return m_model; } void KItemListHeaderWidget::setAutomaticColumnResizing(bool automatic) { m_automaticColumnResizing = automatic; } bool KItemListHeaderWidget::automaticColumnResizing() const { return m_automaticColumnResizing; } void KItemListHeaderWidget::setColumns(const QList& roles) { foreach (const QByteArray& role, roles) { if (!m_columnWidths.contains(role)) { m_preferredColumnWidths.remove(role); } } m_columns = roles; update(); } QList KItemListHeaderWidget::columns() const { return m_columns; } void KItemListHeaderWidget::setColumnWidth(const QByteArray& role, qreal width) { const qreal minWidth = minimumColumnWidth(); if (width < minWidth) { width = minWidth; } if (m_columnWidths.value(role) != width) { m_columnWidths.insert(role, width); update(); } } qreal KItemListHeaderWidget::columnWidth(const QByteArray& role) const { return m_columnWidths.value(role); } void KItemListHeaderWidget::setPreferredColumnWidth(const QByteArray& role, qreal width) { m_preferredColumnWidths.insert(role, width); } qreal KItemListHeaderWidget::preferredColumnWidth(const QByteArray& role) const { return m_preferredColumnWidths.value(role); } void KItemListHeaderWidget::setOffset(qreal offset) { if (m_offset != offset) { m_offset = offset; update(); } } qreal KItemListHeaderWidget::offset() const { return m_offset; } qreal KItemListHeaderWidget::minimumColumnWidth() const { QFontMetricsF fontMetrics(font()); return fontMetrics.height() * 4; } void KItemListHeaderWidget::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) { - Q_UNUSED(option); - Q_UNUSED(widget); + Q_UNUSED(option) + Q_UNUSED(widget) if (!m_model) { return; } // Draw roles painter->setFont(font()); painter->setPen(palette().text().color()); qreal x = -m_offset; int orderIndex = 0; foreach (const QByteArray& role, m_columns) { const qreal roleWidth = m_columnWidths.value(role); const QRectF rect(x, 0, roleWidth, size().height()); paintRole(painter, role, rect, orderIndex, widget); x += roleWidth; ++orderIndex; } if (!m_movingRole.pixmap.isNull()) { Q_ASSERT(m_roleOperation == MoveRoleOperation); painter->drawPixmap(m_movingRole.x, 0, m_movingRole.pixmap); } } void KItemListHeaderWidget::mousePressEvent(QGraphicsSceneMouseEvent* event) { if (event->button() & Qt::LeftButton) { updatePressedRoleIndex(event->pos()); m_pressedMousePos = event->pos(); m_roleOperation = isAboveRoleGrip(m_pressedMousePos, m_pressedRoleIndex) ? ResizeRoleOperation : NoRoleOperation; event->accept(); } else { event->ignore(); } } void KItemListHeaderWidget::mouseReleaseEvent(QGraphicsSceneMouseEvent* event) { QGraphicsWidget::mouseReleaseEvent(event); if (m_pressedRoleIndex == -1) { return; } switch (m_roleOperation) { case NoRoleOperation: { // Only a click has been done and no moving or resizing has been started const QByteArray sortRole = m_model->sortRole(); const int sortRoleIndex = m_columns.indexOf(sortRole); if (m_pressedRoleIndex == sortRoleIndex) { // Toggle the sort order const Qt::SortOrder previous = m_model->sortOrder(); const Qt::SortOrder current = (m_model->sortOrder() == Qt::AscendingOrder) ? Qt::DescendingOrder : Qt::AscendingOrder; m_model->setSortOrder(current); emit sortOrderChanged(current, previous); } else { // Change the sort role and reset to the ascending order const QByteArray previous = m_model->sortRole(); const QByteArray current = m_columns[m_pressedRoleIndex]; const bool resetSortOrder = m_model->sortOrder() == Qt::DescendingOrder; m_model->setSortRole(current, !resetSortOrder); emit sortRoleChanged(current, previous); if (resetSortOrder) { m_model->setSortOrder(Qt::AscendingOrder); emit sortOrderChanged(Qt::AscendingOrder, Qt::DescendingOrder); } } break; } case ResizeRoleOperation: { const QByteArray pressedRole = m_columns[m_pressedRoleIndex]; const qreal currentWidth = m_columnWidths.value(pressedRole); emit columnWidthChangeFinished(pressedRole, currentWidth); break; } case MoveRoleOperation: m_movingRole.pixmap = QPixmap(); m_movingRole.x = 0; m_movingRole.xDec = 0; m_movingRole.index = -1; break; default: break; } m_pressedRoleIndex = -1; m_roleOperation = NoRoleOperation; update(); QApplication::restoreOverrideCursor(); } void KItemListHeaderWidget::mouseMoveEvent(QGraphicsSceneMouseEvent* event) { QGraphicsWidget::mouseMoveEvent(event); switch (m_roleOperation) { case NoRoleOperation: if ((event->pos() - m_pressedMousePos).manhattanLength() >= QApplication::startDragDistance()) { // A role gets dragged by the user. Create a pixmap of the role that will get // synchronized on each furter mouse-move-event with the mouse-position. m_roleOperation = MoveRoleOperation; const int roleIndex = roleIndexAt(m_pressedMousePos); m_movingRole.index = roleIndex; if (roleIndex == 0) { // TODO: It should be configurable whether moving the first role is allowed. // In the context of Dolphin this is not required, however this should be // changed if KItemViews are used in a more generic way. QApplication::setOverrideCursor(QCursor(Qt::ForbiddenCursor)); } else { m_movingRole.pixmap = createRolePixmap(roleIndex); qreal roleX = -m_offset; for (int i = 0; i < roleIndex; ++i) { const QByteArray role = m_columns[i]; roleX += m_columnWidths.value(role); } m_movingRole.xDec = event->pos().x() - roleX; m_movingRole.x = roleX; update(); } } break; case ResizeRoleOperation: { const QByteArray pressedRole = m_columns[m_pressedRoleIndex]; qreal previousWidth = m_columnWidths.value(pressedRole); qreal currentWidth = previousWidth; currentWidth += event->pos().x() - event->lastPos().x(); currentWidth = qMax(minimumColumnWidth(), currentWidth); m_columnWidths.insert(pressedRole, currentWidth); update(); emit columnWidthChanged(pressedRole, currentWidth, previousWidth); break; } case MoveRoleOperation: { // TODO: It should be configurable whether moving the first role is allowed. // In the context of Dolphin this is not required, however this should be // changed if KItemViews are used in a more generic way. if (m_movingRole.index > 0) { m_movingRole.x = event->pos().x() - m_movingRole.xDec; update(); const int targetIndex = targetOfMovingRole(); if (targetIndex > 0 && targetIndex != m_movingRole.index) { const QByteArray role = m_columns[m_movingRole.index]; const int previousIndex = m_movingRole.index; m_movingRole.index = targetIndex; emit columnMoved(role, targetIndex, previousIndex); m_movingRole.xDec = event->pos().x() - roleXPosition(role); } } break; } default: break; } } void KItemListHeaderWidget::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* event) { QGraphicsItem::mouseDoubleClickEvent(event); const int roleIndex = roleIndexAt(event->pos()); if (roleIndex >= 0 && isAboveRoleGrip(event->pos(), roleIndex)) { const QByteArray role = m_columns.at(roleIndex); qreal previousWidth = columnWidth(role); setColumnWidth(role, preferredColumnWidth(role)); qreal currentWidth = columnWidth(role); emit columnWidthChanged(role, currentWidth, previousWidth); emit columnWidthChangeFinished(role, currentWidth); } } void KItemListHeaderWidget::hoverEnterEvent(QGraphicsSceneHoverEvent* event) { QGraphicsWidget::hoverEnterEvent(event); updateHoveredRoleIndex(event->pos()); } void KItemListHeaderWidget::hoverLeaveEvent(QGraphicsSceneHoverEvent* event) { QGraphicsWidget::hoverLeaveEvent(event); if (m_hoveredRoleIndex != -1) { m_hoveredRoleIndex = -1; update(); } } void KItemListHeaderWidget::hoverMoveEvent(QGraphicsSceneHoverEvent* event) { QGraphicsWidget::hoverMoveEvent(event); const QPointF& pos = event->pos(); updateHoveredRoleIndex(pos); if (m_hoveredRoleIndex >= 0 && isAboveRoleGrip(pos, m_hoveredRoleIndex)) { setCursor(Qt::SplitHCursor); } else { unsetCursor(); } } void KItemListHeaderWidget::slotSortRoleChanged(const QByteArray& current, const QByteArray& previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) update(); } void KItemListHeaderWidget::slotSortOrderChanged(Qt::SortOrder current, Qt::SortOrder previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) update(); } void KItemListHeaderWidget::paintRole(QPainter* painter, const QByteArray& role, const QRectF& rect, int orderIndex, QWidget* widget) const { // The following code is based on the code from QHeaderView::paintSection(). // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). QStyleOptionHeader option; option.section = orderIndex; option.state = QStyle::State_None | QStyle::State_Raised | QStyle::State_Horizontal; if (isEnabled()) { option.state |= QStyle::State_Enabled; } if (window() && window()->isActiveWindow()) { option.state |= QStyle::State_Active; } if (m_hoveredRoleIndex == orderIndex) { option.state |= QStyle::State_MouseOver; } if (m_pressedRoleIndex == orderIndex) { option.state |= QStyle::State_Sunken; } if (m_model->sortRole() == role) { option.sortIndicator = (m_model->sortOrder() == Qt::AscendingOrder) ? QStyleOptionHeader::SortDown : QStyleOptionHeader::SortUp; } option.rect = rect.toRect(); bool paintBackgroundForEmptyArea = false; if (m_columns.count() == 1) { option.position = QStyleOptionHeader::OnlyOneSection; } else if (orderIndex == 0) { option.position = QStyleOptionHeader::Beginning; } else if (orderIndex == m_columns.count() - 1) { // We are just painting the header for the last column. Check if there // is some empty space to the right which needs to be filled. if (rect.right() < size().width()) { option.position = QStyleOptionHeader::Middle; paintBackgroundForEmptyArea = true; } else { option.position = QStyleOptionHeader::End; } } else { option.position = QStyleOptionHeader::Middle; } option.orientation = Qt::Horizontal; option.selectedPosition = QStyleOptionHeader::NotAdjacent; option.text = m_model->roleDescription(role); style()->drawControl(QStyle::CE_Header, &option, painter, widget); if (paintBackgroundForEmptyArea) { option.state = QStyle::State_None | QStyle::State_Raised | QStyle::State_Horizontal; option.section = m_columns.count(); option.sortIndicator = QStyleOptionHeader::None; qreal backgroundRectX = rect.x() + rect.width(); QRectF backgroundRect(backgroundRectX, 0.0, size().width() - backgroundRectX, rect.height()); option.rect = backgroundRect.toRect(); option.position = QStyleOptionHeader::End; option.text = QString(); style()->drawControl(QStyle::CE_Header, &option, painter, widget); } } void KItemListHeaderWidget::updatePressedRoleIndex(const QPointF& pos) { const int pressedIndex = roleIndexAt(pos); if (m_pressedRoleIndex != pressedIndex) { m_pressedRoleIndex = pressedIndex; update(); } } void KItemListHeaderWidget::updateHoveredRoleIndex(const QPointF& pos) { const int hoverIndex = roleIndexAt(pos); if (m_hoveredRoleIndex != hoverIndex) { m_hoveredRoleIndex = hoverIndex; update(); } } int KItemListHeaderWidget::roleIndexAt(const QPointF& pos) const { int index = -1; qreal x = -m_offset; foreach (const QByteArray& role, m_columns) { ++index; x += m_columnWidths.value(role); if (pos.x() <= x) { break; } } return index; } bool KItemListHeaderWidget::isAboveRoleGrip(const QPointF& pos, int roleIndex) const { qreal x = -m_offset; for (int i = 0; i <= roleIndex; ++i) { const QByteArray role = m_columns[i]; x += m_columnWidths.value(role); } const int grip = style()->pixelMetric(QStyle::PM_HeaderGripMargin); return pos.x() >= (x - grip) && pos.x() <= x; } QPixmap KItemListHeaderWidget::createRolePixmap(int roleIndex) const { const QByteArray role = m_columns[roleIndex]; const qreal roleWidth = m_columnWidths.value(role); const QRect rect(0, 0, roleWidth, size().height()); QImage image(rect.size(), QImage::Format_ARGB32_Premultiplied); QPainter painter(&image); paintRole(&painter, role, rect, roleIndex); // Apply a highlighting-color const QPalette::ColorGroup group = isActiveWindow() ? QPalette::Active : QPalette::Inactive; QColor highlightColor = palette().color(group, QPalette::Highlight); highlightColor.setAlpha(64); painter.fillRect(rect, highlightColor); // Make the image transparent painter.setCompositionMode(QPainter::CompositionMode_DestinationIn); painter.fillRect(0, 0, image.width(), image.height(), QColor(0, 0, 0, 192)); return QPixmap::fromImage(image); } int KItemListHeaderWidget::targetOfMovingRole() const { const int movingWidth = m_movingRole.pixmap.width(); const int movingLeft = m_movingRole.x; const int movingRight = movingLeft + movingWidth - 1; int targetIndex = 0; qreal targetLeft = -m_offset; while (targetIndex < m_columns.count()) { const QByteArray role = m_columns[targetIndex]; const qreal targetWidth = m_columnWidths.value(role); const qreal targetRight = targetLeft + targetWidth - 1; const bool isInTarget = (targetWidth >= movingWidth && movingLeft >= targetLeft && movingRight <= targetRight) || (targetWidth < movingWidth && movingLeft <= targetLeft && movingRight >= targetRight); if (isInTarget) { return targetIndex; } targetLeft += targetWidth; ++targetIndex; } return m_movingRole.index; } qreal KItemListHeaderWidget::roleXPosition(const QByteArray& role) const { qreal x = -m_offset; foreach (const QByteArray& visibleRole, m_columns) { if (visibleRole == role) { return x; } x += m_columnWidths.value(visibleRole); } return -1; } diff --git a/src/kitemviews/private/kitemlistkeyboardsearchmanager.cpp b/src/kitemviews/private/kitemlistkeyboardsearchmanager.cpp index 09b4eaf23..5256f69ca 100644 --- a/src/kitemviews/private/kitemlistkeyboardsearchmanager.cpp +++ b/src/kitemviews/private/kitemlistkeyboardsearchmanager.cpp @@ -1,112 +1,112 @@ /*************************************************************************** * Copyright (C) 2011 by Tirtha Chatterjee * * * * Based on the Itemviews NG project from Trolltech Labs: * * http://qt.gitorious.org/qt-labs/itemviews-ng * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kitemlistkeyboardsearchmanager.h" KItemListKeyboardSearchManager::KItemListKeyboardSearchManager(QObject* parent) : QObject(parent), m_isSearchRestarted(false), m_timeout(1000) { m_keyboardInputTime.invalidate(); } KItemListKeyboardSearchManager::~KItemListKeyboardSearchManager() { } bool KItemListKeyboardSearchManager::shouldClearSearchIfInputTimeReached() { const bool keyboardTimeWasValid = m_keyboardInputTime.isValid(); const qint64 keyboardInputTimeElapsed = m_keyboardInputTime.restart(); return (keyboardInputTimeElapsed > m_timeout) || !keyboardTimeWasValid; } void KItemListKeyboardSearchManager::addKeys(const QString& keys) { if (shouldClearSearchIfInputTimeReached()) { m_searchedString.clear(); } const bool newSearch = m_searchedString.isEmpty(); // Do not start a new search if the user pressed Space. Only add // it to the search string if a search is in progress already. if (newSearch && keys == QLatin1Char(' ')) { return; } if (!keys.isEmpty()) { m_searchedString.append(keys); // Special case: // If the same key is pressed repeatedly, the next item matching that key should be highlighted const QChar firstKey = m_searchedString.length() > 0 ? m_searchedString.at(0) : QChar(); const bool sameKey = m_searchedString.length() > 1 && m_searchedString.count(firstKey) == m_searchedString.length(); // Searching for a matching item should start from the next item if either // 1. a new search is started and a search has not been restarted or // 2. a 'repeated key' search is done. const bool searchFromNextItem = (!m_isSearchRestarted && newSearch) || sameKey; // to remember not to searchFromNextItem if selection was deselected // loosing keyboard search context basically m_isSearchRestarted = false; emit changeCurrentItem(sameKey ? firstKey : m_searchedString, searchFromNextItem); } m_keyboardInputTime.start(); } void KItemListKeyboardSearchManager::setTimeout(qint64 milliseconds) { m_timeout = milliseconds; } qint64 KItemListKeyboardSearchManager::timeout() const { return m_timeout; } void KItemListKeyboardSearchManager::cancelSearch() { m_isSearchRestarted = true; m_searchedString.clear(); } void KItemListKeyboardSearchManager::slotCurrentChanged(int current, int previous) { - Q_UNUSED(previous); + Q_UNUSED(previous) if (current < 0) { // The current item has been removed. We should cancel the search. cancelSearch(); } } void KItemListKeyboardSearchManager::slotSelectionChanged(const KItemSet& current, const KItemSet& previous) { if (!previous.isEmpty() && current.isEmpty() && previous.count() > 0 && current.count() == 0) { // The selection has been emptied. We should cancel the search. cancelSearch(); } } diff --git a/src/kitemviews/private/kitemlistselectiontoggle.cpp b/src/kitemviews/private/kitemlistselectiontoggle.cpp index 5519e4da8..6bdd18da5 100644 --- a/src/kitemviews/private/kitemlistselectiontoggle.cpp +++ b/src/kitemviews/private/kitemlistselectiontoggle.cpp @@ -1,115 +1,115 @@ /*************************************************************************** * Copyright (C) 2011 by Peter Penz * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kitemlistselectiontoggle.h" #include #include #include KItemListSelectionToggle::KItemListSelectionToggle(QGraphicsItem* parent) : QGraphicsWidget(parent, nullptr), m_checked(false), m_hovered(false) { } KItemListSelectionToggle::~KItemListSelectionToggle() { } void KItemListSelectionToggle::setChecked(bool checked) { if (m_checked != checked) { m_checked = checked; m_pixmap = QPixmap(); update(); } } bool KItemListSelectionToggle::isChecked() const { return m_checked; } void KItemListSelectionToggle::setHovered(bool hovered) { if (m_hovered != hovered) { m_hovered = hovered; m_pixmap = QPixmap(); update(); } } void KItemListSelectionToggle::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) { - Q_UNUSED(option); - Q_UNUSED(widget); + Q_UNUSED(option) + Q_UNUSED(widget) if (m_pixmap.isNull()) { updatePixmap(); } const qreal x = (size().width() - qreal(m_pixmap.width() / m_pixmap.devicePixelRatioF())) / 2; const qreal y = (size().height() - qreal(m_pixmap.height() / m_pixmap.devicePixelRatioF())) / 2; painter->drawPixmap(x, y, m_pixmap); } void KItemListSelectionToggle::resizeEvent(QGraphicsSceneResizeEvent* event) { QGraphicsWidget::resizeEvent(event); if (!m_pixmap.isNull()) { const int pixmapSize = m_pixmap.size().width() / m_pixmap.devicePixelRatioF(); // Pixmap width is always equal pixmap height if (pixmapSize != iconSize()) { // If the required icon size is different from the actual pixmap size, // overwrite the m_pixmap with an empty pixmap and reload the new // icon on next re-painting. m_pixmap = QPixmap(); } } } void KItemListSelectionToggle::updatePixmap() { const QString icon = m_checked ? QStringLiteral("emblem-remove") : QStringLiteral("emblem-added"); m_pixmap = QIcon::fromTheme(icon).pixmap(iconSize(), m_hovered ? QIcon::Active : QIcon::Disabled); } int KItemListSelectionToggle::iconSize() const { const int iconSize = qMin(size().width(), size().height()); if (iconSize < KIconLoader::SizeSmallMedium) { return KIconLoader::SizeSmall; } else if (iconSize < KIconLoader::SizeMedium) { return KIconLoader::SizeSmallMedium; } else if (iconSize < KIconLoader::SizeLarge) { return KIconLoader::SizeMedium; } else if (iconSize < KIconLoader::SizeHuge) { return KIconLoader::SizeLarge; } else if (iconSize < KIconLoader::SizeEnormous) { return KIconLoader::SizeHuge; } return iconSize; } diff --git a/src/kitemviews/private/kitemlistsizehintresolver.cpp b/src/kitemviews/private/kitemlistsizehintresolver.cpp index 4dee67396..c53bb2a4a 100644 --- a/src/kitemviews/private/kitemlistsizehintresolver.cpp +++ b/src/kitemviews/private/kitemlistsizehintresolver.cpp @@ -1,174 +1,174 @@ /*************************************************************************** * Copyright (C) 2011 by Peter Penz * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kitemlistsizehintresolver.h" #include "kitemviews/kitemlistview.h" KItemListSizeHintResolver::KItemListSizeHintResolver(const KItemListView* itemListView) : m_itemListView(itemListView), m_logicalHeightHintCache(), m_logicalWidthHint(0.0), m_logicalHeightHint(0.0), m_minHeightHint(0.0), m_needsResolving(false) { } KItemListSizeHintResolver::~KItemListSizeHintResolver() { } QSizeF KItemListSizeHintResolver::maxSizeHint() { updateCache(); return QSizeF(m_logicalWidthHint, m_logicalHeightHint); } QSizeF KItemListSizeHintResolver::minSizeHint() { updateCache(); return QSizeF(m_logicalWidthHint, m_minHeightHint); } QSizeF KItemListSizeHintResolver::sizeHint(int index) { updateCache(); return QSizeF(m_logicalWidthHint, m_logicalHeightHintCache.at(index)); } void KItemListSizeHintResolver::itemsInserted(const KItemRangeList& itemRanges) { int insertedCount = 0; foreach (const KItemRange& range, itemRanges) { insertedCount += range.count; } const int currentCount = m_logicalHeightHintCache.count(); m_logicalHeightHintCache.reserve(currentCount + insertedCount); // We build the new list from the end to the beginning to mimize the // number of moves. m_logicalHeightHintCache.insert(m_logicalHeightHintCache.end(), insertedCount, 0.0); int sourceIndex = currentCount - 1; int targetIndex = m_logicalHeightHintCache.count() - 1; int itemsToInsertBeforeCurrentRange = insertedCount; for (int rangeIndex = itemRanges.count() - 1; rangeIndex >= 0; --rangeIndex) { const KItemRange& range = itemRanges.at(rangeIndex); itemsToInsertBeforeCurrentRange -= range.count; // First: move all existing items that must be put behind 'range'. while (targetIndex >= itemsToInsertBeforeCurrentRange + range.index + range.count) { m_logicalHeightHintCache[targetIndex] = m_logicalHeightHintCache[sourceIndex]; --sourceIndex; --targetIndex; } // Then: insert QSizeF() for the items which are inserted into 'range'. while (targetIndex >= itemsToInsertBeforeCurrentRange + range.index) { m_logicalHeightHintCache[targetIndex] = 0.0; --targetIndex; } } m_needsResolving = true; Q_ASSERT(m_logicalHeightHintCache.count() == m_itemListView->model()->count()); } void KItemListSizeHintResolver::itemsRemoved(const KItemRangeList& itemRanges) { const QVector::iterator begin = m_logicalHeightHintCache.begin(); const QVector::iterator end = m_logicalHeightHintCache.end(); KItemRangeList::const_iterator rangeIt = itemRanges.constBegin(); const KItemRangeList::const_iterator rangeEnd = itemRanges.constEnd(); QVector::iterator destIt = begin + rangeIt->index; QVector::iterator srcIt = destIt + rangeIt->count; ++rangeIt; while (srcIt != end) { *destIt = *srcIt; ++destIt; ++srcIt; if (rangeIt != rangeEnd && srcIt == begin + rangeIt->index) { // Skip the items in the next removed range. srcIt += rangeIt->count; ++rangeIt; } } m_logicalHeightHintCache.erase(destIt, end); // Note that the cache size might temporarily not match the model size if // this function is called from KItemListView::setModel() to empty the cache. if (!m_logicalHeightHintCache.isEmpty() && m_itemListView->model()) { Q_ASSERT(m_logicalHeightHintCache.count() == m_itemListView->model()->count()); } } void KItemListSizeHintResolver::itemsMoved(const KItemRange& range, const QList& movedToIndexes) { QVector newLogicalHeightHintCache(m_logicalHeightHintCache); const int movedRangeEnd = range.index + range.count; for (int i = range.index; i < movedRangeEnd; ++i) { const int newIndex = movedToIndexes.at(i - range.index); newLogicalHeightHintCache[newIndex] = m_logicalHeightHintCache.at(i); } m_logicalHeightHintCache = newLogicalHeightHintCache; } void KItemListSizeHintResolver::itemsChanged(int index, int count, const QSet& roles) { - Q_UNUSED(roles); + Q_UNUSED(roles) while (count) { m_logicalHeightHintCache[index] = 0.0; ++index; --count; } m_needsResolving = true; } void KItemListSizeHintResolver::clearCache() { m_logicalHeightHintCache.fill(0.0); m_needsResolving = true; } void KItemListSizeHintResolver::updateCache() { if (m_needsResolving) { m_itemListView->calculateItemSizeHints(m_logicalHeightHintCache, m_logicalWidthHint); // Set logical height as the max cached height (if the cache is not empty). if (m_logicalHeightHintCache.isEmpty()) { m_logicalHeightHint = 0.0; } else { m_logicalHeightHint = *std::max_element(m_logicalHeightHintCache.begin(), m_logicalHeightHintCache.end()); m_minHeightHint = *std::min_element(m_logicalHeightHintCache.begin(), m_logicalHeightHintCache.end()); } m_needsResolving = false; } } diff --git a/src/kitemviews/private/kitemlistsmoothscroller.cpp b/src/kitemviews/private/kitemlistsmoothscroller.cpp index 4f6fd3015..cdb7f255b 100644 --- a/src/kitemviews/private/kitemlistsmoothscroller.cpp +++ b/src/kitemviews/private/kitemlistsmoothscroller.cpp @@ -1,210 +1,210 @@ /*************************************************************************** * Copyright (C) 2011 by Peter Penz * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kitemlistsmoothscroller.h" #include #include #include #include #include KItemListSmoothScroller::KItemListSmoothScroller(QScrollBar* scrollBar, QObject* parent) : QObject(parent), m_scrollBarPressed(false), m_smoothScrolling(true), m_scrollBar(scrollBar), m_animation(nullptr) { m_animation = new QPropertyAnimation(this); #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) const int animationDuration = m_scrollBar->style()->styleHint(QStyle::SH_Widget_Animation_Duration, nullptr, m_scrollBar); const bool animationEnabled = (animationDuration > 0); #else const int animationDuration = 100; const bool animationEnabled = m_scrollBar->style()->styleHint(QStyle::SH_Widget_Animate, nullptr, m_scrollBar); #endif m_animation->setDuration(animationEnabled ? animationDuration : 1); connect(m_animation, &QPropertyAnimation::stateChanged, this, &KItemListSmoothScroller::slotAnimationStateChanged); m_scrollBar->installEventFilter(this); } KItemListSmoothScroller::~KItemListSmoothScroller() { } void KItemListSmoothScroller::setScrollBar(QScrollBar *scrollBar) { m_scrollBar = scrollBar; } QScrollBar* KItemListSmoothScroller::scrollBar() const { return m_scrollBar; } void KItemListSmoothScroller::setTargetObject(QObject* target) { m_animation->setTargetObject(target); } QObject* KItemListSmoothScroller::targetObject() const { return m_animation->targetObject(); } void KItemListSmoothScroller::setPropertyName(const QByteArray& propertyName) { m_animation->setPropertyName(propertyName); } QByteArray KItemListSmoothScroller::propertyName() const { return m_animation->propertyName(); } void KItemListSmoothScroller::scrollContentsBy(qreal distance) { QObject* target = targetObject(); if (!target) { return; } const QByteArray name = propertyName(); const qreal currentOffset = target->property(name).toReal(); if (static_cast(currentOffset) == m_scrollBar->value()) { // The current offset is already synchronous to the scrollbar return; } const bool animRunning = (m_animation->state() == QAbstractAnimation::Running); if (animRunning) { // Stopping a running animation means skipping the range from the current offset // until the target offset. To prevent skipping of the range the difference // is added to the new target offset. const qreal oldEndOffset = m_animation->endValue().toReal(); distance += (currentOffset - oldEndOffset); } const qreal endOffset = currentOffset - distance; if (m_smoothScrolling || animRunning) { qreal startOffset = currentOffset; if (animRunning) { // If the animation was running and has been interrupted by assigning a new end-offset // one frame must be added to the start-offset to keep the animation smooth. This also // assures that animation proceeds even in cases where new end-offset are triggered // within a very short timeslots. startOffset += (endOffset - currentOffset) * 1000 / (m_animation->duration() * 60); if (currentOffset < endOffset) { startOffset = qMin(startOffset, endOffset); } else { startOffset = qMax(startOffset, endOffset); } } m_animation->stop(); m_animation->setStartValue(startOffset); m_animation->setEndValue(endOffset); m_animation->setEasingCurve(animRunning ? QEasingCurve::OutQuad : QEasingCurve::InOutQuad); m_animation->start(); target->setProperty(name, startOffset); } else { target->setProperty(name, endOffset); } } void KItemListSmoothScroller::scrollTo(qreal position) { int newValue = position; newValue = qBound(0, newValue, m_scrollBar->maximum()); if (newValue != m_scrollBar->value()) { m_smoothScrolling = true; m_scrollBar->setValue(newValue); } } bool KItemListSmoothScroller::requestScrollBarUpdate(int newMaximum) { if (m_animation->state() == QAbstractAnimation::Running) { if (newMaximum == m_scrollBar->maximum()) { // The value has been changed by the animation, no update // of the scrollbars is required as their target state will be // reached with the end of the animation. return false; } // The maximum has been changed which indicates that the content // of the view has been changed. Stop the animation in any case and // update the scrollbars immediately. m_animation->stop(); } return true; } bool KItemListSmoothScroller::eventFilter(QObject* obj, QEvent* event) { Q_ASSERT(obj == m_scrollBar); switch (event->type()) { case QEvent::MouseButtonPress: m_scrollBarPressed = true; m_smoothScrolling = true; break; case QEvent::MouseButtonRelease: m_scrollBarPressed = false; m_smoothScrolling = false; break; case QEvent::Wheel: return false; // we're the ones sending them default: break; } return QObject::eventFilter(obj, event); } void KItemListSmoothScroller::slotAnimationStateChanged(QAbstractAnimation::State newState, QAbstractAnimation::State oldState) { - Q_UNUSED(oldState); + Q_UNUSED(oldState) if (newState == QAbstractAnimation::Stopped && m_smoothScrolling && !m_scrollBarPressed) { m_smoothScrolling = false; } } void KItemListSmoothScroller::handleWheelEvent(QWheelEvent* event) { const bool previous = m_smoothScrolling; m_smoothScrolling = true; QWheelEvent copy = *event; QApplication::sendEvent(m_scrollBar, ©); event->setAccepted(copy.isAccepted()); m_smoothScrolling = previous; } diff --git a/src/panels/places/placesitem.cpp b/src/panels/places/placesitem.cpp index 3d3645ea2..b06bac9a6 100644 --- a/src/panels/places/placesitem.cpp +++ b/src/panels/places/placesitem.cpp @@ -1,284 +1,284 @@ /*************************************************************************** * Copyright (C) 2012 by Peter Penz * * Copyright (C) 2018 by Elvis Angelaccio * * * * Based on KFilePlacesItem from kdelibs: * * Copyright (C) 2007 Kevin Ottens * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "placesitem.h" #include "trash/dolphintrash.h" #include "dolphindebug.h" #include "placesitemsignalhandler.h" #include #include #include PlacesItem::PlacesItem(const KBookmark& bookmark, PlacesItem* parent) : KStandardItem(parent), m_device(), m_access(), m_volume(), m_disc(), m_mtp(), m_signalHandler(nullptr), m_bookmark() { m_signalHandler = new PlacesItemSignalHandler(this); setBookmark(bookmark); } PlacesItem::~PlacesItem() { delete m_signalHandler; } void PlacesItem::setUrl(const QUrl &url) { // The default check in KStandardItem::setDataValue() // for equal values does not work with a custom value // like QUrl. Hence do a manual check to prevent that // setting an equal URL results in an itemsChanged() // signal. if (dataValue("url").toUrl() != url) { if (url.scheme() == QLatin1String("trash")) { QObject::connect(&Trash::instance(), &Trash::emptinessChanged, m_signalHandler.data(), &PlacesItemSignalHandler::onTrashEmptinessChanged); } setDataValue("url", url); } } QUrl PlacesItem::url() const { return dataValue("url").toUrl(); } void PlacesItem::setUdi(const QString& udi) { setDataValue("udi", udi); } QString PlacesItem::udi() const { return dataValue("udi").toString(); } void PlacesItem::setApplicationName(const QString &applicationName) { setDataValue("applicationName", applicationName); } QString PlacesItem::applicationName() const { return dataValue("applicationName").toString(); } void PlacesItem::setHidden(bool hidden) { setDataValue("isHidden", hidden); } bool PlacesItem::isHidden() const { return dataValue("isHidden").toBool(); } bool PlacesItem::isGroupHidden() const { return dataValue("isGroupHidden").toBool(); } void PlacesItem::setGroupHidden(bool hidden) { setDataValue("isGroupHidden", hidden); } void PlacesItem::setSystemItem(bool isSystemItem) { setDataValue("isSystemItem", isSystemItem); } bool PlacesItem::isSystemItem() const { return dataValue("isSystemItem").toBool(); } Solid::Device PlacesItem::device() const { return m_device; } void PlacesItem::setBookmark(const KBookmark& bookmark) { const bool bookmarkDataChanged = !(bookmark == m_bookmark); // bookmark object must be updated to keep in sync with source model m_bookmark = bookmark; if (!bookmarkDataChanged) { return; } delete m_access; delete m_volume; delete m_disc; delete m_mtp; const QString udi = bookmark.metaDataItem(QStringLiteral("UDI")); if (udi.isEmpty()) { setIcon(bookmark.icon()); setText(i18ndc("kio5", "KFile System Bookmarks", bookmark.text().toUtf8().constData())); setUrl(bookmark.url()); setSystemItem(bookmark.metaDataItem(QStringLiteral("isSystemItem")) == QLatin1String("true")); } else { initializeDevice(udi); } setHidden(bookmark.metaDataItem(QStringLiteral("IsHidden")) == QLatin1String("true")); } KBookmark PlacesItem::bookmark() const { return m_bookmark; } bool PlacesItem::storageSetupNeeded() const { return m_access ? !m_access->isAccessible() : false; } bool PlacesItem::isSearchOrTimelineUrl() const { const QString urlScheme = url().scheme(); return (urlScheme.contains("search") || urlScheme.contains("timeline")); } void PlacesItem::onDataValueChanged(const QByteArray& role, const QVariant& current, const QVariant& previous) { - Q_UNUSED(current); - Q_UNUSED(previous); + Q_UNUSED(current) + Q_UNUSED(previous) if (!m_bookmark.isNull()) { updateBookmarkForRole(role); } } void PlacesItem::onDataChanged(const QHash& current, const QHash& previous) { - Q_UNUSED(previous); + Q_UNUSED(previous) if (!m_bookmark.isNull()) { QHashIterator it(current); while (it.hasNext()) { it.next(); updateBookmarkForRole(it.key()); } } } void PlacesItem::initializeDevice(const QString& udi) { m_device = Solid::Device(udi); if (!m_device.isValid()) { return; } m_access = m_device.as(); m_volume = m_device.as(); m_disc = m_device.as(); m_mtp = m_device.as(); setText(m_device.description()); setIcon(m_device.icon()); setIconOverlays(m_device.emblems()); setUdi(udi); if (m_access) { setUrl(QUrl::fromLocalFile(m_access->filePath())); QObject::connect(m_access.data(), &Solid::StorageAccess::accessibilityChanged, m_signalHandler.data(), &PlacesItemSignalHandler::onAccessibilityChanged); QObject::connect(m_access.data(), &Solid::StorageAccess::teardownRequested, m_signalHandler.data(), &PlacesItemSignalHandler::onTearDownRequested); } else if (m_disc && (m_disc->availableContent() & Solid::OpticalDisc::Audio) != 0) { Solid::Block *block = m_device.as(); if (block) { const QString device = block->device(); setUrl(QUrl(QStringLiteral("audiocd:/?device=%1").arg(device))); } else { setUrl(QUrl(QStringLiteral("audiocd:/"))); } } else if (m_mtp) { setUrl(QUrl(QStringLiteral("mtp:udi=%1").arg(m_device.udi()))); } } void PlacesItem::onAccessibilityChanged() { setIconOverlays(m_device.emblems()); setUrl(QUrl::fromLocalFile(m_access->filePath())); } void PlacesItem::updateBookmarkForRole(const QByteArray& role) { Q_ASSERT(!m_bookmark.isNull()); if (role == "iconName") { m_bookmark.setIcon(icon()); } else if (role == "text") { // Only store the text in the KBookmark if it is not the translation of // the current text. This makes sure that the text is re-translated if // the user chooses another language, or the translation itself changes. // // NOTE: It is important to use "KFile System Bookmarks" as context // (see PlacesItemModel::createSystemBookmarks()). if (text() != i18ndc("kio5", "KFile System Bookmarks", m_bookmark.text().toUtf8().data())) { m_bookmark.setFullText(text()); } } else if (role == "url") { m_bookmark.setUrl(url()); } else if (role == "udi") { m_bookmark.setMetaDataItem(QStringLiteral("UDI"), udi()); } else if (role == "applicationName") { m_bookmark.setMetaDataItem(QStringLiteral("OnlyInApp"), applicationName()); } else if (role == "isSystemItem") { m_bookmark.setMetaDataItem(QStringLiteral("isSystemItem"), isSystemItem() ? QStringLiteral("true") : QStringLiteral("false")); } else if (role == "isHidden") { m_bookmark.setMetaDataItem(QStringLiteral("IsHidden"), isHidden() ? QStringLiteral("true") : QStringLiteral("false")); } } QString PlacesItem::generateNewId() { // The ID-generation must be different as done in KFilePlacesItem from kdelibs // to prevent identical IDs, because 'count' is of course not shared. We append a // " (V2)" to indicate that the ID has been generated by // a new version of the places view. static int count = 0; return QString::number(QDateTime::currentDateTimeUtc().toTime_t()) + '/' + QString::number(count++) + " (V2)"; } PlacesItemSignalHandler *PlacesItem::signalHandler() const { return m_signalHandler.data(); } diff --git a/src/panels/places/placesitemlistgroupheader.cpp b/src/panels/places/placesitemlistgroupheader.cpp index e17fd4d66..884859d5b 100644 --- a/src/panels/places/placesitemlistgroupheader.cpp +++ b/src/panels/places/placesitemlistgroupheader.cpp @@ -1,44 +1,44 @@ /*************************************************************************** * Copyright (C) 2012 by Peter Penz * * * * Based on the Itemviews NG project from Trolltech Labs: * * http://qt.gitorious.org/qt-labs/itemviews-ng * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "placesitemlistgroupheader.h" PlacesItemListGroupHeader::PlacesItemListGroupHeader(QGraphicsWidget* parent) : KStandardItemListGroupHeader(parent) { } PlacesItemListGroupHeader::~PlacesItemListGroupHeader() { } void PlacesItemListGroupHeader::paintSeparator(QPainter* painter, const QColor& color) { - Q_UNUSED(painter); - Q_UNUSED(color); + Q_UNUSED(painter) + Q_UNUSED(color) } QPalette::ColorRole PlacesItemListGroupHeader::normalTextColorRole() const { return QPalette::WindowText; } diff --git a/src/panels/places/placesitemmodel.cpp b/src/panels/places/placesitemmodel.cpp index 4a91ec6d8..9dbfe7f91 100644 --- a/src/panels/places/placesitemmodel.cpp +++ b/src/panels/places/placesitemmodel.cpp @@ -1,799 +1,799 @@ /*************************************************************************** * Copyright (C) 2012 by Peter Penz * * * * Based on KFilePlacesModel from kdelibs: * * Copyright (C) 2007 Kevin Ottens * * Copyright (C) 2007 David Faure * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "placesitemmodel.h" #include "dolphin_generalsettings.h" #include "dolphindebug.h" #include "dolphinplacesmodelsingleton.h" #include "placesitem.h" #include "placesitemsignalhandler.h" #include "views/dolphinview.h" #include "views/viewproperties.h" #include #include #include #include #include #include #include #include #include #include #include PlacesItemModel::PlacesItemModel(QObject* parent) : KStandardItemModel(parent), m_hiddenItemsShown(false), m_deviceToTearDown(nullptr), m_storageSetupInProgress(), m_sourceModel(DolphinPlacesModelSingleton::instance().placesModel()) { cleanupBookmarks(); loadBookmarks(); initializeDefaultViewProperties(); connect(m_sourceModel, &KFilePlacesModel::rowsInserted, this, &PlacesItemModel::onSourceModelRowsInserted); connect(m_sourceModel, &KFilePlacesModel::rowsAboutToBeRemoved, this, &PlacesItemModel::onSourceModelRowsAboutToBeRemoved); connect(m_sourceModel, &KFilePlacesModel::dataChanged, this, &PlacesItemModel::onSourceModelDataChanged); connect(m_sourceModel, &KFilePlacesModel::rowsAboutToBeMoved, this, &PlacesItemModel::onSourceModelRowsAboutToBeMoved); connect(m_sourceModel, &KFilePlacesModel::rowsMoved, this, &PlacesItemModel::onSourceModelRowsMoved); connect(m_sourceModel, &KFilePlacesModel::groupHiddenChanged, this, &PlacesItemModel::onSourceModelGroupHiddenChanged); } PlacesItemModel::~PlacesItemModel() { } void PlacesItemModel::createPlacesItem(const QString &text, const QUrl &url, const QString &iconName, const QString &appName) { createPlacesItem(text, url, iconName, appName, -1); } void PlacesItemModel::createPlacesItem(const QString &text, const QUrl &url, const QString &iconName, const QString &appName, int after) { m_sourceModel->addPlace(text, url, iconName, appName, mapToSource(after)); } PlacesItem* PlacesItemModel::placesItem(int index) const { return dynamic_cast(item(index)); } int PlacesItemModel::hiddenCount() const { return m_sourceModel->hiddenCount(); } void PlacesItemModel::setHiddenItemsShown(bool show) { if (m_hiddenItemsShown == show) { return; } m_hiddenItemsShown = show; if (show) { for (int r = 0, rMax = m_sourceModel->rowCount(); r < rMax; r++) { const QModelIndex index = m_sourceModel->index(r, 0); if (!m_sourceModel->isHidden(index)) { continue; } addItemFromSourceModel(index); } } else { for (int r = 0, rMax = m_sourceModel->rowCount(); r < rMax; r++) { const QModelIndex index = m_sourceModel->index(r, 0); if (m_sourceModel->isHidden(index)) { removeItemByIndex(index); } } } } bool PlacesItemModel::hiddenItemsShown() const { return m_hiddenItemsShown; } int PlacesItemModel::closestItem(const QUrl& url) const { return mapFromSource(m_sourceModel->closestItem(url)); } // look for the correct position for the item based on source model void PlacesItemModel::insertSortedItem(PlacesItem* item) { if (!item) { return; } const KBookmark iBookmark = item->bookmark(); const QString iBookmarkId = bookmarkId(iBookmark); QModelIndex sourceIndex; int pos = 0; for(int r = 0, rMax = m_sourceModel->rowCount(); r < rMax; r++) { sourceIndex = m_sourceModel->index(r, 0); const KBookmark sourceBookmark = m_sourceModel->bookmarkForIndex(sourceIndex); if (bookmarkId(sourceBookmark) == iBookmarkId) { break; } if (m_hiddenItemsShown || !m_sourceModel->isHidden(sourceIndex)) { pos++; } } m_indexMap.insert(pos, sourceIndex); insertItem(pos, item); } void PlacesItemModel::onItemInserted(int index) { KStandardItemModel::onItemInserted(index); } void PlacesItemModel::onItemRemoved(int index, KStandardItem* removedItem) { m_indexMap.removeAt(index); KStandardItemModel::onItemRemoved(index, removedItem); } void PlacesItemModel::onItemChanged(int index, const QSet& changedRoles) { const QModelIndex sourceIndex = mapToSource(index); const PlacesItem *changedItem = placesItem(mapFromSource(sourceIndex)); if (!changedItem || !sourceIndex.isValid()) { qWarning() << "invalid item changed signal"; return; } if (changedRoles.contains("isHidden")) { if (m_sourceModel->isHidden(sourceIndex) != changedItem->isHidden()) { m_sourceModel->setPlaceHidden(sourceIndex, changedItem->isHidden()); } else { m_sourceModel->refresh(); } } KStandardItemModel::onItemChanged(index, changedRoles); } QAction* PlacesItemModel::ejectAction(int index) const { const PlacesItem* item = placesItem(index); if (item && item->device().is()) { return new QAction(QIcon::fromTheme(QStringLiteral("media-eject")), i18nc("@item", "Eject"), nullptr); } return nullptr; } QAction* PlacesItemModel::teardownAction(int index) const { const PlacesItem* item = placesItem(index); if (!item) { return nullptr; } Solid::Device device = item->device(); const bool providesTearDown = device.is() && device.as()->isAccessible(); if (!providesTearDown) { return nullptr; } Solid::StorageDrive* drive = device.as(); if (!drive) { drive = device.parent().as(); } bool hotPluggable = false; bool removable = false; if (drive) { hotPluggable = drive->isHotpluggable(); removable = drive->isRemovable(); } QString iconName; QString text; if (device.is()) { text = i18nc("@item", "Release"); } else if (removable || hotPluggable) { text = i18nc("@item", "Safely Remove"); iconName = QStringLiteral("media-eject"); } else { text = i18nc("@item", "Unmount"); iconName = QStringLiteral("media-eject"); } if (iconName.isEmpty()) { return new QAction(text, nullptr); } return new QAction(QIcon::fromTheme(iconName), text, nullptr); } void PlacesItemModel::requestEject(int index) { const PlacesItem* item = placesItem(index); if (item) { Solid::OpticalDrive* drive = item->device().parent().as(); if (drive) { connect(drive, &Solid::OpticalDrive::ejectDone, this, &PlacesItemModel::slotStorageTearDownDone); drive->eject(); } else { const QString label = item->text(); const QString message = i18nc("@info", "The device '%1' is not a disk and cannot be ejected.", label); emit errorMessage(message); } } } void PlacesItemModel::requestTearDown(int index) { const PlacesItem* item = placesItem(index); if (item) { Solid::StorageAccess *tmp = item->device().as(); if (tmp) { m_deviceToTearDown = tmp; // disconnect the Solid::StorageAccess::teardownRequested // to prevent emitting PlacesItemModel::storageTearDownExternallyRequested // after we have emitted PlacesItemModel::storageTearDownRequested disconnect(tmp, &Solid::StorageAccess::teardownRequested, item->signalHandler(), &PlacesItemSignalHandler::onTearDownRequested); emit storageTearDownRequested(tmp->filePath()); } } } bool PlacesItemModel::storageSetupNeeded(int index) const { const PlacesItem* item = placesItem(index); return item ? item->storageSetupNeeded() : false; } void PlacesItemModel::requestStorageSetup(int index) { const PlacesItem* item = placesItem(index); if (!item) { return; } Solid::Device device = item->device(); const bool setup = device.is() && !m_storageSetupInProgress.contains(device.as()) && !device.as()->isAccessible(); if (setup) { Solid::StorageAccess* access = device.as(); m_storageSetupInProgress[access] = index; connect(access, &Solid::StorageAccess::setupDone, this, &PlacesItemModel::slotStorageSetupDone); access->setup(); } } QMimeData* PlacesItemModel::createMimeData(const KItemSet& indexes) const { QList urls; QByteArray itemData; QDataStream stream(&itemData, QIODevice::WriteOnly); for (int index : indexes) { const QUrl itemUrl = placesItem(index)->url(); if (itemUrl.isValid()) { urls << itemUrl; } stream << index; } QMimeData* mimeData = new QMimeData(); if (!urls.isEmpty()) { mimeData->setUrls(urls); } else { // #378954: prevent itemDropEvent() drops if there isn't a source url. mimeData->setData(blacklistItemDropEventMimeType(), QByteArrayLiteral("true")); } mimeData->setData(internalMimeType(), itemData); return mimeData; } bool PlacesItemModel::supportsDropping(int index) const { return index >= 0 && index < count(); } void PlacesItemModel::dropMimeDataBefore(int index, const QMimeData* mimeData) { if (mimeData->hasFormat(internalMimeType())) { // The item has been moved inside the view QByteArray itemData = mimeData->data(internalMimeType()); QDataStream stream(&itemData, QIODevice::ReadOnly); int oldIndex; stream >> oldIndex; QModelIndex sourceIndex = mapToSource(index); QModelIndex oldSourceIndex = mapToSource(oldIndex); m_sourceModel->movePlace(oldSourceIndex.row(), sourceIndex.row()); } else if (mimeData->hasFormat(QStringLiteral("text/uri-list"))) { // One or more items must be added to the model const QList urls = KUrlMimeData::urlsFromMimeData(mimeData); for (int i = urls.count() - 1; i >= 0; --i) { const QUrl& url = urls[i]; QString text = url.fileName(); if (text.isEmpty()) { text = url.host(); } if ((url.isLocalFile() && !QFileInfo(url.toLocalFile()).isDir()) || url.scheme() == QLatin1String("trash")) { // Only directories outside the trash are allowed continue; } createPlacesItem(text, url, KIO::iconNameForUrl(url), {}, qMax(0, index - 1)); } } // will save bookmark alteration and fix sort if that is broken by the drag/drop operation refresh(); } void PlacesItemModel::addItemFromSourceModel(const QModelIndex &index) { if (!m_hiddenItemsShown && m_sourceModel->isHidden(index)) { return; } const KBookmark bookmark = m_sourceModel->bookmarkForIndex(index); Q_ASSERT(!bookmark.isNull()); PlacesItem *item = new PlacesItem(bookmark); updateItem(item, index); insertSortedItem(item); if (m_sourceModel->isDevice(index)) { connect(item->signalHandler(), &PlacesItemSignalHandler::tearDownExternallyRequested, this, &PlacesItemModel::storageTearDownExternallyRequested); } } void PlacesItemModel::removeItemByIndex(const QModelIndex &sourceIndex) { QString id = bookmarkId(m_sourceModel->bookmarkForIndex(sourceIndex)); for (int i = 0, iMax = count(); i < iMax; ++i) { if (bookmarkId(placesItem(i)->bookmark()) == id) { removeItem(i); return; } } } QString PlacesItemModel::bookmarkId(const KBookmark &bookmark) const { QString id = bookmark.metaDataItem(QStringLiteral("UDI")); if (id.isEmpty()) { id = bookmark.metaDataItem(QStringLiteral("ID")); } return id; } void PlacesItemModel::initializeDefaultViewProperties() const { for(int i = 0, iMax = m_sourceModel->rowCount(); i < iMax; i++) { const QModelIndex index = m_sourceModel->index(i, 0); const PlacesItem *item = placesItem(mapFromSource(index)); if (!item) { continue; } // Create default view-properties for all "Search For" and "Recently Saved" bookmarks // in case the user has not already created custom view-properties for a corresponding // query yet. const bool createDefaultViewProperties = item->isSearchOrTimelineUrl() && !GeneralSettings::self()->globalViewProps(); if (createDefaultViewProperties) { const QUrl itemUrl = item->url(); ViewProperties props(KFilePlacesModel::convertedUrl(itemUrl)); if (!props.exist()) { const QString path = itemUrl.path(); if (path == QLatin1String("/documents")) { props.setViewMode(DolphinView::DetailsView); props.setPreviewsShown(false); props.setVisibleRoles({"text", "path"}); } else if (path == QLatin1String("/images")) { props.setViewMode(DolphinView::IconsView); props.setPreviewsShown(true); props.setVisibleRoles({"text", "height", "width"}); } else if (path == QLatin1String("/audio")) { props.setViewMode(DolphinView::DetailsView); props.setPreviewsShown(false); props.setVisibleRoles({"text", "artist", "album"}); } else if (path == QLatin1String("/videos")) { props.setViewMode(DolphinView::IconsView); props.setPreviewsShown(true); props.setVisibleRoles({"text"}); } else if (itemUrl.scheme() == QLatin1String("timeline")) { props.setViewMode(DolphinView::DetailsView); props.setVisibleRoles({"text", "modificationtime"}); } props.save(); } } } } void PlacesItemModel::updateItem(PlacesItem *item, const QModelIndex &index) { item->setGroup(index.data(KFilePlacesModel::GroupRole).toString()); item->setIcon(index.data(KFilePlacesModel::IconNameRole).toString()); item->setGroupHidden(index.data(KFilePlacesModel::GroupHiddenRole).toBool()); } void PlacesItemModel::slotStorageTearDownDone(Solid::ErrorType error, const QVariant& errorData) { if (error && errorData.isValid()) { if (error == Solid::ErrorType::DeviceBusy) { KListOpenFilesJob* listOpenFilesJob = new KListOpenFilesJob(m_deviceToTearDown->filePath()); connect(listOpenFilesJob, &KIO::Job::result, this, [this, listOpenFilesJob](KJob*) { const KProcessList::KProcessInfoList blockingProcesses = listOpenFilesJob->processInfoList(); QString errorString; if (blockingProcesses.isEmpty()) { errorString = i18n("One or more files on this device are open within an application."); } else { QStringList blockingApps; for (const auto& process : blockingProcesses) { blockingApps << process.name(); } blockingApps.removeDuplicates(); errorString = xi18np("One or more files on this device are opened in application \"%2\".", "One or more files on this device are opened in following applications: %2.", blockingApps.count(), blockingApps.join(i18nc("separator in list of apps blocking device unmount", ", "))); } emit errorMessage(errorString); }); listOpenFilesJob->start(); } else { emit errorMessage(errorData.toString()); } } disconnect(m_deviceToTearDown, &Solid::StorageAccess::teardownDone, this, &PlacesItemModel::slotStorageTearDownDone); m_deviceToTearDown = nullptr; } void PlacesItemModel::slotStorageSetupDone(Solid::ErrorType error, const QVariant& errorData, const QString& udi) { - Q_UNUSED(udi); + Q_UNUSED(udi) const int index = m_storageSetupInProgress.take(sender()); const PlacesItem* item = placesItem(index); if (!item) { return; } if (error != Solid::NoError) { if (errorData.isValid()) { emit errorMessage(i18nc("@info", "An error occurred while accessing '%1', the system responded: %2", item->text(), errorData.toString())); } else { emit errorMessage(i18nc("@info", "An error occurred while accessing '%1'", item->text())); } emit storageSetupDone(index, false); } else { emit storageSetupDone(index, true); } } void PlacesItemModel::onSourceModelRowsInserted(const QModelIndex &parent, int first, int last) { for (int i = first; i <= last; i++) { const QModelIndex index = m_sourceModel->index(i, 0, parent); addItemFromSourceModel(index); } } void PlacesItemModel::onSourceModelRowsAboutToBeRemoved(const QModelIndex &parent, int first, int last) { for(int r = first; r <= last; r++) { const QModelIndex index = m_sourceModel->index(r, 0, parent); int oldIndex = mapFromSource(index); if (oldIndex != -1) { removeItem(oldIndex); } } } void PlacesItemModel::onSourceModelRowsAboutToBeMoved(const QModelIndex &parent, int start, int end, const QModelIndex &destination, int row) { - Q_UNUSED(destination); - Q_UNUSED(row); + Q_UNUSED(destination) + Q_UNUSED(row) for(int r = start; r <= end; r++) { const QModelIndex sourceIndex = m_sourceModel->index(r, 0, parent); // remove moved item removeItem(mapFromSource(sourceIndex)); } } void PlacesItemModel::onSourceModelRowsMoved(const QModelIndex &parent, int start, int end, const QModelIndex &destination, int row) { - Q_UNUSED(destination); - Q_UNUSED(parent); + Q_UNUSED(destination) + Q_UNUSED(parent) const int blockSize = (end - start) + 1; for (int r = start; r <= end; r++) { // insert the moved item in the new position const int targetRow = row + (start - r) - (r < row ? blockSize : 0); const QModelIndex targetIndex = m_sourceModel->index(targetRow, 0, destination); addItemFromSourceModel(targetIndex); } } void PlacesItemModel::onSourceModelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { - Q_UNUSED(roles); + Q_UNUSED(roles) for (int r = topLeft.row(); r <= bottomRight.row(); r++) { const QModelIndex sourceIndex = m_sourceModel->index(r, 0); const KBookmark bookmark = m_sourceModel->bookmarkForIndex(sourceIndex); PlacesItem *placeItem = itemFromBookmark(bookmark); if (placeItem && (!m_hiddenItemsShown && m_sourceModel->isHidden(sourceIndex))) { //hide item if it became invisible removeItem(index(placeItem)); return; } if (!placeItem && (m_hiddenItemsShown || !m_sourceModel->isHidden(sourceIndex))) { //show item if it became visible addItemFromSourceModel(sourceIndex); return; } if (placeItem && !m_sourceModel->isDevice(sourceIndex)) { placeItem->setText(bookmark.text()); placeItem->setIcon(sourceIndex.data(KFilePlacesModel::IconNameRole).toString()); placeItem->setUrl(m_sourceModel->url(sourceIndex)); placeItem->bookmark().setMetaDataItem(QStringLiteral("OnlyInApp"), bookmark.metaDataItem(QStringLiteral("OnlyInApp"))); // must update the bookmark object placeItem->setBookmark(bookmark); } } } void PlacesItemModel::onSourceModelGroupHiddenChanged(KFilePlacesModel::GroupType group, bool hidden) { const auto groupIndexes = m_sourceModel->groupIndexes(group); for (const QModelIndex &sourceIndex : groupIndexes) { PlacesItem *item = placesItem(mapFromSource(sourceIndex)); if (item) { item->setGroupHidden(hidden); } } } void PlacesItemModel::cleanupBookmarks() { // KIO model now provides support for baloo urls, and because of that we // need to remove old URLs that were visible only in Dolphin to avoid duplication static const QVector balooURLs = { QUrl(QStringLiteral("timeline:/today")), QUrl(QStringLiteral("timeline:/yesterday")), QUrl(QStringLiteral("timeline:/thismonth")), QUrl(QStringLiteral("timeline:/lastmonth")), QUrl(QStringLiteral("search:/documents")), QUrl(QStringLiteral("search:/images")), QUrl(QStringLiteral("search:/audio")), QUrl(QStringLiteral("search:/videos")) }; int row = 0; do { const QModelIndex sourceIndex = m_sourceModel->index(row, 0); const KBookmark bookmark = m_sourceModel->bookmarkForIndex(sourceIndex); const QUrl url = bookmark.url(); const QString appName = bookmark.metaDataItem(QStringLiteral("OnlyInApp")); if ((appName == KAboutData::applicationData().componentName() || appName == KAboutData::applicationData().componentName() + DolphinPlacesModelSingleton::applicationNameSuffix()) && balooURLs.contains(url)) { qCDebug(DolphinDebug) << "Removing old baloo url:" << url; m_sourceModel->removePlace(sourceIndex); } else { row++; } } while (row < m_sourceModel->rowCount()); } void PlacesItemModel::loadBookmarks() { for(int r = 0, rMax = m_sourceModel->rowCount(); r < rMax; r++) { const QModelIndex sourceIndex = m_sourceModel->index(r, 0); if (m_hiddenItemsShown || !m_sourceModel->isHidden(sourceIndex)) { addItemFromSourceModel(sourceIndex); } } } void PlacesItemModel::clear() { KStandardItemModel::clear(); } void PlacesItemModel::proceedWithTearDown() { Q_ASSERT(m_deviceToTearDown); connect(m_deviceToTearDown, &Solid::StorageAccess::teardownDone, this, &PlacesItemModel::slotStorageTearDownDone); m_deviceToTearDown->teardown(); } void PlacesItemModel::deleteItem(int index) { QModelIndex sourceIndex = mapToSource(index); Q_ASSERT(sourceIndex.isValid()); m_sourceModel->removePlace(sourceIndex); } void PlacesItemModel::refresh() { m_sourceModel->refresh(); } void PlacesItemModel::hideItem(int index) { PlacesItem* shownItem = placesItem(index); if (!shownItem) { return; } shownItem->setHidden(true); } QString PlacesItemModel::internalMimeType() const { return "application/x-dolphinplacesmodel-" + QString::number((qptrdiff)this); } int PlacesItemModel::groupedDropIndex(int index, const PlacesItem* item) const { Q_ASSERT(item); int dropIndex = index; const QString group = item->group(); const int itemCount = count(); if (index < 0) { dropIndex = itemCount; } // Search nearest previous item with the same group int previousIndex = -1; for (int i = dropIndex - 1; i >= 0; --i) { if (placesItem(i)->group() == group) { previousIndex = i; break; } } // Search nearest next item with the same group int nextIndex = -1; for (int i = dropIndex; i < count(); ++i) { if (placesItem(i)->group() == group) { nextIndex = i; break; } } // Adjust the drop-index to be inserted to the // nearest item with the same group. if (previousIndex >= 0 && nextIndex >= 0) { dropIndex = (dropIndex - previousIndex < nextIndex - dropIndex) ? previousIndex + 1 : nextIndex; } else if (previousIndex >= 0) { dropIndex = previousIndex + 1; } else if (nextIndex >= 0) { dropIndex = nextIndex; } return dropIndex; } bool PlacesItemModel::equalBookmarkIdentifiers(const KBookmark& b1, const KBookmark& b2) { const QString udi1 = b1.metaDataItem(QStringLiteral("UDI")); const QString udi2 = b2.metaDataItem(QStringLiteral("UDI")); if (!udi1.isEmpty() && !udi2.isEmpty()) { return udi1 == udi2; } else { return b1.metaDataItem(QStringLiteral("ID")) == b2.metaDataItem(QStringLiteral("ID")); } } int PlacesItemModel::mapFromSource(const QModelIndex &index) const { if (!index.isValid()) { return -1; } return m_indexMap.indexOf(index); } bool PlacesItemModel::isDir(int index) const { - Q_UNUSED(index); + Q_UNUSED(index) return true; } KFilePlacesModel::GroupType PlacesItemModel::groupType(int row) const { return m_sourceModel->groupType(mapToSource(row)); } bool PlacesItemModel::isGroupHidden(KFilePlacesModel::GroupType type) const { return m_sourceModel->isGroupHidden(type); } void PlacesItemModel::setGroupHidden(KFilePlacesModel::GroupType type, bool hidden) { return m_sourceModel->setGroupHidden(type, hidden); } QModelIndex PlacesItemModel::mapToSource(int row) const { return m_indexMap.value(row); } PlacesItem *PlacesItemModel::itemFromBookmark(const KBookmark &bookmark) const { const QString id = bookmarkId(bookmark); for (int i = 0, iMax = count(); i < iMax; i++) { PlacesItem *item = placesItem(i); const KBookmark itemBookmark = item->bookmark(); if (bookmarkId(itemBookmark) == id) { return item; } } return nullptr; } diff --git a/src/search/dolphinsearchbox.cpp b/src/search/dolphinsearchbox.cpp index 20f767cbc..751b23942 100644 --- a/src/search/dolphinsearchbox.cpp +++ b/src/search/dolphinsearchbox.cpp @@ -1,558 +1,558 @@ /*************************************************************************** * Copyright (C) 2010 by Peter Penz * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * **************************************************************************/ #include "global.h" #include "dolphinsearchbox.h" #include "dolphin_searchsettings.h" #include "dolphinfacetswidget.h" #include "panels/places/placesitemmodel.h" #include #include #include #include #ifdef HAVE_BALOO #include #include #endif #include #include #include #include #include #include #include #include #include #include #include DolphinSearchBox::DolphinSearchBox(QWidget* parent) : QWidget(parent), m_startedSearching(false), m_active(true), m_topLayout(nullptr), m_searchInput(nullptr), m_saveSearchAction(nullptr), m_optionsScrollArea(nullptr), m_fileNameButton(nullptr), m_contentButton(nullptr), m_separator(nullptr), m_fromHereButton(nullptr), m_everywhereButton(nullptr), m_facetsWidget(nullptr), m_searchPath(), m_startSearchTimer(nullptr) { } DolphinSearchBox::~DolphinSearchBox() { saveSettings(); } void DolphinSearchBox::setText(const QString& text) { m_searchInput->setText(text); } QString DolphinSearchBox::text() const { return m_searchInput->text(); } void DolphinSearchBox::setSearchPath(const QUrl& url) { if (url == m_searchPath) { return; } const QUrl cleanedUrl = url.adjusted(QUrl::RemoveUserInfo | QUrl::StripTrailingSlash); if (cleanedUrl.path() == QDir::homePath()) { m_fromHereButton->setChecked(false); m_everywhereButton->setChecked(true); if (!m_searchPath.isEmpty()) { return; } } else { m_everywhereButton->setChecked(false); m_fromHereButton->setChecked(true); } m_searchPath = url; QFontMetrics metrics(m_fromHereButton->font()); const int maxWidth = metrics.height() * 8; QString location = cleanedUrl.fileName(); if (location.isEmpty()) { location = cleanedUrl.toString(QUrl::PreferLocalFile); } const QString elidedLocation = metrics.elidedText(location, Qt::ElideMiddle, maxWidth); m_fromHereButton->setText(i18nc("action:button", "From Here (%1)", elidedLocation)); m_fromHereButton->setToolTip(i18nc("action:button", "Limit search to '%1' and its subfolders", cleanedUrl.toString(QUrl::PreferLocalFile))); } QUrl DolphinSearchBox::searchPath() const { return m_everywhereButton->isChecked() ? QUrl::fromLocalFile(QDir::homePath()) : m_searchPath; } QUrl DolphinSearchBox::urlForSearching() const { QUrl url; if (isIndexingEnabled()) { url = balooUrlForSearching(); } else { url.setScheme(QStringLiteral("filenamesearch")); QUrlQuery query; query.addQueryItem(QStringLiteral("search"), m_searchInput->text()); if (m_contentButton->isChecked()) { query.addQueryItem(QStringLiteral("checkContent"), QStringLiteral("yes")); } query.addQueryItem(QStringLiteral("url"), searchPath().url()); url.setQuery(query); } return url; } void DolphinSearchBox::fromSearchUrl(const QUrl& url) { if (url.scheme() == QLatin1String("baloosearch")) { fromBalooSearchUrl(url); } else if (url.scheme() == QLatin1String("filenamesearch")) { const QUrlQuery query(url); setText(query.queryItemValue(QStringLiteral("search"))); if (m_searchPath.scheme() != url.scheme()) { m_searchPath = QUrl(); } setSearchPath(QUrl::fromUserInput(query.queryItemValue(QStringLiteral("url")), QString(), QUrl::AssumeLocalFile)); m_contentButton->setChecked(query.queryItemValue(QStringLiteral("checkContent")) == QLatin1String("yes")); } else { setText(QString()); m_searchPath = QUrl(); setSearchPath(url); } updateFacetsVisible(); } void DolphinSearchBox::selectAll() { m_searchInput->selectAll(); } void DolphinSearchBox::setActive(bool active) { if (active != m_active) { m_active = active; if (active) { emit activated(); } } } bool DolphinSearchBox::isActive() const { return m_active; } bool DolphinSearchBox::event(QEvent* event) { if (event->type() == QEvent::Polish) { init(); } return QWidget::event(event); } void DolphinSearchBox::showEvent(QShowEvent* event) { if (!event->spontaneous()) { m_searchInput->setFocus(); m_startedSearching = false; } } void DolphinSearchBox::hideEvent(QHideEvent* event) { - Q_UNUSED(event); + Q_UNUSED(event) m_startedSearching = false; m_startSearchTimer->stop(); } void DolphinSearchBox::keyReleaseEvent(QKeyEvent* event) { QWidget::keyReleaseEvent(event); if (event->key() == Qt::Key_Escape) { if (m_searchInput->text().isEmpty()) { emit closeRequest(); } else { m_searchInput->clear(); } } } bool DolphinSearchBox::eventFilter(QObject* obj, QEvent* event) { switch (event->type()) { case QEvent::FocusIn: // #379135: we get the FocusIn event when we close a tab but we don't want to emit // the activated() signal before the removeTab() call in DolphinTabWidget::closeTab() returns. // To avoid this issue, we delay the activation of the search box. // We also don't want to schedule the activation process if we are already active, // otherwise we can enter in a loop of FocusIn/FocusOut events with the searchbox of another tab. if (!isActive()) { QTimer::singleShot(0, this, [this] { setActive(true); setFocus(); }); } break; default: break; } return QObject::eventFilter(obj, event); } void DolphinSearchBox::emitSearchRequest() { m_startSearchTimer->stop(); m_startedSearching = true; m_saveSearchAction->setEnabled(true); emit searchRequest(); } void DolphinSearchBox::emitCloseRequest() { m_startSearchTimer->stop(); m_startedSearching = false; m_saveSearchAction->setEnabled(false); emit closeRequest(); } void DolphinSearchBox::slotConfigurationChanged() { saveSettings(); if (m_startedSearching) { emitSearchRequest(); } } void DolphinSearchBox::slotSearchTextChanged(const QString& text) { if (text.isEmpty()) { m_startSearchTimer->stop(); } else { m_startSearchTimer->start(); } emit searchTextChanged(text); } void DolphinSearchBox::slotReturnPressed() { emitSearchRequest(); emit returnPressed(); } void DolphinSearchBox::slotFacetChanged() { m_startedSearching = true; m_startSearchTimer->stop(); emit searchRequest(); } void DolphinSearchBox::slotSearchSaved() { const QUrl searchURL = urlForSearching(); if (searchURL.isValid()) { PlacesItemModel model; const QString label = i18n("Search for %1 in %2", text(), searchPath().fileName()); model.createPlacesItem(label, searchURL, QStringLiteral("folder-saved-search-symbolic")); } } void DolphinSearchBox::initButton(QToolButton* button) { button->installEventFilter(this); button->setAutoExclusive(true); button->setAutoRaise(true); button->setCheckable(true); connect(button, &QToolButton::clicked, this, &DolphinSearchBox::slotConfigurationChanged); } void DolphinSearchBox::loadSettings() { if (SearchSettings::location() == QLatin1String("Everywhere")) { m_everywhereButton->setChecked(true); } else { m_fromHereButton->setChecked(true); } if (SearchSettings::what() == QLatin1String("Content")) { m_contentButton->setChecked(true); } else { m_fileNameButton->setChecked(true); } updateFacetsVisible(); } void DolphinSearchBox::saveSettings() { SearchSettings::setLocation(m_fromHereButton->isChecked() ? QStringLiteral("FromHere") : QStringLiteral("Everywhere")); SearchSettings::setWhat(m_fileNameButton->isChecked() ? QStringLiteral("FileName") : QStringLiteral("Content")); SearchSettings::self()->save(); } void DolphinSearchBox::init() { // Create close button QToolButton* closeButton = new QToolButton(this); closeButton->setAutoRaise(true); closeButton->setIcon(QIcon::fromTheme(QStringLiteral("dialog-close"))); closeButton->setToolTip(i18nc("@info:tooltip", "Quit searching")); connect(closeButton, &QToolButton::clicked, this, &DolphinSearchBox::emitCloseRequest); // Create search box m_searchInput = new QLineEdit(this); m_searchInput->setPlaceholderText(i18n("Search...")); m_searchInput->installEventFilter(this); m_searchInput->setClearButtonEnabled(true); m_searchInput->setFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont)); connect(m_searchInput, &QLineEdit::returnPressed, this, &DolphinSearchBox::slotReturnPressed); connect(m_searchInput, &QLineEdit::textChanged, this, &DolphinSearchBox::slotSearchTextChanged); setFocusProxy(m_searchInput); // Add "Save search" button inside search box m_saveSearchAction = new QAction(this); m_saveSearchAction->setIcon (QIcon::fromTheme(QStringLiteral("document-save-symbolic"))); m_saveSearchAction->setText(i18nc("action:button", "Save this search to quickly access it again in the future")); m_saveSearchAction->setEnabled(false); m_searchInput->addAction(m_saveSearchAction, QLineEdit::TrailingPosition); connect(m_saveSearchAction, &QAction::triggered, this, &DolphinSearchBox::slotSearchSaved); // Apply layout for the search input QHBoxLayout* searchInputLayout = new QHBoxLayout(); searchInputLayout->setContentsMargins(0, 0, 0, 0); searchInputLayout->addWidget(closeButton); searchInputLayout->addWidget(m_searchInput); // Create "Filename" and "Content" button m_fileNameButton = new QToolButton(this); m_fileNameButton->setText(i18nc("action:button", "Filename")); initButton(m_fileNameButton); m_contentButton = new QToolButton(); m_contentButton->setText(i18nc("action:button", "Content")); initButton(m_contentButton); QButtonGroup* searchWhatGroup = new QButtonGroup(this); searchWhatGroup->addButton(m_fileNameButton); searchWhatGroup->addButton(m_contentButton); m_separator = new KSeparator(Qt::Vertical, this); // Create "From Here" and "Your files" buttons m_fromHereButton = new QToolButton(this); m_fromHereButton->setText(i18nc("action:button", "From Here")); initButton(m_fromHereButton); m_everywhereButton = new QToolButton(this); m_everywhereButton->setText(i18nc("action:button", "Your files")); m_everywhereButton->setToolTip(i18nc("action:button", "Search in your home directory")); m_everywhereButton->setIcon(QIcon::fromTheme(QStringLiteral("user-home"))); m_everywhereButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); initButton(m_everywhereButton); QButtonGroup* searchLocationGroup = new QButtonGroup(this); searchLocationGroup->addButton(m_fromHereButton); searchLocationGroup->addButton(m_everywhereButton); auto moreSearchToolsButton = new QToolButton(this); moreSearchToolsButton->setAutoRaise(true); moreSearchToolsButton->setPopupMode(QToolButton::InstantPopup); moreSearchToolsButton->setIcon(QIcon::fromTheme("arrow-down-double")); moreSearchToolsButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); moreSearchToolsButton->setText(i18n("More Search Tools")); moreSearchToolsButton->setMenu(new QMenu(this)); connect(moreSearchToolsButton->menu(), &QMenu::aboutToShow, moreSearchToolsButton->menu(), [this, moreSearchToolsButton]() { m_menuFactory.reset(new KMoreToolsMenuFactory("dolphin/search-tools")); moreSearchToolsButton->menu()->clear(); m_menuFactory->fillMenuFromGroupingNames(moreSearchToolsButton->menu(), { "files-find" }, this->m_searchPath); } ); // Create "Facets" widget m_facetsWidget = new DolphinFacetsWidget(this); m_facetsWidget->installEventFilter(this); m_facetsWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); m_facetsWidget->layout()->setSpacing(Dolphin::LAYOUT_SPACING_SMALL); connect(m_facetsWidget, &DolphinFacetsWidget::facetChanged, this, &DolphinSearchBox::slotFacetChanged); // Apply layout for the options QHBoxLayout* optionsLayout = new QHBoxLayout(); optionsLayout->setContentsMargins(0, 0, 0, 0); optionsLayout->setSpacing(Dolphin::LAYOUT_SPACING_SMALL); optionsLayout->addWidget(m_fileNameButton); optionsLayout->addWidget(m_contentButton); optionsLayout->addWidget(m_separator); optionsLayout->addWidget(m_fromHereButton); optionsLayout->addWidget(m_everywhereButton); optionsLayout->addWidget(new KSeparator(Qt::Vertical, this)); optionsLayout->addWidget(moreSearchToolsButton); optionsLayout->addStretch(1); // Put the options into a QScrollArea. This prevents increasing the view width // in case that not enough width for the options is available. QWidget* optionsContainer = new QWidget(this); optionsContainer->setLayout(optionsLayout); m_optionsScrollArea = new QScrollArea(this); m_optionsScrollArea->setFrameShape(QFrame::NoFrame); m_optionsScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_optionsScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_optionsScrollArea->setMaximumHeight(optionsContainer->sizeHint().height()); m_optionsScrollArea->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); m_optionsScrollArea->setWidget(optionsContainer); m_optionsScrollArea->setWidgetResizable(true); m_topLayout = new QVBoxLayout(this); m_topLayout->setContentsMargins(0, 0, 0, 0); m_topLayout->setSpacing(Dolphin::LAYOUT_SPACING_SMALL); m_topLayout->addLayout(searchInputLayout); m_topLayout->addWidget(m_optionsScrollArea); m_topLayout->addWidget(m_facetsWidget); loadSettings(); // The searching should be started automatically after the user did not change // the text within one second m_startSearchTimer = new QTimer(this); m_startSearchTimer->setSingleShot(true); m_startSearchTimer->setInterval(1000); connect(m_startSearchTimer, &QTimer::timeout, this, &DolphinSearchBox::emitSearchRequest); } QUrl DolphinSearchBox::balooUrlForSearching() const { #ifdef HAVE_BALOO const QString text = m_searchInput->text(); Baloo::Query query; query.addType(m_facetsWidget->facetType()); QStringList queryStrings; QString ratingQuery = m_facetsWidget->ratingTerm(); if (!ratingQuery.isEmpty()) { queryStrings << ratingQuery; } if (m_contentButton->isChecked()) { queryStrings << text; } else if (!text.isEmpty()) { queryStrings << QStringLiteral("filename:\"%1\"").arg(text); } if (m_fromHereButton->isChecked()) { query.setIncludeFolder(m_searchPath.toLocalFile()); } query.setSearchString(queryStrings.join(QLatin1Char(' '))); return query.toSearchUrl(i18nc("@title UDS_DISPLAY_NAME for a KIO directory listing. %1 is the query the user entered.", "Query Results from '%1'", text)); #else return QUrl(); #endif } void DolphinSearchBox::fromBalooSearchUrl(const QUrl& url) { #ifdef HAVE_BALOO const Baloo::Query query = Baloo::Query::fromSearchUrl(url); // Block all signals to avoid unnecessary "searchRequest" signals // while we adjust the search text and the facet widget. blockSignals(true); const QString customDir = query.includeFolder(); if (!customDir.isEmpty()) { setSearchPath(QUrl::fromLocalFile(customDir)); } else { setSearchPath(QUrl::fromLocalFile(QDir::homePath())); } m_facetsWidget->resetOptions(); setText(query.searchString()); QStringList types = query.types(); if (!types.isEmpty()) { m_facetsWidget->setFacetType(types.first()); } const QStringList subTerms = query.searchString().split(' ', QString::SkipEmptyParts); foreach (const QString& subTerm, subTerms) { if (subTerm.startsWith(QLatin1String("filename:"))) { const QString value = subTerm.mid(9); setText(value); } else if (m_facetsWidget->isRatingTerm(subTerm)) { m_facetsWidget->setRatingTerm(subTerm); } } m_startSearchTimer->stop(); blockSignals(false); #else - Q_UNUSED(url); + Q_UNUSED(url) #endif } void DolphinSearchBox::updateFacetsVisible() { const bool indexingEnabled = isIndexingEnabled(); m_facetsWidget->setEnabled(indexingEnabled); m_facetsWidget->setVisible(indexingEnabled); } bool DolphinSearchBox::isIndexingEnabled() const { #ifdef HAVE_BALOO const Baloo::IndexerConfig searchInfo; return searchInfo.fileIndexingEnabled() && searchInfo.shouldBeIndexed(searchPath().toLocalFile()); #else return false; #endif } diff --git a/src/settings/kcm/kcmdolphingeneral.cpp b/src/settings/kcm/kcmdolphingeneral.cpp index 032c2df4d..ccc32a8a6 100644 --- a/src/settings/kcm/kcmdolphingeneral.cpp +++ b/src/settings/kcm/kcmdolphingeneral.cpp @@ -1,88 +1,88 @@ /*************************************************************************** * Copyright (C) 2009 by Peter Penz * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kcmdolphingeneral.h" #include "settings/general/behaviorsettingspage.h" #include "settings/general/previewssettingspage.h" #include "settings/general/confirmationssettingspage.h" #include #include #include #include #include K_PLUGIN_FACTORY(KCMDolphinGeneralConfigFactory, registerPlugin(QStringLiteral("dolphingeneral"));) DolphinGeneralConfigModule::DolphinGeneralConfigModule(QWidget* parent, const QVariantList& args) : KCModule(parent), m_pages() { - Q_UNUSED(args); + Q_UNUSED(args) setButtons(KCModule::Default | KCModule::Help); QVBoxLayout* topLayout = new QVBoxLayout(this); topLayout->setContentsMargins(0, 0, 0, 0); QTabWidget* tabWidget = new QTabWidget(this); // initialize 'Behavior' tab BehaviorSettingsPage* behaviorPage = new BehaviorSettingsPage(QUrl::fromLocalFile(QDir::homePath()), tabWidget); tabWidget->addTab(behaviorPage, i18nc("@title:tab Behavior settings", "Behavior")); connect(behaviorPage, &BehaviorSettingsPage::changed, this, QOverload<>::of(&DolphinGeneralConfigModule::changed)); // initialize 'Previews' tab PreviewsSettingsPage* previewsPage = new PreviewsSettingsPage(tabWidget); tabWidget->addTab(previewsPage, i18nc("@title:tab Previews settings", "Previews")); connect(previewsPage, &PreviewsSettingsPage::changed, this, QOverload<>::of(&DolphinGeneralConfigModule::changed)); // initialize 'Confirmations' tab ConfirmationsSettingsPage* confirmationsPage = new ConfirmationsSettingsPage(tabWidget); tabWidget->addTab(confirmationsPage, i18nc("@title:tab Confirmations settings", "Confirmations")); connect(confirmationsPage, &ConfirmationsSettingsPage::changed, this, QOverload<>::of(&DolphinGeneralConfigModule::changed)); m_pages.append(behaviorPage); m_pages.append(previewsPage); m_pages.append(confirmationsPage); topLayout->addWidget(tabWidget, 0, nullptr); } DolphinGeneralConfigModule::~DolphinGeneralConfigModule() { } void DolphinGeneralConfigModule::save() { foreach (SettingsPageBase* page, m_pages) { page->applySettings(); } } void DolphinGeneralConfigModule::defaults() { foreach (SettingsPageBase* page, m_pages) { page->applySettings(); } } #include "kcmdolphingeneral.moc" diff --git a/src/settings/kcm/kcmdolphinnavigation.cpp b/src/settings/kcm/kcmdolphinnavigation.cpp index 2cd9ff30c..6b88dadac 100644 --- a/src/settings/kcm/kcmdolphinnavigation.cpp +++ b/src/settings/kcm/kcmdolphinnavigation.cpp @@ -1,61 +1,61 @@ /*************************************************************************** * Copyright (C) 2009 by Peter Penz * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kcmdolphinnavigation.h" #include "settings/navigation/navigationsettingspage.h" #include #include #include K_PLUGIN_FACTORY(KCMDolphinNavigationConfigFactory, registerPlugin(QStringLiteral("dolphinnavigation"));) DolphinNavigationConfigModule::DolphinNavigationConfigModule(QWidget* parent, const QVariantList& args) : KCModule(parent), m_navigation(nullptr) { - Q_UNUSED(args); + Q_UNUSED(args) setButtons(KCModule::Default | KCModule::Help); QVBoxLayout* topLayout = new QVBoxLayout(this); topLayout->setContentsMargins(0, 0, 0, 0); m_navigation = new NavigationSettingsPage(this); connect(m_navigation, &NavigationSettingsPage::changed, this, QOverload<>::of(&DolphinNavigationConfigModule::changed)); topLayout->addWidget(m_navigation, 0, nullptr); } DolphinNavigationConfigModule::~DolphinNavigationConfigModule() { } void DolphinNavigationConfigModule::save() { m_navigation->applySettings(); } void DolphinNavigationConfigModule::defaults() { m_navigation->restoreDefaults(); } #include "kcmdolphinnavigation.moc" diff --git a/src/settings/kcm/kcmdolphinservices.cpp b/src/settings/kcm/kcmdolphinservices.cpp index 94494c184..d5457f1fd 100644 --- a/src/settings/kcm/kcmdolphinservices.cpp +++ b/src/settings/kcm/kcmdolphinservices.cpp @@ -1,61 +1,61 @@ /*************************************************************************** * Copyright (C) 2009 by Peter Penz * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kcmdolphinservices.h" #include "settings/services/servicessettingspage.h" #include #include #include K_PLUGIN_FACTORY(KCMDolphinServicesConfigFactory, registerPlugin(QStringLiteral("dolphinservices"));) DolphinServicesConfigModule::DolphinServicesConfigModule(QWidget* parent, const QVariantList& args) : KCModule(parent), m_services(nullptr) { - Q_UNUSED(args); + Q_UNUSED(args) setButtons(KCModule::Default | KCModule::Help); QVBoxLayout* topLayout = new QVBoxLayout(this); topLayout->setContentsMargins(0, 0, 0, 0); m_services = new ServicesSettingsPage(this); connect(m_services, &ServicesSettingsPage::changed, this, QOverload<>::of(&DolphinServicesConfigModule::changed)); topLayout->addWidget(m_services, 0, nullptr); } DolphinServicesConfigModule::~DolphinServicesConfigModule() { } void DolphinServicesConfigModule::save() { m_services->applySettings(); } void DolphinServicesConfigModule::defaults() { m_services->restoreDefaults(); } #include "kcmdolphinservices.moc" diff --git a/src/settings/kcm/kcmdolphinviewmodes.cpp b/src/settings/kcm/kcmdolphinviewmodes.cpp index 584b7f64c..12a2056c1 100644 --- a/src/settings/kcm/kcmdolphinviewmodes.cpp +++ b/src/settings/kcm/kcmdolphinviewmodes.cpp @@ -1,102 +1,102 @@ /*************************************************************************** * Copyright (C) 2008 by Peter Penz * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kcmdolphinviewmodes.h" #include "settings/viewmodes/viewsettingstab.h" #include #include #include #include #include #include #include #include K_PLUGIN_FACTORY(KCMDolphinViewModesConfigFactory, registerPlugin(QStringLiteral("dolphinviewmodes"));) DolphinViewModesConfigModule::DolphinViewModesConfigModule(QWidget* parent, const QVariantList& args) : KCModule(parent), m_tabs() { - Q_UNUSED(args); + Q_UNUSED(args) setButtons(KCModule::Default | KCModule::Help); QVBoxLayout* topLayout = new QVBoxLayout(this); topLayout->setContentsMargins(0, 0, 0, 0); QTabWidget* tabWidget = new QTabWidget(this); // Initialize 'Icons' tab ViewSettingsTab* iconsTab = new ViewSettingsTab(ViewSettingsTab::IconsMode, tabWidget); tabWidget->addTab(iconsTab, QIcon::fromTheme(QStringLiteral("view-list-icons")), i18nc("@title:tab", "Icons")); connect(iconsTab, &ViewSettingsTab::changed, this, &DolphinViewModesConfigModule::viewModeChanged); // Initialize 'Compact' tab ViewSettingsTab* compactTab = new ViewSettingsTab(ViewSettingsTab::CompactMode, tabWidget); tabWidget->addTab(compactTab, QIcon::fromTheme(QStringLiteral("view-list-details")), i18nc("@title:tab", "Compact")); connect(compactTab, &ViewSettingsTab::changed, this, &DolphinViewModesConfigModule::viewModeChanged); // Initialize 'Details' tab ViewSettingsTab* detailsTab = new ViewSettingsTab(ViewSettingsTab::DetailsMode, tabWidget); tabWidget->addTab(detailsTab, QIcon::fromTheme(QStringLiteral("view-list-tree")), i18nc("@title:tab", "Details")); connect(detailsTab, &ViewSettingsTab::changed, this, &DolphinViewModesConfigModule::viewModeChanged); m_tabs.append(iconsTab); m_tabs.append(compactTab); m_tabs.append(detailsTab); topLayout->addWidget(tabWidget, 0, nullptr); } DolphinViewModesConfigModule::~DolphinViewModesConfigModule() { } void DolphinViewModesConfigModule::save() { foreach (ViewSettingsTab* tab, m_tabs) { tab->applySettings(); } reparseConfiguration(); } void DolphinViewModesConfigModule::defaults() { foreach (ViewSettingsTab* tab, m_tabs) { tab->restoreDefaultSettings(); } reparseConfiguration(); } void DolphinViewModesConfigModule::reparseConfiguration() { QDBusMessage message = QDBusMessage::createSignal(QStringLiteral("/KonqMain"), QStringLiteral("org.kde.Konqueror.Main"), QStringLiteral("reparseConfiguration")); QDBusConnection::sessionBus().send(message); } void DolphinViewModesConfigModule::viewModeChanged() { emit changed(true); } #include "kcmdolphinviewmodes.moc" diff --git a/src/settings/serviceitemdelegate.cpp b/src/settings/serviceitemdelegate.cpp index 0f2030484..fa0b306c1 100644 --- a/src/settings/serviceitemdelegate.cpp +++ b/src/settings/serviceitemdelegate.cpp @@ -1,127 +1,127 @@ /*************************************************************************** * Copyright (C) 2011 by Peter Penz * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "serviceitemdelegate.h" #include "servicemodel.h" #include #include #include #include ServiceItemDelegate::ServiceItemDelegate(QAbstractItemView* itemView, QObject* parent) : KWidgetItemDelegate(itemView, parent) { } ServiceItemDelegate::~ServiceItemDelegate() { } QSize ServiceItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { - Q_UNUSED(index); + Q_UNUSED(index) const QStyle *style = itemView()->style(); const int buttonHeight = style->pixelMetric(QStyle::PM_ButtonMargin) * 2 + style->pixelMetric(QStyle::PM_ButtonIconSize); const int fontHeight = option.fontMetrics.height(); return QSize(100, qMax(buttonHeight, fontHeight)); } void ServiceItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { - Q_UNUSED(index); + Q_UNUSED(index) painter->save(); itemView()->style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, painter); if (option.state & QStyle::State_Selected) { painter->setPen(option.palette.highlightedText().color()); } painter->restore(); } QList ServiceItemDelegate::createItemWidgets(const QModelIndex&) const { QCheckBox* checkBox = new QCheckBox(); QPalette palette = checkBox->palette(); palette.setColor(QPalette::WindowText, palette.color(QPalette::Text)); checkBox->setPalette(palette); connect(checkBox, &QCheckBox::clicked, this, &ServiceItemDelegate::slotCheckBoxClicked); QPushButton* configureButton = new QPushButton(); connect(configureButton, &QPushButton::clicked, this, &ServiceItemDelegate::slotConfigureButtonClicked); return {checkBox, configureButton}; } void ServiceItemDelegate::updateItemWidgets(const QList widgets, const QStyleOptionViewItem& option, const QPersistentModelIndex& index) const { QCheckBox* checkBox = static_cast(widgets[0]); QPushButton *configureButton = static_cast(widgets[1]); const int itemHeight = sizeHint(option, index).height(); // Update the checkbox showing the service name and icon const QAbstractItemModel* model = index.model(); checkBox->setText(model->data(index).toString()); const QString iconName = model->data(index, Qt::DecorationRole).toString(); if (!iconName.isEmpty()) { checkBox->setIcon(QIcon::fromTheme(iconName)); } checkBox->setChecked(model->data(index, Qt::CheckStateRole).toBool()); const bool configurable = model->data(index, ServiceModel::ConfigurableRole).toBool(); int checkBoxWidth = option.rect.width(); if (configurable) { checkBoxWidth -= configureButton->sizeHint().width(); } checkBox->resize(checkBoxWidth, checkBox->sizeHint().height()); checkBox->move(0, (itemHeight - checkBox->height()) / 2); // Update the configuration button if (configurable) { configureButton->setEnabled(checkBox->isChecked()); configureButton->setIcon(QIcon::fromTheme(QStringLiteral("configure"))); configureButton->resize(configureButton->sizeHint()); configureButton->move(option.rect.right() - configureButton->width(), (itemHeight - configureButton->height()) / 2); } configureButton->setVisible(configurable); } void ServiceItemDelegate::slotCheckBoxClicked(bool checked) { QAbstractItemModel* model = const_cast(focusedIndex().model()); model->setData(focusedIndex(), checked, Qt::CheckStateRole); } void ServiceItemDelegate::slotConfigureButtonClicked() { emit requestServiceConfiguration(focusedIndex()); } diff --git a/src/settings/servicemodel.cpp b/src/settings/servicemodel.cpp index e3b015147..2dd0097bb 100644 --- a/src/settings/servicemodel.cpp +++ b/src/settings/servicemodel.cpp @@ -1,107 +1,107 @@ /*************************************************************************** * Copyright (C) 2011 by Peter Penz * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "servicemodel.h" ServiceModel::ServiceModel(QObject* parent) : QAbstractListModel(parent), m_items() { } ServiceModel::~ServiceModel() { } bool ServiceModel::insertRows(int row, int count, const QModelIndex& parent) { if (row > rowCount()) { return false; } if (count <= 0) { count = 1; } beginInsertRows(parent, row, row + count - 1); for (int i = 0; i < count; ++i) { ServiceItem item; item.checked = false; item.configurable = false; m_items.insert(row, item); } endInsertRows(); return true; } bool ServiceModel::setData(const QModelIndex& index, const QVariant& value, int role) { const int row = index.row(); if (row >= rowCount()) { return false; } switch (role) { case Qt::CheckStateRole: m_items[row].checked = value.toBool(); break; case ConfigurableRole: m_items[row].configurable = value.toBool(); break; case Qt::DecorationRole: m_items[row].icon = value.toString(); break; case Qt::DisplayRole: m_items[row].text = value.toString(); break; case DesktopEntryNameRole: m_items[row].desktopEntryName = value.toString(); break; default: return false; } emit dataChanged(index, index); return true; } QVariant ServiceModel::data(const QModelIndex& index, int role) const { const int row = index.row(); if (row < rowCount()) { switch (role) { case ConfigurableRole: return m_items[row].configurable; case Qt::CheckStateRole: return m_items[row].checked; case Qt::DecorationRole: return m_items[row].icon; case Qt::DisplayRole: return m_items[row].text; case DesktopEntryNameRole: return m_items[row].desktopEntryName; default: break; } } return QVariant(); } int ServiceModel::rowCount(const QModelIndex& parent) const { - Q_UNUSED(parent); + Q_UNUSED(parent) return m_items.count(); } diff --git a/src/statusbar/dolphinstatusbar.cpp b/src/statusbar/dolphinstatusbar.cpp index 9bb050d05..8a81960af 100644 --- a/src/statusbar/dolphinstatusbar.cpp +++ b/src/statusbar/dolphinstatusbar.cpp @@ -1,338 +1,338 @@ /*************************************************************************** * Copyright (C) 2006-2012 by Peter Penz * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "dolphinstatusbar.h" #include "dolphin_generalsettings.h" #include "statusbarspaceinfo.h" #include "views/dolphinview.h" #include "views/zoomlevelinfo.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace { const int UpdateDelay = 50; } DolphinStatusBar::DolphinStatusBar(QWidget* parent) : QWidget(parent), m_text(), m_defaultText(), m_label(nullptr), m_spaceInfo(nullptr), m_zoomSlider(nullptr), m_progressBar(nullptr), m_stopButton(nullptr), m_progress(100), m_showProgressBarTimer(nullptr), m_delayUpdateTimer(nullptr), m_textTimestamp() { // Initialize text label m_label = new KSqueezedTextLabel(m_text, this); m_label->setWordWrap(true); m_label->setTextFormat(Qt::PlainText); // Initialize zoom widget m_zoomSlider = new QSlider(Qt::Horizontal, this); m_zoomSlider->setAccessibleName(i18n("Zoom")); m_zoomSlider->setAccessibleDescription(i18nc("Description for zoom-slider (accessibility)", "Sets the size of the file icons.")); m_zoomSlider->setPageStep(1); m_zoomSlider->setRange(ZoomLevelInfo::minimumLevel(), ZoomLevelInfo::maximumLevel()); connect(m_zoomSlider, &QSlider::valueChanged, this, &DolphinStatusBar::zoomLevelChanged); connect(m_zoomSlider, &QSlider::valueChanged, this, &DolphinStatusBar::updateZoomSliderToolTip); connect(m_zoomSlider, &QSlider::sliderMoved, this, &DolphinStatusBar::showZoomSliderToolTip); // Initialize space information m_spaceInfo = new StatusBarSpaceInfo(this); // Initialize progress information m_stopButton = new QToolButton(this); m_stopButton->setIcon(QIcon::fromTheme(QStringLiteral("process-stop"))); m_stopButton->setAccessibleName(i18n("Stop")); m_stopButton->setAutoRaise(true); m_stopButton->setToolTip(i18nc("@tooltip", "Stop loading")); m_stopButton->hide(); connect(m_stopButton, &QToolButton::clicked, this, &DolphinStatusBar::stopPressed); m_progressTextLabel = new QLabel(this); m_progressTextLabel->hide(); m_progressBar = new QProgressBar(this); m_progressBar->hide(); m_showProgressBarTimer = new QTimer(this); m_showProgressBarTimer->setInterval(500); 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 top layout and size policies const int fontHeight = QFontMetrics(m_label->font()).height(); const int zoomSliderHeight = m_zoomSlider->minimumSizeHint().height(); const int buttonHeight = m_stopButton->height(); const int contentHeight = qMax(qMax(fontHeight, zoomSliderHeight), buttonHeight); QFontMetrics fontMetrics(m_label->font()); m_label->setFixedHeight(contentHeight); m_label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); m_zoomSlider->setMaximumWidth(fontMetrics.averageCharWidth() * 25); m_spaceInfo->setFixedHeight(zoomSliderHeight); m_spaceInfo->setMaximumWidth(fontMetrics.averageCharWidth() * 25); m_spaceInfo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); m_progressBar->setFixedHeight(zoomSliderHeight); m_progressBar->setMaximumWidth(fontMetrics.averageCharWidth() * 25); QHBoxLayout* topLayout = new QHBoxLayout(this); topLayout->setContentsMargins(2, 0, 2, 0); topLayout->setSpacing(4); topLayout->addWidget(m_label, 1); topLayout->addWidget(m_zoomSlider, 1); topLayout->addWidget(m_spaceInfo, 1); topLayout->addWidget(m_stopButton); topLayout->addWidget(m_progressTextLabel); topLayout->addWidget(m_progressBar); setExtensionsVisible(true); setWhatsThis(xi18nc("@info:whatsthis Statusbar", "This is " "the Statusbar. It contains three elements " "by default (left to right):A text field" " that displays the size of selected items. If only " "one item is selected the name and type is shown as well." "A zoom slider that allows you " "to adjust the size of the icons in the view." "Space information about the " "current storage device.")); } DolphinStatusBar::~DolphinStatusBar() { } 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::setProgressText(const QString& text) { m_progressTextLabel->setText(text); } QString DolphinStatusBar::progressText() const { return m_progressTextLabel->text(); } void DolphinStatusBar::setProgress(int percent) { // Show a busy indicator if a value < 0 is provided: m_progressBar->setMaximum((percent < 0) ? 0 : 100); percent = qBound(0, percent, 100); const bool progressRestarted = (percent < 100) && (percent < m_progress); m_progress = percent; if (progressRestarted && !m_progressBar->isVisible()) { // Show the progress bar delayed: In the case if 100 % are reached within // a short time, no progress bar will be shown at all. m_showProgressBarTimer->start(); } m_progressBar->setValue(m_progress); if (percent == 100) { // The end of the progress has been reached. Assure that the progress bar // gets hidden and the extensions widgets get visible again. m_showProgressBarTimer->stop(); updateProgressInfo(); } } int DolphinStatusBar::progress() const { return m_progress; } void DolphinStatusBar::resetToDefaultText() { m_text.clear(); QTime currentTime; if (currentTime.msecsTo(m_textTimestamp) < UpdateDelay) { m_delayUpdateTimer->start(); } else { updateLabelText(); } } void DolphinStatusBar::setDefaultText(const QString& text) { m_defaultText = text; updateLabelText(); } QString DolphinStatusBar::defaultText() const { return m_defaultText; } void DolphinStatusBar::setUrl(const QUrl& url) { m_spaceInfo->setUrl(url); } QUrl DolphinStatusBar::url() const { return m_spaceInfo->url(); } void DolphinStatusBar::setZoomLevel(int zoomLevel) { if (zoomLevel != m_zoomSlider->value()) { m_zoomSlider->setValue(zoomLevel); } } int DolphinStatusBar::zoomLevel() const { return m_zoomSlider->value(); } void DolphinStatusBar::readSettings() { setExtensionsVisible(true); } void DolphinStatusBar::updateSpaceInfo() { m_spaceInfo->update(); } void DolphinStatusBar::contextMenuEvent(QContextMenuEvent* event) { - Q_UNUSED(event); + Q_UNUSED(event) QMenu menu(this); QAction* showZoomSliderAction = menu.addAction(i18nc("@action:inmenu", "Show Zoom Slider")); showZoomSliderAction->setCheckable(true); showZoomSliderAction->setChecked(GeneralSettings::showZoomSlider()); QAction* showSpaceInfoAction = menu.addAction(i18nc("@action:inmenu", "Show Space Information")); showSpaceInfoAction->setCheckable(true); showSpaceInfoAction->setChecked(GeneralSettings::showSpaceInfo()); const QAction* action = menu.exec(QCursor::pos()); if (action == showZoomSliderAction) { const bool visible = showZoomSliderAction->isChecked(); GeneralSettings::setShowZoomSlider(visible); m_zoomSlider->setVisible(visible); } else if (action == showSpaceInfoAction) { const bool visible = showSpaceInfoAction->isChecked(); GeneralSettings::setShowSpaceInfo(visible); m_spaceInfo->setVisible(visible); } } void DolphinStatusBar::showZoomSliderToolTip(int zoomLevel) { updateZoomSliderToolTip(zoomLevel); QPoint global = m_zoomSlider->rect().topLeft(); global.ry() += m_zoomSlider->height() / 2; QHelpEvent toolTipEvent(QEvent::ToolTip, QPoint(0, 0), m_zoomSlider->mapToGlobal(global)); QApplication::sendEvent(m_zoomSlider, &toolTipEvent); } void DolphinStatusBar::updateProgressInfo() { if (m_progress < 100) { // Show the progress information and hide the extensions m_stopButton->show(); m_progressTextLabel->show(); m_progressBar->show(); setExtensionsVisible(false); } else { // Hide the progress information and show the extensions m_stopButton->hide(); m_progressTextLabel->hide(); m_progressBar->hide(); setExtensionsVisible(true); } } void DolphinStatusBar::updateLabelText() { const QString text = m_text.isEmpty() ? m_defaultText : m_text; m_label->setText(text); } void DolphinStatusBar::updateZoomSliderToolTip(int zoomLevel) { const int size = ZoomLevelInfo::iconSizeForZoomLevel(zoomLevel); m_zoomSlider->setToolTip(i18ncp("@info:tooltip", "Size: 1 pixel", "Size: %1 pixels", size)); } void DolphinStatusBar::setExtensionsVisible(bool visible) { bool showSpaceInfo = visible; bool showZoomSlider = visible; if (visible) { showSpaceInfo = GeneralSettings::showSpaceInfo(); showZoomSlider = GeneralSettings::showZoomSlider(); } m_spaceInfo->setVisible(showSpaceInfo); m_zoomSlider->setVisible(showZoomSlider); } diff --git a/src/tests/kfileitemmodelbenchmark.cpp b/src/tests/kfileitemmodelbenchmark.cpp index 64ad116f6..8dfb4f622 100644 --- a/src/tests/kfileitemmodelbenchmark.cpp +++ b/src/tests/kfileitemmodelbenchmark.cpp @@ -1,222 +1,222 @@ /*************************************************************************** * Copyright (C) 2011 by Peter Penz * * Copyright (C) 2013 by Frank Reininghaus * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include #include #include #include "kitemviews/kfileitemmodel.h" #include "kitemviews/private/kfileitemmodelsortalgorithm.h" void myMessageOutput(QtMsgType type, const QMessageLogContext& context, const QString& msg) { - Q_UNUSED(context); + Q_UNUSED(context) switch (type) { case QtDebugMsg: break; case QtWarningMsg: break; case QtCriticalMsg: fprintf(stderr, "Critical: %s\n", msg.toLocal8Bit().data()); break; case QtFatalMsg: fprintf(stderr, "Fatal: %s\n", msg.toLocal8Bit().data()); abort(); default: break; } } Q_DECLARE_METATYPE(KFileItemList) Q_DECLARE_METATYPE(KItemRangeList) class KFileItemModelBenchmark : public QObject { Q_OBJECT public: KFileItemModelBenchmark(); private slots: void insertAndRemoveManyItems_data(); void insertAndRemoveManyItems(); private: static KFileItemList createFileItemList(const QStringList& fileNames, const QString& urlPrefix = QLatin1String("file:///")); }; KFileItemModelBenchmark::KFileItemModelBenchmark() { } void KFileItemModelBenchmark::insertAndRemoveManyItems_data() { QTest::addColumn("initialItems"); QTest::addColumn("newItems"); QTest::addColumn("removedItems"); QTest::addColumn("expectedFinalItems"); QTest::addColumn("expectedItemsInserted"); QTest::addColumn("expectedItemsRemoved"); QList sizes; sizes << 100000; foreach (int n, sizes) { QStringList allStrings; for (int i = 0; i < n; ++i) { allStrings << QString::number(i); } // We want to keep the sorting overhead in the benchmark low. // Therefore, we do not use natural sorting. However, this // means that our list is currently not sorted. allStrings.sort(); KFileItemList all = createFileItemList(allStrings); KFileItemList firstHalf, secondHalf, even, odd; for (int i = 0; i < n; ++i) { if (i < n / 2) { firstHalf << all.at(i); } else { secondHalf << all.at(i); } if (i % 2 == 0) { even << all.at(i); } else { odd << all.at(i); } } KItemRangeList itemRangeListFirstHalf; itemRangeListFirstHalf << KItemRange(0, firstHalf.count()); KItemRangeList itemRangeListSecondHalf; itemRangeListSecondHalf << KItemRange(firstHalf.count(), secondHalf.count()); KItemRangeList itemRangeListOddInserted, itemRangeListOddRemoved; for (int i = 0; i < odd.count(); ++i) { // Note that the index in the KItemRange is the index of // the model *before* the items have been inserted. itemRangeListOddInserted << KItemRange(i + 1, 1); itemRangeListOddRemoved << KItemRange(2 * i + 1, 1); } const int bufferSize = 128; char buffer[bufferSize]; snprintf(buffer, bufferSize, "all--n=%i", n); QTest::newRow(buffer) << all << KFileItemList() << KFileItemList() << all << KItemRangeList() << KItemRangeList(); snprintf(buffer, bufferSize, "1st half + 2nd half--n=%i", n); QTest::newRow(buffer) << firstHalf << secondHalf << KFileItemList() << all << itemRangeListSecondHalf << KItemRangeList(); snprintf(buffer, bufferSize, "2nd half + 1st half--n=%i", n); QTest::newRow(buffer) << secondHalf << firstHalf << KFileItemList() << all << itemRangeListFirstHalf << KItemRangeList(); snprintf(buffer, bufferSize, "even + odd--n=%i", n); QTest::newRow(buffer) << even << odd << KFileItemList() << all << itemRangeListOddInserted << KItemRangeList(); snprintf(buffer, bufferSize, "all - 2nd half--n=%i", n); QTest::newRow(buffer) << all << KFileItemList() << secondHalf << firstHalf << KItemRangeList() << itemRangeListSecondHalf; snprintf(buffer, bufferSize, "all - 1st half--n=%i", n); QTest::newRow(buffer) << all << KFileItemList() << firstHalf << secondHalf << KItemRangeList() << itemRangeListFirstHalf; snprintf(buffer, bufferSize, "all - odd--n=%i", n); QTest::newRow(buffer) << all << KFileItemList() << odd << even << KItemRangeList() << itemRangeListOddRemoved; } } void KFileItemModelBenchmark::insertAndRemoveManyItems() { QFETCH(KFileItemList, initialItems); QFETCH(KFileItemList, newItems); QFETCH(KFileItemList, removedItems); QFETCH(KFileItemList, expectedFinalItems); QFETCH(KItemRangeList, expectedItemsInserted); QFETCH(KItemRangeList, expectedItemsRemoved); KFileItemModel model; // Avoid overhead caused by natural sorting // and determining the isDir/isLink roles. model.m_naturalSorting = false; model.setRoles({"text"}); QSignalSpy spyItemsInserted(&model, &KFileItemModel::itemsInserted); QSignalSpy spyItemsRemoved(&model, &KFileItemModel::itemsRemoved); QBENCHMARK { model.slotClear(); model.slotItemsAdded(model.directory(), initialItems); model.slotCompleted(); QCOMPARE(model.count(), initialItems.count()); if (!newItems.isEmpty()) { model.slotItemsAdded(model.directory(), newItems); model.slotCompleted(); } QCOMPARE(model.count(), initialItems.count() + newItems.count()); if (!removedItems.isEmpty()) { model.slotItemsDeleted(removedItems); } QCOMPARE(model.count(), initialItems.count() + newItems.count() - removedItems.count()); } QVERIFY(model.isConsistent()); for (int i = 0; i < model.count(); ++i) { QCOMPARE(model.fileItem(i), expectedFinalItems.at(i)); } if (!expectedItemsInserted.empty()) { QVERIFY(!spyItemsInserted.empty()); const KItemRangeList actualItemsInserted = spyItemsInserted.last().first().value(); QCOMPARE(actualItemsInserted, expectedItemsInserted); } if (!expectedItemsRemoved.empty()) { QVERIFY(!spyItemsRemoved.empty()); const KItemRangeList actualItemsRemoved = spyItemsRemoved.last().first().value(); QCOMPARE(actualItemsRemoved, expectedItemsRemoved); } } KFileItemList KFileItemModelBenchmark::createFileItemList(const QStringList& fileNames, const QString& prefix) { // Suppress 'file does not exist anymore' messages from KFileItemPrivate::init(). qInstallMessageHandler(myMessageOutput); KFileItemList result; foreach (const QString& name, fileNames) { const KFileItem item(QUrl::fromLocalFile(prefix + name), QString(), KFileItem::Unknown); result << item; } return result; } QTEST_MAIN(KFileItemModelBenchmark) #include "kfileitemmodelbenchmark.moc" diff --git a/src/tests/kfileitemmodeltest.cpp b/src/tests/kfileitemmodeltest.cpp index f081eba86..0c38f02bf 100644 --- a/src/tests/kfileitemmodeltest.cpp +++ b/src/tests/kfileitemmodeltest.cpp @@ -1,1758 +1,1758 @@ /*************************************************************************** * Copyright (C) 2011 by Peter Penz * * Copyright (C) 2011 by Frank Reininghaus * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include #include #include #include #include #include "kitemviews/kfileitemmodel.h" #include "kitemviews/private/kfileitemmodeldirlister.h" #include "testdir.h" void myMessageOutput(QtMsgType type, const QMessageLogContext& context, const QString& msg) { - Q_UNUSED(context); + Q_UNUSED(context) switch (type) { case QtDebugMsg: break; case QtWarningMsg: break; case QtCriticalMsg: fprintf(stderr, "Critical: %s\n", msg.toLocal8Bit().data()); break; case QtFatalMsg: fprintf(stderr, "Fatal: %s\n", msg.toLocal8Bit().data()); abort(); default: break; } } Q_DECLARE_METATYPE(KItemRange) Q_DECLARE_METATYPE(KItemRangeList) Q_DECLARE_METATYPE(QList) class KFileItemModelTest : public QObject { Q_OBJECT private slots: void init(); void cleanup(); void testDefaultRoles(); void testDefaultSortRole(); void testDefaultGroupedSorting(); void testNewItems(); void testRemoveItems(); void testDirLoadingCompleted(); void testSetData(); void testSetDataWithModifiedSortRole_data(); void testSetDataWithModifiedSortRole(); void testChangeSortRole(); void testResortAfterChangingName(); void testModelConsistencyWhenInsertingItems(); void testItemRangeConsistencyWhenInsertingItems(); void testExpandItems(); void testExpandParentItems(); void testMakeExpandedItemHidden(); void testRemoveFilteredExpandedItems(); void testSorting(); void testIndexForKeyboardSearch(); void testNameFilter(); void testEmptyPath(); void testRefreshExpandedItem(); void testRemoveHiddenItems(); void collapseParentOfHiddenItems(); void removeParentOfHiddenItems(); void testGeneralParentChildRelationships(); void testNameRoleGroups(); void testNameRoleGroupsWithExpandedItems(); void testInconsistentModel(); void testChangeRolesForFilteredItems(); void testChangeSortRoleWhileFiltering(); void testRefreshFilteredItems(); void testCollapseFolderWhileLoading(); void testCreateMimeData(); void testDeleteFileMoreThanOnce(); private: QStringList itemsInModel() const; private: KFileItemModel* m_model; TestDir* m_testDir; }; void KFileItemModelTest::init() { // The item-model tests result in a huge number of debugging // output from kdelibs. Only show critical and fatal messages. qInstallMessageHandler(myMessageOutput); qRegisterMetaType("KItemRange"); qRegisterMetaType("KItemRangeList"); qRegisterMetaType("KFileItemList"); m_testDir = new TestDir(); m_model = new KFileItemModel(); m_model->m_dirLister->setAutoUpdate(false); // Reduce the timer interval to make the test run faster. m_model->m_resortAllItemsTimer->setInterval(0); } void KFileItemModelTest::cleanup() { delete m_model; m_model = nullptr; delete m_testDir; m_testDir = nullptr; } void KFileItemModelTest::testDefaultRoles() { const QSet roles = m_model->roles(); QCOMPARE(roles.count(), 4); QVERIFY(roles.contains("text")); QVERIFY(roles.contains("isDir")); QVERIFY(roles.contains("isLink")); QVERIFY(roles.contains("isHidden")); } void KFileItemModelTest::testDefaultSortRole() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); QVERIFY(itemsInsertedSpy.isValid()); QCOMPARE(m_model->sortRole(), QByteArray("text")); m_testDir->createFiles({"c.txt", "a.txt", "b.txt"}); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(m_model->count(), 3); QCOMPARE(m_model->data(0).value("text").toString(), QString("a.txt")); QCOMPARE(m_model->data(1).value("text").toString(), QString("b.txt")); QCOMPARE(m_model->data(2).value("text").toString(), QString("c.txt")); } void KFileItemModelTest::testDefaultGroupedSorting() { QCOMPARE(m_model->groupedSorting(), false); } void KFileItemModelTest::testNewItems() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); m_testDir->createFiles({"a.txt", "b.txt", "c.txt"}); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(m_model->count(), 3); QVERIFY(m_model->isConsistent()); } void KFileItemModelTest::testRemoveItems() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); QSignalSpy itemsRemovedSpy(m_model, &KFileItemModel::itemsRemoved); m_testDir->createFiles({"a.txt", "b.txt"}); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(m_model->count(), 2); QVERIFY(m_model->isConsistent()); m_testDir->removeFile("a.txt"); m_model->m_dirLister->updateDirectory(m_testDir->url()); QVERIFY(itemsRemovedSpy.wait()); QCOMPARE(m_model->count(), 1); QVERIFY(m_model->isConsistent()); } void KFileItemModelTest::testDirLoadingCompleted() { QSignalSpy loadingCompletedSpy(m_model, &KFileItemModel::directoryLoadingCompleted); QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); QSignalSpy itemsRemovedSpy(m_model, &KFileItemModel::itemsRemoved); m_testDir->createFiles({"a.txt", "b.txt", "c.txt"}); m_model->loadDirectory(m_testDir->url()); QVERIFY(loadingCompletedSpy.wait()); QCOMPARE(loadingCompletedSpy.count(), 1); QCOMPARE(itemsInsertedSpy.count(), 1); QCOMPARE(itemsRemovedSpy.count(), 0); QCOMPARE(m_model->count(), 3); m_testDir->createFiles({"d.txt", "e.txt"}); m_model->m_dirLister->updateDirectory(m_testDir->url()); QVERIFY(loadingCompletedSpy.wait()); QCOMPARE(loadingCompletedSpy.count(), 2); QCOMPARE(itemsInsertedSpy.count(), 2); QCOMPARE(itemsRemovedSpy.count(), 0); QCOMPARE(m_model->count(), 5); m_testDir->removeFile("a.txt"); m_testDir->createFile("f.txt"); m_model->m_dirLister->updateDirectory(m_testDir->url()); QVERIFY(loadingCompletedSpy.wait()); QCOMPARE(loadingCompletedSpy.count(), 3); QCOMPARE(itemsInsertedSpy.count(), 3); QCOMPARE(itemsRemovedSpy.count(), 1); QCOMPARE(m_model->count(), 5); m_testDir->removeFile("b.txt"); m_model->m_dirLister->updateDirectory(m_testDir->url()); QVERIFY(itemsRemovedSpy.wait()); QCOMPARE(loadingCompletedSpy.count(), 4); QCOMPARE(itemsInsertedSpy.count(), 3); QCOMPARE(itemsRemovedSpy.count(), 2); QCOMPARE(m_model->count(), 4); QVERIFY(m_model->isConsistent()); } void KFileItemModelTest::testSetData() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); QVERIFY(itemsInsertedSpy.isValid()); QSignalSpy itemsChangedSpy(m_model, &KFileItemModel::itemsChanged); QVERIFY(itemsChangedSpy.isValid()); m_testDir->createFile("a.txt"); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); QHash values; values.insert("customRole1", "Test1"); values.insert("customRole2", "Test2"); m_model->setData(0, values); QCOMPARE(itemsChangedSpy.count(), 1); values = m_model->data(0); QCOMPARE(values.value("customRole1").toString(), QString("Test1")); QCOMPARE(values.value("customRole2").toString(), QString("Test2")); QVERIFY(m_model->isConsistent()); } void KFileItemModelTest::testSetDataWithModifiedSortRole_data() { QTest::addColumn("changedIndex"); QTest::addColumn("changedRating"); QTest::addColumn("expectMoveSignal"); QTest::addColumn("ratingIndex0"); QTest::addColumn("ratingIndex1"); QTest::addColumn("ratingIndex2"); // Default setup: // Index 0 = rating 2 // Index 1 = rating 4 // Index 2 = rating 6 QTest::newRow("Index 0: Rating 3") << 0 << 3 << false << 3 << 4 << 6; QTest::newRow("Index 0: Rating 5") << 0 << 5 << true << 4 << 5 << 6; QTest::newRow("Index 0: Rating 8") << 0 << 8 << true << 4 << 6 << 8; QTest::newRow("Index 2: Rating 1") << 2 << 1 << true << 1 << 2 << 4; QTest::newRow("Index 2: Rating 3") << 2 << 3 << true << 2 << 3 << 4; QTest::newRow("Index 2: Rating 5") << 2 << 5 << false << 2 << 4 << 5; } void KFileItemModelTest::testSetDataWithModifiedSortRole() { QFETCH(int, changedIndex); QFETCH(int, changedRating); QFETCH(bool, expectMoveSignal); QFETCH(int, ratingIndex0); QFETCH(int, ratingIndex1); QFETCH(int, ratingIndex2); QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); QVERIFY(itemsInsertedSpy.isValid()); QSignalSpy itemsMovedSpy(m_model, &KFileItemModel::itemsMoved); QVERIFY(itemsMovedSpy.isValid()); // Changing the value of a sort-role must result in // a reordering of the items. QCOMPARE(m_model->sortRole(), QByteArray("text")); m_model->setSortRole("rating"); QCOMPARE(m_model->sortRole(), QByteArray("rating")); m_testDir->createFiles({"a.txt", "b.txt", "c.txt"}); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); // Fill the "rating" role of each file: // a.txt -> 2 // b.txt -> 4 // c.txt -> 6 QHash ratingA; ratingA.insert("rating", 2); m_model->setData(0, ratingA); QHash ratingB; ratingB.insert("rating", 4); m_model->setData(1, ratingB); QHash ratingC; ratingC.insert("rating", 6); m_model->setData(2, ratingC); QCOMPARE(m_model->data(0).value("rating").toInt(), 2); QCOMPARE(m_model->data(1).value("rating").toInt(), 4); QCOMPARE(m_model->data(2).value("rating").toInt(), 6); // Now change the rating from a.txt. This usually results // in reordering of the items. QHash rating; rating.insert("rating", changedRating); m_model->setData(changedIndex, rating); if (expectMoveSignal) { QVERIFY(itemsMovedSpy.wait()); } QCOMPARE(m_model->data(0).value("rating").toInt(), ratingIndex0); QCOMPARE(m_model->data(1).value("rating").toInt(), ratingIndex1); QCOMPARE(m_model->data(2).value("rating").toInt(), ratingIndex2); QVERIFY(m_model->isConsistent()); } void KFileItemModelTest::testChangeSortRole() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); QSignalSpy itemsMovedSpy(m_model, &KFileItemModel::itemsMoved); QVERIFY(itemsMovedSpy.isValid()); QCOMPARE(m_model->sortRole(), QByteArray("text")); m_testDir->createFiles({"a.txt", "b.jpg", "c.txt"}); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "b.jpg" << "c.txt"); // Simulate that KFileItemModelRolesUpdater determines the mime type. // Resorting the files by 'type' will only work immediately if their // mime types are known. for (int index = 0; index < m_model->count(); ++index) { m_model->fileItem(index).determineMimeType(); } // Now: sort by type. m_model->setSortRole("type"); QCOMPARE(m_model->sortRole(), QByteArray("type")); QVERIFY(!itemsMovedSpy.isEmpty()); // The actual order of the files might depend on the translation of the // result of KFileItem::mimeComment() in the user's language. QStringList version1; version1 << "b.jpg" << "a.txt" << "c.txt"; QStringList version2; version2 << "a.txt" << "c.txt" << "b.jpg"; const bool ok1 = (itemsInModel() == version1); const bool ok2 = (itemsInModel() == version2); QVERIFY(ok1 || ok2); } void KFileItemModelTest::testResortAfterChangingName() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); QSignalSpy itemsMovedSpy(m_model, &KFileItemModel::itemsMoved); QVERIFY(itemsMovedSpy.isValid()); // We sort by size in a directory where all files have the same size. // Therefore, the files are sorted by their names. m_model->setSortRole("size"); m_testDir->createFiles({"a.txt", "b.txt", "c.txt"}); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "b.txt" << "c.txt"); // We rename a.txt to d.txt. Even though the size has not changed at all, // the model must re-sort the items. QHash data; data.insert("text", "d.txt"); m_model->setData(0, data); QVERIFY(itemsMovedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "b.txt" << "c.txt" << "d.txt"); // We rename d.txt back to a.txt using the dir lister's refreshItems() signal. const KFileItem fileItemD = m_model->fileItem(2); KFileItem fileItemA = fileItemD; QUrl urlA = fileItemA.url().adjusted(QUrl::RemoveFilename); urlA.setPath(urlA.path() + "a.txt"); fileItemA.setUrl(urlA); m_model->slotRefreshItems({qMakePair(fileItemD, fileItemA)}); QVERIFY(itemsMovedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "b.txt" << "c.txt"); } void KFileItemModelTest::testModelConsistencyWhenInsertingItems() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); // KFileItemModel prevents that inserting a punch of items sequentially // results in an itemsInserted()-signal for each item. Instead internally // a timeout is given that collects such operations and results in only // one itemsInserted()-signal. However in this test we want to stress // KFileItemModel to do a lot of insert operation and hence decrease // the timeout to 1 millisecond. m_testDir->createFile("1"); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(m_model->count(), 1); // Insert 10 items for 20 times. After each insert operation the model consistency // is checked. QSet insertedItems; for (int i = 0; i < 20; ++i) { itemsInsertedSpy.clear(); for (int j = 0; j < 10; ++j) { int itemName = qrand(); while (insertedItems.contains(itemName)) { itemName = qrand(); } insertedItems.insert(itemName); m_testDir->createFile(QString::number(itemName)); } m_model->m_dirLister->updateDirectory(m_testDir->url()); if (itemsInsertedSpy.isEmpty()) { QVERIFY(itemsInsertedSpy.wait()); } QVERIFY(m_model->isConsistent()); } QCOMPARE(m_model->count(), 201); } void KFileItemModelTest::testItemRangeConsistencyWhenInsertingItems() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); m_testDir->createFiles({"B", "E", "G"}); // Due to inserting the 3 items one item-range with index == 0 and // count == 3 must be given m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInsertedSpy.count(), 1); QList arguments = itemsInsertedSpy.takeFirst(); KItemRangeList itemRangeList = arguments.at(0).value(); QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(0, 3)); // The indexes of the item-ranges must always be related to the model before // the items have been inserted. Having: // 0 1 2 // B E G // and inserting A, C, D, F the resulting model will be: // 0 1 2 3 4 5 6 // A B C D E F G // and the item-ranges must be: // index: 0, count: 1 for A // index: 1, count: 2 for B, C // index: 2, count: 1 for G m_testDir->createFiles({"A", "C", "D", "F"}); m_model->m_dirLister->updateDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInsertedSpy.count(), 1); arguments = itemsInsertedSpy.takeFirst(); itemRangeList = arguments.at(0).value(); QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(0, 1) << KItemRange(1, 2) << KItemRange(2, 1)); } void KFileItemModelTest::testExpandItems() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); QVERIFY(itemsInsertedSpy.isValid()); QSignalSpy itemsRemovedSpy(m_model, &KFileItemModel::itemsRemoved); QVERIFY(itemsRemovedSpy.isValid()); QSignalSpy loadingCompletedSpy(m_model, &KFileItemModel::directoryLoadingCompleted); QVERIFY(loadingCompletedSpy.isValid()); // Test expanding subfolders in a folder with the items "a/", "a/a/", "a/a/1", "a/a-1/", "a/a-1/1". // Besides testing the basic item expansion functionality, the test makes sure that // KFileItemModel::expansionLevelsCompare(const KFileItem& a, const KFileItem& b) // yields the correct result for "a/a/1" and "a/a-1/", whis is non-trivial because they share the // first three characters. QSet originalModelRoles = m_model->roles(); QSet modelRoles = originalModelRoles; modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; m_model->setRoles(modelRoles); m_testDir->createFiles({"a/a/1", "a/a-1/1"}); // Store the URLs of all folders in a set. QSet allFolders; allFolders << QUrl::fromLocalFile(m_testDir->path() + "/a") << QUrl::fromLocalFile(m_testDir->path() + "/a/a") << QUrl::fromLocalFile(m_testDir->path() + "/a/a-1"); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); // So far, the model contains only "a/" QCOMPARE(m_model->count(), 1); QVERIFY(m_model->isExpandable(0)); QVERIFY(!m_model->isExpanded(0)); QVERIFY(m_model->expandedDirectories().empty()); QCOMPARE(itemsInsertedSpy.count(), 1); KItemRangeList itemRangeList = itemsInsertedSpy.takeFirst().at(0).value(); QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(0, 1)); // 1 new item "a/" with index 0 // Expand the folder "a/" -> "a/a/" and "a/a-1/" become visible m_model->setExpanded(0, true); QVERIFY(m_model->isExpanded(0)); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(m_model->count(), 3); // 3 items: "a/", "a/a/", "a/a-1/" QCOMPARE(m_model->expandedDirectories(), QSet() << QUrl::fromLocalFile(m_testDir->path() + "/a")); QCOMPARE(itemsInsertedSpy.count(), 1); itemRangeList = itemsInsertedSpy.takeFirst().at(0).value(); QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(1, 2)); // 2 new items "a/a/" and "a/a-1/" with indices 1 and 2 QVERIFY(m_model->isExpandable(1)); QVERIFY(!m_model->isExpanded(1)); QVERIFY(m_model->isExpandable(2)); QVERIFY(!m_model->isExpanded(2)); // Expand the folder "a/a/" -> "a/a/1" becomes visible m_model->setExpanded(1, true); QVERIFY(m_model->isExpanded(1)); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(m_model->count(), 4); // 4 items: "a/", "a/a/", "a/a/1", "a/a-1/" QCOMPARE(m_model->expandedDirectories(), QSet() << QUrl::fromLocalFile(m_testDir->path() + "/a") << QUrl::fromLocalFile(m_testDir->path() + "/a/a")); QCOMPARE(itemsInsertedSpy.count(), 1); itemRangeList = itemsInsertedSpy.takeFirst().at(0).value(); QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(2, 1)); // 1 new item "a/a/1" with index 2 QVERIFY(!m_model->isExpandable(2)); QVERIFY(!m_model->isExpanded(2)); // Expand the folder "a/a-1/" -> "a/a-1/1" becomes visible m_model->setExpanded(3, true); QVERIFY(m_model->isExpanded(3)); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(m_model->count(), 5); // 5 items: "a/", "a/a/", "a/a/1", "a/a-1/", "a/a-1/1" QCOMPARE(m_model->expandedDirectories(), allFolders); QCOMPARE(itemsInsertedSpy.count(), 1); itemRangeList = itemsInsertedSpy.takeFirst().at(0).value(); QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(4, 1)); // 1 new item "a/a-1/1" with index 4 QVERIFY(!m_model->isExpandable(4)); QVERIFY(!m_model->isExpanded(4)); // Collapse the top-level folder -> all other items should disappear m_model->setExpanded(0, false); QVERIFY(!m_model->isExpanded(0)); QCOMPARE(m_model->count(), 1); QVERIFY(!m_model->expandedDirectories().contains(QUrl::fromLocalFile(m_testDir->path() + "/a"))); // TODO: Make sure that child URLs are also removed QCOMPARE(itemsRemovedSpy.count(), 1); itemRangeList = itemsRemovedSpy.takeFirst().at(0).value(); QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(1, 4)); // 4 items removed QVERIFY(m_model->isConsistent()); // Clear the model, reload the folder and try to restore the expanded folders. m_model->clear(); QCOMPARE(m_model->count(), 0); QVERIFY(m_model->expandedDirectories().empty()); m_model->loadDirectory(m_testDir->url()); m_model->restoreExpandedDirectories(allFolders); QVERIFY(loadingCompletedSpy.wait()); QCOMPARE(m_model->count(), 5); // 5 items: "a/", "a/a/", "a/a/1", "a/a-1/", "a/a-1/1" QVERIFY(m_model->isExpanded(0)); QVERIFY(m_model->isExpanded(1)); QVERIFY(!m_model->isExpanded(2)); QVERIFY(m_model->isExpanded(3)); QVERIFY(!m_model->isExpanded(4)); QCOMPARE(m_model->expandedDirectories(), allFolders); QVERIFY(m_model->isConsistent()); // Move to a sub folder, then call restoreExpandedFolders() *before* going back. // This is how DolphinView restores the expanded folders when navigating in history. m_model->loadDirectory(QUrl::fromLocalFile(m_testDir->path() + "/a/a/")); QVERIFY(loadingCompletedSpy.wait()); QCOMPARE(m_model->count(), 1); // 1 item: "1" m_model->restoreExpandedDirectories(allFolders); m_model->loadDirectory(m_testDir->url()); QVERIFY(loadingCompletedSpy.wait()); QCOMPARE(m_model->count(), 5); // 5 items: "a/", "a/a/", "a/a/1", "a/a-1/", "a/a-1/1" QCOMPARE(m_model->expandedDirectories(), allFolders); // Remove all expanded items by changing the roles itemsRemovedSpy.clear(); m_model->setRoles(originalModelRoles); QVERIFY(!m_model->isExpanded(0)); QCOMPARE(m_model->count(), 1); QVERIFY(!m_model->expandedDirectories().contains(QUrl::fromLocalFile(m_testDir->path() + "/a"))); QCOMPARE(itemsRemovedSpy.count(), 1); itemRangeList = itemsRemovedSpy.takeFirst().at(0).value(); QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(1, 4)); // 4 items removed QVERIFY(m_model->isConsistent()); } void KFileItemModelTest::testExpandParentItems() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); QSignalSpy loadingCompletedSpy(m_model, &KFileItemModel::directoryLoadingCompleted); QVERIFY(loadingCompletedSpy.isValid()); // Create a tree structure of folders: // a 1/ // a 1/b1/ // a 1/b1/c1/ // a2/ // a2/b2/ // a2/b2/c2/ // a2/b2/c2/d2/ QSet modelRoles = m_model->roles(); modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; m_model->setRoles(modelRoles); m_testDir->createFiles({"a 1/b1/c1/file.txt", "a2/b2/c2/d2/file.txt"}); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); // So far, the model contains only "a 1/" and "a2/". QCOMPARE(m_model->count(), 2); QVERIFY(m_model->expandedDirectories().empty()); // Expand the parents of "a2/b2/c2". m_model->expandParentDirectories(QUrl::fromLocalFile(m_testDir->path() + "a2/b2/c2")); QVERIFY(loadingCompletedSpy.wait()); // The model should now contain "a 1/", "a2/", "a2/b2/", and "a2/b2/c2/". // It's important that only the parents of "a1/b1/c1" are expanded. QCOMPARE(m_model->count(), 4); QVERIFY(!m_model->isExpanded(0)); QVERIFY(m_model->isExpanded(1)); QVERIFY(m_model->isExpanded(2)); QVERIFY(!m_model->isExpanded(3)); // Expand the parents of "a 1/b1". m_model->expandParentDirectories(QUrl::fromLocalFile(m_testDir->path() + "a 1/b1")); QVERIFY(loadingCompletedSpy.wait()); // The model should now contain "a 1/", "a 1/b1/", "a2/", "a2/b2", and "a2/b2/c2/". // It's important that only the parents of "a 1/b1/" and "a2/b2/c2/" are expanded. QCOMPARE(m_model->count(), 5); QVERIFY(m_model->isExpanded(0)); QVERIFY(!m_model->isExpanded(1)); QVERIFY(m_model->isExpanded(2)); QVERIFY(m_model->isExpanded(3)); QVERIFY(!m_model->isExpanded(4)); QVERIFY(m_model->isConsistent()); // Expand "a 1/b1/". m_model->setExpanded(1, true); QVERIFY(loadingCompletedSpy.wait()); QCOMPARE(m_model->count(), 6); QVERIFY(m_model->isExpanded(0)); QVERIFY(m_model->isExpanded(1)); QVERIFY(!m_model->isExpanded(2)); QVERIFY(m_model->isExpanded(3)); QVERIFY(m_model->isExpanded(4)); QVERIFY(!m_model->isExpanded(5)); QVERIFY(m_model->isConsistent()); // Collapse "a 1/b1/" again, and verify that the previous state is restored. m_model->setExpanded(1, false); QCOMPARE(m_model->count(), 5); QVERIFY(m_model->isExpanded(0)); QVERIFY(!m_model->isExpanded(1)); QVERIFY(m_model->isExpanded(2)); QVERIFY(m_model->isExpanded(3)); QVERIFY(!m_model->isExpanded(4)); QVERIFY(m_model->isConsistent()); } /** * Renaming an expanded folder by prepending its name with a dot makes it * hidden. Verify that this does not cause an inconsistent model state and * a crash later on, see https://bugs.kde.org/show_bug.cgi?id=311947 */ void KFileItemModelTest::testMakeExpandedItemHidden() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); QSignalSpy itemsRemovedSpy(m_model, &KFileItemModel::itemsRemoved); QSet modelRoles = m_model->roles(); modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; m_model->setRoles(modelRoles); m_testDir->createFiles({"1a/2a/3a", "1a/2a/3b", "1a/2b", "1b"}); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); // So far, the model contains only "1a/" and "1b". QCOMPARE(m_model->count(), 2); m_model->setExpanded(0, true); QVERIFY(itemsInsertedSpy.wait()); // Now "1a/2a" and "1a/2b" have appeared. QCOMPARE(m_model->count(), 4); m_model->setExpanded(1, true); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(m_model->count(), 6); // Rename "1a/2" and make it hidden. const QUrl oldUrl = QUrl::fromLocalFile(m_model->fileItem(0).url().path() + "/2a"); const QUrl newUrl = QUrl::fromLocalFile(m_model->fileItem(0).url().path() + "/.2a"); KIO::SimpleJob* job = KIO::rename(oldUrl, newUrl, KIO::HideProgressInfo); bool ok = job->exec(); QVERIFY(ok); QVERIFY(itemsRemovedSpy.wait()); // "1a/2" and its subfolders have disappeared now. QVERIFY(m_model->isConsistent()); QCOMPARE(m_model->count(), 3); m_model->setExpanded(0, false); QCOMPARE(m_model->count(), 2); } void KFileItemModelTest::testRemoveFilteredExpandedItems() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); QSet originalModelRoles = m_model->roles(); QSet modelRoles = originalModelRoles; modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; m_model->setRoles(modelRoles); m_testDir->createFiles({"folder/child", "file"}); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); // So far, the model contains only "folder/" and "file". QCOMPARE(m_model->count(), 2); QVERIFY(m_model->isExpandable(0)); QVERIFY(!m_model->isExpandable(1)); QVERIFY(!m_model->isExpanded(0)); QVERIFY(!m_model->isExpanded(1)); QCOMPARE(itemsInModel(), QStringList() << "folder" << "file"); // Expand "folder" -> "folder/child" becomes visible. m_model->setExpanded(0, true); QVERIFY(m_model->isExpanded(0)); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "folder" << "child" << "file"); // Add a name filter. m_model->setNameFilter("f"); QCOMPARE(itemsInModel(), QStringList() << "folder" << "file"); m_model->setNameFilter("fo"); QCOMPARE(itemsInModel(), QStringList() << "folder"); // Remove all expanded items by changing the roles m_model->setRoles(originalModelRoles); QVERIFY(!m_model->isExpanded(0)); QCOMPARE(itemsInModel(), QStringList() << "folder"); // Remove the name filter and verify that "folder/child" does not reappear. m_model->setNameFilter(QString()); QCOMPARE(itemsInModel(), QStringList() << "folder" << "file"); } void KFileItemModelTest::testSorting() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); QSignalSpy itemsMovedSpy(m_model, &KFileItemModel::itemsMoved); QVERIFY(itemsMovedSpy.isValid()); // Create some files with different sizes and modification times to check the different sorting options QDateTime now = QDateTime::currentDateTime(); QSet roles; roles.insert("text"); roles.insert("isExpanded"); roles.insert("isExpandable"); roles.insert("expandedParentsCount"); m_model->setRoles(roles); m_testDir->createDir("c/c-2"); m_testDir->createFile("c/c-2/c-3"); m_testDir->createFile("c/c-1"); m_testDir->createFile("a", "A file", now.addDays(-3)); m_testDir->createFile("b", "A larger file", now.addDays(0)); m_testDir->createDir("c", now.addDays(-2)); m_testDir->createFile("d", "The largest file in this directory", now.addDays(-1)); m_testDir->createFile("e", "An even larger file", now.addDays(-4)); m_testDir->createFile(".f"); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); int index = m_model->index(QUrl(m_testDir->url().url() + "/c")); m_model->setExpanded(index, true); QVERIFY(itemsInsertedSpy.wait()); index = m_model->index(QUrl(m_testDir->url().url() + "/c/c-2")); m_model->setExpanded(index, true); QVERIFY(itemsInsertedSpy.wait()); // Default: Sort by Name, ascending QCOMPARE(m_model->sortRole(), QByteArray("text")); QCOMPARE(m_model->sortOrder(), Qt::AscendingOrder); QVERIFY(m_model->sortDirectoriesFirst()); QVERIFY(!m_model->showHiddenFiles()); QCOMPARE(itemsInModel(), QStringList() << "c" << "c-2" << "c-3" << "c-1" << "a" << "b" << "d" << "e"); // Sort by Name, ascending, 'Sort Folders First' disabled m_model->setSortDirectoriesFirst(false); QCOMPARE(m_model->sortRole(), QByteArray("text")); QCOMPARE(m_model->sortOrder(), Qt::AscendingOrder); QCOMPARE(itemsInModel(), QStringList() << "a" << "b" << "c" << "c-1" << "c-2" << "c-3" << "d" << "e"); QCOMPARE(itemsMovedSpy.count(), 1); QCOMPARE(itemsMovedSpy.first().at(0).value(), KItemRange(0, 6)); QCOMPARE(itemsMovedSpy.takeFirst().at(1).value >(), QList() << 2 << 4 << 5 << 3 << 0 << 1); // Sort by Name, descending m_model->setSortDirectoriesFirst(true); m_model->setSortOrder(Qt::DescendingOrder); QCOMPARE(m_model->sortRole(), QByteArray("text")); QCOMPARE(m_model->sortOrder(), Qt::DescendingOrder); QCOMPARE(itemsInModel(), QStringList() << "c" << "c-2" << "c-3" << "c-1" << "e" << "d" << "b" << "a"); QCOMPARE(itemsMovedSpy.count(), 2); QCOMPARE(itemsMovedSpy.first().at(0).value(), KItemRange(0, 6)); QCOMPARE(itemsMovedSpy.takeFirst().at(1).value >(), QList() << 4 << 5 << 0 << 3 << 1 << 2); QCOMPARE(itemsMovedSpy.first().at(0).value(), KItemRange(4, 4)); QCOMPARE(itemsMovedSpy.takeFirst().at(1).value >(), QList() << 7 << 6 << 5 << 4); // Sort by Date, descending m_model->setSortDirectoriesFirst(true); m_model->setSortRole("modificationtime"); QCOMPARE(m_model->sortRole(), QByteArray("modificationtime")); QCOMPARE(m_model->sortOrder(), Qt::DescendingOrder); QCOMPARE(itemsInModel(), QStringList() << "c" << "c-2" << "c-3" << "c-1" << "b" << "d" << "a" << "e"); QCOMPARE(itemsMovedSpy.count(), 1); QCOMPARE(itemsMovedSpy.first().at(0).value(), KItemRange(4, 4)); QCOMPARE(itemsMovedSpy.takeFirst().at(1).value >(), QList() << 7 << 5 << 4 << 6); // Sort by Date, ascending m_model->setSortOrder(Qt::AscendingOrder); QCOMPARE(m_model->sortRole(), QByteArray("modificationtime")); QCOMPARE(m_model->sortOrder(), Qt::AscendingOrder); QCOMPARE(itemsInModel(), QStringList() << "c" << "c-2" << "c-3" << "c-1" << "e" << "a" << "d" << "b"); QCOMPARE(itemsMovedSpy.count(), 1); QCOMPARE(itemsMovedSpy.first().at(0).value(), KItemRange(4, 4)); QCOMPARE(itemsMovedSpy.takeFirst().at(1).value >(), QList() << 7 << 6 << 5 << 4); // Sort by Date, ascending, 'Sort Folders First' disabled m_model->setSortDirectoriesFirst(false); QCOMPARE(m_model->sortRole(), QByteArray("modificationtime")); QCOMPARE(m_model->sortOrder(), Qt::AscendingOrder); QVERIFY(!m_model->sortDirectoriesFirst()); QCOMPARE(itemsInModel(), QStringList() << "e" << "a" << "c" << "c-1" << "c-2" << "c-3" << "d" << "b"); QCOMPARE(itemsMovedSpy.count(), 1); QCOMPARE(itemsMovedSpy.first().at(0).value(), KItemRange(0, 6)); QCOMPARE(itemsMovedSpy.takeFirst().at(1).value >(), QList() << 2 << 4 << 5 << 3 << 0 << 1); // Sort by Name, ascending, 'Sort Folders First' disabled m_model->setSortRole("text"); QCOMPARE(m_model->sortOrder(), Qt::AscendingOrder); QVERIFY(!m_model->sortDirectoriesFirst()); QCOMPARE(itemsInModel(), QStringList() << "a" << "b" << "c" << "c-1" << "c-2" << "c-3" << "d" << "e"); QCOMPARE(itemsMovedSpy.count(), 1); QCOMPARE(itemsMovedSpy.first().at(0).value(), KItemRange(0, 8)); QCOMPARE(itemsMovedSpy.takeFirst().at(1).value >(), QList() << 7 << 0 << 2 << 3 << 4 << 5 << 6 << 1); // Sort by Size, ascending, 'Sort Folders First' disabled m_model->setSortRole("size"); QCOMPARE(m_model->sortRole(), QByteArray("size")); QCOMPARE(m_model->sortOrder(), Qt::AscendingOrder); QVERIFY(!m_model->sortDirectoriesFirst()); QCOMPARE(itemsInModel(), QStringList() << "c" << "c-2" << "c-3" << "c-1" << "a" << "b" << "e" << "d"); QCOMPARE(itemsMovedSpy.count(), 1); QCOMPARE(itemsMovedSpy.first().at(0).value(), KItemRange(0, 8)); QCOMPARE(itemsMovedSpy.takeFirst().at(1).value >(), QList() << 4 << 5 << 0 << 3 << 1 << 2 << 7 << 6); // In 'Sort by Size' mode, folders are always first -> changing 'Sort Folders First' does not resort the model m_model->setSortDirectoriesFirst(true); QCOMPARE(m_model->sortRole(), QByteArray("size")); QCOMPARE(m_model->sortOrder(), Qt::AscendingOrder); QVERIFY(m_model->sortDirectoriesFirst()); QCOMPARE(itemsInModel(), QStringList() << "c" << "c-2" << "c-3" << "c-1" << "a" << "b" << "e" << "d"); QCOMPARE(itemsMovedSpy.count(), 0); // Sort by Size, descending, 'Sort Folders First' enabled m_model->setSortOrder(Qt::DescendingOrder); QCOMPARE(m_model->sortRole(), QByteArray("size")); QCOMPARE(m_model->sortOrder(), Qt::DescendingOrder); QVERIFY(m_model->sortDirectoriesFirst()); QCOMPARE(itemsInModel(), QStringList() << "c" << "c-2" << "c-3" << "c-1" << "d" << "e" << "b" << "a"); QCOMPARE(itemsMovedSpy.count(), 1); QCOMPARE(itemsMovedSpy.first().at(0).value(), KItemRange(4, 4)); QCOMPARE(itemsMovedSpy.takeFirst().at(1).value >(), QList() << 7 << 6 << 5 << 4); // TODO: Sort by other roles; show/hide hidden files } void KFileItemModelTest::testIndexForKeyboardSearch() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); m_testDir->createFiles({"a", "aa", "Image.jpg", "Image.png", "Text", "Text1", "Text2", "Text11"}); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); // Search from index 0 QCOMPARE(m_model->indexForKeyboardSearch("a", 0), 0); QCOMPARE(m_model->indexForKeyboardSearch("aa", 0), 1); QCOMPARE(m_model->indexForKeyboardSearch("i", 0), 2); QCOMPARE(m_model->indexForKeyboardSearch("image", 0), 2); QCOMPARE(m_model->indexForKeyboardSearch("image.jpg", 0), 2); QCOMPARE(m_model->indexForKeyboardSearch("image.png", 0), 3); QCOMPARE(m_model->indexForKeyboardSearch("t", 0), 4); QCOMPARE(m_model->indexForKeyboardSearch("text", 0), 4); QCOMPARE(m_model->indexForKeyboardSearch("text1", 0), 5); QCOMPARE(m_model->indexForKeyboardSearch("text2", 0), 6); QCOMPARE(m_model->indexForKeyboardSearch("text11", 0), 7); // Start a search somewhere in the middle QCOMPARE(m_model->indexForKeyboardSearch("a", 1), 1); QCOMPARE(m_model->indexForKeyboardSearch("i", 3), 3); QCOMPARE(m_model->indexForKeyboardSearch("t", 5), 5); QCOMPARE(m_model->indexForKeyboardSearch("text1", 6), 7); // Test searches that go past the last item back to index 0 QCOMPARE(m_model->indexForKeyboardSearch("a", 2), 0); QCOMPARE(m_model->indexForKeyboardSearch("i", 7), 2); QCOMPARE(m_model->indexForKeyboardSearch("image.jpg", 3), 2); QCOMPARE(m_model->indexForKeyboardSearch("text2", 7), 6); // Test searches that yield no result QCOMPARE(m_model->indexForKeyboardSearch("aaa", 0), -1); QCOMPARE(m_model->indexForKeyboardSearch("b", 0), -1); QCOMPARE(m_model->indexForKeyboardSearch("image.svg", 0), -1); QCOMPARE(m_model->indexForKeyboardSearch("text3", 0), -1); QCOMPARE(m_model->indexForKeyboardSearch("text3", 5), -1); // Test upper case searches (note that search is case insensitive) QCOMPARE(m_model->indexForKeyboardSearch("A", 0), 0); QCOMPARE(m_model->indexForKeyboardSearch("aA", 0), 1); QCOMPARE(m_model->indexForKeyboardSearch("TexT", 5), 5); QCOMPARE(m_model->indexForKeyboardSearch("IMAGE", 4), 2); // TODO: Maybe we should also test keyboard searches in directories which are not sorted by Name? } void KFileItemModelTest::testNameFilter() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); m_testDir->createFiles({"A1", "A2", "Abc", "Bcd", "Cde"}); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); m_model->setNameFilter("A"); // Shows A1, A2 and Abc QCOMPARE(m_model->count(), 3); m_model->setNameFilter("A2"); // Shows only A2 QCOMPARE(m_model->count(), 1); m_model->setNameFilter("A2"); // Shows only A1 QCOMPARE(m_model->count(), 1); m_model->setNameFilter("Bc"); // Shows "Abc" and "Bcd" QCOMPARE(m_model->count(), 2); m_model->setNameFilter("bC"); // Shows "Abc" and "Bcd" QCOMPARE(m_model->count(), 2); m_model->setNameFilter(QString()); // Shows again all items QCOMPARE(m_model->count(), 5); } /** * Verifies that we do not crash when adding a KFileItem with an empty path. * Before this issue was fixed, KFileItemModel::expandedParentsCountCompare() * tried to always read the first character of the path, even if the path is empty. */ void KFileItemModelTest::testEmptyPath() { QSet roles; roles.insert("text"); roles.insert("isExpanded"); roles.insert("isExpandable"); roles.insert("expandedParentsCount"); m_model->setRoles(roles); const QUrl emptyUrl; QVERIFY(emptyUrl.path().isEmpty()); const QUrl url("file:///test/"); KFileItemList items; items << KFileItem(emptyUrl, QString(), KFileItem::Unknown) << KFileItem(url, QString(), KFileItem::Unknown); m_model->slotItemsAdded(emptyUrl, items); m_model->slotCompleted(); } /** * Verifies that the 'isExpanded' state of folders does not change when the * 'refreshItems' signal is received, see https://bugs.kde.org/show_bug.cgi?id=299675. */ void KFileItemModelTest::testRefreshExpandedItem() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); QSignalSpy itemsChangedSpy(m_model, &KFileItemModel::itemsChanged); QVERIFY(itemsChangedSpy.isValid()); QSet modelRoles = m_model->roles(); modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; m_model->setRoles(modelRoles); m_testDir->createFiles({"a/1", "a/2", "3", "4"}); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(m_model->count(), 3); // "a/", "3", "4" m_model->setExpanded(0, true); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(m_model->count(), 5); // "a/", "a/1", "a/2", "3", "4" QVERIFY(m_model->isExpanded(0)); const KFileItem item = m_model->fileItem(0); m_model->slotRefreshItems({qMakePair(item, item)}); QVERIFY(!itemsChangedSpy.isEmpty()); QCOMPARE(m_model->count(), 5); // "a/", "a/1", "a/2", "3", "4" QVERIFY(m_model->isExpanded(0)); } /** * Verify that removing hidden files and folders from the model does not * result in a crash, see https://bugs.kde.org/show_bug.cgi?id=314046 */ void KFileItemModelTest::testRemoveHiddenItems() { m_testDir->createDir(".a"); m_testDir->createDir(".b"); m_testDir->createDir("c"); m_testDir->createDir("d"); m_testDir->createFiles({".f", ".g", "h", "i"}); QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); QSignalSpy itemsRemovedSpy(m_model, &KFileItemModel::itemsRemoved); m_model->setShowHiddenFiles(true); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << ".a" << ".b" << "c" << "d" <<".f" << ".g" << "h" << "i"); QCOMPARE(itemsInsertedSpy.count(), 1); QCOMPARE(itemsRemovedSpy.count(), 0); KItemRangeList itemRangeList = itemsInsertedSpy.takeFirst().at(0).value(); QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(0, 8)); m_model->setShowHiddenFiles(false); QCOMPARE(itemsInModel(), QStringList() << "c" << "d" << "h" << "i"); QCOMPARE(itemsInsertedSpy.count(), 0); QCOMPARE(itemsRemovedSpy.count(), 1); itemRangeList = itemsRemovedSpy.takeFirst().at(0).value(); QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(0, 2) << KItemRange(4, 2)); m_model->setShowHiddenFiles(true); QCOMPARE(itemsInModel(), QStringList() << ".a" << ".b" << "c" << "d" <<".f" << ".g" << "h" << "i"); QCOMPARE(itemsInsertedSpy.count(), 1); QCOMPARE(itemsRemovedSpy.count(), 0); itemRangeList = itemsInsertedSpy.takeFirst().at(0).value(); QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(0, 2) << KItemRange(2, 2)); m_model->clear(); QCOMPARE(itemsInModel(), QStringList()); QCOMPARE(itemsInsertedSpy.count(), 0); QCOMPARE(itemsRemovedSpy.count(), 1); itemRangeList = itemsRemovedSpy.takeFirst().at(0).value(); QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(0, 8)); // Hiding hidden files makes the dir lister emit its itemsDeleted signal. // Verify that this does not make the model crash. m_model->setShowHiddenFiles(false); } /** * Verify that filtered items are removed when their parent is collapsed. */ void KFileItemModelTest::collapseParentOfHiddenItems() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); QSignalSpy itemsRemovedSpy(m_model, &KFileItemModel::itemsRemoved); QSet modelRoles = m_model->roles(); modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; m_model->setRoles(modelRoles); m_testDir->createFiles({"a/1", "a/b/1", "a/b/c/1", "a/b/c/d/1"}); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(m_model->count(), 1); // Only "a/" // Expand "a/". m_model->setExpanded(0, true); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(m_model->count(), 3); // 3 items: "a/", "a/b/", "a/1" // Expand "a/b/". m_model->setExpanded(1, true); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(m_model->count(), 5); // 5 items: "a/", "a/b/", "a/b/c", "a/b/1", "a/1" // Expand "a/b/c/". m_model->setExpanded(2, true); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(m_model->count(), 7); // 7 items: "a/", "a/b/", "a/b/c", "a/b/c/d/", "a/b/c/1", "a/b/1", "a/1" // Set a name filter that matches nothing -> only the expanded folders remain. m_model->setNameFilter("xyz"); QCOMPARE(itemsRemovedSpy.count(), 1); QCOMPARE(m_model->count(), 3); QCOMPARE(itemsInModel(), QStringList() << "a" << "b" << "c"); // Collapse the folder "a/". m_model->setExpanded(0, false); QCOMPARE(itemsRemovedSpy.count(), 2); QCOMPARE(m_model->count(), 1); QCOMPARE(itemsInModel(), QStringList() << "a"); // Remove the filter -> no files should appear (and we should not get a crash). m_model->setNameFilter(QString()); QCOMPARE(m_model->count(), 1); } /** * Verify that filtered items are removed when their parent is deleted. */ void KFileItemModelTest::removeParentOfHiddenItems() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); QSignalSpy itemsRemovedSpy(m_model, &KFileItemModel::itemsRemoved); QSet modelRoles = m_model->roles(); modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; m_model->setRoles(modelRoles); m_testDir->createFiles({"a/1", "a/b/1", "a/b/c/1", "a/b/c/d/1"}); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(m_model->count(), 1); // Only "a/" // Expand "a/". m_model->setExpanded(0, true); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(m_model->count(), 3); // 3 items: "a/", "a/b/", "a/1" // Expand "a/b/". m_model->setExpanded(1, true); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(m_model->count(), 5); // 5 items: "a/", "a/b/", "a/b/c", "a/b/1", "a/1" // Expand "a/b/c/". m_model->setExpanded(2, true); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(m_model->count(), 7); // 7 items: "a/", "a/b/", "a/b/c", "a/b/c/d/", "a/b/c/1", "a/b/1", "a/1" // Set a name filter that matches nothing -> only the expanded folders remain. m_model->setNameFilter("xyz"); QCOMPARE(itemsRemovedSpy.count(), 1); QCOMPARE(m_model->count(), 3); QCOMPARE(itemsInModel(), QStringList() << "a" << "b" << "c"); // Simulate the deletion of the directory "a/b/". m_model->slotItemsDeleted(KFileItemList() << m_model->fileItem(1)); QCOMPARE(itemsRemovedSpy.count(), 2); QCOMPARE(m_model->count(), 1); QCOMPARE(itemsInModel(), QStringList() << "a"); // Remove the filter -> only the file "a/1" should appear. m_model->setNameFilter(QString()); QCOMPARE(m_model->count(), 2); QCOMPARE(itemsInModel(), QStringList() << "a" << "1"); } /** * Create a tree structure where parent-child relationships can not be * determined by parsing the URLs, and verify that KFileItemModel * handles them correctly. */ void KFileItemModelTest::testGeneralParentChildRelationships() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); QSignalSpy itemsRemovedSpy(m_model, &KFileItemModel::itemsRemoved); QSet modelRoles = m_model->roles(); modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; m_model->setRoles(modelRoles); m_testDir->createFiles({"parent1/realChild1/realGrandChild1", "parent2/realChild2/realGrandChild2"}); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "parent1" << "parent2"); // Expand all folders. m_model->setExpanded(0, true); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "parent2"); m_model->setExpanded(1, true); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "realGrandChild1" << "parent2"); m_model->setExpanded(3, true); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "realGrandChild1" << "parent2" << "realChild2"); m_model->setExpanded(4, true); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "realGrandChild1" << "parent2" << "realChild2" << "realGrandChild2"); // Add some more children and grand-children. const QUrl parent1 = m_model->fileItem(0).url(); const QUrl parent2 = m_model->fileItem(3).url(); const QUrl realChild1 = m_model->fileItem(1).url(); const QUrl realChild2 = m_model->fileItem(4).url(); m_model->slotItemsAdded(parent1, KFileItemList() << KFileItem(QUrl("child1"), QString(), KFileItem::Unknown)); m_model->slotCompleted(); QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "realGrandChild1" << "child1" << "parent2" << "realChild2" << "realGrandChild2"); m_model->slotItemsAdded(parent2, KFileItemList() << KFileItem(QUrl("child2"), QString(), KFileItem::Unknown)); m_model->slotCompleted(); QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "realGrandChild1" << "child1" << "parent2" << "realChild2" << "realGrandChild2" << "child2"); m_model->slotItemsAdded(realChild1, KFileItemList() << KFileItem(QUrl("grandChild1"), QString(), KFileItem::Unknown)); m_model->slotCompleted(); QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "grandChild1" << "realGrandChild1" << "child1" << "parent2" << "realChild2" << "realGrandChild2" << "child2"); m_model->slotItemsAdded(realChild1, KFileItemList() << KFileItem(QUrl("grandChild1"), QString(), KFileItem::Unknown)); m_model->slotCompleted(); QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "grandChild1" << "realGrandChild1" << "child1" << "parent2" << "realChild2" << "realGrandChild2" << "child2"); m_model->slotItemsAdded(realChild2, KFileItemList() << KFileItem(QUrl("grandChild2"), QString(), KFileItem::Unknown)); m_model->slotCompleted(); QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "grandChild1" << "realGrandChild1" << "child1" << "parent2" << "realChild2" << "grandChild2" << "realGrandChild2" << "child2"); // Set a name filter that matches nothing -> only expanded folders remain. m_model->setNameFilter("xyz"); QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "parent2" << "realChild2"); QCOMPARE(itemsRemovedSpy.count(), 1); QList arguments = itemsRemovedSpy.takeFirst(); KItemRangeList itemRangeList = arguments.at(0).value(); QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(2, 3) << KItemRange(7, 3)); // Collapse "parent1". m_model->setExpanded(0, false); QCOMPARE(itemsInModel(), QStringList() << "parent1" << "parent2" << "realChild2"); QCOMPARE(itemsRemovedSpy.count(), 1); arguments = itemsRemovedSpy.takeFirst(); itemRangeList = arguments.at(0).value(); QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(1, 1)); // Remove "parent2". m_model->slotItemsDeleted(KFileItemList() << m_model->fileItem(1)); QCOMPARE(itemsInModel(), QStringList() << "parent1"); QCOMPARE(itemsRemovedSpy.count(), 1); arguments = itemsRemovedSpy.takeFirst(); itemRangeList = arguments.at(0).value(); QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(1, 2)); // Clear filter, verify that no items reappear. m_model->setNameFilter(QString()); QCOMPARE(itemsInModel(), QStringList() << "parent1"); } void KFileItemModelTest::testNameRoleGroups() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); QSignalSpy itemsMovedSpy(m_model, &KFileItemModel::itemsMoved); QVERIFY(itemsMovedSpy.isValid()); QSignalSpy groupsChangedSpy(m_model, &KFileItemModel::groupsChanged); QVERIFY(groupsChangedSpy.isValid()); m_testDir->createFiles({"b.txt", "c.txt", "d.txt", "e.txt"}); m_model->setGroupedSorting(true); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "b.txt" << "c.txt" << "d.txt" << "e.txt"); QList > expectedGroups; expectedGroups << QPair(0, QLatin1String("B")); expectedGroups << QPair(1, QLatin1String("C")); expectedGroups << QPair(2, QLatin1String("D")); expectedGroups << QPair(3, QLatin1String("E")); QCOMPARE(m_model->groups(), expectedGroups); // Rename d.txt to a.txt. QHash data; data.insert("text", "a.txt"); m_model->setData(2, data); QVERIFY(itemsMovedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "b.txt" << "c.txt" << "e.txt"); expectedGroups.clear(); expectedGroups << QPair(0, QLatin1String("A")); expectedGroups << QPair(1, QLatin1String("B")); expectedGroups << QPair(2, QLatin1String("C")); expectedGroups << QPair(3, QLatin1String("E")); QCOMPARE(m_model->groups(), expectedGroups); // Rename c.txt to d.txt. data.insert("text", "d.txt"); m_model->setData(2, data); QVERIFY(groupsChangedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "b.txt" << "d.txt" << "e.txt"); expectedGroups.clear(); expectedGroups << QPair(0, QLatin1String("A")); expectedGroups << QPair(1, QLatin1String("B")); expectedGroups << QPair(2, QLatin1String("D")); expectedGroups << QPair(3, QLatin1String("E")); QCOMPARE(m_model->groups(), expectedGroups); // Change d.txt back to c.txt, but this time using the dir lister's refreshItems() signal. const KFileItem fileItemD = m_model->fileItem(2); KFileItem fileItemC = fileItemD; QUrl urlC = fileItemC.url().adjusted(QUrl::RemoveFilename); urlC.setPath(urlC.path() + "c.txt"); fileItemC.setUrl(urlC); m_model->slotRefreshItems({qMakePair(fileItemD, fileItemC)}); QVERIFY(groupsChangedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "b.txt" << "c.txt" << "e.txt"); expectedGroups.clear(); expectedGroups << QPair(0, QLatin1String("A")); expectedGroups << QPair(1, QLatin1String("B")); expectedGroups << QPair(2, QLatin1String("C")); expectedGroups << QPair(3, QLatin1String("E")); QCOMPARE(m_model->groups(), expectedGroups); } void KFileItemModelTest::testNameRoleGroupsWithExpandedItems() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); QSet modelRoles = m_model->roles(); modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; m_model->setRoles(modelRoles); m_testDir->createFiles({"a/b.txt", "a/c.txt", "d/e.txt", "d/f.txt"}); m_model->setGroupedSorting(true); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "a" << "d"); QList > expectedGroups; expectedGroups << QPair(0, QLatin1String("A")); expectedGroups << QPair(1, QLatin1String("D")); QCOMPARE(m_model->groups(), expectedGroups); // Verify that expanding "a" and "d" will not change the groups (except for the index of "D"). expectedGroups.clear(); expectedGroups << QPair(0, QLatin1String("A")); expectedGroups << QPair(3, QLatin1String("D")); m_model->setExpanded(0, true); QVERIFY(m_model->isExpanded(0)); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "a" << "b.txt" << "c.txt" << "d"); QCOMPARE(m_model->groups(), expectedGroups); m_model->setExpanded(3, true); QVERIFY(m_model->isExpanded(3)); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "a" << "b.txt" << "c.txt" << "d" << "e.txt" << "f.txt"); QCOMPARE(m_model->groups(), expectedGroups); } void KFileItemModelTest::testInconsistentModel() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); QSet modelRoles = m_model->roles(); modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; m_model->setRoles(modelRoles); m_testDir->createFiles({"a/b/c1.txt", "a/b/c2.txt"}); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "a"); // Expand "a/" and "a/b/". m_model->setExpanded(0, true); QVERIFY(m_model->isExpanded(0)); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "a" << "b"); m_model->setExpanded(1, true); QVERIFY(m_model->isExpanded(1)); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "a" << "b" << "c1.txt" << "c2.txt"); // Add the files "c1.txt" and "c2.txt" to the model also as top-level items. // Such a thing can in principle happen when performing a search, and there // are files which // (a) match the search string, and // (b) are children of a folder that matches the search string and is expanded. // // Note that the first item in the list of added items must be new (i.e., not // in the model yet). Otherwise, KFileItemModel::slotItemsAdded() will see that // it receives items that are in the model already and ignore them. QUrl url(m_model->directory().url() + "/a2"); KFileItem newItem(url); KFileItemList items; items << newItem << m_model->fileItem(2) << m_model->fileItem(3); m_model->slotItemsAdded(m_model->directory(), items); m_model->slotCompleted(); QCOMPARE(itemsInModel(), QStringList() << "a" << "b" << "c1.txt" << "c2.txt" << "a2" << "c1.txt" << "c2.txt"); m_model->setExpanded(0, false); // Test that the right items have been removed, see // https://bugs.kde.org/show_bug.cgi?id=324371 QCOMPARE(itemsInModel(), QStringList() << "a" << "a2" << "c1.txt" << "c2.txt"); // Test that resorting does not cause a crash, see // https://bugs.kde.org/show_bug.cgi?id=325359 // The crash is not 100% reproducible, but Valgrind will report an invalid memory access. m_model->resortAllItems(); } void KFileItemModelTest::testChangeRolesForFilteredItems() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); QSet modelRoles = m_model->roles(); modelRoles << "owner"; m_model->setRoles(modelRoles); m_testDir->createFiles({"a.txt", "aa.txt", "aaa.txt"}); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "aa.txt" << "aaa.txt"); for (int index = 0; index < m_model->count(); ++index) { // All items should have the "text" and "owner" roles, but not "group". QVERIFY(m_model->data(index).contains("text")); QVERIFY(m_model->data(index).contains("owner")); QVERIFY(!m_model->data(index).contains("group")); } // Add a filter, such that only "aaa.txt" remains in the model. m_model->setNameFilter("aaa"); QCOMPARE(itemsInModel(), QStringList() << "aaa.txt"); // Add the "group" role. modelRoles << "group"; m_model->setRoles(modelRoles); // Modify the filter, such that "aa.txt" reappears, and verify that all items have the expected roles. m_model->setNameFilter("aa"); QCOMPARE(itemsInModel(), QStringList() << "aa.txt" << "aaa.txt"); for (int index = 0; index < m_model->count(); ++index) { // All items should have the "text", "owner", and "group" roles. QVERIFY(m_model->data(index).contains("text")); QVERIFY(m_model->data(index).contains("owner")); QVERIFY(m_model->data(index).contains("group")); } // Remove the "owner" role. modelRoles.remove("owner"); m_model->setRoles(modelRoles); // Clear the filter, and verify that all items have the expected roles m_model->setNameFilter(QString()); QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "aa.txt" << "aaa.txt"); for (int index = 0; index < m_model->count(); ++index) { // All items should have the "text" and "group" roles, but now "owner". QVERIFY(m_model->data(index).contains("text")); QVERIFY(!m_model->data(index).contains("owner")); QVERIFY(m_model->data(index).contains("group")); } } void KFileItemModelTest::testChangeSortRoleWhileFiltering() { KFileItemList items; KIO::UDSEntry entry[3]; entry[0].fastInsert(KIO::UDSEntry::UDS_NAME, "a.txt"); entry[0].fastInsert(KIO::UDSEntry::UDS_USER, "user-b"); entry[1].fastInsert(KIO::UDSEntry::UDS_NAME, "b.txt"); entry[1].fastInsert(KIO::UDSEntry::UDS_USER, "user-c"); entry[2].fastInsert(KIO::UDSEntry::UDS_NAME, "c.txt"); entry[2].fastInsert(KIO::UDSEntry::UDS_USER, "user-a"); for (int i = 0; i < 3; ++i) { entry[i].fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, 0100000); // S_IFREG might not be defined on non-Unix platforms. entry[i].fastInsert(KIO::UDSEntry::UDS_ACCESS, 07777); entry[i].fastInsert(KIO::UDSEntry::UDS_SIZE, 0); entry[i].fastInsert(KIO::UDSEntry::UDS_MODIFICATION_TIME, 0); entry[i].fastInsert(KIO::UDSEntry::UDS_GROUP, "group"); entry[i].fastInsert(KIO::UDSEntry::UDS_ACCESS_TIME, 0); items.append(KFileItem(entry[i], m_testDir->url(), false, true)); } m_model->slotItemsAdded(m_testDir->url(), items); m_model->slotCompleted(); QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "b.txt" << "c.txt"); // Add a filter. m_model->setNameFilter("a"); QCOMPARE(itemsInModel(), QStringList() << "a.txt"); // Sort by "owner". m_model->setSortRole("owner"); // Clear the filter, and verify that the items are sorted correctly. m_model->setNameFilter(QString()); QCOMPARE(itemsInModel(), QStringList() << "c.txt" << "a.txt" << "b.txt"); } void KFileItemModelTest::testRefreshFilteredItems() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); m_testDir->createFiles({"a.txt", "b.txt", "c.jpg", "d.jpg"}); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "b.txt" << "c.jpg" << "d.jpg"); const KFileItem fileItemC = m_model->fileItem(2); // Show only the .txt files. m_model->setNameFilter(".txt"); QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "b.txt"); // Rename one of the .jpg files. KFileItem fileItemE = fileItemC; QUrl urlE = fileItemE.url().adjusted(QUrl::RemoveFilename); urlE.setPath(urlE.path() + "/e.jpg"); fileItemE.setUrl(urlE); m_model->slotRefreshItems({qMakePair(fileItemC, fileItemE)}); // Show all files again, and verify that the model has updated the file name. m_model->setNameFilter(QString()); QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "b.txt" << "d.jpg" << "e.jpg"); } void KFileItemModelTest::testCreateMimeData() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); QSet modelRoles = m_model->roles(); modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; m_model->setRoles(modelRoles); m_testDir->createFile("a/1"); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "a"); // Expand "a/". m_model->setExpanded(0, true); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "a" << "1"); // Verify that creating the MIME data for a child of an expanded folder does // not cause a crash, see https://bugs.kde.org/show_bug.cgi?id=329119 KItemSet selection; selection.insert(1); QMimeData* mimeData = m_model->createMimeData(selection); delete mimeData; } void KFileItemModelTest::testCollapseFolderWhileLoading() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); QSet modelRoles = m_model->roles(); modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; m_model->setRoles(modelRoles); m_testDir->createFile("a2/b/c1.txt"); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "a2"); // Expand "a2/". m_model->setExpanded(0, true); QVERIFY(m_model->isExpanded(0)); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "a2" << "b"); // Expand "a2/b/". m_model->setExpanded(1, true); QVERIFY(m_model->isExpanded(1)); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "a2" << "b" << "c1.txt"); // Simulate that a new item "c2.txt" appears, but that the dir lister's completed() // signal is not emitted yet. const KFileItem fileItemC1 = m_model->fileItem(2); KFileItem fileItemC2 = fileItemC1; QUrl urlC2 = fileItemC2.url(); urlC2 = urlC2.adjusted(QUrl::RemoveFilename); urlC2.setPath(urlC2.path() + "c2.txt"); fileItemC2.setUrl(urlC2); const QUrl urlB = m_model->fileItem(1).url(); m_model->slotItemsAdded(urlB, KFileItemList() << fileItemC2); QCOMPARE(itemsInModel(), QStringList() << "a2" << "b" << "c1.txt"); // Collapse "a2/". This should also remove all its (indirect) children from // the model and from the model's m_pendingItemsToInsert member. m_model->setExpanded(0, false); QCOMPARE(itemsInModel(), QStringList() << "a2"); // Simulate that the dir lister's completed() signal is emitted. If "c2.txt" // is still in m_pendingItemsToInsert, then we might get a crash, see // https://bugs.kde.org/show_bug.cgi?id=332102. Even if the crash is not // reproducible here, Valgrind will complain, and the item "c2.txt" will appear // without parent in the model. m_model->slotCompleted(); QCOMPARE(itemsInModel(), QStringList() << "a2"); // Expand "a2/" again. m_model->setExpanded(0, true); QVERIFY(m_model->isExpanded(0)); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "a2" << "b"); // Now simulate that a new folder "a1/" is appears, but that the dir lister's // completed() signal is not emitted yet. const KFileItem fileItemA2 = m_model->fileItem(0); KFileItem fileItemA1 = fileItemA2; QUrl urlA1 = fileItemA1.url().adjusted(QUrl::RemoveFilename); urlA1.setPath(urlA1.path() + "a1"); fileItemA1.setUrl(urlA1); m_model->slotItemsAdded(m_model->directory(), KFileItemList() << fileItemA1); QCOMPARE(itemsInModel(), QStringList() << "a2" << "b"); // Collapse "a2/". Note that this will cause "a1/" to be added to the model, // i.e., the index of "a2/" will change from 0 to 1. Check that this does not // confuse the code which collapses the folder. m_model->setExpanded(0, false); QCOMPARE(itemsInModel(), QStringList() << "a1" << "a2"); QVERIFY(!m_model->isExpanded(0)); QVERIFY(!m_model->isExpanded(1)); } void KFileItemModelTest::testDeleteFileMoreThanOnce() { QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted); m_testDir->createFiles({"a.txt", "b.txt", "c.txt", "d.txt"}); m_model->loadDirectory(m_testDir->url()); QVERIFY(itemsInsertedSpy.wait()); QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "b.txt" << "c.txt" << "d.txt"); const KFileItem fileItemB = m_model->fileItem(1); // Tell the model that a list of items has been deleted, where "b.txt" appears twice in the list. KFileItemList list; list << fileItemB << fileItemB; m_model->slotItemsDeleted(list); QVERIFY(m_model->isConsistent()); QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "c.txt" << "d.txt"); } QStringList KFileItemModelTest::itemsInModel() const { QStringList items; for (int i = 0; i < m_model->count(); i++) { items << m_model->fileItem(i).text(); } return items; } QTEST_MAIN(KFileItemModelTest) #include "kfileitemmodeltest.moc" diff --git a/src/tests/kitemlistselectionmanagertest.cpp b/src/tests/kitemlistselectionmanagertest.cpp index 0655dcfb0..99313ff80 100644 --- a/src/tests/kitemlistselectionmanagertest.cpp +++ b/src/tests/kitemlistselectionmanagertest.cpp @@ -1,577 +1,577 @@ /*************************************************************************** * Copyright (C) 2011 by Peter Penz * * Copyright (C) 2011 by Frank Reininghaus * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kitemviews/kitemmodelbase.h" #include "kitemviews/kitemlistselectionmanager.h" #include #include class DummyModel : public KItemModelBase { Q_OBJECT public: DummyModel(); void setCount(int count); int count() const override; QHash data(int index) const override; private: int m_count; }; DummyModel::DummyModel() : KItemModelBase(), m_count(100) { } void DummyModel::setCount(int count) { m_count = count; } int DummyModel::count() const { return m_count; } QHash DummyModel::data(int index) const { - Q_UNUSED(index); + Q_UNUSED(index) return QHash(); } class KItemListSelectionManagerTest : public QObject { Q_OBJECT private slots: void init(); void cleanup(); void testConstructor(); void testCurrentItemAnchorItem(); void testSetSelected_data(); void testSetSelected(); void testItemsInserted(); void testItemsRemoved(); void testAnchoredSelection(); void testChangeSelection_data(); void testChangeSelection(); void testDeleteCurrentItem_data(); void testDeleteCurrentItem(); void testAnchoredSelectionAfterMovingItems(); private: void verifySelectionChange(QSignalSpy& spy, const KItemSet& currentSelection, const KItemSet& previousSelection) const; KItemListSelectionManager* m_selectionManager; DummyModel* m_model; }; void KItemListSelectionManagerTest::init() { m_model = new DummyModel(); m_selectionManager = new KItemListSelectionManager(); m_selectionManager->setModel(m_model); } void KItemListSelectionManagerTest::cleanup() { delete m_selectionManager; m_selectionManager = nullptr; delete m_model; m_model = nullptr; } void KItemListSelectionManagerTest::testConstructor() { QVERIFY(!m_selectionManager->hasSelection()); QCOMPARE(m_selectionManager->selectedItems().count(), 0); QCOMPARE(m_selectionManager->currentItem(), 0); QCOMPARE(m_selectionManager->m_anchorItem, -1); } void KItemListSelectionManagerTest::testCurrentItemAnchorItem() { QSignalSpy spyCurrent(m_selectionManager, &KItemListSelectionManager::currentChanged); // Set current item and check that the selection manager emits the currentChanged(int,int) signal correctly. m_selectionManager->setCurrentItem(4); QCOMPARE(m_selectionManager->currentItem(), 4); QCOMPARE(spyCurrent.count(), 1); QCOMPARE(qvariant_cast(spyCurrent.at(0).at(0)), 4); spyCurrent.takeFirst(); // Begin an anchored selection. m_selectionManager->beginAnchoredSelection(5); QVERIFY(m_selectionManager->isAnchoredSelectionActive()); QCOMPARE(m_selectionManager->m_anchorItem, 5); // Items between current and anchor should be selected now QCOMPARE(m_selectionManager->selectedItems(), KItemSet() << 4 << 5); QVERIFY(m_selectionManager->hasSelection()); // Change current item again and check the selection m_selectionManager->setCurrentItem(2); QCOMPARE(m_selectionManager->currentItem(), 2); QCOMPARE(spyCurrent.count(), 1); QCOMPARE(qvariant_cast(spyCurrent.at(0).at(0)), 2); QCOMPARE(qvariant_cast(spyCurrent.at(0).at(1)), 4); spyCurrent.takeFirst(); QCOMPARE(m_selectionManager->selectedItems(), KItemSet() << 2 << 3 << 4 << 5); QVERIFY(m_selectionManager->hasSelection()); // Inserting items should update current item and anchor item. m_selectionManager->itemsInserted(KItemRangeList() << KItemRange(0, 1) << KItemRange(2, 2) << KItemRange(6, 3)); QCOMPARE(m_selectionManager->currentItem(), 5); QCOMPARE(spyCurrent.count(), 1); QCOMPARE(qvariant_cast(spyCurrent.at(0).at(0)), 5); QCOMPARE(qvariant_cast(spyCurrent.at(0).at(1)), 2); spyCurrent.takeFirst(); QCOMPARE(m_selectionManager->m_anchorItem, 8); QCOMPARE(m_selectionManager->selectedItems(), KItemSet() << 5 << 6 << 7 << 8); QVERIFY(m_selectionManager->hasSelection()); // Removing items should update current item and anchor item. m_selectionManager->itemsRemoved(KItemRangeList() << KItemRange(0, 2) << KItemRange(2, 1) << KItemRange(9, 2)); QCOMPARE(m_selectionManager->currentItem(), 2); QCOMPARE(spyCurrent.count(), 1); QCOMPARE(qvariant_cast(spyCurrent.at(0).at(0)), 2); QCOMPARE(qvariant_cast(spyCurrent.at(0).at(1)), 5); spyCurrent.takeFirst(); QCOMPARE(m_selectionManager->m_anchorItem, 5); QCOMPARE(m_selectionManager->selectedItems(), KItemSet() << 2 << 3 << 4 << 5); QVERIFY(m_selectionManager->hasSelection()); // Verify that clearSelection() also clears the anchored selection. m_selectionManager->clearSelection(); QCOMPARE(m_selectionManager->selectedItems(), KItemSet()); QVERIFY(!m_selectionManager->hasSelection()); m_selectionManager->endAnchoredSelection(); QVERIFY(!m_selectionManager->isAnchoredSelectionActive()); } void KItemListSelectionManagerTest::testSetSelected_data() { QTest::addColumn("index"); QTest::addColumn("count"); QTest::addColumn("expectedSelectionCount"); QTest::newRow("Select all") << 0 << 100 << 100; QTest::newRow("Sub selection 15 items") << 20 << 15 << 15; QTest::newRow("Sub selection 1 item") << 20 << 1 << 1; QTest::newRow("Too small index") << -1 << 100 << 0; QTest::newRow("Too large index") << 100 << 100 << 0; QTest::newRow("Too large count") << 0 << 100000 << 100; QTest::newRow("Too small count") << 0 << 0 << 0; } void KItemListSelectionManagerTest::testSetSelected() { QFETCH(int, index); QFETCH(int, count); QFETCH(int, expectedSelectionCount); m_selectionManager->setSelected(index, count); QCOMPARE(m_selectionManager->selectedItems().count(), expectedSelectionCount); } void KItemListSelectionManagerTest::testItemsInserted() { // Select items 10 to 12 m_selectionManager->setSelected(10, 3); KItemSet selectedItems = m_selectionManager->selectedItems(); QCOMPARE(selectedItems.count(), 3); QVERIFY(selectedItems.contains(10)); QVERIFY(selectedItems.contains(11)); QVERIFY(selectedItems.contains(12)); // Insert items 0 to 4 -> selection must be 15 to 17 m_selectionManager->itemsInserted(KItemRangeList() << KItemRange(0, 5)); selectedItems = m_selectionManager->selectedItems(); QCOMPARE(selectedItems.count(), 3); QVERIFY(selectedItems.contains(15)); QVERIFY(selectedItems.contains(16)); QVERIFY(selectedItems.contains(17)); // Insert 3 items between the selections m_selectionManager->itemsInserted(KItemRangeList() << KItemRange(15, 1) << KItemRange(16, 1) << KItemRange(17, 1)); selectedItems = m_selectionManager->selectedItems(); QCOMPARE(selectedItems.count(), 3); QVERIFY(selectedItems.contains(16)); QVERIFY(selectedItems.contains(18)); QVERIFY(selectedItems.contains(20)); } void KItemListSelectionManagerTest::testItemsRemoved() { // Select items 10 to 15 m_selectionManager->setSelected(10, 6); KItemSet selectedItems = m_selectionManager->selectedItems(); QCOMPARE(selectedItems.count(), 6); for (int i = 10; i <= 15; ++i) { QVERIFY(selectedItems.contains(i)); } // Remove items 0 to 4 -> selection must be 5 to 10 m_selectionManager->itemsRemoved(KItemRangeList() << KItemRange(0, 5)); selectedItems = m_selectionManager->selectedItems(); QCOMPARE(selectedItems.count(), 6); for (int i = 5; i <= 10; ++i) { QVERIFY(selectedItems.contains(i)); } // Remove the items 6 , 8 and 10 m_selectionManager->itemsRemoved(KItemRangeList() << KItemRange(6, 1) << KItemRange(8, 1) << KItemRange(10, 1)); selectedItems = m_selectionManager->selectedItems(); QCOMPARE(selectedItems.count(), 3); QVERIFY(selectedItems.contains(5)); QVERIFY(selectedItems.contains(6)); QVERIFY(selectedItems.contains(7)); } void KItemListSelectionManagerTest::testAnchoredSelection() { m_selectionManager->beginAnchoredSelection(5); QVERIFY(m_selectionManager->isAnchoredSelectionActive()); QCOMPARE(m_selectionManager->m_anchorItem, 5); m_selectionManager->setCurrentItem(6); QCOMPARE(m_selectionManager->currentItem(), 6); QCOMPARE(m_selectionManager->selectedItems(), KItemSet() << 5 << 6); m_selectionManager->setCurrentItem(4); QCOMPARE(m_selectionManager->currentItem(), 4); QCOMPARE(m_selectionManager->selectedItems(), KItemSet() << 4 << 5); m_selectionManager->setCurrentItem(7); QCOMPARE(m_selectionManager->currentItem(), 7); QCOMPARE(m_selectionManager->selectedItems(), KItemSet() << 5 << 6 << 7); // Ending the anchored selection should not change the selected items. m_selectionManager->endAnchoredSelection(); QVERIFY(!m_selectionManager->isAnchoredSelectionActive()); QCOMPARE(m_selectionManager->selectedItems(), KItemSet() << 5 << 6 << 7); // Start a new anchored selection that overlaps the previous one m_selectionManager->beginAnchoredSelection(9); QVERIFY(m_selectionManager->isAnchoredSelectionActive()); QCOMPARE(m_selectionManager->m_anchorItem, 9); m_selectionManager->setCurrentItem(6); QCOMPARE(m_selectionManager->currentItem(), 6); QCOMPARE(m_selectionManager->selectedItems(), KItemSet() << 5 << 6 << 7 << 8 << 9); m_selectionManager->setCurrentItem(10); QCOMPARE(m_selectionManager->currentItem(), 10); QCOMPARE(m_selectionManager->selectedItems(), KItemSet() << 5 << 6 << 7 << 9 << 10); m_selectionManager->endAnchoredSelection(); QVERIFY(!m_selectionManager->isAnchoredSelectionActive()); QCOMPARE(m_selectionManager->selectedItems(), KItemSet() << 5 << 6 << 7 << 9 << 10); } namespace { enum ChangeType { NoChange, InsertItems, RemoveItems, MoveItems, EndAnchoredSelection, SetSelected }; } Q_DECLARE_METATYPE(KItemSet) Q_DECLARE_METATYPE(ChangeType) Q_DECLARE_METATYPE(KItemRange) Q_DECLARE_METATYPE(KItemRangeList) Q_DECLARE_METATYPE(KItemListSelectionManager::SelectionMode) Q_DECLARE_METATYPE(QList) /** * The following function provides a generic way to test the selection functionality. * * The test is data-driven and takes the following arguments: * * param initialSelection The selection at the beginning. * param anchor This item will be the anchor item. * param current This item will be the current item. * param expectedSelection Expected selection after anchor and current are set. * param changeType Type of the change that is done then: * - NoChange * - InsertItems -> data.at(0) provides the KItemRangeList. \sa KItemListSelectionManager::itemsInserted() * - RemoveItems -> data.at(0) provides the KItemRangeList. \sa KItemListSelectionManager::itemsRemoved() * - MoveItems -> data.at(0) provides the KItemRange containing the original indices, * data.at(1) provides the list containing the new indices * \sa KItemListSelectionManager::itemsMoved(), KItemModelBase::itemsMoved() * - EndAnchoredSelection * - SetSelected -> data.at(0) provides the index where the selection process starts, * data.at(1) provides the number of indices to be selected, * data.at(2) provides the selection mode. * \sa KItemListSelectionManager::setSelected() * param data A list of QVariants which will be cast to the arguments needed for the chosen ChangeType (see above). * param finalSelection The expected final selection. * */ void KItemListSelectionManagerTest::testChangeSelection_data() { QTest::addColumn("initialSelection"); QTest::addColumn("anchor"); QTest::addColumn("current"); QTest::addColumn("expectedSelection"); QTest::addColumn("changeType"); QTest::addColumn >("data"); QTest::addColumn("finalSelection"); QTest::newRow("No change") << (KItemSet() << 5 << 6) << 2 << 3 << (KItemSet() << 2 << 3 << 5 << 6) << NoChange << QList{} << (KItemSet() << 2 << 3 << 5 << 6); QTest::newRow("Insert Items") << (KItemSet() << 5 << 6) << 2 << 3 << (KItemSet() << 2 << 3 << 5 << 6) << InsertItems << QList{QVariant::fromValue(KItemRangeList() << KItemRange(1, 1) << KItemRange(5, 2) << KItemRange(10, 5))} << (KItemSet() << 3 << 4 << 8 << 9); QTest::newRow("Remove Items") << (KItemSet() << 5 << 6) << 2 << 3 << (KItemSet() << 2 << 3 << 5 << 6) << RemoveItems << QList{QVariant::fromValue(KItemRangeList() << KItemRange(1, 1) << KItemRange(3, 1) << KItemRange(10, 5))} << (KItemSet() << 1 << 2 << 3 << 4); QTest::newRow("Empty Anchored Selection") << KItemSet() << 2 << 2 << KItemSet() << EndAnchoredSelection << QList{} << KItemSet(); QTest::newRow("Toggle selection") << (KItemSet() << 1 << 3 << 4) << 6 << 8 << (KItemSet() << 1 << 3 << 4 << 6 << 7 << 8) << SetSelected << QList{0, 10, QVariant::fromValue(KItemListSelectionManager::Toggle)} << (KItemSet() << 0 << 2 << 5 << 9); // Swap items 2, 3 and 4, 5 QTest::newRow("Move items") << (KItemSet() << 0 << 1 << 2 << 3) << -1 << -1 << (KItemSet() << 0 << 1 << 2 << 3) << MoveItems << QList{QVariant::fromValue(KItemRange(2, 4)), QVariant::fromValue(QList{4, 5, 2, 3})} << (KItemSet() << 0 << 1 << 4 << 5); QTest::newRow("Move items with active anchored selection") << KItemSet() << 0 << 3 << (KItemSet() << 0 << 1 << 2 << 3) << MoveItems << QList{QVariant::fromValue(KItemRange(2, 4)), QVariant::fromValue(QList{4, 5, 2, 3})} << (KItemSet() << 0 << 1 << 4 << 5); // Revert sort order QTest::newRow("Revert sort order") << (KItemSet() << 0 << 1) << 3 << 4 << (KItemSet() << 0 << 1 << 3 << 4) << MoveItems << QList{QVariant::fromValue(KItemRange(0, 10)), QVariant::fromValue(QList{9, 8, 7, 6, 5, 4, 3, 2, 1, 0})} << (KItemSet() << 5 << 6 << 8 << 9); } void KItemListSelectionManagerTest::testChangeSelection() { QFETCH(KItemSet, initialSelection); QFETCH(int, anchor); QFETCH(int, current); QFETCH(KItemSet, expectedSelection); QFETCH(ChangeType, changeType); QFETCH(QList, data); QFETCH(KItemSet, finalSelection); QSignalSpy spySelectionChanged(m_selectionManager, &KItemListSelectionManager::selectionChanged); // Initial selection should be empty QVERIFY(!m_selectionManager->hasSelection()); QVERIFY(m_selectionManager->selectedItems().isEmpty()); // Perform the initial selection m_selectionManager->setSelectedItems(initialSelection); verifySelectionChange(spySelectionChanged, initialSelection, KItemSet()); // Perform an anchored selection. // Note that current and anchor index are equal first because this is the case in typical uses of the // selection manager, and because this makes it easier to test the correctness of the signal's arguments. m_selectionManager->setCurrentItem(anchor); m_selectionManager->beginAnchoredSelection(anchor); m_selectionManager->setCurrentItem(current); QCOMPARE(m_selectionManager->m_anchorItem, anchor); QCOMPARE(m_selectionManager->currentItem(), current); verifySelectionChange(spySelectionChanged, expectedSelection, initialSelection); // Change the model by inserting or removing items. switch (changeType) { case InsertItems: m_selectionManager->itemsInserted(data.at(0).value()); break; case RemoveItems: m_selectionManager->itemsRemoved(data.at(0).value()); break; case MoveItems: m_selectionManager->itemsMoved(data.at(0).value(), data.at(1).value>()); break; case EndAnchoredSelection: m_selectionManager->endAnchoredSelection(); QVERIFY(!m_selectionManager->isAnchoredSelectionActive()); break; case SetSelected: m_selectionManager->setSelected(data.at(0).value(), // index data.at(1).value(), // count data.at(2).value()); break; case NoChange: break; } verifySelectionChange(spySelectionChanged, finalSelection, expectedSelection); // Finally, clear the selection m_selectionManager->clearSelection(); verifySelectionChange(spySelectionChanged, KItemSet(), finalSelection); } void KItemListSelectionManagerTest::testDeleteCurrentItem_data() { QTest::addColumn("oldCurrentItemIndex"); QTest::addColumn("removeIndex"); QTest::addColumn("removeCount"); QTest::addColumn("newCurrentItemIndex"); QTest::newRow("Remove before") << 50 << 0 << 10 << 40; QTest::newRow("Remove after") << 50 << 51 << 10 << 50; QTest::newRow("Remove exactly current item") << 50 << 50 << 1 << 50; QTest::newRow("Remove around current item") << 50 << 45 << 10 << 45; QTest::newRow("Remove all except one item") << 50 << 1 << 99 << 0; } void KItemListSelectionManagerTest::testDeleteCurrentItem() { QFETCH(int, oldCurrentItemIndex); QFETCH(int, removeIndex); QFETCH(int, removeCount); QFETCH(int, newCurrentItemIndex); m_selectionManager->setCurrentItem(oldCurrentItemIndex); const int newCount = m_model->count() - removeCount; m_model->setCount(newCount); m_selectionManager->itemsRemoved(KItemRangeList() << KItemRange(removeIndex, removeCount)); QCOMPARE(m_selectionManager->currentItem(), newCurrentItemIndex); } void KItemListSelectionManagerTest::testAnchoredSelectionAfterMovingItems() { m_selectionManager->setCurrentItem(4); m_selectionManager->beginAnchoredSelection(4); // Reverse the items between 0 and 5. m_selectionManager->itemsMoved(KItemRange(0, 6), {5, 4, 3, 2, 1, 0}); QCOMPARE(m_selectionManager->currentItem(), 1); QCOMPARE(m_selectionManager->m_anchorItem, 1); // Make 2 the current item -> 1 and 2 should be selected. m_selectionManager->setCurrentItem(2); QCOMPARE(m_selectionManager->selectedItems(), KItemSet() << 1 << 2); } void KItemListSelectionManagerTest::verifySelectionChange(QSignalSpy& spy, const KItemSet& currentSelection, const KItemSet& previousSelection) const { QCOMPARE(m_selectionManager->selectedItems(), currentSelection); QCOMPARE(m_selectionManager->hasSelection(), !currentSelection.isEmpty()); for (int index = 0; index < m_selectionManager->model()->count(); ++index) { if (currentSelection.contains(index)) { QVERIFY(m_selectionManager->isSelected(index)); } else { QVERIFY(!m_selectionManager->isSelected(index)); } } if (currentSelection == previousSelection) { QCOMPARE(spy.count(), 0); } else { QCOMPARE(spy.count(), 1); QList arguments = spy.takeFirst(); QCOMPARE(qvariant_cast(arguments.at(0)), currentSelection); QCOMPARE(qvariant_cast(arguments.at(1)), previousSelection); } } QTEST_GUILESS_MAIN(KItemListSelectionManagerTest) #include "kitemlistselectionmanagertest.moc" diff --git a/src/tests/viewpropertiestest.cpp b/src/tests/viewpropertiestest.cpp index 0cd55662d..6e0452209 100644 --- a/src/tests/viewpropertiestest.cpp +++ b/src/tests/viewpropertiestest.cpp @@ -1,98 +1,98 @@ /*************************************************************************** * Copyright (C) 2012 by Peter Penz * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "dolphin_generalsettings.h" #include "views/viewproperties.h" #include "testdir.h" #include class ViewPropertiesTest : public QObject { Q_OBJECT private slots: void init(); void cleanup(); void testReadOnlyBehavior(); void testAutoSave(); private: bool m_globalViewProps; TestDir* m_testDir; }; void ViewPropertiesTest::init() { m_globalViewProps = GeneralSettings::self()->globalViewProps(); GeneralSettings::self()->setGlobalViewProps(false); GeneralSettings::self()->save(); // It is mandatory to create the test-directory inside the home-directory // of the user: ViewProperties does not write inside directories // outside the home-directory to prevent overwriting other user-settings // in case if write-permissions are given. m_testDir = new TestDir(QDir::homePath() + "/.viewPropertiesTest-"); } void ViewPropertiesTest::cleanup() { delete m_testDir; m_testDir = nullptr; GeneralSettings::self()->setGlobalViewProps(m_globalViewProps); GeneralSettings::self()->save(); } /** * Test whether only reading properties won't result in creating * a .directory file when destructing the ViewProperties instance * and autosaving is enabled. */ void ViewPropertiesTest::testReadOnlyBehavior() { QString dotDirectoryFile = m_testDir->url().toLocalFile() + "/.directory"; QVERIFY(!QFile::exists(dotDirectoryFile)); QScopedPointer props(new ViewProperties(m_testDir->url())); QVERIFY(props->isAutoSaveEnabled()); const QByteArray sortRole = props->sortRole(); - Q_UNUSED(sortRole); + Q_UNUSED(sortRole) props.reset(); QVERIFY(!QFile::exists(dotDirectoryFile)); } void ViewPropertiesTest::testAutoSave() { QString dotDirectoryFile = m_testDir->url().toLocalFile() + "/.directory"; QVERIFY(!QFile::exists(dotDirectoryFile)); QScopedPointer props(new ViewProperties(m_testDir->url())); QVERIFY(props->isAutoSaveEnabled()); props->setSortRole("someNewSortRole"); props.reset(); QVERIFY(QFile::exists(dotDirectoryFile)); } QTEST_GUILESS_MAIN(ViewPropertiesTest) #include "viewpropertiestest.moc" diff --git a/src/views/dolphinitemlistview.cpp b/src/views/dolphinitemlistview.cpp index 4651d1068..6397a48b7 100644 --- a/src/views/dolphinitemlistview.cpp +++ b/src/views/dolphinitemlistview.cpp @@ -1,247 +1,247 @@ /*************************************************************************** * Copyright (C) 2011 by Peter Penz * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "dolphinitemlistview.h" #include "dolphin_compactmodesettings.h" #include "dolphin_detailsmodesettings.h" #include "dolphin_generalsettings.h" #include "dolphin_iconsmodesettings.h" #include "dolphinfileitemlistwidget.h" #include "kitemviews/kfileitemmodel.h" #include "kitemviews/kitemlistcontroller.h" #include "views/viewmodecontroller.h" #include "zoomlevelinfo.h" #include DolphinItemListView::DolphinItemListView(QGraphicsWidget* parent) : KFileItemListView(parent), m_zoomLevel(0) { updateFont(); updateGridSize(); } DolphinItemListView::~DolphinItemListView() { writeSettings(); } void DolphinItemListView::setZoomLevel(int level) { if (level < ZoomLevelInfo::minimumLevel()) { level = ZoomLevelInfo::minimumLevel(); } else if (level > ZoomLevelInfo::maximumLevel()) { level = ZoomLevelInfo::maximumLevel(); } if (level == m_zoomLevel) { return; } m_zoomLevel = level; ViewModeSettings settings(viewMode()); if (previewsShown()) { const int previewSize = ZoomLevelInfo::iconSizeForZoomLevel(level); settings.setPreviewSize(previewSize); } else { const int iconSize = ZoomLevelInfo::iconSizeForZoomLevel(level); settings.setIconSize(iconSize); } updateGridSize(); } int DolphinItemListView::zoomLevel() const { return m_zoomLevel; } void DolphinItemListView::readSettings() { ViewModeSettings settings(viewMode()); settings.readConfig(); beginTransaction(); setEnabledSelectionToggles(GeneralSettings::showSelectionToggle()); setSupportsItemExpanding(itemLayoutSupportsItemExpanding(itemLayout())); updateFont(); updateGridSize(); const KConfigGroup globalConfig(KSharedConfig::openConfig(), "PreviewSettings"); setEnabledPlugins(globalConfig.readEntry("Plugins", KIO::PreviewJob::defaultPlugins())); endTransaction(); } void DolphinItemListView::writeSettings() { IconsModeSettings::self()->save(); CompactModeSettings::self()->save(); DetailsModeSettings::self()->save(); } KItemListWidgetCreatorBase* DolphinItemListView::defaultWidgetCreator() const { return new KItemListWidgetCreator(); } bool DolphinItemListView::itemLayoutSupportsItemExpanding(ItemLayout layout) const { return layout == DetailsLayout && DetailsModeSettings::expandableFolders(); } void DolphinItemListView::onItemLayoutChanged(ItemLayout current, ItemLayout previous) { setHeaderVisible(current == DetailsLayout); updateFont(); updateGridSize(); KFileItemListView::onItemLayoutChanged(current, previous); } void DolphinItemListView::onPreviewsShownChanged(bool shown) { - Q_UNUSED(shown); + Q_UNUSED(shown) updateGridSize(); } void DolphinItemListView::onVisibleRolesChanged(const QList& current, const QList& previous) { KFileItemListView::onVisibleRolesChanged(current, previous); updateGridSize(); } void DolphinItemListView::updateFont() { const ViewModeSettings settings(viewMode()); if (settings.useSystemFont()) { KItemListView::updateFont(); } else { QFont font(settings.fontFamily(), qRound(settings.fontSize())); font.setItalic(settings.italicFont()); font.setWeight(settings.fontWeight()); font.setPointSizeF(settings.fontSize()); KItemListStyleOption option = styleOption(); option.font = font; option.fontMetrics = QFontMetrics(font); setStyleOption(option); } } void DolphinItemListView::updateGridSize() { const ViewModeSettings settings(viewMode()); // Calculate the size of the icon const int iconSize = previewsShown() ? settings.previewSize() : settings.iconSize(); m_zoomLevel = ZoomLevelInfo::zoomLevelForIconSize(QSize(iconSize, iconSize)); KItemListStyleOption option = styleOption(); const int padding = 2; int horizontalMargin = 0; int verticalMargin = 0; // Calculate the item-width and item-height int itemWidth; int itemHeight; int maxTextLines = 0; int maxTextWidth = 0; switch (itemLayout()) { case KFileItemListView::IconsLayout: { const int minItemWidth = 48; itemWidth = minItemWidth + IconsModeSettings::textWidthIndex() * 64; if (itemWidth < iconSize + padding * 2) { itemWidth = iconSize + padding * 2; } itemHeight = padding * 3 + iconSize + option.fontMetrics.lineSpacing(); horizontalMargin = 4; verticalMargin = 8; maxTextLines = IconsModeSettings::maximumTextLines(); break; } case KFileItemListView::CompactLayout: { itemWidth = padding * 4 + iconSize + option.fontMetrics.height() * 5; const int textLinesCount = visibleRoles().count(); itemHeight = padding * 2 + qMax(iconSize, textLinesCount * option.fontMetrics.lineSpacing()); if (CompactModeSettings::maximumTextWidthIndex() > 0) { // A restriction is given for the maximum width of the text (0 means // having no restriction) maxTextWidth = option.fontMetrics.height() * 10 * CompactModeSettings::maximumTextWidthIndex(); } horizontalMargin = 8; break; } case KFileItemListView::DetailsLayout: { itemWidth = -1; itemHeight = padding * 2 + qMax(iconSize, option.fontMetrics.lineSpacing()); break; } default: itemWidth = -1; itemHeight = -1; Q_ASSERT(false); break; } // Apply the calculated values option.padding = padding; option.horizontalMargin = horizontalMargin; option.verticalMargin = verticalMargin; option.iconSize = iconSize; option.maxTextLines = maxTextLines; option.maxTextWidth = maxTextWidth; beginTransaction(); setStyleOption(option); setItemSize(QSizeF(itemWidth, itemHeight)); endTransaction(); } ViewModeSettings::ViewMode DolphinItemListView::viewMode() const { ViewModeSettings::ViewMode mode; switch (itemLayout()) { case KFileItemListView::IconsLayout: mode = ViewModeSettings::IconsMode; break; case KFileItemListView::CompactLayout: mode = ViewModeSettings::CompactMode; break; case KFileItemListView::DetailsLayout: mode = ViewModeSettings::DetailsMode; break; default: mode = ViewModeSettings::IconsMode; Q_ASSERT(false); break; } return mode; } diff --git a/src/views/dolphinview.cpp b/src/views/dolphinview.cpp index 3597a2aa4..3437db7a7 100644 --- a/src/views/dolphinview.cpp +++ b/src/views/dolphinview.cpp @@ -1,1858 +1,1858 @@ /*************************************************************************** * Copyright (C) 2006-2009 by Peter Penz * * Copyright (C) 2006 by Gregor KaliÅ¡nik * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "dolphinview.h" #include "dolphin_detailsmodesettings.h" #include "dolphin_generalsettings.h" #include "dolphinitemlistview.h" #include "dolphinnewfilemenuobserver.h" #include "draganddrophelper.h" #include "kitemviews/kfileitemlistview.h" #include "kitemviews/kfileitemmodel.h" #include "kitemviews/kitemlistcontainer.h" #include "kitemviews/kitemlistcontroller.h" #include "kitemviews/kitemlistheader.h" #include "kitemviews/kitemlistselectionmanager.h" #include "renamedialog.h" #include "versioncontrol/versioncontrolobserver.h" #include "viewproperties.h" #include "views/tooltips/tooltipmanager.h" #include "zoomlevelinfo.h" #ifdef HAVE_BALOO #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include DolphinView::DolphinView(const QUrl& url, QWidget* parent) : QWidget(parent), m_active(true), m_tabsForFiles(false), m_assureVisibleCurrentIndex(false), m_isFolderWritable(true), m_dragging(false), m_url(url), m_viewPropertiesContext(), m_mode(DolphinView::IconsView), m_visibleRoles(), m_topLayout(nullptr), m_model(nullptr), m_view(nullptr), m_container(nullptr), m_toolTipManager(nullptr), m_selectionChangedTimer(nullptr), m_currentItemUrl(), m_scrollToCurrentItem(false), m_restoredContentsPosition(), m_selectedUrls(), m_clearSelectionBeforeSelectingNewItems(false), m_markFirstNewlySelectedItemAsCurrent(false), m_versionControlObserver(nullptr), m_twoClicksRenamingTimer(nullptr) { m_topLayout = new QVBoxLayout(this); m_topLayout->setSpacing(0); m_topLayout->setContentsMargins(0, 0, 0, 0); // When a new item has been created by the "Create New..." menu, the item should // get selected and it must be assured that the item will get visible. As the // creation is done asynchronously, several signals must be checked: connect(&DolphinNewFileMenuObserver::instance(), &DolphinNewFileMenuObserver::itemCreated, this, &DolphinView::observeCreatedItem); m_selectionChangedTimer = new QTimer(this); m_selectionChangedTimer->setSingleShot(true); m_selectionChangedTimer->setInterval(300); connect(m_selectionChangedTimer, &QTimer::timeout, this, &DolphinView::emitSelectionChangedSignal); m_model = new KFileItemModel(this); m_view = new DolphinItemListView(); m_view->setEnabledSelectionToggles(GeneralSettings::showSelectionToggle()); m_view->setVisibleRoles({"text"}); applyModeToView(); KItemListController* controller = new KItemListController(m_model, m_view, this); const int delay = GeneralSettings::autoExpandFolders() ? 750 : -1; controller->setAutoActivationDelay(delay); // The EnlargeSmallPreviews setting can only be changed after the model // has been set in the view by KItemListController. m_view->setEnlargeSmallPreviews(GeneralSettings::enlargeSmallPreviews()); m_container = new KItemListContainer(controller, this); m_container->installEventFilter(this); setFocusProxy(m_container); connect(m_container->horizontalScrollBar(), &QScrollBar::valueChanged, this, [=] { hideToolTip(); }); connect(m_container->verticalScrollBar(), &QScrollBar::valueChanged, this, [=] { hideToolTip(); }); controller->setSelectionBehavior(KItemListController::MultiSelection); connect(controller, &KItemListController::itemActivated, this, &DolphinView::slotItemActivated); connect(controller, &KItemListController::itemsActivated, this, &DolphinView::slotItemsActivated); connect(controller, &KItemListController::itemMiddleClicked, this, &DolphinView::slotItemMiddleClicked); connect(controller, &KItemListController::itemContextMenuRequested, this, &DolphinView::slotItemContextMenuRequested); connect(controller, &KItemListController::viewContextMenuRequested, this, &DolphinView::slotViewContextMenuRequested); connect(controller, &KItemListController::headerContextMenuRequested, this, &DolphinView::slotHeaderContextMenuRequested); connect(controller, &KItemListController::mouseButtonPressed, this, &DolphinView::slotMouseButtonPressed); connect(controller, &KItemListController::itemHovered, this, &DolphinView::slotItemHovered); connect(controller, &KItemListController::itemUnhovered, this, &DolphinView::slotItemUnhovered); connect(controller, &KItemListController::itemDropEvent, this, &DolphinView::slotItemDropEvent); connect(controller, &KItemListController::escapePressed, this, &DolphinView::stopLoading); connect(controller, &KItemListController::modelChanged, this, &DolphinView::slotModelChanged); connect(controller, &KItemListController::selectedItemTextPressed, this, &DolphinView::slotSelectedItemTextPressed); connect(m_model, &KFileItemModel::directoryLoadingStarted, this, &DolphinView::slotDirectoryLoadingStarted); connect(m_model, &KFileItemModel::directoryLoadingCompleted, this, &DolphinView::slotDirectoryLoadingCompleted); connect(m_model, &KFileItemModel::directoryLoadingCanceled, this, &DolphinView::directoryLoadingCanceled); connect(m_model, &KFileItemModel::directoryLoadingProgress, this, &DolphinView::directoryLoadingProgress); connect(m_model, &KFileItemModel::directorySortingProgress, this, &DolphinView::directorySortingProgress); connect(m_model, &KFileItemModel::itemsChanged, this, &DolphinView::slotItemsChanged); connect(m_model, &KFileItemModel::itemsRemoved, this, &DolphinView::itemCountChanged); connect(m_model, &KFileItemModel::itemsInserted, this, &DolphinView::itemCountChanged); connect(m_model, &KFileItemModel::infoMessage, this, &DolphinView::infoMessage); connect(m_model, &KFileItemModel::errorMessage, this, &DolphinView::errorMessage); connect(m_model, &KFileItemModel::directoryRedirection, this, &DolphinView::slotDirectoryRedirection); connect(m_model, &KFileItemModel::urlIsFileError, this, &DolphinView::urlIsFileError); m_view->installEventFilter(this); connect(m_view, &DolphinItemListView::sortOrderChanged, this, &DolphinView::slotSortOrderChangedByHeader); connect(m_view, &DolphinItemListView::sortRoleChanged, this, &DolphinView::slotSortRoleChangedByHeader); connect(m_view, &DolphinItemListView::visibleRolesChanged, this, &DolphinView::slotVisibleRolesChangedByHeader); connect(m_view, &DolphinItemListView::roleEditingCanceled, this, &DolphinView::slotRoleEditingCanceled); connect(m_view->header(), &KItemListHeader::columnWidthChangeFinished, this, &DolphinView::slotHeaderColumnWidthChangeFinished); KItemListSelectionManager* selectionManager = controller->selectionManager(); connect(selectionManager, &KItemListSelectionManager::selectionChanged, this, &DolphinView::slotSelectionChanged); #ifdef HAVE_BALOO m_toolTipManager = new ToolTipManager(this); connect(m_toolTipManager, &ToolTipManager::urlActivated, this, &DolphinView::urlActivated); #endif m_versionControlObserver = new VersionControlObserver(this); m_versionControlObserver->setView(this); m_versionControlObserver->setModel(m_model); connect(m_versionControlObserver, &VersionControlObserver::infoMessage, this, &DolphinView::infoMessage); connect(m_versionControlObserver, &VersionControlObserver::errorMessage, this, &DolphinView::errorMessage); connect(m_versionControlObserver, &VersionControlObserver::operationCompletedMessage, this, &DolphinView::operationCompletedMessage); m_twoClicksRenamingTimer = new QTimer(this); m_twoClicksRenamingTimer->setSingleShot(true); connect(m_twoClicksRenamingTimer, &QTimer::timeout, this, &DolphinView::slotTwoClicksRenamingTimerTimeout); applyViewProperties(); m_topLayout->addWidget(m_container); loadDirectory(url); } DolphinView::~DolphinView() { } QUrl DolphinView::url() const { return m_url; } void DolphinView::setActive(bool active) { if (active == m_active) { return; } m_active = active; updatePalette(); if (active) { m_container->setFocus(); emit activated(); emit writeStateChanged(m_isFolderWritable); } } bool DolphinView::isActive() const { return m_active; } void DolphinView::setMode(Mode mode) { if (mode != m_mode) { ViewProperties props(viewPropertiesUrl()); props.setViewMode(mode); // We pass the new ViewProperties to applyViewProperties, rather than // storing them on disk and letting applyViewProperties() read them // from there, to prevent that changing the view mode fails if the // .directory file is not writable (see bug 318534). applyViewProperties(props); } } DolphinView::Mode DolphinView::mode() const { return m_mode; } void DolphinView::setPreviewsShown(bool show) { if (previewsShown() == show) { return; } ViewProperties props(viewPropertiesUrl()); props.setPreviewsShown(show); const int oldZoomLevel = m_view->zoomLevel(); m_view->setPreviewsShown(show); emit previewsShownChanged(show); const int newZoomLevel = m_view->zoomLevel(); if (newZoomLevel != oldZoomLevel) { emit zoomLevelChanged(newZoomLevel, oldZoomLevel); } } bool DolphinView::previewsShown() const { return m_view->previewsShown(); } void DolphinView::setHiddenFilesShown(bool show) { if (m_model->showHiddenFiles() == show) { return; } const KFileItemList itemList = selectedItems(); m_selectedUrls.clear(); m_selectedUrls = itemList.urlList(); ViewProperties props(viewPropertiesUrl()); props.setHiddenFilesShown(show); m_model->setShowHiddenFiles(show); emit hiddenFilesShownChanged(show); } bool DolphinView::hiddenFilesShown() const { return m_model->showHiddenFiles(); } void DolphinView::setGroupedSorting(bool grouped) { if (grouped == groupedSorting()) { return; } ViewProperties props(viewPropertiesUrl()); props.setGroupedSorting(grouped); props.save(); m_container->controller()->model()->setGroupedSorting(grouped); emit groupedSortingChanged(grouped); } bool DolphinView::groupedSorting() const { return m_model->groupedSorting(); } KFileItemList DolphinView::items() const { KFileItemList list; const int itemCount = m_model->count(); list.reserve(itemCount); for (int i = 0; i < itemCount; ++i) { list.append(m_model->fileItem(i)); } return list; } int DolphinView::itemsCount() const { return m_model->count(); } KFileItemList DolphinView::selectedItems() const { const KItemListSelectionManager* selectionManager = m_container->controller()->selectionManager(); KFileItemList selectedItems; const auto items = selectionManager->selectedItems(); selectedItems.reserve(items.count()); for (int index : items) { selectedItems.append(m_model->fileItem(index)); } return selectedItems; } int DolphinView::selectedItemsCount() const { const KItemListSelectionManager* selectionManager = m_container->controller()->selectionManager(); return selectionManager->selectedItems().count(); } void DolphinView::markUrlsAsSelected(const QList& urls) { m_selectedUrls = urls; } void DolphinView::markUrlAsCurrent(const QUrl &url) { m_currentItemUrl = url; m_scrollToCurrentItem = true; } void DolphinView::selectItems(const QRegExp& pattern, bool enabled) { const KItemListSelectionManager::SelectionMode mode = enabled ? KItemListSelectionManager::Select : KItemListSelectionManager::Deselect; KItemListSelectionManager* selectionManager = m_container->controller()->selectionManager(); for (int index = 0; index < m_model->count(); index++) { const KFileItem item = m_model->fileItem(index); if (pattern.exactMatch(item.text())) { // An alternative approach would be to store the matching items in a KItemSet and // select them in one go after the loop, but we'd need a new function // KItemListSelectionManager::setSelected(KItemSet, SelectionMode mode) // for that. selectionManager->setSelected(index, 1, mode); } } } void DolphinView::setZoomLevel(int level) { const int oldZoomLevel = zoomLevel(); m_view->setZoomLevel(level); if (zoomLevel() != oldZoomLevel) { hideToolTip(); emit zoomLevelChanged(zoomLevel(), oldZoomLevel); } } int DolphinView::zoomLevel() const { return m_view->zoomLevel(); } void DolphinView::setSortRole(const QByteArray& role) { if (role != sortRole()) { updateSortRole(role); } } QByteArray DolphinView::sortRole() const { const KItemModelBase* model = m_container->controller()->model(); return model->sortRole(); } void DolphinView::setSortOrder(Qt::SortOrder order) { if (sortOrder() != order) { updateSortOrder(order); } } Qt::SortOrder DolphinView::sortOrder() const { return m_model->sortOrder(); } void DolphinView::setSortFoldersFirst(bool foldersFirst) { if (sortFoldersFirst() != foldersFirst) { updateSortFoldersFirst(foldersFirst); } } bool DolphinView::sortFoldersFirst() const { return m_model->sortDirectoriesFirst(); } void DolphinView::setVisibleRoles(const QList& roles) { const QList previousRoles = roles; ViewProperties props(viewPropertiesUrl()); props.setVisibleRoles(roles); m_visibleRoles = roles; m_view->setVisibleRoles(roles); emit visibleRolesChanged(m_visibleRoles, previousRoles); } QList DolphinView::visibleRoles() const { return m_visibleRoles; } void DolphinView::reload() { QByteArray viewState; QDataStream saveStream(&viewState, QIODevice::WriteOnly); saveState(saveStream); setUrl(url()); loadDirectory(url(), true); QDataStream restoreStream(viewState); restoreState(restoreStream); } void DolphinView::readSettings() { const int oldZoomLevel = m_view->zoomLevel(); GeneralSettings::self()->load(); m_view->readSettings(); applyViewProperties(); const int delay = GeneralSettings::autoExpandFolders() ? 750 : -1; m_container->controller()->setAutoActivationDelay(delay); const int newZoomLevel = m_view->zoomLevel(); if (newZoomLevel != oldZoomLevel) { emit zoomLevelChanged(newZoomLevel, oldZoomLevel); } } void DolphinView::writeSettings() { GeneralSettings::self()->save(); m_view->writeSettings(); } void DolphinView::setNameFilter(const QString& nameFilter) { m_model->setNameFilter(nameFilter); } QString DolphinView::nameFilter() const { return m_model->nameFilter(); } void DolphinView::setMimeTypeFilters(const QStringList& filters) { return m_model->setMimeTypeFilters(filters); } QStringList DolphinView::mimeTypeFilters() const { return m_model->mimeTypeFilters(); } QString DolphinView::statusBarText() const { QString summary; QString foldersText; QString filesText; int folderCount = 0; int fileCount = 0; KIO::filesize_t totalFileSize = 0; if (m_container->controller()->selectionManager()->hasSelection()) { // Give a summary of the status of the selected files const KFileItemList list = selectedItems(); foreach (const KFileItem& item, list) { if (item.isDir()) { ++folderCount; } else { ++fileCount; totalFileSize += item.size(); } } if (folderCount + fileCount == 1) { // If only one item is selected, show info about it return list.first().getStatusBarInfo(); } else { // At least 2 items are selected foldersText = i18ncp("@info:status", "1 Folder selected", "%1 Folders selected", folderCount); filesText = i18ncp("@info:status", "1 File selected", "%1 Files selected", fileCount); } } else { calculateItemCount(fileCount, folderCount, totalFileSize); foldersText = i18ncp("@info:status", "1 Folder", "%1 Folders", folderCount); filesText = i18ncp("@info:status", "1 File", "%1 Files", fileCount); } if (fileCount > 0 && folderCount > 0) { summary = i18nc("@info:status folders, files (size)", "%1, %2 (%3)", foldersText, filesText, KFormat().formatByteSize(totalFileSize)); } else if (fileCount > 0) { summary = i18nc("@info:status files (size)", "%1 (%2)", filesText, KFormat().formatByteSize(totalFileSize)); } else if (folderCount > 0) { summary = foldersText; } else { summary = i18nc("@info:status", "0 Folders, 0 Files"); } return summary; } QList DolphinView::versionControlActions(const KFileItemList& items) const { QList actions; if (items.isEmpty()) { const KFileItem item = m_model->rootItem(); if (!item.isNull()) { actions = m_versionControlObserver->actions(KFileItemList() << item); } } else { actions = m_versionControlObserver->actions(items); } return actions; } void DolphinView::setUrl(const QUrl& url) { if (url == m_url) { return; } clearSelection(); m_url = url; hideToolTip(); disconnect(m_view, &DolphinItemListView::roleEditingFinished, this, &DolphinView::slotRoleEditingFinished); // It is important to clear the items from the model before // applying the view properties, otherwise expensive operations // might be done on the existing items although they get cleared // anyhow afterwards by loadDirectory(). m_model->clear(); applyViewProperties(); loadDirectory(url); emit urlChanged(url); } void DolphinView::selectAll() { KItemListSelectionManager* selectionManager = m_container->controller()->selectionManager(); selectionManager->setSelected(0, m_model->count()); } void DolphinView::invertSelection() { KItemListSelectionManager* selectionManager = m_container->controller()->selectionManager(); selectionManager->setSelected(0, m_model->count(), KItemListSelectionManager::Toggle); } void DolphinView::clearSelection() { m_selectedUrls.clear(); m_container->controller()->selectionManager()->clearSelection(); } void DolphinView::renameSelectedItems() { const KFileItemList items = selectedItems(); if (items.isEmpty()) { return; } if (items.count() == 1 && GeneralSettings::renameInline()) { const int index = m_model->index(items.first()); m_view->editRole(index, "text"); hideToolTip(); connect(m_view, &DolphinItemListView::roleEditingFinished, this, &DolphinView::slotRoleEditingFinished); } else { RenameDialog* dialog = new RenameDialog(this, items); connect(dialog, &RenameDialog::renamingFinished, this, &DolphinView::slotRenameDialogRenamingFinished); dialog->open(); } // Assure that the current index remains visible when KFileItemModel // will notify the view about changed items (which might result in // a changed sorting). m_assureVisibleCurrentIndex = true; } void DolphinView::trashSelectedItems() { const QList list = simplifiedSelectedUrls(); KIO::JobUiDelegate uiDelegate; uiDelegate.setWindow(window()); if (uiDelegate.askDeleteConfirmation(list, KIO::JobUiDelegate::Trash, KIO::JobUiDelegate::DefaultConfirmation)) { KIO::Job* job = KIO::trash(list); KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Trash, list, QUrl(QStringLiteral("trash:/")), job); KJobWidgets::setWindow(job, this); connect(job, &KIO::Job::result, this, &DolphinView::slotTrashFileFinished); } } void DolphinView::deleteSelectedItems() { const QList list = simplifiedSelectedUrls(); KIO::JobUiDelegate uiDelegate; uiDelegate.setWindow(window()); if (uiDelegate.askDeleteConfirmation(list, KIO::JobUiDelegate::Delete, KIO::JobUiDelegate::DefaultConfirmation)) { KIO::Job* job = KIO::del(list); KJobWidgets::setWindow(job, this); connect(job, &KIO::Job::result, this, &DolphinView::slotDeleteFileFinished); } } void DolphinView::cutSelectedItems() { QMimeData* mimeData = selectionMimeData(); KIO::setClipboardDataCut(mimeData, true); QApplication::clipboard()->setMimeData(mimeData); } void DolphinView::copySelectedItems() { QMimeData* mimeData = selectionMimeData(); QApplication::clipboard()->setMimeData(mimeData); } void DolphinView::paste() { pasteToUrl(url()); } void DolphinView::pasteIntoFolder() { const KFileItemList items = selectedItems(); if ((items.count() == 1) && items.first().isDir()) { pasteToUrl(items.first().url()); } } void DolphinView::stopLoading() { m_model->cancelDirectoryLoading(); } void DolphinView::updatePalette() { QColor color = KColorScheme(isActiveWindow() ? QPalette::Active : QPalette::Inactive, KColorScheme::View).background().color(); if (!m_active) { color.setAlpha(150); } QWidget* viewport = m_container->viewport(); if (viewport) { QPalette palette; palette.setColor(viewport->backgroundRole(), color); viewport->setPalette(palette); } update(); } void DolphinView::abortTwoClicksRenaming() { m_twoClicksRenamingItemUrl.clear(); m_twoClicksRenamingTimer->stop(); } bool DolphinView::eventFilter(QObject* watched, QEvent* event) { switch (event->type()) { case QEvent::PaletteChange: updatePalette(); QPixmapCache::clear(); break; case QEvent::WindowActivate: case QEvent::WindowDeactivate: updatePalette(); break; case QEvent::KeyPress: hideToolTip(ToolTipManager::HideBehavior::Instantly); if (GeneralSettings::useTabForSwitchingSplitView()) { QKeyEvent* keyEvent = static_cast(event); if (keyEvent->key() == Qt::Key_Tab && keyEvent->modifiers() == Qt::NoModifier) { emit toggleActiveViewRequested(); return true; } } break; case QEvent::FocusIn: if (watched == m_container) { setActive(true); } break; case QEvent::GraphicsSceneDragEnter: if (watched == m_view) { m_dragging = true; abortTwoClicksRenaming(); } break; case QEvent::GraphicsSceneDragLeave: if (watched == m_view) { m_dragging = false; } break; case QEvent::GraphicsSceneDrop: if (watched == m_view) { m_dragging = false; } default: break; } return QWidget::eventFilter(watched, event); } void DolphinView::wheelEvent(QWheelEvent* event) { if (event->modifiers().testFlag(Qt::ControlModifier)) { const int numDegrees = event->delta() / 8; const int numSteps = numDegrees / 15; setZoomLevel(zoomLevel() + numSteps); event->accept(); } else { event->ignore(); } } void DolphinView::hideEvent(QHideEvent* event) { hideToolTip(); QWidget::hideEvent(event); } bool DolphinView::event(QEvent* event) { if (event->type() == QEvent::WindowDeactivate) { /* See Bug 297355 * Dolphin leaves file preview tooltips open even when is not visible. * * Hide tool-tip when Dolphin loses focus. */ hideToolTip(); abortTwoClicksRenaming(); } return QWidget::event(event); } void DolphinView::activate() { setActive(true); } void DolphinView::slotItemActivated(int index) { abortTwoClicksRenaming(); const KFileItem item = m_model->fileItem(index); if (!item.isNull()) { emit itemActivated(item); } } void DolphinView::slotItemsActivated(const KItemSet& indexes) { Q_ASSERT(indexes.count() >= 2); abortTwoClicksRenaming(); if (indexes.count() > 5) { QString question = i18np("Are you sure you want to open 1 item?", "Are you sure you want to open %1 items?", indexes.count()); const int answer = KMessageBox::warningYesNo(this, question); if (answer != KMessageBox::Yes) { return; } } KFileItemList items; items.reserve(indexes.count()); for (int index : indexes) { KFileItem item = m_model->fileItem(index); const QUrl& url = openItemAsFolderUrl(item); if (!url.isEmpty()) { // Open folders in new tabs emit tabRequested(url, DolphinTabWidget::AfterLastTab); } else { items.append(item); } } if (items.count() == 1) { emit itemActivated(items.first()); } else if (items.count() > 1) { emit itemsActivated(items); } } void DolphinView::slotItemMiddleClicked(int index) { const KFileItem& item = m_model->fileItem(index); const QUrl& url = openItemAsFolderUrl(item); if (!url.isEmpty()) { emit tabRequested(url, DolphinTabWidget::AfterCurrentTab); } else if (isTabsForFilesEnabled()) { emit tabRequested(item.url(), DolphinTabWidget::AfterCurrentTab); } } void DolphinView::slotItemContextMenuRequested(int index, const QPointF& pos) { // Force emit of a selection changed signal before we request the // context menu, to update the edit-actions first. (See Bug 294013) if (m_selectionChangedTimer->isActive()) { emitSelectionChangedSignal(); } const KFileItem item = m_model->fileItem(index); emit requestContextMenu(pos.toPoint(), item, url(), QList()); } void DolphinView::slotViewContextMenuRequested(const QPointF& pos) { emit requestContextMenu(pos.toPoint(), KFileItem(), url(), QList()); } void DolphinView::slotHeaderContextMenuRequested(const QPointF& pos) { ViewProperties props(viewPropertiesUrl()); QPointer menu = new QMenu(QApplication::activeWindow()); KItemListView* view = m_container->controller()->view(); const QSet visibleRolesSet = view->visibleRoles().toSet(); bool indexingEnabled = false; #ifdef HAVE_BALOO Baloo::IndexerConfig config; indexingEnabled = config.fileIndexingEnabled(); #endif QString groupName; QMenu* groupMenu = nullptr; // Add all roles to the menu that can be shown or hidden by the user const QList rolesInfo = KFileItemModel::rolesInformation(); foreach (const KFileItemModel::RoleInfo& info, rolesInfo) { if (info.role == "text") { // It should not be possible to hide the "text" role continue; } const QString text = m_model->roleDescription(info.role); QAction* action = nullptr; if (info.group.isEmpty()) { action = menu->addAction(text); } else { if (!groupMenu || info.group != groupName) { groupName = info.group; groupMenu = menu->addMenu(groupName); } action = groupMenu->addAction(text); } action->setCheckable(true); action->setChecked(visibleRolesSet.contains(info.role)); action->setData(info.role); const bool enable = (!info.requiresBaloo && !info.requiresIndexer) || (info.requiresBaloo) || (info.requiresIndexer && indexingEnabled); action->setEnabled(enable); } menu->addSeparator(); QActionGroup* widthsGroup = new QActionGroup(menu); const bool autoColumnWidths = props.headerColumnWidths().isEmpty(); QAction* autoAdjustWidthsAction = menu->addAction(i18nc("@action:inmenu", "Automatic Column Widths")); autoAdjustWidthsAction->setCheckable(true); autoAdjustWidthsAction->setChecked(autoColumnWidths); autoAdjustWidthsAction->setActionGroup(widthsGroup); QAction* customWidthsAction = menu->addAction(i18nc("@action:inmenu", "Custom Column Widths")); customWidthsAction->setCheckable(true); customWidthsAction->setChecked(!autoColumnWidths); customWidthsAction->setActionGroup(widthsGroup); QAction* action = menu->exec(pos.toPoint()); if (menu && action) { KItemListHeader* header = view->header(); if (action == autoAdjustWidthsAction) { // Clear the column-widths from the viewproperties and turn on // the automatic resizing of the columns props.setHeaderColumnWidths(QList()); header->setAutomaticColumnResizing(true); } else if (action == customWidthsAction) { // Apply the current column-widths as custom column-widths and turn // off the automatic resizing of the columns QList columnWidths; columnWidths.reserve(view->visibleRoles().count()); foreach (const QByteArray& role, view->visibleRoles()) { columnWidths.append(header->columnWidth(role)); } props.setHeaderColumnWidths(columnWidths); header->setAutomaticColumnResizing(false); } else { // Show or hide the selected role const QByteArray selectedRole = action->data().toByteArray(); QList visibleRoles = view->visibleRoles(); if (action->isChecked()) { visibleRoles.append(selectedRole); } else { visibleRoles.removeOne(selectedRole); } view->setVisibleRoles(visibleRoles); props.setVisibleRoles(visibleRoles); QList columnWidths; if (!header->automaticColumnResizing()) { columnWidths.reserve(view->visibleRoles().count()); foreach (const QByteArray& role, view->visibleRoles()) { columnWidths.append(header->columnWidth(role)); } } props.setHeaderColumnWidths(columnWidths); } } delete menu; } void DolphinView::slotHeaderColumnWidthChangeFinished(const QByteArray& role, qreal current) { const QList visibleRoles = m_view->visibleRoles(); ViewProperties props(viewPropertiesUrl()); QList columnWidths = props.headerColumnWidths(); if (columnWidths.count() != visibleRoles.count()) { columnWidths.clear(); columnWidths.reserve(visibleRoles.count()); const KItemListHeader* header = m_view->header(); foreach (const QByteArray& role, visibleRoles) { const int width = header->columnWidth(role); columnWidths.append(width); } } const int roleIndex = visibleRoles.indexOf(role); Q_ASSERT(roleIndex >= 0 && roleIndex < columnWidths.count()); columnWidths[roleIndex] = current; props.setHeaderColumnWidths(columnWidths); } void DolphinView::slotItemHovered(int index) { const KFileItem item = m_model->fileItem(index); if (GeneralSettings::showToolTips() && !m_dragging) { QRectF itemRect = m_container->controller()->view()->itemContextRect(index); const QPoint pos = m_container->mapToGlobal(itemRect.topLeft().toPoint()); itemRect.moveTo(pos); #ifdef HAVE_BALOO m_toolTipManager->showToolTip(item, itemRect, nativeParentWidget()->windowHandle()); #endif } emit requestItemInfo(item); } void DolphinView::slotItemUnhovered(int index) { - Q_UNUSED(index); + Q_UNUSED(index) hideToolTip(); emit requestItemInfo(KFileItem()); } void DolphinView::slotItemDropEvent(int index, QGraphicsSceneDragDropEvent* event) { QUrl destUrl; KFileItem destItem = m_model->fileItem(index); if (destItem.isNull() || (!destItem.isDir() && !destItem.isDesktopFile())) { // Use the URL of the view as drop target if the item is no directory // or desktop-file destItem = m_model->rootItem(); destUrl = url(); } else { // The item represents a directory or desktop-file destUrl = destItem.mostLocalUrl(); } QDropEvent dropEvent(event->pos().toPoint(), event->possibleActions(), event->mimeData(), event->buttons(), event->modifiers()); dropUrls(destUrl, &dropEvent, this); setActive(true); } void DolphinView::dropUrls(const QUrl &destUrl, QDropEvent *dropEvent, QWidget *dropWidget) { KIO::DropJob* job = DragAndDropHelper::dropUrls(destUrl, dropEvent, dropWidget); if (job) { connect(job, &KIO::DropJob::result, this, &DolphinView::slotPasteJobResult); if (destUrl == url()) { // Mark the dropped urls as selected. m_clearSelectionBeforeSelectingNewItems = true; m_markFirstNewlySelectedItemAsCurrent = true; connect(job, &KIO::DropJob::itemCreated, this, &DolphinView::slotItemCreated); } } } void DolphinView::slotModelChanged(KItemModelBase* current, KItemModelBase* previous) { if (previous != nullptr) { Q_ASSERT(qobject_cast(previous)); KFileItemModel* fileItemModel = static_cast(previous); disconnect(fileItemModel, &KFileItemModel::directoryLoadingCompleted, this, &DolphinView::slotDirectoryLoadingCompleted); m_versionControlObserver->setModel(nullptr); } if (current) { Q_ASSERT(qobject_cast(current)); KFileItemModel* fileItemModel = static_cast(current); connect(fileItemModel, &KFileItemModel::directoryLoadingCompleted, this, &DolphinView::slotDirectoryLoadingCompleted); m_versionControlObserver->setModel(fileItemModel); } } void DolphinView::slotMouseButtonPressed(int itemIndex, Qt::MouseButtons buttons) { - Q_UNUSED(itemIndex); + Q_UNUSED(itemIndex) hideToolTip(); if (buttons & Qt::BackButton) { emit goBackRequested(); } else if (buttons & Qt::ForwardButton) { emit goForwardRequested(); } } void DolphinView::slotSelectedItemTextPressed(int index) { if (GeneralSettings::renameInline() && !m_view->style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick)) { const KFileItem item = m_model->fileItem(index); const KFileItemListProperties capabilities(KFileItemList() << item); if (capabilities.supportsMoving()) { m_twoClicksRenamingItemUrl = item.url(); m_twoClicksRenamingTimer->start(QApplication::doubleClickInterval()); } } } void DolphinView::slotItemCreated(const QUrl& url) { if (m_markFirstNewlySelectedItemAsCurrent) { markUrlAsCurrent(url); m_markFirstNewlySelectedItemAsCurrent = false; } m_selectedUrls << url; } void DolphinView::slotPasteJobResult(KJob *job) { if (job->error()) { emit errorMessage(job->errorString()); } if (!m_selectedUrls.isEmpty()) { m_selectedUrls << KDirModel::simplifiedUrlList(m_selectedUrls); } } void DolphinView::slotSelectionChanged(const KItemSet& current, const KItemSet& previous) { const int currentCount = current.count(); const int previousCount = previous.count(); const bool selectionStateChanged = (currentCount == 0 && previousCount > 0) || (currentCount > 0 && previousCount == 0); // If nothing has been selected before and something got selected (or if something // was selected before and now nothing is selected) the selectionChangedSignal must // be emitted asynchronously as fast as possible to update the edit-actions. m_selectionChangedTimer->setInterval(selectionStateChanged ? 0 : 300); m_selectionChangedTimer->start(); } void DolphinView::emitSelectionChangedSignal() { m_selectionChangedTimer->stop(); emit selectionChanged(selectedItems()); } void DolphinView::updateSortRole(const QByteArray& role) { ViewProperties props(viewPropertiesUrl()); props.setSortRole(role); KItemModelBase* model = m_container->controller()->model(); model->setSortRole(role); emit sortRoleChanged(role); } void DolphinView::updateSortOrder(Qt::SortOrder order) { ViewProperties props(viewPropertiesUrl()); props.setSortOrder(order); m_model->setSortOrder(order); emit sortOrderChanged(order); } void DolphinView::updateSortFoldersFirst(bool foldersFirst) { ViewProperties props(viewPropertiesUrl()); props.setSortFoldersFirst(foldersFirst); m_model->setSortDirectoriesFirst(foldersFirst); emit sortFoldersFirstChanged(foldersFirst); } QPair DolphinView::pasteInfo() const { const QMimeData *mimeData = QApplication::clipboard()->mimeData(); QPair info; info.second = KIO::pasteActionText(mimeData, &info.first, rootItem()); return info; } void DolphinView::setTabsForFilesEnabled(bool tabsForFiles) { m_tabsForFiles = tabsForFiles; } bool DolphinView::isTabsForFilesEnabled() const { return m_tabsForFiles; } bool DolphinView::itemsExpandable() const { return m_mode == DetailsView; } void DolphinView::restoreState(QDataStream& stream) { // Read the version number of the view state and check if the version is supported. quint32 version = 0; stream >> version; if (version != 1) { // The version of the view state isn't supported, we can't restore it. return; } // Restore the current item that had the keyboard focus stream >> m_currentItemUrl; // Restore the previously selected items stream >> m_selectedUrls; // Restore the view position stream >> m_restoredContentsPosition; // Restore expanded folders (only relevant for the details view - will be ignored by the view in other view modes) QSet urls; stream >> urls; m_model->restoreExpandedDirectories(urls); } void DolphinView::saveState(QDataStream& stream) { stream << quint32(1); // View state version // Save the current item that has the keyboard focus const int currentIndex = m_container->controller()->selectionManager()->currentItem(); if (currentIndex != -1) { KFileItem item = m_model->fileItem(currentIndex); Q_ASSERT(!item.isNull()); // If the current index is valid a item must exist QUrl currentItemUrl = item.url(); stream << currentItemUrl; } else { stream << QUrl(); } // Save the selected urls stream << selectedItems().urlList(); // Save view position const qreal x = m_container->horizontalScrollBar()->value(); const qreal y = m_container->verticalScrollBar()->value(); stream << QPoint(x, y); // Save expanded folders (only relevant for the details view - the set will be empty in other view modes) stream << m_model->expandedDirectories(); } KFileItem DolphinView::rootItem() const { return m_model->rootItem(); } void DolphinView::setViewPropertiesContext(const QString& context) { m_viewPropertiesContext = context; } QString DolphinView::viewPropertiesContext() const { return m_viewPropertiesContext; } QUrl DolphinView::openItemAsFolderUrl(const KFileItem& item, const bool browseThroughArchives) { if (item.isNull()) { return QUrl(); } QUrl url = item.targetUrl(); if (item.isDir()) { return url; } if (item.isMimeTypeKnown()) { const QString& mimetype = item.mimetype(); if (browseThroughArchives && item.isFile() && url.isLocalFile()) { // Generic mechanism for redirecting to tar:// when clicking on a tar file, // zip:// when clicking on a zip file, etc. // The .protocol file specifies the mimetype that the kioslave handles. // Note that we don't use mimetype inheritance since we don't want to // open OpenDocument files as zip folders... const QString& protocol = KProtocolManager::protocolForArchiveMimetype(mimetype); if (!protocol.isEmpty()) { url.setScheme(protocol); return url; } } if (mimetype == QLatin1String("application/x-desktop")) { // Redirect to the URL in Type=Link desktop files, unless it is a http(s) URL. KDesktopFile desktopFile(url.toLocalFile()); if (desktopFile.hasLinkType()) { const QString linkUrl = desktopFile.readUrl(); if (!linkUrl.startsWith(QLatin1String("http"))) { return QUrl::fromUserInput(linkUrl); } } } } return QUrl(); } void DolphinView::resetZoomLevel() { ViewModeSettings::ViewMode mode; switch (m_mode) { case IconsView: mode = ViewModeSettings::IconsMode; break; case CompactView: mode = ViewModeSettings::CompactMode; break; case DetailsView: mode = ViewModeSettings::DetailsMode; break; } const ViewModeSettings settings(mode); const QSize iconSize = QSize(settings.iconSize(), settings.iconSize()); setZoomLevel(ZoomLevelInfo::zoomLevelForIconSize(iconSize)); } void DolphinView::observeCreatedItem(const QUrl& url) { if (m_active) { forceUrlsSelection(url, {url}); } } void DolphinView::slotDirectoryRedirection(const QUrl& oldUrl, const QUrl& newUrl) { if (oldUrl.matches(url(), QUrl::StripTrailingSlash)) { emit redirection(oldUrl, newUrl); m_url = newUrl; // #186947 } } void DolphinView::updateViewState() { if (m_currentItemUrl != QUrl()) { KItemListSelectionManager* selectionManager = m_container->controller()->selectionManager(); // if there is a selection already, leave it that way if (!selectionManager->hasSelection()) { const int currentIndex = m_model->index(m_currentItemUrl); if (currentIndex != -1) { selectionManager->setCurrentItem(currentIndex); // scroll to current item and reset the state if (m_scrollToCurrentItem) { m_view->scrollToItem(currentIndex); m_scrollToCurrentItem = false; } } else { selectionManager->setCurrentItem(0); } } m_currentItemUrl = QUrl(); } if (!m_restoredContentsPosition.isNull()) { const int x = m_restoredContentsPosition.x(); const int y = m_restoredContentsPosition.y(); m_restoredContentsPosition = QPoint(); m_container->horizontalScrollBar()->setValue(x); m_container->verticalScrollBar()->setValue(y); } if (!m_selectedUrls.isEmpty()) { KItemListSelectionManager* selectionManager = m_container->controller()->selectionManager(); // if there is a selection already, leave it that way if (!selectionManager->hasSelection()) { if (m_clearSelectionBeforeSelectingNewItems) { selectionManager->clearSelection(); m_clearSelectionBeforeSelectingNewItems = false; } KItemSet selectedItems = selectionManager->selectedItems(); QList::iterator it = m_selectedUrls.begin(); while (it != m_selectedUrls.end()) { const int index = m_model->index(*it); if (index >= 0) { selectedItems.insert(index); it = m_selectedUrls.erase(it); } else { ++it; } } selectionManager->beginAnchoredSelection(selectionManager->currentItem()); selectionManager->setSelectedItems(selectedItems); } } } void DolphinView::hideToolTip(const ToolTipManager::HideBehavior behavior) { #ifdef HAVE_BALOO if (GeneralSettings::showToolTips()) { m_toolTipManager->hideToolTip(behavior); } #endif } void DolphinView::calculateItemCount(int& fileCount, int& folderCount, KIO::filesize_t& totalFileSize) const { const int itemCount = m_model->count(); for (int i = 0; i < itemCount; ++i) { const KFileItem item = m_model->fileItem(i); if (item.isDir()) { ++folderCount; } else { ++fileCount; totalFileSize += item.size(); } } } void DolphinView::slotTwoClicksRenamingTimerTimeout() { const KItemListSelectionManager* selectionManager = m_container->controller()->selectionManager(); // verify that only one item is selected if (selectionManager->selectedItems().count() == 1) { const int index = selectionManager->currentItem(); const QUrl fileItemUrl = m_model->fileItem(index).url(); // check if the selected item was the same item that started the twoClicksRenaming if (fileItemUrl.isValid() && m_twoClicksRenamingItemUrl == fileItemUrl) { renameSelectedItems(); } } } void DolphinView::slotTrashFileFinished(KJob* job) { if (job->error() == 0) { emit operationCompletedMessage(i18nc("@info:status", "Trash operation completed.")); } else if (job->error() != KIO::ERR_USER_CANCELED) { emit errorMessage(job->errorString()); } } void DolphinView::slotDeleteFileFinished(KJob* job) { if (job->error() == 0) { emit operationCompletedMessage(i18nc("@info:status", "Delete operation completed.")); } else if (job->error() != KIO::ERR_USER_CANCELED) { emit errorMessage(job->errorString()); } } void DolphinView::slotRenamingResult(KJob* job) { if (job->error()) { KIO::CopyJob *copyJob = qobject_cast(job); Q_ASSERT(copyJob); const QUrl newUrl = copyJob->destUrl(); const int index = m_model->index(newUrl); if (index >= 0) { QHash data; const QUrl oldUrl = copyJob->srcUrls().at(0); data.insert("text", oldUrl.fileName()); m_model->setData(index, data); } } } void DolphinView::slotDirectoryLoadingStarted() { // Disable the writestate temporary until it can be determined in a fast way // in DolphinView::slotDirectoryLoadingCompleted() if (m_isFolderWritable) { m_isFolderWritable = false; emit writeStateChanged(m_isFolderWritable); } emit directoryLoadingStarted(); } void DolphinView::slotDirectoryLoadingCompleted() { // Update the view-state. This has to be done asynchronously // because the view might not be in its final state yet. QTimer::singleShot(0, this, &DolphinView::updateViewState); emit directoryLoadingCompleted(); updateWritableState(); } void DolphinView::slotItemsChanged() { m_assureVisibleCurrentIndex = false; } void DolphinView::slotSortOrderChangedByHeader(Qt::SortOrder current, Qt::SortOrder previous) { - Q_UNUSED(previous); + Q_UNUSED(previous) Q_ASSERT(m_model->sortOrder() == current); ViewProperties props(viewPropertiesUrl()); props.setSortOrder(current); emit sortOrderChanged(current); } void DolphinView::slotSortRoleChangedByHeader(const QByteArray& current, const QByteArray& previous) { - Q_UNUSED(previous); + Q_UNUSED(previous) Q_ASSERT(m_model->sortRole() == current); ViewProperties props(viewPropertiesUrl()); props.setSortRole(current); emit sortRoleChanged(current); } void DolphinView::slotVisibleRolesChangedByHeader(const QList& current, const QList& previous) { - Q_UNUSED(previous); + Q_UNUSED(previous) Q_ASSERT(m_container->controller()->view()->visibleRoles() == current); const QList previousVisibleRoles = m_visibleRoles; m_visibleRoles = current; ViewProperties props(viewPropertiesUrl()); props.setVisibleRoles(m_visibleRoles); emit visibleRolesChanged(m_visibleRoles, previousVisibleRoles); } void DolphinView::slotRoleEditingCanceled() { disconnect(m_view, &DolphinItemListView::roleEditingFinished, this, &DolphinView::slotRoleEditingFinished); } void DolphinView::slotRoleEditingFinished(int index, const QByteArray& role, const QVariant& value) { disconnect(m_view, &DolphinItemListView::roleEditingFinished, this, &DolphinView::slotRoleEditingFinished); if (index < 0 || index >= m_model->count()) { return; } if (role == "text") { const KFileItem oldItem = m_model->fileItem(index); const QString newName = value.toString(); if (!newName.isEmpty() && newName != oldItem.text() && newName != QLatin1Char('.') && newName != QLatin1String("..")) { const QUrl oldUrl = oldItem.url(); QUrl newUrl = oldUrl.adjusted(QUrl::RemoveFilename); newUrl.setPath(newUrl.path() + KIO::encodeFileName(newName)); #ifndef Q_OS_WIN //Confirm hiding file/directory by renaming inline if (!hiddenFilesShown() && newName.startsWith(QLatin1Char('.')) && !oldItem.name().startsWith(QLatin1Char('.'))) { KGuiItem yesGuiItem(KStandardGuiItem::yes()); yesGuiItem.setText(i18nc("@action:button", "Rename and Hide")); const auto code = KMessageBox::questionYesNo(this, oldItem.isFile() ? i18n("Adding a dot to the beginning of this file's name will hide it from view.\n" "Do you still want to rename it?") : i18n("Adding a dot to the beginning of this folder's name will hide it from view.\n" "Do you still want to rename it?"), oldItem.isFile() ? i18n("Hide this File?") : i18n("Hide this Folder?"), yesGuiItem, KStandardGuiItem::cancel(), QStringLiteral("ConfirmHide") ); if (code == KMessageBox::No) { return; } } #endif const bool newNameExistsAlready = (m_model->index(newUrl) >= 0); if (!newNameExistsAlready) { // Only change the data in the model if no item with the new name // is in the model yet. If there is an item with the new name // already, calling KIO::CopyJob will open a dialog // asking for a new name, and KFileItemModel will update the // data when the dir lister signals that the file name has changed. QHash data; data.insert(role, value); m_model->setData(index, data); } KIO::Job * job = KIO::moveAs(oldUrl, newUrl); KJobWidgets::setWindow(job, this); KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Rename, {oldUrl}, newUrl, job); job->uiDelegate()->setAutoErrorHandlingEnabled(true); forceUrlsSelection(newUrl, {newUrl}); if (!newNameExistsAlready) { // Only connect the result signal if there is no item with the new name // in the model yet, see bug 328262. connect(job, &KJob::result, this, &DolphinView::slotRenamingResult); } } } } void DolphinView::loadDirectory(const QUrl& url, bool reload) { if (!url.isValid()) { const QString location(url.toDisplayString(QUrl::PreferLocalFile)); if (location.isEmpty()) { emit errorMessage(i18nc("@info:status", "The location is empty.")); } else { emit errorMessage(i18nc("@info:status", "The location '%1' is invalid.", location)); } return; } if (reload) { m_model->refreshDirectory(url); } else { m_model->loadDirectory(url); } } void DolphinView::applyViewProperties() { const ViewProperties props(viewPropertiesUrl()); applyViewProperties(props); } void DolphinView::applyViewProperties(const ViewProperties& props) { m_view->beginTransaction(); const Mode mode = props.viewMode(); if (m_mode != mode) { const Mode previousMode = m_mode; m_mode = mode; // Changing the mode might result in changing // the zoom level. Remember the old zoom level so // that zoomLevelChanged() can get emitted. const int oldZoomLevel = m_view->zoomLevel(); applyModeToView(); emit modeChanged(m_mode, previousMode); if (m_view->zoomLevel() != oldZoomLevel) { emit zoomLevelChanged(m_view->zoomLevel(), oldZoomLevel); } } const bool hiddenFilesShown = props.hiddenFilesShown(); if (hiddenFilesShown != m_model->showHiddenFiles()) { m_model->setShowHiddenFiles(hiddenFilesShown); emit hiddenFilesShownChanged(hiddenFilesShown); } const bool groupedSorting = props.groupedSorting(); if (groupedSorting != m_model->groupedSorting()) { m_model->setGroupedSorting(groupedSorting); emit groupedSortingChanged(groupedSorting); } const QByteArray sortRole = props.sortRole(); if (sortRole != m_model->sortRole()) { m_model->setSortRole(sortRole); emit sortRoleChanged(sortRole); } const Qt::SortOrder sortOrder = props.sortOrder(); if (sortOrder != m_model->sortOrder()) { m_model->setSortOrder(sortOrder); emit sortOrderChanged(sortOrder); } const bool sortFoldersFirst = props.sortFoldersFirst(); if (sortFoldersFirst != m_model->sortDirectoriesFirst()) { m_model->setSortDirectoriesFirst(sortFoldersFirst); emit sortFoldersFirstChanged(sortFoldersFirst); } const QList visibleRoles = props.visibleRoles(); if (visibleRoles != m_visibleRoles) { const QList previousVisibleRoles = m_visibleRoles; m_visibleRoles = visibleRoles; m_view->setVisibleRoles(visibleRoles); emit visibleRolesChanged(m_visibleRoles, previousVisibleRoles); } const bool previewsShown = props.previewsShown(); if (previewsShown != m_view->previewsShown()) { const int oldZoomLevel = zoomLevel(); m_view->setPreviewsShown(previewsShown); emit previewsShownChanged(previewsShown); // Changing the preview-state might result in a changed zoom-level if (oldZoomLevel != zoomLevel()) { emit zoomLevelChanged(zoomLevel(), oldZoomLevel); } } KItemListView* itemListView = m_container->controller()->view(); if (itemListView->isHeaderVisible()) { KItemListHeader* header = itemListView->header(); const QList headerColumnWidths = props.headerColumnWidths(); const int rolesCount = m_visibleRoles.count(); if (headerColumnWidths.count() == rolesCount) { header->setAutomaticColumnResizing(false); QHash columnWidths; for (int i = 0; i < rolesCount; ++i) { columnWidths.insert(m_visibleRoles[i], headerColumnWidths[i]); } header->setColumnWidths(columnWidths); } else { header->setAutomaticColumnResizing(true); } } m_view->endTransaction(); } void DolphinView::applyModeToView() { switch (m_mode) { case IconsView: m_view->setItemLayout(KFileItemListView::IconsLayout); break; case CompactView: m_view->setItemLayout(KFileItemListView::CompactLayout); break; case DetailsView: m_view->setItemLayout(KFileItemListView::DetailsLayout); break; default: Q_ASSERT(false); break; } } void DolphinView::pasteToUrl(const QUrl& url) { KIO::PasteJob *job = KIO::paste(QApplication::clipboard()->mimeData(), url); KJobWidgets::setWindow(job, this); m_clearSelectionBeforeSelectingNewItems = true; m_markFirstNewlySelectedItemAsCurrent = true; connect(job, &KIO::PasteJob::itemCreated, this, &DolphinView::slotItemCreated); connect(job, &KIO::PasteJob::result, this, &DolphinView::slotPasteJobResult); } QList DolphinView::simplifiedSelectedUrls() const { QList urls; const KFileItemList items = selectedItems(); urls.reserve(items.count()); foreach (const KFileItem& item, items) { urls.append(item.url()); } if (itemsExpandable()) { // TODO: Check if we still need KDirModel for this in KDE 5.0 urls = KDirModel::simplifiedUrlList(urls); } return urls; } QMimeData* DolphinView::selectionMimeData() const { const KItemListSelectionManager* selectionManager = m_container->controller()->selectionManager(); const KItemSet selectedIndexes = selectionManager->selectedItems(); return m_model->createMimeData(selectedIndexes); } void DolphinView::updateWritableState() { const bool wasFolderWritable = m_isFolderWritable; m_isFolderWritable = false; KFileItem item = m_model->rootItem(); if (item.isNull()) { // Try to find out if the URL is writable even if the "root item" is // null, see https://bugs.kde.org/show_bug.cgi?id=330001 item = KFileItem(url()); item.setDelayedMimeTypes(true); } KFileItemListProperties capabilities(KFileItemList() << item); m_isFolderWritable = capabilities.supportsWriting(); if (m_isFolderWritable != wasFolderWritable) { emit writeStateChanged(m_isFolderWritable); } } QUrl DolphinView::viewPropertiesUrl() const { if (m_viewPropertiesContext.isEmpty()) { return m_url; } QUrl url; url.setScheme(m_url.scheme()); url.setPath(m_viewPropertiesContext); return url; } void DolphinView::slotRenameDialogRenamingFinished(const QList& urls) { forceUrlsSelection(urls.first(), urls); } void DolphinView::forceUrlsSelection(const QUrl& current, const QList& selected) { clearSelection(); m_clearSelectionBeforeSelectingNewItems = true; markUrlAsCurrent(current); markUrlsAsSelected(selected); } diff --git a/src/views/dolphinviewactionhandler.cpp b/src/views/dolphinviewactionhandler.cpp index 25b7f82cb..0d9f9f81c 100644 --- a/src/views/dolphinviewactionhandler.cpp +++ b/src/views/dolphinviewactionhandler.cpp @@ -1,698 +1,698 @@ /*************************************************************************** * Copyright (C) 2008 by David Faure * * Copyright (C) 2012 by Peter Penz * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "dolphinviewactionhandler.h" #include "dolphindebug.h" #include "kitemviews/kfileitemmodel.h" #include "settings/viewpropertiesdialog.h" #include "views/zoomlevelinfo.h" #ifdef HAVE_BALOO #include #endif #include #include #include #include #include #include #include #include DolphinViewActionHandler::DolphinViewActionHandler(KActionCollection* collection, QObject* parent) : QObject(parent), m_actionCollection(collection), m_currentView(nullptr), m_sortByActions(), m_visibleRoles() { Q_ASSERT(m_actionCollection); createActions(); } void DolphinViewActionHandler::setCurrentView(DolphinView* view) { Q_ASSERT(view); if (m_currentView) { disconnect(m_currentView, nullptr, this, nullptr); } m_currentView = view; connect(view, &DolphinView::modeChanged, this, &DolphinViewActionHandler::updateViewActions); connect(view, &DolphinView::previewsShownChanged, this, &DolphinViewActionHandler::slotPreviewsShownChanged); connect(view, &DolphinView::sortOrderChanged, this, &DolphinViewActionHandler::slotSortOrderChanged); connect(view, &DolphinView::sortFoldersFirstChanged, this, &DolphinViewActionHandler::slotSortFoldersFirstChanged); connect(view, &DolphinView::visibleRolesChanged, this, &DolphinViewActionHandler::slotVisibleRolesChanged); connect(view, &DolphinView::groupedSortingChanged, this, &DolphinViewActionHandler::slotGroupedSortingChanged); connect(view, &DolphinView::hiddenFilesShownChanged, this, &DolphinViewActionHandler::slotHiddenFilesShownChanged); connect(view, &DolphinView::sortRoleChanged, this, &DolphinViewActionHandler::slotSortRoleChanged); connect(view, &DolphinView::zoomLevelChanged, this, &DolphinViewActionHandler::slotZoomLevelChanged); connect(view, &DolphinView::writeStateChanged, this, &DolphinViewActionHandler::slotWriteStateChanged); } DolphinView* DolphinViewActionHandler::currentView() { return m_currentView; } void DolphinViewActionHandler::createActions() { // This action doesn't appear in the GUI, it's for the shortcut only. // KNewFileMenu takes care of the GUI stuff. QAction* newDirAction = m_actionCollection->addAction(QStringLiteral("create_dir")); newDirAction->setText(i18nc("@action", "Create Folder...")); m_actionCollection->setDefaultShortcut(newDirAction, Qt::Key_F10); newDirAction->setIcon(QIcon::fromTheme(QStringLiteral("folder-new"))); newDirAction->setEnabled(false); // Will be enabled in slotWriteStateChanged(bool) if the current URL is writable connect(newDirAction, &QAction::triggered, this, &DolphinViewActionHandler::createDirectoryTriggered); // File menu auto renameAction = KStandardAction::renameFile(this, &DolphinViewActionHandler::slotRename, m_actionCollection); renameAction->setWhatsThis(xi18nc("@info:whatsthis", "This renames the " "items in your current selection.Renaming multiple items " "at once amounts to their new names differing only in a number.")); auto trashAction = KStandardAction::moveToTrash(this, &DolphinViewActionHandler::slotTrashActivated, m_actionCollection); auto trashShortcuts = trashAction->shortcuts(); if (!trashShortcuts.contains(QKeySequence::Delete)) { trashShortcuts.append(QKeySequence::Delete); m_actionCollection->setDefaultShortcuts(trashAction, trashShortcuts); } trashAction->setWhatsThis(xi18nc("@info:whatsthis", "This moves the " "items in your current selection to the Trash" ".The trash is a temporary storage where " "items can be deleted from if disk space is needed.")); auto deleteAction = KStandardAction::deleteFile(this, &DolphinViewActionHandler::slotDeleteItems, m_actionCollection); auto deleteShortcuts = deleteAction->shortcuts(); if (!deleteShortcuts.contains(Qt::SHIFT | Qt::Key_Delete)) { deleteShortcuts.append(Qt::SHIFT | Qt::Key_Delete); m_actionCollection->setDefaultShortcuts(deleteAction, deleteShortcuts); } deleteAction->setWhatsThis(xi18nc("@info:whatsthis", "This deletes " "the items in your current selection completely. They can " "not be recovered by normal means.")); // This action is useful for being enabled when KStandardAction::MoveToTrash should be // disabled and KStandardAction::DeleteFile is enabled (e.g. non-local files), so that Key_Del // can be used for deleting the file (#76016). It needs to be a separate action // so that the Edit menu isn't affected. QAction* deleteWithTrashShortcut = m_actionCollection->addAction(QStringLiteral("delete_shortcut")); // The descriptive text is just for the shortcuts editor. deleteWithTrashShortcut->setText(i18nc("@action \"Move to Trash\" for non-local files, etc.", "Delete (using shortcut for Trash)")); m_actionCollection->setDefaultShortcuts(deleteWithTrashShortcut, KStandardShortcut::moveToTrash()); deleteWithTrashShortcut->setEnabled(false); connect(deleteWithTrashShortcut, &QAction::triggered, this, &DolphinViewActionHandler::slotDeleteItems); QAction *propertiesAction = m_actionCollection->addAction( QStringLiteral("properties") ); // Well, it's the File menu in dolphinmainwindow and the Edit menu in dolphinpart... :) propertiesAction->setText( i18nc("@action:inmenu File", "Properties") ); propertiesAction->setWhatsThis(xi18nc("@info:whatsthis properties", "This shows a complete list of properties of the currently " "selected items in a new window.If nothing is selected the " "window will be about the currently viewed folder instead." "You can configure advanced options there like managing " "read- and write-permissions.")); propertiesAction->setIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); m_actionCollection->setDefaultShortcuts(propertiesAction, {Qt::ALT + Qt::Key_Return, Qt::ALT + Qt::Key_Enter}); connect(propertiesAction, &QAction::triggered, this, &DolphinViewActionHandler::slotProperties); // View menu KToggleAction* iconsAction = iconsModeAction(); KToggleAction* compactAction = compactModeAction(); KToggleAction* detailsAction = detailsModeAction(); iconsAction->setWhatsThis(xi18nc("@info:whatsthis Icons view mode", "This switches to a view mode that focuses on the folder " "and file icons. This mode makes it easy to distinguish folders " "from files and to detect items with distinctive " "file types. This mode is handy to " "browse through pictures when the Preview" " option is enabled.")); compactAction->setWhatsThis(xi18nc("@info:whatsthis Compact view mode", "This switches to a compact view mode that lists the folders " "and files in columns with the names beside the icons." "This helps to keep the overview in folders with many items.")); detailsAction->setWhatsThis(xi18nc("@info:whatsthis Details view mode", "This switches to a list view mode that focuses on folder " "and file details.Click on a detail in the column " "header to sort the items by it. Click again to sort the other " "way around. To select which details should be displayed click " "the header with the right mouse button.You can " "view the contents of a folder without leaving the current " "location by clicking to the left of it. This way you can view " "the contents of multiple folders in the same list.")); KSelectAction* viewModeActions = m_actionCollection->add(QStringLiteral("view_mode")); viewModeActions->setText(i18nc("@action:intoolbar", "View Mode")); viewModeActions->addAction(iconsAction); viewModeActions->addAction(compactAction); viewModeActions->addAction(detailsAction); viewModeActions->setToolBarMode(KSelectAction::MenuMode); connect(viewModeActions, QOverload::of(&KSelectAction::triggered), this, &DolphinViewActionHandler::slotViewModeActionTriggered); QAction* zoomInAction = KStandardAction::zoomIn(this, &DolphinViewActionHandler::zoomIn, m_actionCollection); zoomInAction->setWhatsThis(i18nc("@info:whatsthis zoom in", "This increases the icon size.")); QAction* zoomResetAction = m_actionCollection->addAction(QStringLiteral("view_zoom_reset")); zoomResetAction->setText(i18nc("@action:inmenu View", "Reset Zoom Level")); zoomResetAction->setToolTip(i18n("Zoom To Default")); zoomResetAction->setWhatsThis(i18nc("@info:whatsthis zoom reset", "This resets the icon size to default.")); zoomResetAction->setIcon(QIcon::fromTheme(QStringLiteral("zoom-original"))); m_actionCollection->setDefaultShortcuts(zoomResetAction, {Qt::CTRL + Qt::Key_0}); connect(zoomResetAction, &QAction::triggered, this, &DolphinViewActionHandler::zoomReset); QAction* zoomOutAction = KStandardAction::zoomOut(this, &DolphinViewActionHandler::zoomOut, m_actionCollection); zoomOutAction->setWhatsThis(i18nc("@info:whatsthis zoom out", "This reduces the icon size.")); KToggleAction* showPreview = m_actionCollection->add(QStringLiteral("show_preview")); showPreview->setText(i18nc("@action:intoolbar", "Show Previews")); showPreview->setToolTip(i18nc("@info", "Show preview of files and folders")); showPreview->setWhatsThis(xi18nc("@info:whatsthis", "When this is " "enabled, the icons are based on the actual file or folder " "contents.For example the icons of images become scaled " "down versions of the images.")); showPreview->setIcon(QIcon::fromTheme(QStringLiteral("view-preview"))); connect(showPreview, &KToggleAction::triggered, this, &DolphinViewActionHandler::togglePreview); KToggleAction* sortFoldersFirst = m_actionCollection->add(QStringLiteral("folders_first")); sortFoldersFirst->setText(i18nc("@action:inmenu Sort", "Folders First")); connect(sortFoldersFirst, &KToggleAction::triggered, this, &DolphinViewActionHandler::toggleSortFoldersFirst); // View -> Sort By QActionGroup* sortByActionGroup = createFileItemRolesActionGroup(QStringLiteral("sort_by_")); KActionMenu* sortByActionMenu = m_actionCollection->add(QStringLiteral("sort")); sortByActionMenu->setIcon(QIcon::fromTheme(QStringLiteral("view-sort"))); sortByActionMenu->setText(i18nc("@action:inmenu View", "Sort By")); sortByActionMenu->setDelayed(false); foreach (QAction* action, sortByActionGroup->actions()) { sortByActionMenu->addAction(action); } sortByActionMenu->addSeparator(); QActionGroup* group = new QActionGroup(sortByActionMenu); group->setExclusive(true); KToggleAction* ascendingAction = m_actionCollection->add(QStringLiteral("ascending")); ascendingAction->setActionGroup(group); connect(ascendingAction, &QAction::triggered, this, [this] { m_currentView->setSortOrder(Qt::AscendingOrder); }); KToggleAction* descendingAction = m_actionCollection->add(QStringLiteral("descending")); descendingAction->setActionGroup(group); connect(descendingAction, &QAction::triggered, this, [this] { m_currentView->setSortOrder(Qt::DescendingOrder); }); sortByActionMenu->addAction(ascendingAction); sortByActionMenu->addAction(descendingAction); sortByActionMenu->addSeparator(); sortByActionMenu->addAction(sortFoldersFirst); // View -> Additional Information QActionGroup* visibleRolesGroup = createFileItemRolesActionGroup(QStringLiteral("show_")); KActionMenu* visibleRolesMenu = m_actionCollection->add(QStringLiteral("additional_info")); visibleRolesMenu->setText(i18nc("@action:inmenu View", "Show Additional Information")); visibleRolesMenu->setIcon(QIcon::fromTheme(QStringLiteral("documentinfo"))); visibleRolesMenu->setDelayed(false); foreach (QAction* action, visibleRolesGroup->actions()) { visibleRolesMenu->addAction(action); } KToggleAction* showInGroups = m_actionCollection->add(QStringLiteral("show_in_groups")); showInGroups->setIcon(QIcon::fromTheme(QStringLiteral("view-group"))); showInGroups->setText(i18nc("@action:inmenu View", "Show in Groups")); showInGroups->setWhatsThis(i18nc("@info:whatsthis", "This groups files and folders by their first letter.")); connect(showInGroups, &KToggleAction::triggered, this, &DolphinViewActionHandler::toggleGroupedSorting); KToggleAction* showHiddenFiles = m_actionCollection->add(QStringLiteral("show_hidden_files")); showHiddenFiles->setText(i18nc("@action:inmenu View", "Show Hidden Files")); showHiddenFiles->setToolTip(i18nc("@info", "Visibility of hidden files and folders")); showHiddenFiles->setWhatsThis(xi18nc("@info:whatsthis", "When " "this is enabled hidden files and folders " "are visible. They will be displayed semi-transparent." "Hidden items only differ from other ones in that their " "name starts with a \".\". In general there is no need for " "users to access them which is why they are hidden.")); m_actionCollection->setDefaultShortcuts(showHiddenFiles, {Qt::ALT + Qt::Key_Period, Qt::CTRL + Qt::Key_H, Qt::Key_F8}); connect(showHiddenFiles, &KToggleAction::triggered, this, &DolphinViewActionHandler::toggleShowHiddenFiles); QAction* adjustViewProps = m_actionCollection->addAction(QStringLiteral("view_properties")); adjustViewProps->setText(i18nc("@action:inmenu View", "Adjust View Properties...")); adjustViewProps->setIcon(QIcon::fromTheme(QStringLiteral("view-choose"))); adjustViewProps->setWhatsThis(i18nc("@info:whatsthis", "This opens a window " "in which all folder view properties can be adjusted.")); connect(adjustViewProps, &QAction::triggered, this, &DolphinViewActionHandler::slotAdjustViewProperties); } QActionGroup* DolphinViewActionHandler::createFileItemRolesActionGroup(const QString& groupPrefix) { const bool isSortGroup = (groupPrefix == QLatin1String("sort_by_")); Q_ASSERT(isSortGroup || groupPrefix == QLatin1String("show_")); QActionGroup* rolesActionGroup = new QActionGroup(m_actionCollection); rolesActionGroup->setExclusive(isSortGroup); if (isSortGroup) { connect(rolesActionGroup, &QActionGroup::triggered, this, &DolphinViewActionHandler::slotSortTriggered); } else { connect(rolesActionGroup, &QActionGroup::triggered, this, &DolphinViewActionHandler::toggleVisibleRole); } QString groupName; KActionMenu* groupMenu = nullptr; QActionGroup* groupMenuGroup = nullptr; bool indexingEnabled = false; #ifdef HAVE_BALOO Baloo::IndexerConfig config; indexingEnabled = config.fileIndexingEnabled(); #endif const QList rolesInfo = KFileItemModel::rolesInformation(); foreach (const KFileItemModel::RoleInfo& info, rolesInfo) { if (!isSortGroup && info.role == "text") { // It should not be possible to hide the "text" role continue; } KToggleAction* action = nullptr; const QString name = groupPrefix + info.role; if (info.group.isEmpty()) { action = m_actionCollection->add(name); action->setActionGroup(rolesActionGroup); } else { if (!groupMenu || info.group != groupName) { groupName = info.group; groupMenu = m_actionCollection->add(groupName); groupMenu->setText(groupName); groupMenu->setActionGroup(rolesActionGroup); groupMenuGroup = new QActionGroup(groupMenu); groupMenuGroup->setExclusive(isSortGroup); if (isSortGroup) { connect(groupMenuGroup, &QActionGroup::triggered, this, &DolphinViewActionHandler::slotSortTriggered); } else { connect(groupMenuGroup, &QActionGroup::triggered, this, &DolphinViewActionHandler::toggleVisibleRole); } } action = new KToggleAction(groupMenu); action->setActionGroup(groupMenuGroup); groupMenu->addAction(action); } action->setText(info.translation); action->setData(info.role); const bool enable = (!info.requiresBaloo && !info.requiresIndexer) || (info.requiresBaloo) || (info.requiresIndexer && indexingEnabled); action->setEnabled(enable); if (isSortGroup) { m_sortByActions.insert(info.role, action); } else { m_visibleRoles.insert(info.role, action); } } return rolesActionGroup; } void DolphinViewActionHandler::slotViewModeActionTriggered(QAction* action) { const DolphinView::Mode mode = action->data().value(); m_currentView->setMode(mode); QAction* viewModeMenu = m_actionCollection->action(QStringLiteral("view_mode")); viewModeMenu->setIcon(action->icon()); } void DolphinViewActionHandler::slotRename() { emit actionBeingHandled(); m_currentView->renameSelectedItems(); } void DolphinViewActionHandler::slotTrashActivated() { emit actionBeingHandled(); m_currentView->trashSelectedItems(); } void DolphinViewActionHandler::slotDeleteItems() { emit actionBeingHandled(); m_currentView->deleteSelectedItems(); } void DolphinViewActionHandler::togglePreview(bool show) { emit actionBeingHandled(); m_currentView->setPreviewsShown(show); } void DolphinViewActionHandler::slotPreviewsShownChanged(bool shown) { - Q_UNUSED(shown); + Q_UNUSED(shown) // It is not enough to update the 'Show Preview' action, also // the 'Zoom In', 'Zoom Out' and 'Zoom Reset' actions must be adapted. updateViewActions(); } QString DolphinViewActionHandler::currentViewModeActionName() const { switch (m_currentView->mode()) { case DolphinView::IconsView: return QStringLiteral("icons"); case DolphinView::DetailsView: return QStringLiteral("details"); case DolphinView::CompactView: return QStringLiteral("compact"); default: Q_ASSERT(false); break; } return QString(); // can't happen } KActionCollection* DolphinViewActionHandler::actionCollection() { return m_actionCollection; } void DolphinViewActionHandler::updateViewActions() { QAction* viewModeAction = m_actionCollection->action(currentViewModeActionName()); if (viewModeAction) { viewModeAction->setChecked(true); QAction* viewModeMenu = m_actionCollection->action(QStringLiteral("view_mode")); viewModeMenu->setIcon(viewModeAction->icon()); } QAction* showPreviewAction = m_actionCollection->action(QStringLiteral("show_preview")); showPreviewAction->setChecked(m_currentView->previewsShown()); slotSortOrderChanged(m_currentView->sortOrder()); slotSortFoldersFirstChanged(m_currentView->sortFoldersFirst()); slotVisibleRolesChanged(m_currentView->visibleRoles(), QList()); slotGroupedSortingChanged(m_currentView->groupedSorting()); slotSortRoleChanged(m_currentView->sortRole()); slotZoomLevelChanged(m_currentView->zoomLevel(), -1); // Updates the "show_hidden_files" action state and icon slotHiddenFilesShownChanged(m_currentView->hiddenFilesShown()); } void DolphinViewActionHandler::zoomIn() { const int level = m_currentView->zoomLevel(); m_currentView->setZoomLevel(level + 1); updateViewActions(); } void DolphinViewActionHandler::zoomOut() { const int level = m_currentView->zoomLevel(); m_currentView->setZoomLevel(level - 1); updateViewActions(); } void DolphinViewActionHandler::zoomReset() { m_currentView->resetZoomLevel(); updateViewActions(); } void DolphinViewActionHandler::toggleSortFoldersFirst() { const bool sortFirst = m_currentView->sortFoldersFirst(); m_currentView->setSortFoldersFirst(!sortFirst); } void DolphinViewActionHandler::slotSortOrderChanged(Qt::SortOrder order) { QAction* descending = m_actionCollection->action(QStringLiteral("descending")); QAction* ascending = m_actionCollection->action(QStringLiteral("ascending")); const bool sortDescending = (order == Qt::DescendingOrder); descending->setChecked(sortDescending); ascending->setChecked(!sortDescending); } void DolphinViewActionHandler::slotSortFoldersFirstChanged(bool foldersFirst) { m_actionCollection->action(QStringLiteral("folders_first"))->setChecked(foldersFirst); } void DolphinViewActionHandler::toggleVisibleRole(QAction* action) { emit actionBeingHandled(); const QByteArray toggledRole = action->data().toByteArray(); QList roles = m_currentView->visibleRoles(); const bool show = action->isChecked(); const int index = roles.indexOf(toggledRole); const bool containsInfo = (index >= 0); if (show && !containsInfo) { roles.append(toggledRole); m_currentView->setVisibleRoles(roles); } else if (!show && containsInfo) { roles.removeAt(index); m_currentView->setVisibleRoles(roles); Q_ASSERT(roles.indexOf(toggledRole) < 0); } } void DolphinViewActionHandler::slotVisibleRolesChanged(const QList& current, const QList& previous) { - Q_UNUSED(previous); + Q_UNUSED(previous) const QSet checkedRoles = current.toSet(); QHashIterator it(m_visibleRoles); while (it.hasNext()) { it.next(); const QByteArray& role = it.key(); KToggleAction* action = it.value(); action->setChecked(checkedRoles.contains(role)); } } void DolphinViewActionHandler::toggleGroupedSorting(bool grouped) { m_currentView->setGroupedSorting(grouped); } void DolphinViewActionHandler::slotGroupedSortingChanged(bool groupedSorting) { QAction* showInGroupsAction = m_actionCollection->action(QStringLiteral("show_in_groups")); showInGroupsAction->setChecked(groupedSorting); } void DolphinViewActionHandler::toggleShowHiddenFiles(bool show) { emit actionBeingHandled(); m_currentView->setHiddenFilesShown(show); } void DolphinViewActionHandler::slotHiddenFilesShownChanged(bool shown) { QAction* showHiddenFilesAction = m_actionCollection->action(QStringLiteral("show_hidden_files")); showHiddenFilesAction->setChecked(shown); // #374508: don't overwrite custom icons. const QString iconName = showHiddenFilesAction->icon().name(); if (!iconName.isEmpty() && iconName != QLatin1String("view-visible") && iconName != QLatin1String("view-hidden")) { return; } showHiddenFilesAction->setIcon(QIcon::fromTheme(shown ? QStringLiteral("view-visible") : QStringLiteral("view-hidden"))); } void DolphinViewActionHandler::slotWriteStateChanged(bool isFolderWritable) { m_actionCollection->action(QStringLiteral("create_dir"))->setEnabled(isFolderWritable && KProtocolManager::supportsMakeDir(currentView()->url())); } KToggleAction* DolphinViewActionHandler::iconsModeAction() { KToggleAction* iconsView = m_actionCollection->add(QStringLiteral("icons")); iconsView->setText(i18nc("@action:inmenu View Mode", "Icons")); iconsView->setToolTip(i18nc("@info", "Icons view mode")); m_actionCollection->setDefaultShortcut(iconsView, Qt::CTRL + Qt::Key_1); iconsView->setIcon(QIcon::fromTheme(QStringLiteral("view-list-icons"))); iconsView->setData(QVariant::fromValue(DolphinView::IconsView)); return iconsView; } KToggleAction* DolphinViewActionHandler::compactModeAction() { KToggleAction* iconsView = m_actionCollection->add(QStringLiteral("compact")); iconsView->setText(i18nc("@action:inmenu View Mode", "Compact")); iconsView->setToolTip(i18nc("@info", "Compact view mode")); m_actionCollection->setDefaultShortcut(iconsView, Qt::CTRL + Qt::Key_2); iconsView->setIcon(QIcon::fromTheme(QStringLiteral("view-list-details"))); // TODO: discuss with Oxygen-team the wrong (?) name iconsView->setData(QVariant::fromValue(DolphinView::CompactView)); return iconsView; } KToggleAction* DolphinViewActionHandler::detailsModeAction() { KToggleAction* detailsView = m_actionCollection->add(QStringLiteral("details")); detailsView->setText(i18nc("@action:inmenu View Mode", "Details")); detailsView->setToolTip(i18nc("@info", "Details view mode")); m_actionCollection->setDefaultShortcut(detailsView, Qt::CTRL + Qt::Key_3); detailsView->setIcon(QIcon::fromTheme(QStringLiteral("view-list-tree"))); detailsView->setData(QVariant::fromValue(DolphinView::DetailsView)); return detailsView; } void DolphinViewActionHandler::slotSortRoleChanged(const QByteArray& role) { KToggleAction* action = m_sortByActions.value(role); if (action) { action->setChecked(true); if (!action->icon().isNull()) { QAction* sortByMenu = m_actionCollection->action(QStringLiteral("sort")); sortByMenu->setIcon(action->icon()); } } QAction* descending = m_actionCollection->action(QStringLiteral("descending")); QAction* ascending = m_actionCollection->action(QStringLiteral("ascending")); if (role == "text" || role == "type" || role == "tags" || role == "comment") { descending->setText(i18nc("Sort descending", "Z-A")); ascending->setText(i18nc("Sort ascending", "A-Z")); } else if (role == "size") { descending->setText(i18nc("Sort descending", "Largest first")); ascending->setText(i18nc("Sort ascending", "Smallest first")); } else if (role == "modificationtime" || role == "creationtime" || role == "accesstime") { descending->setText(i18nc("Sort descending", "Newest first")); ascending->setText(i18nc("Sort ascending", "Oldest first")); } else if (role == "rating") { descending->setText(i18nc("Sort descending", "Highest first")); ascending->setText(i18nc("Sort ascending", "Lowest first")); } else { descending->setText(i18nc("Sort descending", "Descending")); ascending->setText(i18nc("Sort ascending", "Ascending")); } slotSortOrderChanged(m_currentView->sortOrder()); } void DolphinViewActionHandler::slotZoomLevelChanged(int current, int previous) { - Q_UNUSED(previous); + Q_UNUSED(previous) QAction* zoomInAction = m_actionCollection->action(KStandardAction::name(KStandardAction::ZoomIn)); if (zoomInAction) { zoomInAction->setEnabled(current < ZoomLevelInfo::maximumLevel()); } QAction* zoomOutAction = m_actionCollection->action(KStandardAction::name(KStandardAction::ZoomOut)); if (zoomOutAction) { zoomOutAction->setEnabled(current > ZoomLevelInfo::minimumLevel()); } } void DolphinViewActionHandler::slotSortTriggered(QAction* action) { // The radiobuttons of the "Sort By"-menu are split between the main-menu // and several sub-menus. Because of this they don't have a common // action-group that assures an exclusive toggle-state between the main-menu // actions and the sub-menu-actions. If an action gets checked, it must // be assured that all other actions get unchecked, except the ascending/ // descending actions for (QAction *groupAction : qAsConst(m_sortByActions)) { KActionMenu* actionMenu = qobject_cast(groupAction); if (actionMenu) { foreach (QAction* subAction, actionMenu->menu()->actions()) { subAction->setChecked(false); } } else if (groupAction->actionGroup()) { groupAction->setChecked(false); } } action->setChecked(true); // Apply the activated sort-role to the view const QByteArray role = action->data().toByteArray(); m_currentView->setSortRole(role); } void DolphinViewActionHandler::slotAdjustViewProperties() { emit actionBeingHandled(); QPointer dialog = new ViewPropertiesDialog(m_currentView); dialog->exec(); delete dialog; } void DolphinViewActionHandler::slotProperties() { KPropertiesDialog* dialog = nullptr; const KFileItemList list = m_currentView->selectedItems(); if (list.isEmpty()) { const QUrl url = m_currentView->url(); dialog = new KPropertiesDialog(url, m_currentView); } else { dialog = new KPropertiesDialog(list, m_currentView); } dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->show(); dialog->raise(); dialog->activateWindow(); }