From ca7bd7b1c80e537e52047c3811180869003e6da4 Mon Sep 17 00:00:00 2001 From: Alf Gaida Date: Tue, 22 May 2018 00:37:24 +0200 Subject: [PATCH] Adding upstream version 0.13.0. Signed-off-by: Alf Gaida --- AUTHORS | 4 +- CHANGELOG | 81 ++- CMakeLists.txt | 19 +- Doxyfile.in | 12 +- README.md | 36 +- data/terminals.list | 3 + src/CMakeLists.txt | 14 +- src/appchoosercombobox.cpp | 7 +- src/appmenuview.cpp | 1 - src/appmenuview_p.h | 1 - src/archiver.h | 143 ----- src/cachedfoldermodel.cpp | 2 +- src/core/archiver.cpp | 174 ++++++ src/core/archiver.h | 69 +++ src/core/basicfilelauncher.cpp | 345 ++++++++++++ src/core/basicfilelauncher.h | 72 +++ src/core/bookmarks.cpp | 55 ++ src/core/bookmarks.h | 21 +- src/core/compat_p.h | 53 -- src/core/copyjob.cpp | 453 --------------- src/core/copyjob.h | 46 -- src/core/deletejob.cpp | 47 +- src/core/deletejob.h | 9 +- src/core/dirlistjob.cpp | 4 +- src/core/filechangeattrjob.cpp | 317 ++++++++++- src/core/filechangeattrjob.h | 130 ++++- src/core/fileinfo.cpp | 55 +- src/core/fileinfo.h | 23 +- src/core/fileinfo_p.h | 2 +- src/core/fileinfojob.cpp | 10 +- src/core/fileinfojob.h | 7 +- src/core/fileoperationjob.cpp | 33 +- src/core/fileoperationjob.h | 23 + src/core/filetransferjob.cpp | 641 ++++++++++++++++++++++ src/core/filetransferjob.h | 58 ++ src/core/folder.cpp | 76 +-- src/core/folder.h | 4 +- src/core/iconinfo.cpp | 55 +- src/core/iconinfo.h | 6 +- src/core/templates.cpp | 130 +++++ src/core/templates.h | 100 ++++ src/core/thumbnailer.cpp | 19 +- src/core/thumbnailer.h | 2 +- src/core/thumbnailjob.cpp | 4 +- src/core/totalsizejob.cpp | 25 +- src/core/trashjob.cpp | 38 +- src/core/trashjob.h | 3 +- src/core/untrashjob.cpp | 164 ++---- src/core/untrashjob.h | 6 +- src/createnewmenu.cpp | 124 +++-- src/createnewmenu.h | 15 +- src/customactions/fileactioncondition.cpp | 11 +- src/customactions/fileactionprofile.cpp | 3 + src/dirtreemodel.cpp | 4 +- src/dirtreemodelitem.cpp | 11 +- src/dirtreeview.cpp | 2 +- src/dnddest.cpp | 1 + src/editbookmarksdialog.cpp | 4 +- src/execfiledialog.cpp | 41 +- src/execfiledialog_p.h | 16 +- src/file-operation-dialog.ui | 17 +- src/file-props.ui | 19 +- src/filedialog.cpp | 78 ++- src/filelauncher.cpp | 121 ++-- src/filelauncher.h | 47 +- src/filemenu.cpp | 34 +- src/filemenu_p.h | 1 - src/fileoperation.cpp | 356 +++++++----- src/fileoperation.h | 102 ++-- src/fileoperationdialog.cpp | 60 +- src/fileoperationdialog.h | 8 +- src/fileoperationdialog_p.h | 69 +++ src/filepropsdialog.cpp | 11 +- src/filepropsdialog.h | 4 +- src/filesearchdialog.cpp | 7 +- src/filesearchdialog.h | 6 +- src/fm-search.c | 6 +- src/fm-search.h | 4 +- src/folderitemdelegate.cpp | 28 +- src/foldermodel.cpp | 31 +- src/foldermodel.h | 2 + src/foldermodelitem.cpp | 5 +- src/foldermodelitem.h | 1 - src/folderview.cpp | 159 +++++- src/folderview.h | 11 +- src/icontheme.cpp | 80 --- src/icontheme.h | 53 -- src/libfm-qt.pc.in | 2 +- src/libfmqt.cpp | 4 - src/mountoperation.cpp | 20 + src/mountoperation.h | 9 +- src/path.h | 441 --------------- src/pathbar.cpp | 3 +- src/pathedit.cpp | 2 +- src/placesmodel.cpp | 46 +- src/placesmodelitem.cpp | 3 +- src/placesview.cpp | 233 +++++++- src/placesview.h | 41 ++ src/proxyfoldermodel.cpp | 46 +- src/proxyfoldermodel.h | 6 + src/renamedialog.cpp | 54 +- src/renamedialog.h | 4 +- src/sidepane.cpp | 13 +- src/sidepane.h | 5 + src/templates.h | 158 ------ src/utilities.cpp | 38 +- src/utilities.h | 11 +- src/xdndworkaround.cpp | 2 +- src/xdndworkaround.h | 5 +- 109 files changed, 3911 insertions(+), 2389 deletions(-) delete mode 100644 src/archiver.h create mode 100644 src/core/archiver.cpp create mode 100644 src/core/archiver.h create mode 100644 src/core/basicfilelauncher.cpp create mode 100644 src/core/basicfilelauncher.h delete mode 100644 src/core/compat_p.h delete mode 100644 src/core/copyjob.cpp delete mode 100644 src/core/copyjob.h create mode 100644 src/core/filetransferjob.cpp create mode 100644 src/core/filetransferjob.h create mode 100644 src/core/templates.cpp create mode 100644 src/core/templates.h create mode 100644 src/fileoperationdialog_p.h delete mode 100644 src/icontheme.cpp delete mode 100644 src/icontheme.h delete mode 100644 src/path.h delete mode 100644 src/templates.h diff --git a/AUTHORS b/AUTHORS index 40631a6..a95b73b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,6 +1,6 @@ Upstream Authors: - LXQt team: http://lxqt.org + LXQt team: https://lxqt.org Hong Jen Yee (PCMan) Copyright: - Copyright (c) 2013-2017 LXQt team + Copyright (c) 2013-2018 LXQt team diff --git a/CHANGELOG b/CHANGELOG index 55392c4..06bfd1f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,86 @@ -libfm-qt-0.12.0 / 2017-10-21 +libfm-qt-0.13.0 / 2018-05-21 ============================ + * Bumped minor version to 13 + * Fixed shortcut detection + * Delete CachedFolderModel immediately on unreffing it + * Fix crashes in new templates code + * Remove the C++ wrapper for the old FmPath struct in libfm. + * Avoid using FmPath from libfm in FmSearch. + * Removed redundant code + * Modify Fm::BasicFileLauncher APIs to use Fm::FileInfoPtr consistently. + * Add typedef FileInfoPtr for std::shared_ptr since it's so frequently used. + * Correctly handle the mounting of mountable paths and correctly parse the target URIs of shortcuts. + * Add new APIs Fm::MountOperation::mountEnclosingVolume() and Fm::MountOperation::mountMountable() and deprecate Fm::MountOperation::mount(). + * Also cover local shortcuts + * Added "reject" to exec dialog + * Delete some libfm compatibility wrappers. + * Delete the unused old FmTemplate wrapper. + * Create the template items in the "Create New" menu with the new C++ file template APIs. + * Port template support to C++ (Fm::Templates class). + * Add convinient functions in Fm::FileOperation to set dest paths for file transfer jobs. + * Made job async again + * Fixed launching of desktop files + * Cleanup + * CMake: Prevent in-source builds + * Fix Fm::FileInfo::isDir() and isExecutableType() to make its behavior consistent with libfm. + * Port file lancher to C++ and Implement Fm::BasicFileLauncher base class. * Migrate Fm::FileLauncher to the new C++ implementation. + * Port archiver integration to C++ (#182) + * Fully port all file operations to C++ 11 (#181) + * Add kitty to terminals.list + * Italic font for hidden items + * Select multiple files + * No visible cursor in File Properties labels + * Just corrected a misspelling + * Elided labels for file operation dialog + * fix namespace, fixes lxqt/libfm-qt/issues/174 + * Misc link fixes + * Fixes some pathes after repo move + * Fix a cause of crash in `AppChooserComboBox` + * build: Bump version + * Drop Fm::IconTheme + * iconinfo: Properly handle multiple names + * Some code cleanup + * Fixed custom action execution mode + * Don't drop on files + * Prevent possible c++11 range-loop container detach + * See `.bak` and `.old` as backup extensions; also follow GLib + * Ensure that rename editor has opaque background + * Use special/custom folder icons for bookmarks + * Added two missing cases of `mapFromSource()` + * Support hiding items in Places side-pane + * Fixed sorting by type/owner + * Fix lambda connections in filedialog (#159) + * Drop Q_FOREACH + * Fix memory leak in thumbnail loading (#150) + * Be more tolerant + * Fix the "Folders" key in custom actions + * Add Group column and don't use owner's full name + * Fix comparison of integers of different signs + * Guarantee 64-bit time attributes (#148) + * Added a proxy setting for backup as hidden (#145) + * Fixed the logic of queued deletion + * Track folders containing cut files only with QString + * Copy selected pathbar text to selection clipboard + * Smooth scrolling for icon and thumbnail views + * cmake: Handle CMP0071 + * Prepare libfm-qt for bulk rename + * No change queue of files in the deletion queue + * FileInfo: Fix potential SEGFAULT + * Fix two devices for one mount + * Always wait for the folder to load before selecting + * Really cancel multiple renaming on cancelling + * Prevent a potential crash in xdndworkaround + * Fix wrong gray-out of cut files + * Merge side-pane with its surroundings + * Prompt dialog specifically for desktop files + * Remove unnecessary noise + +0.12.0 / 2017-10-21 +=================== + + * Release 0.12.0: Update changelog * Add data transferred to file operation dialog. * Bump versions * Disable context-menu actions that cannot be used diff --git a/CMakeLists.txt b/CMakeLists.txt index 064bdd7..4f2b822 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,27 +4,27 @@ project(libfm-qt) set(LIBFM_QT_LIBRARY_NAME "fm-qt" CACHE STRING "fm-qt") set(LIBFM_QT_VERSION_MAJOR 0) -set(LIBFM_QT_VERSION_MINOR 12) +set(LIBFM_QT_VERSION_MINOR 13) set(LIBFM_QT_VERSION_PATCH 0) set(LIBFM_QT_VERSION ${LIBFM_QT_VERSION_MAJOR}.${LIBFM_QT_VERSION_MINOR}.${LIBFM_QT_VERSION_PATCH}) list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") # We use the libtool versioning scheme for the internal so name, "current:revision:age" -# http://www.gnu.org/software/libtool/manual/html_node/Updating-version-info.html#Updating-version-info +# https://www.gnu.org/software/libtool/manual/html_node/Updating-version-info.html#Updating-version-info # https://www.sourceware.org/autobook/autobook/autobook_91.html # http://pusling.com/blog/?p=352 # Actually, libtool uses different ways on different operating systems. So there is no # universal way to translate a libtool version-info to a cmake version. # We use "(current-age).age.revision" as the cmake version. -# current: 4, revision: 0, age: 1 => version: 3.1.0 -set(LIBFM_QT_LIB_VERSION "3.1.0") -set(LIBFM_QT_LIB_SOVERSION "3") +# current: 5, revision: 0, age: 0 => version: 5.0.0 +set(LIBFM_QT_LIB_VERSION "5.0.0") +set(LIBFM_QT_LIB_SOVERSION "5") -set(REQUIRED_QT_VERSION "5.2") +set(REQUIRED_QT_VERSION "5.7.1") set(REQUIRED_LIBFM_VERSION "1.2.0") set(REQUIRED_LIBMENUCACHE_VERSION "0.4.0") -set(REQUIRED_LXQT_BUILD_TOOLS_VERSION "0.4.0") +set(REQUIRED_LXQT_BUILD_TOOLS_VERSION "0.5.0") if (NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release) @@ -46,11 +46,13 @@ option(UPDATE_TRANSLATIONS "Update source translation translations/*.ts files" O include(GNUInstallDirs) include(GenerateExportHeader) include(CMakePackageConfigHelpers) +include(LXQtPreventInSourceBuilds) include(LXQtTranslateTs) include(LXQtTranslateDesktop) include(LXQtCompilerSettings NO_POLICY_SCOPE) set(CMAKE_AUTOMOC TRUE) +set(CMAKE_AUTOUIC ON) set(CMAKE_INCLUDE_CURRENT_DIR ON) write_basic_package_version_file( @@ -70,8 +72,7 @@ add_subdirectory(data) # add Doxygen support to generate API docs # References: -# http://majewsky.wordpress.com/2010/08/14/tip-of-the-day-cmake-and-doxygen/ -# http://www.bluequartz.net/projects/EIM_Segmentation/SoftwareDocumentation/html/usewithcmakeproject.html +# https://majewsky.wordpress.com/2010/08/14/tip-of-the-day-cmake-and-doxygen/ option(BUILD_DOCUMENTATION "Use Doxygen to create the HTML based API documentation" OFF) if(BUILD_DOCUMENTATION) find_package(Doxygen REQUIRED) diff --git a/Doxyfile.in b/Doxyfile.in index b7961be..9b102f0 100644 --- a/Doxyfile.in +++ b/Doxyfile.in @@ -20,7 +20,7 @@ # that follow. The default is UTF-8 which is also the encoding used for all # text before the first occurrence of this tag. Doxygen uses libiconv (or the # iconv built into libc) for the transcoding. See -# http://www.gnu.org/software/libiconv for the list of possible encodings. +# https://www.gnu.org/software/libiconv for the list of possible encodings. DOXYFILE_ENCODING = UTF-8 @@ -247,7 +247,7 @@ EXTENSION_MAPPING = # If MARKDOWN_SUPPORT is enabled (the default) then doxygen pre-processes all # comments according to the Markdown format, which allows for more readable -# documentation. See http://daringfireball.net/projects/markdown/ for details. +# documentation. See https://daringfireball.net/projects/markdown/ for details. # The output of markdown processing is further processed by doxygen, so you # can mix doxygen, HTML, and XML commands with Markdown formatting. # Disable only in case of backward compatibilities issues. @@ -587,7 +587,7 @@ LAYOUT_FILE = # containing the references data. This must be a list of .bib files. The # .bib extension is automatically appended if omitted. Using this command # requires the bibtex tool to be installed. See also -# http://en.wikipedia.org/wiki/BibTeX for more info. For LaTeX the style +# https://en.wikipedia.org/wiki/BibTeX for more info. For LaTeX the style # of the bibliography can be controlled using LATEX_BIB_STYLE. To use this # feature you need bibtex and perl available in the search path. Do not use # file names with spaces, bibtex cannot handle them. @@ -659,7 +659,7 @@ INPUT = "@PROJECT_SOURCE_DIR@/src" # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is # also the default input encoding. Doxygen uses libiconv (or the iconv built -# into libc) for the transcoding. See http://www.gnu.org/software/libiconv for +# into libc) for the transcoding. See https://www.gnu.org/software/libiconv for # the list of possible encodings. INPUT_ENCODING = UTF-8 @@ -931,7 +931,7 @@ HTML_EXTRA_FILES = # The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. # Doxygen will adjust the colors in the style sheet and background images # according to this color. Hue is specified as an angle on a colorwheel, -# see http://en.wikipedia.org/wiki/Hue for more information. +# see https://en.wikipedia.org/wiki/Hue for more information. # For instance the value 0 represents red, 60 is yellow, 120 is green, # 180 is cyan, 240 is blue, 300 purple, and 360 is red again. # The allowed range is 0 to 359. @@ -984,7 +984,7 @@ HTML_INDEX_NUM_ENTRIES = 100 # directory and running "make install" will install the docset in # ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find # it at startup. -# See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html +# See https://developer.apple.com/tools/creatingdocsetswithdoxygen.html # for more information. GENERATE_DOCSET = NO diff --git a/README.md b/README.md index 29ec1b8..d3e3ade 100644 --- a/README.md +++ b/README.md @@ -2,26 +2,44 @@ ## Overview -libfm-qt is the Qt port of libfm, a library providing components to build desktop file managers which belongs to [LXDE](http://lxde.org). +libfm-qt is the Qt port of libfm, a library providing components to build +desktop file managers which belongs to [LXDE](https://lxde.org). -libfm-qt is licensed under the terms of the [LGPLv2.1](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html) or any later version. See file LICENSE for its full text. +libfm-qt is licensed under the terms of the +[LGPLv2.1](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html) +or any later version. See file LICENSE for its full text. ## Installation ### Compiling source code -Runtime dependencies are Qt X11 Extras and libfm ≥ 1,2 (not all features are provided by libfm-qt yet). -Additional build dependencies are CMake, [lxqt-build-tools](https://github.com/lxde/lxqt-build-tools) and optionally Git to pull latest VCS checkouts. The localization files were outsourced to repository [lxqt-l10n](https://github.com/lxde/lxqt-l10n) so the corresponding dependencies are needed, too. Please refer to this repository's `README.md` for further information. +Runtime dependencies are Qt X11 Extras and libfm ≥ 1.2 +(not all features are provided by libfm-qt yet). +Additional build dependencies are CMake, +[lxqt-build-tools](https://github.com/lxqt/lxqt-build-tools) and optionally Git +to pull latest VCS checkouts. The localization files were outsourced to +repository [lxqt-l10n](https://github.com/lxqt/lxqt-l10n) so the corresponding +dependencies are needed, too. Please refer to this repository's `README.md` for +further information. -Code configuration is handled by CMake. CMake variable `CMAKE_INSTALL_PREFIX` has to be set to `/usr` on most operating systems, depending on the way library paths are dealt with on 64bit systems variables like `CMAKE_INSTALL_LIBDIR` may have to be set as well. +Code configuration is handled by CMake. CMake variable `CMAKE_INSTALL_PREFIX` +has to be set to `/usr` on most operating systems, depending on the way library +paths are dealt with on 64bit systems variables like `CMAKE_INSTALL_LIBDIR` may +have to be set as well. -To build run `make`, to install `make install` which accepts variable `DESTDIR` as usual. +To build run `make`, to install `make install` which accepts variable `DESTDIR` +as usual. ### Binary packages -Official binary packages are available in Arch Linux, Debian (as of Debian stretch) and openSUSE (Leap 42.1 and Tumbleweed). -The library is still missing in Fedora which is providing version 0.10.0 of PCManFM-Qt only so far. This version was still including the code outsourced into libfm-qt later so libfm-qt will have to be provided by Fedora, too, as soon as the distribution upgrades to PCManFM-Qt ≥ 0.10.1. +Official binary packages are available in Arch Linux, Debian (as of Debian +stretch) and openSUSE (Leap 42.1 and Tumbleweed). +The library is still missing in Fedora which is providing version 0.10.0 of +PCManFM-Qt only so far. This version was still including the code outsourced +into libfm-qt later so libfm-qt will have to be provided by Fedora, too, +as soon as the distribution upgrades to PCManFM-Qt ≥ 0.10.1. ## Development -Issues should go to the tracker of PCManFM-Qt at https://github.com/lxde/pcmanfm-qt/issues. +Issues should go to the tracker of PCManFM-Qt at +https://github.com/lxqt/pcmanfm-qt/issues. diff --git a/data/terminals.list b/data/terminals.list index ec6d202..3c49a10 100644 --- a/data/terminals.list +++ b/data/terminals.list @@ -75,3 +75,6 @@ desktop_id=terminology.desktop open_arg=-e noclose_arg=--hold -e desktop_id=termite.desktop + +[kitty] +desktop_id=kitty.desktop diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b586381..0695854 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -9,7 +9,7 @@ set(libfm_core_SRCS core/filemonitor.cpp # i/o jobs core/job.cpp - core/copyjob.cpp + core/filetransferjob.cpp core/deletejob.cpp core/dirlistjob.cpp core/filechangeattrjob.cpp @@ -24,10 +24,13 @@ set(libfm_core_SRCS core/thumbnailjob.cpp # extra desktop services core/bookmarks.cpp + core/basicfilelauncher.cpp core/volumemanager.cpp core/userinfocache.cpp core/thumbnailer.cpp core/terminal.cpp + core/archiver.cpp + core/templates.cpp # custom actions customactions/fileaction.cpp customactions/fileactionprofile.cpp @@ -39,7 +42,6 @@ set(libfm_SRCS libfmqt.cpp bookmarkaction.cpp sidepane.cpp - icontheme.cpp filelauncher.cpp foldermodel.cpp foldermodelitem.cpp @@ -95,9 +97,6 @@ set(libfm_UIS filedialog.ui ) -qt5_wrap_ui(libfm_UIS_H ${libfm_UIS}) - - set(LIBFM_QT_DATA_DIR "${CMAKE_INSTALL_FULL_DATADIR}/libfm-qt") set(LIBFM_QT_INTREE_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/include") @@ -114,7 +113,7 @@ lxqt_translate_ts(QM_FILES add_library(${LIBFM_QT_LIBRARY_NAME} SHARED ${libfm_SRCS} - ${libfm_UIS_H} + ${libfm_UIS} ${QM_FILES} ) @@ -154,6 +153,7 @@ target_include_directories(${LIBFM_QT_LIBRARY_NAME} target_compile_definitions(${LIBFM_QT_LIBRARY_NAME} PRIVATE "LIBFM_QT_DATA_DIR=\"${LIBFM_QT_DATA_DIR}\"" + "QT_NO_FOREACH" PUBLIC "QT_NO_KEYWORDS" ) @@ -216,7 +216,7 @@ export(TARGETS ${LIBFM_QT_LIBRARY_NAME} set(REQUIRED_QT "Qt5Widgets >= ${REQUIRED_QT_VERSION} Qt5X11Extras >= ${REQUIRED_QT_VERSION}") configure_file(libfm-qt.pc.in lib${LIBFM_QT_LIBRARY_NAME}.pc @ONLY) # FreeBSD loves to install files to different locations -# http://www.freebsd.org/doc/handbook/dirstructure.html +# https://www.freebsd.org/doc/handbook/dirstructure.html if(${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") install(FILES "${CMAKE_CURRENT_BINARY_DIR}/lib${LIBFM_QT_LIBRARY_NAME}.pc" diff --git a/src/appchoosercombobox.cpp b/src/appchoosercombobox.cpp index bf7d0f1..24efa31 100644 --- a/src/appchoosercombobox.cpp +++ b/src/appchoosercombobox.cpp @@ -18,7 +18,6 @@ */ #include "appchoosercombobox.h" -#include "icontheme.h" #include "appchooserdialog.h" #include "utilities.h" #include "core/iconinfo.h" @@ -33,7 +32,7 @@ AppChooserComboBox::AppChooserComboBox(QWidget* parent): // the new Qt5 signal/slot syntax cannot handle overloaded methods by default // hence a type-casting is needed here. really ugly! - // reference: http://qt-project.org/forums/viewthread/21513 + // reference: https://forum.qt.io/topic/20998/qt5-new-signals-slots-syntax-does-not-work-solved connect((QComboBox*)this, static_cast(&QComboBox::currentIndexChanged), this, &AppChooserComboBox::onCurrentIndexChanged); } @@ -72,8 +71,10 @@ void AppChooserComboBox::setMimeType(std::shared_ptr mimeTyp // returns the currently selected app. Fm::GAppInfoPtr AppChooserComboBox::selectedApp() const { + // the elements of appInfos_ and the combo indexes before "Customize" + // always have a one-to-one correspondence int idx = currentIndex(); - return idx >= 0 ? appInfos_[idx] : Fm::GAppInfoPtr{}; + return idx >= 0 && !appInfos_.empty() ? appInfos_[idx] : Fm::GAppInfoPtr{}; } bool AppChooserComboBox::isChanged() const { diff --git a/src/appmenuview.cpp b/src/appmenuview.cpp index cc669d1..615b971 100644 --- a/src/appmenuview.cpp +++ b/src/appmenuview.cpp @@ -19,7 +19,6 @@ #include "appmenuview.h" #include -#include "icontheme.h" #include "appmenuview_p.h" #include diff --git a/src/appmenuview_p.h b/src/appmenuview_p.h index 815c84e..7dd3d53 100644 --- a/src/appmenuview_p.h +++ b/src/appmenuview_p.h @@ -22,7 +22,6 @@ #include #include -#include "icontheme.h" #include "core/iconinfo.h" namespace Fm { diff --git a/src/archiver.h b/src/archiver.h deleted file mode 100644 index 216a32c..0000000 --- a/src/archiver.h +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright (C) 2016 Hong Jen Yee (PCMan) - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * - */ - -#ifndef __LIBFM_QT_FM_ARCHIVER_H__ -#define __LIBFM_QT_FM_ARCHIVER_H__ - -#include -#include -#include -#include "libfmqtglobals.h" - - -namespace Fm { - - -class LIBFM_QT_API Archiver { -public: - - - // default constructor - Archiver() { - dataPtr_ = nullptr; - } - - - // move constructor - Archiver(Archiver&& other) noexcept { - dataPtr_ = reinterpret_cast(other.takeDataPtr()); - } - - - // destructor - ~Archiver() { - if(dataPtr_ != nullptr) { - (dataPtr_); - } - } - - - // create a wrapper for the data pointer without increasing the reference count - static Archiver wrapPtr(FmArchiver* dataPtr) { - Archiver obj; - obj.dataPtr_ = reinterpret_cast(dataPtr); - return obj; - } - - // disown the managed data pointer - FmArchiver* takeDataPtr() { - FmArchiver* data = reinterpret_cast(dataPtr_); - dataPtr_ = nullptr; - return data; - } - - // get the raw pointer wrapped - FmArchiver* dataPtr() { - return reinterpret_cast(dataPtr_); - } - - // automatic type casting - operator FmArchiver*() { - return dataPtr(); - } - - // automatic type casting - operator void*() { - return dataPtr(); - } - - - - // move assignment - Archiver& operator=(Archiver&& other) noexcept { - dataPtr_ = reinterpret_cast(other.takeDataPtr()); - return *this; - } - - bool isNull() { - return (dataPtr_ == nullptr); - } - - // methods - - void setDefault(void) { - fm_archiver_set_default(dataPtr()); - } - - - static Archiver getDefault( ) { - return wrapPtr(fm_archiver_get_default()); - } - - - bool extractArchivesTo(GAppLaunchContext* ctx, FmPathList* files, FmPath* dest_dir) { - return fm_archiver_extract_archives_to(dataPtr(), ctx, files, dest_dir); - } - - - bool extractArchives(GAppLaunchContext* ctx, FmPathList* files) { - return fm_archiver_extract_archives(dataPtr(), ctx, files); - } - - - bool createArchive(GAppLaunchContext* ctx, FmPathList* files) { - return fm_archiver_create_archive(dataPtr(), ctx, files); - } - - - bool isMimeTypeSupported(const char* type) { - return fm_archiver_is_mime_type_supported(dataPtr(), type); - } - - -// the wrapped object cannot be copied. -private: - Archiver(const Archiver& other) = delete; - Archiver& operator=(const Archiver& other) = delete; - - -private: - FmArchiver* dataPtr_; // data pointer for the underlying C struct - -}; - - -} - -#endif // __LIBFM_QT_FM_ARCHIVER_H__ diff --git a/src/cachedfoldermodel.cpp b/src/cachedfoldermodel.cpp index c594cb9..defa398 100644 --- a/src/cachedfoldermodel.cpp +++ b/src/cachedfoldermodel.cpp @@ -59,7 +59,7 @@ void CachedFolderModel::unref() { --refCount; if(refCount <= 0) { folder()->setProperty(cacheKey, QVariant()); - deleteLater(); + delete(this); } } diff --git a/src/core/archiver.cpp b/src/core/archiver.cpp new file mode 100644 index 0000000..b38990e --- /dev/null +++ b/src/core/archiver.cpp @@ -0,0 +1,174 @@ +#include "libfmqtglobals.h" +#include "archiver.h" + +#include +#include +#include + +#include + +namespace Fm { + +Archiver* Archiver::defaultArchiver_ = nullptr; // static +std::vector> Archiver::allArchivers_; // static + +Archiver::Archiver() { +} + +bool Archiver::isMimeTypeSupported(const char* type) { + char** p; + if(G_UNLIKELY(!type)) { + return false; + } + for(p = mimeTypes_.get(); *p; ++p) { + if(strcmp(*p, type) == 0) { + return true; + } + } + return false; +} + +bool Archiver::launchProgram(GAppLaunchContext* ctx, const char* cmd, const FilePathList& files, const FilePath& dir) { + char* _cmd = NULL; + const char* dir_place_holder; + GKeyFile* dummy; + + if(dir.isValid() && (dir_place_holder = strstr(cmd, "%d"))) { + CStrPtr dir_str; + int len; + if(strstr(cmd, "%U") || strstr(cmd, "%u")) { /* supports URI */ + dir_str = dir.uri(); + } + else { + dir_str = dir.localPath(); + } + + // FIXME: remove libfm dependency here + /* replace all % with %% so encoded URI can be handled correctly when parsing Exec key. */ + std::string percentEscapedDir; + for(auto p = dir_str.get(); *p; ++p) { + percentEscapedDir += *p; + if(*p == '%') { + percentEscapedDir += '%'; + } + } + + /* quote the path or URI */ + dir_str = CStrPtr{g_shell_quote(percentEscapedDir.c_str())}; + + len = strlen(cmd) - 2 + strlen(dir_str.get()) + 1; + _cmd = (char*)g_malloc(len); + len = (dir_place_holder - cmd); + strncpy(_cmd, cmd, len); + strcpy(_cmd + len, dir_str.get()); + strcat(_cmd, dir_place_holder + 2); + cmd = _cmd; + } + + /* create a fake key file to cheat GDesktopAppInfo */ + dummy = g_key_file_new(); + g_key_file_set_string(dummy, G_KEY_FILE_DESKTOP_GROUP, "Type", "Application"); + g_key_file_set_string(dummy, G_KEY_FILE_DESKTOP_GROUP, "Name", program_.get()); + + /* replace all % with %% so encoded URI can be handled correctly when parsing Exec key. */ + g_key_file_set_string(dummy, G_KEY_FILE_DESKTOP_GROUP, "Exec", cmd); + GAppInfoPtr app{reinterpret_cast(g_desktop_app_info_new_from_keyfile(dummy)), false}; + + g_key_file_free(dummy); + g_debug("cmd = %s", cmd); + if(app) { + GList* uris = NULL; + for(auto& file: files) { + uris = g_list_prepend(uris, g_strdup(file.uri().get())); + } + g_app_info_launch_uris(app.get(), uris, ctx, NULL); + g_list_foreach(uris, (GFunc)g_free, NULL); + g_list_free(uris); + } + g_free(_cmd); + return true; +} + +bool Archiver::createArchive(GAppLaunchContext* ctx, const FilePathList& files) { + if(createCmd_ && !files.empty()) { + launchProgram(ctx, createCmd_.get(), files, FilePath{}); + } + return false; +} + +bool Archiver::extractArchives(GAppLaunchContext* ctx, const FilePathList& files) { + if(extractCmd_ && !files.empty()) { + launchProgram(ctx, extractCmd_.get(), files, FilePath{}); + } + return false; +} + +bool Archiver::extractArchivesTo(GAppLaunchContext* ctx, const FilePathList& files, const FilePath& dest_dir) { + if(extractToCmd_ && !files.empty()) { + launchProgram(ctx, extractToCmd_.get(), files, dest_dir); + } + return false; +} + +// static +Archiver* Archiver::defaultArchiver() { + allArchivers(); // to have a preliminary default archiver + return defaultArchiver_; +} + +void Archiver::setDefaultArchiverByName(const char *name) { + if(name) { + auto& all = allArchivers(); + for(auto& archiver: all) { + if(archiver->program_ && strcmp(archiver->program_.get(), name) == 0) { + defaultArchiver_ = archiver.get(); + break; + } + } + } +} + +// static +void Archiver::setDefaultArchiver(Archiver* archiver) { + if(archiver) { + defaultArchiver_ = archiver; + } +} + +// static +const std::vector >& Archiver::allArchivers() { + // load all archivers on demand + if(allArchivers_.empty()) { + GKeyFile* kf = g_key_file_new(); + if(g_key_file_load_from_file(kf, LIBFM_QT_DATA_DIR "/archivers.list", G_KEY_FILE_NONE, NULL)) { + gsize n_archivers; + CStrArrayPtr programs{g_key_file_get_groups(kf, &n_archivers)}; + if(programs) { + gsize i; + for(i = 0; i < n_archivers; ++i) { + auto program = programs[i]; + std::unique_ptr archiver{new Archiver{}}; + archiver->createCmd_ = CStrPtr{g_key_file_get_string(kf, program, "create", NULL)}; + archiver->extractCmd_ = CStrPtr{g_key_file_get_string(kf, program, "extract", NULL)}; + archiver->extractToCmd_ = CStrPtr{g_key_file_get_string(kf, program, "extract_to", NULL)}; + archiver->mimeTypes_ = CStrArrayPtr{g_key_file_get_string_list(kf, program, "mime_types", NULL, NULL)}; + archiver->program_ = CStrPtr{g_strdup(program)}; + + // if default archiver is not set, find the first program existing in the current system. + if(!defaultArchiver_) { + CStrPtr fullPath{g_find_program_in_path(program)}; + if(fullPath) { + defaultArchiver_ = archiver.get(); + } + } + + allArchivers_.emplace_back(std::move(archiver)); + } + } + } + g_key_file_free(kf); + } + return allArchivers_; +} + +} // namespace Fm diff --git a/src/core/archiver.h b/src/core/archiver.h new file mode 100644 index 0000000..ba5368d --- /dev/null +++ b/src/core/archiver.h @@ -0,0 +1,69 @@ +#ifndef ARCHIVER_H +#define ARCHIVER_H + +#include "../libfmqtglobals.h" +#include "filepath.h" +#include "gioptrs.h" + +#include +#include + +namespace Fm { + +class LIBFM_QT_API Archiver { +public: + Archiver(); + + bool isMimeTypeSupported(const char* type); + + bool canCreateArchive() const { + return createCmd_ != nullptr; + } + + bool createArchive(GAppLaunchContext* ctx, const FilePathList& files); + + bool canExtractArchives() const { + return extractCmd_ != nullptr; + } + + bool extractArchives(GAppLaunchContext* ctx, const FilePathList& files); + + bool canExtractArchivesTo() const { + return extractToCmd_ != nullptr; + } + + bool extractArchivesTo(GAppLaunchContext* ctx, const FilePathList& files, const FilePath& dest_dir); + + /* get default GUI archivers used by libfm */ + static Archiver* defaultArchiver(); + + /* set default GUI archivers used by libfm */ + static void setDefaultArchiverByName(const char* name); + + /* set default GUI archivers used by libfm */ + static void setDefaultArchiver(Archiver* archiver); + + /* get a list of FmArchiver* of all GUI archivers known to libfm */ + static const std::vector>& allArchivers(); + + const char* program() const { + return program_.get(); + } + +private: + bool launchProgram(GAppLaunchContext* ctx, const char* cmd, const FilePathList& files, const FilePath &dir); + +private: + CStrPtr program_; + CStrPtr createCmd_; + CStrPtr extractCmd_; + CStrPtr extractToCmd_; + CStrArrayPtr mimeTypes_; + + static Archiver* defaultArchiver_; + static std::vector> allArchivers_; +}; + +} // namespace Fm + +#endif // ARCHIVER_H diff --git a/src/core/basicfilelauncher.cpp b/src/core/basicfilelauncher.cpp new file mode 100644 index 0000000..2012e09 --- /dev/null +++ b/src/core/basicfilelauncher.cpp @@ -0,0 +1,345 @@ +#include "basicfilelauncher.h" +#include "fileinfojob.h" +#include "mountoperation.h" + +#include +#include + +#include +#include + +#include +#include +#include + +namespace Fm { + +BasicFileLauncher::BasicFileLauncher(): + quickExec_{false} { +} + +BasicFileLauncher::~BasicFileLauncher() { +} + +bool BasicFileLauncher::launchFiles(const FileInfoList& fileInfos, GAppLaunchContext* ctx) { + std::unordered_map mimeTypeToFiles; + FileInfoList folderInfos; + FilePathList pathsToLaunch; + // classify files according to different mimetypes + for(auto& fileInfo : fileInfos) { + // qDebug("path: %s, target: %s", fileInfo->path().toString().get(), fileInfo->target().c_str()); + if(fileInfo->isDir()) { + folderInfos.emplace_back(fileInfo); + } + else if(fileInfo->isMountable()) { + if(fileInfo->target().empty()) { + // the mountable is not yet mounted so we have no target URI. + GErrorPtr err{G_IO_ERROR, G_IO_ERROR_NOT_MOUNTED, + QObject::tr("The path is not mounted.")}; + if(!showError(ctx, err, fileInfo->path(), fileInfo)) { + // the user fail to handle the error, skip this file. + continue; + } + + // we do not have the target path in the FileInfo object. + // try to launch our path again to query the new file info later so we can get the mounted target URI. + pathsToLaunch.emplace_back(fileInfo->path()); + } + else { + // we have the target path, launch it later + pathsToLaunch.emplace_back(FilePath::fromPathStr(fileInfo->target().c_str())); + } + } + else if(fileInfo->isDesktopEntry()) { + // launch the desktop entry + launchDesktopEntry(fileInfo, FilePathList{}, ctx); + } + else if(fileInfo->isExecutableType()) { + // directly execute the file + launchExecutable(fileInfo, ctx); + } + else if(fileInfo->isShortcut()) { + // for shortcuts, launch their targets instead + auto path = handleShortcut(fileInfo, ctx); + if(path.isValid()) { + pathsToLaunch.emplace_back(path); + } + } + else { + auto& mimeType = fileInfo->mimeType(); + mimeTypeToFiles[mimeType->name()].emplace_back(fileInfo); + } + } + + // open folders + if(!folderInfos.empty()) { + GErrorPtr err; + openFolder(ctx, folderInfos, err); + } + + // open files of different mime-types with their default app + for(auto& typeFiles : mimeTypeToFiles) { + auto& mimeType = typeFiles.first; + auto& files = typeFiles.second; + GErrorPtr err; + GAppInfoPtr app{g_app_info_get_default_for_type(mimeType.c_str(), false), false}; + if(!app) { + app = chooseApp(files, mimeType.c_str(), err); + } + if(app) { + launchWithApp(app.get(), files.paths(), ctx); + } + } + + if(!pathsToLaunch.empty()) { + launchPaths(pathsToLaunch, ctx); + } + + return true; +} + +bool BasicFileLauncher::launchPaths(FilePathList paths, GAppLaunchContext* ctx) { + // FIXME: blocking with an event loop is not a good design :-( + QEventLoop eventLoop; + + auto job = new FileInfoJob{paths}; + job->setAutoDelete(false); // do not automatically delete the job since we want its results later. + + GObjectPtr ctxPtr{ctx}; + QObject::connect(job, &FileInfoJob::finished, + [&eventLoop]() { + // exit the event loop when the job is done + eventLoop.exit(); + }); + // run the job in another thread to not block the UI + job->runAsync(); + + // blocking until the job is done with a event loop + eventLoop.exec(); + + // launch the file info + launchFiles(job->files(), ctx); + + delete job; + return false; +} + +GAppInfoPtr BasicFileLauncher::chooseApp(const FileInfoList& /* fileInfos */, const char* /*mimeType*/, GErrorPtr& /* err */) { + return GAppInfoPtr{}; +} + +bool BasicFileLauncher::openFolder(GAppLaunchContext* ctx, const FileInfoList& folderInfos, GErrorPtr& err) { + auto app = chooseApp(folderInfos, "inode/directory", err); + if(app) { + launchWithApp(app.get(), folderInfos.paths(), ctx); + } + else { + showError(ctx, err); + } + return false; +} + +BasicFileLauncher::ExecAction BasicFileLauncher::askExecFile(const FileInfoPtr & /* file */) { + return ExecAction::DIRECT_EXEC; +} + +bool BasicFileLauncher::showError(GAppLaunchContext* /* ctx */, GErrorPtr& /* err */, const FilePath& /* path */, const FileInfoPtr& /* info */) { + return false; +} + +int BasicFileLauncher::ask(const char* /* msg */, char* const* /* btn_labels */, int default_btn) { + return default_btn; +} + +bool BasicFileLauncher::launchWithApp(GAppInfo* app, const FilePathList& paths, GAppLaunchContext* ctx) { + GList* uris = nullptr; + for(auto& path : paths) { + auto uri = path.uri(); + uris = g_list_prepend(uris, uri.release()); + } + GErrorPtr err; + bool ret = bool(g_app_info_launch_uris(app, uris, ctx, &err)); + g_list_foreach(uris, reinterpret_cast(g_free), nullptr); + g_list_free(uris); + if(!ret) { + // FIXME: show error for all files + showError(ctx, err, paths[0]); + } + return ret; +} + + +bool BasicFileLauncher::launchDesktopEntry(const FileInfoPtr &fileInfo, const FilePathList &paths, GAppLaunchContext* ctx) { + /* treat desktop entries as executables */ + auto target = fileInfo->target(); + CStrPtr filename; + const char* desktopEntryName = nullptr; + FilePathList shortcutTargetPaths; + if(fileInfo->isExecutableType()) { + auto act = quickExec_ ? ExecAction::DIRECT_EXEC : askExecFile(fileInfo); + switch(act) { + case ExecAction::EXEC_IN_TERMINAL: + case ExecAction::DIRECT_EXEC: { + if(fileInfo->isShortcut()) { + auto path = handleShortcut(fileInfo, ctx); + if(path.isValid()) { + shortcutTargetPaths.emplace_back(path); + } + } + else { + if(target.empty()) { + filename = fileInfo->path().localPath(); + } + desktopEntryName = !target.empty() ? target.c_str() : filename.get(); + } + break; + } + case ExecAction::OPEN_WITH_DEFAULT_APP: + return launchWithDefaultApp(fileInfo, ctx); + case ExecAction::CANCEL: + return false; + default: + return false; + } + } + /* make exception for desktop entries under menu */ + else if(fileInfo->isNative() /* an exception */ || + fileInfo->path().hasUriScheme("menu")) { + if(target.empty()) { + filename = fileInfo->path().localPath(); + } + desktopEntryName = !target.empty() ? target.c_str() : filename.get(); + } + + if(desktopEntryName) { + return launchDesktopEntry(desktopEntryName, paths, ctx); + } + if(!shortcutTargetPaths.empty()) { + launchPaths(shortcutTargetPaths, ctx); + } + return false; +} + +bool BasicFileLauncher::launchDesktopEntry(const char *desktopEntryName, const FilePathList &paths, GAppLaunchContext *ctx) { + bool ret = false; + GAppInfo* app; + + /* Let GDesktopAppInfo try first. */ + if(g_path_is_absolute(desktopEntryName)) { + app = G_APP_INFO(g_desktop_app_info_new_from_filename(desktopEntryName)); + } + else { + app = G_APP_INFO(g_desktop_app_info_new(desktopEntryName)); + } + /* we handle Type=Link in FmFileInfo so if GIO failed then + it cannot be launched in fact */ + + if(app) { + return launchWithApp(app, paths, ctx); + } + else { + QString msg = QObject::tr("Invalid desktop entry file: '%1'").arg(desktopEntryName); + GErrorPtr err{G_IO_ERROR, G_IO_ERROR_FAILED, msg}; + showError(ctx, err); + } + return ret; +} + +FilePath BasicFileLauncher::handleShortcut(const FileInfoPtr& fileInfo, GAppLaunchContext* ctx) { + auto target = fileInfo->target(); + auto scheme = CStrPtr{g_uri_parse_scheme(target.c_str())}; + if(scheme) { + // collect the uri schemes we support + if(strcmp(scheme.get(), "file") == 0 + || strcmp(scheme.get(), "trash") == 0 + || strcmp(scheme.get(), "network") == 0 + || strcmp(scheme.get(), "computer") == 0) { + return FilePath::fromUri(fileInfo->target().c_str()); + } + else { + // ask gio to launch the default handler for the uri scheme + GAppInfoPtr app{g_app_info_get_default_for_uri_scheme(scheme.get()), false}; + FilePathList uris{FilePath::fromUri(fileInfo->target().c_str())}; + launchWithApp(app.get(), uris, ctx); + } + } + else { + // see it as a local path + return FilePath::fromLocalPath(fileInfo->target().c_str()); + } + return FilePath(); +} + +bool BasicFileLauncher::launchExecutable(const FileInfoPtr &fileInfo, GAppLaunchContext* ctx) { + /* if it's an executable file, directly execute it. */ + auto filename = fileInfo->path().localPath(); + /* FIXME: we need to use eaccess/euidaccess here. */ + if(g_file_test(filename.get(), G_FILE_TEST_IS_EXECUTABLE)) { + auto act = quickExec_ ? ExecAction::DIRECT_EXEC : askExecFile(fileInfo); + int flags = G_APP_INFO_CREATE_NONE; + switch(act) { + case ExecAction::EXEC_IN_TERMINAL: + flags |= G_APP_INFO_CREATE_NEEDS_TERMINAL; + /* Falls through. */ + case ExecAction::DIRECT_EXEC: { + /* filename may contain spaces. Fix #3143296 */ + CStrPtr quoted{g_shell_quote(filename.get())}; + // FIXME: remove libfm dependency + GAppInfoPtr app{fm_app_info_create_from_commandline(quoted.get(), nullptr, GAppInfoCreateFlags(flags), nullptr)}; + if(app) { + CStrPtr run_path{g_path_get_dirname(filename.get())}; + CStrPtr cwd; + /* bug #3589641: scripts are ran from $HOME. + since GIO launcher is kinda ugly - it has + no means to set running directory so we + do workaround - change directory to it */ + if(run_path && strcmp(run_path.get(), ".")) { + cwd = CStrPtr{g_get_current_dir()}; + if(chdir(run_path.get()) != 0) { + cwd.reset(); + // show errors + QString msg = QObject::tr("Cannot set working directory to '%1': %2").arg(run_path.get()).arg(g_strerror(errno)); + GErrorPtr err{G_IO_ERROR, g_io_error_from_errno(errno), msg}; + showError(ctx, err); + } + } + + // FIXME: remove libfm dependency + GErrorPtr err; + if(!fm_app_info_launch(app.get(), nullptr, ctx, &err)) { + showError(ctx, err); + } + if(cwd) { /* return back */ + if(chdir(cwd.get()) != 0) { + g_warning("fm_launch_files(): chdir() failed"); + } + } + return true; + } + break; + } + case ExecAction::OPEN_WITH_DEFAULT_APP: + return launchWithDefaultApp(fileInfo, ctx); + case ExecAction::CANCEL: + default: + break; + } + } + return false; +} + +bool BasicFileLauncher::launchWithDefaultApp(const FileInfoPtr &fileInfo, GAppLaunchContext* ctx) { + FileInfoList files; + files.emplace_back(fileInfo); + GErrorPtr err; + GAppInfoPtr app{g_app_info_get_default_for_type(fileInfo->mimeType()->name(), false), false}; + if(app) { + return launchWithApp(app.get(), files.paths(), ctx); + } + else { + showError(ctx, err, fileInfo->path()); + } + return false; +} + +} // namespace Fm diff --git a/src/core/basicfilelauncher.h b/src/core/basicfilelauncher.h new file mode 100644 index 0000000..a28aa75 --- /dev/null +++ b/src/core/basicfilelauncher.h @@ -0,0 +1,72 @@ +#ifndef BASICFILELAUNCHER_H +#define BASICFILELAUNCHER_H + +#include "../libfmqtglobals.h" + +#include "fileinfo.h" +#include "filepath.h" +#include "mimetype.h" + +#include + +namespace Fm { + +class LIBFM_QT_API BasicFileLauncher { +public: + + enum class ExecAction { + NONE, + DIRECT_EXEC, + EXEC_IN_TERMINAL, + OPEN_WITH_DEFAULT_APP, + CANCEL + }; + + explicit BasicFileLauncher(); + virtual ~BasicFileLauncher(); + + bool launchFiles(const FileInfoList &fileInfos, GAppLaunchContext* ctx = nullptr); + + bool launchPaths(FilePathList paths, GAppLaunchContext* ctx = nullptr); + + bool launchDesktopEntry(const FileInfoPtr &fileInfo, const FilePathList& paths = FilePathList{}, GAppLaunchContext* ctx = nullptr); + + bool launchDesktopEntry(const char* desktopEntryName, const FilePathList& paths = FilePathList{}, GAppLaunchContext* ctx = nullptr); + + bool launchWithDefaultApp(const FileInfoPtr& fileInfo, GAppLaunchContext* ctx = nullptr); + + bool launchWithApp(GAppInfo* app, const FilePathList& paths, GAppLaunchContext* ctx = nullptr); + + bool launchExecutable(const FileInfoPtr &fileInfo, GAppLaunchContext* ctx = nullptr); + + bool quickExec() const { + return quickExec_; + } + + void setQuickExec(bool value) { + quickExec_ = value; + } + +protected: + + virtual GAppInfoPtr chooseApp(const FileInfoList& fileInfos, const char* mimeType, GErrorPtr& err); + + virtual bool openFolder(GAppLaunchContext* ctx, const FileInfoList& folderInfos, GErrorPtr& err); + + virtual bool showError(GAppLaunchContext* ctx, GErrorPtr& err, const FilePath& path = FilePath{}, const FileInfoPtr& info = FileInfoPtr{}); + + virtual ExecAction askExecFile(const FileInfoPtr& file); + + virtual int ask(const char* msg, char* const* btn_labels, int default_btn); + +private: + + FilePath handleShortcut(const FileInfoPtr &fileInfo, GAppLaunchContext* ctx = nullptr); + +private: + bool quickExec_; // Don't ask options on launch executable file +}; + +} // namespace Fm + +#endif // BASICFILELAUNCHER_H diff --git a/src/core/bookmarks.cpp b/src/core/bookmarks.cpp index 8397550..93996c8 100644 --- a/src/core/bookmarks.cpp +++ b/src/core/bookmarks.cpp @@ -2,6 +2,7 @@ #include "cstrptr.h" #include #include +#include namespace Fm { @@ -15,6 +16,60 @@ static inline CStrPtr get_new_bookmarks_file(void) { return CStrPtr{g_build_filename(g_get_user_config_dir(), "gtk-3.0", "bookmarks", nullptr)}; } +BookmarkItem::BookmarkItem(const FilePath& path, const QString name): + path_{path}, + name_{name} { + if(name_.isEmpty()) { // if the name is not specified, use basename of the path + name_ = path_.baseName().get(); + } + // We cannot rely on FileInfos to set bookmark icons because there is no guarantee + // that FileInfos already exist, while their creation is costly. Therefore, we have + // to get folder icons directly, as is done at `FileInfo::setFromGFileInfo` and more. + auto local_path = path.localPath(); + auto dot_dir = CStrPtr{g_build_filename(local_path.get(), ".directory", nullptr)}; + if(g_file_test(dot_dir.get(), G_FILE_TEST_IS_REGULAR)) { + GKeyFile* kf = g_key_file_new(); + if(g_key_file_load_from_file(kf, dot_dir.get(), G_KEY_FILE_NONE, nullptr)) { + CStrPtr icon_name{g_key_file_get_string(kf, "Desktop Entry", "Icon", nullptr)}; + if(icon_name) { + icon_ = IconInfo::fromName(icon_name.get()); + } + } + g_key_file_free(kf); + } + if(!icon_ || !icon_->isValid()) { + // first check some standard folders that are shared by Qt and GLib + if(path_ == FilePath::homeDir()) { + icon_ = IconInfo::fromName("user-home"); + } + else if (path_.parent() == FilePath::homeDir()) { + QString folderPath = QString::fromUtf8(path_.toString().get()); + if(folderPath == QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)) { + icon_ = IconInfo::fromName("user-desktop"); + } + else if(folderPath == QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)) { + icon_ = IconInfo::fromName("folder-documents"); + } + else if(folderPath == QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)) { + icon_ = IconInfo::fromName("folder-download"); + } + else if(folderPath == QStandardPaths::writableLocation(QStandardPaths::MusicLocation)) { + icon_ = IconInfo::fromName("folder-music"); + } + else if(folderPath == QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)) { + icon_ = IconInfo::fromName("folder-pictures"); + } + else if(folderPath == QStandardPaths::writableLocation(QStandardPaths::MoviesLocation)) { + icon_ = IconInfo::fromName("folder-videos"); + } + } + // fall back to the default folder icon + if(!icon_ || !icon_->isValid()) { + icon_ = IconInfo::fromName("folder"); + } + } +} + Bookmarks::Bookmarks(QObject* parent): QObject(parent), idle_handler{false} { diff --git a/src/core/bookmarks.h b/src/core/bookmarks.h index 5c19d2f..7b5af79 100644 --- a/src/core/bookmarks.h +++ b/src/core/bookmarks.h @@ -1,11 +1,10 @@ #ifndef FM2_BOOKMARKS_H #define FM2_BOOKMARKS_H -#include "../libfmqtglobals.h" #include #include "gobjectptr.h" -#include "fileinfo.h" - +#include "filepath.h" +#include "iconinfo.h" namespace Fm { @@ -13,11 +12,7 @@ class LIBFM_QT_API BookmarkItem { public: friend class Bookmarks; - explicit BookmarkItem(const FilePath& path, const QString name): path_{path}, name_{name} { - if(name_.isEmpty()) { // if the name is not specified, use basename of the path - name_ = path_.baseName().get(); - } - } + explicit BookmarkItem(const FilePath& path, const QString name); const QString& name() const { return name_; @@ -27,15 +22,11 @@ public: return path_; } - const std::shared_ptr& info() const { - return info_; + const std::shared_ptr& icon() const { + return icon_; } private: - void setInfo(const std::shared_ptr& info) { - info_ = info; - } - void setName(const QString& name) { name_ = name; } @@ -43,7 +34,7 @@ private: private: FilePath path_; QString name_; - std::shared_ptr info_; + std::shared_ptr icon_; }; diff --git a/src/core/compat_p.h b/src/core/compat_p.h deleted file mode 100644 index 86d550f..0000000 --- a/src/core/compat_p.h +++ /dev/null @@ -1,53 +0,0 @@ -#ifndef LIBFM_QT_COMPAT_P_H -#define LIBFM_QT_COMPAT_P_H - -#include "../libfmqtglobals.h" -#include "core/filepath.h" -#include "core/fileinfo.h" -#include "core/gioptrs.h" - -// deprecated -#include -#include "path.h" - -// compatibility functions bridging the old libfm C APIs and new C++ APIs. - -namespace Fm { - -inline FM_QT_DEPRECATED Fm::Path _convertPath(const Fm::FilePath& path) { - return Fm::Path::newForGfile(path.gfile().get()); -} - -inline FM_QT_DEPRECATED Fm::PathList _convertPathList(const Fm::FilePathList& srcFiles) { - Fm::PathList ret; - for(auto& file: srcFiles) { - ret.pushTail(_convertPath(file)); - } - return ret; -} - -inline FM_QT_DEPRECATED FmFileInfo* _convertFileInfo(const std::shared_ptr& info) { - // conver to GFileInfo first - GFileInfoPtr ginfo{g_file_info_new(), false}; - g_file_info_set_name(ginfo.get(), info->name().c_str()); - g_file_info_set_display_name(ginfo.get(), info->displayName().toUtf8().constData()); - g_file_info_set_content_type(ginfo.get(), info->mimeType()->name()); - - auto mode = info->mode(); - g_file_info_set_attribute_uint32(ginfo.get(), G_FILE_ATTRIBUTE_UNIX_MODE, mode); - GFileType ftype = info->isDir() ? G_FILE_TYPE_DIRECTORY : G_FILE_TYPE_REGULAR; // FIXME: generate more accurate type - g_file_info_set_file_type(ginfo.get(), ftype); - g_file_info_set_size(ginfo.get(), info->size()); - g_file_info_set_icon(ginfo.get(), info->icon()->gicon().get()); - - g_file_info_set_attribute_uint64(ginfo.get(), G_FILE_ATTRIBUTE_TIME_MODIFIED, info->mtime()); - g_file_info_set_attribute_uint64(ginfo.get(), G_FILE_ATTRIBUTE_TIME_ACCESS, info->atime()); - g_file_info_set_attribute_uint64(ginfo.get(), G_FILE_ATTRIBUTE_TIME_CHANGED, info->ctime()); - - auto gf = info->path().gfile(); - return fm_file_info_new_from_g_file_data(gf.get(), ginfo.get(), nullptr); -} - -} - -#endif // LIBFM_QT_COMPAT_P_H diff --git a/src/core/copyjob.cpp b/src/core/copyjob.cpp deleted file mode 100644 index 73ef3cf..0000000 --- a/src/core/copyjob.cpp +++ /dev/null @@ -1,453 +0,0 @@ -#include "copyjob.h" -#include "totalsizejob.h" -#include "fileinfo_p.h" - -namespace Fm { - -CopyJob::CopyJob(const FilePathList& paths, const FilePath& destDirPath, Mode mode): - FileOperationJob{}, - srcPaths_{paths}, - destDirPath_{destDirPath}, - mode_{mode}, - skip_dir_content{false} { -} - -CopyJob::CopyJob(const FilePathList &&paths, const FilePath &&destDirPath, Mode mode): - FileOperationJob{}, - srcPaths_{paths}, - destDirPath_{destDirPath}, - mode_{mode}, - skip_dir_content{false} { -} - -void CopyJob::gfileProgressCallback(goffset current_num_bytes, goffset total_num_bytes, CopyJob* _this) { - _this->setCurrentFileProgress(total_num_bytes, current_num_bytes); -} - -bool CopyJob::copyRegularFile(const FilePath& srcPath, GFileInfoPtr /*srcFile*/, const FilePath& destPath) { - int flags = G_FILE_COPY_ALL_METADATA | G_FILE_COPY_NOFOLLOW_SYMLINKS; - GErrorPtr err; -_retry_copy: - if(!g_file_copy(srcPath.gfile().get(), destPath.gfile().get(), GFileCopyFlags(flags), cancellable().get(), - GFileProgressCallback(gfileProgressCallback), this, &err)) { - flags &= ~G_FILE_COPY_OVERWRITE; - /* handle existing files or file name conflict */ - if(err.domain() == G_IO_ERROR && (err.code() == G_IO_ERROR_EXISTS || - err.code() == G_IO_ERROR_INVALID_FILENAME || - err.code() == G_IO_ERROR_FILENAME_TOO_LONG)) { -#if 0 - GFile* dest_cp = new_dest; - bool dest_exists = (err->code == G_IO_ERROR_EXISTS); - FmFileOpOption opt = 0; - g_error_free(err); - err = nullptr; - - new_dest = nullptr; - opt = _fm_file_ops_job_ask_new_name(job, src, dest, &new_dest, dest_exists); - if(!new_dest) { /* restoring status quo */ - new_dest = dest_cp; - } - else if(dest_cp) { /* we got new new_dest, forget old one */ - g_object_unref(dest_cp); - } - switch(opt) { - case FM_FILE_OP_RENAME: - dest = new_dest; - goto _retry_copy; - break; - case FM_FILE_OP_OVERWRITE: - flags |= G_FILE_COPY_OVERWRITE; - goto _retry_copy; - break; - case FM_FILE_OP_CANCEL: - fm_job_cancel(fmjob); - break; - case FM_FILE_OP_SKIP: - ret = true; - delete_src = false; /* don't delete source file. */ - break; - case FM_FILE_OP_SKIP_ERROR: ; /* FIXME */ - } -#endif - } - else { - ErrorAction act = emitError( err, ErrorSeverity::MODERATE); - err.reset(); - if(act == ErrorAction::RETRY) { - // FIXME: job->current_file_finished = 0; - goto _retry_copy; - } -# if 0 - const bool is_no_space = (err.domain() == G_IO_ERROR && - err.code() == G_IO_ERROR_NO_SPACE); - /* FIXME: ask to leave partial content? */ - if(is_no_space) { - g_file_delete(dest, fm_job_get_cancellable(fmjob), nullptr); - } - ret = false; - delete_src = false; -#endif - } - err.reset(); - } - else { - return true; - } - return false; -} - -bool CopyJob::copySpecialFile(const FilePath& srcPath, GFileInfoPtr srcFile, const FilePath& destPath) { - bool ret = false; - GError* err = nullptr; - /* only handle FIFO for local files */ - if(srcPath.isNative() && destPath.isNative()) { - auto src_path = srcPath.localPath(); - struct stat src_st; - int r; - r = lstat(src_path.get(), &src_st); - if(r == 0) { - /* Handle FIFO on native file systems. */ - if(S_ISFIFO(src_st.st_mode)) { - auto dest_path = destPath.localPath(); - if(mkfifo(dest_path.get(), src_st.st_mode) == 0) { - ret = true; - } - } - /* FIXME: how about block device, char device, and socket? */ - } - } - if(!ret) { - g_set_error(&err, G_IO_ERROR, G_IO_ERROR_FAILED, - ("Cannot copy file '%s': not supported"), - g_file_info_get_display_name(srcFile.get())); - // emitError( err, ErrorSeverity::MODERATE); - g_clear_error(&err); - } - return ret; -} - -bool CopyJob::copyDir(const FilePath& srcPath, GFileInfoPtr srcFile, const FilePath& destPath) { - bool ret = false; - if(makeDir(srcPath, srcFile, destPath)) { - GError* err = nullptr; - auto enu = GFileEnumeratorPtr{ - g_file_enumerate_children(srcPath.gfile().get(), - gfile_info_query_attribs, - G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, - cancellable().get(), &err), - false}; - if(enu) { - int n_children = 0; - int n_copied = 0; - ret = true; - while(!isCancelled()) { - auto inf = GFileInfoPtr{g_file_enumerator_next_file(enu.get(), cancellable().get(), &err), false}; - if(inf) { - ++n_children; - /* don't overwrite dir content, only calculate progress. */ - if(Q_UNLIKELY(skip_dir_content)) { - /* FIXME: this is incorrect as we don't do the calculation recursively. */ - addFinishedAmount(g_file_info_get_size(inf.get()), 1); - } - else { - const char* name = g_file_info_get_name(inf.get()); - FilePath childPath = srcPath.child(name); - bool child_ret = copyPath(childPath, inf, destPath, name); - if(child_ret) { - ++n_copied; - } - else { - ret = false; - } - } - } - else { - if(err) { - // FIXME: emitError( err, ErrorSeverity::MODERATE); - g_error_free(err); - err = nullptr; - /* ErrorAction::RETRY is not supported here */ - ret = false; - } - else { /* EOF is reached */ - /* all files are successfully copied. */ - if(isCancelled()) { - ret = false; - } - else { - /* some files are not copied */ - if(n_children != n_copied) { - /* if the copy actions are skipped deliberately, it's ok */ - if(!skip_dir_content) { - ret = false; - } - } - /* else job->skip_dir_content is true */ - } - break; - } - } - } - g_file_enumerator_close(enu.get(), nullptr, &err); - } - } - return false; -} - -bool CopyJob::makeDir(const FilePath& srcPath, GFileInfoPtr srcFile, const FilePath& dirPath) { - GError* err = nullptr; - if(isCancelled()) - return false; - - FilePath destPath = dirPath; - bool mkdir_done = false; - do { - mkdir_done = g_file_make_directory(destPath.gfile().get(), cancellable().get(), &err); - if(err->domain == G_IO_ERROR && (err->code == G_IO_ERROR_EXISTS || - err->code == G_IO_ERROR_INVALID_FILENAME || - err->code == G_IO_ERROR_FILENAME_TOO_LONG)) { - GFileInfoPtr destFile; - // FIXME: query its info - FilePath newDestPath; - FileExistsAction opt = askRename(FileInfo{srcFile, srcPath.parent()}, FileInfo{destFile, dirPath.parent()}, newDestPath); - g_error_free(err); - err = nullptr; - - switch(opt) { - case FileOperationJob::RENAME: - destPath = newDestPath; - break; - case FileOperationJob::SKIP: - /* when a dir is skipped, we need to know its total size to calculate correct progress */ - // job->finished += size; - // fm_file_ops_job_emit_percent(job); - // job->skip_dir_content = skip_dir_content = true; - mkdir_done = true; /* pretend that dir creation succeeded */ - break; - case FileOperationJob::OVERWRITE: - mkdir_done = true; /* pretend that dir creation succeeded */ - break; - case FileOperationJob::CANCEL: - cancel(); - break; - case FileOperationJob::SKIP_ERROR: ; /* FIXME */ - } - } - else { -#if 0 - ErrorAction act = emitError( err, ErrorSeverity::MODERATE); - g_error_free(err); - err = nullptr; - if(act == ErrorAction::RETRY) { - goto _retry_mkdir; - } -#endif - break; - } - // job->finished += size; - } while(!mkdir_done && !isCancelled()); - - if(mkdir_done && !isCancelled()) { - bool chmod_done = false; - mode_t mode = g_file_info_get_attribute_uint32(srcFile.get(), G_FILE_ATTRIBUTE_UNIX_MODE); - if(mode) { - mode |= (S_IRUSR | S_IWUSR); /* ensure we have rw permission to this file. */ - do { - /* chmod the newly created dir properly */ - // if(!fm_job_is_cancelled(fmjob) && !job->skip_dir_content) - chmod_done = g_file_set_attribute_uint32(destPath.gfile().get(), - G_FILE_ATTRIBUTE_UNIX_MODE, - mode, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, - cancellable().get(), &err); - if(!chmod_done) { -/* - ErrorAction act = emitError( err, ErrorSeverity::MODERATE); - g_error_free(err); - err = nullptr; - if(act == ErrorAction::RETRY) { - goto _retry_chmod_for_dir; - } -*/ - /* FIXME: some filesystems may not support this. */ - } - } while(!chmod_done && !isCancelled()); - // finished += size; - // fm_file_ops_job_emit_percent(job); - } - } - return false; -} - -bool CopyJob::copyPath(const FilePath& srcPath, const FilePath& destDirPath, const char* destFileName) { - GErrorPtr err; - GFileInfoPtr srcInfo = GFileInfoPtr { - g_file_query_info(srcPath.gfile().get(), - gfile_info_query_attribs, - G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, - cancellable().get(), &err), - false - }; - if(!srcInfo || isCancelled()) { - return false; - } - return copyPath(srcPath, srcInfo, destDirPath, destFileName); -} - -bool CopyJob::copyPath(const FilePath& srcPath, const GFileInfoPtr& srcInfo, const FilePath& destDirPath, const char* destFileName) { - setCurrentFile(srcPath); - GErrorPtr err; - GFileInfoPtr destDirInfo = GFileInfoPtr { - g_file_query_info(destDirPath.gfile().get(), - "id::filesystem", - G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, - cancellable().get(), &err), - false - }; - - if(!destDirInfo || isCancelled()) { - return false; - } - - auto size = g_file_info_get_size(srcInfo.get()); - setCurrentFileProgress(size, 0); - - auto destPath = destDirPath.child(destFileName); - bool success = false; - switch(g_file_info_get_file_type(srcInfo.get())) { - case G_FILE_TYPE_DIRECTORY: - success = copyDir(srcPath, srcInfo, destPath); - break; - case G_FILE_TYPE_SPECIAL: - success = copySpecialFile(srcPath, srcInfo, destPath); - break; - default: - success = copyRegularFile(srcPath, srcInfo, destPath); - break; - } - - if(success) { - addFinishedAmount(size, 1); -#if 0 - - if(ret && dest_folder) { - fm_dest = fm_path_new_for_gfile(dest); - if(!_fm_folder_event_file_added(dest_folder, fm_dest)) { - fm_path_unref(fm_dest); - } - } -#endif - } - - return false; -} - -#if 0 - -bool _fm_file_ops_job_copy_run(FmFileOpsJob* job) { - bool ret = true; - GFile* dest_dir; - GList* l; - FmJob* fmjob = FM_JOB(job); - /* prepare the job, count total work needed with FmDeepCountJob */ - FmDeepCountJob* dc = fm_deep_count_job_new(job->srcs, FM_DC_JOB_DEFAULT); - FmFolder* df; - - /* let the deep count job share the same cancellable object. */ - fm_job_set_cancellable(FM_JOB(dc), fm_job_get_cancellable(fmjob)); - fm_job_run_sync(FM_JOB(dc)); - job->total = dc->total_size; - if(fm_job_is_cancelled(fmjob)) { - g_object_unref(dc); - return false; - } - g_object_unref(dc); - g_debug("total size to copy: %llu", (long long unsigned int)job->total); - - dest_dir = fm_path_to_gfile(job->dest); - /* suspend updates for destination */ - df = fm_folder_find_by_path(job->dest); - if(df) { - fm_folder_block_updates(df); - } - - fm_file_ops_job_emit_prepared(job); - - for(l = fm_path_list_peek_head_link(job->srcs); !fm_job_is_cancelled(fmjob) && l; l = l->next) { - FmPath* path = FM_PATH(l->data); - GFile* src = fm_path_to_gfile(path); - GFile* dest; - char* tmp_basename; - - if(g_file_is_native(src) && g_file_is_native(dest_dir)) - /* both are native */ - { - tmp_basename = nullptr; - } - else if(g_file_is_native(src)) /* copy from native to virtual */ - tmp_basename = g_filename_to_utf8(fm_path_get_basename(path), - -1, nullptr, nullptr, nullptr); - /* gvfs escapes it itself */ - else { /* copy from virtual to native/virtual */ - /* if we drop URI query onto native filesystem, omit query part */ - const char* basename = fm_path_get_basename(path); - char* sub_name; - - sub_name = strchr(basename, '?'); - if(sub_name) { - sub_name = g_strndup(basename, sub_name - basename); - basename = strrchr(sub_name, G_DIR_SEPARATOR); - if(basename) { - basename++; - } - else { - basename = sub_name; - } - } - tmp_basename = fm_uri_subpath_to_native_subpath(basename, nullptr); - g_free(sub_name); - } - dest = g_file_get_child(dest_dir, - tmp_basename ? tmp_basename : fm_path_get_basename(path)); - g_free(tmp_basename); - if(!_fm_file_ops_job_copy_file(job, src, nullptr, dest, nullptr, df)) { - ret = false; - } - g_object_unref(src); - g_object_unref(dest); - } - - /* g_debug("finished: %llu, total: %llu", job->finished, job->total); */ - fm_file_ops_job_emit_percent(job); - - /* restore updates for destination */ - if(df) { - fm_folder_unblock_updates(df); - g_object_unref(df); - } - g_object_unref(dest_dir); - return ret; -} -#endif - -void CopyJob::exec() { - TotalSizeJob totalSizeJob{srcPaths_}; - connect(&totalSizeJob, &TotalSizeJob::error, this, &CopyJob::error); - connect(this, &CopyJob::cancelled, &totalSizeJob, &TotalSizeJob::cancel); - totalSizeJob.run(); - if(isCancelled()) { - return; - } - - setTotalAmount(totalSizeJob.totalSize(), totalSizeJob.fileCount()); - Q_EMIT preparedToRun(); - - for(auto& srcPath : srcPaths_) { - if(isCancelled()) { - break; - } - copyPath(srcPath, destDirPath_, srcPath.baseName().get()); - } -} - - -} // namespace Fm diff --git a/src/core/copyjob.h b/src/core/copyjob.h deleted file mode 100644 index 1cdbbe5..0000000 --- a/src/core/copyjob.h +++ /dev/null @@ -1,46 +0,0 @@ -#ifndef FM2_COPYJOB_H -#define FM2_COPYJOB_H - -#include "../libfmqtglobals.h" -#include "fileoperationjob.h" -#include "gioptrs.h" - -namespace Fm { - -class LIBFM_QT_API CopyJob : public Fm::FileOperationJob { - Q_OBJECT -public: - - enum class Mode { - COPY, - MOVE - }; - - explicit CopyJob(const FilePathList& paths, const FilePath& destDirPath, Mode mode = Mode::COPY); - - explicit CopyJob(const FilePathList&& paths, const FilePath&& destDirPath, Mode mode = Mode::COPY); - -protected: - void exec() override; - -private: - bool copyPath(const FilePath& srcPath, const FilePath& destPath, const char *destFileName); - bool copyPath(const FilePath &srcPath, const GFileInfoPtr &srcInfo, const FilePath &destDirPath, const char *destFileName); - bool copyRegularFile(const FilePath &srcPath, GFileInfoPtr srcFile, const FilePath& destPath); - bool copySpecialFile(const FilePath &srcPath, GFileInfoPtr srcFile, const FilePath& destPath); - bool copyDir(const FilePath &srcPath, GFileInfoPtr srcFile, const FilePath& destPath); - bool makeDir(const FilePath &srcPath, GFileInfoPtr srcFile, const FilePath& dirPath); - - static void gfileProgressCallback(goffset current_num_bytes, goffset total_num_bytes, CopyJob* _this); - -private: - FilePathList srcPaths_; - FilePath destDirPath_; - Mode mode_; - bool skip_dir_content; -}; - - -} // namespace Fm - -#endif // FM2_COPYJOB_H diff --git a/src/core/deletejob.cpp b/src/core/deletejob.cpp index b2c518f..6c3c550 100644 --- a/src/core/deletejob.cpp +++ b/src/core/deletejob.cpp @@ -25,6 +25,9 @@ bool DeleteJob::deleteFile(const FilePath& path, GFileInfoPtr inf) { } } + // TODO: get parent dir of the current path. + // if there is a Fm::Folder object created for it, block the update for the folder temporarily. + /* currently processed file. */ setCurrentFile(path); @@ -33,11 +36,21 @@ bool DeleteJob::deleteFile(const FilePath& path, GFileInfoPtr inf) { deleteDirContent(path, inf); } + bool isTrashRoot = false; + // special handling for trash:/// + if(!path.isNative() && g_strcmp0(path.uriScheme().get(), "trash") == 0) { + // little trick: basename of trash root is / + auto basename = path.baseName(); + if(basename && basename[0] == G_DIR_SEPARATOR) { + isTrashRoot = true; + } + } + bool hasError = false; while(!isCancelled()) { GErrorPtr err; - // try to delete the path directly - if(g_file_delete(path.gfile().get(), cancellable().get(), &err)) { + // try to delete the path directly (but don't delete if it's trash:///) + if(isTrashRoot || g_file_delete(path.gfile().get(), cancellable().get(), &err)) { break; } if(err) { @@ -69,26 +82,9 @@ bool DeleteJob::deleteFile(const FilePath& path, GFileInfoPtr inf) { } bool DeleteJob::deleteDirContent(const FilePath& path, GFileInfoPtr inf) { -#if 0 - FmFolder* sub_folder; - /* special handling for trash:/// */ - if(!g_file_is_native(gf)) { - char* scheme = g_file_get_uri_scheme(gf); - if(g_strcmp0(scheme, "trash") == 0) { - /* little trick: basename of trash root is /. */ - char* basename = g_file_get_basename(gf); - if(basename && basename[0] == G_DIR_SEPARATOR) { - is_trash_root = true; - } - g_free(basename); - } - g_free(scheme); - } -#endif - GErrorPtr err; GFileEnumeratorPtr enu { - g_file_enumerate_children(path.gfile().get(), gfile_info_query_attribs, + g_file_enumerate_children(path.gfile().get(), defaultGFileInfoQueryAttribs, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, cancellable().get(), &err), false @@ -126,6 +122,17 @@ bool DeleteJob::deleteDirContent(const FilePath& path, GFileInfoPtr inf) { } +DeleteJob::DeleteJob(const FilePathList &paths): paths_{paths} { + setCalcProgressUsingSize(false); +} + +DeleteJob::DeleteJob(FilePathList &&paths): paths_{paths} { + setCalcProgressUsingSize(false); +} + +DeleteJob::~DeleteJob() { +} + void DeleteJob::exec() { /* prepare the job, count total work needed with FmDeepCountJob */ TotalSizeJob totalSizeJob{paths_, TotalSizeJob::Flags::PREPARE_DELETE}; diff --git a/src/core/deletejob.h b/src/core/deletejob.h index 4560098..8ad2321 100644 --- a/src/core/deletejob.h +++ b/src/core/deletejob.h @@ -11,14 +11,11 @@ namespace Fm { class LIBFM_QT_API DeleteJob : public Fm::FileOperationJob { Q_OBJECT public: - explicit DeleteJob(const FilePathList& paths): paths_{paths} { - } + explicit DeleteJob(const FilePathList& paths); - explicit DeleteJob(FilePathList&& paths): paths_{paths} { - } + explicit DeleteJob(FilePathList&& paths); - ~DeleteJob() { - } + ~DeleteJob(); protected: void exec() override; diff --git a/src/core/dirlistjob.cpp b/src/core/dirlistjob.cpp index d782ede..caad634 100644 --- a/src/core/dirlistjob.cpp +++ b/src/core/dirlistjob.cpp @@ -25,7 +25,7 @@ void DirListJob::exec() { _retry: err.reset(); dir_inf = GFileInfoPtr{ - g_file_query_info(dir_gfile.get(), gfile_info_query_attribs, + g_file_query_info(dir_gfile.get(), defaultGFileInfoQueryAttribs, G_FILE_QUERY_INFO_NONE, cancellable().get(), &err), false }; @@ -58,7 +58,7 @@ _retry: // FIXME: _fm_file_info_job_update_fs_readonly(gf, inf, nullptr, nullptr); err.reset(); GFileEnumeratorPtr enu = GFileEnumeratorPtr{ - g_file_enumerate_children(dir_gfile.get(), gfile_info_query_attribs, + g_file_enumerate_children(dir_gfile.get(), defaultGFileInfoQueryAttribs, G_FILE_QUERY_INFO_NONE, cancellable().get(), &err), false }; diff --git a/src/core/filechangeattrjob.cpp b/src/core/filechangeattrjob.cpp index 5ebf43d..8ed2abb 100644 --- a/src/core/filechangeattrjob.cpp +++ b/src/core/filechangeattrjob.cpp @@ -1,9 +1,324 @@ #include "filechangeattrjob.h" +#include "totalsizejob.h" + +#include namespace Fm { -FileChangeAttrJob::FileChangeAttrJob() { +static const char query[] = G_FILE_ATTRIBUTE_STANDARD_TYPE"," + G_FILE_ATTRIBUTE_STANDARD_NAME"," + G_FILE_ATTRIBUTE_UNIX_GID"," + G_FILE_ATTRIBUTE_UNIX_UID"," + G_FILE_ATTRIBUTE_UNIX_MODE"," + G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME; + +FileChangeAttrJob::FileChangeAttrJob(FilePathList paths): + paths_{std::move(paths)}, + recursive_{false}, + // chmod + fileModeEnabled_{false}, + newMode_{0}, + newModeMask_{0}, + // chown + ownerEnabled_{false}, + uid_{0}, + groupEnabled_{false}, + gid_{0}, + // Display name + displayNameEnabled_{false}, + // icon + iconEnabled_{false}, + // hidden + hiddenEnabled_{false}, + hidden_{false}, + // target uri + targetUriEnabled_{false} { + + // the progress of chmod/chown is not related to file size + setCalcProgressUsingSize(false); +} + +void FileChangeAttrJob::exec() { + // count total amount of the work + if(recursive_) { + TotalSizeJob totalSizeJob{paths_}; + connect(&totalSizeJob, &TotalSizeJob::error, this, &FileChangeAttrJob::error); + connect(this, &FileChangeAttrJob::cancelled, &totalSizeJob, &TotalSizeJob::cancel); + totalSizeJob.run(); + std::uint64_t totalSize, totalCount; + totalSizeJob.totalAmount(totalSize, totalCount); + setTotalAmount(totalSize, totalCount); + } + else { + setTotalAmount(paths_.size(), paths_.size()); + } + + // ready to start + Q_EMIT preparedToRun(); + + // do the actual change attrs job + for(auto& path : paths_) { + if(isCancelled()) { + break; + } + GErrorPtr err; + GFileInfoPtr info{ + g_file_query_info(path.gfile().get(), query, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err), + false + }; + if(info) { + processFile(path, info); + } + else { + handleError(err, path, info); + } + } +} + +bool FileChangeAttrJob::processFile(const FilePath& path, const GFileInfoPtr& info) { + setCurrentFile(path); + bool ret = true; + + if(ownerEnabled_) { + changeFileOwner(path, info, uid_); + } + if(groupEnabled_) { + changeFileGroup(path, info, gid_); + } + if(fileModeEnabled_) { + changeFileMode(path, info, newMode_, newModeMask_); + } + /* change display name, icon, hidden, target */ + if(displayNameEnabled_ && !displayName().empty()) { + changeFileDisplayName(path, info, displayName_.c_str()); + } + if(iconEnabled_ && icon_) { + changeFileIcon(path, info, icon_); + } + if(hiddenEnabled_) { + changeFileHidden(path, info, hidden_); + } + if(targetUriEnabled_ && !targetUri_.empty()) { + changeFileTargetUri(path, info, targetUri_.c_str()); + } + + // FIXME: do not use size 1 here. + addFinishedAmount(1, 1); + + // recursively apply to subfolders + auto type = g_file_info_get_file_type(info.get()); + if(!isCancelled() && recursive_ && type == G_FILE_TYPE_DIRECTORY) { + bool retry; + do { + retry = false; + GErrorPtr err; + GFileEnumeratorPtr enu{ + g_file_enumerate_children(path.gfile().get(), query, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err), + false + }; + if(enu) { + while(!isCancelled()) { + err.reset(); + GFileInfoPtr childInfo{g_file_enumerator_next_file(enu.get(), cancellable().get(), &err), false}; + if(childInfo) { + auto childPath = path.child(g_file_info_get_name(childInfo.get())); + ret = processFile(childPath, childInfo); + if(!ret) { /* _fm_file_ops_job_change_attr_file() failed */ + break; + } + } + else { + if(err) { + handleError(err, path, info, ErrorSeverity::MILD); + retry = false; + /* FM_JOB_RETRY is not supported here */ + } + else { /* EOF */ + break; + } + } + } + g_file_enumerator_close(enu.get(), cancellable().get(), nullptr); + } + else { + retry = handleError(err, path, info); + } + } while(!isCancelled() && retry); + } + return ret; +} + +bool FileChangeAttrJob::handleError(GErrorPtr &err, const FilePath &path, const GFileInfoPtr &info, ErrorSeverity severity) { + auto act = emitError(err, severity); + if (act == ErrorAction::RETRY) { + err.reset(); + return true; + } + return false; +} + +bool FileChangeAttrJob::changeFileOwner(const FilePath& path, const GFileInfoPtr& info, uid_t uid) { + /* change owner */ + bool ret = false; + bool retry; + do { + GErrorPtr err; + if(!g_file_set_attribute_uint32(path.gfile().get(), G_FILE_ATTRIBUTE_UNIX_UID, + uid, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err)) { + retry = handleError(err, path, info, ErrorSeverity::MILD); + err.reset(); + } + else { + ret = true; + retry = false; + } + } while(retry && !isCancelled()); + return ret; +} + +bool FileChangeAttrJob::changeFileGroup(const FilePath& path, const GFileInfoPtr& info, gid_t gid) { + /* change group */ + bool ret = false; + bool retry; + do { + GErrorPtr err; + if(!g_file_set_attribute_uint32(path.gfile().get(), G_FILE_ATTRIBUTE_UNIX_GID, + gid, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err)) { + retry = handleError(err, path, info, ErrorSeverity::MILD); + err.reset(); + } + else { + ret = true; + retry = false; + } + } while(retry && !isCancelled()); + return ret; +} + +bool FileChangeAttrJob::changeFileMode(const FilePath& path, const GFileInfoPtr& info, mode_t newMode, mode_t newModeMask) { + bool ret = false; + /* change mode */ + if(newModeMask) { + guint32 mode = g_file_info_get_attribute_uint32(info.get(), G_FILE_ATTRIBUTE_UNIX_MODE); + mode &= ~newModeMask; + mode |= (newMode & newModeMask); + + auto type = g_file_info_get_file_type(info.get()); + /* FIXME: this behavior should be optional. */ + /* treat dirs with 'r' as 'rx' */ + if(type == G_FILE_TYPE_DIRECTORY) { + if((newModeMask & S_IRUSR) && (mode & S_IRUSR)) { + mode |= S_IXUSR; + } + if((newModeMask & S_IRGRP) && (mode & S_IRGRP)) { + mode |= S_IXGRP; + } + if((newModeMask & S_IROTH) && (mode & S_IROTH)) { + mode |= S_IXOTH; + } + } + + /* new mode */ + bool retry; + do { + GErrorPtr err; + if(!g_file_set_attribute_uint32(path.gfile().get(), G_FILE_ATTRIBUTE_UNIX_MODE, + mode, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err)) { + retry = handleError(err, path, info, ErrorSeverity::MILD); + err.reset(); + } + else { + ret = true; + retry = false; + } + } while(retry && !isCancelled()); + } + return ret; + +} + +bool FileChangeAttrJob::changeFileDisplayName(const FilePath& path, const GFileInfoPtr& info, const char* displayName) { + bool ret = false; + bool retry; + do { + GErrorPtr err; + if(!g_file_set_display_name(path.gfile().get(), displayName, cancellable().get(), &err)) { + retry = handleError(err, path, info, ErrorSeverity::MILD); + err.reset(); + } + else { + ret = true; + retry = false; + } + } while(retry && !isCancelled()); + return ret; +} + +bool FileChangeAttrJob::changeFileIcon(const FilePath& path, const GFileInfoPtr& info, GIconPtr& icon) { + bool ret = false; + bool retry; + do { + GErrorPtr err; + if(!g_file_set_attribute(path.gfile().get(), G_FILE_ATTRIBUTE_STANDARD_ICON, + G_FILE_ATTRIBUTE_TYPE_OBJECT, icon.get(), + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err)) { + retry = handleError(err, path, info, ErrorSeverity::MILD); + err.reset(); + } + else { + ret = true; + retry = false; + } + } while(retry && !isCancelled()); + return ret; +} + +bool FileChangeAttrJob::changeFileHidden(const FilePath& path, const GFileInfoPtr& info, bool hidden) { + bool ret = false; + bool retry; + do { + GErrorPtr err; + gboolean g_hidden = hidden; + if(!g_file_set_attribute(path.gfile().get(), G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN, + G_FILE_ATTRIBUTE_TYPE_BOOLEAN, &g_hidden, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err)) { + retry = handleError(err, path, info, ErrorSeverity::MILD); + err.reset(); + } + else { + ret = true; + retry = false; + } + } while(retry && !isCancelled()); + return ret; +} +bool FileChangeAttrJob::changeFileTargetUri(const FilePath& path, const GFileInfoPtr& info, const char* targetUri) { + bool ret = false; + bool retry; + do { + GErrorPtr err; + if(!g_file_set_attribute_string(path.gfile().get(), G_FILE_ATTRIBUTE_STANDARD_TARGET_URI, + targetUri, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err)) { + retry = handleError(err, path, info, ErrorSeverity::MILD); + err.reset(); + } + else { + ret = true; + retry = false; + } + } while(retry && !isCancelled()); + return ret; } } // namespace Fm diff --git a/src/core/filechangeattrjob.h b/src/core/filechangeattrjob.h index 2b0aea4..3f15863 100644 --- a/src/core/filechangeattrjob.h +++ b/src/core/filechangeattrjob.h @@ -3,13 +3,141 @@ #include "../libfmqtglobals.h" #include "fileoperationjob.h" +#include "gioptrs.h" + +#include + +#include namespace Fm { class LIBFM_QT_API FileChangeAttrJob : public Fm::FileOperationJob { Q_OBJECT public: - explicit FileChangeAttrJob(); + explicit FileChangeAttrJob(FilePathList paths); + + void setFileModeEnabled(bool enabled) { + fileModeEnabled_ = enabled; + } + void setFileMode(mode_t newMode, mode_t newModeMask) { + newMode_ = newMode; + newModeMask_ = newModeMask; + } + + bool ownerEnabled() const { + return ownerEnabled_; + } + void setOwnerEnabled(bool enabled) { + ownerEnabled_ = enabled; + } + void setOwner(uid_t uid) { + uid_ = uid; + } + + bool groupEnabled() const { + return groupEnabled_; + } + void setGroupEnabled(bool groupEnabled) { + groupEnabled_ = groupEnabled; + } + void setGroup(gid_t gid) { + gid_ = gid; + } + + // This only work for change attr jobs. + void setRecursive(bool recursive) { + recursive_ = recursive; + } + + void setHiddenEnabled(bool enabled) { + hiddenEnabled_ = enabled; + } + void setHidden(bool hidden) { + hidden_ = hidden; + } + + bool iconEnabled() const { + return iconEnabled_; + } + void setIconEnabled(bool iconEnabled) { + iconEnabled_ = iconEnabled; + } + + bool displayNameEnabled() const { + return displayNameEnabled_; + } + void setDisplayNameEnabled(bool displayNameEnabled) { + displayNameEnabled_ = displayNameEnabled; + } + + const std::string& displayName() const { + return displayName_; + } + void setDisplayName(const std::string& displayName) { + displayName_ = displayName; + } + + bool targetUriEnabled() const { + return targetUriEnabled_; + } + void setTargetUriEnabled(bool targetUriEnabled) { + targetUriEnabled_ = targetUriEnabled; + } + + const std::string& targetUri() const { + return targetUri_; + } + void setTargetUri(const std::string& value) { + targetUri_ = value; + } + + +protected: + void exec() override; + +private: + bool processFile(const FilePath& path, const GFileInfoPtr& info); + bool handleError(GErrorPtr& err, const FilePath& path, const GFileInfoPtr& info, ErrorSeverity severity = ErrorSeverity::MODERATE); + + bool changeFileOwner(const FilePath& path, const GFileInfoPtr& info, uid_t uid); + bool changeFileGroup(const FilePath& path, const GFileInfoPtr& info, gid_t gid); + bool changeFileMode(const FilePath& path, const GFileInfoPtr& info, mode_t newMode, mode_t newModeMask); + bool changeFileDisplayName(const FilePath& path, const GFileInfoPtr& info, const char* displayName); + bool changeFileIcon(const FilePath& path, const GFileInfoPtr& info, GIconPtr& icon); + bool changeFileHidden(const FilePath& path, const GFileInfoPtr& info, bool hidden); + bool changeFileTargetUri(const FilePath& path, const GFileInfoPtr& info, const char* targetUri_); + +private: + FilePathList paths_; + bool recursive_; + + // chmod + bool fileModeEnabled_; + mode_t newMode_; + mode_t newModeMask_; + + // chown + bool ownerEnabled_; + uid_t uid_; + + bool groupEnabled_; + gid_t gid_; + + // Display name + bool displayNameEnabled_; + std::string displayName_; + + // icon + bool iconEnabled_; + GIconPtr icon_; + + // hidden + bool hiddenEnabled_; + bool hidden_; + + // target uri + bool targetUriEnabled_; + std::string targetUri_; }; } // namespace Fm diff --git a/src/core/fileinfo.cpp b/src/core/fileinfo.cpp index 0258c67..8e86f8d 100644 --- a/src/core/fileinfo.cpp +++ b/src/core/fileinfo.cpp @@ -4,12 +4,12 @@ namespace Fm { -const char gfile_info_query_attribs[] = "standard::*," - "unix::*," - "time::*," - "access::*," - "id::filesystem," - "metadata::emblems"; +const char defaultGFileInfoQueryAttribs[] = "standard::*," + "unix::*," + "time::*," + "access::*," + "id::filesystem," + "metadata::emblems"; FileInfo::FileInfo() { // FIXME: initialize numeric data members @@ -28,7 +28,8 @@ void FileInfo::setFromGFileInfo(const GObjectPtr& inf, const FilePath GIcon* gicon; GFileType type; - name_ = g_file_info_get_name(inf.get()); + if (const char * name = g_file_info_get_name(inf.get())) + name_ = name; dispName_ = g_file_info_get_display_name(inf.get()); @@ -118,6 +119,8 @@ void FileInfo::setFromGFileInfo(const GObjectPtr& inf, const FilePath isDeletable_ = true; } + isShortcut_ = false; + /* special handling for symlinks */ if(g_file_info_get_is_symlink(inf.get())) { mode_ &= ~S_IFMT; /* reset type */ @@ -125,11 +128,10 @@ void FileInfo::setFromGFileInfo(const GObjectPtr& inf, const FilePath goto _file_is_symlink; } - isShortcut_ = false; - switch(type) { case G_FILE_TYPE_SHORTCUT: isShortcut_ = true; + /* Falls through. */ case G_FILE_TYPE_MOUNTABLE: uri = g_file_info_get_attribute_string(inf.get(), G_FILE_ATTRIBUTE_STANDARD_TARGET_URI); if(uri) { @@ -184,6 +186,7 @@ _file_is_symlink: mimeType_ = MimeType::guessFromFileName(target_.c_str()); } } + /* Falls through. */ /* continue with absent mime type */ default: /* G_FILE_TYPE_UNKNOWN G_FILE_TYPE_REGULAR G_FILE_TYPE_SPECIAL */ if(G_UNLIKELY(!mimeType_)) { @@ -211,7 +214,7 @@ _file_is_symlink: g_key_file_free(kf); } } - + if(!icon_) { /* try file-specific icon first */ gicon = g_file_info_get_icon(inf.get()); @@ -246,7 +249,11 @@ _file_is_symlink: atime_ = g_file_info_get_attribute_uint64(inf.get(), G_FILE_ATTRIBUTE_TIME_ACCESS); ctime_ = g_file_info_get_attribute_uint64(inf.get(), G_FILE_ATTRIBUTE_TIME_CHANGED); isHidden_ = g_file_info_get_is_hidden(inf.get()); - isBackup_ = g_file_info_get_is_backup(inf.get()); + // g_file_info_get_is_backup() does not cover ".bak" and ".old". + // NOTE: Here, dispName_ is not modified for desktop entries yet. + isBackup_ = g_file_info_get_is_backup(inf.get()) + || dispName_.endsWith(QLatin1String(".bak")) + || dispName_.endsWith(QLatin1String(".old")); isNameChangeable_ = true; /* GVFS tends to ignore this attribute */ isIconChangeable_ = isHiddenChangeable_ = false; if(g_file_info_has_attribute(inf.get(), G_FILE_ATTRIBUTE_ACCESS_CAN_RENAME)) { @@ -326,7 +333,31 @@ bool FileInfo::canThumbnail() const { /* full path of the file is required by this function */ bool FileInfo::isExecutableType() const { - if(isText()) { /* g_content_type_can_be_executable reports text files as executables too */ + if(isDesktopEntry()) { + /* treat desktop entries as executables if + they are native and have read permission */ + if(isNative() && (mode_ & (S_IRUSR|S_IRGRP|S_IROTH))) { + if(isShortcut() && !target_.empty()) { + /* handle shortcuts from desktop to menu entries: + first check for entries in /usr/share/applications and such + which may be considered as a safe desktop entry path + then check if that is a shortcut to a native file + otherwise it is a link to a file under menu:// */ + if (!g_str_has_prefix(target_.c_str(), "/usr/share/")) { + auto target = FilePath::fromPathStr(target_.c_str()); + bool is_native = target.isNative(); + if (is_native) { + return true; + } + } + } + else { + return true; + } + } + return false; + } + else if(isText()) { /* g_content_type_can_be_executable reports text files as executables too */ /* We don't execute remote files nor files in trash */ if(isNative() && (mode_ & (S_IXOTH | S_IXGRP | S_IXUSR))) { /* it has executable bits so lets check shell-bang */ diff --git a/src/core/fileinfo.h b/src/core/fileinfo.h index 1208e1e..03bf825 100644 --- a/src/core/fileinfo.h +++ b/src/core/fileinfo.h @@ -90,16 +90,16 @@ public: return mimeType_; } - time_t ctime() const { + quint64 ctime() const { return ctime_; } - time_t atime() const { + quint64 atime() const { return atime_; } - time_t mtime() const { + quint64 mtime() const { return mtime_; } @@ -163,7 +163,7 @@ public: } bool isDir() const { - return mimeType_->isDir(); + return S_ISDIR(mode_) || mimeType_->isDir(); } bool isNative() const { @@ -194,6 +194,10 @@ public: return dispName_; } + QString description() const { + return QString::fromUtf8(mimeType_ ? mimeType_->desc() : ""); + } + FilePath path() const { return dirPath_ ? dirPath_.child(name_.c_str()) : FilePath::fromPathStr(name_.c_str()); } @@ -221,9 +225,9 @@ private: uid_t uid_; gid_t gid_; uint64_t size_; - time_t mtime_; - time_t atime_; - time_t ctime_; + quint64 mtime_; + quint64 atime_; + quint64 ctime_; uint64_t blksize_; uint64_t blocks_; @@ -266,9 +270,10 @@ public: } }; +// smart pointer to FileInfo objects (once created, FileInfo objects should stay immutable so const is needed here) +typedef std::shared_ptr FileInfoPtr; -typedef std::pair, std::shared_ptr> FileInfoPair; - +typedef std::pair FileInfoPair; } diff --git a/src/core/fileinfo_p.h b/src/core/fileinfo_p.h index 37553fa..f7a51a6 100644 --- a/src/core/fileinfo_p.h +++ b/src/core/fileinfo_p.h @@ -3,7 +3,7 @@ namespace Fm { - extern const char gfile_info_query_attribs[]; + extern const char defaultGFileInfoQueryAttribs[]; } // namespace Fm diff --git a/src/core/fileinfojob.cpp b/src/core/fileinfojob.cpp index 75142a4..3c222af 100644 --- a/src/core/fileinfojob.cpp +++ b/src/core/fileinfojob.cpp @@ -3,9 +3,10 @@ namespace Fm { -FileInfoJob::FileInfoJob(FilePathList paths, FilePath commonDirPath, const std::shared_ptr& cutFilesHashSet): +FileInfoJob::FileInfoJob(FilePathList paths, FilePathList deletionPaths, FilePath commonDirPath, const std::shared_ptr& cutFilesHashSet): Job(), paths_{std::move(paths)}, + deletionPaths_{std::move(deletionPaths)}, commonDirPath_{std::move(commonDirPath)}, cutFilesHashSet_{cutFilesHashSet} { } @@ -15,12 +16,13 @@ void FileInfoJob::exec() { if(!isCancelled()) { GErrorPtr err; GFileInfoPtr inf{ - g_file_query_info(path.gfile().get(), gfile_info_query_attribs, + g_file_query_info(path.gfile().get(), defaultGFileInfoQueryAttribs, G_FILE_QUERY_INFO_NONE, cancellable().get(), &err), false }; - if(!inf) - return; + if(!inf) { + continue; + } // Reuse the same dirPath object when the path remains the same (optimize for files in the same dir) auto dirPath = commonDirPath_.isValid() ? commonDirPath_ : path.parent(); diff --git a/src/core/fileinfojob.h b/src/core/fileinfojob.h index ba59eb7..53a03c5 100644 --- a/src/core/fileinfojob.h +++ b/src/core/fileinfojob.h @@ -13,12 +13,16 @@ class LIBFM_QT_API FileInfoJob : public Job { Q_OBJECT public: - explicit FileInfoJob(FilePathList paths, FilePath commonDirPath = FilePath(), const std::shared_ptr& cutFilesHashSet = nullptr); + explicit FileInfoJob(FilePathList paths, FilePathList deletionPaths = FilePathList(), FilePath commonDirPath = FilePath(), const std::shared_ptr& cutFilesHashSet = nullptr); const FilePathList& paths() const { return paths_; } + const FilePathList& deletionPaths() const { + return deletionPaths_; + } + const FileInfoList& files() const { return results_; } @@ -31,6 +35,7 @@ protected: private: FilePathList paths_; + FilePathList deletionPaths_; FileInfoList results_; FilePath commonDirPath_; const std::shared_ptr cutFilesHashSet_; diff --git a/src/core/fileoperationjob.cpp b/src/core/fileoperationjob.cpp index 346da42..c2fbb26 100644 --- a/src/core/fileoperationjob.cpp +++ b/src/core/fileoperationjob.cpp @@ -4,6 +4,7 @@ namespace Fm { FileOperationJob::FileOperationJob(): hasTotalAmount_{false}, + calcProgressUsingSize_{true}, totalSize_{0}, totalCount_{0}, finishedSize_{0}, @@ -31,6 +32,22 @@ bool FileOperationJob::currentFileProgress(FilePath& path, uint64_t& totalSize, return currentFile_.isValid(); } +double FileOperationJob::progress() const { + std::lock_guard lock{mutex_}; + double finishedRatio; + if(calcProgressUsingSize_) { + finishedRatio = totalSize_ > 0 ? double(finishedSize_ + currentFileFinished_) / totalSize_ : 0.0; + } + else { + finishedRatio = totalCount_ > 0 ? double(finishedCount_) / totalCount_ : 0.0; + } + + if(finishedRatio > 1.0) { + finishedRatio = 1.0; + } + return finishedRatio; +} + FileOperationJob::FileExistsAction FileOperationJob::askRename(const FileInfo &src, const FileInfo &dest, FilePath &newDest) { FileExistsAction action = SKIP; Q_EMIT fileExists(src, dest, action, newDest); @@ -47,31 +64,37 @@ bool FileOperationJob::finishedAmount(uint64_t& finishedSize, uint64_t& finished } void FileOperationJob::setTotalAmount(uint64_t fileSize, uint64_t fileCount) { - std::lock_guard locl{mutex_}; + std::lock_guard lock{mutex_}; hasTotalAmount_ = true; totalSize_ = fileSize; totalCount_ = fileCount; } void FileOperationJob::setFinishedAmount(uint64_t finishedSize, uint64_t finishedCount) { - std::lock_guard locl{mutex_}; + std::lock_guard lock{mutex_}; finishedSize_ = finishedSize; finishedCount_ = finishedCount; } void FileOperationJob::addFinishedAmount(uint64_t finishedSize, uint64_t finishedCount) { - std::lock_guard locl{mutex_}; + std::lock_guard lock{mutex_}; finishedSize_ += finishedSize; finishedCount_ += finishedCount; } +FilePath FileOperationJob::currentFile() const { + std::lock_guard lock{mutex_}; + auto ret = currentFile_; + return ret; +} + void FileOperationJob::setCurrentFile(const FilePath& path) { - std::lock_guard locl{mutex_}; + std::lock_guard lock{mutex_}; currentFile_ = path; } void FileOperationJob::setCurrentFileProgress(uint64_t totalSize, uint64_t finishedSize) { - std::lock_guard locl{mutex_}; + std::lock_guard lock{mutex_}; currentFileSize_ = totalSize; currentFileFinished_ = finishedSize; } diff --git a/src/core/fileoperationjob.h b/src/core/fileoperationjob.h index c9ce569..f491a4b 100644 --- a/src/core/fileoperationjob.h +++ b/src/core/fileoperationjob.h @@ -24,12 +24,26 @@ public: explicit FileOperationJob(); + // get total amount of work to do bool totalAmount(std::uint64_t& fileSize, std::uint64_t& fileCount) const; + // get currently finished job amount bool finishedAmount(std::uint64_t& finishedSize, std::uint64_t& finishedCount) const; + // get the current file + FilePath currentFile() const; + + // get progress of the current file bool currentFileProgress(FilePath& path, std::uint64_t& totalSize, std::uint64_t& finishedSize) const; + // is the job calculate progress based on file size or file counts + bool calcProgressUsingSize() const { + return calcProgressUsingSize_; + } + + // get currently finished amount (0.0 to 1.0) + virtual double progress() const; + Q_SIGNALS: void preparedToRun(); @@ -55,8 +69,17 @@ protected: void setCurrentFileProgress(uint64_t totalSize, uint64_t finishedSize); + void setCalcProgressUsingSize(bool value) { + calcProgressUsingSize_ = value; + } + + std::mutex& mutex() { + return mutex_; + } + private: bool hasTotalAmount_; + bool calcProgressUsingSize_; std::uint64_t totalSize_; std::uint64_t totalCount_; std::uint64_t finishedSize_; diff --git a/src/core/filetransferjob.cpp b/src/core/filetransferjob.cpp new file mode 100644 index 0000000..266b792 --- /dev/null +++ b/src/core/filetransferjob.cpp @@ -0,0 +1,641 @@ +#include "filetransferjob.h" +#include "totalsizejob.h" +#include "fileinfo_p.h" + +namespace Fm { + +FileTransferJob::FileTransferJob(FilePathList srcPaths, Mode mode): + FileOperationJob{}, + srcPaths_{std::move(srcPaths)}, + mode_{mode} { +} + +FileTransferJob::FileTransferJob(FilePathList srcPaths, FilePathList destPaths, Mode mode): + FileTransferJob{std::move(srcPaths), mode} { + destPaths_ = std::move(destPaths); +} + +FileTransferJob::FileTransferJob(FilePathList srcPaths, const FilePath& destDirPath, Mode mode): + FileTransferJob{std::move(srcPaths), mode} { + setDestDirPath(destDirPath); +} + +void FileTransferJob::setSrcPaths(FilePathList srcPaths) { + srcPaths_ = std::move(srcPaths); +} + +void FileTransferJob::setDestPaths(FilePathList destPaths) { + destPaths_ = std::move(destPaths); +} + +void FileTransferJob::setDestDirPath(const FilePath& destDirPath) { + destPaths_.clear(); + destPaths_.reserve(srcPaths_.size()); + for(const auto& srcPath: srcPaths_) { + FilePath destPath; + if(mode_ == Mode::LINK && !srcPath.isNative()) { + // special handling for URLs + auto fullBasename = srcPath.baseName(); + char* basename = fullBasename.get(); + char* dname = nullptr; + // if we drop URI query onto native filesystem, omit query part + if(!srcPath.isNative()) { + dname = strchr(basename, '?'); + } + // if basename consist only from query then use first part of it + if(dname == basename) { + basename++; + dname = strchr(basename, '&'); + } + + CStrPtr _basename; + if(dname) { + _basename = CStrPtr{g_strndup(basename, dname - basename)}; + dname = strrchr(_basename.get(), G_DIR_SEPARATOR); + g_debug("cutting '%s' to '%s'", basename, dname ? &dname[1] : _basename.get()); + if(dname) { + basename = &dname[1]; + } + else { + basename = _basename.get(); + } + } + destPath = destDirPath.child(basename); + } + else { + destPath = destDirPath.child(srcPath.baseName().get()); + } + destPaths_.emplace_back(std::move(destPath)); + } +} + +void FileTransferJob::gfileCopyProgressCallback(goffset current_num_bytes, goffset total_num_bytes, FileTransferJob* _this) { + _this->setCurrentFileProgress(total_num_bytes, current_num_bytes); +} + +bool FileTransferJob::moveFileSameFs(const FilePath& srcPath, const GFileInfoPtr& srcInfo, FilePath& destPath) { + int flags = G_FILE_COPY_ALL_METADATA | G_FILE_COPY_NOFOLLOW_SYMLINKS; + GErrorPtr err; + bool retry; + do { + retry = false; + err.reset(); + // do the file operation + if(!g_file_move(srcPath.gfile().get(), destPath.gfile().get(), GFileCopyFlags(flags), cancellable().get(), + nullptr, this, &err)) { + retry = handleError(err, srcPath, srcInfo, destPath, flags); + } + else { + return true; + } + } while(retry && !isCancelled()); + return false; +} + +bool FileTransferJob::copyRegularFile(const FilePath& srcPath, const GFileInfoPtr& srcInfo, FilePath& destPath) { + int flags = G_FILE_COPY_ALL_METADATA | G_FILE_COPY_NOFOLLOW_SYMLINKS; + GErrorPtr err; + bool retry; + do { + retry = false; + err.reset(); + + // reset progress of the current file (only for copy) + auto size = g_file_info_get_size(srcInfo.get()); + setCurrentFileProgress(size, 0); + + // do the file operation + if(!g_file_copy(srcPath.gfile().get(), destPath.gfile().get(), GFileCopyFlags(flags), cancellable().get(), + (GFileProgressCallback)&gfileCopyProgressCallback, this, &err)) { + retry = handleError(err, srcPath, srcInfo, destPath, flags); + } + else { + return true; + } + } while(retry && !isCancelled()); + return false; +} + +bool FileTransferJob::copySpecialFile(const FilePath& srcPath, const GFileInfoPtr& srcInfo, FilePath &destPath) { + bool ret = false; + // only handle FIFO for local files + if(srcPath.isNative() && destPath.isNative()) { + auto src_path = srcPath.localPath(); + struct stat src_st; + int r; + r = lstat(src_path.get(), &src_st); + if(r == 0) { + // Handle FIFO on native file systems. + if(S_ISFIFO(src_st.st_mode)) { + auto dest_path = destPath.localPath(); + if(mkfifo(dest_path.get(), src_st.st_mode) == 0) { + ret = true; + } + } + // FIXME: how about block device, char device, and socket? + } + } + if(!ret) { + GErrorPtr err; + g_set_error(&err, G_IO_ERROR, G_IO_ERROR_FAILED, + ("Cannot copy file '%s': not supported"), + g_file_info_get_display_name(srcInfo.get())); + emitError(err, ErrorSeverity::MODERATE); + } + return ret; +} + +bool FileTransferJob::copyDirContent(const FilePath& srcPath, GFileInfoPtr srcInfo, FilePath& destPath, bool skip) { + bool ret = false; + // copy dir content + GErrorPtr err; + auto enu = GFileEnumeratorPtr{ + g_file_enumerate_children(srcPath.gfile().get(), + defaultGFileInfoQueryAttribs, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err), + false}; + if(enu) { + int n_children = 0; + int n_copied = 0; + ret = true; + while(!isCancelled()) { + err.reset(); + GFileInfoPtr inf{g_file_enumerator_next_file(enu.get(), cancellable().get(), &err), false}; + if(inf) { + ++n_children; + const char* name = g_file_info_get_name(inf.get()); + FilePath childPath = srcPath.child(name); + bool child_ret = copyFile(childPath, inf, destPath, name, skip); + if(child_ret) { + ++n_copied; + } + else { + ret = false; + } + } + else { + if(err) { + // fail to read directory content + // NOTE: since we cannot read the source dir, we cannot calculate the progress correctly, either. + emitError(err, ErrorSeverity::MODERATE); + err.reset(); + /* ErrorAction::RETRY is not supported here */ + ret = false; + } + else { /* EOF is reached */ + /* all files are successfully copied. */ + if(isCancelled()) { + ret = false; + } + else { + /* some files are not copied */ + if(n_children != n_copied) { + /* if the copy actions are skipped deliberately, it's ok */ + if(!skip) { + ret = false; + } + } + /* else job->skip_dir_content is true */ + } + break; + } + } + } + g_file_enumerator_close(enu.get(), nullptr, &err); + } + else { + if(err) { + emitError(err, ErrorSeverity::MODERATE); + } + } + return ret; +} + +bool FileTransferJob::makeDir(const FilePath& srcPath, GFileInfoPtr srcInfo, FilePath& destPath) { + if(isCancelled()) { + return false; + } + + bool mkdir_done = false; + do { + GErrorPtr err; + mkdir_done = g_file_make_directory_with_parents(destPath.gfile().get(), cancellable().get(), &err); + if(!mkdir_done) { + if(err->domain == G_IO_ERROR && (err->code == G_IO_ERROR_EXISTS || + err->code == G_IO_ERROR_INVALID_FILENAME || + err->code == G_IO_ERROR_FILENAME_TOO_LONG)) { + GFileInfoPtr destInfo = GFileInfoPtr { + g_file_query_info(destPath.gfile().get(), + defaultGFileInfoQueryAttribs, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), nullptr), + false + }; + if(!destInfo) { + // FIXME: error handling + break; + } + + FilePath newDestPath; + FileExistsAction opt = askRename(FileInfo{srcInfo, srcPath.parent()}, FileInfo{destInfo, destPath.parent()}, newDestPath); + switch(opt) { + case FileOperationJob::RENAME: + destPath = std::move(newDestPath); + break; + case FileOperationJob::SKIP: + /* when a dir is skipped, we need to know its total size to calculate correct progress */ + mkdir_done = true; /* pretend that dir creation succeeded */ + break; + case FileOperationJob::OVERWRITE: + mkdir_done = true; /* pretend that dir creation succeeded */ + break; + case FileOperationJob::CANCEL: + cancel(); + return false; + case FileOperationJob::SKIP_ERROR: ; /* FIXME */ + } + } + else { + ErrorAction act = emitError(err, ErrorSeverity::MODERATE); + if(act != ErrorAction::RETRY) { + break; + } + } + } + } while(!mkdir_done && !isCancelled()); + + bool chmod_done = false; + if(mkdir_done && !isCancelled()) { + mode_t mode = g_file_info_get_attribute_uint32(srcInfo.get(), G_FILE_ATTRIBUTE_UNIX_MODE); + if(mode) { + mode |= (S_IRUSR | S_IWUSR); /* ensure we have rw permission to this file. */ + do { + GErrorPtr err; + // chmod the newly created dir properly + // if(!fm_job_is_cancelled(fmjob) && !job->skip_dir_content) + chmod_done = g_file_set_attribute_uint32(destPath.gfile().get(), + G_FILE_ATTRIBUTE_UNIX_MODE, + mode, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err); + if(!chmod_done) { + ErrorAction act = emitError(err, ErrorSeverity::MODERATE); + if(act != ErrorAction::RETRY) { + break; + } + /* FIXME: some filesystems may not support this. */ + } + } while(!chmod_done && !isCancelled()); + } + } + return mkdir_done && chmod_done; +} + +bool FileTransferJob::handleError(GErrorPtr &err, const FilePath &srcPath, const GFileInfoPtr &srcInfo, FilePath &destPath, int& flags) { + bool retry = false; + /* handle existing files or file name conflict */ + if(err.domain() == G_IO_ERROR && (err.code() == G_IO_ERROR_EXISTS || + err.code() == G_IO_ERROR_INVALID_FILENAME || + err.code() == G_IO_ERROR_FILENAME_TOO_LONG)) { + flags &= ~G_FILE_COPY_OVERWRITE; + + // get info of the existing file + GFileInfoPtr destInfo = GFileInfoPtr { + g_file_query_info(destPath.gfile().get(), + defaultGFileInfoQueryAttribs, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), nullptr), + false + }; + + // ask the user to rename or overwrite the existing file + if(!isCancelled() && destInfo) { + FilePath newDestPath; + FileExistsAction opt = askRename(FileInfo{srcInfo, srcPath.parent()}, + FileInfo{destInfo, destPath.parent()}, + newDestPath); + switch(opt) { + case FileOperationJob::RENAME: + // try a new file name + if(newDestPath.isValid()) { + destPath = std::move(newDestPath); + // FIXME: handle the error when newDestPath is invalid. + } + retry = true; + break; + case FileOperationJob::OVERWRITE: + // overwrite existing file + flags |= G_FILE_COPY_OVERWRITE; + retry = true; + err.reset(); + break; + case FileOperationJob::CANCEL: + // cancel the whole job. + cancel(); + break; + case FileOperationJob::SKIP: + // skip current file and don't copy it + case FileOperationJob::SKIP_ERROR: ; /* FIXME */ + retry = false; + break; + } + err.reset(); + } + } + + // show error message + if(!isCancelled() && err) { + ErrorAction act = emitError(err, ErrorSeverity::MODERATE); + err.reset(); + if(act == ErrorAction::RETRY) { + // the user wants retry the operation again + retry = true; + } + const bool is_no_space = (err.domain() == G_IO_ERROR && err.code() == G_IO_ERROR_NO_SPACE); + /* FIXME: ask to leave partial content? */ + if(is_no_space) { + // run out of disk space. delete the partial content we copied. + g_file_delete(destPath.gfile().get(), cancellable().get(), nullptr); + } + } + return retry; +} + +bool FileTransferJob::processPath(const FilePath& srcPath, const FilePath& destDirPath, const char* destFileName) { + GErrorPtr err; + GFileInfoPtr srcInfo = GFileInfoPtr { + g_file_query_info(srcPath.gfile().get(), + defaultGFileInfoQueryAttribs, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err), + false + }; + if(!srcInfo || isCancelled()) { + // FIXME: report error + return false; + } + + bool ret; + switch(mode_) { + case Mode::MOVE: + ret = moveFile(srcPath, srcInfo, destDirPath, destFileName); + break; + case Mode::COPY: { + bool deleteSrc = false; + ret = copyFile(srcPath, srcInfo, destDirPath, destFileName, deleteSrc); + break; + } + case Mode::LINK: + ret = linkFile(srcPath, srcInfo, destDirPath, destFileName); + break; + default: + ret = false; + break; + } + return ret; +} + +bool FileTransferJob::moveFile(const FilePath &srcPath, const GFileInfoPtr &srcInfo, const FilePath &destDirPath, const char *destFileName) { + setCurrentFile(srcPath); + + GErrorPtr err; + GFileInfoPtr destDirInfo = GFileInfoPtr { + g_file_query_info(destDirPath.gfile().get(), + "id::filesystem", + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err), + false + }; + + if(!destDirInfo || isCancelled()) { + // FIXME: report errors + return false; + } + + // If src and dest are on the same filesystem, do move. + // Exception: if src FS is trash:///, we always do move + // Otherwise, do copy & delete src files. + auto src_fs = g_file_info_get_attribute_string(srcInfo.get(), "id::filesystem"); + auto dest_fs = g_file_info_get_attribute_string(destDirInfo.get(), "id::filesystem"); + bool ret; + if(src_fs && dest_fs && (strcmp(src_fs, dest_fs) == 0 || g_str_has_prefix(src_fs, "trash"))) { + // src and dest are on the same filesystem + auto destPath = destDirPath.child(destFileName); + ret = moveFileSameFs(srcPath, srcInfo, destPath); + + // increase current progress + // FIXME: it's not appropriate to calculate the progress of move operations using file size + // since the time required to move a file is not related to it's file size. + auto size = g_file_info_get_size(srcInfo.get()); + addFinishedAmount(size, 1); + } + else { + // cross device/filesystem move: copy & delete + ret = copyFile(srcPath, srcInfo, destDirPath, destFileName); + // NOTE: do not need to increase progress here since it's done by copyPath(). + } + return ret; +} + +bool FileTransferJob::copyFile(const FilePath& srcPath, const GFileInfoPtr& srcInfo, const FilePath& destDirPath, const char* destFileName, bool skip) { + setCurrentFile(srcPath); + + auto size = g_file_info_get_size(srcInfo.get()); + bool success = false; + setCurrentFileProgress(size, 0); + + auto destPath = destDirPath.child(destFileName); + auto file_type = g_file_info_get_file_type(srcInfo.get()); + if(!skip) { + switch(file_type) { + case G_FILE_TYPE_DIRECTORY: + success = makeDir(srcPath, srcInfo, destPath); + break; + case G_FILE_TYPE_SPECIAL: + success = copySpecialFile(srcPath, srcInfo, destPath); + break; + default: + success = copyRegularFile(srcPath, srcInfo, destPath); + break; + } + } + else { // skip the file + success = true; + } + + if(success) { + // finish copying the file + addFinishedAmount(size, 1); + setCurrentFileProgress(0, 0); + + // recursively copy dir content + if(file_type == G_FILE_TYPE_DIRECTORY) { + success = copyDirContent(srcPath, srcInfo, destPath, skip); + } + + if(!skip && success && mode_ == Mode::MOVE) { + // delete the source file for cross-filesystem move + GErrorPtr err; + if(g_file_delete(srcPath.gfile().get(), cancellable().get(), &err)) { + // FIXME: add some file size to represent the amount of work need to delete a file + addFinishedAmount(1, 1); + } + else { + success = false; + } + } + } + return success; +} + +bool FileTransferJob::linkFile(const FilePath &srcPath, const GFileInfoPtr &srcInfo, const FilePath &destDirPath, const char *destFileName) { + setCurrentFile(srcPath); + + bool ret = false; + // cannot create links on non-native filesystems + if(!destDirPath.isNative()) { + auto msg = tr("Cannot create a link on non-native filesystem"); + GErrorPtr err{g_error_new_literal(G_IO_ERROR, G_IO_ERROR_FAILED, msg.toUtf8().constData())}; + emitError(err, ErrorSeverity::CRITICAL); + return false; + } + + if(srcPath.isNative()) { + // create symlinks for native files + auto destPath = destDirPath.child(destFileName); + ret = createSymlink(srcPath, srcInfo, destPath); + } + else { + // ensure that the dest file has *.desktop filename extension. + CStrPtr desktopEntryFileName{g_strconcat(destFileName, ".desktop", nullptr)}; + auto destPath = destDirPath.child(desktopEntryFileName.get()); + ret = createShortcut(srcPath, srcInfo, destPath); + } + + // update progress + // FIXME: increase the progress by 1 byte is not appropriate + addFinishedAmount(1, 1); + return ret; +} + +bool FileTransferJob::createSymlink(const FilePath &srcPath, const GFileInfoPtr &srcInfo, FilePath &destPath) { + bool ret = false; + auto src = srcPath.localPath(); + int flags = 0; + GErrorPtr err; + bool retry; + do { + retry = false; + if(flags & G_FILE_COPY_OVERWRITE) { // overwrite existing file + // creating symlink cannot overwrite existing files directly, so we delete the existing file first. + g_file_delete(destPath.gfile().get(), cancellable().get(), nullptr); + } + if(!g_file_make_symbolic_link(destPath.gfile().get(), src.get(), cancellable().get(), &err)) { + retry = handleError(err, srcPath, srcInfo, destPath, flags); + } + else { + ret = true; + break; + } + } while(!isCancelled() && retry); + return ret; +} + +bool FileTransferJob::createShortcut(const FilePath &srcPath, const GFileInfoPtr &srcInfo, FilePath &destPath) { + bool ret = false; + const char* iconName = nullptr; + GIcon* icon = g_file_info_get_icon(srcInfo.get()); + if(icon && G_IS_THEMED_ICON(icon)) { + auto iconNames = g_themed_icon_get_names(G_THEMED_ICON(icon)); + if(iconNames && iconNames[0]) { + iconName = iconNames[0]; + } + } + + CStrPtr srcPathUri; + auto uri = g_file_info_get_attribute_string(srcInfo.get(), G_FILE_ATTRIBUTE_STANDARD_TARGET_URI); + if(!uri) { + srcPathUri = srcPath.uri(); + uri = srcPathUri.get(); + } + + CStrPtr srcPathDispName; + auto name = g_file_info_get_display_name(srcInfo.get()); + if(!name) { + srcPathDispName = srcPath.displayName(); + name = srcPathDispName.get(); + } + + GKeyFile* kf = g_key_file_new(); + if(kf) { + g_key_file_set_string(kf, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_TYPE, "Link"); + g_key_file_set_string(kf, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_NAME, name); + if(iconName) { + g_key_file_set_string(kf, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_ICON, iconName); + } + if(uri) { + g_key_file_set_string(kf, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_URL, uri); + } + gsize contentLen; + CStrPtr content{g_key_file_to_data(kf, &contentLen, nullptr)}; + g_key_file_free(kf); + + int flags = 0; + if(content) { + bool retry; + GErrorPtr err; + do { + retry = false; + if(flags & G_FILE_COPY_OVERWRITE) { // overwrite existing file + g_file_delete(destPath.gfile().get(), cancellable().get(), nullptr); + } + + if(!g_file_replace_contents(destPath.gfile().get(), content.get(), contentLen, nullptr, false, G_FILE_CREATE_NONE, nullptr, cancellable().get(), &err)) { + retry = handleError(err, srcPath, srcInfo, destPath, flags); + err.reset(); + } + else { + ret = true; + } + } while(!isCancelled() && retry); + ret = true; + } + } + return ret; +} + + +void FileTransferJob::exec() { + // calculate the total size of files to copy + auto totalSizeFlags = (mode_ == Mode::COPY ? TotalSizeJob::DEFAULT : TotalSizeJob::PREPARE_MOVE); + TotalSizeJob totalSizeJob{srcPaths_, totalSizeFlags}; + connect(&totalSizeJob, &TotalSizeJob::error, this, &FileTransferJob::error); + connect(this, &FileTransferJob::cancelled, &totalSizeJob, &TotalSizeJob::cancel); + totalSizeJob.run(); + if(isCancelled()) { + return; + } + + // ready to start + setTotalAmount(totalSizeJob.totalSize(), totalSizeJob.fileCount()); + Q_EMIT preparedToRun(); + + if(srcPaths_.size() != destPaths_.size()) { + qWarning("error: srcPaths.size() != destPaths.size() when copying files"); + return; + } + + // copy the files + for(size_t i = 0; i < srcPaths_.size(); ++i) { + if(isCancelled()) { + break; + } + const auto& srcPath = srcPaths_[i]; + const auto& destPath = destPaths_[i]; + auto destDirPath = destPath.parent(); + processPath(srcPath, destDirPath, destPath.baseName().get()); + } +} + + +} // namespace Fm diff --git a/src/core/filetransferjob.h b/src/core/filetransferjob.h new file mode 100644 index 0000000..7c3d8a0 --- /dev/null +++ b/src/core/filetransferjob.h @@ -0,0 +1,58 @@ +#ifndef FM2_COPYJOB_H +#define FM2_COPYJOB_H + +#include "../libfmqtglobals.h" +#include "fileoperationjob.h" +#include "gioptrs.h" + +namespace Fm { + +class LIBFM_QT_API FileTransferJob : public Fm::FileOperationJob { + Q_OBJECT +public: + + enum class Mode { + COPY, + MOVE, + LINK + }; + + explicit FileTransferJob(FilePathList srcPaths, Mode mode = Mode::COPY); + explicit FileTransferJob(FilePathList srcPaths, FilePathList destPaths, Mode mode = Mode::COPY); + explicit FileTransferJob(FilePathList srcPaths, const FilePath &destDirPath, Mode mode = Mode::COPY); + + void setSrcPaths(FilePathList srcPaths); + void setDestPaths(FilePathList destPaths); + void setDestDirPath(const FilePath &destDirPath); + +protected: + void exec() override; + +private: + bool processPath(const FilePath& srcPath, const FilePath& destPath, const char *destFileName); + bool moveFile(const FilePath &srcPath, const GFileInfoPtr &srcInfo, const FilePath &destDirPath, const char *destFileName); + bool copyFile(const FilePath &srcPath, const GFileInfoPtr &srcInfo, const FilePath &destDirPath, const char *destFileName, bool skip = false); + bool linkFile(const FilePath &srcPath, const GFileInfoPtr &srcInfo, const FilePath &destDirPath, const char *destFileName); + + bool moveFileSameFs(const FilePath &srcPath, const GFileInfoPtr& srcInfo, FilePath &destPath); + bool copyRegularFile(const FilePath &srcPath, const GFileInfoPtr& srcInfo, FilePath &destPath); + bool copySpecialFile(const FilePath &srcPath, const GFileInfoPtr& srcInfo, FilePath& destPath); + bool copyDirContent(const FilePath &srcPath, GFileInfoPtr srcInfo, FilePath &destPath, bool skip = false); + bool makeDir(const FilePath &srcPath, GFileInfoPtr srcInfo, FilePath &destPath); + bool createSymlink(const FilePath &srcPath, const GFileInfoPtr& srcInfo, FilePath& destPath); + bool createShortcut(const FilePath &srcPath, const GFileInfoPtr& srcInfo, FilePath& destPath); + + bool handleError(GErrorPtr& err, const FilePath &srcPath, const GFileInfoPtr &srcInfo, FilePath &destPath, int& flags); + + static void gfileCopyProgressCallback(goffset current_num_bytes, goffset total_num_bytes, FileTransferJob* _this); + +private: + FilePathList srcPaths_; + FilePathList destPaths_; + Mode mode_; +}; + + +} // namespace Fm + +#endif // FM2_COPYJOB_H diff --git a/src/core/folder.cpp b/src/core/folder.cpp index d91600a..66d4b7b 100644 --- a/src/core/folder.cpp +++ b/src/core/folder.cpp @@ -34,8 +34,8 @@ namespace Fm { std::unordered_map, FilePathHash> Folder::cache_; -FilePath Folder::cutFilesDirPath_; -FilePath Folder::lastCutFilesDirPath_; +QString Folder::cutFilesDirPath_; +QString Folder::lastCutFilesDirPath_; std::shared_ptr Folder::cutFilesHashSet_; std::mutex Folder::mutex_; @@ -205,9 +205,11 @@ void Folder::onFileInfoFinished() { return; FileInfoList files_to_add; + FileInfoList files_to_delete; std::vector files_to_update; const auto& paths = job->paths(); + const auto& deletionPaths = job->deletionPaths(); const auto& infos = job->files(); auto path_it = paths.cbegin(); auto info_it = infos.cbegin(); @@ -218,7 +220,8 @@ void Folder::onFileInfoFinished() { if(path == dirPath_) { // got the info for the folder itself. dirInfo_ = info; } - else { + // add/update the file only if it isn't going to be deleted + else if(std::find(deletionPaths.cbegin(), deletionPaths.cend(), path) == deletionPaths.cend()) { auto it = files_.find(info->name()); if(it != files_.end()) { // the file already exists, update files_to_update.push_back(std::make_pair(it->second, info)); @@ -235,6 +238,18 @@ void Folder::onFileInfoFinished() { if(!files_to_update.empty()) { Q_EMIT filesChanged(files_to_update); } + // deletion should be done now, after the info job is processed + for(const auto &path: deletionPaths) { + auto name = path.baseName(); + auto it = files_.find(name.get()); + if(it != files_.end()) { + files_to_delete.push_back(it->second); + files_.erase(it); + } + } + if(!files_to_delete.empty()) { + Q_EMIT filesRemoved(files_to_delete); + } Q_EMIT contentChanged(); } @@ -250,14 +265,16 @@ void Folder::processPendingChanges() { } FileInfoJob* info_job = nullptr; - if(!paths_to_update.empty() || !paths_to_add.empty()) { - FilePathList paths; + if(!paths_to_update.empty() || !paths_to_add.empty() || !paths_to_del.empty()) { + FilePathList paths, deletionPaths; paths.insert(paths.end(), paths_to_add.cbegin(), paths_to_add.cend()); paths.insert(paths.end(), paths_to_update.cbegin(), paths_to_update.cend()); - info_job = new FileInfoJob{paths, dirPath_, - hasCutFiles() ? cutFilesHashSet_ : nullptr}; + deletionPaths.insert(deletionPaths.end(), paths_to_del.cbegin(), paths_to_del.cend()); + info_job = new FileInfoJob{paths, deletionPaths, dirPath_, + hasCutFiles() ? cutFilesHashSet_ : nullptr}; paths_to_update.clear(); paths_to_add.clear(); + paths_to_del.clear(); } if(info_job) { @@ -275,21 +292,6 @@ void Folder::processPendingChanges() { #endif } - if(!paths_to_del.empty()) { - FileInfoList deleted_files; - for(const auto &path: paths_to_del) { - auto name = path.baseName(); - auto it = files_.find(name.get()); - if(it != files_.end()) { - deleted_files.push_back(it->second); - files_.erase(it); - } - } - Q_EMIT filesRemoved(deleted_files); - Q_EMIT contentChanged(); - paths_to_del.clear(); - } - if(pending_change_notify) { Q_EMIT changed(); /* update volume info */ @@ -346,10 +348,10 @@ bool Folder::eventFileAdded(const FilePath &path) { bool Folder::eventFileChanged(const FilePath &path) { bool added; // G_LOCK(lists); - /* make sure that the file is not already queued for changes or - * it's already queued for addition. */ + /* make sure that the file is not already queued for changes, addition or deletion */ if(std::find(paths_to_update.cbegin(), paths_to_update.cend(), path) == paths_to_update.cend() - && std::find(paths_to_add.cbegin(), paths_to_add.cend(), path) == paths_to_add.cend()) { + && std::find(paths_to_add.cbegin(), paths_to_add.cend(), path) == paths_to_add.cend() + && std::find(paths_to_del.cbegin(), paths_to_del.cend(), path) == paths_to_del.cend()) { /* Since this function is called only when a file already exists, even if that file isn't included in "files_" yet, it will be soon due to a previous call to queueUpdate(). So, here, we should queue it for changes regardless of what "files_" may contain. */ @@ -367,14 +369,19 @@ bool Folder::eventFileChanged(const FilePath &path) { void Folder::eventFileDeleted(const FilePath& path) { // qDebug() << "delete " << path.baseName().get(); // G_LOCK(lists); - if(files_.find(path.baseName().get()) != files_.cend()) { + /* Queue the file for deletion only if it is not in the addition queue but + if it is, remove it from that queue instead of queueing it for deletion. + Moreover, as was the case with eventFileChanged(), here too queueing + should be done regardless of what "files_" may contain. */ + if(std::find(paths_to_add.cbegin(), paths_to_add.cend(), path) == paths_to_add.cend()) { if(std::find(paths_to_del.cbegin(), paths_to_del.cend(), path) == paths_to_del.cend()) { paths_to_del.push_back(path); } } - /* if the file is already queued for addition or update, that operation - will be just a waste, therefore cancel it right now */ - paths_to_add.erase(std::remove(paths_to_add.begin(), paths_to_add.end(), path), paths_to_add.cend()); + else { + paths_to_add.erase(std::remove(paths_to_add.begin(), paths_to_add.end(), path), paths_to_add.cend()); + } + /* the update queue should be canceled for a file that is going to be deleted */ paths_to_update.erase(std::remove(paths_to_update.begin(), paths_to_update.end(), path), paths_to_update.cend()); queueUpdate(); // G_UNLOCK(lists); @@ -451,17 +458,16 @@ void Folder::onFileChangeEvents(GFileMonitor* /*monitor*/, GFile* gf, GFile* /*o eventFileDeleted(path); break; default: - return; + break; } - queueUpdate(); } } // checks whether there were cut files here // and if there were, invalidates this last cut path bool Folder::hadCutFilesUnset() { - if(lastCutFilesDirPath_ == dirPath_) { - lastCutFilesDirPath_ = FilePath(); + if(lastCutFilesDirPath_ == dirPath_.toString().get()) { + lastCutFilesDirPath_ = QString(); return true; } return false; @@ -470,14 +476,14 @@ bool Folder::hadCutFilesUnset() { bool Folder::hasCutFiles() { return cutFilesHashSet_ && !cutFilesHashSet_->empty() - && cutFilesDirPath_ == dirPath_; + && cutFilesDirPath_ == dirPath_.toString().get(); } void Folder::setCutFiles(const std::shared_ptr& cutFilesHashSet) { if(cutFilesHashSet_ && !cutFilesHashSet_->empty()) { lastCutFilesDirPath_ = cutFilesDirPath_; } - cutFilesDirPath_ = dirPath_; + cutFilesDirPath_ = dirPath_.toString().get(); cutFilesHashSet_ = cutFilesHashSet; } diff --git a/src/core/folder.h b/src/core/folder.h index b4b1f39..d2bbb76 100644 --- a/src/core/folder.h +++ b/src/core/folder.h @@ -185,8 +185,8 @@ private: bool defer_content_test : 1; static std::unordered_map, FilePathHash> cache_; - static FilePath cutFilesDirPath_; - static FilePath lastCutFilesDirPath_; + static QString cutFilesDirPath_; + static QString lastCutFilesDirPath_; static std::shared_ptr cutFilesHashSet_; static std::mutex mutex_; }; diff --git a/src/core/iconinfo.cpp b/src/core/iconinfo.cpp index 5ec2abb..7e1f145 100644 --- a/src/core/iconinfo.cpp +++ b/src/core/iconinfo.cpp @@ -5,7 +5,7 @@ namespace Fm { std::unordered_map, IconInfo::GIconHash, IconInfo::GIconEqual> IconInfo::cache_; std::mutex IconInfo::mutex_; -QIcon IconInfo::fallbackQicon_; +QList IconInfo::fallbackQicons_; static const char* fallbackIconNames[] = { "unknown", @@ -15,6 +15,15 @@ static const char* fallbackIconNames[] = { nullptr }; +static QIcon getFirst(const QList & icons) +{ + for (const auto & icon : icons) { + if (!icon.isNull()) + return icon; + } + return QIcon{}; +} + IconInfo::IconInfo(const char* name): gicon_{g_themed_icon_new(name), false} { } @@ -50,10 +59,9 @@ std::shared_ptr IconInfo::fromGIcon(GIconPtr gicon) { void IconInfo::updateQIcons() { std::lock_guard lock{mutex_}; - fallbackQicon_ = QIcon(); for(auto& elem: cache_) { auto& info = elem.second; - info->internalQicon_ = QIcon(); + info->internalQicons_.clear(); } } @@ -64,7 +72,7 @@ QIcon IconInfo::qicon(const bool& transparent) const { qicon_ = QIcon(new IconEngine{shared_from_this()}); } else { - qicon_ = internalQicon_; + qicon_ = getFirst(internalQicons_); } } } @@ -74,24 +82,21 @@ QIcon IconInfo::qicon(const bool& transparent) const { qiconTransparent_ = QIcon(new IconEngine{shared_from_this(), transparent}); } else { - qiconTransparent_ = internalQicon_; + qiconTransparent_ = getFirst(internalQicons_); } } } return !transparent ? qicon_ : qiconTransparent_; } -QIcon IconInfo::qiconFromNames(const char* const* names) { - const gchar* const* name; +QList IconInfo::qiconsFromNames(const char* const* names) { + QList icons; // qDebug("names: %p", names); - for(name = names; *name; ++name) { + for(const gchar* const* name = names; *name; ++name) { // qDebug("icon name=%s", *name); - QIcon qicon = QIcon::fromTheme(*name); - if(!qicon.isNull()) { - return qicon; - } + icons.push_back(QIcon::fromTheme(*name)); } - return QIcon(); + return icons; } std::forward_list> IconInfo::emblems() const { @@ -109,30 +114,34 @@ std::forward_list> IconInfo::emblems() const { } QIcon IconInfo::internalQicon() const { - if(Q_UNLIKELY(internalQicon_.isNull())) { + QIcon ret_icon; + if(Q_UNLIKELY(internalQicons_.isEmpty())) { GIcon* gicon = gicon_.get(); if(G_IS_EMBLEMED_ICON(gicon_.get())) { gicon = g_emblemed_icon_get_icon(G_EMBLEMED_ICON(gicon)); } if(G_IS_THEMED_ICON(gicon)) { const gchar* const* names = g_themed_icon_get_names(G_THEMED_ICON(gicon)); - internalQicon_ = qiconFromNames(names); + internalQicons_ = qiconsFromNames(names); } else if(G_IS_FILE_ICON(gicon)) { GFile* file = g_file_icon_get_file(G_FILE_ICON(gicon)); CStrPtr fpath{g_file_get_path(file)}; - internalQicon_ = QIcon(fpath.get()); + internalQicons_.push_back(QIcon(fpath.get())); } - // fallback to default icon - if(Q_UNLIKELY(internalQicon_.isNull())) { - if(Q_UNLIKELY(fallbackQicon_.isNull())) { - fallbackQicon_ = qiconFromNames(fallbackIconNames); - } - internalQicon_ = fallbackQicon_; + } + + ret_icon = getFirst(internalQicons_); + + // fallback to default icon + if(Q_UNLIKELY(ret_icon.isNull())) { + if(Q_UNLIKELY(fallbackQicons_.isEmpty())) { + fallbackQicons_ = qiconsFromNames(fallbackIconNames); } + ret_icon = getFirst(fallbackQicons_); } - return internalQicon_; + return ret_icon; } } // namespace Fm diff --git a/src/core/iconinfo.h b/src/core/iconinfo.h index 60924b1..ad21f0c 100644 --- a/src/core/iconinfo.h +++ b/src/core/iconinfo.h @@ -77,7 +77,7 @@ public: private: - static QIcon qiconFromNames(const char* const* names); + static QList qiconsFromNames(const char* const* names); // actual QIcon loaded by QIcon::fromTheme QIcon internalQicon() const; @@ -98,11 +98,11 @@ private: GIconPtr gicon_; mutable QIcon qicon_; mutable QIcon qiconTransparent_; - mutable QIcon internalQicon_; + mutable QList internalQicons_; static std::unordered_map, GIconHash, GIconEqual> cache_; static std::mutex mutex_; - static QIcon fallbackQicon_; + static QList fallbackQicons_; }; } // namespace Fm diff --git a/src/core/templates.cpp b/src/core/templates.cpp new file mode 100644 index 0000000..082f83b --- /dev/null +++ b/src/core/templates.cpp @@ -0,0 +1,130 @@ +#include "templates.h" +#include "gioptrs.h" + +#include +#include + +using namespace std; + +namespace Fm { + +std::weak_ptr Templates::globalInstance_; + +TemplateItem::TemplateItem(std::shared_ptr file): fileInfo_{file} { +} + +FilePath TemplateItem::filePath() const { + auto& target = fileInfo_->target(); + if(fileInfo_->isDesktopEntry() && !target.empty()) { + if(target[0] == '/') { // target is an absolute path + return FilePath::fromLocalPath(target.c_str()); + } + else { // resolve relative path + return fileInfo_->dirPath().relativePath(target.c_str()); + } + } + return fileInfo_->path(); +} + +Templates::Templates() : QObject() { + auto* data_dirs = g_get_system_data_dirs(); + // system-wide template dirs + for(auto data_dir = data_dirs; *data_dir; ++data_dir) { + CStrPtr dir_name{g_build_filename(*data_dir, "templates", nullptr)}; + addTemplateDir(dir_name.get()); + } + + // user-specific template dir + CStrPtr dir_name{g_build_filename(g_get_user_data_dir(), "templates", nullptr)}; + addTemplateDir(dir_name.get()); + + // $XDG_TEMPLATES_DIR (FIXME: this might change at runtime) + const gchar *special_dir = g_get_user_special_dir(G_USER_DIRECTORY_TEMPLATES); + if (special_dir) { + addTemplateDir(special_dir); + } +} + +shared_ptr Templates::globalInstance() { + auto templates = globalInstance_.lock(); + if(!templates) { + templates = make_shared(); + globalInstance_ = templates; + } + return templates; +} + +void Templates::addTemplateDir(const char* dirPathName) { + auto dir_path = FilePath::fromLocalPath(dirPathName); + if(dir_path.isValid()) { + auto folder = Folder::fromPath(dir_path); + connect(folder.get(), &Folder::filesAdded, this, &Templates::onFilesAdded); + connect(folder.get(), &Folder::filesChanged, this, &Templates::onFilesChanged); + connect(folder.get(), &Folder::filesRemoved, this, &Templates::onFilesRemoved); + connect(folder.get(), &Folder::removed, this, &Templates::onTemplateDirRemoved); + templateFolders_.emplace_back(std::move(folder)); + } +} + +void Templates::onFilesAdded(FileInfoList& addedFiles) { + for(auto& file : addedFiles) { + // FIXME: we do not support subdirs right now (only XFCE supports this) + if(file->isHidden() || file->isDir()) { + continue; + } + items_.emplace_back(std::make_shared(file)); + // emit a signal for the addition + Q_EMIT itemAdded(items_.back()); + } +} + +void Templates::onFilesChanged(std::vector& changePairs) { + for(auto& change: changePairs) { + auto& old_file = change.first; + auto& new_file = change.second; + auto it = std::find_if(items_.begin(), items_.end(), [&](const std::shared_ptr& item) { + return item->fileInfo() == old_file; + }); + if(it != items_.end()) { + // emit a signal for the change + auto old = *it; + *it = std::make_shared(new_file); + Q_EMIT itemChanged(old, *it); + } + } +} + +void Templates::onFilesRemoved(FileInfoList& removedFiles) { + for(auto& file : removedFiles) { + auto filePath = file->path(); + auto it = std::remove_if(items_.begin(), items_.end(), [&](const std::shared_ptr& item) { + return item->fileInfo() == file; + }); + for(auto removed_it = it; it != items_.end(); ++it) { + // emit a signal for the removal + Q_EMIT itemRemoved(*removed_it); + } + items_.erase(it, items_.end()); + } +} + +void Templates::onTemplateDirRemoved() { + // the whole template dir is removed + auto folder = static_cast(sender()); + if(!folder) { + return; + } + auto dirPath = folder->path(); + + // remote all files under this dir + auto it = std::remove_if(items_.begin(), items_.end(), [&](const std::shared_ptr& item) { + return dirPath.isPrefixOf(item->filePath()); + }); + for(auto removed_it = it; it != items_.end(); ++it) { + // emit a signal for the removal + Q_EMIT itemRemoved(*removed_it); + } + items_.erase(it, items_.end()); +} + +} // namespace Fm diff --git a/src/core/templates.h b/src/core/templates.h new file mode 100644 index 0000000..6f656dd --- /dev/null +++ b/src/core/templates.h @@ -0,0 +1,100 @@ +#ifndef TEMPLATES_H +#define TEMPLATES_H + +#include "../libfmqtglobals.h" + +#include +#include +#include +#include "folder.h" +#include "fileinfo.h" +#include "mimetype.h" +#include "iconinfo.h" + +namespace Fm { + +class LIBFM_QT_API TemplateItem { +public: + explicit TemplateItem(std::shared_ptr fileInfo); + + QString displayName() const { + return fileInfo_->displayName(); + } + + const std::string& name() const { + return fileInfo_->name(); + } + + std::shared_ptr icon() const { + return fileInfo_->icon(); + } + + std::shared_ptr fileInfo() const { + return fileInfo_; + } + + std::shared_ptr mimeType() const { + return fileInfo_->mimeType(); + } + + FilePath filePath() const; + +private: + std::shared_ptr fileInfo_; +}; + + +class LIBFM_QT_API Templates : public QObject { + Q_OBJECT +public: + explicit Templates(); + + // FIXME: the first call to this method will get no templates since dir loading is in progress. + static std::shared_ptr globalInstance(); + + void forEachItem(std::function&)> func) const { + for(const auto& item : items_) { + func(item); + } + } + + std::vector> items() const { + std::vector> tmp_items; + for(auto& item: items_) { + tmp_items.emplace_back(item); + } + return tmp_items; + } + + bool hasTemplates() const { + return !items_.empty(); + } + +Q_SIGNALS: + void itemAdded(const std::shared_ptr& item); + + void itemChanged(const std::shared_ptr& oldItem, const std::shared_ptr& newItem); + + void itemRemoved(const std::shared_ptr& item); + +private: + void addTemplateDir(const char* dirPathName); + +private Q_SLOTS: + void onFilesAdded(FileInfoList& addedFiles); + + void onFilesChanged(std::vector& changePairs); + + void onFilesRemoved(FileInfoList& removedFiles); + + void onTemplateDirRemoved(); + +private: + std::vector> items_; + std::vector> templateFolders_; + static std::weak_ptr globalInstance_; +}; + +} // namespace Fm + +#endif // TEMPLATES_H diff --git a/src/core/thumbnailer.cpp b/src/core/thumbnailer.cpp index 8aedd75..ab2c80d 100644 --- a/src/core/thumbnailer.cpp +++ b/src/core/thumbnailer.cpp @@ -21,7 +21,7 @@ CStrPtr Thumbnailer::commandForUri(const char* uri, const char* output_file, gui /* FIXME: how to handle TryExec? */ /* parse the command line and do required substitutions according to: - * http://developer.gnome.org/integration-guide/stable/thumbnailer.html.en + * https://developer.gnome.org/integration-guide/stable/thumbnailer.html.en */ GString* cmd_line = g_string_sized_new(1024); const char* p; @@ -119,16 +119,17 @@ void Thumbnailer::loadAll() { CStrPtr file_path{g_build_filename(dir_path, "thumbnailers", base_name.c_str(), nullptr)}; if(g_key_file_load_from_file(kf, file_path.get(), G_KEY_FILE_NONE, nullptr)) { auto thumbnailer = std::make_shared(base_name.c_str(), kf); - char** mime_types = g_key_file_get_string_list(kf, "Thumbnailer Entry", "MimeType", nullptr, nullptr); - if(mime_types && thumbnailer->exec_) { - for(char** name = mime_types; *name; ++name) { - auto mime_type = MimeType::fromName(*name); - if(mime_type) { - thumbnailer->mimeTypes_.push_back(mime_type); - std::const_pointer_cast(mime_type)->addThumbnailer(thumbnailer); + if(thumbnailer->exec_) { + char** mime_types = g_key_file_get_string_list(kf, "Thumbnailer Entry", "MimeType", nullptr, nullptr); + if(mime_types) { + for(char** name = mime_types; *name; ++name) { + auto mime_type = MimeType::fromName(*name); + if(mime_type) { + std::const_pointer_cast(mime_type)->addThumbnailer(thumbnailer); + } } + g_strfreev(mime_types); } - g_strfreev(mime_types); } allThumbnailers_.push_back(std::move(thumbnailer)); } diff --git a/src/core/thumbnailer.h b/src/core/thumbnailer.h index c1a7328..91a21d7 100644 --- a/src/core/thumbnailer.h +++ b/src/core/thumbnailer.h @@ -26,7 +26,7 @@ private: CStrPtr id_; CStrPtr try_exec_; /* FIXME: is this useful? */ CStrPtr exec_; - std::vector> mimeTypes_; + //std::vector> mimeTypes_; static std::mutex mutex_; static std::vector> allThumbnailers_; diff --git a/src/core/thumbnailjob.cpp b/src/core/thumbnailjob.cpp index 61eccff..21e256a 100644 --- a/src/core/thumbnailjob.cpp +++ b/src/core/thumbnailjob.cpp @@ -120,7 +120,7 @@ bool ThumbnailJob::isSupportedImageType(const std::shared_ptr& m bool ThumbnailJob::isThumbnailOutdated(const std::shared_ptr& file, const QImage &thumbnail) const { QString thumb_mtime = thumbnail.text("Thumb::MTime"); - return (thumb_mtime.isEmpty() || thumb_mtime.toInt() != file->mtime()); + return (thumb_mtime.isEmpty() || thumb_mtime.toULongLong() != file->mtime()); } bool ThumbnailJob::readJpegExif(GInputStream *stream, QImage& thumbnail, int& rotate_degrees) { @@ -140,7 +140,7 @@ bool ThumbnailJob::readJpegExif(GInputStream *stream, QImage& thumbnail, int& ro exif_loader_unref(exif_loader); if(exif_data) { /* reference for EXIF orientation tag: - * http://www.impulseadventure.com/photo/exif-orientation.html */ + * https://www.impulseadventure.com/photo/exif-orientation.html */ ExifEntry* orient_ent = exif_data_get_entry(exif_data, EXIF_TAG_ORIENTATION); if(orient_ent) { /* orientation flag found in EXIF */ gushort orient; diff --git a/src/core/totalsizejob.cpp b/src/core/totalsizejob.cpp index 81ff0db..090be86 100644 --- a/src/core/totalsizejob.cpp +++ b/src/core/totalsizejob.cpp @@ -61,37 +61,34 @@ _retry_query_info: /* prepare for moving across different devices */ if(flags_ & PREPARE_MOVE) { fs_id = g_file_info_get_attribute_string(inf.get(), G_FILE_ATTRIBUTE_ID_FILESYSTEM); - fs_id = g_intern_string(fs_id); - if(g_strcmp0(fs_id, dest_fs_id) != 0) { + if(fs_id && dest_fs_id && (strcmp(fs_id, dest_fs_id) == 0 || g_str_has_prefix(fs_id, "trash"))) { + // same filesystem or move from trash:/// + descend = false; + } + else { /* files on different device requires an additional 'delete' for the source file. */ ++totalSize_; /* this is for the additional delete */ ++totalOndiskSize_; ++fileCount_; - } - else { - descend = false; + descend = true; } } if(type == G_FILE_TYPE_DIRECTORY) { -#if 0 - FmPath* fm_path = fm_path_new_for_gfile(gf); /* check if we need to decends into the dir. */ - /* trash:/// doesn't support deleting files recursively */ - if(flags & PREPARE_DELETE && fm_path_is_trash(fm_path) && ! fm_path_is_trash_root(fm_path)) { + /* trash:/// doesn't support deleting files recursively (but we want to descend into trash root "trash:///" */ + if(flags_ & PREPARE_DELETE && path.hasUriScheme("trash") && path.baseName()[0] != '/') { descend = false; } else { /* only descends into files on the same filesystem */ - if(flags & FM_DC_JOB_SAME_FS) { - fs_id = g_file_info_get_attribute_string(inf, G_FILE_ATTRIBUTE_ID_FILESYSTEM); + if(flags_ & SAME_FS) { + fs_id = g_file_info_get_attribute_string(inf.get(), G_FILE_ATTRIBUTE_ID_FILESYSTEM); descend = (g_strcmp0(fs_id, dest_fs_id) == 0); } } - fm_path_unref(fm_path); -#endif - inf = nullptr; + inf = nullptr; if(descend) { _retry_enum_children: GErrorPtr err; diff --git a/src/core/trashjob.cpp b/src/core/trashjob.cpp index 31a5c3c..18996a9 100644 --- a/src/core/trashjob.cpp +++ b/src/core/trashjob.cpp @@ -2,10 +2,9 @@ namespace Fm { -TrashJob::TrashJob(const FilePathList& paths): paths_{paths} { -} - -TrashJob::TrashJob(const FilePathList&& paths): paths_{paths} { +TrashJob::TrashJob(FilePathList paths): paths_{std::move(paths)} { + // calculate progress using finished file counts rather than their sizes + setCalcProgressUsingSize(false); } void TrashJob::exec() { @@ -20,33 +19,32 @@ void TrashJob::exec() { setCurrentFile(path); - for(;;) { - GErrorPtr err; - GFile* gf = path.gfile().get(); - GFileInfoPtr inf{ - g_file_query_info(gf, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME, G_FILE_QUERY_INFO_NONE, - cancellable().get(), &err), - false - }; + // TODO: get parent dir of the current path. + // if there is a Fm::Folder object created for it, block the update for the folder temporarily. - bool ret = FALSE; + for(;;) { // retry the i/o operation on errors + auto gf = path.gfile(); + bool ret = false; + // FIXME: do not depend on fm_config if(fm_config->no_usb_trash) { - err.reset(); - GMountPtr mnt{g_file_find_enclosing_mount(gf, nullptr, &err), false}; + GMountPtr mnt{g_file_find_enclosing_mount(gf.get(), nullptr, nullptr), false}; if(mnt) { ret = g_mount_can_unmount(mnt.get()); /* TRUE if it's removable media */ if(ret) { unsupportedFiles_.push_back(path); + break; // don't trash the file } } } - if(!ret) { - err.reset(); - ret = g_file_trash(gf, cancellable().get(), &err); + // move the file to trash + GErrorPtr err; + ret = g_file_trash(gf.get(), cancellable().get(), &err); + if(ret) { // trash operation succeeded + break; } - if(!ret) { - /* if trashing is not supported by the file system */ + else { // failed + // if trashing is not supported by the file system if(err.domain() == G_IO_ERROR && err.code() == G_IO_ERROR_NOT_SUPPORTED) { unsupportedFiles_.push_back(path); } diff --git a/src/core/trashjob.h b/src/core/trashjob.h index 519fdd8..65d93cf 100644 --- a/src/core/trashjob.h +++ b/src/core/trashjob.h @@ -10,8 +10,7 @@ namespace Fm { class LIBFM_QT_API TrashJob : public Fm::FileOperationJob { Q_OBJECT public: - explicit TrashJob(const FilePathList& paths); - explicit TrashJob(const FilePathList&& paths); + explicit TrashJob(FilePathList paths); FilePathList unsupportedFiles() const { return unsupportedFiles_; diff --git a/src/core/untrashjob.cpp b/src/core/untrashjob.cpp index 14148af..031509e 100644 --- a/src/core/untrashjob.cpp +++ b/src/core/untrashjob.cpp @@ -1,132 +1,76 @@ #include "untrashjob.h" +#include "filetransferjob.h" namespace Fm { -UntrashJob::UntrashJob() { - -} - -static const char trash_query[] = - G_FILE_ATTRIBUTE_STANDARD_TYPE"," - G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME"," - G_FILE_ATTRIBUTE_STANDARD_NAME"," - G_FILE_ATTRIBUTE_STANDARD_IS_VIRTUAL"," - G_FILE_ATTRIBUTE_STANDARD_SIZE"," - G_FILE_ATTRIBUTE_UNIX_BLOCKS"," - G_FILE_ATTRIBUTE_UNIX_BLOCK_SIZE"," - G_FILE_ATTRIBUTE_ID_FILESYSTEM"," - "trash::orig-path"; - -bool UntrashJob::ensure_parent_dir(GFile* orig_path) { - GFile* parent = g_file_get_parent(orig_path); - gboolean ret = g_file_query_exists(parent, cancellable().get()); - if(!ret) { - GErrorPtr err; -_retry_mkdir: - if(!g_file_make_directory_with_parents(parent, cancellable().get(), &err)) { - if(!isCancelled()) { - ErrorAction act = emitError(err, ErrorSeverity::MODERATE); - err = nullptr; - if(act == ErrorAction::RETRY) { - goto _retry_mkdir; - } - } - } - else { - ret = TRUE; - } - } - g_object_unref(parent); - return ret; +UntrashJob::UntrashJob(FilePathList srcPaths): + srcPaths_{std::move(srcPaths)} { } - void UntrashJob::exec() { -#if 0 - gboolean ret = TRUE; - GList* l; - GError* err = nullptr; - FmJob* fmjob = FM_JOB(job); - job->total = fm_path_list_get_length(job->srcs); - fm_file_ops_job_emit_prepared(job); - - l = fm_path_list_peek_head_link(job->srcs); - for(; !fm_job_is_cancelled(fmjob) && l; l = l->next) { - GFile* gf; - GFileInfo* inf; - FmPath* path = FM_PATH(l->data); - if(!fm_path_is_trash(path)) { - continue; + // preparing for the job + FilePathList validSrcPaths; + FilePathList origPaths; + for(auto& srcPath: srcPaths_) { + if(isCancelled()) { + break; } - gf = fm_path_to_gfile(path); -_retry_get_orig_path: - inf = g_file_query_info(gf, trash_query, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, fm_job_get_cancellable(fmjob), &err); - if(inf) { - const char* orig_path_str = g_file_info_get_attribute_byte_string(inf, "trash::orig-path"); - fm_file_ops_job_emit_cur_file(job, g_file_info_get_display_name(inf)); - + GErrorPtr err; + GFileInfoPtr srcInfo{ + g_file_query_info(srcPath.gfile().get(), + "trash::orig-path", + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), + &err), + false + }; + if(srcInfo) { + const char* orig_path_str = g_file_info_get_attribute_byte_string(srcInfo.get(), "trash::orig-path"); if(orig_path_str) { - /* FIXME: what if orig_path_str is a relative path? - * This is actually allowed by the horrible trash spec. */ - GFile* orig_path = fm_file_new_for_commandline_arg(orig_path_str); - FmFolder* src_folder = fm_folder_find_by_path(fm_path_get_parent(path)); - FmPath* orig_fm_path = fm_path_new_for_gfile(orig_path); - FmFolder* dst_folder = fm_folder_find_by_path(fm_path_get_parent(orig_fm_path)); - fm_path_unref(orig_fm_path); - /* ensure the existence of parent folder. */ - if(ensure_parent_dir(fmjob, orig_path)) { - ret = _fm_file_ops_job_move_file(job, gf, inf, orig_path, path, src_folder, dst_folder); - } - if(src_folder) { - g_object_unref(src_folder); - } - if(dst_folder) { - g_object_unref(dst_folder); - } - g_object_unref(orig_path); + validSrcPaths.emplace_back(srcPath); + origPaths.emplace_back(FilePath::fromPathStr(orig_path_str)); } else { ErrorAction act; - g_set_error(&err, G_IO_ERROR, G_IO_ERROR_FAILED, - _("Cannot untrash file '%s': original path not known"), - g_file_info_get_display_name(inf)); - act = emitError( err, ErrorSeverity::MODERATE); - g_clear_error(&err); - if(act == ErrorAction::ABORT) { - g_object_unref(inf); - g_object_unref(gf); - return FALSE; - } + tr("Cannot untrash file '%s': original path not known").toUtf8().constData(), + g_file_info_get_display_name(srcInfo.get())); + // FIXME: do we need to retry here? + emitError(err, ErrorSeverity::MODERATE); } - g_object_unref(inf); } else { - char* basename = g_file_get_basename(gf); - char* disp = basename ? g_filename_display_name(basename) : nullptr; - g_free(basename); - /* FIXME: translate it */ - fm_file_ops_job_emit_cur_file(job, disp ? disp : "(invalid file)"); - g_free(disp); + // FIXME: do we need to retry here? + emitError(err); + } + } + + // collected original paths of the trashed files + // use the file transfer job to handle the actual file move + FileTransferJob fileTransferJob{std::move(validSrcPaths), std::move(origPaths), FileTransferJob::Mode::MOVE}; + // FIXME: + // I'm not sure why specifying Qt::DirectConnection is needed here since the caller & receiver are in the same thread. :-( + // However without this, the signals/slots here will cause deadlocks. + connect(&fileTransferJob, &FileTransferJob::preparedToRun, this, &UntrashJob::preparedToRun, Qt::DirectConnection); + connect(&fileTransferJob, &FileTransferJob::error, this, &UntrashJob::error, Qt::DirectConnection); + connect(&fileTransferJob, &FileTransferJob::fileExists, this, &UntrashJob::fileExists, Qt::DirectConnection); - if(err) { - ErrorAction act = emitError( err, ErrorSeverity::MODERATE); - g_error_free(err); - err = nullptr; - if(act == ErrorAction::RETRY) { - goto _retry_get_orig_path; + // cancel the file transfer subjob if the parent job is cancelled + connect(this, &UntrashJob::cancelled, &fileTransferJob, + [&fileTransferJob]() { + if(!fileTransferJob.isCancelled()) { + fileTransferJob.cancel(); } - else if(act == ErrorAction::ABORT) { - g_object_unref(gf); - return FALSE; + }, Qt::DirectConnection); + + // cancel the parent job if the file transfer subjob is cancelled + connect(&fileTransferJob, &FileTransferJob::cancelled, this, + [this]() { + if(!isCancelled()) { + cancel(); } - } - } - g_object_unref(gf); - ++job->finished; - fm_file_ops_job_emit_percent(job); - } -#endif + }, Qt::DirectConnection); + fileTransferJob.run(); } } // namespace Fm diff --git a/src/core/untrashjob.h b/src/core/untrashjob.h index 1574abd..22a5129 100644 --- a/src/core/untrashjob.h +++ b/src/core/untrashjob.h @@ -6,15 +6,15 @@ namespace Fm { -class LIBFM_QT_API UntrashJob : public Fm::FileOperationJob { +class LIBFM_QT_API UntrashJob : public FileOperationJob { public: - explicit UntrashJob(); + explicit UntrashJob(FilePathList srcPaths); protected: void exec() override; private: - bool ensure_parent_dir(GFile *orig_path); + FilePathList srcPaths_; }; } // namespace Fm diff --git a/src/createnewmenu.cpp b/src/createnewmenu.cpp index 9191201..227982e 100644 --- a/src/createnewmenu.cpp +++ b/src/createnewmenu.cpp @@ -19,14 +19,45 @@ #include "createnewmenu.h" #include "folderview.h" -#include "icontheme.h" #include "utilities.h" #include "core/iconinfo.h" +#include "core/templates.h" + +#include namespace Fm { + +class TemplateAction: public QAction { +public: + TemplateAction(std::shared_ptr item, QObject* parent): + QAction(parent) { + setTemplateItem(std::move(item)); + } + + const std::shared_ptr templateItem() const { + return templateItem_; + } + + void setTemplateItem(std::shared_ptr item) { + templateItem_ = std::move(item); + auto mimeType = templateItem_->mimeType(); + setText(QString("%1 (%2)").arg(templateItem_->displayName()).arg(mimeType->desc())); + setIcon(templateItem_->icon()->qicon()); + } + +private: + std::shared_ptr templateItem_; +}; + + CreateNewMenu::CreateNewMenu(QWidget* dialogParent, Fm::FilePath dirPath, QWidget* parent): - QMenu(parent), dialogParent_(dialogParent), dirPath_(std::move(dirPath)) { + QMenu(parent), + dialogParent_(dialogParent), + dirPath_(std::move(dirPath)), + templateSeparator_{nullptr}, + templates_{Templates::globalInstance()} { + QAction* action = new QAction(QIcon::fromTheme("folder-new"), tr("Folder"), this); connect(action, &QAction::triggered, this, &CreateNewMenu::onCreateNewFolder); addAction(action); @@ -36,27 +67,12 @@ CreateNewMenu::CreateNewMenu(QWidget* dialogParent, Fm::FilePath dirPath, QWidge addAction(action); // add more items to "Create New" menu from templates - GList* templates = fm_template_list_all(fm_config->only_user_templates); - if(templates) { - addSeparator(); - for(GList* l = templates; l; l = l->next) { - FmTemplate* templ = (FmTemplate*)l->data; - /* we support directories differently */ - if(fm_template_is_directory(templ)) { - continue; - } - FmMimeType* mime_type = fm_template_get_mime_type(templ); - const char* label = fm_template_get_label(templ); - QString text = QString("%1 (%2)").arg(QString::fromUtf8(label)).arg(QString::fromUtf8(fm_mime_type_get_desc(mime_type))); - FmIcon* icon = fm_template_get_icon(templ); - if(!icon) { - icon = fm_mime_type_get_icon(mime_type); - } - QAction* action = addAction(Fm::IconInfo::fromGIcon(G_ICON(icon))->qicon(), text); - action->setObjectName(QString::fromUtf8(fm_template_get_name(templ, nullptr))); - connect(action, &QAction::triggered, this, &CreateNewMenu::onCreateNew); - } - } + connect(templates_.get(), &Templates::itemAdded, this, &CreateNewMenu::addTemplateItem); + connect(templates_.get(), &Templates::itemChanged, this, &CreateNewMenu::updateTemplateItem); + connect(templates_.get(), &Templates::itemRemoved, this, &CreateNewMenu::removeTemplateItem); + templates_->forEachItem([this](const std::shared_ptr& item) { + addTemplateItem(item); + }); } CreateNewMenu::~CreateNewMenu() { @@ -75,22 +91,62 @@ void CreateNewMenu::onCreateNewFolder() { } void CreateNewMenu::onCreateNew() { - QAction* action = static_cast(sender()); - QByteArray name = action->objectName().toUtf8(); - GList* templates = fm_template_list_all(fm_config->only_user_templates); - FmTemplate* templ = nullptr; - for(GList* l = templates; l; l = l->next) { - FmTemplate* t = (FmTemplate*)l->data; - if(name == fm_template_get_name(t, nullptr)) { - templ = t; + TemplateAction* action = static_cast(sender()); + if(dirPath_) { + createFileOrFolder(CreateWithTemplate, dirPath_, action->templateItem().get(), dialogParent_); + } +} + +void CreateNewMenu::addTemplateItem(const std::shared_ptr &item) { + if(!templateSeparator_) { + templateSeparator_= addSeparator(); + } + auto mimeType = item->mimeType(); + /* we support directories differently */ + if(mimeType->isDir()) { + return; + } + QAction* action = new TemplateAction{item, this}; + connect(action, &QAction::triggered, this, &CreateNewMenu::onCreateNew); + addAction(action); +} + +void CreateNewMenu::updateTemplateItem(const std::shared_ptr &oldItem, const std::shared_ptr &newItem) { + auto allActions = actions(); + auto separatorPos = allActions.indexOf(templateSeparator_); + // all items after the separator are templates + for(auto i = separatorPos + 1; i < allActions.size(); ++i) { + auto action = static_cast(allActions[i]); + if(action->templateItem() == oldItem) { + // update the menu item + action->setTemplateItem(newItem); break; } } - if(templ) { // template found - if(dirPath_) { - createFileOrFolder(CreateWithTemplate, dirPath_, templ, dialogParent_); +} + +void CreateNewMenu::removeTemplateItem(const std::shared_ptr &item) { + if(!templateSeparator_) { + return; + } + auto allActions = actions(); + auto separatorPos = allActions.indexOf(templateSeparator_); + // all items after the separator are templates + for(auto i = separatorPos + 1; i < allActions.size(); ++i) { + auto action = static_cast(allActions[i]); + if(action->templateItem() == item) { + // delete the action from the menu + removeAction(action); + allActions.removeAt(i); + break; } } + + // no more template items. remove the separator + if(separatorPos == allActions.size() - 1) { + removeAction(templateSeparator_); + templateSeparator_ = nullptr; + } } } // namespace Fm diff --git a/src/createnewmenu.h b/src/createnewmenu.h index d2e7282..e6b4d4f 100644 --- a/src/createnewmenu.h +++ b/src/createnewmenu.h @@ -1,4 +1,4 @@ -/* +/* * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) * * This library is free software; you can redistribute it and/or @@ -29,6 +29,8 @@ namespace Fm { class FolderView; +class Templates; +class TemplateItem; class LIBFM_QT_API CreateNewMenu : public QMenu { Q_OBJECT @@ -39,12 +41,23 @@ public: protected Q_SLOTS: void onCreateNewFolder(); + void onCreateNewFile(); + void onCreateNew(); +private Q_SLOTS: + void addTemplateItem(const std::shared_ptr& item); + + void updateTemplateItem(const std::shared_ptr& oldItem, const std::shared_ptr& newItem); + + void removeTemplateItem(const std::shared_ptr& item); + private: QWidget* dialogParent_; Fm::FilePath dirPath_; + QAction* templateSeparator_; + std::shared_ptr templates_; }; } diff --git a/src/customactions/fileactioncondition.cpp b/src/customactions/fileactioncondition.cpp index 16e545b..55ee2e4 100644 --- a/src/customactions/fileactioncondition.cpp +++ b/src/customactions/fileactioncondition.cpp @@ -373,12 +373,17 @@ bool FileActionCondition::match_folder(const FileInfoList& files, const char* fo pattern = g_pattern_spec_new(folder); } else { - auto pat_str = string(folder) + "/*"; + auto pat_str = g_str_has_suffix(folder, "/") ? string(folder) + "*" // be tolerant + : string(folder) + "/*"; pattern = g_pattern_spec_new(pat_str.c_str()); } for(auto& fi: files) { - auto dirname = fi->dirPath().toString(); - if(g_pattern_match_string(pattern, dirname.get())) { // at least 1 file is in the folder + auto dirname = fi->isDir() ? fi->path().toString() // also match "folder" itself + : fi->dirPath().toString(); + // Since the pattern ends with "/*", if the directory path is equal to "folder", + // it should end with "/" to be found as a match. Adding "/" is always harmless. + auto path_str = string(dirname.get()) + "/"; + if(g_pattern_match_string(pattern, path_str.c_str())) { // at least 1 file is in the folder if(negated) { return false; } diff --git a/src/customactions/fileactionprofile.cpp b/src/customactions/fileactionprofile.cpp index e356703..0150b1c 100644 --- a/src/customactions/fileactionprofile.cpp +++ b/src/customactions/fileactionprofile.cpp @@ -32,6 +32,9 @@ FileActionProfile::FileActionProfile(GKeyFile *kf, const char* profile_name) { exec_mode = FileActionExecMode::NORMAL; } } + else { + exec_mode = FileActionExecMode::NORMAL; + } startup_notify = g_key_file_get_boolean(kf, group_name.c_str(), "StartupNotify", nullptr); startup_wm_class = CStrPtr{g_key_file_get_string(kf, group_name.c_str(), "StartupWMClass", nullptr)}; diff --git a/src/dirtreemodel.cpp b/src/dirtreemodel.cpp index 2d87de9..c6d030f 100644 --- a/src/dirtreemodel.cpp +++ b/src/dirtreemodel.cpp @@ -167,7 +167,7 @@ QModelIndex DirTreeModel::indexFromPath(const Fm::FilePath &path) const { } DirTreeModelItem* DirTreeModel::itemFromPath(const Fm::FilePath &path) const { - Q_FOREACH(DirTreeModelItem* item, rootItems_) { + for(DirTreeModelItem* const item : qAsConst(rootItems_)) { if(item->fileInfo_ && path == item->fileInfo_->path()) { return item; } @@ -224,7 +224,7 @@ QString DirTreeModel::dispName(const QModelIndex& index) { void DirTreeModel::setShowHidden(bool show_hidden) { showHidden_ = show_hidden; - Q_FOREACH(DirTreeModelItem* item, rootItems_) { + for(DirTreeModelItem* const item : qAsConst(rootItems_)) { item->setShowHidden(show_hidden); } } diff --git a/src/dirtreemodelitem.cpp b/src/dirtreemodelitem.cpp index 52dc1dd..57ecf41 100644 --- a/src/dirtreemodelitem.cpp +++ b/src/dirtreemodelitem.cpp @@ -19,7 +19,6 @@ #include "dirtreemodelitem.h" #include "dirtreemodel.h" -#include "icontheme.h" #include namespace Fm { @@ -55,12 +54,12 @@ DirTreeModelItem::~DirTreeModelItem() { freeFolder(); // delete child items if needed if(!children_.empty()) { - Q_FOREACH(DirTreeModelItem* item, children_) { + for(DirTreeModelItem* const item : qAsConst(children_)) { delete item; } } if(!hiddenChildren_.empty()) { - Q_FOREACH(DirTreeModelItem* item, hiddenChildren_) { + for(DirTreeModelItem* const item : qAsConst(hiddenChildren_)) { delete item; } } @@ -124,7 +123,7 @@ void DirTreeModelItem::unloadFolder() { // delete all visible child items model_->beginRemoveRows(index(), 0, children_.size() - 1); if(!children_.empty()) { - Q_FOREACH(DirTreeModelItem* item, children_) { + for(DirTreeModelItem* const item : qAsConst(children_)) { delete item; } children_.clear(); @@ -133,7 +132,7 @@ void DirTreeModelItem::unloadFolder() { // remove hidden children if(!hiddenChildren_.empty()) { - Q_FOREACH(DirTreeModelItem* item, hiddenChildren_) { + for(DirTreeModelItem* const item : qAsConst(hiddenChildren_)) { delete item; } hiddenChildren_.clear(); @@ -343,7 +342,7 @@ DirTreeModelItem* DirTreeModelItem::childFromName(const char* utf8_name, int* po DirTreeModelItem* DirTreeModelItem::childFromPath(Fm::FilePath path, bool recursive) const { Q_ASSERT(path != nullptr); - Q_FOREACH(DirTreeModelItem* item, children_) { + for(DirTreeModelItem* const item : qAsConst(children_)) { // if(item->fileInfo_) // qDebug() << "child: " << QString::fromUtf8(fm_file_info_get_disp_name(item->fileInfo_)); if(item->fileInfo_ && item->fileInfo_->path() == path) { diff --git a/src/dirtreeview.cpp b/src/dirtreeview.cpp index a16c7df..7230559 100644 --- a/src/dirtreeview.cpp +++ b/src/dirtreeview.cpp @@ -306,7 +306,7 @@ void DirTreeView::rowsRemoved(const QModelIndex& parent, int start, int end) { void DirTreeView::doQueuedDeletions() { if(!queuedForDeletion_.empty()) { - Q_FOREACH(DirTreeModelItem* item, queuedForDeletion_) { + for(DirTreeModelItem* const item : qAsConst(queuedForDeletion_)) { delete item; } queuedForDeletion_.clear(); diff --git a/src/dnddest.cpp b/src/dnddest.cpp index be20b66..b0f35c9 100644 --- a/src/dnddest.cpp +++ b/src/dnddest.cpp @@ -51,6 +51,7 @@ bool DndDest::dropMimeData(const QMimeData* data, Qt::DropAction action) { break; case Qt::LinkAction: FileOperation::symlinkFiles(srcPaths, destPath_); + /* Falls through. */ default: return false; } diff --git a/src/editbookmarksdialog.cpp b/src/editbookmarksdialog.cpp index 36e0958..d9f70e4 100644 --- a/src/editbookmarksdialog.cpp +++ b/src/editbookmarksdialog.cpp @@ -102,8 +102,8 @@ void EditBookmarksDialog::onAddItem() { } void EditBookmarksDialog::onRemoveItem() { - QList sels = ui->treeWidget->selectedItems(); - Q_FOREACH(QTreeWidgetItem* item, sels) { + const QList sels = ui->treeWidget->selectedItems(); + for(QTreeWidgetItem* const item : sels) { delete item; } } diff --git a/src/execfiledialog.cpp b/src/execfiledialog.cpp index a1ec05f..b0b2780 100644 --- a/src/execfiledialog.cpp +++ b/src/execfiledialog.cpp @@ -19,31 +19,37 @@ #include "execfiledialog_p.h" #include "ui_exec-file.h" -#include "icontheme.h" #include "core/iconinfo.h" namespace Fm { -ExecFileDialog::ExecFileDialog(FmFileInfo* file, QWidget* parent, Qt::WindowFlags f): +ExecFileDialog::ExecFileDialog(const FileInfo &fileInfo, QWidget* parent, Qt::WindowFlags f): QDialog(parent, f), ui(new Ui::ExecFileDialog()), - fileInfo_(fm_file_info_ref(file)), - result_(FM_FILE_LAUNCHER_EXEC_CANCEL) { + result_(BasicFileLauncher::ExecAction::DIRECT_EXEC) { ui->setupUi(this); // show file icon - GIcon* gicon = G_ICON(fm_file_info_get_icon(fileInfo_)); - ui->icon->setPixmap(Fm::IconInfo::fromGIcon(gicon)->qicon().pixmap(QSize(48, 48))); + auto gicon = fileInfo.icon(); + if(gicon) { + ui->icon->setPixmap(gicon->qicon().pixmap(QSize(48, 48))); + } QString msg; - if(fm_file_info_is_text(file)) { + if(fileInfo.isDesktopEntry()) { + msg = tr("This file '%1' seems to be a desktop entry.\nWhat do you want to do with it?") + .arg(fileInfo.displayName()); + ui->exec->setDefault(true); + ui->execTerm->hide(); + } + else if(fileInfo.isText()) { msg = tr("This text file '%1' seems to be an executable script.\nWhat do you want to do with it?") - .arg(QString::fromUtf8(fm_file_info_get_disp_name(file))); + .arg(fileInfo.displayName()); ui->execTerm->setDefault(true); } else { msg = tr("This file '%1' is executable. Do you want to execute it?") - .arg(QString::fromUtf8(fm_file_info_get_disp_name(file))); + .arg(fileInfo.displayName()); ui->exec->setDefault(true); ui->open->hide(); } @@ -52,23 +58,28 @@ ExecFileDialog::ExecFileDialog(FmFileInfo* file, QWidget* parent, Qt::WindowFlag ExecFileDialog::~ExecFileDialog() { delete ui; - if(fileInfo_) { - fm_file_info_unref(fileInfo_); - } } void ExecFileDialog::accept() { QObject* _sender = sender(); if(_sender == ui->exec) { - result_ = FM_FILE_LAUNCHER_EXEC; + result_ = BasicFileLauncher::ExecAction::DIRECT_EXEC; } else if(_sender == ui->execTerm) { - result_ = FM_FILE_LAUNCHER_EXEC_IN_TERMINAL; + result_ = BasicFileLauncher::ExecAction::EXEC_IN_TERMINAL; } else if(_sender == ui->open) { - result_ = FM_FILE_LAUNCHER_EXEC_OPEN; + result_ = BasicFileLauncher::ExecAction::OPEN_WITH_DEFAULT_APP; + } + else { + result_ = BasicFileLauncher::ExecAction::CANCEL; } QDialog::accept(); } +void ExecFileDialog::reject() { + result_ = BasicFileLauncher::ExecAction::CANCEL; + QDialog::reject(); +} + } // namespace Fm diff --git a/src/execfiledialog_p.h b/src/execfiledialog_p.h index 10af394..8ceae1a 100644 --- a/src/execfiledialog_p.h +++ b/src/execfiledialog_p.h @@ -20,8 +20,12 @@ #ifndef FM_EXECFILEDIALOG_H #define FM_EXECFILEDIALOG_H +#include "core/basicfilelauncher.h" +#include "core/fileinfo.h" + #include -#include + +#include namespace Ui { class ExecFileDialog; @@ -33,19 +37,19 @@ class ExecFileDialog : public QDialog { Q_OBJECT public: ~ExecFileDialog(); - ExecFileDialog(FmFileInfo* fileInfo, QWidget* parent = 0, Qt::WindowFlags f = 0); + ExecFileDialog(const FileInfo& fileInfo, QWidget* parent = 0, Qt::WindowFlags f = 0); - FmFileLauncherExecAction result() { + BasicFileLauncher::ExecAction result() { return result_; } protected: - virtual void accept(); + virtual void accept() override; + virtual void reject() override; private: Ui::ExecFileDialog* ui; - FmFileInfo* fileInfo_; - FmFileLauncherExecAction result_; + BasicFileLauncher::ExecAction result_; }; } diff --git a/src/file-operation-dialog.ui b/src/file-operation-dialog.ui index f000afd..47f8c67 100644 --- a/src/file-operation-dialog.ui +++ b/src/file-operation-dialog.ui @@ -7,7 +7,7 @@ 0 0 450 - 246 + 297 @@ -34,7 +34,7 @@ - + 0 @@ -57,7 +57,7 @@ - + 0 @@ -122,12 +122,12 @@ - Data transferred: + Files processed: - + @@ -147,6 +147,13 @@ + + + Fm::ElidedLabel + QLabel +
fileoperationdialog_p.h
+
+
diff --git a/src/file-props.ui b/src/file-props.ui index 23dd661..9454fa0 100644 --- a/src/file-props.ui +++ b/src/file-props.ui @@ -96,7 +96,7 @@ true
- Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse @@ -119,7 +119,7 @@ - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse @@ -142,7 +142,7 @@ - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse @@ -165,7 +165,7 @@ - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse @@ -188,7 +188,7 @@ - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse @@ -211,7 +211,7 @@ - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse @@ -237,7 +237,7 @@ true - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse @@ -246,9 +246,6 @@ Open With: - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - @@ -274,7 +271,7 @@ - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse diff --git a/src/filedialog.cpp b/src/filedialog.cpp index 3d05650..0ef0753 100644 --- a/src/filedialog.cpp +++ b/src/filedialog.cpp @@ -346,41 +346,43 @@ void FileDialog::goHome() { } void FileDialog::setDirectoryPath(FilePath directory, FilePath selectedPath, bool addHistory) { - if(!directory.isValid() || directoryPath_ == directory) { + if(!directory.isValid()) { updateAcceptButtonState(); // FIXME: is this needed? return; } - if(folder_) { - if(folderModel_) { - proxyModel_->setSourceModel(nullptr); - folderModel_->unref(); // unref the cached model - folderModel_ = nullptr; - } - freeFolder(); + if(directoryPath_ != directory) { + if(folder_) { + if(folderModel_) { + proxyModel_->setSourceModel(nullptr); + folderModel_->unref(); // unref the cached model + folderModel_ = nullptr; + } + freeFolder(); + } + + directoryPath_ = std::move(directory); + + ui->location->setPath(directoryPath_); + ui->sidePane->chdir(directoryPath_); + if(addHistory) { + history_.add(directoryPath_); + } + backAction_->setEnabled(history_.canBackward()); + forwardAction_->setEnabled(history_.canForward()); + + folder_ = Fm::Folder::fromPath(directoryPath_); + folderModel_ = CachedFolderModel::modelFromFolder(folder_); + proxyModel_->setSourceModel(folderModel_); + + // no lambda in these connections for easy disconnection + connect(folder_.get(), &Fm::Folder::removed, this, &FileDialog::goHome); + connect(folder_.get(), &Fm::Folder::unmount, this, &FileDialog::goHome); + + QUrl uri = QUrl::fromEncoded(directory.uri().get()); + Q_EMIT directoryEntered(uri); } - directoryPath_ = std::move(directory); - - ui->location->setPath(directoryPath_); - ui->sidePane->chdir(directoryPath_); - if(addHistory) { - history_.add(directoryPath_); - } - backAction_->setEnabled(history_.canBackward()); - forwardAction_->setEnabled(history_.canForward()); - - folder_ = Fm::Folder::fromPath(directoryPath_); - folderModel_ = CachedFolderModel::modelFromFolder(folder_); - proxyModel_->setSourceModel(folderModel_); - - // no lambda in these connections for easy disconnection - connect(folder_.get(), &Fm::Folder::removed, this, &FileDialog::goHome); - connect(folder_.get(), &Fm::Folder::unmount, this, &FileDialog::goHome); - - QUrl uri = QUrl::fromEncoded(directory.uri().get()); - Q_EMIT directoryEntered(uri); - // select the path if valid if(selectedPath.isValid()) { if(folder_->isLoaded()) { @@ -415,13 +417,13 @@ void FileDialog::selectFilePath(const FilePath &path) { QItemSelectionModel* selModel = ui->folderView->selectionModel(); selModel->select(idx, flags); selModel->setCurrentIndex(idx, QItemSelectionModel::Current); - QTimer::singleShot(0, [this, idx]() { + QTimer::singleShot(0, this, [this, idx]() { ui->folderView->childView()->scrollTo(idx, QAbstractItemView::PositionAtCenter); }); } void FileDialog::selectFilePathWithDelay(const FilePath &path) { - QTimer::singleShot(0, [this, path]() { + QTimer::singleShot(0, this, [this, path]() { if(acceptMode_ == QFileDialog::AcceptSave) { // with a save dialog, always put the base name in line-edit, regardless of selection ui->fileName->setText(path.baseName().get()); @@ -436,7 +438,7 @@ void FileDialog::selectFilePathWithDelay(const FilePath &path) { void FileDialog::selectFilesOnReload(const Fm::FileInfoList& infos) { QObject::disconnect(lambdaConnection_); - QTimer::singleShot(0, [this, infos]() { + QTimer::singleShot(0, this, [this, infos]() { for(auto& fileInfo: infos) { selectFilePath(fileInfo->path()); } @@ -635,13 +637,8 @@ void FileDialog::selectFile(const QUrl& filename) { auto urlStr = filename.toEncoded(); auto path = FilePath::fromUri(urlStr.constData()); auto parent = path.parent(); - if(parent.isValid() && parent != directoryPath_) { - // chdir into file's parent if it isn't the current directory - setDirectoryPath(parent, path); - } - else { - selectFilePathWithDelay(path); - } + // chdir into file's parent if needed and select the file + setDirectoryPath(parent, path); } QList FileDialog::selectedFiles() { @@ -770,7 +767,8 @@ void FileDialog::setMimeTypeFilters(const QStringList& filters) { auto nameFilter = mimeType.comment(); if(!mimeType.suffixes().empty()) { nameFilter + " ("; - for(const auto& suffix: mimeType.suffixes()) { + const auto suffixes = mimeType.suffixes(); + for(const auto& suffix: suffixes) { nameFilter += "*."; nameFilter += suffix; nameFilter += ' '; diff --git a/src/filelauncher.cpp b/src/filelauncher.cpp index 6131c01..5f667fc 100644 --- a/src/filelauncher.cpp +++ b/src/filelauncher.cpp @@ -26,111 +26,73 @@ #include "appchooserdialog.h" #include "utilities.h" +#include "core/fileinfojob.h" +#include "mountoperation.h" + namespace Fm { -FmFileLauncher FileLauncher::funcs = { - FileLauncher::_getApp, - /* gboolean (*before_open)(GAppLaunchContext* ctx, GList* folder_infos, gpointer user_data); */ - (FmLaunchFolderFunc)FileLauncher::_openFolder, - FileLauncher::_execFile, - FileLauncher::_error, - FileLauncher::_ask, - nullptr -}; - -FileLauncher::FileLauncher(): - quickExec_(false) { +FileLauncher::FileLauncher() { } FileLauncher::~FileLauncher() { } -bool FileLauncher::launchFiles(QWidget *parent, Fm::FileInfoList file_infos) { - // FIXME: rewrite - return launchPaths(parent, file_infos.paths()); -} -bool FileLauncher::launchPaths(QWidget *parent, Fm::FilePathList paths) { - // FIXME: rewrite, port to new api - GList* tmp = nullptr; - for(auto& path: paths) { - auto fmpath = fm_path_new_for_gfile(path.gfile().get()); - tmp = g_list_prepend(tmp, fmpath); - } - tmp = g_list_reverse(tmp); - bool ret = launchPaths(parent, tmp); - g_list_free(tmp); +bool FileLauncher::launchFiles(QWidget* parent, const FileInfoList &file_infos) { + GObjectPtr context{fm_app_launch_context_new_for_widget(parent), false}; + bool ret = BasicFileLauncher::launchFiles(file_infos, G_APP_LAUNCH_CONTEXT(context.get())); return ret; } -bool FileLauncher::launchFiles(QWidget* parent, GList* file_infos) { - FmAppLaunchContext* context = fm_app_launch_context_new_for_widget(parent); - bool ret = fm_launch_files(G_APP_LAUNCH_CONTEXT(context), file_infos, &funcs, this); - g_object_unref(context); +bool FileLauncher::launchPaths(QWidget* parent, const FilePathList& paths) { + GObjectPtr context{fm_app_launch_context_new_for_widget(parent), false}; + bool ret = BasicFileLauncher::launchPaths(paths, G_APP_LAUNCH_CONTEXT(context.get())); return ret; } -bool FileLauncher::launchPaths(QWidget* parent, GList* paths) { - FmAppLaunchContext* context = fm_app_launch_context_new_for_widget(parent); - bool ret = fm_launch_paths(G_APP_LAUNCH_CONTEXT(context), paths, &funcs, this); - g_object_unref(context); - return ret; +int FileLauncher::ask(const char* /*msg*/, char* const* /*btn_labels*/, int /*default_btn*/) { + /* FIXME: set default button properly */ + // return fm_askv(data->parent, nullptr, msg, btn_labels); + return -1; } -GAppInfo* FileLauncher::getApp(GList* /*file_infos*/, FmMimeType* mime_type, GError** /*err*/) { +GAppInfoPtr FileLauncher::chooseApp(const FileInfoList& /*fileInfos*/, const char *mimeType, GErrorPtr& /*err*/) { AppChooserDialog dlg(nullptr); - if(mime_type) { - dlg.setMimeType(Fm::MimeType::fromName(fm_mime_type_get_type(mime_type))); + GAppInfoPtr app; + if(mimeType) { + dlg.setMimeType(Fm::MimeType::fromName(mimeType)); } else { dlg.setCanSetDefault(false); } // FIXME: show error properly? if(execModelessDialog(&dlg) == QDialog::Accepted) { - auto app = dlg.selectedApp(); - return app.release(); + app = dlg.selectedApp(); } - return nullptr; + return app; } -bool FileLauncher::openFolder(GAppLaunchContext* /*ctx*/, GList* folder_infos, GError** /*err*/) { - for(GList* l = folder_infos; l; l = l->next) { - FmFileInfo* fi = FM_FILE_INFO(l->data); - qDebug() << " folder:" << QString::fromUtf8(fm_file_info_get_disp_name(fi)); - } - return false; +bool FileLauncher::openFolder(GAppLaunchContext *ctx, const FileInfoList &folderInfos, GErrorPtr &err) { + return BasicFileLauncher::openFolder(ctx, folderInfos, err); } -FmFileLauncherExecAction FileLauncher::execFile(FmFileInfo* file) { - if(quickExec_) { - /* SF bug#838: open terminal for each script may be just a waste. - User should open a terminal and start the script there - in case if user wants to see the script output anyway. - if (fm_file_info_is_text(file)) - return FM_FILE_LAUNCHER_EXEC_IN_TERMINAL; */ - return FM_FILE_LAUNCHER_EXEC; - } - - FmFileLauncherExecAction res = FM_FILE_LAUNCHER_EXEC_CANCEL; - ExecFileDialog dlg(file); - if(execModelessDialog(&dlg) == QDialog::Accepted) { - res = dlg.result(); - } - return res; -} - -int FileLauncher::ask(const char* /*msg*/, char* const* /*btn_labels*/, int /*default_btn*/) { - /* FIXME: set default button properly */ - // return fm_askv(data->parent, nullptr, msg, btn_labels); - return -1; -} - -bool FileLauncher::error(GAppLaunchContext* /*ctx*/, GError* err, FmPath* path) { +bool FileLauncher::showError(GAppLaunchContext* /*ctx*/, GErrorPtr &err, const FilePath &path, const FileInfoPtr &info) { /* ask for mount if trying to launch unmounted path */ if(err->domain == G_IO_ERROR) { if(path && err->code == G_IO_ERROR_NOT_MOUNTED) { - //if(fm_mount_path(data->parent, path, TRUE)) - // return FALSE; /* ask to retry */ + MountOperation* op = new MountOperation(true); + op->setAutoDestroy(true); + if(info && info->isMountable()) { + // this is a mountable shortcut (such as computer:///xxxx.drive) + op->mountMountable(path); + } + else { + op->mountEnclosingVolume(path); + } + if(op->wait()) { + // if the mount operation succeeds, we can ignore the error and continue + return true; + } } else if(err->code == G_IO_ERROR_FAILED_HANDLED) { return true; /* don't show error message */ @@ -138,7 +100,16 @@ bool FileLauncher::error(GAppLaunchContext* /*ctx*/, GError* err, FmPath* path) } QMessageBox dlg(QMessageBox::Critical, QObject::tr("Error"), QString::fromUtf8(err->message), QMessageBox::Ok); execModelessDialog(&dlg); - return true; + return false; +} + +BasicFileLauncher::ExecAction FileLauncher::askExecFile(const FileInfoPtr &file) { + auto res = BasicFileLauncher::ExecAction::CANCEL; + ExecFileDialog dlg(*file); + if(execModelessDialog(&dlg) == QDialog::Accepted) { + res = dlg.result(); + } + return res; } diff --git a/src/filelauncher.h b/src/filelauncher.h index 9ebcfe6..be5be5a 100644 --- a/src/filelauncher.h +++ b/src/filelauncher.h @@ -23,60 +23,31 @@ #include "libfmqtglobals.h" #include -#include #include "core/fileinfo.h" +#include "core/basicfilelauncher.h" namespace Fm { -class LIBFM_QT_API FileLauncher { +class LIBFM_QT_API FileLauncher: public BasicFileLauncher { public: explicit FileLauncher(); virtual ~FileLauncher(); - bool launchFiles(QWidget* parent, Fm::FileInfoList file_infos); + bool launchFiles(QWidget* parent, const FileInfoList& file_infos); - bool launchPaths(QWidget* parent, Fm::FilePathList paths); - - bool quickExec() const { - return quickExec_; - } - - void setQuickExec(bool value) { - quickExec_ = value; - } + bool launchPaths(QWidget* parent, const FilePathList &paths); protected: - virtual GAppInfo* getApp(GList* file_infos, FmMimeType* mime_type, GError** err); - virtual bool openFolder(GAppLaunchContext* ctx, GList* folder_infos, GError** err); - virtual FmFileLauncherExecAction execFile(FmFileInfo* file); - virtual bool error(GAppLaunchContext* ctx, GError* err, FmPath* path); - virtual int ask(const char* msg, char* const* btn_labels, int default_btn); + GAppInfoPtr chooseApp(const FileInfoList& fileInfos, const char* mimeType, GErrorPtr& err) override; -private: - bool launchFiles(QWidget* parent, GList* file_infos); + bool openFolder(GAppLaunchContext* ctx, const FileInfoList& folderInfos, GErrorPtr& err) override; - bool launchPaths(QWidget* parent, GList* paths); + bool showError(GAppLaunchContext* ctx, GErrorPtr& err, const FilePath& path = FilePath{}, const FileInfoPtr& info = FileInfoPtr{}) override; - static GAppInfo* _getApp(GList* file_infos, FmMimeType* mime_type, gpointer user_data, GError** err) { - return reinterpret_cast(user_data)->getApp(file_infos, mime_type, err); - } - static gboolean _openFolder(GAppLaunchContext* ctx, GList* folder_infos, gpointer user_data, GError** err) { - return reinterpret_cast(user_data)->openFolder(ctx, folder_infos, err); - } - static FmFileLauncherExecAction _execFile(FmFileInfo* file, gpointer user_data) { - return reinterpret_cast(user_data)->execFile(file); - } - static gboolean _error(GAppLaunchContext* ctx, GError* err, FmPath* file, gpointer user_data) { - return reinterpret_cast(user_data)->error(ctx, err, file); - } - static int _ask(const char* msg, char* const* btn_labels, int default_btn, gpointer user_data) { - return reinterpret_cast(user_data)->ask(msg, btn_labels, default_btn); - } + ExecAction askExecFile(const FileInfoPtr& file) override; -private: - static FmFileLauncher funcs; - bool quickExec_; // Don't ask options on launch executable file + int ask(const char* msg, char* const* btn_labels, int default_btn) override; }; } diff --git a/src/filemenu.cpp b/src/filemenu.cpp index e3942a3..97b0757 100644 --- a/src/filemenu.cpp +++ b/src/filemenu.cpp @@ -20,7 +20,6 @@ #include "filemenu.h" #include "createnewmenu.h" -#include "icontheme.h" #include "filepropsdialog.h" #include "utilities.h" #include "fileoperation.h" @@ -35,7 +34,7 @@ #include #include "filemenu_p.h" -#include "core/compat_p.h" +#include "core/archiver.h" namespace Fm { @@ -207,18 +206,17 @@ FileMenu::FileMenu(Fm::FileInfoList files, std::shared_ptr i // FIXME: we need to modify upstream libfm to include some Qt-based archiver programs. if(!allVirtual_) { if(sameType_) { - // FIXME: port these parts to Fm API - FmArchiver* archiver = fm_archiver_get_default(); + auto archiver = Archiver::defaultArchiver(); if(archiver) { - if(fm_archiver_is_mime_type_supported(archiver, mime_type->name())) { + if(archiver->isMimeTypeSupported(mime_type->name())) { QAction* archiverSeparator = nullptr; - if(cwd_ && archiver->extract_to_cmd) { + if(cwd_ && archiver->canExtractArchivesTo()) { archiverSeparator = addSeparator(); QAction* action = new QAction(tr("Extract to..."), this); connect(action, &QAction::triggered, this, &FileMenu::onExtract); addAction(action); } - if(archiver->extract_cmd) { + if(archiver->canExtractArchives()) { if(!archiverSeparator) { addSeparator(); } @@ -227,7 +225,7 @@ FileMenu::FileMenu(Fm::FileInfoList files, std::shared_ptr i addAction(action); } } - else { + else if(archiver->canCreateArchive()){ addSeparator(); QAction* action = new QAction(tr("Compress"), this); connect(action, &QAction::triggered, this, &FileMenu::onCompress); @@ -376,7 +374,9 @@ void FileMenu::onRenameTriggered() { } } for(auto& info: files_) { - Fm::renameFile(info, nullptr); + if(!Fm::renameFile(info, nullptr)) { + break; + } } } @@ -391,27 +391,23 @@ void FileMenu::setUseTrash(bool trash) { } void FileMenu::onCompress() { - FmArchiver* archiver = fm_archiver_get_default(); + auto archiver = Archiver::defaultArchiver(); if(archiver) { - auto paths = Fm::_convertPathList(files_.paths()); - fm_archiver_create_archive(archiver, nullptr, paths.dataPtr()); + archiver->createArchive(nullptr, files_.paths()); } } void FileMenu::onExtract() { - FmArchiver* archiver = fm_archiver_get_default(); + auto archiver = Archiver::defaultArchiver(); if(archiver) { - auto paths = Fm::_convertPathList(files_.paths()); - fm_archiver_extract_archives(archiver, nullptr, paths.dataPtr()); + archiver->extractArchives(nullptr, files_.paths()); } } void FileMenu::onExtractHere() { - FmArchiver* archiver = fm_archiver_get_default(); + auto archiver = Archiver::defaultArchiver(); if(archiver) { - auto paths = Fm::_convertPathList(files_.paths()); - auto cwd = Fm::_convertPath(cwd_); - fm_archiver_extract_archives_to(archiver, nullptr, paths.dataPtr(), cwd); + archiver->extractArchivesTo(nullptr, files_.paths(), cwd_); } } diff --git a/src/filemenu_p.h b/src/filemenu_p.h index c257dc0..9cbd6ee 100644 --- a/src/filemenu_p.h +++ b/src/filemenu_p.h @@ -20,7 +20,6 @@ #ifndef FM_FILEMENU_P_H #define FM_FILEMENU_P_H -#include "icontheme.h" #include #include "core/gioptrs.h" #include "core/iconinfo.h" diff --git a/src/fileoperation.cpp b/src/fileoperation.cpp index af0ad7c..1e5d582 100644 --- a/src/fileoperation.cpp +++ b/src/fileoperation.cpp @@ -24,87 +24,207 @@ #include #include #include -#include "path.h" -#include "core/compat_p.h" +#include "core/deletejob.h" +#include "core/trashjob.h" +#include "core/untrashjob.h" +#include "core/filetransferjob.h" +#include "core/filechangeattrjob.h" +#include "utilities.h" namespace Fm { #define SHOW_DLG_DELAY 1000 -FileOperation::FileOperation(Type type, Fm::FilePathList srcFiles, QObject* parent): +FileOperation::FileOperation(Type type, Fm::FilePathList srcPaths, QObject* parent): QObject(parent), - job_{fm_file_ops_job_new((FmFileOpType)type, Fm::_convertPathList(srcFiles))}, - dlg{nullptr}, - srcPaths{std::move(srcFiles)}, - uiTimer(nullptr), + type_{type}, + job_{nullptr}, + dlg_{nullptr}, + srcPaths_{std::move(srcPaths)}, + uiTimer_(nullptr), elapsedTimer_(nullptr), lastElapsed_(0), updateRemainingTime_(true), autoDestroy_(true) { - g_signal_connect(job_, "ask", G_CALLBACK(onFileOpsJobAsk), this); - g_signal_connect(job_, "ask-rename", G_CALLBACK(onFileOpsJobAskRename), this); - g_signal_connect(job_, "error", G_CALLBACK(onFileOpsJobError), this); - g_signal_connect(job_, "prepared", G_CALLBACK(onFileOpsJobPrepared), this); - g_signal_connect(job_, "cur-file", G_CALLBACK(onFileOpsJobCurFile), this); - g_signal_connect(job_, "percent", G_CALLBACK(onFileOpsJobPercent), this); - g_signal_connect(job_, "finished", G_CALLBACK(onFileOpsJobFinished), this); - g_signal_connect(job_, "cancelled", G_CALLBACK(onFileOpsJobCancelled), this); + switch(type_) { + case Copy: + job_ = new FileTransferJob(srcPaths_, FileTransferJob::Mode::COPY); + break; + case Move: + job_ = new FileTransferJob(srcPaths_, FileTransferJob::Mode::MOVE); + break; + case Link: + job_ = new FileTransferJob(srcPaths_, FileTransferJob::Mode::LINK); + break; + case Delete: + job_ = new Fm::DeleteJob(srcPaths_); + break; + case Trash: + job_ = new Fm::TrashJob(srcPaths_); + break; + case UnTrash: + job_ = new Fm::UntrashJob(srcPaths_); + break; + case ChangeAttr: + job_ = new Fm::FileChangeAttrJob(srcPaths_); + break; + default: + break; + } + + if(job_) { + // automatically delete the job object when it's finished. + job_->setAutoDelete(true); + + // new C++ jobs + connect(job_, &Fm::Job::finished, this, &Fm::FileOperation::onJobFinish); + connect(job_, &Fm::Job::cancelled, this, &Fm::FileOperation::onJobCancalled); + connect(job_, &Fm::Job::error, this, &Fm::FileOperation::onJobError, Qt::BlockingQueuedConnection); + connect(job_, &Fm::FileOperationJob::fileExists, this, &Fm::FileOperation::onJobFileExists, Qt::BlockingQueuedConnection); + + // we block the job deliberately until we prepare to start (initiailize the timer) so we can calculate elapsed time correctly. + connect(job_, &Fm::FileOperationJob::preparedToRun, this, &Fm::FileOperation::onJobPrepared, Qt::BlockingQueuedConnection); + } } void FileOperation::disconnectJob() { - g_signal_handlers_disconnect_by_func(job_, (gpointer)G_CALLBACK(onFileOpsJobAsk), this); - g_signal_handlers_disconnect_by_func(job_, (gpointer)G_CALLBACK(onFileOpsJobAskRename), this); - g_signal_handlers_disconnect_by_func(job_, (gpointer)G_CALLBACK(onFileOpsJobError), this); - g_signal_handlers_disconnect_by_func(job_, (gpointer)G_CALLBACK(onFileOpsJobPrepared), this); - g_signal_handlers_disconnect_by_func(job_, (gpointer)G_CALLBACK(onFileOpsJobCurFile), this); - g_signal_handlers_disconnect_by_func(job_, (gpointer)G_CALLBACK(onFileOpsJobPercent), this); - g_signal_handlers_disconnect_by_func(job_, (gpointer)G_CALLBACK(onFileOpsJobFinished), this); - g_signal_handlers_disconnect_by_func(job_, (gpointer)G_CALLBACK(onFileOpsJobCancelled), this); + if(job_) { + disconnect(job_, &Fm::Job::finished, this, &Fm::FileOperation::onJobFinish); + disconnect(job_, &Fm::Job::cancelled, this, &Fm::FileOperation::onJobCancalled); + disconnect(job_, &Fm::Job::error, this, &Fm::FileOperation::onJobError); + disconnect(job_, &Fm::FileOperationJob::fileExists, this, &Fm::FileOperation::onJobFileExists); + disconnect(job_, &Fm::FileOperationJob::preparedToRun, this, &Fm::FileOperation::onJobPrepared); + } } FileOperation::~FileOperation() { - if(uiTimer) { - uiTimer->stop(); - delete uiTimer; - uiTimer = nullptr; + if(uiTimer_) { + uiTimer_->stop(); + delete uiTimer_; + uiTimer_ = nullptr; } if(elapsedTimer_) { delete elapsedTimer_; elapsedTimer_ = nullptr; } +} +void FileOperation::setDestination(Fm::FilePath dest) { + destPath_ = std::move(dest); + switch(type_) { + case Copy: + case Move: + case Link: + if(job_) { + static_cast(job_)->setDestDirPath(destPath_); + } + break; + default: + break; + } +} + +void FileOperation::setDestFiles(FilePathList destFiles) { + switch(type_) { + case Copy: + case Move: + case Link: + if(job_) { + static_cast(job_)->setDestPaths(std::move(destFiles)); + } + break; + default: + break; + } +} + +void FileOperation::setChmod(mode_t newMode, mode_t newModeMask) { if(job_) { - disconnectJob(); - g_object_unref(job_); + auto job = static_cast(job_); + job->setFileModeEnabled(true); + job->setFileMode(newMode, newModeMask); } } -void FileOperation::setDestination(Fm::FilePath dest) { - destPath = std::move(dest); - auto tmp = Fm::Path::newForGfile(dest.gfile().get()); - fm_file_ops_job_set_dest(job_, tmp.dataPtr()); +void FileOperation::setChown(uid_t uid, gid_t gid) { + if(job_) { + auto job = static_cast(job_); + if(uid != INVALID_UID) { + job->setOwnerEnabled(true); + job->setOwner(uid); + } + if(gid != INVALID_GID) { + job->setGroupEnabled(true); + job->setGroup(gid); + } + } +} + +void FileOperation::setRecursiveChattr(bool recursive) { + if(job_) { + auto job = static_cast(job_); + job->setRecursive(recursive); + } } bool FileOperation::run() { - delete uiTimer; + delete uiTimer_; // run the job - uiTimer = new QTimer(); - uiTimer->start(SHOW_DLG_DELAY); - connect(uiTimer, &QTimer::timeout, this, &FileOperation::onUiTimeout); + uiTimer_ = new QTimer(); + uiTimer_->start(SHOW_DLG_DELAY); + connect(uiTimer_, &QTimer::timeout, this, &FileOperation::onUiTimeout); + + if(job_) { + job_->runAsync(); + return true; + } + return false; +} - return fm_job_run_async(FM_JOB(job_)); +void FileOperation::cancel() { + if(job_) { + job_->cancel(); + } } void FileOperation::onUiTimeout() { - if(dlg) { - dlg->setCurFile(curFile); + if(dlg_) { // estimate remaining time based on past history - // FIXME: avoid directly access data member of FmFileOpsJob - if(Q_LIKELY(job_->percent > 0 && updateRemainingTime_)) { - gint64 remaining = elapsedTime() * ((double(100 - job_->percent) / job_->percent) / 1000); - dlg->setRemainingTime(remaining); + if(job_) { + Fm::FilePath curFilePath = job_->currentFile(); + // update progress bar + double finishedRatio = job_->progress(); + if(finishedRatio > 0.0 && updateRemainingTime_) { + dlg_->setPercent(int(finishedRatio * 100)); + + std::uint64_t totalSize, totalCount, finishedSize, finishedCount; + job_->totalAmount(totalSize, totalCount); + job_->finishedAmount(finishedSize, finishedCount); + + // only show data transferred if the job progress can be calculated by file size. + // for jobs not related to data transfer (for example: change attr, delete,...), hide the UI + if(job_->calcProgressUsingSize()) { + dlg_->setDataTransferred(finishedSize, totalSize); + } + else { + dlg_->setFilesProcessed(finishedCount, totalCount); + } + + double remainRatio = 1.0 - finishedRatio; + gint64 remaining = elapsedTime() * (remainRatio / finishedRatio) / 1000; + // qDebug("etime: %llu, finished: %lf, remain:%lf, remaining secs: %llu", + // elapsedTime(), finishedRatio, remainRatio, remaining); + dlg_->setRemainingTime(remaining); + } + // update currently processed file + if(curFilePath_ != curFilePath) { + curFilePath_ = std::move(curFilePath); + // FIXME: make this cleaner + curFile = QString::fromUtf8(curFilePath_.toString().get()); + dlg_->setCurFile(curFile); + } } // this timeout slot is called every 0.5 second. // by adding this flag, we can update remaining time every 1 second. @@ -116,125 +236,84 @@ void FileOperation::onUiTimeout() { } void FileOperation::showDialog() { - if(!dlg) { - dlg = new FileOperationDialog(this); - dlg->setSourceFiles(srcPaths); + if(!dlg_) { + dlg_ = new FileOperationDialog(this); + dlg_->setSourceFiles(srcPaths_); - if(destPath) { - dlg->setDestPath(destPath); + if(destPath_) { + dlg_->setDestPath(destPath_); } if(curFile.isEmpty()) { - dlg->setPrepared(); - dlg->setCurFile(curFile); + dlg_->setPrepared(); + dlg_->setCurFile(curFile); } - uiTimer->setInterval(500); // change the interval of the timer + uiTimer_->setInterval(500); // change the interval of the timer // now the timer is used to update current file display - dlg->show(); + dlg_->show(); } } -gint FileOperation::onFileOpsJobAsk(FmFileOpsJob* /*job*/, const char* question, char* const* options, FileOperation* pThis) { - pThis->pauseElapsedTimer(); - pThis->showDialog(); - int ret = pThis->dlg->ask(QString::fromUtf8(question), options); - pThis->resumeElapsedTimer(); - return ret; -} - -gint FileOperation::onFileOpsJobAskRename(FmFileOpsJob* /*job*/, FmFileInfo* src, FmFileInfo* dest, char** new_name, FileOperation* pThis) { - pThis->pauseElapsedTimer(); - pThis->showDialog(); - QString newName; - int ret = pThis->dlg->askRename(src, dest, newName); - if(!newName.isEmpty()) { - *new_name = g_strdup(newName.toUtf8().constData()); - } - pThis->resumeElapsedTimer(); - return ret; +void FileOperation::onJobFileExists(const FileInfo& src, const FileInfo& dest, Fm::FileOperationJob::FileExistsAction& response, FilePath& newDest) { + pauseElapsedTimer(); + showDialog(); + response = dlg_->askRename(src, dest, newDest); + resumeElapsedTimer(); } -void FileOperation::onFileOpsJobCancelled(FmFileOpsJob* /*job*/, FileOperation* /*pThis*/) { +void FileOperation::onJobCancalled() { qDebug("file operation is cancelled!"); } -void FileOperation::onFileOpsJobCurFile(FmFileOpsJob* /*job*/, const char* cur_file, FileOperation* pThis) { - pThis->curFile = QString::fromUtf8(cur_file); - - // We update the current file name in a timeout slot because drawing a string - // in the UI is expansive. Updating the label text too often cause - // significant impact on performance. - // if(pThis->dlg) - // pThis->dlg->setCurFile(pThis->curFile); -} - -FmJobErrorAction FileOperation::onFileOpsJobError(FmFileOpsJob* /*job*/, GError* err, FmJobErrorSeverity severity, FileOperation* pThis) { - pThis->pauseElapsedTimer(); - pThis->showDialog(); - FmJobErrorAction act = pThis->dlg->error(err, severity); - pThis->resumeElapsedTimer(); - return act; +void FileOperation::onJobError(const GErrorPtr& err, Fm::Job::ErrorSeverity severity, Fm::Job::ErrorAction& response) { + pauseElapsedTimer(); + showDialog(); + response = Fm::Job::ErrorAction(dlg_->error(err.get(), severity)); + resumeElapsedTimer(); } -void FileOperation::onFileOpsJobFinished(FmFileOpsJob* /*job*/, FileOperation* pThis) { - pThis->handleFinish(); -} - -void FileOperation::onFileOpsJobPercent(FmFileOpsJob* job, guint percent, FileOperation* pThis) { - if(pThis->dlg) { - pThis->dlg->setPercent(percent); - pThis->dlg->setDataTransferred(job->finished, job->total); - } -} - -void FileOperation::onFileOpsJobPrepared(FmFileOpsJob* /*job*/, FileOperation* pThis) { - if(!pThis->elapsedTimer_) { - pThis->elapsedTimer_ = new QElapsedTimer(); - pThis->elapsedTimer_->start(); +void FileOperation::onJobPrepared() { + if(!elapsedTimer_) { + elapsedTimer_ = new QElapsedTimer(); + elapsedTimer_->start(); } - if(pThis->dlg) { - pThis->dlg->setPrepared(); + if(dlg_) { + dlg_->setPrepared(); } } -void FileOperation::handleFinish() { +void FileOperation::onJobFinish() { disconnectJob(); - if(uiTimer) { - uiTimer->stop(); - delete uiTimer; - uiTimer = nullptr; + if(uiTimer_) { + uiTimer_->stop(); + delete uiTimer_; + uiTimer_ = nullptr; } - if(dlg) { - dlg->done(QDialog::Accepted); - delete dlg; - dlg = nullptr; + if(dlg_) { + dlg_->done(QDialog::Accepted); + delete dlg_; + dlg_ = nullptr; } Q_EMIT finished(); - /* sepcial handling for trash - * FIXME: need to refactor this to use a more elegant way later. */ - if(job_->type == FM_FILE_OP_TRASH) { /* FIXME: direct access to job struct! */ - auto unable_to_trash = static_cast(g_object_get_data(G_OBJECT(job_), "trash-unsupported")); + // special handling for trash job + if(type_ == Trash && !job_->isCancelled()) { + auto trashJob = static_cast(job_); /* some files cannot be trashed because underlying filesystems don't support it. */ - if(unable_to_trash) { /* delete them instead */ - Fm::FilePathList filesToDel; - for(GList* l = fm_path_list_peek_head_link(unable_to_trash); l; l = l->next) { - filesToDel.push_back(Fm::FilePath{fm_path_to_gfile(FM_PATH(l->data)), false}); - } + auto unsupportedFiles = trashJob->unsupportedFiles(); + if(!unsupportedFiles.empty()) { /* delete them instead */ /* FIXME: parent window might be already destroyed! */ QWidget* parent = nullptr; // FIXME: currently, parent window is not set if(QMessageBox::question(parent, tr("Error"), tr("Some files cannot be moved to trash can because " "the underlying file systems don't support this operation.\n" "Do you want to delete them instead?")) == QMessageBox::Yes) { - deleteFiles(std::move(filesToDel), false); + deleteFiles(std::move(unsupportedFiles), false); } } } - g_object_unref(job_); - job_ = nullptr; if(autoDestroy_) { delete this; @@ -249,6 +328,15 @@ FileOperation* FileOperation::copyFiles(Fm::FilePathList srcFiles, Fm::FilePath return op; } +//static +FileOperation *FileOperation::copyFiles(FilePathList srcFiles, FilePathList destFiles, QWidget *parent) { + qDebug("copy: %s -> %s", srcFiles[0].toString().get(), destFiles[0].toString().get()); + FileOperation* op = new FileOperation(FileOperation::Copy, std::move(srcFiles), parent); + op->setDestFiles(std::move(destFiles)); + op->run(); + return op; +} + // static FileOperation* FileOperation::moveFiles(Fm::FilePathList srcFiles, Fm::FilePath dest, QWidget* parent) { FileOperation* op = new FileOperation(FileOperation::Move, std::move(srcFiles), parent); @@ -257,6 +345,14 @@ FileOperation* FileOperation::moveFiles(Fm::FilePathList srcFiles, Fm::FilePath return op; } +//static +FileOperation *FileOperation::moveFiles(FilePathList srcFiles, FilePathList destFiles, QWidget *parent) { + FileOperation* op = new FileOperation(FileOperation::Move, std::move(srcFiles), parent); + op->setDestFiles(std::move(destFiles)); + op->run(); + return op; +} + //static FileOperation* FileOperation::symlinkFiles(Fm::FilePathList srcFiles, Fm::FilePath dest, QWidget* parent) { FileOperation* op = new FileOperation(FileOperation::Link, std::move(srcFiles), parent); @@ -265,6 +361,14 @@ FileOperation* FileOperation::symlinkFiles(Fm::FilePathList srcFiles, Fm::FilePa return op; } +//static +FileOperation *FileOperation::symlinkFiles(FilePathList srcFiles, FilePathList destFiles, QWidget *parent) { + FileOperation* op = new FileOperation(FileOperation::Link, std::move(srcFiles), parent); + op->setDestFiles(std::move(destFiles)); + op->run(); + return op; +} + //static FileOperation* FileOperation::deleteFiles(Fm::FilePathList srcFiles, bool prompt, QWidget* parent) { if(prompt) { diff --git a/src/fileoperation.h b/src/fileoperation.h index 33f7fea..46c5215 100644 --- a/src/fileoperation.h +++ b/src/fileoperation.h @@ -24,8 +24,8 @@ #include "libfmqtglobals.h" #include #include -#include #include "core/filepath.h" +#include "core/fileoperationjob.h" class QTimer; @@ -37,51 +37,47 @@ class LIBFM_QT_API FileOperation : public QObject { Q_OBJECT public: enum Type { - Copy = FM_FILE_OP_COPY, - Move = FM_FILE_OP_MOVE, - Link = FM_FILE_OP_LINK, - Delete = FM_FILE_OP_DELETE, - Trash = FM_FILE_OP_TRASH, - UnTrash = FM_FILE_OP_UNTRASH, - ChangeAttr = FM_FILE_OP_CHANGE_ATTR + Copy, + Move, + Link, + Delete, + Trash, + UnTrash, + ChangeAttr }; public: explicit FileOperation(Type type, Fm::FilePathList srcFiles, QObject* parent = 0); + virtual ~FileOperation(); void setDestination(Fm::FilePath dest); - void setChmod(mode_t newMode, mode_t newModeMask) { - fm_file_ops_job_set_chmod(job_, newMode, newModeMask); - } + void setDestFiles(FilePathList destFiles); - void setChown(gint uid, gint gid) { - fm_file_ops_job_set_chown(job_, uid, gid); - } + void setChmod(mode_t newMode, mode_t newModeMask); + + void setChown(uid_t uid, gid_t gid); // This only work for change attr jobs. - void setRecursiveChattr(bool recursive) { - fm_file_ops_job_set_recursive(job_, (gboolean)recursive); - } + void setRecursiveChattr(bool recursive); bool run(); - void cancel() { - if(job_) { - fm_job_cancel(FM_JOB(job_)); - } - } + void cancel(); bool isRunning() const { - return job_ ? fm_job_is_running(FM_JOB(job_)) : false; + return job_ && !isCancelled(); } bool isCancelled() const { - return job_ ? fm_job_is_cancelled(FM_JOB(job_)) : false; + if(job_) { + return job_->isCancelled(); + } + return false; } - FmFileOpsJob* job() { + Fm::FileOperationJob* job() { return job_; } @@ -93,32 +89,54 @@ public: } Type type() { - return (Type)job_->type; + return type_; } // convinient static functions static FileOperation* copyFiles(Fm::FilePathList srcFiles, Fm::FilePath dest, QWidget* parent = 0); + + static FileOperation* copyFiles(Fm::FilePathList srcFiles, Fm::FilePathList destFiles, QWidget* parent = 0); + + static FileOperation* copyFile(Fm::FilePath srcFile, Fm::FilePath destFile, QWidget* parent = 0) { + return copyFiles(FilePathList{std::move(srcFile)}, FilePathList{std::move(destFile)}, parent); + } + static FileOperation* moveFiles(Fm::FilePathList srcFiles, Fm::FilePath dest, QWidget* parent = 0); + + static FileOperation* moveFiles(Fm::FilePathList srcFiles, Fm::FilePathList destFiles, QWidget* parent = 0); + + static FileOperation* moveFile(Fm::FilePath srcFile, Fm::FilePath destFile, QWidget* parent = 0) { + return moveFiles(FilePathList{std::move(srcFile)}, FilePathList{std::move(destFile)}, parent); + } + static FileOperation* symlinkFiles(Fm::FilePathList srcFiles, Fm::FilePath dest, QWidget* parent = 0); + + static FileOperation* symlinkFiles(Fm::FilePathList srcFiles, Fm::FilePathList destFiles, QWidget* parent = 0); + + static FileOperation* symlinkFile(Fm::FilePath srcFile, Fm::FilePath destFile, QWidget* parent = 0) { + return symlinkFiles(FilePathList{std::move(srcFile)}, FilePathList{std::move(destFile)}, parent); + } + static FileOperation* deleteFiles(Fm::FilePathList srcFiles, bool promp = true, QWidget* parent = 0); + static FileOperation* trashFiles(Fm::FilePathList srcFiles, bool promp = true, QWidget* parent = 0); + static FileOperation* unTrashFiles(Fm::FilePathList srcFiles, QWidget* parent = 0); + static FileOperation* changeAttrFiles(Fm::FilePathList srcFiles, QWidget* parent = 0); Q_SIGNALS: void finished(); +private Q_SLOTS: + void onJobPrepared(); + void onJobFinish(); + void onJobCancalled(); + void onJobError(const GErrorPtr& err, Fm::Job::ErrorSeverity severity, Fm::Job::ErrorAction& response); + void onJobFileExists(const FileInfo& src, const FileInfo& dest, Fm::FileOperationJob::FileExistsAction& response, FilePath& newDest); + private: - static gint onFileOpsJobAsk(FmFileOpsJob* job, const char* question, char* const* options, FileOperation* pThis); - static gint onFileOpsJobAskRename(FmFileOpsJob* job, FmFileInfo* src, FmFileInfo* dest, char** new_name, FileOperation* pThis); - static FmJobErrorAction onFileOpsJobError(FmFileOpsJob* job, GError* err, FmJobErrorSeverity severity, FileOperation* pThis); - static void onFileOpsJobPrepared(FmFileOpsJob* job, FileOperation* pThis); - static void onFileOpsJobCurFile(FmFileOpsJob* job, const char* cur_file, FileOperation* pThis); - static void onFileOpsJobPercent(FmFileOpsJob* job, guint percent, FileOperation* pThis); - static void onFileOpsJobFinished(FmFileOpsJob* job, FileOperation* pThis); - static void onFileOpsJobCancelled(FmFileOpsJob* job, FileOperation* pThis); - - void handleFinish(); + void disconnectJob(); void showDialog(); @@ -146,11 +164,13 @@ private Q_SLOTS: void onUiTimeout(); private: - FmFileOpsJob* job_; - FileOperationDialog* dlg; - Fm::FilePath destPath; - Fm::FilePathList srcPaths; - QTimer* uiTimer; + Type type_; + FileOperationJob* job_; + FileOperationDialog* dlg_; + FilePath destPath_; + FilePath curFilePath_; + FilePathList srcPaths_; + QTimer* uiTimer_; QElapsedTimer* elapsedTimer_; qint64 lastElapsed_; bool updateRemainingTime_; diff --git a/src/fileoperationdialog.cpp b/src/fileoperationdialog.cpp index 0b26fcd..73f80fd 100644 --- a/src/fileoperationdialog.cpp +++ b/src/fileoperationdialog.cpp @@ -27,6 +27,7 @@ #include "utilities.h" #include "ui_file-operation-dialog.h" + namespace Fm { FileOperationDialog::FileOperationDialog(FileOperation* _operation): @@ -41,35 +42,35 @@ FileOperationDialog::FileOperationDialog(FileOperation* _operation): QString title; QString message; switch(_operation->type()) { - case FM_FILE_OP_MOVE: + case FileOperation::Move: title = tr("Move files"); message = tr("Moving the following files to destination folder:"); break; - case FM_FILE_OP_COPY: + case FileOperation::Copy: title = tr("Copy Files"); message = tr("Copying the following files to destination folder:"); break; - case FM_FILE_OP_TRASH: + case FileOperation::Trash: title = tr("Trash Files"); message = tr("Moving the following files to trash can:"); break; - case FM_FILE_OP_DELETE: + case FileOperation::Delete: title = tr("Delete Files"); message = tr("Deleting the following files:"); ui->dest->hide(); ui->destLabel->hide(); break; - case FM_FILE_OP_LINK: + case FileOperation::Link: title = tr("Create Symlinks"); message = tr("Creating symlinks for the following files:"); break; - case FM_FILE_OP_CHANGE_ATTR: + case FileOperation::ChangeAttr: title = tr("Change Attributes"); message = tr("Changing attributes of the following files:"); ui->dest->hide(); ui->destLabel->hide(); break; - case FM_FILE_OP_UNTRASH: + case FileOperation::UnTrash: title = tr("Restore Trashed Files"); message = tr("Restoring the following files from trash can:"); ui->dest->hide(); @@ -100,47 +101,53 @@ int FileOperationDialog::ask(QString /*question*/, char* const* /*options*/) { return 0; } -int FileOperationDialog::askRename(FmFileInfo* src, FmFileInfo* dest, QString& new_name) { - int ret; + +FileOperationJob::FileExistsAction FileOperationDialog::askRename(const FileInfo &src, const FileInfo &dest, FilePath &newDest) { + FileOperationJob::FileExistsAction ret; if(defaultOption == -1) { // default action is not set, ask the user RenameDialog dlg(src, dest, this); dlg.exec(); switch(dlg.action()) { case RenameDialog::ActionOverwrite: - ret = FM_FILE_OP_OVERWRITE; + ret = FileOperationJob::OVERWRITE; if(dlg.applyToAll()) { defaultOption = ret; } break; - case RenameDialog::ActionRename: - ret = FM_FILE_OP_RENAME; - new_name = dlg.newName(); + case RenameDialog::ActionRename: { + ret = FileOperationJob::RENAME; + auto newName = dlg.newName(); + if(!newName.isEmpty()) { + auto destDirPath = dest.path().parent(); + newDest = destDirPath.child(newName.toUtf8().constData()); + } break; + } case RenameDialog::ActionIgnore: - ret = FM_FILE_OP_SKIP; + ret = FileOperationJob::SKIP; if(dlg.applyToAll()) { defaultOption = ret; } break; default: - ret = FM_FILE_OP_CANCEL; + ret = FileOperationJob::CANCEL; break; } } else { - ret = defaultOption; + ret = (FileOperationJob::FileExistsAction)defaultOption; } return ret; } -FmJobErrorAction FileOperationDialog::error(GError* err, FmJobErrorSeverity severity) { - if(severity >= FM_JOB_ERROR_MODERATE) { - if(severity == FM_JOB_ERROR_CRITICAL) { +Job::ErrorAction FileOperationDialog::error(GError* err, Job::ErrorSeverity severity) { + if(severity >= Job::ErrorSeverity::MODERATE) { + if(severity == Job::ErrorSeverity::CRITICAL) { QMessageBox::critical(this, tr("Error"), QString::fromUtf8(err->message)); - return FM_JOB_ABORT; + return Job::ErrorAction::ABORT; } if (ignoreNonCriticalErrors_) { - return FM_JOB_CONTINUE; + return Job::ErrorAction::CONTINUE; } QMessageBox::StandardButton stb = QMessageBox::critical(this, tr("Error"), QString::fromUtf8(err->message), QMessageBox::Ok | QMessageBox::Ignore); @@ -148,7 +155,7 @@ FmJobErrorAction FileOperationDialog::error(GError* err, FmJobErrorSeverity seve ignoreNonCriticalErrors_ = true; } } - return FM_JOB_CONTINUE; + return Job::ErrorAction::CONTINUE; } void FileOperationDialog::setCurFile(QString cur_file) { @@ -156,15 +163,22 @@ void FileOperationDialog::setCurFile(QString cur_file) { } void FileOperationDialog::setDataTransferred(uint64_t finishedSize, std::uint64_t totalSize) { - ui->dataTransferred->setText(QString("%1 / %2") + ui->filesProcessed->setText(QString("%1 / %2") .arg(formatFileSize(finishedSize, fm_config->si_unit)) .arg(formatFileSize(totalSize, fm_config->si_unit))); } +void FileOperationDialog::setFilesProcessed(uint64_t finishedCount, uint64_t totalCount) { + ui->filesProcessed->setText(QString("%1 / %2") + .arg(finishedCount) + .arg(totalCount)); +} + void FileOperationDialog::setPercent(unsigned int percent) { ui->progressBar->setValue(percent); } + void FileOperationDialog::setRemainingTime(unsigned int sec) { unsigned int min = 0; unsigned int hr = 0; diff --git a/src/fileoperationdialog.h b/src/fileoperationdialog.h index 8f21493..47cfe1c 100644 --- a/src/fileoperationdialog.h +++ b/src/fileoperationdialog.h @@ -27,6 +27,7 @@ #include #include "core/filepath.h" #include "core/fileinfo.h" +#include "core/fileoperationjob.h" namespace Ui { class FileOperationDialog; @@ -46,12 +47,15 @@ public: void setDestPath(const Fm::FilePath& dest); int ask(QString question, char* const* options); - int askRename(FmFileInfo* src, FmFileInfo* dest, QString& new_name); - FmJobErrorAction error(GError* err, FmJobErrorSeverity severity); + + FileOperationJob::FileExistsAction askRename(const FileInfo& src, const FileInfo& dest, FilePath& newDest); + + Job::ErrorAction error(GError* err, Job::ErrorSeverity severity); void setPrepared(); void setCurFile(QString cur_file); void setPercent(unsigned int percent); void setDataTransferred(std::uint64_t finishedSize, std::uint64_t totalSize); + void setFilesProcessed(std::uint64_t finishedCount, std::uint64_t totalCount); void setRemainingTime(unsigned int sec); virtual void reject(); diff --git a/src/fileoperationdialog_p.h b/src/fileoperationdialog_p.h new file mode 100644 index 0000000..a73f3f4 --- /dev/null +++ b/src/fileoperationdialog_p.h @@ -0,0 +1,69 @@ +/* + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_FILEOPERATIONDIALOG_P_H +#define FM_FILEOPERATIONDIALOG_P_H + +#include +#include +#include + +namespace Fm { + +class ElidedLabel : public QLabel { +Q_OBJECT + +public: + explicit ElidedLabel(QWidget *parent = 0, Qt::WindowFlags f = Qt::WindowFlags()): + QLabel(parent, f), + lastWidth_(0) { + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); + // set a min width to prevent the window from widening with long texts + setMinimumWidth(fontMetrics().averageCharWidth() * 10); + } + +protected: + // A simplified version of QLabel::paintEvent() without pixmap or shortcut but with eliding. + void paintEvent(QPaintEvent* /*event*/) override { + QRect cr = contentsRect().adjusted(margin(), margin(), -margin(), -margin()); + QString txt = text(); + // if the text is changed or its rect is resized (due to window resizing), + // find whether it needs to be elided... + if (txt != lastText_ || cr.width() != lastWidth_) { + lastText_ = txt; + lastWidth_ = cr.width(); + elidedText_ = fontMetrics().elidedText(txt, Qt::ElideMiddle, cr.width()); + } + // ... then, draw the (elided) text + if(!elidedText_.isEmpty()) { + QPainter painter(this); + QStyleOption opt; + opt.initFrom(this); + style()->drawItemText(&painter, cr, alignment(), opt.palette, isEnabled(), elidedText_, foregroundRole()); + } + } + +private: + QString elidedText_; + QString lastText_; + int lastWidth_; +}; + +} + +#endif // FM_FILEOPERATIONDIALOG_P_H diff --git a/src/filepropsdialog.cpp b/src/filepropsdialog.cpp index 858aadf..06500f9 100644 --- a/src/filepropsdialog.cpp +++ b/src/filepropsdialog.cpp @@ -20,7 +20,6 @@ #include "filepropsdialog.h" #include "ui_file-props.h" -#include "icontheme.h" #include "utilities.h" #include "fileoperation.h" #include @@ -403,9 +402,9 @@ void FilePropsDialog::accept() { } // check if chown or chmod is needed - gint32 newUid = uidFromName(ui->owner->text()); - gint32 newGid = gidFromName(ui->ownerGroup->text()); - bool needChown = (newUid != -1 && newUid != uid) || (newGid != -1 && newGid != gid); + uid_t newUid = uidFromName(ui->owner->text()); + gid_t newGid = gidFromName(ui->ownerGroup->text()); + bool needChown = (newUid != INVALID_UID && newUid != uid) || (newGid != INVALID_GID && newGid != gid); int newOwnerPermSel = ui->ownerPerm->currentIndex(); int newGroupPermSel = ui->groupPerm->currentIndex(); @@ -421,10 +420,10 @@ void FilePropsDialog::accept() { if(needChown) { // don't do chown if new uid/gid and the original ones are actually the same. if(newUid == uid) { - newUid = -1; + newUid = INVALID_UID; } if(newGid == gid) { - newGid = -1; + newGid = INVALID_GID; } op->setChown(newUid, newGid); } diff --git a/src/filepropsdialog.h b/src/filepropsdialog.h index d155c24..4762a67 100644 --- a/src/filepropsdialog.h +++ b/src/filepropsdialog.h @@ -80,8 +80,8 @@ private: std::shared_ptr mimeType; // mime type of the files - gint32 uid; // owner uid of the files, -1 means all files do not have the same uid - gint32 gid; // owner gid of the files, -1 means all files do not have the same uid + uid_t uid; // owner uid of the files, INVALID_UID means all files do not have the same uid + gid_t gid; // owner gid of the files, INVALID_GID means all files do not have the same uid mode_t ownerPerm; // read permission of the files, -1 means not all files have the same value int ownerPermSel; mode_t groupPerm; // read permission of the files, -1 means not all files have the same value diff --git a/src/filesearchdialog.cpp b/src/filesearchdialog.cpp index 9dbec7c..2ee1af7 100644 --- a/src/filesearchdialog.cpp +++ b/src/filesearchdialog.cpp @@ -33,7 +33,7 @@ FileSearchDialog::FileSearchDialog(QStringList paths, QWidget* parent, Qt::Windo ui->setupUi(this); ui->minSize->setMaximum(std::numeric_limits().max()); ui->maxSize->setMaximum(std::numeric_limits().max()); - Q_FOREACH(const QString& path, paths) { + for(const QString& path : qAsConst(paths)) { ui->listView->addItem(path); } @@ -120,7 +120,7 @@ void FileSearchDialog::accept() { fm_search_set_min_mtime(search, ui->minTime->date().toString(QStringLiteral("yyyy-MM-dd")).toUtf8().constData()); } - searchUri_ = Path::wrapPtr(fm_search_dup_path(search)); + searchUri_ = FilePath{fm_search_to_gfile(search), false}; fm_search_free(search); } @@ -144,7 +144,8 @@ void FileSearchDialog::onAddPath() { void FileSearchDialog::onRemovePath() { // remove selected items - Q_FOREACH(QListWidgetItem* item, ui->listView->selectedItems()) { + const auto itemList = ui->listView->selectedItems(); + for(QListWidgetItem* const item : itemList) { delete item; } } diff --git a/src/filesearchdialog.h b/src/filesearchdialog.h index 1ca6632..5f6126b 100644 --- a/src/filesearchdialog.h +++ b/src/filesearchdialog.h @@ -22,7 +22,7 @@ #include "libfmqtglobals.h" #include -#include "path.h" +#include "core/filepath.h" namespace Ui { class SearchDialog; @@ -35,7 +35,7 @@ public: explicit FileSearchDialog(QStringList paths = QStringList(), QWidget* parent = 0, Qt::WindowFlags f = 0); ~FileSearchDialog(); - Path searchUri() const { + const FilePath& searchUri() const { return searchUri_; } @@ -65,7 +65,7 @@ private Q_SLOTS: private: Ui::SearchDialog* ui; - Path searchUri_; + FilePath searchUri_; }; } diff --git a/src/fm-search.c b/src/fm-search.c index 4298848..4fa20d7 100644 --- a/src/fm-search.c +++ b/src/fm-search.c @@ -229,9 +229,9 @@ void fm_search_set_min_mtime(FmSearch* search, const char* mtime) } /* really build the path */ -FmPath* fm_search_dup_path(FmSearch* search) +GFile* fm_search_to_gfile(FmSearch* search) { - FmPath* search_path = NULL; + GFile* search_path = NULL; GString* search_str = g_string_sized_new(1024); /* build the search:// URI to perform the search */ g_string_append(search_str, "search://"); @@ -310,7 +310,7 @@ FmPath* fm_search_dup_path(FmSearch* search) if(search->max_mtime) g_string_append_printf(search_str, "&max_mtime=%s", search->max_mtime); - search_path = fm_path_new_for_uri(search_str->str); + search_path = g_file_new_for_uri(search_str->str); g_string_free(search_str, TRUE); } return search_path; diff --git a/src/fm-search.h b/src/fm-search.h index c99f71d..f85b152 100644 --- a/src/fm-search.h +++ b/src/fm-search.h @@ -27,7 +27,7 @@ #ifndef _FM_SEARCH_H_ #define _FM_SEARCH_H_ -#include +#include G_BEGIN_DECLS @@ -36,7 +36,7 @@ typedef struct _FmSearch FmSearch; FmSearch* fm_search_new(void); void fm_search_free(FmSearch* search); -FmPath* fm_search_dup_path(FmSearch* search); +GFile* fm_search_to_gfile(FmSearch* search); gboolean fm_search_get_recursive(FmSearch* search); void fm_search_set_recursive(FmSearch* search, gboolean recursive); diff --git a/src/folderitemdelegate.cpp b/src/folderitemdelegate.cpp index 62e1dbc..3cb0b6a 100644 --- a/src/folderitemdelegate.cpp +++ b/src/folderitemdelegate.cpp @@ -111,6 +111,16 @@ void FolderItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& op auto file = index.data(fileInfoRole_).value>(); const auto& emblems = file ? file->emblems() : icon_emblems; + QStyleOptionViewItem opt = option; + initStyleOption(&opt, index); + + // distinguish the hidden items visually by making their texts italic + if(file && file->isHidden()) { + QFont f(opt.font); + f.setItalic(true); + opt.font = f; + } + bool isSymlink = file && file->isSymlink(); // vertical layout (icon mode, thumbnail mode) if(option.decorationPosition == QStyleOptionViewItem::Top || @@ -118,8 +128,6 @@ void FolderItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& op painter->save(); painter->setClipRect(option.rect); - QStyleOptionViewItem opt = option; - initStyleOption(&opt, index); opt.decorationAlignment = Qt::AlignHCenter | Qt::AlignTop; opt.displayAlignment = Qt::AlignTop | Qt::AlignHCenter; @@ -168,12 +176,10 @@ void FolderItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& op // let QStyledItemDelegate does its default painting // FIXME: For better text alignment, here we should increase // the icon width if it's smaller that the requested size - QStyledItemDelegate::paint(painter, option, index); + QStyledItemDelegate::paint(painter, opt, index); // draw emblems if needed if(isSymlink || !emblems.empty()) { - QStyleOptionViewItem opt = option; - initStyleOption(&opt, index); QIcon::Mode iconMode = iconModeFromState(opt.state); // draw some emblems for the item if needed if(isSymlink) { @@ -249,7 +255,8 @@ void FolderItemDelegate::drawText(QPainter* painter, QStyleOptionViewItem& opt, return; } - // ????? + // Respect the active and inactive palettes (some styles can use different colors for them). + // Also, take into account a probable disabled palette. QPalette::ColorGroup cg = (opt.state & QStyle::State_Enabled) ? (opt.state & QStyle::State_Active) ? QPalette::Active @@ -350,8 +357,13 @@ QWidget* FolderItemDelegate::createEditor(QWidget* parent, const QStyleOptionVie return textEdit; } else { - // return the default line-edit in compact view - return QStyledItemDelegate::createEditor(parent, option, index); + // return the default line-edit in other views and + // ensure that its background isn't transparent (on the side-pane) + QWidget* editor = QStyledItemDelegate::createEditor(parent, option, index); + QPalette p = editor->palette(); + p.setColor(QPalette::Base, QApplication::palette().color(QPalette::Base)); + editor->setPalette(p); + return editor; } } diff --git a/src/foldermodel.cpp b/src/foldermodel.cpp index 3437ca4..972e17f 100644 --- a/src/foldermodel.cpp +++ b/src/foldermodel.cpp @@ -19,7 +19,6 @@ #include "foldermodel.h" -#include "icontheme.h" #include #include #include @@ -40,7 +39,6 @@ FolderModel::FolderModel(): } FolderModel::~FolderModel() { - qDebug("delete FolderModel"); // if the thumbnail requests list is not empty, cancel them for(auto job: pendingThumbnailJobs_) { job->cancel(); @@ -87,6 +85,7 @@ void FolderModel::onFilesAdded(const Fm::FileInfoList& files) { items.append(item); } endInsertRows(); + Q_EMIT filesAdded(files); } void FolderModel::onFilesChanged(std::vector& files) { @@ -161,7 +160,8 @@ void FolderModel::setCutFiles(const QItemSelection& selection) { if(!selection.isEmpty()) { auto cutFilesHashSet = std::make_shared(); folder_->setCutFiles(cutFilesHashSet); - for(const auto& index : selection.indexes()) { + const auto indexes = selection.indexes(); + for(const auto& index : indexes) { auto item = itemFromIndex(index); item->bindCutFiles(cutFilesHashSet); cutFilesHashSet->insert(item->info->path().hash()); @@ -230,6 +230,8 @@ QVariant FolderModel::data(const QModelIndex& index, int role/* = Qt::DisplayRol return item->displaySize(); case ColumnFileOwner: return item->ownerName(); + case ColumnFileGroup: + return item->ownerGroup(); } break; } @@ -275,6 +277,9 @@ QVariant FolderModel::headerData(int section, Qt::Orientation orientation, int r case ColumnFileOwner: title = tr("Owner"); break; + case ColumnFileGroup: + title = tr("Group"); + break; } return QVariant(title); } @@ -361,12 +366,12 @@ QList< FolderModelItem >::iterator FolderModel::findItemByFileInfo(const Fm::Fil } QStringList FolderModel::mimeTypes() const { - qDebug("FolderModel::mimeTypes"); + //qDebug("FolderModel::mimeTypes"); QStringList types = QAbstractItemModel::mimeTypes(); // now types contains "application/x-qabstractitemmodeldatalist" // add support for freedesktop Xdnd direct save (XDS) protocol. - // http://www.freedesktop.org/wiki/Specifications/XDS/#index4h2 + // https://www.freedesktop.org/wiki/Specifications/XDS/#index4h2 // the real implementation is in FolderView::childDropEvent(). types << "XdndDirectSave0"; types << "text/uri-list"; @@ -376,7 +381,7 @@ QStringList FolderModel::mimeTypes() const { QMimeData* FolderModel::mimeData(const QModelIndexList& indexes) const { QMimeData* data = QAbstractItemModel::mimeData(indexes); - qDebug("FolderModel::mimeData"); + //qDebug("FolderModel::mimeData"); // build a uri list QByteArray urilist; urilist.reserve(4096); @@ -398,7 +403,7 @@ QMimeData* FolderModel::mimeData(const QModelIndexList& indexes) const { } bool FolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) { - qDebug("FolderModel::dropMimeData"); + //qDebug("FolderModel::dropMimeData"); if(!folder_ || !data) { return false; } @@ -413,7 +418,12 @@ bool FolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int info = fileInfoFromIndex(itemIndex); } if(info) { - destPath = info->path(); + if (info->isDir()) { + destPath = info->path(); + } + else { + destPath = path(); // don't drop on file + } } else { return false; @@ -425,7 +435,7 @@ bool FolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int // FIXME: should we put this in dropEvent handler of FolderView instead? if(data->hasUrls()) { - qDebug("drop action: %d", action); + //qDebug("drop action: %d", action); auto srcPaths = pathListFromQUrls(data->urls()); switch(action) { case Qt::CopyAction: @@ -436,6 +446,7 @@ bool FolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int break; case Qt::LinkAction: FileOperation::symlinkFiles(srcPaths, destPath); + /* Falls through. */ default: return false; } @@ -448,7 +459,7 @@ bool FolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int } Qt::DropActions FolderModel::supportedDropActions() const { - qDebug("FolderModel::supportedDropActions"); + //qDebug("FolderModel::supportedDropActions"); return Qt::CopyAction | Qt::MoveAction | Qt::LinkAction; } diff --git a/src/foldermodel.h b/src/foldermodel.h index 3fd34c7..8a680cb 100644 --- a/src/foldermodel.h +++ b/src/foldermodel.h @@ -54,6 +54,7 @@ public: ColumnFileSize, ColumnFileMTime, ColumnFileOwner, + ColumnFileGroup, NumOfColumns }; @@ -98,6 +99,7 @@ public: Q_SIGNALS: void thumbnailLoaded(const QModelIndex& index, int size); void fileSizeChanged(const QModelIndex& index); + void filesAdded(FileInfoList infoList); protected Q_SLOTS: diff --git a/src/foldermodelitem.cpp b/src/foldermodelitem.cpp index 273fca1..44922ce 100644 --- a/src/foldermodelitem.cpp +++ b/src/foldermodelitem.cpp @@ -43,10 +43,7 @@ QString FolderModelItem::ownerName() const { QString name; auto user = Fm::UserInfoCache::globalInstance()->userFromId(info->uid()); if(user) { - name = user->realName(); - if(name.isEmpty()) { - name = user->name(); - } + name = user->name(); } return name; } diff --git a/src/foldermodelitem.h b/src/foldermodelitem.h index d0bcee6..6ab6f4c 100644 --- a/src/foldermodelitem.h +++ b/src/foldermodelitem.h @@ -27,7 +27,6 @@ #include #include #include -#include "icontheme.h" #include "core/folder.h" diff --git a/src/folderview.cpp b/src/folderview.cpp index c2166f9..c9f2f05 100644 --- a/src/folderview.cpp +++ b/src/folderview.cpp @@ -45,12 +45,16 @@ #include // for XDS support #include // for XDS support #include "xdndworkaround.h" // for XDS support -#include "path.h" #include "folderview_p.h" #include "utilities.h" Q_DECLARE_OPAQUE_POINTER(FmFileInfo*) +#define SCROLL_FRAMES_PER_SEC 50 +#define SCROLL_DURATION 300 // in ms + +static const int scrollAnimFrames = SCROLL_FRAMES_PER_SEC * SCROLL_DURATION / 1000; + using namespace Fm; FolderViewListView::FolderViewListView(QWidget* parent): @@ -144,7 +148,7 @@ void FolderViewListView::dragEnterEvent(QDragEnterEvent* event) { else { QAbstractItemView::dragEnterEvent(event); } - qDebug("dragEnterEvent"); + //qDebug("dragEnterEvent"); //static_cast(parent())->childDragEnterEvent(event); } @@ -410,7 +414,7 @@ void FolderViewTreeView::reset() { // This is for performance reason so in this case rowsInserted() and rowsAboutToBeRemoved() // might not be called. Hence we also have to re-layout the columns when the model is reset. // This fixes bug #190 - // https://github.com/lxde/pcmanfm-qt/issues/190 + // https://github.com/lxqt/pcmanfm-qt/issues/190 QTreeView::reset(); queueLayoutColumns(); } @@ -476,7 +480,9 @@ FolderView::FolderView(FolderView::ViewMode _mode, QWidget *parent): autoSelectionDelay_(600), autoSelectionTimer_(nullptr), selChangedTimer_(nullptr), - itemDelegateMargins_(QSize(3, 3)) { + itemDelegateMargins_(QSize(3, 3)), + smoothScrollTimer_(nullptr), + wheelEvent_(nullptr) { iconSize_[IconMode - FirstViewMode] = QSize(48, 48); iconSize_[CompactMode - FirstViewMode] = QSize(24, 24); @@ -495,6 +501,11 @@ FolderView::FolderView(FolderView::ViewMode _mode, QWidget *parent): } FolderView::~FolderView() { + if(smoothScrollTimer_) { + disconnect(smoothScrollTimer_, &QTimer::timeout, this, &FolderView::scrollSmoothly); + smoothScrollTimer_->stop(); + delete smoothScrollTimer_; + } } void FolderView::onItemActivated(QModelIndex index) { @@ -567,6 +578,13 @@ void FolderView::setViewMode(ViewMode _mode) { if(_mode == mode) { // if it's the same more, ignore return; } + // smooth scrolling is only for icon and thumbnail modes + if(smoothScrollTimer_ && (_mode == DetailedListMode || _mode == CompactMode)) { + disconnect(smoothScrollTimer_, &QTimer::timeout, this, &FolderView::scrollSmoothly); + smoothScrollTimer_->stop(); + delete smoothScrollTimer_; + smoothScrollTimer_ = nullptr; + } // FIXME: retain old selection // since only detailed list mode uses QTreeView, and others @@ -656,11 +674,10 @@ void FolderView::setViewMode(ViewMode _mode) { view->setSelectionMode(QAbstractItemView::ExtendedSelection); layout()->addWidget(view); - // enable dnd + // enable dnd (the drop indicator is set at "FolderView::childDragMoveEvent()") view->setDragEnabled(true); view->setAcceptDrops(true); view->setDragDropMode(QAbstractItemView::DragDrop); - view->setDropIndicatorShown(true); // inline renaming if(delegate) { @@ -891,6 +908,43 @@ QModelIndex FolderView::indexFromFolderPath(const Fm::FilePath& folderPath) cons return QModelIndex(); } +void FolderView::selectFiles(const Fm::FileInfoList& files, bool add) { + if(!model_ || files.empty()) { + return; + } + if(!add) { + selectionModel()->clear(); + } + QModelIndex index, firstIndex; + int count = model_->rowCount(); + Fm::FileInfoList list = files; + bool singleFile(files.size() == 1); + for(int row = 0; row < count; ++row) { + if (list.empty()) { + break; + } + index = model_->index(row, 0); + auto info = model_->fileInfoFromIndex(index); + for(auto it = list.cbegin(); it != list.cend(); ++it) { + auto& item = *it; + if(item == info) { + selectionModel()->select(index, QItemSelectionModel::Select); + if (!firstIndex.isValid()) { + firstIndex = index; + } + list.erase(it); + break; + } + } + } + if (firstIndex.isValid()) { + view->scrollTo(firstIndex, QAbstractItemView::EnsureVisible); + if (singleFile) { // give focus to the single file + selectionModel()->setCurrentIndex(firstIndex, QItemSelectionModel::Current); + } + } +} + Fm::FileInfoList FolderView::selectedFiles() const { if(model_) { QModelIndexList selIndexes = mode == DetailedListMode ? selectedRows() : selectedIndexes(); @@ -941,7 +995,7 @@ void FolderView::invertSelection() { } void FolderView::childDragEnterEvent(QDragEnterEvent* event) { - qDebug("drag enter"); + //qDebug("drag enter"); if(event->mimeData()->hasFormat("text/uri-list")) { event->accept(); } @@ -951,12 +1005,23 @@ void FolderView::childDragEnterEvent(QDragEnterEvent* event) { } void FolderView::childDragLeaveEvent(QDragLeaveEvent* e) { - qDebug("drag leave"); + //qDebug("drag leave"); e->accept(); } -void FolderView::childDragMoveEvent(QDragMoveEvent* /*e*/) { - qDebug("drag move"); +void FolderView::childDragMoveEvent(QDragMoveEvent* e) { + // Since it isn't possible to drop on a file (see "FolderModel::dropMimeData()"), + // we enable the drop indicator only when the cursor is on a folder. + QModelIndex index = view->indexAt(e->pos()); + if(index.isValid() && index.model()) { + QVariant data = index.model()->data(index, FolderModel::FileInfoRole); + auto info = data.value>(); + if(info && !info->isDir()) { + view->setDropIndicatorShown(false); + return; + } + } + view->setDropIndicatorShown(true); } void FolderView::childDropEvent(QDropEvent* e) { @@ -1049,8 +1114,8 @@ bool FolderView::eventFilter(QObject* watched, QEvent* event) { } autoSelectionTimer_->start(autoSelectionDelay_); } - break; } + break; case QEvent::HoverLeave: if(style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick)) { setCursor(Qt::ArrowCursor); @@ -1086,6 +1151,42 @@ bool FolderView::eventFilter(QObject* watched, QEvent* event) { return true; } } + // Smooth Scrolling + // Some tricks are adapted from . + else if(mode != DetailedListMode + && event->spontaneous() + && !(QApplication::keyboardModifiers() & (Qt::ShiftModifier | Qt::AltModifier))) { + if(QScrollBar* vbar = view->verticalScrollBar()) { + // keep track of the wheel event for smooth scrolling + wheelEvent_ = static_cast(event); + int delta = wheelEvent_->angleDelta().y(); + if((delta > 0 && vbar->value() == vbar->minimum()) || (delta < 0 && vbar->value() == vbar->maximum())) { + break; // the scrollbar can't move + } + // get a rough estimation of the wheel speed and disable animation if it's too high + static QList wheelEvents; + wheelEvents << QDateTime::currentMSecsSinceEpoch(); + while(wheelEvents.last() - wheelEvents.first() > 500) { + wheelEvents.removeFirst(); + } + if(wheelEvents.size() > 10) { + break; + } + + if(!smoothScrollTimer_) { + smoothScrollTimer_ = new QTimer(); + connect(smoothScrollTimer_, &QTimer::timeout, this, &FolderView::scrollSmoothly); + } + + // set the data for smooth scrolling + scollData data; + data.delta = delta; + data.leftFrames = scrollAnimFrames; + queuedScrollSteps_.append(data); + smoothScrollTimer_->start(1000 / SCROLL_FRAMES_PER_SEC); + return true; + } + } break; default: break; @@ -1094,6 +1195,36 @@ bool FolderView::eventFilter(QObject* watched, QEvent* event) { return QObject::eventFilter(watched, event); } +void FolderView::scrollSmoothly() { + if(!wheelEvent_ || !view->verticalScrollBar()) { + return; + } + + int totalDelta = 0; + QList::iterator it = queuedScrollSteps_.begin(); + while(it != queuedScrollSteps_.end()) { + if(it->leftFrames == 1) { // find the exact delta for the last frame + totalDelta += it->delta - (scrollAnimFrames - 1) * qRound((qreal)it->delta / (qreal)scrollAnimFrames); + it = queuedScrollSteps_.erase(it); + } + else { + totalDelta += qRound((qreal)it->delta / (qreal)scrollAnimFrames); + -- it->leftFrames; + ++it; + } + } + if(totalDelta != 0) { + // as in qevent.cpp -> QWheelEvent::QWheelEvent() + QWheelEvent e(wheelEvent_->pos(), wheelEvent_->globalPos(), + totalDelta, + wheelEvent_->buttons(), Qt::NoModifier, Qt::Vertical); + QApplication::sendEvent(view->verticalScrollBar(), &e); + } + if(queuedScrollSteps_.empty()) { + smoothScrollTimer_->stop(); + } +} + // this slot handles auto-selection of items. void FolderView::onAutoSelectionTimeout() { if(QApplication::mouseButtons() != Qt::NoButton) { @@ -1218,8 +1349,10 @@ void FolderView::onClipboardDataChange() { if(!folder()->path().hasUriScheme("search") // skip for search results && isCutSelection && Fm::isCurrentPidClipboardData(*data)) { // set cut files only with this app - auto cutDirPath = paths.size() > 0 ? paths[0].parent(): FilePath(); - if(folder()->path() == cutDirPath) { + auto cutDirPath = paths.size() > 0 ? paths[0].parent() : FilePath(); + // set the cut file(s) only if the cutting is done here + if(folder()->path() == cutDirPath + && selectedFilePaths() == paths) { model_->setCutFiles(selectionModel()->selection()); } else if(folder()->hadCutFilesUnset() || folder()->hasCutFiles()) { diff --git a/src/folderview.h b/src/folderview.h index 5e36433..3ea50a3 100644 --- a/src/folderview.h +++ b/src/folderview.h @@ -29,7 +29,6 @@ #include #include "foldermodel.h" #include "proxyfoldermodel.h" -#include "path.h" #include "core/folder.h" @@ -102,6 +101,7 @@ public: Fm::FilePathList selectedFilePaths() const; bool hasSelection() const; QModelIndex indexFromFolderPath(const Fm::FilePath& folderPath) const; + void selectFiles(const Fm::FileInfoList& files, bool add = false); void selectAll(); @@ -160,6 +160,7 @@ private Q_SLOTS: void onAutoSelectionTimeout(); void onSelChangedTimeout(); void onClosingEditor(QWidget* editor, QAbstractItemDelegate::EndEditHint hint); + void scrollSmoothly(); Q_SIGNALS: void clicked(int type, const std::shared_ptr& file); @@ -181,6 +182,14 @@ private: QTimer* selChangedTimer_; // the cell margins in the icon and thumbnail modes QSize itemDelegateMargins_; + // smooth scrolling: + struct scollData { + int delta; + int leftFrames; + }; + QTimer *smoothScrollTimer_; + QWheelEvent *wheelEvent_; + QList queuedScrollSteps_; }; } diff --git a/src/icontheme.cpp b/src/icontheme.cpp deleted file mode 100644 index 55d1f28..0000000 --- a/src/icontheme.cpp +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * - */ - - -#include "icontheme.h" -#include -#include -#include -#include -#include -#include - -#include "core/iconinfo.h" - -namespace Fm { - -static IconTheme* theIconTheme = nullptr; // the global single instance of IconTheme. - -IconTheme::IconTheme(): - currentThemeName_(QIcon::themeName()) { - // NOTE: only one instance is allowed - Q_ASSERT(theIconTheme == nullptr); - Q_ASSERT(qApp != nullptr); // QApplication should exists before contructing IconTheme. - - theIconTheme = this; - - // We need to get notified when there is a QEvent::StyleChange event so - // we can check if the current icon theme name is changed. - // To do this, we can filter QApplication object itself to intercept - // signals of all widgets, but this may be too inefficient. - // So, we only filter the events on QDesktopWidget instead. - qApp->desktop()->installEventFilter(this); -} - -IconTheme::~IconTheme() { -} - -IconTheme* IconTheme::instance() { - return theIconTheme; -} - -// check if the icon theme name is changed and emit "changed()" signal if any change is detected. -void IconTheme::checkChanged() { - if(QIcon::themeName() != theIconTheme->currentThemeName_) { - // if the icon theme is changed - theIconTheme->currentThemeName_ = QIcon::themeName(); - // invalidate the cached data - Fm::IconInfo::updateQIcons(); - Q_EMIT theIconTheme->changed(); - } -} - -// this method is called whenever there is an event on the QDesktopWidget object. -bool IconTheme::eventFilter(QObject* obj, QEvent* event) { - // we're only interested in the StyleChange event. - // FIXME: QEvent::ThemeChange seems to be interal to Qt 5 and is not documented - if(event->type() == QEvent::StyleChange || event->type() == QEvent::ThemeChange) { - checkChanged(); // check if the icon theme is changed - } - return QObject::eventFilter(obj, event); -} - - -} // namespace Fm diff --git a/src/icontheme.h b/src/icontheme.h deleted file mode 100644 index 8337e8b..0000000 --- a/src/icontheme.h +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * - */ - - -#ifndef FM_ICONTHEME_H -#define FM_ICONTHEME_H - -#include "libfmqtglobals.h" -#include -#include -#include "libfm/fm.h" - -namespace Fm { - -class LIBFM_QT_API IconTheme: public QObject { - Q_OBJECT -public: - IconTheme(); - ~IconTheme(); - - static IconTheme* instance(); - - static void checkChanged(); // check if current icon theme name is changed - -Q_SIGNALS: - void changed(); // emitted when the name of current icon theme is changed - -protected: - bool eventFilter(QObject* obj, QEvent* event); - -private: - QString currentThemeName_; -}; - -} - -#endif // FM_ICONTHEME_H diff --git a/src/libfm-qt.pc.in b/src/libfm-qt.pc.in index 11e35a3..2ca5c17 100644 --- a/src/libfm-qt.pc.in +++ b/src/libfm-qt.pc.in @@ -5,7 +5,7 @@ includedir=${prefix}/include Name: libfm-qt Description: A Qt/glib/gio-based lib used to develop file managers providing some file management utilities. (This is a Qt port of the original libfm library) -URL: http://pcmanfm.sourceforge.net/ +URL: https://github.com/lxqt/libfm-qt Requires: @REQUIRED_QT@ libfm >= 1.2.0 Version: @LIBFM_QT_VERSION@ Libs: -L${libdir} -lfm -l@LIBFM_QT_LIBRARY_NAME@ diff --git a/src/libfmqt.cpp b/src/libfmqt.cpp index 756f76b..2b01643 100644 --- a/src/libfmqt.cpp +++ b/src/libfmqt.cpp @@ -21,7 +21,6 @@ #include "libfmqt.h" #include #include -#include "icontheme.h" #include "core/thumbnailer.h" #include "xdndworkaround.h" @@ -31,7 +30,6 @@ struct LibFmQtData { LibFmQtData(); ~LibFmQtData(); - IconTheme* iconTheme; QTranslator translator; XdndWorkaround workaround; int refCount; @@ -52,7 +50,6 @@ LibFmQtData::LibFmQtData(): refCount(1) { fm_init(nullptr); // turn on glib debug message // g_setenv("G_MESSAGES_DEBUG", "all", true); - iconTheme = new IconTheme(); Fm::Thumbnailer::loadAll(); translator.load("libfm-qt_" + QLocale::system().name(), LIBFM_QT_DATA_DIR "/translations"); @@ -67,7 +64,6 @@ LibFmQtData::~LibFmQtData() { GVfs* vfs = g_vfs_get_default(); g_vfs_unregister_uri_scheme(vfs, "menu"); g_vfs_unregister_uri_scheme(vfs, "search"); - delete iconTheme; fm_finalize(); } diff --git a/src/mountoperation.cpp b/src/mountoperation.cpp index f9a1a5b..5502da4 100644 --- a/src/mountoperation.cpp +++ b/src/mountoperation.cpp @@ -26,6 +26,7 @@ #include "mountoperationpassworddialog_p.h" #include "mountoperationquestiondialog_p.h" #include "ui_mount-operation-password.h" +#include "core/gioptrs.h" namespace Fm { @@ -83,6 +84,16 @@ MountOperation::~MountOperation() { // qDebug("MountOperation deleted"); } +void MountOperation::mountEnclosingVolume(const FilePath &path) { + g_file_mount_enclosing_volume(path.gfile().get(), G_MOUNT_MOUNT_NONE, op, cancellable_, + (GAsyncReadyCallback)onMountFileFinished, new QPointer(this)); +} + +void MountOperation::mountMountable(const FilePath &mountable) { + g_file_mount_mountable(mountable.gfile().get(), G_MOUNT_MOUNT_NONE, op, cancellable_, + (GAsyncReadyCallback)onMountMountableFinished, new QPointer(this)); +} + void MountOperation::onAbort(GMountOperation* /*_op*/, MountOperation* /*pThis*/) { } @@ -143,6 +154,15 @@ void MountOperation::onMountFileFinished(GFile* file, GAsyncResult* res, QPointe delete pThis; } +void MountOperation::onMountMountableFinished(GFile* file, GAsyncResult* res, QPointer* pThis) { + if(*pThis) { + GError* error = nullptr; + g_file_mount_mountable_finish(file, res, &error); + (*pThis)->handleFinish(error); + } + delete pThis; +} + void MountOperation::onMountVolumeFinished(GVolume* volume, GAsyncResult* res, QPointer< MountOperation >* pThis) { if(*pThis) { GError* error = nullptr; diff --git a/src/mountoperation.h b/src/mountoperation.h index 97e558f..5587508 100644 --- a/src/mountoperation.h +++ b/src/mountoperation.h @@ -48,11 +48,15 @@ public: explicit MountOperation(bool interactive = true, QWidget* parent = 0); ~MountOperation(); + FM_QT_DEPRECATED void mount(const Fm::FilePath& path) { - g_file_mount_enclosing_volume(path.gfile().get(), G_MOUNT_MOUNT_NONE, op, cancellable_, - (GAsyncReadyCallback)onMountFileFinished, new QPointer(this)); + mountEnclosingVolume(path); } + void mountEnclosingVolume(const Fm::FilePath& path); + + void mountMountable(const Fm::FilePath& mountable); + void mount(GVolume* volume) { g_volume_mount(volume, G_MOUNT_MOUNT_NONE, op, cancellable_, (GAsyncReadyCallback)onMountVolumeFinished, new QPointer(this)); } @@ -135,6 +139,7 @@ private: // it's possible that this object is freed when the callback is called by gio, so guarding with QPointer is needed here. static void onMountFileFinished(GFile* file, GAsyncResult* res, QPointer* pThis); + static void onMountMountableFinished(GFile* file, GAsyncResult* res, QPointer* pThis); static void onMountVolumeFinished(GVolume* volume, GAsyncResult* res, QPointer* pThis); static void onUnmountMountFinished(GMount* mount, GAsyncResult* res, QPointer* pThis); static void onEjectMountFinished(GMount* mount, GAsyncResult* res, QPointer* pThis); diff --git a/src/path.h b/src/path.h deleted file mode 100644 index dccf91e..0000000 --- a/src/path.h +++ /dev/null @@ -1,441 +0,0 @@ -/* - * Copyright (C) 2016 Hong Jen Yee (PCMan) - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * - */ - -#ifndef __LIBFM_QT_FM_PATH_H__ -#define __LIBFM_QT_FM_PATH_H__ - -#include -#include -#include -#include -#include "libfmqtglobals.h" - - -namespace Fm { - - -class LIBFM_QT_API PathList { -public: - - - PathList(void ) { - dataPtr_ = reinterpret_cast(fm_path_list_new()); - } - - - PathList(FmPathList* dataPtr){ - dataPtr_ = dataPtr != nullptr ? reinterpret_cast(fm_list_ref(FM_LIST(dataPtr))) : nullptr; - } - - - // copy constructor - PathList(const PathList& other) { - dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast(fm_list_ref(FM_LIST(other.dataPtr_))) : nullptr; - } - - - // move constructor - PathList(PathList&& other) { - dataPtr_ = reinterpret_cast(other.takeDataPtr()); - } - - - // destructor - ~PathList() { - if(dataPtr_ != nullptr) { - fm_list_unref(FM_LIST(dataPtr_)); - } - } - - - // create a wrapper for the data pointer without increasing the reference count - static PathList wrapPtr(FmPathList* dataPtr) { - PathList obj; - obj.dataPtr_ = reinterpret_cast(dataPtr); - return obj; - } - - // disown the managed data pointer - FmPathList* takeDataPtr() { - FmPathList* data = reinterpret_cast(dataPtr_); - dataPtr_ = nullptr; - return data; - } - - // get the raw pointer wrapped - FmPathList* dataPtr() { - return reinterpret_cast(dataPtr_); - } - - // automatic type casting - operator FmPathList*() { - return dataPtr(); - } - - // copy assignment - PathList& operator=(const PathList& other) { - if(dataPtr_ != nullptr) { - fm_list_unref(FM_LIST(dataPtr_)); - } - dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast(fm_list_ref(FM_LIST(other.dataPtr_))) : nullptr; - return *this; - } - - - // move assignment - PathList& operator=(PathList&& other) { - dataPtr_ = reinterpret_cast(other.takeDataPtr()); - return *this; - } - - bool isNull() { - return (dataPtr_ == nullptr); - } - - // methods - - void writeUriList(GString* buf) { - fm_path_list_write_uri_list(dataPtr(), buf); - } - - char* toUriList(void) { - return fm_path_list_to_uri_list(dataPtr()); - } - - unsigned int getLength() { - return fm_path_list_get_length(dataPtr()); - } - - bool isEmpty() { - return fm_path_list_is_empty(dataPtr()); - } - - FmPath* peekHead() { - return fm_path_list_peek_head(dataPtr()); - } - - GList* peekHeadLink() { - return fm_path_list_peek_head_link(dataPtr()); - } - - void pushTail(FmPath* path) { - fm_path_list_push_tail(dataPtr(), path); - } - - static PathList newFromFileInfoGslist(GSList* fis) { - return PathList::wrapPtr(fm_path_list_new_from_file_info_gslist(fis)); - } - - - static PathList newFromFileInfoGlist(GList* fis) { - return PathList::wrapPtr(fm_path_list_new_from_file_info_glist(fis)); - } - - - static PathList newFromFileInfoList(FmFileInfoList* fis) { - return PathList::wrapPtr(fm_path_list_new_from_file_info_list(fis)); - } - - - static PathList newFromUris(char* const* uris) { - return PathList::wrapPtr(fm_path_list_new_from_uris(uris)); - } - - - static PathList newFromUriList(const char* uri_list) { - return PathList::wrapPtr(fm_path_list_new_from_uri_list(uri_list)); - } - - - -private: - FmPathList* dataPtr_; // data pointer for the underlying C struct - -}; - - - -class LIBFM_QT_API Path { -public: - - - // default constructor - Path() { - dataPtr_ = nullptr; - } - - - Path(FmPath* dataPtr){ - dataPtr_ = dataPtr != nullptr ? reinterpret_cast(fm_path_ref(dataPtr)) : nullptr; - } - - - // copy constructor - Path(const Path& other) { - dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast(fm_path_ref(other.dataPtr_)) : nullptr; - } - - - // move constructor - Path(Path&& other) { - dataPtr_ = reinterpret_cast(other.takeDataPtr()); - } - - - // destructor - ~Path() { - if(dataPtr_ != nullptr) { - fm_path_unref(dataPtr_); - } - } - - - // create a wrapper for the data pointer without increasing the reference count - static Path wrapPtr(FmPath* dataPtr) { - Path obj; - obj.dataPtr_ = reinterpret_cast(dataPtr); - return obj; - } - - // disown the managed data pointer - FmPath* takeDataPtr() { - FmPath* data = reinterpret_cast(dataPtr_); - dataPtr_ = nullptr; - return data; - } - - // get the raw pointer wrapped - FmPath* dataPtr() { - return reinterpret_cast(dataPtr_); - } - - // automatic type casting - operator FmPath*() { - return dataPtr(); - } - - // copy assignment - Path& operator=(const Path& other) { - if(dataPtr_ != nullptr) { - fm_path_unref(dataPtr_); - } - dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast(fm_path_ref(other.dataPtr_)) : nullptr; - return *this; - } - - - // move assignment - Path& operator=(Path&& other) { - dataPtr_ = reinterpret_cast(other.takeDataPtr()); - return *this; - } - - bool isNull() { - return (dataPtr_ == nullptr); - } - - // methods - bool isNative() { - return fm_path_is_native(dataPtr()); - } - - bool isTrash() { - return fm_path_is_trash(dataPtr()); - } - - bool isTrashRoot() { - return fm_path_is_trash_root(dataPtr()); - } - - bool isNativeOrTrash() { - return fm_path_is_native_or_trash(dataPtr()); - } - - int depth(void) { - return fm_path_depth(dataPtr()); - } - - - bool equalStr(const gchar* str, int n) { - return fm_path_equal_str(dataPtr(), str, n); - } - - - int compare(FmPath* p2) { - return fm_path_compare(dataPtr(), p2); - } - - int compare(Path& p2) { - return fm_path_compare(dataPtr(), p2.dataPtr()); - } - - bool equal(FmPath* p2) { - return fm_path_equal(dataPtr(), p2); - } - - bool equal(Path& p2) { - return fm_path_equal(dataPtr(), p2.dataPtr()); - } - - bool operator == (Path& other) { - return fm_path_equal(dataPtr(), other.dataPtr()); - } - - bool operator != (Path& other) { - return !fm_path_equal(dataPtr(), other.dataPtr()); - } - - bool operator < (Path& other) { - return compare(other); - } - - bool operator > (Path& other) { - return (other < *this); - } - - unsigned int hash(void) { - return fm_path_hash(dataPtr()); - } - - - char* displayBasename(void) { - return fm_path_display_basename(dataPtr()); - } - - char* displayName(gboolean human_readable) { - return fm_path_display_name(dataPtr(), human_readable); - } - - - GFile* toGfile(void) { - return fm_path_to_gfile(dataPtr()); - } - - - char* toUri(void) { - return fm_path_to_uri(dataPtr()); - } - - - char* toStr(void) { - return fm_path_to_str(dataPtr()); - } - - - Path getSchemePath(void) { - return Path(fm_path_get_scheme_path(dataPtr())); - } - - - bool hasPrefix(FmPath* prefix) { - return fm_path_has_prefix(dataPtr(), prefix); - } - - - FmPathFlags getFlags(void) { - return fm_path_get_flags(dataPtr()); - } - - - Path getParent(void) { - return Path(fm_path_get_parent(dataPtr())); - } - - - static Path getAppsMenu(void ) { - return Path(fm_path_get_apps_menu()); - } - - - static Path getTrash(void ) { - return Path(fm_path_get_trash()); - } - - - static Path getDesktop(void ) { - return Path(fm_path_get_desktop()); - } - - - static Path getHome(void ) { - return Path(fm_path_get_home()); - } - - - static Path getRoot(void ) { - return Path(fm_path_get_root()); - } - - - static Path newForGfile(GFile* gf) { - return Path::wrapPtr(fm_path_new_for_gfile(gf)); - } - - - Path newRelative(const char* rel) { - return Path::wrapPtr(fm_path_new_relative(dataPtr(), rel)); - } - - - Path newChildLen(const char* basename, int name_len) { - return Path::wrapPtr(fm_path_new_child_len(dataPtr(), basename, name_len)); - } - - - Path newChild(const char* basename) { - return Path::wrapPtr(fm_path_new_child(dataPtr(), basename)); - } - - - static Path newForCommandlineArg(const char* arg) { - return Path::wrapPtr(fm_path_new_for_commandline_arg(arg)); - } - - - static Path newForStr(const char* path_str) { - return Path::wrapPtr(fm_path_new_for_str(path_str)); - } - - - static Path newForDisplayName(const char* path_name) { - return Path::wrapPtr(fm_path_new_for_display_name(path_name)); - } - - - static Path newForUri(const char* uri) { - return Path::wrapPtr(fm_path_new_for_uri(uri)); - } - - - static Path newForPath(const char* path_name) { - return Path::wrapPtr(fm_path_new_for_path(path_name)); - } - - - -private: - FmPath* dataPtr_; // data pointer for the underlying C struct - -}; - -} - -Q_DECLARE_OPAQUE_POINTER(FmPath*) - -#endif // __LIBFM_QT_FM_PATH_H__ diff --git a/src/pathbar.cpp b/src/pathbar.cpp index 3ddb250..558627a 100644 --- a/src/pathbar.cpp +++ b/src/pathbar.cpp @@ -305,8 +305,9 @@ void PathBar::openEditor() { connect(tempPathEdit_, &PathEdit::returnPressed, this, &PathBar::onReturnPressed); connect(tempPathEdit_, &PathEdit::editingFinished, this, &PathBar::closeEditor); } - tempPathEdit_->setFocus(); tempPathEdit_->selectAll(); + QApplication::clipboard()->setText(tempPathEdit_->text(), QClipboard::Selection); + QTimer::singleShot(0, tempPathEdit_, SLOT(setFocus())); } void PathBar::closeEditor() { diff --git a/src/pathedit.cpp b/src/pathedit.cpp index 27b3a2a..61da0d0 100644 --- a/src/pathedit.cpp +++ b/src/pathedit.cpp @@ -112,7 +112,7 @@ bool PathEdit::event(QEvent* e) { if(keyEvent->key() == Qt::Key_Tab && keyEvent->modifiers() == Qt::NoModifier) { // Tab key is pressed e->accept(); // do auto-completion when the user press the Tab key. - // This fixes #201: https://github.com/lxde/pcmanfm-qt/issues/201 + // This fixes #201: https://github.com/lxqt/pcmanfm-qt/issues/201 autoComplete(); return true; } diff --git a/src/placesmodel.cpp b/src/placesmodel.cpp index d6d3776..078bbca 100644 --- a/src/placesmodel.cpp +++ b/src/placesmodel.cpp @@ -19,7 +19,6 @@ #include "placesmodel.h" -#include "icontheme.h" #include #include #include @@ -55,25 +54,19 @@ PlacesModel::PlacesModel(QObject* parent): createTrashItem(); - // FIXME: add an option to hide network:/// - if(true) { - computerItem = new PlacesModelItem("computer", tr("Computer"), Fm::FilePath::fromUri("computer:///")); - placesRoot->appendRow(computerItem); - } - else { - computerItem = nullptr; - } + computerItem = new PlacesModelItem("computer", tr("Computer"), Fm::FilePath::fromUri("computer:///")); + placesRoot->appendRow(computerItem); - // FIXME: add an option to hide applications:/// - const char* applicaion_icon_names[] = {"system-software-install", "applications-accessories", "application-x-executable"}; - // NOTE: g_themed_icon_new_from_names() accepts char**, but actually const char** is OK. - Fm::GIconPtr gicon{g_themed_icon_new_from_names((char**)applicaion_icon_names, G_N_ELEMENTS(applicaion_icon_names)), false}; - auto fmicon = Fm::IconInfo::fromGIcon(std::move(gicon)); - applicationsItem = new PlacesModelItem(fmicon, tr("Applications"), Fm::FilePath::fromUri("menu:///applications/")); - placesRoot->appendRow(applicationsItem); + { // Applications + const char* applicaion_icon_names[] = {"system-software-install", "applications-accessories", "application-x-executable"}; + // NOTE: g_themed_icon_new_from_names() accepts char**, but actually const char** is OK. + Fm::GIconPtr gicon{g_themed_icon_new_from_names((char**)applicaion_icon_names, G_N_ELEMENTS(applicaion_icon_names)), false}; + auto fmicon = Fm::IconInfo::fromGIcon(std::move(gicon)); + applicationsItem = new PlacesModelItem(fmicon, tr("Applications"), Fm::FilePath::fromUri("menu:///applications/")); + placesRoot->appendRow(applicationsItem); + } - // FIXME: add an option to hide network:/// - if(true) { + { // Network const char* network_icon_names[] = {"network", "folder-network", "folder"}; // NOTE: g_themed_icon_new_from_names() accepts char**, but actually const char** is OK. Fm::GIconPtr gicon{g_themed_icon_new_from_names((char**)network_icon_names, G_N_ELEMENTS(network_icon_names)), false}; @@ -81,9 +74,6 @@ PlacesModel::PlacesModel(QObject* parent): networkItem = new PlacesModelItem(fmicon, tr("Network"), Fm::FilePath::fromUri("network:///")); placesRoot->appendRow(networkItem); } - else { - networkItem = nullptr; - } devicesRoot = new QStandardItem(tr("Devices")); devicesRoot->setSelectable(false); @@ -171,7 +161,7 @@ PlacesModel::~PlacesModel() { g_object_unref(trashMonitor_); } - Q_FOREACH(GMount* mount, shadowedMounts_) { + for(GMount* const mount : qAsConst(shadowedMounts_)) { g_object_unref(mount); } } @@ -429,6 +419,18 @@ void PlacesModel::onMountRemoved(GVolumeMonitor* monitor, GMount* mount, PlacesM } void PlacesModel::onVolumeAdded(GVolumeMonitor* /*monitor*/, GVolume* volume, PlacesModel* pThis) { + // the item may have been added with "mount-added" (as in loopback mounting) + bool itemExists = false; + GMount* mount = g_volume_get_mount(volume); + if(mount) { + if(pThis->itemFromMount(mount)) { + itemExists = true; + } + g_object_unref(mount); + } + if(itemExists) { + return; + } // for some unknown reasons, sometimes we get repeated volume-added // signals and added a device more than one. So, make a sanity check here. PlacesModelVolumeItem* volumeItem = pThis->itemFromVolume(volume); diff --git a/src/placesmodelitem.cpp b/src/placesmodelitem.cpp index cc1b2e3..f204223 100644 --- a/src/placesmodelitem.cpp +++ b/src/placesmodelitem.cpp @@ -19,7 +19,6 @@ #include "placesmodelitem.h" -#include "icontheme.h" #include #include @@ -87,7 +86,7 @@ QVariant PlacesModelItem::data(int role) const { } PlacesModelBookmarkItem::PlacesModelBookmarkItem(std::shared_ptr bm_item): - PlacesModelItem{Fm::IconInfo::fromName("folder"), bm_item->name(), bm_item->path()}, + PlacesModelItem{bm_item->icon(), bm_item->name(), bm_item->path()}, bookmarkItem_{std::move(bm_item)} { setEditable(true); } diff --git a/src/placesview.cpp b/src/placesview.cpp index 8fa3609..6c112ef 100644 --- a/src/placesview.cpp +++ b/src/placesview.cpp @@ -32,12 +32,112 @@ namespace Fm { +std::shared_ptr PlacesView::proxyModel_; + +PlacesProxyModel::PlacesProxyModel(QObject* parent) : + QSortFilterProxyModel(parent), + showAll_(false), + hiddenItemsRestored_(false) { +} + +PlacesProxyModel::~PlacesProxyModel() { +} + +void PlacesProxyModel::restoreHiddenItems(const QSet& items) { + // hidden items should be restored only once + if(!hiddenItemsRestored_ && !items.isEmpty()) { + hidden_.clear(); + QSet::const_iterator i = items.constBegin(); + while (i != items.constEnd()) { + if(!(*i).isEmpty()) { + hidden_ << *i; + } + ++i; + } + hiddenItemsRestored_ = true; + invalidateFilter(); + } +} + +void PlacesProxyModel::setHidden(const QString& str, bool hide) { + if(hide) { + if(!str.isEmpty()) { + hidden_ << str; + } + } + else { + hidden_.remove(str); + } + invalidateFilter(); +} + +void PlacesProxyModel::showAll(bool show) { + showAll_ = show; + invalidateFilter(); +} + +bool PlacesProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { + if(showAll_ || hidden_.isEmpty()) { + return true; + } + if(PlacesModel* srcModel = static_cast(sourceModel())) { + QModelIndex index = srcModel->index(source_row, 0, source_parent); + if(PlacesModelItem* item = static_cast(srcModel->itemFromIndex(index))) { + if(item->type() == PlacesModelItem::Places) { + if(auto path = item->path()) { + if(hidden_.contains(path.toString().get())) { + return false; + } + } + } + else if(item->type() == PlacesModelItem::Volume) { + CStrPtr uuid{g_volume_get_uuid(static_cast(item)->volume())}; + if(uuid && hidden_.contains(uuid.get())) { + return false; + } + } + // show a root items only if, at least, one of its children is shown + else if((source_row == 0 || source_row == 1) && !source_parent.isValid()) { + QModelIndex indx = index.child(0, 0); + while(PlacesModelItem* childItem = static_cast(srcModel->itemFromIndex(indx))) { + if(childItem->type() == PlacesModelItem::Places) { + if(auto path = childItem->path()) { + if(!hidden_.contains(path.toString().get())) { + return true; + } + } + } + else if(childItem->type() == PlacesModelItem::Volume) { + CStrPtr uuid{g_volume_get_uuid(static_cast(childItem)->volume())}; + if(uuid == nullptr || !hidden_.contains(uuid.get())) { + return true; + } + } + else { + return true; + } + indx = indx.sibling(indx.row() + 1, 0); + } + return false; + } + } + } + return true; +} + PlacesView::PlacesView(QWidget* parent): QTreeView(parent) { setRootIsDecorated(false); setHeaderHidden(true); setIndentation(12); + /* merge with the surroundings */ + setFrameShape(QFrame::NoFrame); + QPalette p = palette(); + p.setColor(QPalette::Base, QColor(Qt::transparent)); + setPalette(p); + viewport()->setAutoFillBackground(false); + connect(this, &QTreeView::clicked, this, &PlacesView::onClicked); connect(this, &QTreeView::pressed, this, &PlacesView::onPressed); @@ -49,7 +149,27 @@ PlacesView::PlacesView(QWidget* parent): setItemDelegateForColumn(0, delegate); model_ = PlacesModel::globalInstance(); - setModel(model_.get()); + if(!proxyModel_) { + proxyModel_ = std::make_shared(); + } + if(!proxyModel_->sourceModel()) { // all places-views may have been closed + proxyModel_->setSourceModel(model_.get()); + } + setModel(proxyModel_.get()); + + // these 2 connections are needed to update filtering + connect(model_.get(), &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex&, int, int) { + proxyModel_->setHidden(QString()); // just invalidates filter + expandAll(); + // for some reason (a Qt bug?), spanning is reset + setFirstColumnSpanned(0, QModelIndex(), true); + setFirstColumnSpanned(1, QModelIndex(), true); + setFirstColumnSpanned(2, QModelIndex(), true); + + }); + connect(model_.get(), &QAbstractItemModel::rowsRemoved, this, [this](const QModelIndex&, int, int) { + proxyModel_->setHidden(QString()); + }); QHeaderView* headerView = header(); headerView->setSectionResizeMode(0, QHeaderView::Stretch); @@ -84,7 +204,7 @@ void PlacesView::activateRow(int type, const QModelIndex& index) { if(!index.parent().isValid()) { // ignore root items return; } - PlacesModelItem* item = static_cast(model_->itemFromIndex(index)); + PlacesModelItem* item = static_cast(model_->itemFromIndex(proxyModel_->mapToSource(index))); if(item) { auto path = item->path(); if(!path) { @@ -155,10 +275,10 @@ void PlacesView::onClicked(const QModelIndex& index) { activateRow(0, index); } else if(index.column() == 1) { // column 1 contains eject buttons of the mounted devices - if(index.parent() == model_->devicesRoot->index()) { // this is a mounted device + if(index.parent() == proxyModel_->mapFromSource(model_->devicesRoot->index())) { // this is a mounted device // the eject button is clicked QModelIndex itemIndex = index.sibling(index.row(), 0); // the real item is at column 0 - PlacesModelItem* item = static_cast(model_->itemFromIndex(itemIndex)); + PlacesModelItem* item = static_cast(model_->itemFromIndex(proxyModel_->mapToSource(itemIndex))); if(item) { // eject the volume or the mount onEjectButtonClicked(item); @@ -176,7 +296,7 @@ void PlacesView::setCurrentPath(Fm::FilePath path) { // TODO: search for item with the path in model_ and select it. PlacesModelItem* item = model_->itemFromPath(currentPath_); if(item) { - selectionModel()->select(item->index(), QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); + selectionModel()->select(proxyModel_->mapFromSource(item->index()), QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); } else { clearSelection(); @@ -252,7 +372,7 @@ void PlacesView::onDeleteBookmark() { // virtual void PlacesView::commitData(QWidget* editor) { QTreeView::commitData(editor); - PlacesModelBookmarkItem* item = static_cast(model_->itemFromIndex(currentIndex())); + PlacesModelBookmarkItem* item = static_cast(model_->itemFromIndex(proxyModel_->mapToSource(currentIndex()))); auto bookmarkItem = item->bookmark(); // rename bookmark Fm::Bookmarks::globalInstance()->rename(bookmarkItem, item->text()); @@ -287,8 +407,8 @@ void PlacesView::onRenameBookmark() { } PlacesModelBookmarkItem* item = static_cast(model_->itemFromIndex(action->index())); setFocus(); - setCurrentIndex(item->index()); - edit(item->index()); + setCurrentIndex(proxyModel_->mapFromSource(item->index())); + edit(proxyModel_->mapFromSource(item->index())); } void PlacesView::onMountVolume() { @@ -338,21 +458,22 @@ void PlacesView::onEjectVolume() { void PlacesView::contextMenuEvent(QContextMenuEvent* event) { QModelIndex index = indexAt(event->pos()); - if(index.isValid() && index.parent().isValid()) { + if(index.isValid()) { if(index.column() != 0) { // the real item is at column 0 index = index.sibling(index.row(), 0); } // Do not take the ownership of the menu since // it will be deleted with deleteLater() upon hidden. - // This is possibly related to #145 - https://github.com/lxde/pcmanfm-qt/issues/145 + // This is possibly related to #145 - https://github.com/lxqt/pcmanfm-qt/issues/145 QMenu* menu = new QMenu(); - QAction* action; - PlacesModelItem* item = static_cast(model_->itemFromIndex(index)); + QAction* action = nullptr; + PlacesModelItem* item = static_cast(model_->itemFromIndex(proxyModel_->mapToSource(index))); - if(item->type() != PlacesModelItem::Mount - && (item->type() != PlacesModelItem::Volume - || static_cast(item)->isMounted())) { + if(index.parent().isValid() + && item->type() != PlacesModelItem::Mount + && (item->type() != PlacesModelItem::Volume + || static_cast(item)->isMounted())) { action = new PlacesModel::ItemAction(item->index(), tr("Open in New Tab"), menu); connect(action, &QAction::triggered, this, &PlacesView::onOpenNewTab); menu->addAction(action); @@ -364,11 +485,40 @@ void PlacesView::contextMenuEvent(QContextMenuEvent* event) { switch(item->type()) { case PlacesModelItem::Places: { auto path = item->path(); - auto path_str = path.toString(); // FIXME: inefficient - if(path && strcmp(path_str.get(), "trash:///") == 0) { - action = new PlacesModel::ItemAction(item->index(), tr("Empty Trash"), menu); - connect(action, &QAction::triggered, this, &PlacesView::onEmptyTrash); + if(path) { + auto path_str = path.toString(); + if(strcmp(path_str.get(), "trash:///") == 0) { + action = new PlacesModel::ItemAction(item->index(), tr("Empty Trash"), menu); + auto icn = item->icon(); + if(icn && icn->qicon().name() == QLatin1String("user-trash")) { // surely an empty trash + action->setEnabled(false); + } + else { + connect(action, &QAction::triggered, this, &PlacesView::onEmptyTrash); + } + // add the "Empty Trash" item on the top + QList actions = menu->actions(); + if(!actions.isEmpty()) { + menu->insertAction(actions.at(0), action); + menu->insertSeparator(actions.at(0)); + } + else { // impossible + menu->addAction(action); + } + } + // add a "Hide" action to the end + menu->addSeparator(); + action = new PlacesModel::ItemAction(item->index(), tr("Hide"), menu); + QString pathStr(path_str.get()); + action->setCheckable(true); + if(proxyModel_->isShowingAll()) { + action->setChecked(proxyModel_->isHidden(pathStr)); + } + connect(action, &QAction::triggered, [this, pathStr](bool checked) { + proxyModel_->setHidden(pathStr, checked); + Q_EMIT hiddenItemSet(pathStr, checked); + }); menu->addAction(action); } break; @@ -411,6 +561,22 @@ void PlacesView::contextMenuEvent(QContextMenuEvent* event) { connect(action, &QAction::triggered, this, &PlacesView::onEjectVolume); menu->addAction(action); } + // add a "Hide" action to the end + CStrPtr uuid{g_volume_get_uuid(static_cast(item)->volume())}; + if(uuid) { + QString str = uuid.get(); + menu->addSeparator(); + action = new PlacesModel::ItemAction(item->index(), tr("Hide"), menu); + action->setCheckable(true); + if(proxyModel_->isShowingAll()) { + action->setChecked(proxyModel_->isHidden(str)); + } + connect(action, &QAction::triggered, [this, str](bool checked) { + proxyModel_->setHidden(str, checked); + Q_EMIT hiddenItemSet(str, checked); + }); + menu->addAction(action); + } break; } case PlacesModelItem::Mount: { @@ -420,6 +586,21 @@ void PlacesView::contextMenuEvent(QContextMenuEvent* event) { break; } } + + // also add an acton for showing all hidden items + if(proxyModel_->hasHidden()) { + if(item->type() == PlacesModelItem::Bookmark) { + menu->addSeparator(); + } + action = new PlacesModel::ItemAction(item->index(), tr("Show All Entries"), menu); + action->setCheckable(true); + action->setChecked(proxyModel_->isShowingAll()); + connect(action, &QAction::triggered, [this](bool checked) { + showAll(checked); + }); + menu->addAction(action); + } + if(menu->actions().size()) { menu->popup(mapToGlobal(event->pos())); connect(menu, &QMenu::aboutToHide, menu, &QMenu::deleteLater); @@ -430,5 +611,19 @@ void PlacesView::contextMenuEvent(QContextMenuEvent* event) { } } +void PlacesView::restoreHiddenItems(const QSet& items) { + proxyModel_->restoreHiddenItems(items); +} + +void PlacesView::showAll(bool show) { + proxyModel_->showAll(show); + if(show) { + expandAll(); + // for some reason (a Qt bug?), spanning is reset + setFirstColumnSpanned(0, QModelIndex(), true); + setFirstColumnSpanned(1, QModelIndex(), true); + setFirstColumnSpanned(2, QModelIndex(), true); + } +} } // namespace Fm diff --git a/src/placesview.h b/src/placesview.h index ccb7fa9..335c55e 100644 --- a/src/placesview.h +++ b/src/placesview.h @@ -23,6 +23,7 @@ #include "libfmqtglobals.h" #include +#include #include #include @@ -32,6 +33,40 @@ namespace Fm { class PlacesModel; class PlacesModelItem; +class PlacesView; + +class LIBFM_QT_API PlacesProxyModel : public QSortFilterProxyModel { + Q_OBJECT +public: + explicit PlacesProxyModel(QObject* parent = 0); + virtual ~PlacesProxyModel(); + + void setHidden(const QString& str, bool hide = true); + + void restoreHiddenItems(const QSet& items); + + void showAll(bool show); + + bool isShowingAll() const { + return showAll_; + } + + bool isHidden(const QString& str) const { + return hidden_.contains(str); + } + + bool hasHidden() const { + return !hidden_.isEmpty(); + } + +protected: + bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const; + +private: + QSet hidden_; + bool showAll_; + bool hiddenItemsRestored_; +}; class LIBFM_QT_API PlacesView : public QTreeView { Q_OBJECT @@ -59,8 +94,11 @@ public: } #endif + void restoreHiddenItems(const QSet& items); + Q_SIGNALS: void chdirRequested(int type, const Fm::FilePath& path); + void hiddenItemSet(const QString& str, bool hide); protected Q_SLOTS: void onClicked(const QModelIndex& index); @@ -97,10 +135,13 @@ protected: private: void onEjectButtonClicked(PlacesModelItem* item); void activateRow(int type, const QModelIndex& index); + void showAll(bool show); private: std::shared_ptr model_; Fm::FilePath currentPath_; + + static std::shared_ptr proxyModel_; // used to hide items in all views }; } diff --git a/src/proxyfoldermodel.cpp b/src/proxyfoldermodel.cpp index 106e846..21fba56 100644 --- a/src/proxyfoldermodel.cpp +++ b/src/proxyfoldermodel.cpp @@ -27,6 +27,7 @@ namespace Fm { ProxyFolderModel::ProxyFolderModel(QObject* parent): QSortFilterProxyModel(parent), showHidden_(false), + backupAsHidden_(true), folderFirst_(true), showThumbnails_(false), thumbnailSize_(0) { @@ -38,8 +39,6 @@ ProxyFolderModel::ProxyFolderModel(QObject* parent): } ProxyFolderModel::~ProxyFolderModel() { - qDebug("delete ProxyFolderModel"); - if(showThumbnails_ && thumbnailSize_ != 0) { FolderModel* srcModel = static_cast(sourceModel()); // tell the source model that we don't need the thumnails anymore @@ -90,6 +89,14 @@ void ProxyFolderModel::setShowHidden(bool show) { } } +void ProxyFolderModel::setBackupAsHidden(bool backupAsHidden) { + if(backupAsHidden != backupAsHidden_) { + backupAsHidden_ = backupAsHidden; + invalidateFilter(); + Q_EMIT sortFilterChanged(); + } +} + // need to call invalidateFilter() manually. void ProxyFolderModel::setFolderFirst(bool folderFirst) { if(folderFirst != folderFirst_) { @@ -108,18 +115,20 @@ void ProxyFolderModel::setSortCaseSensitivity(Qt::CaseSensitivity cs) { bool ProxyFolderModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { if(!showHidden_) { - QAbstractItemModel* srcModel = sourceModel(); - QString name = srcModel->data(srcModel->index(source_row, 0, source_parent)).toString(); - if(name.startsWith(".") || name.endsWith("~")) { - return false; + if(FolderModel* srcModel = static_cast(sourceModel())) { + auto info = srcModel->fileInfoFromIndex(srcModel->index(source_row, 0, source_parent)); + if(info && (info->isHidden() || (backupAsHidden_ && info->isBackup()))) { + return false; + } } } // apply additional filters if there're any - Q_FOREACH(ProxyFolderModelFilter* filter, filters_) { - FolderModel* srcModel = static_cast(sourceModel()); - auto fileInfo = srcModel->fileInfoFromIndex(srcModel->index(source_row, 0, source_parent)); - if(!filter->filterAcceptsRow(this, fileInfo)) { - return false; + for(ProxyFolderModelFilter* const filter : qAsConst(filters_)) { + if(FolderModel* srcModel = static_cast(sourceModel())){ + auto fileInfo = srcModel->fileInfoFromIndex(srcModel->index(source_row, 0, source_parent)); + if(!filter->filterAcceptsRow(this, fileInfo)) { + return false; + } } } return true; @@ -140,17 +149,26 @@ bool ProxyFolderModel::lessThan(const QModelIndex& left, const QModelIndex& righ } } + int comp; switch(sortColumn()) { case FolderModel::ColumnFileMTime: - return leftInfo->mtime() < rightInfo->mtime(); + comp = leftInfo->mtime() - rightInfo->mtime(); + break; case FolderModel::ColumnFileSize: - return leftInfo->size() < rightInfo->size(); + comp = leftInfo->size() - rightInfo->size(); + break; default: { QString leftText = left.data(Qt::DisplayRole).toString(); QString rightText = right.data(Qt::DisplayRole).toString(); - return collator_.compare(leftText, rightText) < 0; + comp = collator_.compare(leftText, rightText); + break; + } } + // always sort files by their display names when they have the same property + if(comp == 0) { + return collator_.compare(leftInfo->displayName(), rightInfo->displayName()) < 0; } + return comp < 0; } return QSortFilterProxyModel::lessThan(left, right); } diff --git a/src/proxyfoldermodel.h b/src/proxyfoldermodel.h index 9e29239..8949f5f 100644 --- a/src/proxyfoldermodel.h +++ b/src/proxyfoldermodel.h @@ -57,6 +57,11 @@ public: return showHidden_; } + void setBackupAsHidden(bool backupAsHidden); + bool backupAsHidden() const { + return backupAsHidden_; + } + void setFolderFirst(bool folderFirst); bool folderFirst() { return folderFirst_; @@ -103,6 +108,7 @@ protected: private: QCollator collator_; bool showHidden_; + bool backupAsHidden_; bool folderFirst_; bool showThumbnails_; int thumbnailSize_; diff --git a/src/renamedialog.cpp b/src/renamedialog.cpp index 4dc17b8..a91ad6d 100644 --- a/src/renamedialog.cpp +++ b/src/renamedialog.cpp @@ -22,11 +22,14 @@ #include "ui_rename-dialog.h" #include #include +#include + #include "core/iconinfo.h" +#include "utilities.h" namespace Fm { -RenameDialog::RenameDialog(FmFileInfo* src, FmFileInfo* dest, QWidget* parent, Qt::WindowFlags f): +RenameDialog::RenameDialog(const FileInfo &src, const FileInfo &dest, QWidget* parent, Qt::WindowFlags f): QDialog(parent, f), action_(ActionIgnore), applyToAll_(false) { @@ -34,54 +37,57 @@ RenameDialog::RenameDialog(FmFileInfo* src, FmFileInfo* dest, QWidget* parent, Q ui = new Ui::RenameDialog(); ui->setupUi(this); - FmPath* path = fm_file_info_get_path(dest); - FmIcon* srcIcon = fm_file_info_get_icon(src); - FmIcon* destIcon = fm_file_info_get_icon(dest); + auto path = dest.path(); + auto srcIcon = src.icon(); + auto destIcon = dest.icon(); // show info for the source file - QIcon icon = Fm::IconInfo::fromGIcon(G_ICON(srcIcon))->qicon(); + QIcon icon = srcIcon->qicon(); + // FIXME: deprecate fm_config QSize iconSize(fm_config->big_icon_size, fm_config->big_icon_size); QPixmap pixmap = icon.pixmap(iconSize); ui->srcIcon->setPixmap(pixmap); QString infoStr; - const char* disp_size = fm_file_info_get_disp_size(src); - if(disp_size) { + // FIXME: deprecate fm_config + auto disp_size = Fm::formatFileSize(src.size(), fm_config->si_unit); + auto srcMtime = QDateTime::fromMSecsSinceEpoch(src.mtime() * 1000).toString(Qt::SystemLocaleShortDate); + if(!disp_size.isEmpty()) { infoStr = QString(tr("Type: %1\nSize: %2\nModified: %3")) - .arg(QString::fromUtf8(fm_file_info_get_desc(src))) - .arg(QString::fromUtf8(disp_size)) - .arg(QString::fromUtf8(fm_file_info_get_disp_mtime(src))); + .arg(src.description()) + .arg(disp_size) + .arg(srcMtime); } else { infoStr = QString(tr("Type: %1\nModified: %2")) - .arg(QString::fromUtf8(fm_file_info_get_desc(src))) - .arg(QString::fromUtf8(fm_file_info_get_disp_mtime(src))); + .arg(src.description()) + .arg(srcMtime); } ui->srcInfo->setText(infoStr); // show info for the dest file - icon = Fm::IconInfo::fromGIcon(G_ICON(destIcon))->qicon(); + icon = destIcon->qicon(); pixmap = icon.pixmap(iconSize); ui->destIcon->setPixmap(pixmap); - disp_size = fm_file_info_get_disp_size(dest); - if(disp_size) { + disp_size = Fm::formatFileSize(dest.size(), fm_config->si_unit); + auto destMtime = QDateTime::fromMSecsSinceEpoch(dest.mtime() * 1000).toString(Qt::SystemLocaleShortDate); + if(!disp_size.isEmpty()) { infoStr = QString(tr("Type: %1\nSize: %2\nModified: %3")) - .arg(QString::fromUtf8(fm_file_info_get_desc(dest))) - .arg(QString::fromUtf8(disp_size)) - .arg(QString::fromUtf8(fm_file_info_get_disp_mtime(dest))); + .arg(dest.description()) + .arg(disp_size) + .arg(destMtime); } else { infoStr = QString(tr("Type: %1\nModified: %2")) - .arg(QString::fromUtf8(fm_file_info_get_desc(dest))) - .arg(QString::fromUtf8(fm_file_info_get_disp_mtime(dest))); + .arg(dest.description()) + .arg(destMtime); } ui->destInfo->setText(infoStr); - char* basename = fm_path_display_basename(path); - ui->fileName->setText(QString::fromUtf8(basename)); - oldName_ = basename; - g_free(basename); + auto basename = path.baseName(); + ui->fileName->setText(QString::fromUtf8(basename.get())); + oldName_ = basename.get(); connect(ui->fileName, &QLineEdit::textChanged, this, &RenameDialog::onFileNameChanged); // add "Rename" button diff --git a/src/renamedialog.h b/src/renamedialog.h index 994c21a..b893177 100644 --- a/src/renamedialog.h +++ b/src/renamedialog.h @@ -25,6 +25,8 @@ #include #include +#include "core/fileinfo.h" + namespace Ui { class RenameDialog; } @@ -45,7 +47,7 @@ public: }; public: - explicit RenameDialog(FmFileInfo* src, FmFileInfo* dest, QWidget* parent = 0, Qt::WindowFlags f = 0); + explicit RenameDialog(const FileInfo &src, const FileInfo &dest, QWidget* parent = 0, Qt::WindowFlags f = 0); virtual ~RenameDialog(); Action action() { diff --git a/src/sidepane.cpp b/src/sidepane.cpp index 717594f..60fccb1 100644 --- a/src/sidepane.cpp +++ b/src/sidepane.cpp @@ -25,7 +25,6 @@ #include "placesview.h" #include "dirtreeview.h" #include "dirtreemodel.h" -#include "path.h" #include "filemenu.h" namespace Fm { @@ -64,6 +63,7 @@ void SidePane::setIconSize(QSize size) { switch(mode_) { case ModePlaces: static_cast(view_)->setIconSize(size); + /* Falls through. */ case ModeDirTree: static_cast(view_)->setIconSize(size); break; @@ -159,9 +159,11 @@ void SidePane::setMode(Mode mode) { case ModePlaces: { PlacesView* placesView = new Fm::PlacesView(this); view_ = placesView; + placesView->restoreHiddenItems(restorableHiddenPlaces_); placesView->setIconSize(iconSize_); placesView->setCurrentPath(currentPath_); connect(placesView, &PlacesView::chdirRequested, this, &SidePane::chdirRequested); + connect(placesView, &PlacesView::hiddenItemSet, this, &SidePane::hiddenPlaceSet); break; } case ModeDirTree: { @@ -208,4 +210,13 @@ void SidePane::setShowHidden(bool show_hidden) { } } +void SidePane::restoreHiddenPlaces(const QSet& items) { + if(mode_ == ModePlaces) { + static_cast(view_)->restoreHiddenItems(items); + } + else { + restorableHiddenPlaces_.unite(items); + } +} + } // namespace Fm diff --git a/src/sidepane.h b/src/sidepane.h index 04215ca..c2b5201 100644 --- a/src/sidepane.h +++ b/src/sidepane.h @@ -98,6 +98,8 @@ public: setCurrentPath(std::move(path)); } + void restoreHiddenPlaces(const QSet& items); + Q_SIGNALS: void chdirRequested(int type, const Fm::FilePath& path); void openFolderInNewWindowRequested(const Fm::FilePath& path); @@ -108,6 +110,8 @@ Q_SIGNALS: void prepareFileMenu(Fm::FileMenu* menu); // emit before showing a Fm::FileMenu + void hiddenPlaceSet(const QString& str, bool hide); + protected Q_SLOTS: void onComboCurrentIndexChanged(int current); @@ -122,6 +126,7 @@ private: QSize iconSize_; Mode mode_; bool showHidden_; + QSet restorableHiddenPlaces_; }; } diff --git a/src/templates.h b/src/templates.h deleted file mode 100644 index bb7fbed..0000000 --- a/src/templates.h +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright (C) 2016 Hong Jen Yee (PCMan) - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * - */ - -#ifndef __LIBFM_QT_FM_TEMPLATES_H__ -#define __LIBFM_QT_FM_TEMPLATES_H__ - -#include -#include -#include -#include "libfmqtglobals.h" - - -namespace Fm { - - -class LIBFM_QT_API Template { -public: - - - // default constructor - Template() { - dataPtr_ = nullptr; - } - - - Template(FmTemplate* dataPtr){ - dataPtr_ = dataPtr != nullptr ? reinterpret_cast(g_object_ref(dataPtr)) : nullptr; - } - - - // copy constructor - Template(const Template& other) { - dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast(g_object_ref(other.dataPtr_)) : nullptr; - } - - - // move constructor - Template(Template&& other) noexcept { - dataPtr_ = reinterpret_cast(other.takeDataPtr()); - } - - - // destructor - virtual ~Template() { - if(dataPtr_ != nullptr) { - g_object_unref(dataPtr_); - } - } - - - // create a wrapper for the data pointer without increasing the reference count - static Template wrapPtr(FmTemplate* dataPtr) { - Template obj; - obj.dataPtr_ = reinterpret_cast(dataPtr); - return obj; - } - - // disown the managed data pointer - FmTemplate* takeDataPtr() { - FmTemplate* data = reinterpret_cast(dataPtr_); - dataPtr_ = nullptr; - return data; - } - - // get the raw pointer wrapped - FmTemplate* dataPtr() { - return reinterpret_cast(dataPtr_); - } - - // automatic type casting - operator FmTemplate*() { - return dataPtr(); - } - - // automatic type casting - operator void*() { - return dataPtr(); - } - - - // copy assignment - Template& operator=(const Template& other) { - if(dataPtr_ != nullptr) { - g_object_unref(dataPtr_); - } - dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast(g_object_ref(other.dataPtr_)) : nullptr; - return *this; - } - - - // move assignment - Template& operator=(Template&& other) noexcept { - dataPtr_ = reinterpret_cast(other.takeDataPtr()); - return *this; - } - - bool isNull() { - return (dataPtr_ == nullptr); - } - - // methods - - bool createFile(GFile* path, GError** error, gboolean run_default) { - return fm_template_create_file(dataPtr(), path, error, run_default); - } - - - bool isDirectory(void) { - return fm_template_is_directory(dataPtr()); - } - - - FmIcon* getIcon(void) { - return fm_template_get_icon(dataPtr()); - } - - - FmMimeType* getMimeType(void) { - return fm_template_get_mime_type(dataPtr()); - } - - - static GList* listAll(gboolean user_only) { - return fm_template_list_all(user_only); - } - - - // automatic type casting for GObject - operator GObject*() { - return reinterpret_cast(dataPtr_); - } - - -protected: - GObject* dataPtr_; // data pointer for the underlying C struct - -}; - - -} - -#endif // __LIBFM_QT_FM_TEMPLATES_H__ diff --git a/src/utilities.cpp b/src/utilities.cpp index c4351b2..8820587 100644 --- a/src/utilities.cpp +++ b/src/utilities.cpp @@ -156,7 +156,7 @@ bool isCurrentPidClipboardData(const QMimeData& data) { return !clip_pid.isEmpty() && clip_pid == curr_pid; } -void changeFileName(const Fm::FilePath& filePath, const QString& newName, QWidget* parent) { +bool changeFileName(const Fm::FilePath& filePath, const QString& newName, QWidget* parent, bool showMessage) { auto dest = filePath.parent().child(newName.toLocal8Bit().constData()); Fm::GErrorPtr err; if(!g_file_move(filePath.gfile().get(), dest.gfile().get(), @@ -165,11 +165,15 @@ void changeFileName(const Fm::FilePath& filePath, const QString& newName, QWidge G_FILE_COPY_NOFOLLOW_SYMLINKS), nullptr, /* make this cancellable later. */ nullptr, nullptr, &err)) { - QMessageBox::critical(parent, QObject::tr("Error"), err.message()); + if (showMessage){ + QMessageBox::critical(parent, QObject::tr("Error"), err.message()); + } + return false; } + return true; } -void renameFile(std::shared_ptr file, QWidget* parent) { +bool renameFile(std::shared_ptr file, QWidget* parent) { FilenameDialog dlg(parent); dlg.setWindowTitle(QObject::tr("Rename File")); dlg.setLabelText(QObject::tr("Please enter a new name:")); @@ -182,18 +186,19 @@ void renameFile(std::shared_ptr file, QWidget* parent) { } if(dlg.exec() != QDialog::Accepted) { - return; + return false; // stop multiple renaming } QString new_name = dlg.textValue(); if(new_name == old_name) { - return; + return true; // let multiple renaming continue } changeFileName(file->path(), new_name, parent); + return true; } // templateFile is a file path used as a template of the new file. -void createFileOrFolder(CreateFileType type, Fm::FilePath parentDir, FmTemplate* templ, QWidget* parent) { +void createFileOrFolder(CreateFileType type, FilePath parentDir, const TemplateItem* templ, QWidget* parent) { QString defaultNewName; QString prompt; QString dialogTitle = type == CreateNewFolder ? QObject::tr("Create Folder") @@ -211,9 +216,9 @@ void createFileOrFolder(CreateFileType type, Fm::FilePath parentDir, FmTemplate* break; case CreateWithTemplate: { - FmMimeType* mime = fm_template_get_mime_type(templ); - prompt = QObject::tr("Enter a name for the new %1:").arg(QString::fromUtf8(fm_mime_type_get_desc(mime))); - defaultNewName = QString::fromUtf8(fm_template_get_name(templ, nullptr)); + auto mime = templ->mimeType(); + prompt = QObject::tr("Enter a name for the new %1:").arg(mime->desc()); + defaultNewName = QString::fromStdString(templ->name()); } break; } @@ -245,7 +250,8 @@ _retry: g_file_make_directory(dest.gfile().get(), nullptr, &err); break; case CreateWithTemplate: - fm_template_create_file(templ, dest.gfile().get(), &err, false); + // copy the template file to its destination + FileOperation::copyFile(templ->filePath(), dest, parent); break; } if(err) { @@ -261,7 +267,7 @@ _retry: uid_t uidFromName(QString name) { uid_t ret; if(name.isEmpty()) { - return -1; + return INVALID_UID; } if(name.at(0).digitValue() != -1) { ret = uid_t(name.toUInt()); @@ -269,7 +275,7 @@ uid_t uidFromName(QString name) { else { struct passwd* pw = getpwnam(name.toLatin1()); // FIXME: use getpwnam_r instead later to make it reentrant - ret = pw ? pw->pw_uid : -1; + ret = pw ? pw->pw_uid : INVALID_UID; } return ret; @@ -292,7 +298,7 @@ QString uidToName(uid_t uid) { gid_t gidFromName(QString name) { gid_t ret; if(name.isEmpty()) { - return -1; + return INVALID_GID; } if(name.at(0).digitValue() != -1) { ret = gid_t(name.toUInt()); @@ -300,7 +306,7 @@ gid_t gidFromName(QString name) { else { // FIXME: use getgrnam_r instead later to make it reentrant struct group* grp = getgrnam(name.toLatin1()); - ret = grp ? grp->gr_gid : -1; + ret = grp ? grp->gr_gid : INVALID_GID; } return ret; @@ -332,8 +338,8 @@ int execModelessDialog(QDialog* dlg) { } // check if GVFS can support this uri scheme (lower case) -// NOTE: this does not work reliably due to some problems in gio/gvfs and causes bug lxde/lxqt#512 -// https://github.com/lxde/lxqt/issues/512 +// NOTE: this does not work reliably due to some problems in gio/gvfs and causes bug lxqt/lxqt#512 +// https://github.com/lxqt/lxqt/issues/512 // Use uriExists() whenever possible. bool isUriSchemeSupported(const char* uriScheme) { const gchar* const* schemes = g_vfs_get_supported_uri_schemes(g_vfs_get_default()); diff --git a/src/utilities.h b/src/utilities.h index e6bf715..bf3c90e 100644 --- a/src/utilities.h +++ b/src/utilities.h @@ -32,6 +32,7 @@ #include "core/filepath.h" #include "core/fileinfo.h" +#include "core/templates.h" class QDialog; @@ -53,9 +54,9 @@ LIBFM_QT_API void cutFilesToClipboard(const Fm::FilePathList& files); LIBFM_QT_API bool isCurrentPidClipboardData(const QMimeData& data); -LIBFM_QT_API void changeFileName(const Fm::FilePath& path, const QString& newName, QWidget* parent); +LIBFM_QT_API bool changeFileName(const Fm::FilePath& path, const QString& newName, QWidget* parent, bool showMessage = true); -LIBFM_QT_API void renameFile(std::shared_ptr file, QWidget* parent = 0); +LIBFM_QT_API bool renameFile(std::shared_ptr file, QWidget* parent = 0); enum CreateFileType { CreateNewFolder, @@ -63,12 +64,16 @@ enum CreateFileType { CreateWithTemplate }; -LIBFM_QT_API void createFileOrFolder(CreateFileType type, Fm::FilePath parentDir, FmTemplate* templ = nullptr, QWidget* parent = 0); +LIBFM_QT_API void createFileOrFolder(CreateFileType type, FilePath parentDir, const TemplateItem* templ = nullptr, QWidget* parent = 0); + +constexpr uid_t INVALID_UID = uid_t(-1); LIBFM_QT_API uid_t uidFromName(QString name); LIBFM_QT_API QString uidToName(uid_t uid); +constexpr gid_t INVALID_GID = gid_t(-1); + LIBFM_QT_API gid_t gidFromName(QString name); LIBFM_QT_API QString gidToName(gid_t gid); diff --git a/src/xdndworkaround.cpp b/src/xdndworkaround.cpp index e60feac..f0a35d2 100644 --- a/src/xdndworkaround.cpp +++ b/src/xdndworkaround.cpp @@ -176,7 +176,7 @@ bool XdndWorkaround::clientMessage(xcb_client_message_event_t* event) { // NOTE: Because of the limitation of Qt, this hack is required to provide // Xdnd direct save (XDS) protocol support. - // http://www.freedesktop.org/wiki/Specifications/XDS/#index4h2 + // https://www.freedesktop.org/wiki/Specifications/XDS/#index4h2 // // XDS requires that the drop target should get and set the window property of the // drag source to pass the file path, but in Qt there is NO way to know the diff --git a/src/xdndworkaround.h b/src/xdndworkaround.h index e5a3f51..9d40c11 100644 --- a/src/xdndworkaround.h +++ b/src/xdndworkaround.h @@ -24,7 +24,7 @@ * #49947: Drop events have broken mimeData()->urls() and text/uri-list. * #47981: Qt5.4 regression: Dropping text/urilist over browser windows stop working. * - * Related LXQt bug: https://github.com/lxde/lxqt/issues/688 + * Related LXQt bug: https://github.com/lxqt/lxqt/issues/688 * * This workaround is not 100% reliable, but it should work most of the time. * In theory, when there are multiple drag and drops at nearly the same time and @@ -52,6 +52,7 @@ #include #include #include +#include class QDrag; @@ -77,7 +78,7 @@ private: // _QBasicDrag* xcbDrag() const; void buttonRelease(); - QDrag* lastDrag_; + QPointer lastDrag_; // xinput related bool xinput2Enabled_; int xinputOpCode_;