diff options
Diffstat (limited to 'src')
46 files changed, 2415 insertions, 1556 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 41efa3589..6856991d5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,8 +1,17 @@ -macro_optional_find_package(Soprano) macro_optional_find_package(NepomukCore) +set_package_properties(NepomukCore PROPERTIES DESCRIPTION "Nepomuk Core libraries" + URL "http://www.kde.org" + TYPE OPTIONAL + PURPOSE "For adding desktop-wide tagging support to dolphin" + ) + macro_optional_find_package(NepomukWidgets) -macro_log_feature(NepomukCore_FOUND "Nepomuk Core" "Nepomuk Core functionality" "http://www.kde.org" FALSE "" "For fetching additional file metadata in dolphin") -macro_log_feature(NepomukWidgets_FOUND "Nepomuk Widgets" "Nepomuk Widgets" "http://www.kde.org" FALSE "" "For adding desktop-wide tagging support to dolphin") +set_package_properties(NepomukWidgets PROPERTIES DESCRIPTION "Nepomuk Widgets" + URL "http://www.kde.org" + TYPE OPTIONAL + PURPOSE "For adding desktop-wide tagging support to dolphin" + ) + if(NepomukCore_FOUND AND NepomukWidgets_FOUND) set(HAVE_NEPOMUK TRUE) endif() @@ -15,7 +24,13 @@ configure_file(config-X11.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-X11.h ) include_directories( ${KACTIVITIES_INCLUDE_DIRS} ) if(HAVE_NEPOMUK) - # Yes, Soprano includes is what we need here + find_package(Soprano 2.7.56) + set_package_properties(Soprano PROPERTIES DESCRIPTION "Qt-based RDF storage and parsing solution" + URL "http://soprano.sourceforge.net" + TYPE REQUIRED + PURPOSE "Required for everything (storage and general data management)" + ) + include_directories( ${SOPRANO_INCLUDE_DIR} ${NEPOMUK_CORE_INCLUDE_DIR} ${NEPOMUK_WIDGETS_INCLUDE_DIR} ) endif() @@ -45,7 +60,6 @@ set(dolphinprivate_LIB_SRCS kitemviews/kstandarditemmodel.cpp kitemviews/private/kfileitemclipboard.cpp kitemviews/private/kfileitemmodeldirlister.cpp - kitemviews/private/kfileitemmodelsortalgorithm.cpp kitemviews/private/kfileitemmodelfilter.cpp kitemviews/private/kitemlistheaderwidget.cpp kitemviews/private/kitemlistkeyboardsearchmanager.cpp @@ -77,6 +91,7 @@ set(dolphinprivate_LIB_SRCS views/viewmodecontroller.cpp views/viewproperties.cpp views/zoomlevelinfo.cpp + dolphinremoveaction.cpp ) if(HAVE_NEPOMUK) diff --git a/src/dolphin.desktop b/src/dolphin.desktop index 13d66d657..9364ebbdf 100755 --- a/src/dolphin.desktop +++ b/src/dolphin.desktop @@ -88,7 +88,7 @@ Name[wa]=Dolphin Name[x-test]=xxDolphinxx Name[zh_CN]=Dolphin Name[zh_TW]=Dolphin -Exec=dolphin %i -caption "%c" %u +Exec=dolphin %i -caption %c %u Icon=system-file-manager Type=Application X-DocPath=dolphin/index.html @@ -182,7 +182,7 @@ GenericName[vi]=Bộ quản lý tập tin GenericName[wa]=Manaedjeu di fitchîs GenericName[x-test]=xxFile Managerxx GenericName[zh_CN]=文件管理器 -GenericName[zh_TW]=檔案管理程式 +GenericName[zh_TW]=檔案管理員 Terminal=false MimeType=inode/directory; InitialPreference=10 diff --git a/src/dolphincontextmenu.cpp b/src/dolphincontextmenu.cpp index bb26c7aae..f66847334 100644 --- a/src/dolphincontextmenu.cpp +++ b/src/dolphincontextmenu.cpp @@ -24,6 +24,7 @@ #include "dolphinnewfilemenu.h" #include "dolphinviewcontainer.h" #include "dolphin_generalsettings.h" +#include "dolphinremoveaction.h" #include <KActionCollection> #include <KDesktopFile> @@ -38,7 +39,6 @@ #include <KMenuBar> #include <KMessageBox> #include <KMimeTypeTrader> -#include <KModifierKeyInfo> #include <KNewFileMenu> #include <konqmimedata.h> #include <konq_operations.h> @@ -59,13 +59,11 @@ #include "views/dolphinview.h" #include "views/viewmodecontroller.h" -K_GLOBAL_STATIC(KModifierKeyInfo, m_keyInfo) - DolphinContextMenu::DolphinContextMenu(DolphinMainWindow* parent, const QPoint& pos, const KFileItem& fileInfo, const KUrl& baseUrl) : - QObject(parent), + KMenu(parent), m_pos(pos), m_mainWindow(parent), m_fileInfo(fileInfo), @@ -76,37 +74,20 @@ DolphinContextMenu::DolphinContextMenu(DolphinMainWindow* parent, m_context(NoContext), m_copyToMenu(parent), m_customActions(), - m_popup(0), - m_command(None), - m_shiftPressed(false), - m_removeAction(0) + m_command(None) { // The context menu either accesses the URLs of the selected items // or the items itself. To increase the performance both lists are cached. const DolphinView* view = m_mainWindow->activeViewContainer()->view(); m_selectedItems = view->selectedItems(); - if (m_keyInfo) { - if (m_keyInfo->isKeyPressed(Qt::Key_Shift) || m_keyInfo->isKeyLatched(Qt::Key_Shift)) { - m_shiftPressed = true; - } - connect(m_keyInfo, SIGNAL(keyPressed(Qt::Key,bool)), - this, SLOT(slotKeyModifierPressed(Qt::Key,bool))); - } - - m_removeAction = new QAction(this); - connect(m_removeAction, SIGNAL(triggered()), this, SLOT(slotRemoveActionTriggered())); - - m_popup = new KMenu(m_mainWindow); + m_removeAction = new DolphinRemoveAction(this, m_mainWindow->actionCollection()); } DolphinContextMenu::~DolphinContextMenu() { delete m_selectedItemsProperties; m_selectedItemsProperties = 0; - - delete m_popup; - m_popup = 0; } void DolphinContextMenu::setCustomActions(const QList<QAction*>& actions) @@ -143,47 +124,39 @@ DolphinContextMenu::Command DolphinContextMenu::open() return m_command; } -void DolphinContextMenu::initializeModifierKeyInfo() -{ - // Access m_keyInfo, so that it gets instantiated by - // K_GLOBAL_STATIC - KModifierKeyInfo* keyInfo = m_keyInfo; - Q_UNUSED(keyInfo); -} - -void DolphinContextMenu::slotKeyModifierPressed(Qt::Key key, bool pressed) +void DolphinContextMenu::keyPressEvent(QKeyEvent *ev) { - m_shiftPressed = (key == Qt::Key_Shift) && pressed; - updateRemoveAction(); + if (ev->key() == Qt::Key_Shift) { + m_removeAction->update(); + } + KMenu::keyPressEvent(ev); } -void DolphinContextMenu::slotRemoveActionTriggered() +void DolphinContextMenu::keyReleaseEvent(QKeyEvent *ev) { - const KActionCollection* collection = m_mainWindow->actionCollection(); - if (moveToTrash()) { - collection->action("move_to_trash")->trigger(); - } else { - collection->action("delete")->trigger(); + if (ev->key() == Qt::Key_Shift) { + m_removeAction->update(); } + KMenu::keyReleaseEvent(ev); } void DolphinContextMenu::openTrashContextMenu() { Q_ASSERT(m_context & TrashContext); - QAction* emptyTrashAction = new QAction(KIcon("trash-empty"), i18nc("@action:inmenu", "Empty Trash"), m_popup); + QAction* emptyTrashAction = new QAction(KIcon("trash-empty"), i18nc("@action:inmenu", "Empty Trash"), this); KConfig trashConfig("trashrc", KConfig::SimpleConfig); emptyTrashAction->setEnabled(!trashConfig.group("Status").readEntry("Empty", true)); - m_popup->addAction(emptyTrashAction); + addAction(emptyTrashAction); addCustomActions(); QAction* propertiesAction = m_mainWindow->actionCollection()->action("properties"); - m_popup->addAction(propertiesAction); + addAction(propertiesAction); addShowMenuBarAction(); - if (m_popup->exec(m_pos) == emptyTrashAction) { + if (exec(m_pos) == emptyTrashAction) { KonqOperations::emptyTrash(m_mainWindow); } } @@ -194,15 +167,15 @@ void DolphinContextMenu::openTrashItemContextMenu() Q_ASSERT(m_context & ItemContext); QAction* restoreAction = new QAction(i18nc("@action:inmenu", "Restore"), m_mainWindow); - m_popup->addAction(restoreAction); + addAction(restoreAction); QAction* deleteAction = m_mainWindow->actionCollection()->action("delete"); - m_popup->addAction(deleteAction); + addAction(deleteAction); QAction* propertiesAction = m_mainWindow->actionCollection()->action("properties"); - m_popup->addAction(propertiesAction); + addAction(propertiesAction); - if (m_popup->exec(m_pos) == restoreAction) { + if (exec(m_pos) == restoreAction) { KUrl::List selectedUrls; foreach (const KFileItem &item, m_selectedItems) { selectedUrls.append(item.url()); @@ -234,41 +207,62 @@ void DolphinContextMenu::openItemContextMenu() KMenu* menu = newFileMenu->menu(); menu->setTitle(i18nc("@title:menu Create new folder, file, link, etc.", "Create New")); menu->setIcon(KIcon("document-new")); - m_popup->addMenu(menu); - m_popup->addSeparator(); + addMenu(menu); + addSeparator(); // insert 'Open in new window' and 'Open in new tab' entries - m_popup->addAction(m_mainWindow->actionCollection()->action("open_in_new_window")); - m_popup->addAction(m_mainWindow->actionCollection()->action("open_in_new_tab")); + addAction(m_mainWindow->actionCollection()->action("open_in_new_window")); + addAction(m_mainWindow->actionCollection()->action("open_in_new_tab")); // insert 'Add to Places' entry if (!placeExists(m_fileInfo.url())) { - addToPlacesAction = m_popup->addAction(KIcon("bookmark-new"), + addToPlacesAction = addAction(KIcon("bookmark-new"), i18nc("@action:inmenu Add selected folder to places", "Add to Places")); } - m_popup->addSeparator(); + addSeparator(); } else if (m_baseUrl.protocol().contains("search")) { openParentInNewWindowAction = new QAction(KIcon("window-new"), i18nc("@action:inmenu", "Open Path in New Window"), this); - m_popup->addAction(openParentInNewWindowAction); + addAction(openParentInNewWindowAction); openParentInNewTabAction = new QAction(KIcon("tab-new"), i18nc("@action:inmenu", "Open Path in New Tab"), this); - m_popup->addAction(openParentInNewTabAction); + addAction(openParentInNewTabAction); + + addSeparator(); + } else if (!DolphinView::openItemAsFolderUrl(m_fileInfo).isEmpty()) { + // insert 'Open in new window' and 'Open in new tab' entries + addAction(m_mainWindow->actionCollection()->action("open_in_new_window")); + addAction(m_mainWindow->actionCollection()->action("open_in_new_tab")); - m_popup->addSeparator(); + addSeparator(); + } + } else { + bool selectionHasOnlyDirs = true; + foreach (const KFileItem& item, m_selectedItems) { + const KUrl& url = DolphinView::openItemAsFolderUrl(item); + if (url.isEmpty()) { + selectionHasOnlyDirs = false; + break; + } + } + + if (selectionHasOnlyDirs) { + // insert 'Open in new tab' entry + addAction(m_mainWindow->actionCollection()->action("open_in_new_tabs")); + addSeparator(); } } insertDefaultItemActions(); - m_popup->addSeparator(); + addSeparator(); KFileItemActions fileItemActions; fileItemActions.setItemListProperties(selectedItemsProperties()); @@ -282,14 +276,14 @@ void DolphinContextMenu::openItemContextMenu() if (GeneralSettings::showCopyMoveMenu()) { m_copyToMenu.setItems(m_selectedItems); m_copyToMenu.setReadOnly(!selectedItemsProperties().supportsWriting()); - m_copyToMenu.addActionsTo(m_popup); + m_copyToMenu.addActionsTo(this); } // insert 'Properties...' entry QAction* propertiesAction = m_mainWindow->actionCollection()->action("properties"); - m_popup->addAction(propertiesAction); + addAction(propertiesAction); - QAction* activatedAction = m_popup->exec(m_pos); + QAction* activatedAction = exec(m_pos); if (activatedAction) { if (activatedAction == addToPlacesAction) { const KUrl selectedUrl(m_fileInfo.url()); @@ -315,26 +309,26 @@ void DolphinContextMenu::openViewportContextMenu() newFileMenu->setViewShowsHiddenFiles(view->hiddenFilesShown()); newFileMenu->checkUpToDate(); newFileMenu->setPopupFiles(m_baseUrl); - m_popup->addMenu(newFileMenu->menu()); - m_popup->addSeparator(); + addMenu(newFileMenu->menu()); + addSeparator(); // Insert 'New Window' and 'New Tab' entries. Don't use "open_in_new_window" and // "open_in_new_tab" here, as the current selection should get ignored. - m_popup->addAction(m_mainWindow->actionCollection()->action("new_window")); - m_popup->addAction(m_mainWindow->actionCollection()->action("new_tab")); + addAction(m_mainWindow->actionCollection()->action("new_window")); + addAction(m_mainWindow->actionCollection()->action("new_tab")); // Insert 'Add to Places' entry if exactly one item is selected QAction* addToPlacesAction = 0; if (!placeExists(m_mainWindow->activeViewContainer()->url())) { - addToPlacesAction = m_popup->addAction(KIcon("bookmark-new"), + addToPlacesAction = addAction(KIcon("bookmark-new"), i18nc("@action:inmenu Add current folder to places", "Add to Places")); } - m_popup->addSeparator(); + addSeparator(); QAction* pasteAction = createPasteAction(); - m_popup->addAction(pasteAction); - m_popup->addSeparator(); + addAction(pasteAction); + addSeparator(); // Insert service actions const KFileItemListProperties baseUrlProperties(KFileItemList() << baseFileItem()); @@ -349,11 +343,11 @@ void DolphinContextMenu::openViewportContextMenu() addCustomActions(); QAction* propertiesAction = m_mainWindow->actionCollection()->action("properties"); - m_popup->addAction(propertiesAction); + addAction(propertiesAction); addShowMenuBarAction(); - QAction* action = m_popup->exec(m_pos); + QAction* action = exec(m_pos); if (addToPlacesAction && (action == addToPlacesAction)) { const DolphinViewContainer* container = m_mainWindow->activeViewContainer(); if (container->url().isValid()) { @@ -370,23 +364,23 @@ void DolphinContextMenu::insertDefaultItemActions() const KActionCollection* collection = m_mainWindow->actionCollection(); // Insert 'Cut', 'Copy' and 'Paste' - m_popup->addAction(collection->action(KStandardAction::name(KStandardAction::Cut))); - m_popup->addAction(collection->action(KStandardAction::name(KStandardAction::Copy))); - m_popup->addAction(createPasteAction()); + addAction(collection->action(KStandardAction::name(KStandardAction::Cut))); + addAction(collection->action(KStandardAction::name(KStandardAction::Copy))); + addAction(createPasteAction()); - m_popup->addSeparator(); + addSeparator(); // Insert 'Rename' QAction* renameAction = collection->action("rename"); - m_popup->addAction(renameAction); + addAction(renameAction); // Insert 'Move to Trash' and/or 'Delete' if (KGlobal::config()->group("KDE").readEntry("ShowDeleteCommand", false)) { - m_popup->addAction(collection->action("move_to_trash")); - m_popup->addAction(collection->action("delete")); + addAction(collection->action("move_to_trash")); + addAction(collection->action("delete")); } else { - m_popup->addAction(m_removeAction); - updateRemoveAction(); + addAction(m_removeAction); + m_removeAction->update(); } } @@ -395,8 +389,8 @@ void DolphinContextMenu::addShowMenuBarAction() const KActionCollection* ac = m_mainWindow->actionCollection(); QAction* showMenuBar = ac->action(KStandardAction::name(KStandardAction::ShowMenubar)); if (!m_mainWindow->menuBar()->isVisible() && !m_mainWindow->toolBar()->isVisible()) { - m_popup->addSeparator(); - m_popup->addAction(showMenuBar); + addSeparator(); + addAction(showMenuBar); } } @@ -453,10 +447,10 @@ void DolphinContextMenu::addServiceActions(KFileItemActions& fileItemActions) fileItemActions.setParentWidget(m_mainWindow); // insert 'Open With...' action or sub menu - fileItemActions.addOpenWithActionsTo(m_popup, "DesktopEntryName != 'dolphin'"); + fileItemActions.addOpenWithActionsTo(this, "DesktopEntryName != 'dolphin'"); // insert 'Actions' sub menu - fileItemActions.addServiceActionsTo(m_popup); + fileItemActions.addServiceActionsTo(this); } void DolphinContextMenu::addFileItemPluginActions() @@ -482,22 +476,27 @@ void DolphinContextMenu::addFileItemPluginActions() const KConfigGroup showGroup = config.group("Show"); foreach (const KSharedPtr<KService>& service, pluginServices) { - if (!showGroup.readEntry(service->desktopEntryName(), true)) { - // The plugin has been disabled - continue; - } - // Old API (kdelibs-4.6.0 only) KFileItemActionPlugin* plugin = service->createInstance<KFileItemActionPlugin>(); if (plugin) { - plugin->setParent(m_popup); - m_popup->addActions(plugin->actions(props, m_mainWindow)); + if (!showGroup.readEntry(service->desktopEntryName(), true)) { + // The plugin has been disabled + continue; + } + + plugin->setParent(this); + addActions(plugin->actions(props, m_mainWindow)); } // New API (kdelibs >= 4.6.1) KAbstractFileItemActionPlugin* abstractPlugin = service->createInstance<KAbstractFileItemActionPlugin>(); if (abstractPlugin) { - abstractPlugin->setParent(m_popup); - m_popup->addActions(abstractPlugin->actions(props, m_mainWindow)); + if (!showGroup.readEntry(service->desktopEntryName(), abstractPlugin->enabledByDefault())) { + // The plugin has been disabled + continue; + } + + abstractPlugin->setParent(this); + addActions(abstractPlugin->actions(props, m_mainWindow)); } } } @@ -508,41 +507,17 @@ void DolphinContextMenu::addVersionControlPluginActions() const QList<QAction*> versionControlActions = view->versionControlActions(m_selectedItems); if (!versionControlActions.isEmpty()) { foreach (QAction* action, versionControlActions) { - m_popup->addAction(action); + addAction(action); } - m_popup->addSeparator(); + addSeparator(); } } void DolphinContextMenu::addCustomActions() { foreach (QAction* action, m_customActions) { - m_popup->addAction(action); + addAction(action); } } -void DolphinContextMenu::updateRemoveAction() -{ - const KActionCollection* collection = m_mainWindow->actionCollection(); - - // Using m_removeAction->setText(action->text()) does not apply the &-shortcut. - // This is only done until the original action has been shown at least once. To - // bypass this issue, the text and &-shortcut is applied manually. - const QAction* action = 0; - if (moveToTrash()) { - action = collection->action("move_to_trash"); - m_removeAction->setText(i18nc("@action:inmenu", "&Move to Trash")); - } else { - action = collection->action("delete"); - m_removeAction->setText(i18nc("@action:inmenu", "&Delete")); - } - m_removeAction->setIcon(action->icon()); - m_removeAction->setShortcuts(action->shortcuts()); -} - -bool DolphinContextMenu::moveToTrash() const -{ - return !m_shiftPressed; -} - #include "dolphincontextmenu.moc" diff --git a/src/dolphincontextmenu.h b/src/dolphincontextmenu.h index 3d0005d30..160f08804 100644 --- a/src/dolphincontextmenu.h +++ b/src/dolphincontextmenu.h @@ -24,6 +24,7 @@ #include <KService> #include <KUrl> #include <konq_copytomenu.h> +#include <KMenu> #include <QObject> @@ -31,12 +32,11 @@ #include <QScopedPointer> -class KMenu; -class KFileItem; class QAction; class DolphinMainWindow; class KFileItemActions; class KFileItemListProperties; +class DolphinRemoveAction; /** * @brief Represents the context menu which appears when doing a right @@ -50,7 +50,7 @@ class KFileItemListProperties; * - 'Actions': Contains all actions which can be applied to the * given item. */ -class DolphinContextMenu : public QObject +class DolphinContextMenu : public KMenu { Q_OBJECT @@ -91,30 +91,9 @@ public: */ Command open(); - /** - * TODO: This method is a workaround for a X11-issue in combination - * with KModifierKeyInfo: When constructing KModifierKeyInfo in the - * constructor of the context menu, the user interface might freeze. - * To bypass this, the KModifierKeyInfo is constructed in DolphinMainWindow - * directly after starting the application. Remove this method, if - * the X11-issue got fixed (contact the maintainer of KModifierKeyInfo for - * more details). - */ - static void initializeModifierKeyInfo(); - -private slots: - /** - * Is invoked if a key modifier has been pressed and updates the context - * menu to show the 'Delete' action instead of the 'Move To Trash' action - * if the shift-key has been pressed. - */ - void slotKeyModifierPressed(Qt::Key key, bool pressed); - - /** - * Triggers the 'Delete'-action if the shift-key has been pressed, otherwise - * the 'Move to Trash'-action gets triggered. - */ - void slotRemoveActionTriggered(); +protected: + virtual void keyPressEvent(QKeyEvent *ev); + virtual void keyReleaseEvent(QKeyEvent *ev); private: void openTrashContextMenu(); @@ -163,20 +142,6 @@ private: */ void addCustomActions(); - /** - * Updates m_removeAction to represent the 'Delete'-action if the shift-key - * has been pressed or the selection is not local. Otherwise it represents - * the 'Move to Trash'-action. - */ - void updateRemoveAction(); - - /** - * @return True if a moving to the trash should be done instead of - * deleting the selected items. - * @see updateRemoveAction(), slotRemoveActionTriggered() - */ - bool moveToTrash() const; - private: struct Entry { @@ -209,12 +174,10 @@ private: int m_context; KonqCopyToMenu m_copyToMenu; QList<QAction*> m_customActions; - KMenu* m_popup; Command m_command; - bool m_shiftPressed; - QAction* m_removeAction; // Action that represents either 'Move To Trash' or 'Delete' + DolphinRemoveAction* m_removeAction; // Action that represents either 'Move To Trash' or 'Delete' }; #endif diff --git a/src/dolphinmainwindow.cpp b/src/dolphinmainwindow.cpp index 9454c8c42..73001bf54 100644 --- a/src/dolphinmainwindow.cpp +++ b/src/dolphinmainwindow.cpp @@ -120,11 +120,6 @@ DolphinMainWindow::DolphinMainWindow() : m_updateToolBarTimer(0), m_lastHandleUrlStatJob(0) { - // Workaround for a X11-issue in combination with KModifierInfo - // (see DolphinContextMenu::initializeModifierKeyInfo() for - // more information): - DolphinContextMenu::initializeModifierKeyInfo(); - setObjectName("Dolphin#"); m_viewTab.append(ViewTab()); @@ -525,11 +520,16 @@ void DolphinMainWindow::activatePrevTab() void DolphinMainWindow::openInNewTab() { - const KFileItemList list = m_activeViewContainer->view()->selectedItems(); + const KFileItemList& list = m_activeViewContainer->view()->selectedItems(); if (list.isEmpty()) { openNewTab(m_activeViewContainer->url()); - } else if ((list.count() == 1) && list[0].isDir()) { - openNewTab(list[0].url()); + } else { + foreach (const KFileItem& item, list) { + const KUrl& url = DolphinView::openItemAsFolderUrl(item); + if (!url.isEmpty()) { + openNewTab(url); + } + } } } @@ -540,8 +540,9 @@ void DolphinMainWindow::openInNewWindow() const KFileItemList list = m_activeViewContainer->view()->selectedItems(); if (list.isEmpty()) { newWindowUrl = m_activeViewContainer->url(); - } else if ((list.count() == 1) && list[0].isDir()) { - newWindowUrl = list[0].url(); + } else if (list.count() == 1) { + const KFileItem& item = list.first(); + newWindowUrl = DolphinView::openItemAsFolderUrl(item); } if (!newWindowUrl.isEmpty()) { @@ -1276,7 +1277,8 @@ void DolphinMainWindow::tabDropEvent(int tab, QDropEvent* event) const ViewTab& viewTab = m_viewTab[tab]; const DolphinView* view = viewTab.isPrimaryViewActive ? viewTab.primaryView->view() : viewTab.secondaryView->view(); - const QString error = DragAndDropHelper::dropUrls(view->rootItem(), view->url(), event); + QString error; + DragAndDropHelper::dropUrls(view->rootItem(), view->url(), event, error); if (!error.isEmpty()) { activeViewContainer()->showMessage(error, DolphinViewContainer::Error); } @@ -1649,6 +1651,11 @@ void DolphinMainWindow::setupActions() openInNewTab->setIcon(KIcon("tab-new")); connect(openInNewTab, SIGNAL(triggered()), this, SLOT(openInNewTab())); + KAction* openInNewTabs = actionCollection()->addAction("open_in_new_tabs"); + openInNewTabs->setText(i18nc("@action:inmenu", "Open in New Tabs")); + openInNewTabs->setIcon(KIcon("tab-new")); + connect(openInNewTabs, SIGNAL(triggered()), this, SLOT(openInNewTab())); + KAction* openInNewWindow = actionCollection()->addAction("open_in_new_window"); openInNewWindow->setText(i18nc("@action:inmenu", "Open in New Window")); openInNewWindow->setIcon(KIcon("window-new")); diff --git a/src/dolphinpart.cpp b/src/dolphinpart.cpp index 627ba79c5..642b15013 100644 --- a/src/dolphinpart.cpp +++ b/src/dolphinpart.cpp @@ -18,6 +18,7 @@ */ #include "dolphinpart.h" +#include "dolphinremoveaction.h" #include <KFileItemListProperties> #include <konq_operations.h> @@ -64,6 +65,7 @@ K_EXPORT_PLUGIN(DolphinPartFactory("dolphinpart", "dolphin")) DolphinPart::DolphinPart(QWidget* parentWidget, QObject* parent, const QVariantList& args) : KParts::ReadOnlyPart(parent) ,m_openTerminalAction(0) + ,m_removeAction(0) { Q_UNUSED(args) setComponentData(DolphinPartFactory::componentData(), false); @@ -145,6 +147,10 @@ DolphinPart::DolphinPart(QWidget* parentWidget, QObject* parent, const QVariantL m_actionHandler->updateViewActions(); slotSelectionChanged(KFileItemList()); // initially disable selection-dependent actions + // Listen to events from the app so we can update the remove key by + // checking for a Shift key press. + qApp->installEventFilter(this); + // TODO there was a "always open a new window" (when clicking on a directory) setting in konqueror // (sort of spacial navigation) @@ -447,10 +453,18 @@ void DolphinPart::slotOpenContextMenu(const QPoint& pos, } } - if (addTrash) + if (!addTrash || !addDel) { + if (!m_removeAction) { + m_removeAction = new DolphinRemoveAction(this, actionCollection()); + } + editActions.append(m_removeAction); + m_removeAction->update(); + } else { + delete m_removeAction; + m_removeAction = 0; editActions.append(actionCollection()->action("move_to_trash")); - if (addDel) editActions.append(actionCollection()->action("delete")); + } // Normally KonqPopupMenu only shows the "Create new" submenu in the current view // since otherwise the created file would not be visible. @@ -593,6 +607,23 @@ void DolphinPart::setFilesToSelect(const KUrl::List& files) m_view->markUrlAsCurrent(files.at(0)); } +bool DolphinPart::eventFilter(QObject* obj, QEvent* event) +{ + const int type = event->type(); + + if ((type == QEvent::KeyPress || type == QEvent::KeyRelease) && m_removeAction) { + QMenu* menu = qobject_cast<QMenu*>(obj); + if (menu && menu->parent() == m_view) { + QKeyEvent* ev = static_cast<QKeyEvent*>(event); + if (ev->key() == Qt::Key_Shift) { + m_removeAction->update(); + } + } + } + + return KParts::ReadOnlyPart::eventFilter(obj, event); +} + //// void DolphinPartBrowserExtension::restoreState(QDataStream &stream) diff --git a/src/dolphinpart.h b/src/dolphinpart.h index 7881ded43..172bfafc6 100644 --- a/src/dolphinpart.h +++ b/src/dolphinpart.h @@ -39,6 +39,7 @@ class DolphinModel; class KDirLister; class DolphinView; class KAboutData; +class DolphinRemoveAction; class DolphinPart : public KParts::ReadOnlyPart { @@ -227,6 +228,8 @@ private Q_SLOTS: void setFilesToSelect(const KUrl::List& files); KUrl::List filesToSelect() const { return KUrl::List(); } // silence moc + virtual bool eventFilter(QObject*, QEvent*); + private: void createActions(); void createGoAction(const char* name, const char* iconName, @@ -245,6 +248,7 @@ private: KAction* m_findFileAction; KAction* m_openTerminalAction; QString m_nameFilter; + DolphinRemoveAction* m_removeAction; Q_DISABLE_COPY(DolphinPart) }; diff --git a/src/dolphinremoveaction.cpp b/src/dolphinremoveaction.cpp new file mode 100644 index 000000000..200fc407d --- /dev/null +++ b/src/dolphinremoveaction.cpp @@ -0,0 +1,60 @@ +/*************************************************************************** + * Copyright (C) 2013 by Dawit Alemayehu <[email protected] * + * * + * 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 "dolphinremoveaction.h" + +#include <QApplication> + +#include <KLocalizedString> + + +DolphinRemoveAction::DolphinRemoveAction(QObject* parent, KActionCollection* collection) : + QAction(parent), + m_collection(collection) +{ + update(); + connect(this, SIGNAL(triggered()), this, SLOT(slotRemoveActionTriggered())); +} + +void DolphinRemoveAction::slotRemoveActionTriggered() +{ + if (m_action) { + m_action->trigger(); + } +} + +void DolphinRemoveAction::update() +{ + Q_ASSERT(m_collection); + // Using setText(action->text()) does not apply the &-shortcut. + // This is only done until the original action has been shown at least once. To + // bypass this issue, the text and &-shortcut is applied manually. + if (qApp->keyboardModifiers() & Qt::ShiftModifier) { + m_action = m_collection ? m_collection->action("delete") : 0; + setText(i18nc("@action:inmenu", "&Delete")); + } else { + m_action = m_collection ? m_collection->action("move_to_trash") : 0; + setText(i18nc("@action:inmenu", "&Move to Trash")); + } + + if (m_action) { + setIcon(m_action->icon()); + setShortcuts(m_action->shortcuts()); + } +} diff --git a/src/dolphinremoveaction.h b/src/dolphinremoveaction.h new file mode 100644 index 000000000..1a123ace5 --- /dev/null +++ b/src/dolphinremoveaction.h @@ -0,0 +1,55 @@ +/*************************************************************************** + * Copyright (C) 2013 by Dawit Alemayehu <[email protected] * + * * + * 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 DOLPHINREMOVEACTION_H +#define DOLPHINREMOVEACTION_H + +#include "libdolphin_export.h" + +#include <QAction> +#include <QPointer> + +#include <KActionCollection> + +/** + * A QAction that manages the delete based on the current state of + * the Shift key or the parameter passed to update. + * + * This class expects the presence of both the "move_to_trash" and "delete" + * actions in @ref collection. + */ +class LIBDOLPHINPRIVATE_EXPORT DolphinRemoveAction : public QAction +{ + Q_OBJECT +public: + DolphinRemoveAction(QObject* parent, KActionCollection* collection); + /** + * Updates this action key based on the state of the Shift key. + */ + void update(); + +private Q_SLOTS: + void slotRemoveActionTriggered(); + +private: + QPointer<KActionCollection> m_collection; + QPointer<QAction> m_action; +}; + +#endif diff --git a/src/dolphinviewcontainer.cpp b/src/dolphinviewcontainer.cpp index 8800a1732..71dc5fd7b 100644 --- a/src/dolphinviewcontainer.cpp +++ b/src/dolphinviewcontainer.cpp @@ -106,6 +106,7 @@ DolphinViewContainer::DolphinViewContainer(const KUrl& url, QWidget* parent) : m_view = new DolphinView(url, this); connect(m_view, SIGNAL(urlChanged(KUrl)), m_urlNavigator, SLOT(setUrl(KUrl))); + connect(m_view, SIGNAL(urlChanged(KUrl)), m_messageWidget, SLOT(hide())); connect(m_view, SIGNAL(writeStateChanged(bool)), this, SIGNAL(writeStateChanged(bool))); connect(m_view, SIGNAL(requestItemInfo(KFileItem)), this, SLOT(showItemInfo(KFileItem))); connect(m_view, SIGNAL(itemActivated(KFileItem)), this, SLOT(slotItemActivated(KFileItem))); @@ -128,6 +129,8 @@ DolphinViewContainer::DolphinViewContainer(const KUrl& url, QWidget* parent) : this, SLOT(slotUrlNavigatorLocationChanged(KUrl))); connect(m_urlNavigator, SIGNAL(historyChanged()), this, SLOT(slotHistoryChanged())); + connect(m_urlNavigator, SIGNAL(returnPressed()), + this, SLOT(slotReturnPressed())); // Initialize status bar m_statusBar = new DolphinStatusBar(this); @@ -156,8 +159,10 @@ DolphinViewContainer::DolphinViewContainer(const KUrl& url, QWidget* parent) : this, SLOT(setNameFilter(QString))); connect(m_filterBar, SIGNAL(closeRequest()), this, SLOT(closeFilterBar())); + connect(m_filterBar, SIGNAL(focusViewRequest()), + this, SLOT(requestFocus())); connect(m_view, SIGNAL(urlChanged(KUrl)), - m_filterBar, SLOT(clear())); + m_filterBar, SLOT(slotUrlChanged())); m_topLayout->addWidget(m_urlNavigator); m_topLayout->addWidget(m_searchBox); @@ -477,37 +482,12 @@ void DolphinViewContainer::slotItemActivated(const KFileItem& item) // results in an active view. m_view->setActive(true); - KUrl url = item.targetUrl(); - - if (item.isDir()) { + const KUrl& url = DolphinView::openItemAsFolderUrl(item, GeneralSettings::browseThroughArchives()); + if (!url.isEmpty()) { m_view->setUrl(url); return; } - if (GeneralSettings::browseThroughArchives() && item.isFile() && url.isLocalFile()) { - // Generic mechanism for redirecting to tar:/<path>/ when clicking on a tar file, - // zip:/<path>/ 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(item.mimetype()); - if (!protocol.isEmpty()) { - url.setProtocol(protocol); - m_view->setUrl(url); - return; - } - } - - if (item.mimetype() == QLatin1String("application/x-desktop")) { - // Redirect to the URL in Type=Link desktop files - KDesktopFile desktopFile(url.toLocalFile()); - if (desktopFile.hasLinkType()) { - url = desktopFile.readUrl(); - m_view->setUrl(url); - return; - } - } - item.run(); } @@ -531,8 +511,7 @@ void DolphinViewContainer::showItemInfo(const KFileItem& item) void DolphinViewContainer::closeFilterBar() { - m_filterBar->hide(); - m_filterBar->clear(); + m_filterBar->closeFilterBar(); m_view->setFocus(); emit showFilterBarChanged(false); } @@ -574,6 +553,8 @@ void DolphinViewContainer::slotUrlNavigatorLocationAboutToBeChanged(const KUrl& void DolphinViewContainer::slotUrlNavigatorLocationChanged(const KUrl& url) { + slotReturnPressed(); + if (KProtocolManager::supportsListing(url)) { setSearchModeEnabled(isSearchUrl(url)); m_view->setUrl(url); @@ -616,7 +597,8 @@ void DolphinViewContainer::slotUrlNavigatorLocationChanged(const KUrl& url) void DolphinViewContainer::dropUrls(const KUrl& destination, QDropEvent* event) { - const QString error = DragAndDropHelper::dropUrls(KFileItem(), destination, event); + QString error; + DragAndDropHelper::dropUrls(KFileItem(), destination, event, error); if (!error.isEmpty()) { showMessage(error, Error); } @@ -657,6 +639,13 @@ void DolphinViewContainer::slotHistoryChanged() } } +void DolphinViewContainer::slotReturnPressed() +{ + if (!GeneralSettings::editableUrl()) { + m_urlNavigator->setUrlEditable(false); + } +} + void DolphinViewContainer::startSearching() { const KUrl url = m_searchBox->urlForSearching(); diff --git a/src/dolphinviewcontainer.h b/src/dolphinviewcontainer.h index e2d1b1875..bc58531a2 100644 --- a/src/dolphinviewcontainer.h +++ b/src/dolphinviewcontainer.h @@ -282,6 +282,8 @@ private slots: void slotHistoryChanged(); + void slotReturnPressed(); + /** * Gets the search URL from the searchbox and starts searching. */ diff --git a/src/filterbar/filterbar.cpp b/src/filterbar/filterbar.cpp index f3076f010..3fa9cc147 100644 --- a/src/filterbar/filterbar.cpp +++ b/src/filterbar/filterbar.cpp @@ -1,6 +1,7 @@ /*************************************************************************** * Copyright (C) 2006-2010 by Peter Penz <[email protected]> * * Copyright (C) 2006 by Gregor Kališnik <[email protected]> * + * Copyright (C) 2012 by Stuart Citrin <[email protected]> * * * * 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 * @@ -39,6 +40,13 @@ FilterBar::FilterBar(QWidget* parent) : closeButton->setToolTip(i18nc("@info:tooltip", "Hide Filter Bar")); connect(closeButton, SIGNAL(clicked()), this, SIGNAL(closeRequest())); + // Create button to lock text when changing folders + m_lockButton = new QToolButton(this); + m_lockButton->setCheckable(true); + m_lockButton->setIcon(KIcon("system-lock-screen.png")); + m_lockButton->setToolTip(i18nc("@info:tooltip", "Keep Filter When Changing Folders")); + connect(m_lockButton, SIGNAL(toggled(bool)), this, SLOT(slotToggleLockButton(bool))); + // Create label QLabel* filterLabel = new QLabel(i18nc("@label:textbox", "Filter:"), this); @@ -56,6 +64,7 @@ FilterBar::FilterBar(QWidget* parent) : hLayout->addWidget(closeButton); hLayout->addWidget(filterLabel); hLayout->addWidget(m_filterInput); + hLayout->addWidget(m_lockButton); filterLabel->setBuddy(m_filterInput); } @@ -64,6 +73,15 @@ FilterBar::~FilterBar() { } +void FilterBar::closeFilterBar() +{ + hide(); + clear(); + if (m_lockButton) { + m_lockButton->setChecked(false); + } +} + void FilterBar::selectAll() { m_filterInput->selectAll(); @@ -74,6 +92,20 @@ void FilterBar::clear() m_filterInput->clear(); } +void FilterBar::slotUrlChanged() +{ + if (!m_lockButton || !(m_lockButton->isChecked())) { + clear(); + } +} + +void FilterBar::slotToggleLockButton(bool checked) +{ + if (!checked) { + clear(); + } +} + void FilterBar::showEvent(QShowEvent* event) { if (!event->spontaneous()) { @@ -84,12 +116,23 @@ void FilterBar::showEvent(QShowEvent* event) void FilterBar::keyReleaseEvent(QKeyEvent* event) { QWidget::keyReleaseEvent(event); - if (event->key() == Qt::Key_Escape) { + + switch (event->key()) { + case Qt::Key_Escape: if (m_filterInput->text().isEmpty()) { emit closeRequest(); } else { m_filterInput->clear(); } + break; + + case Qt::Key_Enter: + case Qt::Key_Return: + emit focusViewRequest(); + break; + + default: + break; } } diff --git a/src/filterbar/filterbar.h b/src/filterbar/filterbar.h index 9546c6371..5d5463a28 100644 --- a/src/filterbar/filterbar.h +++ b/src/filterbar/filterbar.h @@ -1,6 +1,7 @@ /*************************************************************************** * Copyright (C) 2006-2010 by Peter Penz <[email protected]> * * Copyright (C) 2006 by Gregor Kališnik <[email protected]> * + * Copyright (C) 2012 by Stuart Citrin <[email protected]> * * * * 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 * @@ -24,6 +25,7 @@ #include <QWidget> class KLineEdit; +class QToolButton; /** * @brief Provides an input field for filtering the currently shown items. @@ -38,6 +40,9 @@ public: explicit FilterBar(QWidget* parent = 0); virtual ~FilterBar(); + /** Called by view container to hide this **/ + void closeFilterBar(); + /** * Selects the whole text of the filter bar. */ @@ -46,6 +51,10 @@ public: public slots: /** Clears the input field. */ void clear(); + /** Clears the input field if the "lock button" is disabled. */ + void slotUrlChanged(); + /** The input field is cleared also if the "lock button" is released. */ + void slotToggleLockButton(bool checked); signals: /** @@ -59,12 +68,18 @@ signals: */ void closeRequest(); + /* + * Emitted as soon as the focus should be returned back to the view. + */ + void focusViewRequest(); + protected: virtual void showEvent(QShowEvent* event); virtual void keyReleaseEvent(QKeyEvent* event); private: KLineEdit* m_filterInput; + QToolButton* m_lockButton; }; #endif diff --git a/src/kitemviews/kfileitemmodel.cpp b/src/kitemviews/kfileitemmodel.cpp index 400d29849..7ea5e8018 100644 --- a/src/kitemviews/kfileitemmodel.cpp +++ b/src/kitemviews/kfileitemmodel.cpp @@ -1,21 +1,23 @@ -/*************************************************************************** - * Copyright (C) 2011 by Peter Penz <[email protected]> * - * * - * 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 * - ***************************************************************************/ +/***************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * Copyright (C) 2013 by Frank Reininghaus <[email protected]> * + * Copyright (C) 2013 by Emmanuel Pescosta <[email protected]> * + * * + * 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" @@ -56,12 +58,10 @@ KFileItemModel::KFileItemModel(QObject* parent) : m_resortAllItemsTimer(0), m_pendingItemsToInsert(), m_groups(), - m_expandedParentsCountRoot(UninitializedExpandedParentsCountRoot), m_expandedDirs(), m_urlsToExpand() { m_dirLister = new KFileItemModelDirLister(this); - m_dirLister->setAutoUpdate(true); m_dirLister->setDelayedMimeTypes(true); const QWidget* parentWidget = qobject_cast<QWidget*>(parent); @@ -72,7 +72,7 @@ KFileItemModel::KFileItemModel(QObject* parent) : connect(m_dirLister, SIGNAL(started(KUrl)), this, SIGNAL(directoryLoadingStarted())); connect(m_dirLister, SIGNAL(canceled()), this, SLOT(slotCanceled())); connect(m_dirLister, SIGNAL(completed(KUrl)), this, SLOT(slotCompleted())); - connect(m_dirLister, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems(KFileItemList))); + connect(m_dirLister, SIGNAL(itemsAdded(KUrl,KFileItemList)), this, SLOT(slotItemsAdded(KUrl,KFileItemList))); connect(m_dirLister, SIGNAL(itemsDeleted(KFileItemList)), this, SLOT(slotItemsDeleted(KFileItemList))); connect(m_dirLister, SIGNAL(refreshItems(QList<QPair<KFileItem,KFileItem> >)), this, SLOT(slotRefreshItems(QList<QPair<KFileItem,KFileItem> >))); connect(m_dirLister, SIGNAL(clear()), this, SLOT(slotClear())); @@ -113,7 +113,7 @@ KFileItemModel::KFileItemModel(QObject* parent) : KFileItemModel::~KFileItemModel() { qDeleteAll(m_itemData); - m_itemData.clear(); + qDeleteAll(m_filteredItems.values()); } void KFileItemModel::loadDirectory(const KUrl& url) @@ -167,7 +167,7 @@ bool KFileItemModel::setData(int index, const QHash<QByteArray, QVariant>& value QHashIterator<QByteArray, QVariant> it(values); while (it.hasNext()) { it.next(); - const QByteArray role = it.key(); + const QByteArray role = sharedValue(it.key()); const QVariant value = it.value(); if (currentValues[role] != value) { @@ -405,7 +405,7 @@ void KFileItemModel::setRoles(const QSet<QByteArray>& roles) // 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[i]->values = retrieveData(m_itemData.at(i)->item, m_itemData.at(i)->parent); } kWarning() << "TODO: Emitting itemsChanged() with no information what has changed!"; @@ -425,12 +425,13 @@ bool KFileItemModel::setExpanded(int index, bool expanded) } QHash<QByteArray, QVariant> values; - values.insert("isExpanded", expanded); + values.insert(sharedValue("isExpanded"), expanded); if (!setData(index, values)) { return false; } - const KUrl url = m_itemData.at(index)->item.url(); + const KFileItem item = m_itemData.at(index)->item; + const KUrl url = item.url(); if (expanded) { m_expandedDirs.insert(url); m_dirLister->openUrl(url, KDirLister::Keep); @@ -438,38 +439,11 @@ bool KFileItemModel::setExpanded(int index, bool expanded) m_expandedDirs.remove(url); m_dirLister->stop(url); + removeFilteredChildren(KFileItemList() << item); - KFileItemList itemsToRemove; - const int expandedParentsCount = data(index)["expandedParentsCount"].toInt(); - ++index; - while (index < count() && data(index)["expandedParentsCount"].toInt() > expandedParentsCount) { - itemsToRemove.append(m_itemData.at(index)->item); - ++index; - } - - QSet<KUrl> urlsToRemove; - urlsToRemove.reserve(itemsToRemove.count() + 1); - urlsToRemove.insert(url); - foreach (const KFileItem& item, itemsToRemove) { - KUrl url = item.url(); - url.adjustPath(KUrl::RemoveTrailingSlash); - urlsToRemove.insert(url); - } - - QSet<KFileItem>::iterator it = m_filteredItems.begin(); - while (it != m_filteredItems.end()) { - const KUrl url = it->url(); - KUrl parentUrl = url.upUrl(); - parentUrl.adjustPath(KUrl::RemoveTrailingSlash); - - if (urlsToRemove.contains(parentUrl)) { - it = m_filteredItems.erase(it); - } else { - ++it; - } - } - - removeItems(itemsToRemove); + const KFileItemList itemsToRemove = childItems(item); + removeFilteredChildren(itemsToRemove); + removeItems(itemsToRemove, DeleteItemData); } return true; @@ -579,31 +553,57 @@ void KFileItemModel::applyFilters() // Only filter non-expanded items as child items may never // exist without a parent item if (!itemData->values.value("isExpanded").toBool()) { - if (!m_filter.matches(itemData->item)) { - newFilteredItems.append(itemData->item); - m_filteredItems.insert(itemData->item); + const KFileItem item = itemData->item; + if (!m_filter.matches(item)) { + newFilteredItems.append(item); + m_filteredItems.insert(item, itemData); } } } - removeItems(newFilteredItems); + removeItems(newFilteredItems, KeepItemData); // Check which hidden items from m_filteredItems should // get visible again and hence removed from m_filteredItems. - KFileItemList newVisibleItems; + QList<ItemData*> newVisibleItems; - QMutableSetIterator<KFileItem> it(m_filteredItems); - while (it.hasNext()) { - const KFileItem item = it.next(); - if (m_filter.matches(item)) { - newVisibleItems.append(item); - it.remove(); + QHash<KFileItem, ItemData*>::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 KFileItemList& parentsList) +{ + if (m_filteredItems.isEmpty()) { + return; + } + + // First, we put the parent items into a set to provide fast lookup + // while iterating over m_filteredItems and prevent quadratic + // complexity if there are N parents and N filtered items. + const QSet<KFileItem> parents = parentsList.toSet(); + + QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.begin(); + while (it != m_filteredItems.end()) { + const ItemData* parent = it.value()->parent; + + if (parent && parents.contains(parent->item)) { + delete it.value(); + it = m_filteredItems.erase(it); + } else { + ++it; + } + } +} + QList<KFileItemModel::RoleInfo> KFileItemModel::rolesInformation() { static QList<RoleInfo> rolesInfo; @@ -689,7 +689,7 @@ void KFileItemModel::resortAllItems() m_items.clear(); // Resort the items - KFileItemModelSortAlgorithm::sort(this, m_itemData.begin(), m_itemData.end()); + sort(m_itemData.begin(), m_itemData.end()); for (int i = 0; i < itemCount; ++i) { m_items.insert(m_itemData.at(i)->item.url(), i); } @@ -751,11 +751,14 @@ void KFileItemModel::slotCanceled() emit directoryLoadingCanceled(); } -void KFileItemModel::slotNewItems(const KFileItemList& items) +void KFileItemModel::slotItemsAdded(const KUrl& directoryUrl, const KFileItemList& items) { Q_ASSERT(!items.isEmpty()); - if (m_requestRole[ExpandedParentsCountRole] && m_expandedParentsCountRoot >= 0) { + KUrl parentUrl = directoryUrl; + parentUrl.adjustPath(KUrl::RemoveTrailingSlash); + + if (m_requestRole[ExpandedParentsCountRole]) { // 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(); @@ -776,8 +779,6 @@ void KFileItemModel::slotNewItems(const KFileItemList& items) // 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. - KUrl parentUrl = item.url().upUrl(); - parentUrl.adjustPath(KUrl::RemoveTrailingSlash); const int parentIndex = m_items.value(parentUrl, -1); if (parentIndex >= 0 && !m_itemData[parentIndex]->values.value("isExpanded").toBool()) { // The parent is not expanded. @@ -785,22 +786,21 @@ void KFileItemModel::slotNewItems(const KFileItemList& items) } } + QList<ItemData*> itemDataList = createItemDataList(parentUrl, items); + if (!m_filter.hasSetFilters()) { - m_pendingItemsToInsert.append(items); + 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. - KFileItemList filteredItems; - foreach (const KFileItem& item, items) { - if (m_filter.matches(item)) { - filteredItems.append(item); + foreach (ItemData* itemData, itemDataList) { + if (m_filter.matches(itemData->item)) { + m_pendingItemsToInsert.append(itemData); } else { - m_filteredItems.insert(item); + m_filteredItems.insert(itemData->item, itemData); } } - - m_pendingItemsToInsert.append(filteredItems); } if (useMaximumUpdateInterval() && !m_maximumUpdateIntervalTimer->isActive()) { @@ -815,7 +815,7 @@ void KFileItemModel::slotItemsDeleted(const KFileItemList& items) dispatchPendingItemsToInsert(); KFileItemList itemsToRemove = items; - if (m_requestRole[ExpandedParentsCountRole] && m_expandedParentsCountRoot >= 0) { + if (m_requestRole[ExpandedParentsCountRole]) { // Assure that removing a parent item also results in removing all children foreach (const KFileItem& item, items) { itemsToRemove.append(childItems(item)); @@ -824,38 +824,19 @@ void KFileItemModel::slotItemsDeleted(const KFileItemList& items) if (!m_filteredItems.isEmpty()) { foreach (const KFileItem& item, itemsToRemove) { - m_filteredItems.remove(item); - } - - if (m_requestRole[ExpandedParentsCountRole] && m_expandedParentsCountRoot >= 0) { - // Remove all filtered children of deleted items. First, we put the - // deleted URLs into a set to provide fast lookup while iterating - // over m_filteredItems and prevent quadratic complexity if there - // are N removed items and N filtered items. - QSet<KUrl> urlsToRemove; - urlsToRemove.reserve(itemsToRemove.count()); - foreach (const KFileItem& item, itemsToRemove) { - KUrl url = item.url(); - url.adjustPath(KUrl::RemoveTrailingSlash); - urlsToRemove.insert(url); + QHash<KFileItem, ItemData*>::iterator it = m_filteredItems.find(item); + if (it != m_filteredItems.end()) { + delete it.value(); + m_filteredItems.erase(it); } + } - QSet<KFileItem>::iterator it = m_filteredItems.begin(); - while (it != m_filteredItems.end()) { - const KUrl url = it->url(); - KUrl parentUrl = url.upUrl(); - parentUrl.adjustPath(KUrl::RemoveTrailingSlash); - - if (urlsToRemove.contains(parentUrl)) { - it = m_filteredItems.erase(it); - } else { - ++it; - } - } + if (m_requestRole[ExpandedParentsCountRole]) { + removeFilteredChildren(itemsToRemove); } } - removeItems(itemsToRemove); + removeItems(itemsToRemove, DeleteItemData); } void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem> >& items) @@ -865,12 +846,12 @@ void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem> >& kDebug() << "Refreshing" << items.count() << "items"; #endif - m_groups.clear(); - // Get the indexes of all items that have been refreshed QList<int> indexes; indexes.reserve(items.count()); + QSet<QByteArray> changedRoles; + QListIterator<QPair<KFileItem, KFileItem> > it(items); while (it.hasNext()) { const QPair<KFileItem, KFileItem>& itemPair = it.next(); @@ -882,10 +863,15 @@ void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem> >& // 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<QByteArray, QVariant> it(retrieveData(newItem)); + QHashIterator<QByteArray, QVariant> it(retrieveData(newItem, m_itemData.at(index)->parent)); + QHash<QByteArray, QVariant>& values = m_itemData[index]->values; while (it.hasNext()) { it.next(); - m_itemData[index]->values.insert(it.key(), it.value()); + const QByteArray& role = it.key(); + if (values.value(role) != it.value()) { + values.insert(role, it.value()); + changedRoles.insert(role); + } } m_items.remove(oldItem.url()); @@ -926,9 +912,11 @@ void KFileItemModel::slotRefreshItems(const QList<QPair<KFileItem, KFileItem> >& itemRangeList.append(KItemRange(rangeIndex, rangeCount)); } - emit itemsChanged(itemRangeList, m_roles); + emit itemsChanged(itemRangeList, changedRoles); - resortAllItems(); + if (changedRoles.contains(sortRole())) { + resortAllItems(); + } } void KFileItemModel::slotClear() @@ -937,6 +925,7 @@ void KFileItemModel::slotClear() kDebug() << "Clearing all items"; #endif + qDeleteAll(m_filteredItems.values()); m_filteredItems.clear(); m_groups.clear(); @@ -944,8 +933,6 @@ void KFileItemModel::slotClear() m_resortAllItemsTimer->stop(); m_pendingItemsToInsert.clear(); - m_expandedParentsCountRoot = UninitializedExpandedParentsCountRoot; - const int removedCount = m_itemData.count(); if (removedCount > 0) { qDeleteAll(m_itemData); @@ -976,197 +963,212 @@ void KFileItemModel::dispatchPendingItemsToInsert() } } -void KFileItemModel::insertItems(const KFileItemList& items) +void KFileItemModel::insertItems(QList<ItemData*>& newItems) { - if (items.isEmpty()) { + if (newItems.isEmpty()) { return; } - 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); - } - #ifdef KFILEITEMMODEL_DEBUG QElapsedTimer timer; timer.start(); kDebug() << "==========================================================="; - kDebug() << "Inserting" << items.count() << "items"; + kDebug() << "Inserting" << newItems.count() << "items"; #endif m_groups.clear(); - QList<ItemData*> sortedItems = createItemDataList(items); - KFileItemModelSortAlgorithm::sort(this, sortedItems.begin(), sortedItems.end()); + sort(newItems.begin(), newItems.end()); #ifdef KFILEITEMMODEL_DEBUG kDebug() << "[TIME] Sorting:" << timer.elapsed(); #endif KItemRangeList itemRanges; - int targetIndex = 0; - int sourceIndex = 0; - int insertedAtIndex = -1; // Index for the current item-range - int insertedCount = 0; // Count for the current item-range - int previouslyInsertedCount = 0; // Sum of previously inserted items for all ranges - while (sourceIndex < sortedItems.count()) { - // Find target index from m_items to insert the current item - // in a sorted order - const int previousTargetIndex = targetIndex; - while (targetIndex < m_itemData.count()) { - if (!lessThan(m_itemData.at(targetIndex), sortedItems.at(sourceIndex))) { - break; - } - ++targetIndex; - } + const int existingItemCount = m_itemData.count(); + const int newItemCount = newItems.count(); + const int totalItemCount = existingItemCount + newItemCount; - if (targetIndex - previousTargetIndex > 0 && insertedAtIndex >= 0) { - itemRanges << KItemRange(insertedAtIndex, insertedCount); - previouslyInsertedCount += insertedCount; - insertedAtIndex = targetIndex - previouslyInsertedCount; - insertedCount = 0; + 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(0); } - // Insert item at the position targetIndex by transferring - // the ownership of the item-data from sortedItems to m_itemData. - // m_items will be inserted after the loop (see comment below) - m_itemData.insert(targetIndex, sortedItems.at(sourceIndex)); - ++insertedCount; + // We build the new list m_items 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; - if (insertedAtIndex < 0) { - insertedAtIndex = targetIndex; - Q_ASSERT(previouslyInsertedCount == 0); + while (sourceIndexNewItems >= 0) { + ItemData* newItem = newItems.at(sourceIndexNewItems); + if (sourceIndexExistingItems >= 0 && lessThan(newItem, m_itemData.at(sourceIndexExistingItems))) { + // 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); } - ++targetIndex; - ++sourceIndex; + + // Note that itemRanges is still sorted in reverse order. + std::reverse(itemRanges.begin(), itemRanges.end()); } - // The indexes of all m_items must be adjusted, not only the index - // of the new items - const int itemDataCount = m_itemData.count(); - for (int i = 0; i < itemDataCount; ++i) { + // The indexes starting from the first inserted item must be adjusted. + m_items.reserve(totalItemCount); + for (int i = itemRanges.front().index; i < totalItemCount; ++i) { m_items.insert(m_itemData.at(i)->item.url(), i); } - itemRanges << KItemRange(insertedAtIndex, insertedCount); emit itemsInserted(itemRanges); #ifdef KFILEITEMMODEL_DEBUG - kDebug() << "[TIME] Inserting of" << items.count() << "items:" << timer.elapsed(); + kDebug() << "[TIME] Inserting of" << newItems.count() << "items:" << timer.elapsed(); #endif } -void KFileItemModel::removeItems(const KFileItemList& items) +static KItemRangeList sortedIndexesToKItemRangeList(const QList<int>& sortedNumbers) { - if (items.isEmpty()) { - return; + if (sortedNumbers.empty()) { + return KItemRangeList(); + } + + KItemRangeList result; + + QList<int>::const_iterator it = sortedNumbers.begin(); + int index = *it; + int count = 1; + + ++it; + + QList<int>::const_iterator end = sortedNumbers.end(); + while (it != end) { + if (*it == index + count) { + ++count; + } else { + result << KItemRange(index, count); + index = *it; + count = 1; + } + ++it; } + result << KItemRange(index, count); + return result; +} + +void KFileItemModel::removeItems(const KFileItemList& items, RemoveItemsBehavior behavior) +{ #ifdef KFILEITEMMODEL_DEBUG kDebug() << "Removing " << items.count() << "items"; #endif m_groups.clear(); - QList<ItemData*> sortedItems; - sortedItems.reserve(items.count()); - foreach (const KFileItem& item, items) { - const int index = m_items.value(item.url(), -1); - if (index >= 0) { - sortedItems.append(m_itemData.at(index)); - } - } - KFileItemModelSortAlgorithm::sort(this, sortedItems.begin(), sortedItems.end()); - + // Step 1: Determine the indexes of the removed items, remove them from + // the hash m_items, and free the ItemData. QList<int> indexesToRemove; indexesToRemove.reserve(items.count()); + foreach (const KFileItem& item, items) { + const KUrl url = item.url(); + const int index = m_items.value(url, -1); + if (index >= 0) { + indexesToRemove.append(index); - // Calculate the item ranges that will get deleted - KItemRangeList itemRanges; - int removedAtIndex = -1; - int removedCount = 0; - int targetIndex = 0; - foreach (const ItemData* itemData, sortedItems) { - const KFileItem& itemToRemove = itemData->item; + // Prevent repeated expensive rehashing by using QHash::erase(), + // rather than QHash::remove(). + QHash<KUrl, int>::iterator it = m_items.find(url); + m_items.erase(it); - const int previousTargetIndex = targetIndex; - while (targetIndex < m_itemData.count()) { - if (m_itemData.at(targetIndex)->item.url() == itemToRemove.url()) { - break; + if (behavior == DeleteItemData) { + delete m_itemData.at(index); } - ++targetIndex; - } - if (targetIndex >= m_itemData.count()) { - kWarning() << "Item that should be deleted has not been found!"; - return; - } - if (targetIndex - previousTargetIndex > 0 && removedAtIndex >= 0) { - itemRanges << KItemRange(removedAtIndex, removedCount); - removedAtIndex = targetIndex; - removedCount = 0; + m_itemData[index] = 0; } + } - indexesToRemove.append(targetIndex); - if (removedAtIndex < 0) { - removedAtIndex = targetIndex; - } - ++removedCount; - ++targetIndex; + if (indexesToRemove.isEmpty()) { + return; } - // Delete the items - for (int i = indexesToRemove.count() - 1; i >= 0; --i) { - const int indexToRemove = indexesToRemove.at(i); - ItemData* data = m_itemData.at(indexToRemove); + std::sort(indexesToRemove.begin(), indexesToRemove.end()); - m_items.remove(data->item.url()); + // Step 2: Remove the ItemData pointers from the list m_itemData. + const KItemRangeList itemRanges = sortedIndexesToKItemRangeList(indexesToRemove); + int target = itemRanges.at(0).index; + int source = itemRanges.at(0).index + itemRanges.at(0).count; + int nextRange = 1; - delete data; - m_itemData.removeAt(indexToRemove); - } + const int oldItemDataCount = m_itemData.count(); + while (source < oldItemDataCount) { + m_itemData[target] = m_itemData[source]; + ++target; + ++source; - // The indexes of all m_items must be adjusted, not only the index - // of the removed items - const int itemDataCount = m_itemData.count(); - for (int i = 0; i < itemDataCount; ++i) { - m_items.insert(m_itemData.at(i)->item.url(), i); + if (nextRange < itemRanges.count() && source == itemRanges.at(nextRange).index) { + // Skip the items in the next removed range. + source += itemRanges.at(nextRange).count; + ++nextRange; + } } - if (count() <= 0) { - m_expandedParentsCountRoot = UninitializedExpandedParentsCountRoot; + m_itemData.erase(m_itemData.end() - indexesToRemove.count(), m_itemData.end()); + + // Step 3: Adjust indexes in the hash m_items, starting from the + // index of the first removed item. + const int newItemDataCount = m_itemData.count(); + for (int i = itemRanges.front().index; i < newItemDataCount; ++i) { + m_items.insert(m_itemData.at(i)->item.url(), i); } - itemRanges << KItemRange(removedAtIndex, removedCount); emit itemsRemoved(itemRanges); } -QList<KFileItemModel::ItemData*> KFileItemModel::createItemDataList(const KFileItemList& items) const +QList<KFileItemModel::ItemData*> KFileItemModel::createItemDataList(const KUrl& 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 = m_items.value(parentUrl, -1); + ItemData* parentItem = parentIndex < 0 ? 0 : m_itemData.at(parentIndex); + QList<ItemData*> itemDataList; itemDataList.reserve(items.count()); foreach (const KFileItem& item, items) { ItemData* itemData = new ItemData(); itemData->item = item; - itemData->values = retrieveData(item); - itemData->parent = 0; - - const bool determineParent = m_requestRole[ExpandedParentsCountRole] - && itemData->values["expandedParentsCount"].toInt() > 0; - if (determineParent) { - KUrl parentUrl = item.url().upUrl(); - parentUrl.adjustPath(KUrl::RemoveTrailingSlash); - const int parentIndex = m_items.value(parentUrl, -1); - if (parentIndex >= 0) { - itemData->parent = m_itemData.at(parentIndex); - } else { - kWarning() << "Parent item not found for" << item.url(); - } - } - + itemData->values = retrieveData(item, parentItem); + itemData->parent = parentItem; itemDataList.append(itemData); } @@ -1187,9 +1189,8 @@ void KFileItemModel::removeExpandedItems() // The m_expandedParentsCountRoot may not get reset before all items with // a bigger count have been removed. - removeItems(expandedItems); + removeItems(expandedItems, DeleteItemData); - m_expandedParentsCountRoot = UninitializedExpandedParentsCountRoot; m_expandedDirs.clear(); } @@ -1252,33 +1253,33 @@ QByteArray KFileItemModel::roleForType(RoleType roleType) const return roles.value(roleType); } -QHash<QByteArray, QVariant> KFileItemModel::retrieveData(const KFileItem& item) const +QHash<QByteArray, QVariant> 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<QByteArray, QVariant> data; - data.insert("url", item.url()); + data.insert(sharedValue("url"), item.url()); const bool isDir = item.isDir(); if (m_requestRole[IsDirRole]) { - data.insert("isDir", isDir); + data.insert(sharedValue("isDir"), isDir); } if (m_requestRole[IsLinkRole]) { const bool isLink = item.isLink(); - data.insert("isLink", isLink); + data.insert(sharedValue("isLink"), isLink); } if (m_requestRole[NameRole]) { - data.insert("text", item.text()); + data.insert(sharedValue("text"), item.text()); } if (m_requestRole[SizeRole]) { if (isDir) { - data.insert("size", QVariant()); + data.insert(sharedValue("size"), QVariant()); } else { - data.insert("size", item.size()); + data.insert(sharedValue("size"), item.size()); } } @@ -1287,19 +1288,19 @@ QHash<QByteArray, QVariant> KFileItemModel::retrieveData(const KFileItem& item) // having several thousands of items. Instead the formatting of the // date-time will be done on-demand by the view when the date will be shown. const KDateTime dateTime = item.time(KFileItem::ModificationTime); - data.insert("date", dateTime.dateTime()); + data.insert(sharedValue("date"), dateTime.dateTime()); } if (m_requestRole[PermissionsRole]) { - data.insert("permissions", item.permissionsString()); + data.insert(sharedValue("permissions"), item.permissionsString()); } if (m_requestRole[OwnerRole]) { - data.insert("owner", item.user()); + data.insert(sharedValue("owner"), item.user()); } if (m_requestRole[GroupRole]) { - data.insert("group", item.group()); + data.insert(sharedValue("group"), item.group()); } if (m_requestRole[DestinationRole]) { @@ -1307,7 +1308,7 @@ QHash<QByteArray, QVariant> KFileItemModel::retrieveData(const KFileItem& item) if (destination.isEmpty()) { destination = QLatin1String("-"); } - data.insert("destination", destination); + data.insert(sharedValue("destination"), destination); } if (m_requestRole[PathRole]) { @@ -1330,43 +1331,27 @@ QHash<QByteArray, QVariant> KFileItemModel::retrieveData(const KFileItem& item) const int index = path.lastIndexOf(item.text()); path = path.mid(0, index - 1); - data.insert("path", path); + data.insert(sharedValue("path"), path); } if (m_requestRole[IsExpandableRole]) { - data.insert("isExpandable", item.isDir() && item.url() == item.targetUrl()); + data.insert(sharedValue("isExpandable"), item.isDir() && item.url() == item.targetUrl()); } if (m_requestRole[ExpandedParentsCountRole]) { - if (m_expandedParentsCountRoot == UninitializedExpandedParentsCountRoot) { - const KUrl rootUrl = m_dirLister->url(); - const QString protocol = rootUrl.protocol(); - const bool forceExpandedParentsCountRoot = (protocol == QLatin1String("trash") || - protocol == QLatin1String("nepomuk") || - protocol == QLatin1String("remote") || - protocol.contains(QLatin1String("search"))); - if (forceExpandedParentsCountRoot) { - m_expandedParentsCountRoot = ForceExpandedParentsCountRoot; - } else { - const QString rootDir = rootUrl.path(KUrl::AddTrailingSlash); - m_expandedParentsCountRoot = rootDir.count('/'); - } + int level = 0; + if (parent) { + level = parent->values["expandedParentsCount"].toInt() + 1; } - if (m_expandedParentsCountRoot == ForceExpandedParentsCountRoot) { - data.insert("expandedParentsCount", -1); - } else { - const QString dir = item.url().directory(KUrl::AppendTrailingSlash); - const int level = dir.count('/') - m_expandedParentsCountRoot; - data.insert("expandedParentsCount", level); - } + data.insert(sharedValue("expandedParentsCount"), level); } if (item.isMimeTypeKnown()) { - data.insert("iconName", item.iconName()); + data.insert(sharedValue("iconName"), item.iconName()); if (m_requestRole[TypeRole]) { - data.insert("type", item.mimeComment()); + data.insert(sharedValue("type"), item.mimeComment()); } } @@ -1377,11 +1362,34 @@ bool KFileItemModel::lessThan(const ItemData* a, const ItemData* b) const { int result = 0; - if (m_expandedParentsCountRoot >= 0) { - result = expandedParentsCountCompare(a, b); - if (result != 0) { - // The items have parents with different expansion levels - return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0; + if (a->parent != b->parent) { + const int expansionLevelA = a->values.value("expandedParentsCount").toInt(); + const int expansionLevelB = b->values.value("expandedParentsCount").toInt(); + + // 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(a->values.value("expandedParentsCount").toInt() == b->values.value("expandedParentsCount").toInt()); + + // Compare the last parents of a and b which are different. + while (a->parent != b->parent) { + a = a->parent; + b = b->parent; } } @@ -1400,6 +1408,44 @@ bool KFileItemModel::lessThan(const ItemData* a, const ItemData* b) const return (sortOrder() == Qt::AscendingOrder) ? result < 0 : result > 0; } +/** + * Helper class for KFileItemModel::sort(). + */ +class KFileItemModelLessThan +{ +public: + KFileItemModelLessThan(const KFileItemModel* model) : + m_model(model) + { + } + + bool operator()(const KFileItemModel::ItemData* a, const KFileItemModel::ItemData* b) const + { + return m_model->lessThan(a, b); + } + +private: + const KFileItemModel* m_model; +}; + +void KFileItemModel::sort(QList<KFileItemModel::ItemData*>::iterator begin, + QList<KFileItemModel::ItemData*>::iterator end) const +{ + KFileItemModelLessThan lessThan(this); + + 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, lessThan, 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, lessThan); + } +} + int KFileItemModel::sortRoleCompare(const ItemData* a, const ItemData* b) const { const KFileItem& itemA = a->item; @@ -1524,88 +1570,6 @@ int KFileItemModel::stringCompare(const QString& a, const QString& b) const : QString::compare(a, b, Qt::CaseSensitive); } -int KFileItemModel::expandedParentsCountCompare(const ItemData* a, const ItemData* b) const -{ - const KUrl urlA = a->item.url(); - const KUrl urlB = b->item.url(); - if (urlA.directory() == urlB.directory()) { - // Both items have the same directory as parent - return 0; - } - - // Check whether one item is the parent of the other item - if (urlA.isParentOf(urlB)) { - return (sortOrder() == Qt::AscendingOrder) ? -1 : +1; - } else if (urlB.isParentOf(urlA)) { - return (sortOrder() == Qt::AscendingOrder) ? +1 : -1; - } - - // Determine the maximum common path of both items and - // remember the index in 'index' - const QString pathA = urlA.path(); - const QString pathB = urlB.path(); - - const int maxIndex = qMin(pathA.length(), pathB.length()) - 1; - int index = 0; - while (index <= maxIndex && pathA.at(index) == pathB.at(index)) { - ++index; - } - if (index > maxIndex) { - index = maxIndex; - } - while (index > 0 && (pathA.at(index) != QLatin1Char('/') || pathB.at(index) != QLatin1Char('/'))) { - --index; - } - - // Determine the first sub-path after the common path and - // check whether it represents a directory or already a file - bool isDirA = true; - const QString subPathA = subPath(a->item, pathA, index, &isDirA); - bool isDirB = true; - const QString subPathB = subPath(b->item, pathB, index, &isDirB); - - if (m_sortDirsFirst || m_sortRole == SizeRole) { - if (isDirA && !isDirB) { - return (sortOrder() == Qt::AscendingOrder) ? -1 : +1; - } else if (!isDirA && isDirB) { - return (sortOrder() == Qt::AscendingOrder) ? +1 : -1; - } - } - - // Compare the items of the parents that represent the first - // different path after the common path. - const QString parentPathA = pathA.left(index) + subPathA; - const QString parentPathB = pathB.left(index) + subPathB; - - const ItemData* parentA = a; - while (parentA && parentA->item.url().path() != parentPathA) { - parentA = parentA->parent; - } - - const ItemData* parentB = b; - while (parentB && parentB->item.url().path() != parentPathB) { - parentB = parentB->parent; - } - - if (parentA && parentB) { - return sortRoleCompare(parentA, parentB); - } - - kWarning() << "Child items without parent detected:" << a->item.url() << b->item.url(); - return QString::compare(urlA.url(), urlB.url(), Qt::CaseSensitive); -} - -QString KFileItemModel::subPath(const KFileItem& item, - const QString& itemPath, - int start, - bool* isDir) const -{ - Q_ASSERT(isDir); - const int pathIndex = itemPath.indexOf('/', start + 1); - *isDir = (pathIndex > 0) || item.isDir(); - return itemPath.mid(start, pathIndex - start); -} - bool KFileItemModel::useMaximumUpdateInterval() const { return !m_dirLister->url().isLocalFile(); @@ -1722,12 +1686,6 @@ QList<QPair<int, QVariant> > KFileItemModel::dateRoleGroups() const const QDate currentDate = KDateTime::currentLocalDateTime().date(); - int yearForCurrentWeek = 0; - int currentWeek = currentDate.weekNumber(&yearForCurrentWeek); - if (yearForCurrentWeek == currentDate.year() + 1) { - currentWeek = 53; - } - QDate previousModifiedDate; QString groupValue; for (int i = 0; i <= maxIndex; ++i) { @@ -1745,20 +1703,9 @@ QList<QPair<int, QVariant> > KFileItemModel::dateRoleGroups() const const int daysDistance = modifiedDate.daysTo(currentDate); - int yearForModifiedWeek = 0; - int modifiedWeek = modifiedDate.weekNumber(&yearForModifiedWeek); - if (yearForModifiedWeek == modifiedDate.year() + 1) { - modifiedWeek = 53; - } - QString newGroupValue; if (currentDate.year() == modifiedDate.year() && currentDate.month() == modifiedDate.month()) { - if (modifiedWeek > currentWeek) { - // Usecase: modified date = 2010-01-01, current date = 2010-01-22 - // modified week = 53, current week = 3 - modifiedWeek = 0; - } - switch (currentWeek - modifiedWeek) { + switch (daysDistance / 7) { case 0: switch (daysDistance) { case 0: newGroupValue = i18nc("@title:group Date", "Today"); break; @@ -1767,7 +1714,7 @@ QList<QPair<int, QVariant> > KFileItemModel::dateRoleGroups() const } break; case 1: - newGroupValue = i18nc("@title:group Date", "Last Week"); + newGroupValue = i18nc("@title:group Date", "One Week Ago"); break; case 2: newGroupValue = i18nc("@title:group Date", "Two Weeks Ago"); @@ -1790,7 +1737,7 @@ QList<QPair<int, QVariant> > KFileItemModel::dateRoleGroups() const } else if (daysDistance <= 7) { newGroupValue = modifiedTime.toString(i18nc("@title:group The week day name: %A, %B is full month name in current locale, and %Y is full year number", "%A (%B, %Y)")); } else if (daysDistance <= 7 * 2) { - newGroupValue = modifiedTime.toString(i18nc("@title:group Date: %B is full month name in current locale, and %Y is full year number", "Last Week (%B, %Y)")); + newGroupValue = modifiedTime.toString(i18nc("@title:group Date: %B is full month name in current locale, and %Y is full year number", "One Week Ago (%B, %Y)")); } else if (daysDistance <= 7 * 3) { newGroupValue = modifiedTime.toString(i18nc("@title:group Date: %B is full month name in current locale, and %Y is full year number", "Two Weeks Ago (%B, %Y)")); } else if (daysDistance <= 7 * 4) { @@ -2011,7 +1958,7 @@ void KFileItemModel::determineMimeTypes(const KFileItemList& items, int timeout) { QElapsedTimer timer; timer.start(); - foreach (KFileItem item, items) { // krazy:exclude=foreach + foreach (const KFileItem& item, items) { // krazy:exclude=foreach item.determineMimeType(); if (timer.elapsed() > timeout) { // Don't block the user interface, let the remaining items @@ -2021,4 +1968,64 @@ void KFileItemModel::determineMimeTypes(const KFileItemList& items, int timeout) } } +QByteArray KFileItemModel::sharedValue(const QByteArray& value) +{ + static QSet<QByteArray> pool; + const QSet<QByteArray>::const_iterator it = pool.constFind(value); + + if (it != pool.constEnd()) { + return *it; + } else { + pool.insert(value); + return value; + } +} + +bool KFileItemModel::isConsistent() const +{ + if (m_items.count() != m_itemData.count()) { + return false; + } + + for (int i = 0; i < count(); ++i) { + // Check if m_items and m_itemData are consistent. + const KFileItem item = fileItem(i); + if (item.isNull()) { + qWarning() << "Item" << i << "is null"; + return false; + } + + const int itemIndex = index(item); + if (itemIndex != i) { + qWarning() << "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))) { + qWarning() << "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 (data->values.value("expandedParentsCount").toInt() != parent->values.value("expandedParentsCount").toInt() + 1) { + qWarning() << "expandedParentsCount is inconsistent for parent" << parent->item << "and child" << data->item; + return false; + } + + const int parentIndex = index(parent->item); + if (parentIndex >= i) { + qWarning() << "Index" << parentIndex << "of parent" << parent->item << "is not smaller than index" << i << "of child" << data->item; + return false; + } + } + } + + return true; +} + #include "kfileitemmodel.moc" diff --git a/src/kitemviews/kfileitemmodel.h b/src/kitemviews/kfileitemmodel.h index ef9dc98b9..1d2d8c172 100644 --- a/src/kitemviews/kfileitemmodel.h +++ b/src/kitemviews/kfileitemmodel.h @@ -272,7 +272,7 @@ private slots: void slotCompleted(); void slotCanceled(); - void slotNewItems(const KFileItemList& items); + void slotItemsAdded(const KUrl& directoryUrl, const KFileItemList& items); void slotItemsDeleted(const KFileItemList& items); void slotRefreshItems(const QList<QPair<KFileItem, KFileItem> >& items); void slotClear(); @@ -303,8 +303,13 @@ private: ItemData* parent; }; - void insertItems(const KFileItemList& items); - void removeItems(const KFileItemList& items); + enum RemoveItemsBehavior { + KeepItemData, + DeleteItemData + }; + + void insertItems(QList<ItemData*>& items); + void removeItems(const KFileItemList& items, RemoveItemsBehavior behavior); /** * Helper method for insertItems() and removeItems(): Creates @@ -312,7 +317,7 @@ private: * Note that the ItemData instances are created dynamically and * must be deleted by the caller. */ - QList<ItemData*> createItemDataList(const KFileItemList& items) const; + QList<ItemData*> createItemDataList(const KUrl& parentUrl, const KFileItemList& items) const; void removeExpandedItems(); @@ -333,7 +338,7 @@ private: */ QByteArray roleForType(RoleType roleType) const; - QHash<QByteArray, QVariant> retrieveData(const KFileItem& item) const; + QHash<QByteArray, QVariant> retrieveData(const KFileItem& item, const ItemData* parent) const; /** * @return True if the item-data \a a should be ordered before the item-data @@ -342,6 +347,12 @@ private: bool lessThan(const ItemData* a, const ItemData* b) const; /** + * Sorts the items between \a begin and \a end using the comparison + * function lessThan(). + */ + void sort(QList<ItemData*>::iterator begin, QList<ItemData*>::iterator end) const; + + /** * Helper method for lessThan() and expandedParentsCountCompare(): Compares * the passed item-data using m_sortRole as criteria. Both items must * have the same parent item, otherwise the comparison will be wrong. @@ -350,22 +361,6 @@ private: int stringCompare(const QString& a, const QString& b) const; - /** - * Compares the expansion level of both items. The "expansion level" is defined - * by the number of parent directories. However simply comparing just the numbers - * is not sufficient, it is also important to check the hierarchy for having - * a correct order like shown in a tree. - */ - int expandedParentsCountCompare(const ItemData* a, const ItemData* b) const; - - /** - * Helper method for expandedParentsCountCompare(). - */ - QString subPath(const KFileItem& item, - const QString& itemPath, - int start, - bool* isDir) const; - bool useMaximumUpdateInterval() const; QList<QPair<int, QVariant> > nameRoleGroups() const; @@ -402,6 +397,12 @@ private: void applyFilters(); /** + * Removes filtered items whose expanded parents have been deleted + * or collapsed via setExpanded(parentIndex, false). + */ + void removeFilteredChildren(const KFileItemList& parentsList); + + /** * Maps the QByteArray-roles to RoleTypes and provides translation- and * group-contexts. */ @@ -428,6 +429,17 @@ private: */ static void determineMimeTypes(const KFileItemList& items, int timeout); + /** + * @return Returns a copy of \a value that is implicitly shared + * with other users to save memory. + */ + static QByteArray sharedValue(const QByteArray& value); + + /** + * Checks if the model's internal data structures are consistent. + */ + bool isConsistent() const; + private: KFileItemModelDirLister* m_dirLister; @@ -443,32 +455,17 @@ private: QHash<KUrl, int> m_items; // Allows O(1) access for KFileItemModel::index(const KFileItem& item) KFileItemModelFilter m_filter; - QSet<KFileItem> m_filteredItems; // Items that got hidden by KFileItemModel::setNameFilter() + QHash<KFileItem, ItemData*> m_filteredItems; // Items that got hidden by KFileItemModel::setNameFilter() bool m_requestRole[RolesCount]; QTimer* m_maximumUpdateIntervalTimer; QTimer* m_resortAllItemsTimer; - KFileItemList m_pendingItemsToInsert; + QList<ItemData*> m_pendingItemsToInsert; // Cache for KFileItemModel::groups() mutable QList<QPair<int, QVariant> > m_groups; - // Stores the smallest expansion level of the root-URL. Is required to calculate - // the "expandedParentsCount" role in an efficient way. A value < 0 indicates a - // special meaning: - enum ExpandedParentsCountRootTypes - { - // m_expandedParentsCountRoot is uninitialized and must be determined by checking - // the root URL from the KDirLister. - UninitializedExpandedParentsCountRoot = -1, - // All items should be forced to get an expanded parents count of 0 even if they - // represent child items. This is useful for slaves that provide no parent items - // for child items like e.g. the search IO slaves. - ForceExpandedParentsCountRoot = -2 - }; - mutable int m_expandedParentsCountRoot; - // Stores the URLs of the expanded directories. QSet<KUrl> m_expandedDirs; @@ -476,9 +473,10 @@ private: // and done step after step in slotCompleted(). QSet<KUrl> m_urlsToExpand; - friend class KFileItemModelSortAlgorithm; // Accesses lessThan() method + friend class KFileItemModelLessThan; // Accesses lessThan() method friend class KFileItemModelRolesUpdater; // Accesses emitSortProgress() method friend class KFileItemModelTest; // For unit testing + friend class KFileItemModelBenchmark; // For unit testing friend class KFileItemListViewTest; // For unit testing friend class DolphinPart; // Accesses m_dirLister }; diff --git a/src/kitemviews/kfileitemmodelrolesupdater.cpp b/src/kitemviews/kfileitemmodelrolesupdater.cpp index 7cade10f5..294778621 100644 --- a/src/kitemviews/kfileitemmodelrolesupdater.cpp +++ b/src/kitemviews/kfileitemmodelrolesupdater.cpp @@ -38,6 +38,8 @@ #include <QElapsedTimer> #include <QTimer> +#include <algorithm> + #ifdef HAVE_NEPOMUK #include "private/knepomukrolesprovider.h" #include <Nepomuk2/ResourceWatcher> @@ -58,34 +60,41 @@ namespace { // may perform a blocking operation const int MaxBlockTimeout = 200; - // Maximum number of items that will get resolved synchronously. - // The value should roughly represent the number of maximum visible - // items, as it does not make sense to resolve more items synchronously - // and probably reach the MaxBlockTimeout because of invisible items. - const int MaxResolveItemsCount = 100; + // 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_paused(false), + m_state(Idle), m_previewChangedDuringPausing(false), m_iconSizeChangedDuringPausing(false), m_rolesChangedDuringPausing(false), m_previewShown(false), m_enlargeSmallPreviews(true), m_clearPreviews(false), - m_sortingProgress(-1), + m_finishedItems(), m_model(model), m_iconSize(), m_firstVisibleIndex(0), m_lastVisibleIndex(-1), - m_maximumVisibleItems(100), + m_maximumVisibleItems(50), m_roles(), + m_resolvableRoles(), m_enabledPlugins(), - m_pendingVisibleItems(), - m_pendingInvisibleItems(), - m_previewJobs(), - m_changedItemsTimer(0), + m_pendingSortRoleItems(), + m_hasUnknownIcons(false), + m_firstIndexWithoutIcon(0), + m_pendingIndexes(), + m_pendingPreviewItems(), + m_previewJob(), + m_recentlyChangedItemsTimer(0), + m_recentlyChangedItems(), m_changedItems(), m_dirWatcher(0), m_watchedDirs() @@ -108,15 +117,17 @@ KFileItemModelRolesUpdater::KFileItemModelRolesUpdater(KFileItemModel* model, QO this, SLOT(slotItemsRemoved(KItemRangeList))); connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); + connect(m_model, SIGNAL(itemsMoved(KItemRange,QList<int>)), + this, SLOT(slotItemsMoved(KItemRange,QList<int>))); connect(m_model, SIGNAL(sortRoleChanged(QByteArray,QByteArray)), this, SLOT(slotSortRoleChanged(QByteArray,QByteArray))); // 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_changedItemsTimer = new QTimer(this); - m_changedItemsTimer->setInterval(1000); - m_changedItemsTimer->setSingleShot(true); - connect(m_changedItemsTimer, SIGNAL(timeout()), this, SLOT(resolveChangedItems())); + m_recentlyChangedItemsTimer = new QTimer(this); + m_recentlyChangedItemsTimer->setInterval(1000); + m_recentlyChangedItemsTimer->setSingleShot(true); + connect(m_recentlyChangedItemsTimer, SIGNAL(timeout()), this, SLOT(resolveRecentlyChangedItems())); m_resolvableRoles.insert("size"); m_resolvableRoles.insert("type"); @@ -133,21 +144,20 @@ KFileItemModelRolesUpdater::KFileItemModelRolesUpdater(KFileItemModel* model, QO KFileItemModelRolesUpdater::~KFileItemModelRolesUpdater() { - resetPendingRoles(); + killPreviewJob(); } void KFileItemModelRolesUpdater::setIconSize(const QSize& size) { if (size != m_iconSize) { m_iconSize = size; - if (m_paused) { + if (m_state == Paused) { m_iconSizeChangedDuringPausing = true; } else if (m_previewShown) { // An icon size change requires the regenerating of // all previews - sortAndResolveAllRoles(); - } else { - sortAndResolvePendingRoles(); + m_finishedItems.clear(); + startUpdating(); } } } @@ -174,9 +184,7 @@ void KFileItemModelRolesUpdater::setVisibleIndexRange(int index, int count) m_firstVisibleIndex = index; m_lastVisibleIndex = qMin(index + count - 1, m_model->count() - 1); - if (hasPendingRoles() && !m_paused) { - sortAndResolvePendingRoles(); - } + startUpdating(); } void KFileItemModelRolesUpdater::setMaximumVisibleItems(int count) @@ -230,31 +238,33 @@ void KFileItemModelRolesUpdater::setEnabledPlugins(const QStringList& list) void KFileItemModelRolesUpdater::setPaused(bool paused) { - if (paused == m_paused) { + if (paused == (m_state == Paused)) { return; } - m_paused = paused; if (paused) { - if (hasPendingRoles()) { - foreach (KJob* job, m_previewJobs) { - job->kill(); - } - Q_ASSERT(m_previewJobs.isEmpty()); - } + m_state = Paused; + killPreviewJob(); } else { - const bool resolveAll = (m_iconSizeChangedDuringPausing && m_previewShown) || - m_previewChangedDuringPausing || - m_rolesChangedDuringPausing; + const bool updatePreviews = (m_iconSizeChangedDuringPausing && m_previewShown) || + m_previewChangedDuringPausing; + const bool resolveAll = updatePreviews || m_rolesChangedDuringPausing; if (resolveAll) { - sortAndResolveAllRoles(); - } else { - sortAndResolvePendingRoles(); + 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(); } } @@ -292,12 +302,10 @@ void KFileItemModelRolesUpdater::setRoles(const QSet<QByteArray>& roles) } #endif - updateSortProgress(); - - if (m_paused) { + if (m_state == Paused) { m_rolesChangedDuringPausing = true; } else { - sortAndResolveAllRoles(); + startUpdating(); } } } @@ -309,7 +317,7 @@ QSet<QByteArray> KFileItemModelRolesUpdater::roles() const bool KFileItemModelRolesUpdater::isPaused() const { - return m_paused; + return m_state == Paused; } QStringList KFileItemModelRolesUpdater::enabledPlugins() const @@ -319,7 +327,41 @@ QStringList KFileItemModelRolesUpdater::enabledPlugins() const void KFileItemModelRolesUpdater::slotItemsInserted(const KItemRangeList& itemRanges) { - startUpdating(itemRanges); + QElapsedTimer timer; + timer.start(); + + const int firstInsertedIndex = itemRanges.first().index; + m_firstIndexWithoutIcon = qMin(m_firstIndexWithoutIcon, firstInsertedIndex); + m_hasUnknownIcons = true; + + // 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) @@ -328,6 +370,11 @@ void KFileItemModelRolesUpdater::slotItemsRemoved(const KItemRangeList& itemRang const bool allItemsRemoved = (m_model->count() == 0); + if (m_hasUnknownIcons) { + const int firstRemovedIndex = itemRanges.first().index; + m_firstIndexWithoutIcon = qMin(m_firstIndexWithoutIcon, firstRemovedIndex); + } + if (!m_watchedDirs.isEmpty()) { // Don't let KDirWatch watch for removed items if (allItemsRemoved) { @@ -375,35 +422,47 @@ void KFileItemModelRolesUpdater::slotItemsRemoved(const KItemRangeList& itemRang } #endif - m_firstVisibleIndex = 0; - m_lastVisibleIndex = -1; - if (!hasPendingRoles()) { - return; - } - if (allItemsRemoved) { - // Most probably a directory change is done. Clear all pending items - // and also kill all ongoing preview-jobs. - resetPendingRoles(); + m_state = Idle; + m_finishedItems.clear(); + m_pendingSortRoleItems.clear(); + m_pendingIndexes.clear(); + m_pendingPreviewItems.clear(); + m_recentlyChangedItems.clear(); + m_recentlyChangedItemsTimer->stop(); m_changedItems.clear(); - m_changedItemsTimer->stop(); + + killPreviewJob(); } else { - // Remove all items from m_pendingVisibleItems and m_pendingInvisibleItems - // that are not part of the model anymore. The items from m_changedItems - // don't need to be handled here, removed items are just skipped in - // resolveChangedItems(). - for (int i = 0; i <= 1; ++i) { - QSet<KFileItem>& pendingItems = (i == 0) ? m_pendingVisibleItems : m_pendingInvisibleItems; - QMutableSetIterator<KFileItem> it(pendingItems); - while (it.hasNext()) { - const KFileItem item = it.next(); - if (m_model->index(item) < 0) { - pendingItems.remove(item); - } + // Only remove the items from m_finishedItems. They will be removed + // from the other sets later on. + QSet<KFileItem>::iterator it = m_finishedItems.begin(); + while (it != m_finishedItems.end()) { + if (m_model->index(*it) < 0) { + it = m_finishedItems.erase(it); + } else { + ++it; } } + + // The visible items might have changed. + startUpdating(); + } +} + +void KFileItemModelRolesUpdater::slotItemsMoved(const KItemRange& itemRange, QList<int> movedToIndexes) +{ + Q_UNUSED(itemRange); + Q_UNUSED(movedToIndexes); + + if (m_hasUnknownIcons) { + const int firstMovedIndex = itemRange.index; + m_firstIndexWithoutIcon = qMin(m_firstIndexWithoutIcon, firstMovedIndex); } + + // The visible items might have changed. + startUpdating(); } void KFileItemModelRolesUpdater::slotItemsChanged(const KItemRangeList& itemRanges, @@ -411,21 +470,27 @@ void KFileItemModelRolesUpdater::slotItemsChanged(const KItemRangeList& itemRang { Q_UNUSED(roles); - if (m_changedItemsTimer->isActive()) { - // A call of slotItemsChanged() has been done recently. Postpone the resolving - // of the roles until the timer has exceeded. - foreach (const KItemRange& itemRange, itemRanges) { - int index = itemRange.index; - for (int count = itemRange.count; count > 0; --count) { - m_changedItems.insert(m_model->fileItem(index)); - ++index; - } + // 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<KFileItem>& 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; } - } else { - // No call of slotItemsChanged() has been done recently, resolve the roles now. - startUpdating(itemRanges); } - m_changedItemsTimer->start(); + + m_recentlyChangedItemsTimer->start(); + + if (!itemsChangedRecently) { + updateChangedItems(); + } } void KFileItemModelRolesUpdater::slotSortRoleChanged(const QByteArray& current, @@ -433,13 +498,46 @@ void KFileItemModelRolesUpdater::slotSortRoleChanged(const QByteArray& current, { Q_UNUSED(current); Q_UNUSED(previous); - updateSortProgress(); + + 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) { - m_pendingVisibleItems.remove(item); - m_pendingInvisibleItems.remove(item); + if (m_state != PreviewJobRunning) { + return; + } + + m_changedItems.remove(item); const int index = m_model->index(item); if (index < 0) { @@ -493,99 +591,147 @@ void KFileItemModelRolesUpdater::slotGotPreview(const KFileItem& item, const QPi connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); - applySortProgressToModel(); + m_finishedItems.insert(item); } void KFileItemModelRolesUpdater::slotPreviewFailed(const KFileItem& item) { - m_pendingVisibleItems.remove(item); - m_pendingInvisibleItems.remove(item); + if (m_state != PreviewJobRunning) { + return; + } + + m_changedItems.remove(item); + + const int index = m_model->index(item); + if (index >= 0) { + QHash<QByteArray, QVariant> data; + data.insert("iconPixmap", QPixmap()); - const bool clearPreviews = m_clearPreviews; - m_clearPreviews = true; - applyResolvedRoles(item, ResolveAll); - m_clearPreviews = clearPreviews; + disconnect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), + this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); + m_model->setData(index, data); + connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), + this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); - applySortProgressToModel(); + applyResolvedRoles(item, ResolveAll); + m_finishedItems.insert(item); + } } -void KFileItemModelRolesUpdater::slotPreviewJobFinished(KJob* job) +void KFileItemModelRolesUpdater::slotPreviewJobFinished() { -#ifdef KFILEITEMMODELROLESUPDATER_DEBUG - kDebug() << "Preview job finished. Pending visible:" << m_pendingVisibleItems.count() << "invisible:" << m_pendingInvisibleItems.count(); -#endif + m_previewJob = 0; - m_previewJobs.removeOne(job); - if (!m_previewJobs.isEmpty() || !hasPendingRoles()) { + if (m_state != PreviewJobRunning) { return; } - const KFileItemList visibleItems = sortedItems(m_pendingVisibleItems); - startPreviewJob(visibleItems + m_pendingInvisibleItems.toList()); + m_state = Idle; + + if (!m_pendingPreviewItems.isEmpty()) { + startPreviewJob(); + } else { + if (!m_changedItems.isEmpty()) { + updateChangedItems(); + } + } } -void KFileItemModelRolesUpdater::resolveNextPendingRoles() +void KFileItemModelRolesUpdater::resolveNextSortRole() { - if (m_paused) { + if (m_state != ResolvingSortRole) { return; } - if (m_previewShown) { - // The preview has been turned on since the last run. Skip - // resolving further pending roles as this is done as soon - // as a preview has been received. - return; - } + QSet<KFileItem>::iterator it = m_pendingSortRoleItems.begin(); + while (it != m_pendingSortRoleItems.end()) { + const KFileItem item = *it; + const int index = m_model->index(item); - int resolvedCount = 0; - bool changed = false; - for (int i = 0; i <= 1; ++i) { - QSet<KFileItem>& pendingItems = (i == 0) ? m_pendingVisibleItems : m_pendingInvisibleItems; - QSet<KFileItem>::iterator it = pendingItems.begin(); - while (it != pendingItems.end() && !changed && resolvedCount < MaxResolveItemsCount) { - changed = applyResolvedRoles(*it, ResolveAll); - it = pendingItems.erase(it); - ++resolvedCount; + // 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; } - } - if (hasPendingRoles()) { - QTimer::singleShot(0, this, SLOT(resolveNextPendingRoles())); - } else { - m_clearPreviews = false; + applySortRole(index); + m_pendingSortRoleItems.erase(it); + break; } - applySortProgressToModel(); + if (!m_pendingSortRoleItems.isEmpty()) { + applySortProgressToModel(); + QTimer::singleShot(0, this, SLOT(resolveNextSortRole())); + } else { + m_state = Idle; -#ifdef KFILEITEMMODELROLESUPDATER_DEBUG - static int callCount = 0; - ++callCount; - if (callCount % 100 == 0) { - kDebug() << "Remaining visible roles to resolve:" << m_pendingVisibleItems.count() - << "invisible:" << m_pendingInvisibleItems.count(); + // Prevent that we try to update the items twice. + disconnect(m_model, SIGNAL(itemsMoved(KItemRange,QList<int>)), + this, SLOT(slotItemsMoved(KItemRange,QList<int>))); + applySortProgressToModel(); + connect(m_model, SIGNAL(itemsMoved(KItemRange,QList<int>)), + this, SLOT(slotItemsMoved(KItemRange,QList<int>))); + startUpdating(); } -#endif } -void KFileItemModelRolesUpdater::resolveChangedItems() +void KFileItemModelRolesUpdater::resolveNextPendingRoles() { - if (m_changedItems.isEmpty()) { + if (m_state != ResolvingAllRoles) { return; } - KItemRangeList itemRanges; + while (!m_pendingIndexes.isEmpty()) { + const int index = m_pendingIndexes.takeFirst(); + const KFileItem item = m_model->fileItem(index); - QSetIterator<KFileItem> it(m_changedItems); - while (it.hasNext()) { - const KFileItem& item = it.next(); - const int index = m_model->index(item); - if (index >= 0) { - itemRanges.append(KItemRange(index, 1)); + if (m_finishedItems.contains(item)) { + continue; } + + applyResolvedRoles(item, ResolveAll); + m_finishedItems.insert(item); + m_changedItems.remove(item); + break; } - m_changedItems.clear(); - startUpdating(itemRanges); + if (!m_pendingIndexes.isEmpty()) { + QTimer::singleShot(0, this, SLOT(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<QByteArray, QVariant> data; + data.insert("iconPixmap", QPixmap()); + + disconnect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), + this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); + for (int index = 0; index <= m_model->count(); ++index) { + if (m_model->data(index).contains("iconPixmap")) { + m_model->setData(index, data); + } + } + connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), + this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); + + } + m_clearPreviews = false; + } + + if (!m_changedItems.isEmpty()) { + updateChangedItems(); + } + } +} + +void KFileItemModelRolesUpdater::resolveRecentlyChangedItems() +{ + m_changedItems += m_recentlyChangedItems; + m_recentlyChangedItems.clear(); + updateChangedItems(); } void KFileItemModelRolesUpdater::applyChangedNepomukRoles(const Nepomuk2::Resource& resource) @@ -647,366 +793,310 @@ void KFileItemModelRolesUpdater::slotDirWatchDirty(const QString& path) data.insert("isExpandable", count > 0); } + // Note that we do not block the itemsChanged signal here. + // This ensures that a new preview will be generated. m_model->setData(index, data); } } } -void KFileItemModelRolesUpdater::startUpdating(const KItemRangeList& itemRanges) +void KFileItemModelRolesUpdater::startUpdating() { - // If no valid index range is given assume that all items are visible. - // A cleanup will be done later as soon as the index range has been set. - const bool hasValidIndexRange = (m_lastVisibleIndex >= 0); + if (m_state == Paused) { + return; + } - if (hasValidIndexRange) { - // Move all current pending visible items that are not visible anymore - // to the pending invisible items. - QSet<KFileItem>::iterator it = m_pendingVisibleItems.begin(); - while (it != m_pendingVisibleItems.end()) { - const KFileItem item = *it; - const int index = m_model->index(item); - if (index < m_firstVisibleIndex || index > m_lastVisibleIndex) { - it = m_pendingVisibleItems.erase(it); - m_pendingInvisibleItems.insert(item); - } else { - ++it; - } - } + if (m_finishedItems.count() == m_model->count()) { + // All roles have been resolved already. + m_state = Idle; + return; } - int rangesCount = 0; + // Terminate all updates that are currently active. + killPreviewJob(); + m_pendingIndexes.clear(); - foreach (const KItemRange& range, itemRanges) { - rangesCount += range.count; + QElapsedTimer timer; + timer.start(); - // Add the inserted items to the pending visible and invisible items - const int lastIndex = range.index + range.count - 1; - for (int i = range.index; i <= lastIndex; ++i) { - const KFileItem item = m_model->fileItem(i); - bool visible; - if (hasValidIndexRange) { - visible = (i >= m_firstVisibleIndex && i <= m_lastVisibleIndex); - } else { - // If the view has not informed us about the visible range yet, - // just assume that the first items are visible. - visible = (i < m_maximumVisibleItems); - } + // Determine the icons for the visible items synchronously. + updateVisibleIcons(); - if (visible) { - m_pendingVisibleItems.insert(item); - } else { - m_pendingInvisibleItems.insert(item); + // Try to do at least a fast icon loading (without determining the + // mime type) for all items, to reduce the risk that the user sees + // "unknown" icons when scrolling. + if (m_hasUnknownIcons) { + updateAllIconsFast(MaxBlockTimeout - timer.elapsed()); + } + + // 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<int> 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); } } - } - resolvePendingRoles(); + startPreviewJob(); + } else { + m_pendingIndexes = indexes; + // Trigger the asynchronous resolving of all roles. + m_state = ResolvingAllRoles; + QTimer::singleShot(0, this, SLOT(resolveNextPendingRoles())); + } } -void KFileItemModelRolesUpdater::startPreviewJob(const KFileItemList& items) +void KFileItemModelRolesUpdater::updateVisibleIcons() { - if (items.isEmpty() || m_paused) { - return; + 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); + } } - // 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 the MIME-type of the items will determined until the MaxBlockTimeout - // has been reached and only those items will get passed. As soon as the MIME-type - // has been resolved once KIO::PreviewJob() can already access the resolved - // MIME-type in a fast way. QElapsedTimer timer; timer.start(); - KFileItemList itemSubSet; - const int count = items.count(); - itemSubSet.reserve(count); - for (int i = 0; i < count; ++i) { - KFileItem item = items.at(i); - item.determineMimeType(); - itemSubSet.append(item); - if (timer.elapsed() > MaxBlockTimeout) { -#ifdef KFILEITEMMODELROLESUPDATER_DEBUG - kDebug() << "Maximum time of" << MaxBlockTimeout << "ms exceeded, creating only previews for" - << (i + 1) << "items," << (items.count() - (i + 1)) << "will be resolved later"; -#endif - break; - } - } - KIO::PreviewJob* job = new KIO::PreviewJob(itemSubSet, cacheSize, &m_enabledPlugins); - job->setIgnoreMaximumSize(items.first().isLocalFile()); - if (job->ui()) { - job->ui()->setWindow(qApp->activeWindow()); + // Try to determine the final icons for all visible items. + int index; + for (index = m_firstVisibleIndex; index <= lastVisibleIndex && timer.elapsed() < MaxBlockTimeout; ++index) { + const KFileItem item = m_model->fileItem(index); + applyResolvedRoles(item, ResolveFast); } - connect(job, SIGNAL(gotPreview(KFileItem,QPixmap)), - this, SLOT(slotGotPreview(KFileItem,QPixmap))); - connect(job, SIGNAL(failed(KFileItem)), - this, SLOT(slotPreviewFailed(KFileItem))); - connect(job, SIGNAL(finished(KJob*)), - this, SLOT(slotPreviewJobFinished(KJob*))); + if (index > lastVisibleIndex) { + return; + } - m_previewJobs.append(job); -} + // If this didn't work before MaxBlockTimeout was reached, at least + // prevent that the user sees 'unknown' icons. + disconnect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), + this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); + while (index <= lastVisibleIndex) { + if (!m_model->data(index).contains("iconName")) { + const KFileItem item = m_model->fileItem(index); + QHash<QByteArray, QVariant> data; + data.insert("iconName", item.iconName()); + m_model->setData(index, data); + } + ++index; + } -bool KFileItemModelRolesUpdater::hasPendingRoles() const -{ - return !m_pendingVisibleItems.isEmpty() || !m_pendingInvisibleItems.isEmpty(); + connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), + this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); } -void KFileItemModelRolesUpdater::resolvePendingRoles() +void KFileItemModelRolesUpdater::updateAllIconsFast(int timeout) { - int resolvedCount = 0; - - bool hasSlowRoles = m_previewShown; - if (!hasSlowRoles) { - QSetIterator<QByteArray> it(m_roles); - while (it.hasNext()) { - if (m_resolvableRoles.contains(it.next())) { - hasSlowRoles = true; - break; - } - } + if (timeout <= 0) { + return; } - const ResolveHint resolveHint = hasSlowRoles ? ResolveFast : ResolveAll; - - // Resolving the MIME type can be expensive. Assure that not more than MaxBlockTimeout ms are - // spend for resolving them synchronously. Usually this is more than enough to determine - // all visible items, but there are corner cases where this limit gets easily exceeded. QElapsedTimer timer; timer.start(); - // Resolve the MIME type of all visible items - QSet<KFileItem>::iterator visibleIt = m_pendingVisibleItems.begin(); - while (visibleIt != m_pendingVisibleItems.end()) { - const KFileItem item = *visibleIt; - if (!hasSlowRoles) { - Q_ASSERT(!m_pendingInvisibleItems.contains(item)); - // All roles will be resolved by applyResolvedRoles() - visibleIt = m_pendingVisibleItems.erase(visibleIt); - } else { - ++visibleIt; - } - applyResolvedRoles(item, resolveHint); - ++resolvedCount; + disconnect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), + this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); - if (timer.elapsed() > MaxBlockTimeout) { - break; + const int count = m_model->count(); + while (m_firstIndexWithoutIcon < count && timer.elapsed() < timeout) { + if (!m_model->data(m_firstIndexWithoutIcon).contains("iconName")) { + const KFileItem item = m_model->fileItem(m_firstIndexWithoutIcon); + QHash<QByteArray, QVariant> data; + data.insert("iconName", item.iconName()); + m_model->setData(m_firstIndexWithoutIcon, data); } + ++m_firstIndexWithoutIcon; } - // Resolve the MIME type of the invisible items at least until the timeout - // has been exceeded or the maximum number of items has been reached - KFileItemList invisibleItems; - if (m_lastVisibleIndex >= 0) { - // The visible range is valid, don't care about the order how the MIME - // type of invisible items get resolved - invisibleItems = m_pendingInvisibleItems.toList(); - } else { - // The visible range is temporary invalid (e.g. happens when loading - // a directory) so take care to sort the currently invisible items where - // a part will get visible later - invisibleItems = sortedItems(m_pendingInvisibleItems); + if (m_firstIndexWithoutIcon == count) { + m_hasUnknownIcons = false; } - int index = 0; - while (resolvedCount < MaxResolveItemsCount && index < invisibleItems.count() && timer.elapsed() <= MaxBlockTimeout) { - const KFileItem item = invisibleItems.at(index); - applyResolvedRoles(item, resolveHint); + connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), + this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); +} - if (!hasSlowRoles) { - // All roles have been resolved already by applyResolvedRoles() - m_pendingInvisibleItems.remove(item); - } - ++index; - ++resolvedCount; +void KFileItemModelRolesUpdater::startPreviewJob() +{ + m_state = PreviewJobRunning; + + if (m_pendingPreviewItems.isEmpty()) { + QTimer::singleShot(0, this, SLOT(slotPreviewJobFinished())); + return; } - if (m_previewShown) { - KFileItemList items = sortedItems(m_pendingVisibleItems); - items += invisibleItems; - startPreviewJob(items); + // 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 { - QTimer::singleShot(0, this, SLOT(resolveNextPendingRoles())); - } + // Determine mime types for MaxBlockTimeout ms, and start a preview + // job for the corresponding items. + QElapsedTimer timer; + timer.start(); -#ifdef KFILEITEMMODELROLESUPDATER_DEBUG - if (timer.elapsed() > MaxBlockTimeout) { - kDebug() << "Maximum time of" << MaxBlockTimeout - << "ms exceeded, skipping items... Remaining visible:" << m_pendingVisibleItems.count() - << "invisible:" << m_pendingInvisibleItems.count(); + do { + const KFileItem item = m_pendingPreviewItems.takeFirst(); + item.determineMimeType(); + itemSubSet.append(item); + } while (!m_pendingPreviewItems.isEmpty() && timer.elapsed() < MaxBlockTimeout); } - kDebug() << "[TIME] Resolved pending roles:" << timer.elapsed(); -#endif - applySortProgressToModel(); -} - -void KFileItemModelRolesUpdater::resetPendingRoles() -{ - m_pendingVisibleItems.clear(); - m_pendingInvisibleItems.clear(); + KIO::PreviewJob* job = new KIO::PreviewJob(itemSubSet, cacheSize, &m_enabledPlugins); - foreach (KJob* job, m_previewJobs) { - job->kill(); + job->setIgnoreMaximumSize(itemSubSet.first().isLocalFile()); + if (job->ui()) { + job->ui()->setWindow(qApp->activeWindow()); } - Q_ASSERT(m_previewJobs.isEmpty()); + + connect(job, SIGNAL(gotPreview(KFileItem,QPixmap)), + this, SLOT(slotGotPreview(KFileItem,QPixmap))); + connect(job, SIGNAL(failed(KFileItem)), + this, SLOT(slotPreviewFailed(KFileItem))); + connect(job, SIGNAL(finished(KJob*)), + this, SLOT(slotPreviewJobFinished())); + + m_previewJob = job; } -void KFileItemModelRolesUpdater::sortAndResolveAllRoles() +void KFileItemModelRolesUpdater::updateChangedItems() { - if (m_paused) { + if (m_state == Paused) { return; } - resetPendingRoles(); - Q_ASSERT(m_pendingVisibleItems.isEmpty()); - Q_ASSERT(m_pendingInvisibleItems.isEmpty()); - - if (m_model->count() == 0) { + if (m_changedItems.isEmpty()) { return; } - // Determine all visible items - Q_ASSERT(m_firstVisibleIndex >= 0); - for (int i = m_firstVisibleIndex; i <= m_lastVisibleIndex; ++i) { - const KFileItem item = m_model->fileItem(i); - if (!item.isNull()) { - m_pendingVisibleItems.insert(item); - } - } + m_finishedItems -= m_changedItems; - // Determine all invisible items - for (int i = 0; i < m_firstVisibleIndex; ++i) { - const KFileItem item = m_model->fileItem(i); - if (!item.isNull()) { - m_pendingInvisibleItems.insert(item); - } - } - const int count = m_model->count(); - for (int i = m_lastVisibleIndex + 1; i < count; ++i) { - const KFileItem item = m_model->fileItem(i); - if (!item.isNull()) { - m_pendingInvisibleItems.insert(item); - } - } + if (m_resolvableRoles.contains(m_model->sortRole())) { + m_pendingSortRoleItems += m_changedItems; - resolvePendingRoles(); -} + 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, SLOT(resolveNextSortRole())); + } -void KFileItemModelRolesUpdater::sortAndResolvePendingRoles() -{ - Q_ASSERT(!m_paused); - if (m_model->count() == 0) { return; } - // If no valid index range is given assume that all items are visible. - // A cleanup will be done later as soon as the index range has been set. - const bool hasValidIndexRange = (m_lastVisibleIndex >= 0); + QList<int> visibleChangedIndexes; + QList<int> invisibleChangedIndexes; - // Trigger a preview generation of all pending items. Assure that the visible - // pending items get generated first. + foreach (const KFileItem& item, m_changedItems) { + const int index = m_model->index(item); - // Step 1: Check if any items in m_pendingVisibleItems are not visible any more - // and move them to m_pendingInvisibleItems. - QSet<KFileItem>::iterator itVisible = m_pendingVisibleItems.begin(); - while (itVisible != m_pendingVisibleItems.end()) { - const KFileItem item = *itVisible; - if (item.isNull()) { - itVisible = m_pendingVisibleItems.erase(itVisible); + if (index < 0) { + m_changedItems.remove(item); continue; } - const int index = m_model->index(item); - if (!hasValidIndexRange || (index >= m_firstVisibleIndex && index <= m_lastVisibleIndex)) { - ++itVisible; + if (index >= m_firstVisibleIndex && index <= m_lastVisibleIndex) { + visibleChangedIndexes.append(index); } else { - itVisible = m_pendingVisibleItems.erase(itVisible); - m_pendingInvisibleItems.insert(item); + invisibleChangedIndexes.append(index); } } - // Step 2: Check if any items in m_pendingInvisibleItems have become visible - // and move them to m_pendingVisibleItems. - QSet<KFileItem>::iterator itInvisible = m_pendingInvisibleItems.begin(); - while (itInvisible != m_pendingInvisibleItems.end()) { - const KFileItem item = *itInvisible; - if (item.isNull()) { - itInvisible = m_pendingInvisibleItems.erase(itInvisible); - continue; - } + std::sort(visibleChangedIndexes.begin(), visibleChangedIndexes.end()); - const int index = m_model->index(item); - if (!hasValidIndexRange || (index >= m_firstVisibleIndex && index <= m_lastVisibleIndex)) { - itInvisible = m_pendingInvisibleItems.erase(itInvisible); - m_pendingVisibleItems.insert(item); - } else { - ++itInvisible; + if (m_previewShown) { + foreach (int index, visibleChangedIndexes) { + m_pendingPreviewItems.append(m_model->fileItem(index)); } - } - - resolvePendingRoles(); -} -void KFileItemModelRolesUpdater::applySortProgressToModel() -{ - if (m_sortingProgress < 0) { - return; - } + foreach (int index, invisibleChangedIndexes) { + m_pendingPreviewItems.append(m_model->fileItem(index)); + } - // 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_pendingVisibleItems.count() - - m_pendingInvisibleItems.count(); - if (resolvedCount > 0) { - m_model->emitSortProgress(resolvedCount); - if (resolvedCount == m_model->count()) { - m_sortingProgress = -1; + 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, SLOT(resolveNextPendingRoles())); } } } -void KFileItemModelRolesUpdater::updateSortProgress() +void KFileItemModelRolesUpdater::applySortRole(int index) { - const QByteArray sortRole = m_model->sortRole(); + QHash<QByteArray, QVariant> data; + const KFileItem item = m_model->fileItem(index); - // Optimization if the sorting is done by type: In case if all MIME-types - // are known, the types have been resolved already by KFileItemModel and - // no sort-progress feedback is required. - const bool showProgress = (sortRole == "type") - ? hasUnknownMimeTypes() - : m_resolvableRoles.contains(sortRole); + if (m_model->sortRole() == "type") { + if (!item.isMimeTypeKnown()) { + item.determineMimeType(); + } - if (m_sortingProgress >= 0) { - // Mark the current sorting as finished - m_model->emitSortProgress(m_model->count()); + data.insert("type", item.mimeComment()); + } else if (m_model->sortRole() == "size" && item.isLocalFile() && item.isDir()) { + const QString path = item.localPath(); + data.insert("size", subItemsCount(path)); + } else { + // Probably the sort role is a Nepomuk role - just determine all roles. + data = rolesData(item); } - m_sortingProgress = showProgress ? 0 : -1; + + disconnect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), + this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); + m_model->setData(index, data); + connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), + this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); } -bool KFileItemModelRolesUpdater::hasUnknownMimeTypes() const +void KFileItemModelRolesUpdater::applySortProgressToModel() { - const int count = m_model->count(); - for (int i = 0; i < count; ++i) { - const KFileItem item = m_model->fileItem(i); - if (!item.isMimeTypeKnown()) { - return true; - } - } - - return false; + // 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(const KFileItem& item, ResolveHint hint) @@ -1017,13 +1107,18 @@ bool KFileItemModelRolesUpdater::applyResolvedRoles(const KFileItem& item, Resol const bool resolveAll = (hint == ResolveAll); - bool mimeTypeChanged = false; + bool iconChanged = false; if (!item.isMimeTypeKnown() || !item.isFinalIconKnown()) { item.determineMimeType(); - mimeTypeChanged = true; + iconChanged = true; + } else { + const int index = m_model->index(item); + if (!m_model->data(index).contains("iconName")) { + iconChanged = true; + } } - if (mimeTypeChanged || resolveAll || m_clearPreviews) { + if (iconChanged || resolveAll || m_clearPreviews) { const int index = m_model->index(item); if (index < 0) { return false; @@ -1116,42 +1211,6 @@ QHash<QByteArray, QVariant> KFileItemModelRolesUpdater::rolesData(const KFileIte return data; } -KFileItemList KFileItemModelRolesUpdater::sortedItems(const QSet<KFileItem>& items) const -{ - KFileItemList itemList; - if (items.isEmpty()) { - return itemList; - } - -#ifdef KFILEITEMMODELROLESUPDATER_DEBUG - QElapsedTimer timer; - timer.start(); -#endif - - QList<int> indexes; - indexes.reserve(items.count()); - - QSetIterator<KFileItem> it(items); - while (it.hasNext()) { - const KFileItem item = it.next(); - const int index = m_model->index(item); - if (index >= 0) { - indexes.append(index); - } - } - qSort(indexes); - - itemList.reserve(items.count()); - foreach (int index, indexes) { - itemList.append(m_model->fileItem(index)); - } - -#ifdef KFILEITEMMODELROLESUPDATER_DEBUG - kDebug() << "[TIME] Sorting of items:" << timer.elapsed(); -#endif - return itemList; -} - int KFileItemModelRolesUpdater::subItemsCount(const QString& path) const { const bool countHiddenFiles = m_model->showHiddenFiles(); @@ -1209,11 +1268,84 @@ int KFileItemModelRolesUpdater::subItemsCount(const QString& path) const void KFileItemModelRolesUpdater::updateAllPreviews() { - if (m_paused) { + if (m_state == Paused) { m_previewChangedDuringPausing = true; } else { - sortAndResolveAllRoles(); + m_finishedItems.clear(); + startUpdating(); + } +} + +void KFileItemModelRolesUpdater::killPreviewJob() +{ + if (m_previewJob) { + disconnect(m_previewJob, SIGNAL(gotPreview(KFileItem,QPixmap)), + this, SLOT(slotGotPreview(KFileItem,QPixmap))); + disconnect(m_previewJob, SIGNAL(failed(KFileItem)), + this, SLOT(slotPreviewFailed(KFileItem))); + disconnect(m_previewJob, SIGNAL(finished(KJob*)), + this, SLOT(slotPreviewJobFinished())); + m_previewJob->kill(); + m_previewJob = 0; + m_pendingPreviewItems.clear(); } } +QList<int> KFileItemModelRolesUpdater::indexesToResolve() const +{ + const int count = m_model->count(); + + QList<int> 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 Compace 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; +} + #include "kfileitemmodelrolesupdater.moc" diff --git a/src/kitemviews/kfileitemmodelrolesupdater.h b/src/kitemviews/kfileitemmodelrolesupdater.h index b837e8c7f..20ce21cfa 100644 --- a/src/kitemviews/kfileitemmodelrolesupdater.h +++ b/src/kitemviews/kfileitemmodelrolesupdater.h @@ -59,6 +59,38 @@ class QTimer; * KFileItemModel only resolves roles that are inexpensive like e.g. the file name or * the permissions. Creating previews or determining the MIME-type can be quite expensive * and KFileItemModelRolesUpdater takes care to update such roles asynchronously. + * + * To prevent a huge CPU and I/O load, these roles are not updated for all + * items, but only for the visible items, some items around the visible area, + * and the items on the first and last pages of the view. This is a compromise + * that aims to minimize the risk that the user sees items with unknown icons + * in the view when scrolling or pressing Home or End. + * + * Determining the roles is done in several phases: + * + * 1. If the sort role is "slow", it is determined for all items. If this + * cannot be finished synchronously in 200 ms, the remaining items are + * handled asynchronously by \a resolveNextSortRole(). + * + * 2. The function startUpdating(), which is called if either the sort role + * has been successfully determined for all items, or items are inserted + * in the view, or the visible items might have changed because items + * were removed or moved, tries to determine the icons for all visible + * items synchronously for 200 ms. Then: + * + * (a) If previews are disabled, icons and all other roles are determined + * asynchronously for the interesting items. This is done by the + * function \a resolveNextPendingRoles(). + * + * (b) If previews are enabled, a \a KIO::PreviewJob is started that loads + * the previews for the interesting items. At the same time, the icons + * for these items are determined asynchronously as fast as possible + * by \a resolveNextPendingRoles(). This minimizes the risk that the + * user sees "unknown" icons when scrolling before the previews have + * arrived. + * + * 3. Finally, the entire process is repeated for any items that might have + * changed in the mean time. */ class LIBDOLPHINPRIVATE_EXPORT KFileItemModelRolesUpdater : public QObject { @@ -129,6 +161,7 @@ public: private slots: void slotItemsInserted(const KItemRangeList& itemRanges); void slotItemsRemoved(const KItemRangeList& itemRanges); + void slotItemsMoved(const KItemRange& itemRange, QList<int> movedToIndexes); void slotItemsChanged(const KItemRangeList& itemRanges, const QSet<QByteArray>& roles); void slotSortRoleChanged(const QByteArray& current, @@ -147,20 +180,33 @@ private slots: void slotPreviewFailed(const KFileItem& item); /** - * Is invoked when the preview job has been finished and - * removes the job from the m_previewJobs list. + * Is invoked when the preview job has been finished. Starts a new preview + * job if there are any interesting items without previews left, or updates + * the changed items otherwise. * * @see startPreviewJob() */ - void slotPreviewJobFinished(KJob* job); + void slotPreviewJobFinished(); + + /** + * Resolves the sort role of the next item in m_pendingSortRole, applies it + * to the model, and invokes itself if there are any pending items left. If + * that is not the case, \a startUpdating() is called. + */ + void resolveNextSortRole(); + /** + * Resolves the icon name and (if previews are disabled) all other roles + * for the next interesting item. If there are no pending items left, any + * changed items are updated. + */ void resolveNextPendingRoles(); /** * Resolves items that have not been resolved yet after the change has been * notified by slotItemsChanged(). Is invoked if the m_changedItemsTimer - * exceeds. + * expires. */ - void resolveChangedItems(); + void resolveRecentlyChangedItems(); void applyChangedNepomukRoles(const Nepomuk2::Resource& resource); @@ -173,41 +219,46 @@ private slots: private: /** - * Updates the roles for the given item ranges. The roles for the currently - * visible items will get updated first. + * Starts the updating of all roles. The visible items are handled first. + */ + void startUpdating(); + + /** + * Loads the icons for the visible items. After 200 ms, the function + * stops determining mime types and only loads preliminary icons. + * This is a compromise that prevents that + * (a) the GUI is blocked for more than 200 ms, and + * (b) "unknown" icons could be shown in the view. + */ + void updateVisibleIcons(); + + /** + * Tries to load at least preliminary icons (without determining the + * mime type) for all items for \a timeout milliseconds. */ - void startUpdating(const KItemRangeList& itemRanges); + void updateAllIconsFast(int timeout); /** - * Creates previews for the items starting from the first item of the - * given list. + * Creates previews for the items starting from the first item in + * m_pendingPreviewItems. * @see slotGotPreview() * @see slotPreviewFailed() * @see slotPreviewJobFinished() */ - void startPreviewJob(const KFileItemList& items); - - bool hasPendingRoles() const; - void resolvePendingRoles(); - void resetPendingRoles(); - void sortAndResolveAllRoles(); - void sortAndResolvePendingRoles(); - void applySortProgressToModel(); + void startPreviewJob(); /** - * Updates m_sortProgress to be 0 if the sort-role - * needs to get resolved asynchronously and hence a - * progress is required. Otherwise m_sortProgress - * will be set to -1 which means that no progress - * will be provided. + * Ensures that icons, previews, and other roles are determined for any + * items that have been changed. */ - void updateSortProgress(); + void updateChangedItems(); /** - * @return True, if at least one item from the model - * has an unknown MIME-type. + * Resolves the sort role of the item and applies it to the model. */ - bool hasUnknownMimeTypes() const; + void applySortRole(int index); + + void applySortProgressToModel(); enum ResolveHint { ResolveFast, @@ -216,8 +267,6 @@ private: bool applyResolvedRoles(const KFileItem& item, ResolveHint hint); QHash<QByteArray, QVariant> rolesData(const KFileItem& item) const; - KFileItemList sortedItems(const QSet<KFileItem>& items) const; - /** * @return The number of items of the path \a path. */ @@ -229,9 +278,20 @@ private: */ void updateAllPreviews(); + void killPreviewJob(); + + QList<int> indexesToResolve() const; + private: - // Property for setPaused()/isPaused(). - bool m_paused; + enum State { + Idle, + Paused, + ResolvingSortRole, + ResolvingAllRoles, + PreviewJobRunning + }; + + State m_state; // Property changes during pausing must be remembered to be able // to react when unpausing again: @@ -250,7 +310,9 @@ private: // during the roles-updater has been paused by setPaused(). bool m_clearPreviews; - int m_sortingProgress; + // Remembers which items have been handled already, to prevent that + // previews and other expensive roles are determined again. + QSet<KFileItem> m_finishedItems; KFileItemModel* m_model; QSize m_iconSize; @@ -261,16 +323,33 @@ private: QSet<QByteArray> m_resolvableRoles; QStringList m_enabledPlugins; - QSet<KFileItem> m_pendingVisibleItems; - QSet<KFileItem> m_pendingInvisibleItems; - QList<KJob*> m_previewJobs; + // Items for which the sort role still has to be determined. + QSet<KFileItem> m_pendingSortRoleItems; + + // Determines if the next call of startUpdating() will try to do a fast + // icon loading (i.e., without determining the mime type) for all items. + bool m_hasUnknownIcons; + int m_firstIndexWithoutIcon; + + // Indexes of items which still have to be handled by + // resolveNextPendingRoles(). + QList<int> m_pendingIndexes; + + // Items which have been left over from the last call of startPreviewJob(). + // A new preview job will be started from them once the first one finishes. + KFileItemList m_pendingPreviewItems; + + KJob* m_previewJob; // When downloading or copying large files, the slot slotItemsChanged() // will be called periodically within a quite short delay. To prevent // a high CPU-load by generating e.g. previews for each notification, the update // will be postponed until no file change has been done within a longer period // of time. - QTimer* m_changedItemsTimer; + QTimer* m_recentlyChangedItemsTimer; + QSet<KFileItem> m_recentlyChangedItems; + + // Items which have not been changed repeatedly recently. QSet<KFileItem> m_changedItems; KDirWatch* m_dirWatcher; diff --git a/src/kitemviews/kitemlistcontroller.cpp b/src/kitemviews/kitemlistcontroller.cpp index c6239df94..4629b29f1 100644 --- a/src/kitemviews/kitemlistcontroller.cpp +++ b/src/kitemviews/kitemlistcontroller.cpp @@ -838,27 +838,39 @@ bool KItemListController::dragMoveEvent(QGraphicsSceneDragDropEvent* event, cons 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); - } + 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 (!droppingBetweenItems && m_model->supportsDropping(index)) { + const int index = newHoveredWidget->index(); + if (!droppingBetweenItems) { + if (m_model->supportsDropping(index)) { // Something has been dragged on an item. m_view->hideDropIndicator(); - newHoveredWidget->setHovered(true); - emit itemHovered(index); + if (!newHoveredWidget->isHovered()) { + newHoveredWidget->setHovered(true); + emit itemHovered(index); + } - if (m_autoActivationTimer->interval() >= 0) { + 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(); } return false; diff --git a/src/kitemviews/kitemlistview.cpp b/src/kitemviews/kitemlistview.cpp index a2629c565..b5e105843 100644 --- a/src/kitemviews/kitemlistview.cpp +++ b/src/kitemviews/kitemlistview.cpp @@ -678,6 +678,16 @@ void KItemListView::paint(QPainter* painter, const QStyleOptionGraphicsItem* opt } } +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; @@ -2365,7 +2375,8 @@ int KItemListView::showDropIndicator(const QPointF& pos) const QRectF rect = itemRect(widget->index()); if (mappedPos.y() >= 0 && mappedPos.y() <= rect.height()) { if (m_model->supportsDropping(widget->index())) { - const int gap = qMax(4, m_styleOption.padding); + // Keep 30% of the rectangle as the gap instead of always having a fixed gap + const int gap = qMax(4.0, 0.3 * rect.height()); if (mappedPos.y() >= gap && mappedPos.y() <= rect.height() - gap) { return -1; } diff --git a/src/kitemviews/kitemlistview.h b/src/kitemviews/kitemlistview.h index 6d609a9df..6467b8c91 100644 --- a/src/kitemviews/kitemlistview.h +++ b/src/kitemviews/kitemlistview.h @@ -321,6 +321,7 @@ signals: void roleEditingFinished(int index, const QByteArray& role, const QVariant& value); protected: + virtual QVariant itemChange(GraphicsItemChange change, const QVariant &value); void setItemSize(const QSizeF& size); void setStyleOption(const KItemListStyleOption& option); diff --git a/src/kitemviews/private/kfileitemmodelsortalgorithm.cpp b/src/kitemviews/private/kfileitemmodelsortalgorithm.cpp deleted file mode 100644 index ab650efea..000000000 --- a/src/kitemviews/private/kfileitemmodelsortalgorithm.cpp +++ /dev/null @@ -1,190 +0,0 @@ -/*************************************************************************** - * Copyright (C) 2012 by Peter Penz <[email protected]> * - * * - * 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 "kfileitemmodelsortalgorithm.h" - -#include <QThread> -#include <QtCore> - -void KFileItemModelSortAlgorithm::sort(KFileItemModel* model, - QList<KFileItemModel::ItemData*>::iterator begin, - QList<KFileItemModel::ItemData*>::iterator end) -{ - if (model->sortRole() == model->roleForType(KFileItemModel::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(); - parallelSort(model, begin, end, 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 - sequentialSort(model, begin, end); - } -} - -void KFileItemModelSortAlgorithm::sequentialSort(KFileItemModel* model, - QList< KFileItemModel::ItemData* >::iterator begin, - QList< KFileItemModel::ItemData* >::iterator end) -{ - // The implementation is based on qStableSortHelper() from qalgorithms.h - // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). - - const int span = end - begin; - if (span < 2) { - return; - } - - const QList<KFileItemModel::ItemData*>::iterator middle = begin + span / 2; - sequentialSort(model, begin, middle); - sequentialSort(model, middle, end); - merge(model, begin, middle, end); -} - -void KFileItemModelSortAlgorithm::parallelSort(KFileItemModel* model, - QList< KFileItemModel::ItemData* >::iterator begin, - QList< KFileItemModel::ItemData* >::iterator end, - const int numberOfThreads) -{ - const int span = end - begin; - - if (numberOfThreads > 1 && span > 100) { - const int newNumberOfThreads = numberOfThreads / 2; - const QList<KFileItemModel::ItemData*>::iterator middle = begin + span / 2; - - QFuture<void> future = QtConcurrent::run(parallelSort, model, begin, middle, newNumberOfThreads); - parallelSort(model, middle, end, newNumberOfThreads); - - future.waitForFinished(); - - merge(model, begin, middle, end); - } else { - sequentialSort(model, begin, end); - } -} - -void KFileItemModelSortAlgorithm::merge(KFileItemModel* model, - QList<KFileItemModel::ItemData*>::iterator begin, - QList<KFileItemModel::ItemData*>::iterator pivot, - QList<KFileItemModel::ItemData*>::iterator end) -{ - // The implementation is based on qMerge() from qalgorithms.h - // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). - - const int len1 = pivot - begin; - const int len2 = end - pivot; - - if (len1 == 0 || len2 == 0) { - return; - } - - if (len1 + len2 == 2) { - if (model->lessThan(*(begin + 1), *(begin))) { - qSwap(*begin, *(begin + 1)); - } - return; - } - - QList<KFileItemModel::ItemData*>::iterator firstCut; - QList<KFileItemModel::ItemData*>::iterator secondCut; - int len2Half; - if (len1 > len2) { - const int len1Half = len1 / 2; - firstCut = begin + len1Half; - secondCut = lowerBound(model, pivot, end, *firstCut); - len2Half = secondCut - pivot; - } else { - len2Half = len2 / 2; - secondCut = pivot + len2Half; - firstCut = upperBound(model, begin, pivot, *secondCut); - } - - reverse(firstCut, pivot); - reverse(pivot, secondCut); - reverse(firstCut, secondCut); - - const QList<KFileItemModel::ItemData*>::iterator newPivot = firstCut + len2Half; - merge(model, begin, firstCut, newPivot); - merge(model, newPivot, secondCut, end); -} - - -QList<KFileItemModel::ItemData*>::iterator -KFileItemModelSortAlgorithm::lowerBound(KFileItemModel* model, - QList<KFileItemModel::ItemData*>::iterator begin, - QList<KFileItemModel::ItemData*>::iterator end, - const KFileItemModel::ItemData* value) -{ - // The implementation is based on qLowerBound() from qalgorithms.h - // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). - - QList<KFileItemModel::ItemData*>::iterator middle; - int n = int(end - begin); - int half; - - while (n > 0) { - half = n >> 1; - middle = begin + half; - if (model->lessThan(*middle, value)) { - begin = middle + 1; - n -= half + 1; - } else { - n = half; - } - } - return begin; -} - -QList<KFileItemModel::ItemData*>::iterator -KFileItemModelSortAlgorithm::upperBound(KFileItemModel* model, - QList<KFileItemModel::ItemData*>::iterator begin, - QList<KFileItemModel::ItemData*>::iterator end, - const KFileItemModel::ItemData* value) -{ - // The implementation is based on qUpperBound() from qalgorithms.h - // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). - - QList<KFileItemModel::ItemData*>::iterator middle; - int n = end - begin; - int half; - - while (n > 0) { - half = n >> 1; - middle = begin + half; - if (model->lessThan(value, *middle)) { - n = half; - } else { - begin = middle + 1; - n -= half + 1; - } - } - return begin; -} - -void KFileItemModelSortAlgorithm::reverse(QList<KFileItemModel::ItemData*>::iterator begin, - QList<KFileItemModel::ItemData*>::iterator end) -{ - // The implementation is based on qReverse() from qalgorithms.h - // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). - - --end; - while (begin < end) { - qSwap(*begin++, *end--); - } -} diff --git a/src/kitemviews/private/kfileitemmodelsortalgorithm.h b/src/kitemviews/private/kfileitemmodelsortalgorithm.h index 07e5d4a81..1d5689432 100644 --- a/src/kitemviews/private/kfileitemmodelsortalgorithm.h +++ b/src/kitemviews/private/kfileitemmodelsortalgorithm.h @@ -1,79 +1,143 @@ -/*************************************************************************** - * Copyright (C) 2012 by Peter Penz <[email protected]> * - * * - * 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 * - ***************************************************************************/ +/***************************************************************************** + * Copyright (C) 2012 by Peter Penz <[email protected]> * + * Copyright (C) 2012 by Emmanuel Pescosta <[email protected]> * + * Copyright (C) 2013 by Frank Reininghaus <[email protected]> * + * * + * 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 KFILEITEMMODELSORTALGORITHM_H #define KFILEITEMMODELSORTALGORITHM_H -#include <libdolphin_export.h> +#include <QtCore> -#include <kitemviews/kfileitemmodel.h> +#include <algorithm> /** - * @brief Sort algorithm for sorting items of KFileItemModel. - * - * Sorts the items by using KFileItemModel::lessThan() as comparison criteria. - * The merge sort algorithm is used to assure a worst-case - * of O(n * log(n)) and to keep the number of comparisons low. + * Sorts the items using the merge sort algorithm is used to assure a + * worst-case of O(n * log(n)) and to keep the number of comparisons low. * * The implementation is based on qStableSortHelper() from qalgorithms.h * Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). - * The sorting implementations of qAlgorithms could not be used as they - * don't support having a member-function as comparison criteria. */ -class LIBDOLPHINPRIVATE_EXPORT KFileItemModelSortAlgorithm + +template <typename RandomAccessIterator, typename LessThan> +static void mergeSort(RandomAccessIterator begin, + RandomAccessIterator end, + LessThan lessThan) { -public: - static void sort(KFileItemModel* model, - QList<KFileItemModel::ItemData*>::iterator begin, - QList<KFileItemModel::ItemData*>::iterator end); + // The implementation is based on qStableSortHelper() from qalgorithms.h + // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). -private: - static void sequentialSort(KFileItemModel* model, - QList<KFileItemModel::ItemData*>::iterator begin, - QList<KFileItemModel::ItemData*>::iterator end); + const int span = end - begin; + if (span < 2) { + return; + } - static void parallelSort(KFileItemModel* model, - QList<KFileItemModel::ItemData*>::iterator begin, - QList<KFileItemModel::ItemData*>::iterator end, - const int numberOfThreads); + const RandomAccessIterator middle = begin + span / 2; + mergeSort(begin, middle, lessThan); + mergeSort(middle, end, lessThan); + merge(begin, middle, end, lessThan); +} - static void merge(KFileItemModel* model, - QList<KFileItemModel::ItemData*>::iterator begin, - QList<KFileItemModel::ItemData*>::iterator pivot, - QList<KFileItemModel::ItemData*>::iterator end); +/** + * Uses up to \a numberOfThreads threads to sort the items between + * \a begin and \a end. Only item ranges longer than + * \a parallelMergeSortingThreshold are split to be sorted by two different + * threads. + * + * The comparison function \a lessThan must be reentrant. + */ - static QList<KFileItemModel::ItemData*>::iterator - lowerBound(KFileItemModel* model, - QList<KFileItemModel::ItemData*>::iterator begin, - QList<KFileItemModel::ItemData*>::iterator end, - const KFileItemModel::ItemData* value); +template <typename RandomAccessIterator, typename LessThan> +static void parallelMergeSort(RandomAccessIterator begin, + RandomAccessIterator end, + LessThan lessThan, + int numberOfThreads, + int parallelMergeSortingThreshold = 100) +{ + const int span = end - begin; - static QList<KFileItemModel::ItemData*>::iterator - upperBound(KFileItemModel* model, - QList<KFileItemModel::ItemData*>::iterator begin, - QList<KFileItemModel::ItemData*>::iterator end, - const KFileItemModel::ItemData* value); + if (numberOfThreads > 1 && span > parallelMergeSortingThreshold) { + const int newNumberOfThreads = numberOfThreads / 2; + const RandomAccessIterator middle = begin + span / 2; - static void reverse(QList<KFileItemModel::ItemData*>::iterator begin, - QList<KFileItemModel::ItemData*>::iterator end); -}; + QFuture<void> future = QtConcurrent::run(parallelMergeSort<RandomAccessIterator, LessThan>, begin, middle, lessThan, newNumberOfThreads, parallelMergeSortingThreshold); + parallelMergeSort(middle, end, lessThan, newNumberOfThreads, parallelMergeSortingThreshold); -#endif + future.waitForFinished(); + + merge(begin, middle, end, lessThan); + } else { + mergeSort(begin, end, lessThan); + } +} + +/** + * Merges the sorted item ranges between \a begin and \a pivot and + * between \a pivot and \a end into a single sorted range between + * \a begin and \a end. + * + * The implementation is based on qMerge() from qalgorithms.h + * Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). + */ + +template <typename RandomAccessIterator, typename LessThan> +static void merge(RandomAccessIterator begin, + RandomAccessIterator pivot, + RandomAccessIterator end, + LessThan lessThan) +{ + // The implementation is based on qMerge() from qalgorithms.h + // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). + + const int len1 = pivot - begin; + const int len2 = end - pivot; + + if (len1 == 0 || len2 == 0) { + return; + } + if (len1 + len2 == 2) { + if (lessThan(*(begin + 1), *(begin))) { + qSwap(*begin, *(begin + 1)); + } + return; + } + + RandomAccessIterator firstCut; + RandomAccessIterator secondCut; + int len2Half; + if (len1 > len2) { + const int len1Half = len1 / 2; + firstCut = begin + len1Half; + secondCut = std::lower_bound(pivot, end, *firstCut, lessThan); + len2Half = secondCut - pivot; + } else { + len2Half = len2 / 2; + secondCut = pivot + len2Half; + firstCut = std::upper_bound(begin, pivot, *secondCut, lessThan); + } + + std::rotate(firstCut, pivot, secondCut); + + RandomAccessIterator newPivot = firstCut + len2Half; + merge(begin, firstCut, newPivot, lessThan); + merge(newPivot, secondCut, end, lessThan); +} + +#endif diff --git a/src/kitemviews/private/kitemlistkeyboardsearchmanager.cpp b/src/kitemviews/private/kitemlistkeyboardsearchmanager.cpp index da8f72b7e..38154864b 100644 --- a/src/kitemviews/private/kitemlistkeyboardsearchmanager.cpp +++ b/src/kitemviews/private/kitemlistkeyboardsearchmanager.cpp @@ -40,7 +40,7 @@ void KItemListKeyboardSearchManager::addKeys(const QString& keys) { const bool keyboardTimeWasValid = m_keyboardInputTime.isValid(); const qint64 keyboardInputTimeElapsed = m_keyboardInputTime.restart(); - if (keyboardInputTimeElapsed > m_timeout || !keyboardTimeWasValid || keys.isEmpty()) { + if (keyboardInputTimeElapsed > m_timeout || !keyboardTimeWasValid) { m_searchedString.clear(); } diff --git a/src/main.cpp b/src/main.cpp index 5addff194..e5ac4351c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -33,10 +33,10 @@ KDE_EXPORT int kdemain(int argc, char **argv) { KAboutData about("dolphin", 0, ki18nc("@title", "Dolphin"), - "2.2", + "2.2.60", ki18nc("@title", "File Manager"), KAboutData::License_GPL, - ki18nc("@info:credit", "(C) 2006-2012 Peter Penz and Frank Reininghaus")); + ki18nc("@info:credit", "(C) 2006-2013 Peter Penz and Frank Reininghaus")); about.setHomepage("http://dolphin.kde.org"); about.addAuthor(ki18nc("@info:credit", "Frank Reininghaus"), ki18nc("@info:credit", "Maintainer (since 2012) and developer"), @@ -50,6 +50,9 @@ KDE_EXPORT int kdemain(int argc, char **argv) about.addAuthor(ki18nc("@info:credit", "David Faure"), ki18nc("@info:credit", "Developer"), + about.addAuthor(ki18nc("@info:credit", "Emmanuel Pescosta"), + ki18nc("@info:credit", "Developer"), + "[email protected]"); about.addAuthor(ki18nc("@info:credit", "Aaron J. Seigo"), ki18nc("@info:credit", "Developer"), diff --git a/src/panels/folders/folderspanel.cpp b/src/panels/folders/folderspanel.cpp index 98c06fb35..46c1b3450 100644 --- a/src/panels/folders/folderspanel.cpp +++ b/src/panels/folders/folderspanel.cpp @@ -236,7 +236,8 @@ void FoldersPanel::slotItemDropEvent(int index, QGraphicsSceneDragDropEvent* eve event->buttons(), event->modifiers()); - const QString error = DragAndDropHelper::dropUrls(destItem, destItem.url(), &dropEvent); + QString error; + DragAndDropHelper::dropUrls(destItem, destItem.url(), &dropEvent, error); if (!error.isEmpty()) { emit errorMessage(error); } diff --git a/src/panels/places/placesitem.cpp b/src/panels/places/placesitem.cpp index 75e14d0fb..5723b80a2 100644 --- a/src/panels/places/placesitem.cpp +++ b/src/panels/places/placesitem.cpp @@ -176,6 +176,10 @@ PlacesItem::GroupType PlacesItem::groupType() const return SearchForType; } + if (protocol == QLatin1String("bluetooth")) { + return DevicesType; + } + return PlacesType; } diff --git a/src/panels/places/placesitemmodel.cpp b/src/panels/places/placesitemmodel.cpp index caf6b7566..eae2095c9 100644 --- a/src/panels/places/placesitemmodel.cpp +++ b/src/panels/places/placesitemmodel.cpp @@ -444,6 +444,11 @@ void PlacesItemModel::dropMimeDataBefore(int index, const QMimeData* mimeData) text = url.host(); } + if (url.isLocalFile() && !QFileInfo(url.toLocalFile()).isDir()) { + // Only directories are allowed + continue; + } + PlacesItem* newItem = createPlacesItem(text, url); const int dropIndex = groupedDropIndex(index, newItem); insertItem(dropIndex, newItem); @@ -612,17 +617,13 @@ void PlacesItemModel::slotStorageSetupDone(Solid::ErrorType error, } if (error) { - // TODO: Request message-freeze exception if (errorData.isValid()) { - // emit errorMessage(i18nc("@info", "An error occurred while accessing '%1', the system responded: %2", - // item->text(), - // errorData.toString())); - emit errorMessage(QString("An error occurred while accessing '%1', the system responded: %2") - .arg(item->text()).arg(errorData.toString())); + 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 errorMessage(QString("An error occurred while accessing '%1'").arg(item->text())); + emit errorMessage(i18nc("@info", "An error occurred while accessing '%1'", + item->text())); } emit storageSetupDone(index, false); } else { diff --git a/src/panels/places/placespanel.cpp b/src/panels/places/placespanel.cpp index 56042d856..d5308eabe 100644 --- a/src/panels/places/placespanel.cpp +++ b/src/panels/places/placespanel.cpp @@ -332,6 +332,12 @@ void PlacesPanel::slotItemDropEvent(int index, QGraphicsSceneDragDropEvent* even return; } + const PlacesItem* destItem = m_model->placesItem(index); + const PlacesItem::GroupType group = destItem->groupType(); + if (group == PlacesItem::SearchForType || group == PlacesItem::RecentlyAccessedType) { + return; + } + if (m_model->storageSetupNeeded(index)) { connect(m_model, SIGNAL(storageSetupDone(int,bool)), this, SLOT(slotItemDropEventStorageSetupDone(int,bool))); @@ -356,14 +362,18 @@ void PlacesPanel::slotItemDropEvent(int index, QGraphicsSceneDragDropEvent* even return; } - KUrl destUrl = m_model->placesItem(index)->url(); + KUrl destUrl = destItem->url(); QDropEvent dropEvent(event->pos().toPoint(), event->possibleActions(), event->mimeData(), event->buttons(), event->modifiers()); - DragAndDropHelper::dropUrls(KFileItem(), destUrl, &dropEvent); + QString error; + DragAndDropHelper::dropUrls(KFileItem(), destUrl, &dropEvent, error); + if (!error.isEmpty()) { + emit errorMessage(error); + } } void PlacesPanel::slotItemDropEventStorageSetupDone(int index, bool success) @@ -375,7 +385,11 @@ void PlacesPanel::slotItemDropEventStorageSetupDone(int index, bool success) if (success) { KUrl destUrl = m_model->placesItem(index)->url(); - DragAndDropHelper::dropUrls(KFileItem(), destUrl, m_itemDropEvent); + QString error; + DragAndDropHelper::dropUrls(KFileItem(), destUrl, m_itemDropEvent, error); + if (!error.isEmpty()) { + emit errorMessage(error); + } } delete m_itemDropEventMimeData; @@ -395,7 +409,8 @@ void PlacesPanel::slotAboveItemDropEvent(int index, QGraphicsSceneDragDropEvent* void PlacesPanel::slotUrlsDropped(const KUrl& dest, QDropEvent* event, QWidget* parent) { Q_UNUSED(parent); - const QString error = DragAndDropHelper::dropUrls(KFileItem(), dest, event); + QString error; + DragAndDropHelper::dropUrls(KFileItem(), dest, event, error); if (!error.isEmpty()) { emit errorMessage(error); } diff --git a/src/search/dolphinsearchbox.cpp b/src/search/dolphinsearchbox.cpp index 1792b2017..ef9c2bfcf 100644 --- a/src/search/dolphinsearchbox.cpp +++ b/src/search/dolphinsearchbox.cpp @@ -473,7 +473,7 @@ void DolphinSearchBox::updateFacetsToggleButton() const bool facetsIsVisible = SearchSettings::showFacetsWidget(); m_facetsToggleButton->setChecked(facetsIsVisible ? true : false); m_facetsToggleButton->setIcon(KIcon(facetsIsVisible ? "arrow-up-double" : "arrow-down-double")); - m_facetsToggleButton->setText(facetsIsVisible ? i18nc("action:button", "Less Options") : i18nc("action:button", "More Options")); + m_facetsToggleButton->setText(facetsIsVisible ? i18nc("action:button", "Fewer Options") : i18nc("action:button", "More Options")); } #include "dolphinsearchbox.moc" diff --git a/src/search/dolphinsearchinformation.cpp b/src/search/dolphinsearchinformation.cpp index 31f6fcc7b..b723f1ec0 100644 --- a/src/search/dolphinsearchinformation.cpp +++ b/src/search/dolphinsearchinformation.cpp @@ -28,6 +28,8 @@ #include <KGlobal> #include <KUrl> +#include <QFileInfo> +#include <QDir> struct DolphinSearchInformationSingleton { @@ -50,12 +52,35 @@ bool DolphinSearchInformation::isIndexingEnabled() const return m_indexingEnabled; } +namespace { + /// recursively check if a folder is hidden + bool isDirHidden( QDir& dir ) { + if (QFileInfo(dir.path()).isHidden()) { + return true; + } else if (dir.cdUp()) { + return isDirHidden(dir); + } else { + return false; + } + } + + bool isDirHidden(const QString& path) { + QDir dir(path); + return isDirHidden(dir); + } +} + bool DolphinSearchInformation::isPathIndexed(const KUrl& url) const { #ifdef HAVE_NEPOMUK const KConfig strigiConfig("nepomukstrigirc"); const QStringList indexedFolders = strigiConfig.group("General").readPathEntry("folders", QStringList()); + // Nepomuk does not index hidden folders + if (isDirHidden(url.toLocalFile())) { + return false; + } + // Check whether the path is part of an indexed folder bool isIndexed = false; foreach (const QString& indexedFolder, indexedFolders) { diff --git a/src/settings/services/servicessettingspage.cpp b/src/settings/services/servicessettingspage.cpp index 48e816be7..9adca9baf 100644 --- a/src/settings/services/servicessettingspage.cpp +++ b/src/settings/services/servicessettingspage.cpp @@ -22,6 +22,7 @@ #include "dolphin_generalsettings.h" #include "dolphin_versioncontrolsettings.h" +#include <kabstractfileitemactionplugin.h> #include <KConfig> #include <KConfigGroup> #include <KDesktopFile> @@ -223,7 +224,15 @@ void ServicesSettingsPage::loadServices() foreach (const KSharedPtr<KService>& service, pluginServices) { const QString desktopEntryName = service->desktopEntryName(); if (!isInServicesList(desktopEntryName)) { - const bool checked = showGroup.readEntry(desktopEntryName, true); + bool checked; + + KAbstractFileItemActionPlugin* abstractPlugin = service->createInstance<KAbstractFileItemActionPlugin>(); + if (abstractPlugin) { + checked = showGroup.readEntry(desktopEntryName, abstractPlugin->enabledByDefault()); + } else { + checked = showGroup.readEntry(desktopEntryName, true); + } + addRow(service->icon(), service->name(), desktopEntryName, checked); } } diff --git a/src/statusbar/dolphinstatusbar.cpp b/src/statusbar/dolphinstatusbar.cpp index 068b6325a..6f734ed4d 100644 --- a/src/statusbar/dolphinstatusbar.cpp +++ b/src/statusbar/dolphinstatusbar.cpp @@ -107,14 +107,18 @@ DolphinStatusBar::DolphinStatusBar(QWidget* parent) : const int zoomSliderHeight = m_zoomSlider->minimumSizeHint().height(); const int contentHeight = qMax(fontHeight, zoomSliderHeight); - m_label->setMinimumHeight(contentHeight); - m_label->setMaximumHeight(contentHeight); + m_label->setFixedHeight(contentHeight); m_label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - const QSize size(150, contentHeight); - applyFixedWidgetSize(m_spaceInfo, size); - applyFixedWidgetSize(m_progressBar, size); - applyFixedWidgetSize(m_zoomSlider, size); + m_zoomSlider->setFixedHeight(contentHeight); + m_zoomSlider->setMaximumWidth(150); + + m_spaceInfo->setFixedHeight(contentHeight); + m_spaceInfo->setMaximumWidth(150); + m_spaceInfo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + + m_progressBar->setFixedHeight(contentHeight); + m_progressBar->setMaximumWidth(150); QHBoxLayout* topLayout = new QHBoxLayout(this); topLayout->setMargin(0); @@ -349,11 +353,4 @@ void DolphinStatusBar::updateZoomSliderToolTip(int zoomLevel) m_zoomSlider->setToolTip(i18ncp("@info:tooltip", "Size: 1 pixel", "Size: %1 pixels", size)); } -void DolphinStatusBar::applyFixedWidgetSize(QWidget* widget, const QSize& size) -{ - widget->setMinimumSize(size); - widget->setMaximumSize(size); - widget->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); -} - #include "dolphinstatusbar.moc" diff --git a/src/statusbar/dolphinstatusbar.h b/src/statusbar/dolphinstatusbar.h index d7e37f584..b2afe2eb9 100644 --- a/src/statusbar/dolphinstatusbar.h +++ b/src/statusbar/dolphinstatusbar.h @@ -137,8 +137,6 @@ private: */ void updateZoomSliderToolTip(int zoomLevel); - void applyFixedWidgetSize(QWidget* widget, const QSize& size); - private: QString m_text; QString m_defaultText; diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 84c2e5ae5..dd761fc90 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -57,6 +57,16 @@ set(kfileitemmodeltest_SRCS kde4_add_unit_test(kfileitemmodeltest TEST ${kfileitemmodeltest_SRCS}) target_link_libraries(kfileitemmodeltest dolphinprivate ${KDE4_KIO_LIBS} ${QT_QTTEST_LIBRARY}) +# KFileItemModelBenchmark +set(kfileitemmodelbenchmark_SRCS + kfileitemmodelbenchmark.cpp + testdir.cpp + ../kitemviews/kfileitemmodel.cpp + ../kitemviews/kitemmodelbase.cpp +) +kde4_add_executable(kfileitemmodelbenchmark TEST ${kfileitemmodelbenchmark_SRCS}) +target_link_libraries(kfileitemmodelbenchmark dolphinprivate ${KDE4_KIO_LIBS} ${QT_QTTEST_LIBRARY}) + # KItemListKeyboardSearchManagerTest set(kitemlistkeyboardsearchmanagertest_SRCS kitemlistkeyboardsearchmanagertest.cpp diff --git a/src/tests/kfileitemmodelbenchmark.cpp b/src/tests/kfileitemmodelbenchmark.cpp new file mode 100644 index 000000000..f72e43ede --- /dev/null +++ b/src/tests/kfileitemmodelbenchmark.cpp @@ -0,0 +1,334 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * Copyright (C) 2013 by Frank Reininghaus <[email protected]> * + * * + * 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 <qtest_kde.h> + +#include "kitemviews/kfileitemmodel.h" +#include "kitemviews/private/kfileitemmodelsortalgorithm.h" + +#include "testdir.h" + +#include <KRandomSequence> + +void myMessageOutput(QtMsgType type, const char* msg) +{ + switch (type) { + case QtDebugMsg: + break; + case QtWarningMsg: + break; + case QtCriticalMsg: + fprintf(stderr, "Critical: %s\n", msg); + break; + case QtFatalMsg: + fprintf(stderr, "Fatal: %s\n", msg); + abort(); + default: + break; + } +} + +namespace { + const int DefaultTimeout = 5000; +}; + +Q_DECLARE_METATYPE(KFileItemList) +Q_DECLARE_METATYPE(KItemRangeList) + +class KFileItemModelBenchmark : public QObject +{ + Q_OBJECT + +public: + KFileItemModelBenchmark(); + +private slots: + void insertAndRemoveManyItems_data(); + void insertAndRemoveManyItems(); + void insertManyChildItems(); + +private: + static KFileItemList createFileItemList(const QStringList& fileNames, const QString& urlPrefix = QLatin1String("file:///")); +}; + +KFileItemModelBenchmark::KFileItemModelBenchmark() +{ +} + +void KFileItemModelBenchmark::insertAndRemoveManyItems_data() +{ + QTest::addColumn<KFileItemList>("initialItems"); + QTest::addColumn<KFileItemList>("newItems"); + QTest::addColumn<KFileItemList>("removedItems"); + QTest::addColumn<KFileItemList>("expectedFinalItems"); + QTest::addColumn<KItemRangeList>("expectedItemsInserted"); + QTest::addColumn<KItemRangeList>("expectedItemsRemoved"); + + QList<int> sizes; + sizes << 1000 << 4000 << 16000 << 64000 << 256000; + //sizes << 50000 << 100000 << 150000 << 200000 << 250000; + + 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(QSet<QByteArray>() << "text"); + + QSignalSpy spyItemsInserted(&model, SIGNAL(itemsInserted(KItemRangeList))); + QSignalSpy spyItemsRemoved(&model, SIGNAL(itemsRemoved(KItemRangeList))); + + 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.removeItems(removedItems, KFileItemModel::DeleteItemData); + } + 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<KItemRangeList>(); + QCOMPARE(actualItemsInserted, expectedItemsInserted); + } + + if (!expectedItemsRemoved.empty()) { + QVERIFY(!spyItemsRemoved.empty()); + const KItemRangeList actualItemsRemoved = spyItemsRemoved.last().first().value<KItemRangeList>(); + QCOMPARE(actualItemsRemoved, expectedItemsRemoved); + } +} + +void KFileItemModelBenchmark::insertManyChildItems() +{ + // TODO: this function needs to be adjusted to the changes in KFileItemModel + // (replacement of slotNewItems(KFileItemList) by slotItemsAdded(KUrl,KFileItemList)) + // Currently, this function tries to insert child items of multiple + // directories by invoking the slot only once. +#if 0 + qInstallMsgHandler(myMessageOutput); + + KFileItemModel model; + + // Avoid overhead caused by natural sorting. + model.m_naturalSorting = false; + + QSet<QByteArray> modelRoles = model.roles(); + modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; + model.setRoles(modelRoles); + model.setSortDirectoriesFirst(false); + + // Create a test folder with a 3-level tree structure of folders. + TestDir testFolder; + int numberOfFolders = 0; + + QStringList subFolderNames; + subFolderNames << "a/" << "b/" << "c/" << "d/"; + + foreach (const QString& s1, subFolderNames) { + ++numberOfFolders; + foreach (const QString& s2, subFolderNames) { + ++numberOfFolders; + foreach (const QString& s3, subFolderNames) { + testFolder.createDir("level-1-" + s1 + "level-2-" + s2 + "level-3-" + s3); + ++numberOfFolders; + } + } + } + + // Open the folder in the model and expand all subfolders. + model.loadDirectory(testFolder.url()); + QVERIFY(QTest::kWaitForSignal(&model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + + int index = 0; + while (index < model.count()) { + if (model.isExpandable(index)) { + model.setExpanded(index, true); + + if (!model.data(index).value("text").toString().startsWith("level-3")) { + // New subfolders will appear unless we are on the final level already. + QVERIFY(QTest::kWaitForSignal(&model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + } + + QVERIFY(model.isExpanded(index)); + } + ++index; + } + + QCOMPARE(model.count(), numberOfFolders); + + // Create a list of many file items, which will be added to each of the + // "level 1", "level 2", and "level 3" folders. + const int filesPerDirectory = 500; + QStringList allStrings; + for (int i = 0; i < filesPerDirectory; ++i) { + allStrings << QString::number(i); + } + allStrings.sort(); + + KFileItemList newItems; + + // Also keep track of all expected items, including the existing + // folders, to verify the final state of the model. + KFileItemList allExpectedItems; + + for (int i = 0; i < model.count(); ++i) { + const KFileItem folderItem = model.fileItem(i); + allExpectedItems << folderItem; + + const KUrl folderUrl = folderItem.url(); + KFileItemList itemsInFolder = createFileItemList(allStrings, folderUrl.url(KUrl::AddTrailingSlash)); + + newItems.append(itemsInFolder); + allExpectedItems.append(itemsInFolder); + } + + // Bring the items into random order. + KRandomSequence randomSequence(0); + randomSequence.randomize(newItems); + + // Measure how long it takes to insert and then remove all files. + QBENCHMARK { + model.slotNewItems(newItems); + model.slotCompleted(); + + QCOMPARE(model.count(), allExpectedItems.count()); + QVERIFY(model.isConsistent()); + for (int i = 0; i < model.count(); ++i) { + QCOMPARE(model.fileItem(i), allExpectedItems.at(i)); + } + + model.slotItemsDeleted(newItems); + QCOMPARE(model.count(), numberOfFolders); + QVERIFY(model.isConsistent()); + } +#endif +} + +KFileItemList KFileItemModelBenchmark::createFileItemList(const QStringList& fileNames, const QString& prefix) +{ + // Suppress 'file does not exist anymore' messages from KFileItemPrivate::init(). + qInstallMsgHandler(myMessageOutput); + + KFileItemList result; + foreach (const QString& name, fileNames) { + const KUrl url(prefix + name); + const KFileItem item(url, QString(), KFileItem::Unknown); + result << item; + } + return result; +} + +QTEST_KDEMAIN(KFileItemModelBenchmark, NoGUI) + +#include "kfileitemmodelbenchmark.moc" diff --git a/src/tests/kfileitemmodeltest.cpp b/src/tests/kfileitemmodeltest.cpp index fd6c2be90..0ad7a378d 100644 --- a/src/tests/kfileitemmodeltest.cpp +++ b/src/tests/kfileitemmodeltest.cpp @@ -21,6 +21,8 @@ #include <qtest_kde.h> #include <KDirLister> +#include <kio/job.h> + #include "kitemviews/kfileitemmodel.h" #include "kitemviews/private/kfileitemmodeldirlister.h" #include "testdir.h" @@ -72,6 +74,7 @@ private slots: void testItemRangeConsistencyWhenInsertingItems(); void testExpandItems(); void testExpandParentItems(); + void testMakeExpandedItemHidden(); void testSorting(); void testIndexForKeyboardSearch(); void testNameFilter(); @@ -80,9 +83,9 @@ private slots: void testRemoveHiddenItems(); void collapseParentOfHiddenItems(); void removeParentOfHiddenItems(); + void testGeneralParentChildRelationships(); private: - bool isModelConsistent() const; QStringList itemsInModel() const; private: @@ -157,7 +160,7 @@ void KFileItemModelTest::testNewItems() QCOMPARE(m_model->count(), 3); - QVERIFY(isModelConsistent()); + QVERIFY(m_model->isConsistent()); } void KFileItemModelTest::testRemoveItems() @@ -167,13 +170,13 @@ void KFileItemModelTest::testRemoveItems() m_model->loadDirectory(m_testDir->url()); QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); QCOMPARE(m_model->count(), 2); - QVERIFY(isModelConsistent()); + QVERIFY(m_model->isConsistent()); m_testDir->removeFile("a.txt"); m_model->m_dirLister->updateDirectory(m_testDir->url()); QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsRemoved(KItemRangeList)), DefaultTimeout)); QCOMPARE(m_model->count(), 1); - QVERIFY(isModelConsistent()); + QVERIFY(m_model->isConsistent()); } void KFileItemModelTest::testDirLoadingCompleted() @@ -216,7 +219,7 @@ void KFileItemModelTest::testDirLoadingCompleted() QCOMPARE(itemsRemovedSpy.count(), 2); QCOMPARE(m_model->count(), 4); - QVERIFY(isModelConsistent()); + QVERIFY(m_model->isConsistent()); } void KFileItemModelTest::testSetData() @@ -237,7 +240,7 @@ void KFileItemModelTest::testSetData() values = m_model->data(0); QCOMPARE(values.value("customRole1").toString(), QString("Test1")); QCOMPARE(values.value("customRole2").toString(), QString("Test2")); - QVERIFY(isModelConsistent()); + QVERIFY(m_model->isConsistent()); } void KFileItemModelTest::testSetDataWithModifiedSortRole_data() @@ -319,7 +322,7 @@ void KFileItemModelTest::testSetDataWithModifiedSortRole() 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(isModelConsistent()); + QVERIFY(m_model->isConsistent()); } void KFileItemModelTest::testChangeSortRole() @@ -397,7 +400,7 @@ void KFileItemModelTest::testModelConsistencyWhenInsertingItems() QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); } - QVERIFY(isModelConsistent()); + QVERIFY(m_model->isConsistent()); } QCOMPARE(m_model->count(), 201); @@ -531,6 +534,7 @@ void KFileItemModelTest::testExpandItems() QCOMPARE(spyRemoved.count(), 1); itemRangeList = spyRemoved.takeFirst().at(0).value<KItemRangeList>(); 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(); @@ -547,6 +551,7 @@ void KFileItemModelTest::testExpandItems() 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. @@ -609,6 +614,56 @@ void KFileItemModelTest::testExpandParentItems() 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() +{ + QSet<QByteArray> modelRoles = m_model->roles(); + modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; + m_model->setRoles(modelRoles); + + QStringList files; + m_testDir->createFile("1a/2a/3a"); + m_testDir->createFile("1a/2a/3b"); + m_testDir->createFile("1a/2b"); + m_testDir->createFile("1b"); + + m_model->loadDirectory(m_testDir->url()); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + + // So far, the model contains only "1a/" and "1b". + QCOMPARE(m_model->count(), 2); + m_model->setExpanded(0, true); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + + // Now "1a/2a" and "1a/2b" have appeared. + QCOMPARE(m_model->count(), 4); + m_model->setExpanded(1, true); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(m_model->count(), 6); + + // Rename "1a/2" and make it hidden. + const QString oldPath = m_model->fileItem(0).url().path() + "/2a"; + const QString newPath = m_model->fileItem(0).url().path() + "/.2a"; + + KIO::SimpleJob* job = KIO::rename(oldPath, newPath, KIO::HideProgressInfo); + bool ok = job->exec(); + QVERIFY(ok); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsRemoved(KItemRangeList)), DefaultTimeout)); + + // "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::testSorting() @@ -834,12 +889,12 @@ void KFileItemModelTest::testEmptyPath() const KUrl emptyUrl; QVERIFY(emptyUrl.path().isEmpty()); - + const KUrl url("file:///test/"); - + KFileItemList items; items << KFileItem(emptyUrl, QString(), KFileItem::Unknown) << KFileItem(url, QString(), KFileItem::Unknown); - m_model->slotNewItems(items); + m_model->slotItemsAdded(emptyUrl, items); m_model->slotCompleted(); } @@ -1025,27 +1080,96 @@ void KFileItemModelTest::removeParentOfHiddenItems() QCOMPARE(itemsInModel(), QStringList() << "a" << "1"); } -bool KFileItemModelTest::isModelConsistent() const +/** + * 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() { - if (m_model->m_items.count() != m_model->m_itemData.count()) { - return false; - } + QSet<QByteArray> modelRoles = m_model->roles(); + modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; + m_model->setRoles(modelRoles); - for (int i = 0; i < m_model->count(); ++i) { - const KFileItem item = m_model->fileItem(i); - if (item.isNull()) { - qWarning() << "Item" << i << "is null"; - return false; - } + QStringList files; + files << "parent1/realChild1/realGrandChild1" << "parent2/realChild2/realGrandChild2"; + m_testDir->createFiles(files); - const int itemIndex = m_model->index(item); - if (itemIndex != i) { - qWarning() << "Item" << i << "has a wrong index:" << itemIndex; - return false; - } - } + m_model->loadDirectory(m_testDir->url()); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "parent1" << "parent2"); + + // Expand all folders. + m_model->setExpanded(0, true); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "parent2"); + + m_model->setExpanded(1, true); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "realGrandChild1" << "parent2"); + + m_model->setExpanded(3, true); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "realGrandChild1" << "parent2" << "realChild2"); + + m_model->setExpanded(4, true); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "realGrandChild1" << "parent2" << "realChild2" << "realGrandChild2"); + + // Add some more children and grand-children. + const KUrl parent1 = m_model->fileItem(0).url(); + const KUrl parent2 = m_model->fileItem(3).url(); + const KUrl realChild1 = m_model->fileItem(1).url(); + const KUrl realChild2 = m_model->fileItem(4).url(); + + m_model->slotItemsAdded(parent1, KFileItemList() << KFileItem(KUrl("child1"), QString(), KFileItem::Unknown)); + m_model->slotCompleted(); + QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "realGrandChild1" << "child1" << "parent2" << "realChild2" << "realGrandChild2"); + + m_model->slotItemsAdded(parent2, KFileItemList() << KFileItem(KUrl("child2"), QString(), KFileItem::Unknown)); + m_model->slotCompleted(); + QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "realGrandChild1" << "child1" << "parent2" << "realChild2" << "realGrandChild2" << "child2"); - return true; + m_model->slotItemsAdded(realChild1, KFileItemList() << KFileItem(KUrl("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(KUrl("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(KUrl("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. + QSignalSpy itemsRemovedSpy(m_model, SIGNAL(itemsRemoved(KItemRangeList))); + m_model->setNameFilter("xyz"); + QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "parent2" << "realChild2"); + QCOMPARE(itemsRemovedSpy.count(), 1); + QList<QVariant> arguments = itemsRemovedSpy.takeFirst(); + KItemRangeList itemRangeList = arguments.at(0).value<KItemRangeList>(); + 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<KItemRangeList>(); + 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<KItemRangeList>(); + QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(1, 2)); + + // Clear filter, verify that no items reappear. + m_model->setNameFilter(QString()); + QCOMPARE(itemsInModel(), QStringList() << "parent1"); } QStringList KFileItemModelTest::itemsInModel() const diff --git a/src/tests/kitemlistkeyboardsearchmanagertest.cpp b/src/tests/kitemlistkeyboardsearchmanagertest.cpp index cf15324e2..7d5fc3b9a 100644 --- a/src/tests/kitemlistkeyboardsearchmanagertest.cpp +++ b/src/tests/kitemlistkeyboardsearchmanagertest.cpp @@ -31,6 +31,7 @@ private slots: void testBasicKeyboardSearch(); void testAbortedKeyboardSearch(); void testRepeatedKeyPress(); + void testPressShift(); private: KItemListKeyboardSearchManager m_keyboardSearchManager; @@ -39,7 +40,7 @@ private: void KItemListKeyboardSearchManagerTest::init() { // Make sure that the previous search string is cleared - m_keyboardSearchManager.addKeys(""); + m_keyboardSearchManager.cancelSearch(); } void KItemListKeyboardSearchManagerTest::testBasicKeyboardSearch() @@ -120,6 +121,32 @@ void KItemListKeyboardSearchManagerTest::testRepeatedKeyPress() QCOMPARE(spy.takeFirst(), QList<QVariant>() << "pppq" << false); } +void KItemListKeyboardSearchManagerTest::testPressShift() +{ + // If the user presses Shift, i.e., to get a character like '_', + // KItemListController calls the addKeys(QString) method with an empty + // string. Make sure that this does not reset the current search. See + // https://bugs.kde.org/show_bug.cgi?id=321286 + + QSignalSpy spy(&m_keyboardSearchManager, SIGNAL(changeCurrentItem(QString,bool))); + + // Simulate that the user enters "a_b". + m_keyboardSearchManager.addKeys("a"); + QCOMPARE(spy.count(), 1); + QCOMPARE(spy.takeFirst(), QList<QVariant>() << "a" << true); + + m_keyboardSearchManager.addKeys(""); + QCOMPARE(spy.count(), 0); + + m_keyboardSearchManager.addKeys("_"); + QCOMPARE(spy.count(), 1); + QCOMPARE(spy.takeFirst(), QList<QVariant>() << "a_" << false); + + m_keyboardSearchManager.addKeys("b"); + QCOMPARE(spy.count(), 1); + QCOMPARE(spy.takeFirst(), QList<QVariant>() << "a_b" << false); +} + QTEST_KDEMAIN(KItemListKeyboardSearchManagerTest, NoGUI) #include "kitemlistkeyboardsearchmanagertest.moc" diff --git a/src/views/dolphinremoteencoding.cpp b/src/views/dolphinremoteencoding.cpp index 375b3fd46..04b350eda 100644 --- a/src/views/dolphinremoteencoding.cpp +++ b/src/views/dolphinremoteencoding.cpp @@ -38,7 +38,6 @@ #include <KMenu> #include <KProtocolInfo> #include <KProtocolManager> -#include <KIO/SlaveConfig> #include <KIO/Scheduler> #include <KConfigGroup> @@ -132,9 +131,7 @@ void DolphinRemoteEncoding::updateMenu() m_menu->menu()->actions().at(i)->setChecked(false); } - QString charset = KGlobal::charsets()->descriptionForEncoding(KIO::SlaveConfig::self()->configData(m_currentURL.protocol(), - m_currentURL.host(), DATA_KEY)); - + const QString charset = KGlobal::charsets()->descriptionForEncoding(KProtocolManager::charsetFor(m_currentURL)); if (!charset.isEmpty()) { int id = 0; bool isFound = false; diff --git a/src/views/dolphinview.cpp b/src/views/dolphinview.cpp index d69d664af..303ee34af 100644 --- a/src/views/dolphinview.cpp +++ b/src/views/dolphinview.cpp @@ -33,6 +33,8 @@ #include <QTimer> #include <QScrollBar> +#include <KDesktopFile> +#include <KProtocolManager> #include <KActionCollection> #include <KColorScheme> #include <KDirModel> @@ -102,6 +104,7 @@ DolphinView::DolphinView(const KUrl& url, QWidget* parent) : m_restoredContentsPosition(), m_selectedUrls(), m_clearSelectionBeforeSelectingNewItems(false), + m_markFirstNewlySelectedItemAsCurrent(false), m_versionControlObserver(0) { m_topLayout = new QVBoxLayout(this); @@ -175,7 +178,7 @@ DolphinView::DolphinView(const KUrl& url, QWidget* parent) : connect(m_view, SIGNAL(visibleRolesChanged(QList<QByteArray>,QList<QByteArray>)), this, SLOT(slotVisibleRolesChangedByHeader(QList<QByteArray>,QList<QByteArray>))); connect(m_view, SIGNAL(roleEditingCanceled(int,QByteArray,QVariant)), - this, SLOT(slotRoleEditingCanceled(int,QByteArray,QVariant))); + this, SLOT(slotRoleEditingCanceled())); connect(m_view->header(), SIGNAL(columnWidthChanged(QByteArray,qreal,qreal)), this, SLOT(slotHeaderColumnWidthChanged(QByteArray,qreal,qreal))); @@ -815,9 +818,10 @@ void DolphinView::slotItemsActivated(const QSet<int>& indexes) while (it.hasNext()) { const int index = it.next(); KFileItem item = m_model->fileItem(index); + const KUrl& url = openItemAsFolderUrl(item); - if (item.isDir()) { // Open folders in new tabs - emit tabRequested(item.url()); + if (!url.isEmpty()) { // Open folders in new tabs + emit tabRequested(url); } else { items.append(item); } @@ -832,8 +836,11 @@ void DolphinView::slotItemsActivated(const QSet<int>& indexes) void DolphinView::slotItemMiddleClicked(int index) { - const KFileItem item = m_model->fileItem(index); - if (item.isDir() || isTabsForFilesEnabled()) { + const KFileItem& item = m_model->fileItem(index); + const KUrl& url = openItemAsFolderUrl(item); + if (!url.isEmpty()) { + emit tabRequested(url); + } else if (isTabsForFilesEnabled()) { emit tabRequested(item.url()); } } @@ -1029,15 +1036,19 @@ void DolphinView::slotItemDropEvent(int index, QGraphicsSceneDragDropEvent* even event->buttons(), event->modifiers()); - const QString error = DragAndDropHelper::dropUrls(destItem, destUrl, &dropEvent); + QString error; + KonqOperations* op = DragAndDropHelper::dropUrls(destItem, destUrl, &dropEvent, error); if (!error.isEmpty()) { emit infoMessage(error); } - if (destUrl == url()) { + if (op && destUrl == url()) { // Mark the dropped urls as selected. - markPastedUrlsAsSelected(event->mimeData()); + m_clearSelectionBeforeSelectingNewItems = true; + connect(op, SIGNAL(urlPasted(KUrl)), this, SLOT(slotUrlPasted(KUrl))); } + + setActive(true); } void DolphinView::slotModelChanged(KItemModelBase* current, KItemModelBase* previous) @@ -1072,6 +1083,17 @@ void DolphinView::slotMouseButtonPressed(int itemIndex, Qt::MouseButtons buttons } } +void DolphinView::slotAboutToCreate(const KUrl::List& urls) +{ + if (!urls.isEmpty()) { + if (m_markFirstNewlySelectedItemAsCurrent) { + markUrlAsCurrent(urls.first()); + m_markFirstNewlySelectedItemAsCurrent = false; + } + m_selectedUrls << urls; + } +} + void DolphinView::slotSelectionChanged(const QSet<int>& current, const QSet<int>& previous) { const int currentCount = current.count(); @@ -1194,6 +1216,46 @@ QString DolphinView::viewPropertiesContext() const return m_viewPropertiesContext; } +KUrl DolphinView::openItemAsFolderUrl(const KFileItem& item, const bool browseThroughArchives) +{ + if (item.isNull()) { + return KUrl(); + } + + KUrl 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:/<path>/ when clicking on a tar file, + // zip:/<path>/ 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.setProtocol(protocol); + return url; + } + } + + if (mimetype == QLatin1String("application/x-desktop")) { + // Redirect to the URL in Type=Link desktop files + KDesktopFile desktopFile(url.toLocalFile()); + if (desktopFile.hasLinkType()) { + return desktopFile.readUrl(); + } + } + } + + return KUrl(); +} + void DolphinView::observeCreatedItem(const KUrl& url) { if (m_active) { @@ -1224,10 +1286,11 @@ void DolphinView::updateViewState() m_view->scrollToItem(currentIndex); m_scrollToCurrentItem = false; } + + m_currentItemUrl = KUrl(); } else { selectionManager->setCurrentItem(0); } - m_currentItemUrl = KUrl(); } if (!m_restoredContentsPosition.isNull()) { @@ -1306,6 +1369,16 @@ void DolphinView::slotDeleteFileFinished(KJob* job) } } +void DolphinView::slotRenamingFailed(const KUrl& oldUrl, const KUrl& newUrl) +{ + const int index = m_model->index(newUrl); + if (index >= 0) { + QHash<QByteArray, QVariant> data; + 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 @@ -1372,7 +1445,7 @@ void DolphinView::slotVisibleRolesChangedByHeader(const QList<QByteArray>& curre emit visibleRolesChanged(m_visibleRoles, previousVisibleRoles); } -void DolphinView::slotRoleEditingCanceled(int index, const QByteArray& role, const QVariant& value) +void DolphinView::slotRoleEditingCanceled() { disconnect(m_view, SIGNAL(roleEditingFinished(int,QByteArray,QVariant)), this, SLOT(slotRoleEditingFinished(int,QByteArray,QVariant))); @@ -1406,7 +1479,10 @@ void DolphinView::slotRoleEditingFinished(int index, const QByteArray& role, con m_model->setData(index, data); } - KonqOperations::rename(this, oldUrl, newName); + KonqOperations* op = KonqOperations::renameV2(this, oldUrl, newName); + if (op) { + connect(op, SIGNAL(renamingFailed(KUrl,KUrl)), SLOT(slotRenamingFailed(KUrl,KUrl))); + } } } } @@ -1538,8 +1614,12 @@ void DolphinView::applyModeToView() void DolphinView::pasteToUrl(const KUrl& url) { - markPastedUrlsAsSelected(QApplication::clipboard()->mimeData()); - KonqOperations::doPaste(this, url); + KonqOperations* op = KonqOperations::doPasteV2(this, url); + if (op) { + m_clearSelectionBeforeSelectingNewItems = true; + m_markFirstNewlySelectedItemAsCurrent = true; + connect(op, SIGNAL(aboutToCreate(KUrl::List)), this, SLOT(slotAboutToCreate(KUrl::List))); + } } KUrl::List DolphinView::simplifiedSelectedUrls() const @@ -1567,18 +1647,6 @@ QMimeData* DolphinView::selectionMimeData() const return m_model->createMimeData(selectedIndexes); } -void DolphinView::markPastedUrlsAsSelected(const QMimeData* mimeData) -{ - const KUrl::List sourceUrls = KUrl::List::fromMimeData(mimeData); - KUrl::List destUrls; - foreach (const KUrl& source, sourceUrls) { - KUrl destination(url().url() + '/' + source.fileName()); - destUrls << destination; - } - markUrlsAsSelected(destUrls); - m_clearSelectionBeforeSelectingNewItems = true; -} - void DolphinView::updateWritableState() { const bool wasFolderWritable = m_isFolderWritable; diff --git a/src/views/dolphinview.h b/src/views/dolphinview.h index 62b5df7c7..e5e9834b9 100644 --- a/src/views/dolphinview.h +++ b/src/views/dolphinview.h @@ -304,6 +304,14 @@ public: void setViewPropertiesContext(const QString& context); QString viewPropertiesContext() const; + /** + * Checks if the given \a item can be opened as folder (e.g. archives). + * This function will also adjust the \a url (e.g. change the protocol). + * @return a valid and adjusted url if the item can be opened as folder, + * otherwise return an empty url. + */ + static KUrl openItemAsFolderUrl(const KFileItem& item, const bool browseThroughArchives = true); + public slots: /** * Changes the directory to \a url. If the current directory is equal to @@ -566,6 +574,11 @@ private slots: void slotModelChanged(KItemModelBase* current, KItemModelBase* previous); void slotMouseButtonPressed(int itemIndex, Qt::MouseButtons buttons); + /* + * Is called when new items get pasted or dropped. + */ + void slotAboutToCreate(const KUrl::List& urls); + /** * Emits the signal \a selectionChanged() with a small delay. This is * because getting all file items for the selection can be an expensive @@ -619,6 +632,8 @@ private slots: */ void slotDeleteFileFinished(KJob* job); + void slotRenamingFailed(const KUrl& oldUrl, const KUrl& newUrl); + /** * Invoked when the file item model has started the loading * of the directory specified by DolphinView::url(). @@ -655,7 +670,7 @@ private slots: void slotVisibleRolesChangedByHeader(const QList<QByteArray>& current, const QList<QByteArray>& previous); - void slotRoleEditingCanceled(int index, const QByteArray& role, const QVariant& value); + void slotRoleEditingCanceled(); void slotRoleEditingFinished(int index, const QByteArray& role, const QVariant& value); /** @@ -723,14 +738,6 @@ private: QMimeData* selectionMimeData() const; /** - * Is invoked after a paste operation or a drag & drop - * operation and URLs from \a mimeData as selected. - * This allows to select all newly pasted - * items in restoreViewState(). - */ - void markPastedUrlsAsSelected(const QMimeData* mimeData); - - /** * Updates m_isFolderWritable dependent on whether the folder represented by * the current URL is writable. If the state has changed, the signal * writeableStateChanged() will be emitted. @@ -773,6 +780,7 @@ private: QList<KUrl> m_selectedUrls; // Used for making the view to remember selections after F5 bool m_clearSelectionBeforeSelectingNewItems; + bool m_markFirstNewlySelectedItemAsCurrent; VersionControlObserver* m_versionControlObserver; diff --git a/src/views/dolphinviewactionhandler.cpp b/src/views/dolphinviewactionhandler.cpp index 730723785..9a9718c33 100644 --- a/src/views/dolphinviewactionhandler.cpp +++ b/src/views/dolphinviewactionhandler.cpp @@ -314,14 +314,7 @@ void DolphinViewActionHandler::slotRename() void DolphinViewActionHandler::slotTrashActivated(Qt::MouseButtons, Qt::KeyboardModifiers modifiers) { emit actionBeingHandled(); - // Note: kde3's konq_mainwindow.cpp used to check - // reason == KAction::PopupMenuActivation && ... - // but this isn't supported anymore - if (modifiers & Qt::ShiftModifier) { - m_currentView->deleteSelectedItems(); - } else { - m_currentView->trashSelectedItems(); - } + m_currentView->trashSelectedItems(); } void DolphinViewActionHandler::slotDeleteItems() diff --git a/src/views/draganddrophelper.cpp b/src/views/draganddrophelper.cpp index f81d4d0bf..f8ae0ad03 100644 --- a/src/views/draganddrophelper.cpp +++ b/src/views/draganddrophelper.cpp @@ -28,10 +28,13 @@ #include <QtDBus> #include <QDropEvent> -QString DragAndDropHelper::dropUrls(const KFileItem& destItem, const KUrl& destUrl, QDropEvent* event) +KonqOperations* DragAndDropHelper::dropUrls(const KFileItem& destItem, const KUrl& destUrl, QDropEvent* event, QString& error) { + error.clear(); + if (!destItem.isNull() && !destItem.isWritable()) { - return i18nc("@info:status", "Access denied. Could not write to <filename>%1</filename>", destUrl.pathOrUrl()); + error = i18nc("@info:status", "Access denied. Could not write to <filename>%1</filename>", destUrl.pathOrUrl()); + return 0; } const QMimeData* mimeData = event->mimeData(); @@ -49,15 +52,16 @@ QString DragAndDropHelper::dropUrls(const KFileItem& destItem, const KUrl& destU const KUrl::List urls = KUrl::List::fromMimeData(event->mimeData()); foreach (const KUrl& url, urls) { if (url == destUrl) { - return i18nc("@info:status", "A folder cannot be dropped into itself"); + error = i18nc("@info:status", "A folder cannot be dropped into itself"); + return 0; } } - KonqOperations::doDrop(destItem, destUrl, event, QApplication::activeWindow()); + return KonqOperations::doDrop(destItem, destUrl, event, QApplication::activeWindow(), QList<QAction*>()); } else { - KonqOperations::doDrop(KFileItem(), destUrl, event, QApplication::activeWindow()); + return KonqOperations::doDrop(KFileItem(), destUrl, event, QApplication::activeWindow(), QList<QAction*>()); } - return QString(); + return 0; } diff --git a/src/views/draganddrophelper.h b/src/views/draganddrophelper.h index ac16f7cf2..eda5fc5c2 100644 --- a/src/views/draganddrophelper.h +++ b/src/views/draganddrophelper.h @@ -29,6 +29,7 @@ class KFileItem; class KUrl; class QDropEvent; class QWidget; +class KonqOperations; class LIBDOLPHINPRIVATE_EXPORT DragAndDropHelper { @@ -46,13 +47,15 @@ public: * @param destUrl URL of the item destination. Is used only if destItem::isNull() * is true. * @param event Drop event. - * @return Error message intended to be shown for users if dropping is not + * @param error Error message intended to be shown for users if dropping is not * possible. If an empty string is returned, the dropping has been * successful. + * @return KonqOperations pointer */ - static QString dropUrls(const KFileItem& destItem, - const KUrl& destUrl, - QDropEvent* event); + static KonqOperations* dropUrls(const KFileItem& destItem, + const KUrl& destUrl, + QDropEvent* event, + QString& error); }; #endif diff --git a/src/views/versioncontrol/updateitemstatesthread.cpp b/src/views/versioncontrol/updateitemstatesthread.cpp index e07d72c76..fa005f8f1 100644 --- a/src/views/versioncontrol/updateitemstatesthread.cpp +++ b/src/views/versioncontrol/updateitemstatesthread.cpp @@ -23,13 +23,13 @@ #include <QMutexLocker> -UpdateItemStatesThread::UpdateItemStatesThread() : +UpdateItemStatesThread::UpdateItemStatesThread(KVersionControlPlugin* plugin, + const QList<VersionControlObserver::ItemState>& itemStates) : QThread(), m_globalPluginMutex(0), - m_plugin(0), - m_itemMutex(), + m_plugin(plugin), m_retrievedItems(false), - m_itemStates() + m_itemStates(itemStates) { // Several threads may share one instance of a plugin. A global // mutex is required to serialize the retrieval of version control @@ -42,32 +42,16 @@ UpdateItemStatesThread::~UpdateItemStatesThread() { } -void UpdateItemStatesThread::setData(KVersionControlPlugin* plugin, - const QList<VersionControlObserver::ItemState>& itemStates) -{ - // The locks are taken in the same order as in run() - // to avoid potential deadlock. - QMutexLocker pluginLocker(m_globalPluginMutex); - QMutexLocker itemLocker(&m_itemMutex); - - m_itemStates = itemStates; - m_plugin = plugin; -} - void UpdateItemStatesThread::run() { Q_ASSERT(!m_itemStates.isEmpty()); Q_ASSERT(m_plugin); - QMutexLocker itemLocker(&m_itemMutex); - const QString directory = m_itemStates.first().item.url().directory(KUrl::AppendTrailingSlash); m_retrievedItems = false; - itemLocker.unlock(); QMutexLocker pluginLocker(m_globalPluginMutex); if (m_plugin->beginRetrieval(directory)) { - itemLocker.relock(); const int count = m_itemStates.count(); KVersionControlPlugin2* pluginV2 = qobject_cast<KVersionControlPlugin2*>(m_plugin); @@ -99,13 +83,11 @@ void UpdateItemStatesThread::unlockPlugin() QList<VersionControlObserver::ItemState> UpdateItemStatesThread::itemStates() const { - QMutexLocker locker(&m_itemMutex); return m_itemStates; } bool UpdateItemStatesThread::retrievedItems() const { - QMutexLocker locker(&m_itemMutex); return m_retrievedItems; } diff --git a/src/views/versioncontrol/updateitemstatesthread.h b/src/views/versioncontrol/updateitemstatesthread.h index f0f91d7d2..a28169755 100644 --- a/src/views/versioncontrol/updateitemstatesthread.h +++ b/src/views/versioncontrol/updateitemstatesthread.h @@ -38,9 +38,6 @@ class LIBDOLPHINPRIVATE_EXPORT UpdateItemStatesThread : public QThread Q_OBJECT public: - UpdateItemStatesThread(); - virtual ~UpdateItemStatesThread(); - /** * @param plugin Version control plugin that is used to update the * state of the items. Whenever the plugin is accessed @@ -49,8 +46,9 @@ public: * UpdateItemStatesThread::unlockPlugin() must be used. * @param itemStates List of items, where the states get updated. */ - void setData(KVersionControlPlugin* plugin, - const QList<VersionControlObserver::ItemState>& itemStates); + UpdateItemStatesThread(KVersionControlPlugin* plugin, + const QList<VersionControlObserver::ItemState>& itemStates); + virtual ~UpdateItemStatesThread(); /** * Whenever the plugin is accessed by the thread creator, lockPlugin() must @@ -76,7 +74,6 @@ private: QMutex* m_globalPluginMutex; // Protects the m_plugin globally KVersionControlPlugin* m_plugin; - mutable QMutex m_itemMutex; // Protects m_retrievedItems and m_itemStates bool m_retrievedItems; QList<VersionControlObserver::ItemState> m_itemStates; }; diff --git a/src/views/versioncontrol/versioncontrolobserver.cpp b/src/views/versioncontrol/versioncontrolobserver.cpp index 64bc26867..402a2de54 100644 --- a/src/views/versioncontrol/versioncontrolobserver.cpp +++ b/src/views/versioncontrol/versioncontrolobserver.cpp @@ -108,12 +108,7 @@ QList<QAction*> VersionControlObserver::actions(const KFileItemList& items) cons if (pluginV2) { // Use version 2 of the KVersionControlPlugin which allows providing actions // also for non-versioned directories. - if (m_updateItemStatesThread && m_updateItemStatesThread->lockPlugin()) { - actions = pluginV2->actions(items); - m_updateItemStatesThread->unlockPlugin(); - } else { - actions = pluginV2->actions(items); - } + actions = pluginV2->actions(items); } else if (isVersioned()) { // Support deprecated interfaces from KVersionControlPlugin version 1. // Context menu actions where only available for versioned directories. @@ -125,14 +120,8 @@ QList<QAction*> VersionControlObserver::actions(const KFileItemList& items) cons } } - if (m_updateItemStatesThread && m_updateItemStatesThread->lockPlugin()) { - actions = directory.isEmpty() ? m_plugin->contextMenuActions(items) - : m_plugin->contextMenuActions(directory); - m_updateItemStatesThread->unlockPlugin(); - } else { - actions = directory.isEmpty() ? m_plugin->contextMenuActions(items) - : m_plugin->contextMenuActions(directory); - } + actions = directory.isEmpty() ? m_plugin->contextMenuActions(items) + : m_plugin->contextMenuActions(directory); } return actions; @@ -238,20 +227,12 @@ void VersionControlObserver::slotThreadFinished() void VersionControlObserver::updateItemStates() { Q_ASSERT(m_plugin); - if (!m_updateItemStatesThread) { - m_updateItemStatesThread = new UpdateItemStatesThread(); - connect(m_updateItemStatesThread, SIGNAL(finished()), - this, SLOT(slotThreadFinished())); - connect(m_updateItemStatesThread, SIGNAL(finished()), - m_updateItemStatesThread, SLOT(deleteLater())); - } - else { + if (m_updateItemStatesThread) { // An update is currently ongoing. Wait until the thread has finished // the update (see slotThreadFinished()). m_pendingItemStatesUpdate = true; return; } - QList<ItemState> itemStates; const int itemCount = m_model->count(); itemStates.reserve(itemCount); @@ -269,7 +250,12 @@ void VersionControlObserver::updateItemStates() if (!m_silentUpdate) { emit infoMessage(i18nc("@info:status", "Updating version information...")); } - m_updateItemStatesThread->setData(m_plugin, itemStates); + m_updateItemStatesThread = new UpdateItemStatesThread(m_plugin, itemStates); + connect(m_updateItemStatesThread, SIGNAL(finished()), + this, SLOT(slotThreadFinished())); + connect(m_updateItemStatesThread, SIGNAL(finished()), + m_updateItemStatesThread, SLOT(deleteLater())); + m_updateItemStatesThread->start(); // slotThreadFinished() is called when finished } } |
