Adding upstream version 0.12.0.

Signed-off-by: Alf Gaida <agaida@siduction.org>
upstream/0.12.0
Alf Gaida 7 years ago
parent 932a3bcc26
commit b2567a5f34
No known key found for this signature in database
GPG Key ID: CD280A0B4D72827C

@ -3,4 +3,4 @@ Upstream Authors:
Hong Jen Yee (PCMan) <pcman.tw@gmail.com> Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
Copyright: Copyright:
Copyright (c) 2013-2016 LXQt team Copyright (c) 2013-2017 LXQt team

@ -1,7 +1,202 @@
libfm-qt-0.11.2 / 2016-12-21 libfm-qt-0.12.0 / 2017-10-21
============================ ============================
* Add data transferred to file operation dialog.
* Bump versions
* Disable context-menu actions that cannot be used
* Don't export github templates
* Fix partially visible toggled path buttons
* Add functions to get and set search settings
* Fix mistakes in listview column width calculation
* Add archiver separator only when needed
* Add a separator before archiver actions
* Enable XDS subfolder drop
* UI improvements for Fm::MountOperationPasswordDialog()
* Respect inactiveness when drawing text
* Grey out files that have been Ctrl-X'ed (#88)
* Ignore button for error dialog
* Inline renaming for detailed list view (#110)
* Remove redundant code.
* Prefer local paths if they exist
* Removed QFileInfo (as @PCMan recommended)
* Simplification, optimization and a fix
* Really focus text entry on showing dialog
* Two small fixes
* Keep selection on reloading (if not CPU-intensive)
* Added back/forward buttons and fixed 3 issues
* Reload button, hidden shortcut and a fix
* Implement FileDialog::selectMimeTypeFilter() and QString FileDialog::selectedMimeTypeFilter().
* Initialize folder_ to null
* Fixed the quote issue
* Always preserve explicitly set labels
* Update OK button text and state when needed
* Initialize FileInfo::isShortcut_ (#113)
* Set the selected index current
* Fixd open/save and overwrite prompt
* Set open/save text
* Several important fixes
* Added a missing change
* Preliminary fixes
* Hide UI implementation details for Fm::FileDialog.
* Revert the backward incompatible changes in the constructor of Fm::FolderView.
* Fix a bug in creating new folder for Fm::FileDialog.
* Implement toolbar and quick view mode switches for the Fm::FileDialog class.
* Correctly check file types and test the existence of the selected files as needed.
* Correctly handle item activation.
* Correctly handle filename selection for Fm::FileDialog.
* Correctly handle selected files.
* Implement filename filtering for Fm::FileDialog.
* Check nullity of FileInfo before calling FolderMenu
* Arrange Custom Actions
* Support custom folder icons
* Fix multiple pasting of the same cut file(s)
* Fix KDE clipboard tag for cut file(s)
* Add a basic skeleton for Fm::FileDialog.
* Check nullity of QMimeData (#109)
* MountOperationQuestionDialog: Fix handling responses
* Fix all height issues in horizontal layouts (#103)
* Removed a redundant variable (left there from old tests)
* Fix major bugs in Directory Tree
* Consider desktop text color, now that everything is done here
* Inline Renaming
* Fix compact view regression (#102)
* Fix detailed list crash on double clicking folders
* Removed my garbage
* Fixed issues about spacings and click region
* Make Fm::FolderItemDelegate support painting text shadows and margins so it can completely replace PCManFM::DesktopItemDelegate.
* Avoid using grid size on QListView since this disables any spacing settings.
* liblxqt make no sense for libfm-qt
* Copied issue template
* Add noexcept to move constructors and operator=() so they can be used with STL containers.
* FolderView: Optimize selectAll() (#97)
* Emit fileSizeChanged when needed
* Drops Qt5Core_VERSION_STRING (#96)
* Update size column info (#90)
* Fix Detailed List view DND (#94)
* folderview: Don't allow D&D by Back or Forward
* More fixes (#87)
* Added a missing change signal (#86)
* Fix single items when seaching (#85)
* Check for nullity of IconInfo (#84)
* Address compiler warnings
* Remove addRoots() return type
* Remove the unused data/translations/ entry
* Fix broken folder unmount message caused by incorrect FilePath & GFile* comparison. (#80)
* Remove some superfluous semicolons that lead to pedantic warnings (#79)
* Ensure one item per file (#77)
* Fix the broken filesystem status (free disk space) display. (#78)
* Don't make items current on mouseover (#72)
* Fixes a FTBFS in superbuild mode (#75)
* Replace start tilde in PathEdit (#73)
* Really cancel pending thumbnail jobs on chdir (#74)
* Move fixes (#70)
* Fix invalid pointers (#69)
* Continue file selection on next/previous row (#76)
* Code reformat: use 4-space indentation to match the coding style of other LXQt components.
* Make all constructors explicit so we don't get unwanted object construction by C++.
* Prevent a crash since GObjectPtr's move ctor frees resources
* GObjectPtr: Detect & handle "self-assignment"
* Fix compatibility with Qt 5.6.
* No thumbnail for thumbnails
* Fix thumbnail update
* Fixed `PathBar::setPath()`
* Use real name for renaming
* Prevent a crash plus fallback icon
* Fix custom actions
* volumemanager: Return IconInfo as shared_ptr
* FolderModelItem: Check IconInfo existence
* Bookmarks: Check validity of insert position
* Fix a potential crash of bookmark items when the format of the bookmark file is not correct.
* Only load desktop entry files for native filesystems.
* Fix the missing icon and display name handling of desktop entry files in Fm::FileInfo.
* IconEngine: Use weak_ptr for the parent IconInfo
* PathBar: Avoid leak upon QLayout::replaceWidget()
* Use const iterators
* Use the new lxqt-build-tools new FindExif module
* Fix the incorrect header inclusion in fileoperation.cpp.
* Fix incorrect #include of libfmqtglobals.h.
* Fix a bug when copying to and pasting from "x-special/gnome-copied-files" mime-type.
* Fix bugs in the Custom Actions C++ port.
* Try to port libfm custom actions to C++.
* Try to update the content of a folder after its mount status is changed. Handle cancelled dir listing job properly.
* Rename namespace Fm2 to Fm.
* Remove unused header files of the old C API wrappers.
* Fix bugs in search:// support and finish porting file searching to C++. Fix several bugs in Fm2::FileInfo which caused by null parent dir.
* Add a missing test case for places view.
* Try to add support for menu:// and search:// URI scheme implemented in libfm.
* Correctly destroy Fm2::Job objects when their worker threads terminate.
* Fix incorrect handling of PathBar button creation which causes infinite loop when the underlying GFile implementation contains bugs.
* Fix incorrect path of application menu URI.
* Fix QThread related bugs in PathEdit which causes memory leaks.
* Fix a bug in DirTreeModelItem causing crashes. Also speed up batch insertion of large amount of items.
* Use const iterators (#61)
* Fix the broken folder reload().
* Make all Fm2::Job derived classes reimplement exec() instead of run() method. The new run() method will emit finished() signal automatically when exec() returns.
* Fix memory leaks caused by incorrect use of QThread.
* Fix a memory leak in Fm::ThumbnailJob.
* Fix memory leaks caused by broken cache.
* Fix wrong size of generated thumbnails by external thumbnailers.
* Fix memory bugs in Fm2::GErrorPtr and improve the error handling of Fm2::Job and Fm2::Folder.
* Fix some errors related to incorrect use of std::remove() in Fm2::Folder. Replace QList with std::vector and use binary search when inserting items for the Fm::DirTreeModelItem.
* Change the handling of Fm::FolderView::selChanged signal to make it more efficient.
* Port to the new Fm2::TotalSizeJob API.
* Fix compatibility with libfm custom actions.
* Add some compatibility API which helps migrating old C APIs to the new C++ implementation.
* Convert datetime to locale-aware strings using QDateTime.
* Use QCollator to perform file sorting.
* Fix detailed view.
* Finish porting DirTreeModel to the new API. Fix bugs in Fm2::FilePath and Fm2::FileInfo APIs.
* Port the libfm terminal emulator related APIs to C++.
* Rename some methods in Fm2::Folder and Fm2::FileInfo for consistency.
* Port to the new IconInfo API and deprecate manual icon update when the theme is changed.
* Rename Fm::Icon to Fm::IconInfo.
* Port emblem support to the new libfm C++ API.
* Remove unused files, including some old APIs. Replace QVector in BrowseHistory with STL vector.
* Fix a bug in Fm::FileMenu.
* Port file-click handling to the new C++ API.
* Fix bugs in Fm::PathBar getting wrong path when a path button is toggled.
* Remove Fm::FilePath(const char* path_str) to avoid ambiguity.
* Replace all NULL with C++ 11 nullptr;
* Fix FilePath related errors caused by incomplete porting.
* Make Fm::FolderConfig support the new Fm::FilePath class.
* Fix Fm::BookmarkAction to use the new C++ API.
* Fix missing icons of places view caused by memory errors.
* Fix memory errors in Fm2::Bookmarks::reorder(). Add a small test case for the places view.
* Share the same places model among all views.
* Port most of the existing UI-related APIs to the new C++ APIs (excluding the file operations).
* Port the path bar to the new Fm2 API.
* Implement VolumeManager class which is a QObject wrapper of gio GVolumeMonitor.
* Add some getters for Volume and Mount classes.
* Properly associate external thumbnailers with mimetypes they support and fix thumbnail generation from thumbnailers.
* Start porting thumbnail loaders to the new C++ APIs. Add new Fm::ThumbnailJob used to load thumbnails for a given file list. Add commonDirPath paramter to Fm::FileInfoJob to reduce memory usage.
* Add the missing test case for folder view.
* Start porting Fm::FolderModel and Fm::FolderView to the new libfm core c++ API.
* Work in progress.
* Add a c++ wrapper for GFileMonitor. Add LIBFM_QT_API declaration for all public headers.
* Port error handling mechanism of FmJob to C++ and improve the GError wrapper class.
* Bump year
* Add gioptrs.h which defines smart pointer types wrapping gio related data types. Add some basic skeleton for various I/O jobs classes.
* Start porting Copyjob and add basic skeleton for untrash and symlink jobs.
* Finish porting FmFolder to C++.
* Add a very simple test case to test the new Fm core C++ code. Fix bugs in smart pointers and do empty base class optimization for CStrPtr.
* Improve Fm::Folder.
* Rename UserInfo to UserInfoCache.
* Port Fm::Bookmarks to C++.
* Port FmDeepCountJob to C++.
* Add basic skeletion to Fm::VolumeManager.
* Implement Fm2::UserInfo, which is a cache for uid/gid name mapping.
* Add basic skeleton for other C++ classes waiting for porting.
* Add GSignalHandler to do automatic signal/slot connection management with type safety for GObject.
* Add basic skeleton for the C++ 11 port of FmFileInfoJob.
* Try to port Fm::Folder and Fm::DirListJob to Qt5 and C++ 11.
* Try to port FmIcon, FmFileInfo, and FmMimeType of libfm to clean C++.
* Add smart pointer for GObject-derived classes and add Fm::FilePath class which wraps GFile.
0.11.2 / 2016-12-21
===================
* Release 0.11.2: Update changelog
* Fix enabled state of path arrows on starting (#58) * Fix enabled state of path arrows on starting (#58)
* bump patch version (#56) * bump patch version (#56)
* Use QByteArray::constData() where we can (#57) * Use QByteArray::constData() where we can (#57)

@ -4,8 +4,8 @@ project(libfm-qt)
set(LIBFM_QT_LIBRARY_NAME "fm-qt" CACHE STRING "fm-qt") set(LIBFM_QT_LIBRARY_NAME "fm-qt" CACHE STRING "fm-qt")
set(LIBFM_QT_VERSION_MAJOR 0) set(LIBFM_QT_VERSION_MAJOR 0)
set(LIBFM_QT_VERSION_MINOR 11) set(LIBFM_QT_VERSION_MINOR 12)
set(LIBFM_QT_VERSION_PATCH 2) set(LIBFM_QT_VERSION_PATCH 0)
set(LIBFM_QT_VERSION ${LIBFM_QT_VERSION_MAJOR}.${LIBFM_QT_VERSION_MINOR}.${LIBFM_QT_VERSION_PATCH}) 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") list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")
@ -24,7 +24,7 @@ set(LIBFM_QT_LIB_SOVERSION "3")
set(REQUIRED_QT_VERSION "5.2") set(REQUIRED_QT_VERSION "5.2")
set(REQUIRED_LIBFM_VERSION "1.2.0") set(REQUIRED_LIBFM_VERSION "1.2.0")
set(REQUIRED_LIBMENUCACHE_VERSION "0.4.0") set(REQUIRED_LIBMENUCACHE_VERSION "0.4.0")
set(REQUIRED_LXQT_BUILD_TOOLS_VERSION "0.3.0") set(REQUIRED_LXQT_BUILD_TOOLS_VERSION "0.4.0")
if (NOT CMAKE_BUILD_TYPE) if (NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release) set(CMAKE_BUILD_TYPE Release)
@ -37,9 +37,10 @@ find_package(Qt5X11Extras "${REQUIRED_QT_VERSION}" REQUIRED)
find_package(lxqt-build-tools "${REQUIRED_LXQT_BUILD_TOOLS_VERSION}" REQUIRED) find_package(lxqt-build-tools "${REQUIRED_LXQT_BUILD_TOOLS_VERSION}" REQUIRED)
find_package(Fm "${REQUIRED_LIBFM_VERSION}" REQUIRED) find_package(Fm "${REQUIRED_LIBFM_VERSION}" REQUIRED)
find_package(MenuCache "${REQUIRED_LIBMENUCACHE_VERSION}" REQUIRED) find_package(MenuCache "${REQUIRED_LIBMENUCACHE_VERSION}" REQUIRED)
find_package(Exif REQUIRED)
find_package(XCB REQUIRED) find_package(XCB REQUIRED)
message(STATUS "Building ${PROJECT_NAME} with Qt ${Qt5Core_VERSION_STRING}") message(STATUS "Building ${PROJECT_NAME} with Qt ${Qt5Core_VERSION}")
option(UPDATE_TRANSLATIONS "Update source translation translations/*.ts files" OFF) option(UPDATE_TRANSLATIONS "Update source translation translations/*.ts files" OFF)
include(GNUInstallDirs) include(GNUInstallDirs)
@ -65,6 +66,7 @@ install(FILES
) )
add_subdirectory(src) add_subdirectory(src)
add_subdirectory(data)
# add Doxygen support to generate API docs # add Doxygen support to generate API docs
# References: # References:

@ -0,0 +1,10 @@
install(FILES
"archivers.list"
"terminals.list"
DESTINATION "${CMAKE_INSTALL_DATADIR}/libfm-qt"
)
install(FILES
"libfm-qt-mimetypes.xml"
DESTINATION "${CMAKE_INSTALL_DATADIR}/mime/packages"
)

@ -0,0 +1,35 @@
[file-roller]
create=file-roller --add %U
extract=file-roller --extract %U
extract_to=file-roller --extract-to %d %U
mime_types=application/x-7z-compressed;application/x-7z-compressed-tar;application/x-ace;application/x-alz;application/x-ar;application/x-arj;application/x-bzip;application/x-bzip-compressed-tar;application/x-bzip1;application/x-bzip1-compressed-tar;application/x-cabinet;application/x-cbr;application/x-cbz;application/x-cd-image;application/x-compress;application/x-compressed-tar;application/x-cpio;application/x-deb;application/x-ear;application/x-ms-dos-executable;application/x-gtar;application/x-gzip;application/x-gzpostscript;application/x-java-archive;application/x-lha;application/x-lhz;application/x-lzip;application/x-lzip-compressed-tar;application/x-lzma;application/x-lzma-compressed-tar;application/x-lzop;application/x-lzop-compressed-tar;application/x-rar;application/x-rar-compressed;application/vnd.rar;application/x-rpm;application/x-rzip;application/x-tar;application/x-tarz;application/x-stuffit;application/x-war;application/x-xz;application/x-xz-compressed-tar;application/x-zip;application/x-zip-compressed;application/x-zoo;application/zip;multipart/x-zip;
supports_uris=true
[xarchiver]
create=xarchiver --add-to %F
extract=xarchiver --extract %F
extract_to=xarchiver --extract-to %d %F
mime_types=application/x-arj;application/arj;application/x-bzip;application/x-bzip-compressed-tar;application/x-gzip;application/x-rar;application/x-rar-compressed;application/vnd.rar;application/x-tar;application/x-zip;application/x-zip-compressed;application/zip;multipart/x-zip;application/x-7z-compressed;application/x-compressed-tar;application/x-bzip2;application/x-bzip2-compressed-tar;application/x-lzma-compressed-tar;application/x-lzma;application/x-deb;application/deb;application/vnd.debian.binary-package;application/x-xz;application/x-xz-compressed-tar;application/x-rpm;application/x-source-rpm;application/x-lzop;application/x-lzop-compressed-tar;application/x-tzo;application/x-war;application/x-compress;application/x-tarz;application/x-java-archive;application/x-lha;application/x-lhz;
[squeeze]
create=squeeze --new %F
extract=squeeze --extract %F
extract_to=squeeze --extract-to %d %F
mime_types=application/x-bzip-compressed-tar;application/x-bzip2-compressed-tar;application/x-compressed-tar;application/x-tar;application/x-tarz;application/x-tzo;application/x-zip;application/x-zip-compressed;application/zip;application/x-rar;application/vnd.rar;application/x-gzip;application/x-bzip;application/x-lzop;application/x-compress;
[engrampa]
create=engrampa --add %U
extract=engrampa --extract %U
extract_to=engrampa --extract-to %d %U
mime_types=application/x-7z-compressed;application/x-7z-compressed-tar;application/x-ace;application/x-alz;application/x-ar;application/x-arj;application/x-bzip;application/x-bzip-compressed-tar;application/x-bzip1;application/x-bzip1-compressed-tar;application/x-cabinet;application/x-cbr;application/x-cbz;application/x-cd-image;application/x-compress;application/x-compressed-tar;application/x-cpio;application/x-deb;application/x-ear;application/x-ms-dos-executable;application/x-gtar;application/x-gzip;application/x-gzpostscript;application/x-java-archive;application/x-lha;application/x-lhz;application/x-lzip;application/x-lzip-compressed-tar;application/x-lzma;application/x-lzma-compressed-tar;application/x-lzop;application/x-lzop-compressed-tar;application/x-rar;application/x-rar-compressed;application/vnd.rar;application/x-rpm;application/x-rzip;application/x-tar;application/x-tarz;application/x-stuffit;application/x-war;application/x-xz;application/x-xz-compressed-tar;application/x-zip;application/x-zip-compressed;application/x-zoo;application/zip;multipart/x-zip;
supports_uris=true
# The KDE archiver Ark
# Here we use %F instead of %U since KDE programs do not know the URI provided by gvfs.
# GIO will pass FUSE-based file paths to the KDE programs, which should still work.
[ark]
create=ark --add --dialog %F
extract=ark --batch --dialog %F
extract_to=ark --batch --destination %d %F
mime_types=application/x-tar;application/x-compressed-tar;application/x-bzip-compressed-tar;application/x-tarz;application/x-xz-compressed-tar;application/x-lzma-compressed-tar;application/x-deb;application/x-cd-image;application/x-bcpio;application/x-cpio;application/x-cpio-compressed;application/x-sv4cpio;application/x-sv4crc;application/x-rpm;application/x-source-rpm;application/vnd.ms-cab-compressed;application/x-servicepack;application/x-rar;application/vnd.rar;application/x-7z-compressed;application/x-java-archive;application/zip;application/x-compress;application/x-gzip;application/x-bzip;application/x-bzip2;application/x-lzma;application/x-xz;application/lha;application/x-lha;application/maclha;
supports_uris=true

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Additional mime-types provided by libfm, adding some
missing but frequently seen globs for some common mime-types.
-->
<mime-info xmlns='http://www.freedesktop.org/standards/shared-mime-info'>
<mime-type type="text/plain">
<glob pattern="*.ini"/>
<glob pattern="*.inf"/>
</mime-type>
<mime-type type="application/x-ms-dos-executable">
<glob pattern="*.com" />
</mime-type>
<mime-type type="application/x-ms-win-installer">
<comment>Windows installer</comment>
<comment xml:lang="zh_TW">Windows 安裝程式</comment>
<glob pattern="*.msi" />
</mime-type>
<mime-type type="application/x-vbscript">
<comment>MS VBScript</comment>
<glob pattern="*.vbs" />
</mime-type>
<mime-type type="text/x-csharp">
<comment xml:lang="en">C# source</comment>
<comment xml:lang="zh_TW">C# 程式碼</comment>
<glob pattern="*.cs"/>
</mime-type>
<mime-type type="application/x-desktop">
<comment xml:lang="zh_TW">應用程式捷徑</comment>
</mime-type>
<mime-type type="application/x-sharedlib">
<!--
This pattern matching is not very accurate ,but the probability that
a file is named like this and it's not a shared lib, is very low.
-->
<glob pattern="*.dll"/> <!-- Windows dll are shared libs, too -->
<glob pattern="*.so.[0-9]" />
<glob pattern="*.so.[0-9].*" />
</mime-type>
<mime-type type="application/x-desktop">
<glob pattern="*.directory"/>
</mime-type>
</mime-info>

@ -0,0 +1,77 @@
[xterm]
open_arg=-e
noclose_arg=-hold -e
desktop_id=xterm.desktop
[uxterm]
open_arg=-e
noclose_arg=-hold -e
[lxterminal]
open_arg=-e
desktop_id=lxterminal.desktop
[konsole]
open_arg=-e
noclose_arg=--noclose -e
desktop_id=konsole.desktop
[xfce4-terminal]
open_arg=-x
noclose_arg=--hold -x
desktop_id=xfce4-terminal.desktop
[terminator]
open_arg=-x
desktop_id=terminator.desktop
[rxvt]
open_arg=-e
[urxvt]
open_arg=-e
noclose_arg=-hold -e
desktop_id=rxvt-unicode.desktop
[eterm]
open_arg=-e
noclose_arg=--pause -e
desktop_id=eterm.desktop
[gnome-terminal]
open_arg=-x
desktop_id=gnome-terminal.desktop
[wterm]
open_arg=-e
[roxterm]
open_arg=-e
desktop_id=roxterm.desktop
[sakura]
open_arg=-e
desktop_id=sakura.desktop
[qterminal]
open_arg=-e
desktop_id=qterminal.desktop
[lilyterm]
open_arg=-e
noclose_arg=--hold -e
desktop_id=lilyterm.desktop
[urxvtc]
open_arg=-e
noclose_arg=-hold -e
[terminology]
open_arg=-e
noclose_arg=--hold -e
desktop_id=terminology.desktop
[termite]
open_arg=-e
noclose_arg=--hold -e
desktop_id=termite.desktop

@ -1,4 +1,41 @@
set(libfm_core_SRCS
# core data structures
core/gobjectptr.h
core/filepath.cpp
core/iconinfo.cpp
core/mimetype.cpp
core/fileinfo.cpp
core/folder.cpp
core/filemonitor.cpp
# i/o jobs
core/job.cpp
core/copyjob.cpp
core/deletejob.cpp
core/dirlistjob.cpp
core/filechangeattrjob.cpp
core/fileinfojob.cpp
core/filelinkjob.cpp
core/fileoperationjob.cpp
core/filesysteminfojob.cpp
core/job.cpp
core/totalsizejob.cpp
core/trashjob.cpp
core/untrashjob.cpp
core/thumbnailjob.cpp
# extra desktop services
core/bookmarks.cpp
core/volumemanager.cpp
core/userinfocache.cpp
core/thumbnailer.cpp
core/terminal.cpp
# custom actions
customactions/fileaction.cpp
customactions/fileactionprofile.cpp
customactions/fileactioncondition.cpp
)
set(libfm_SRCS set(libfm_SRCS
${libfm_core_SRCS}
libfmqt.cpp libfmqt.cpp
bookmarkaction.cpp bookmarkaction.cpp
sidepane.cpp sidepane.cpp
@ -36,12 +73,12 @@ set(libfm_SRCS
utilities.cpp utilities.cpp
dndactionmenu.cpp dndactionmenu.cpp
editbookmarksdialog.cpp editbookmarksdialog.cpp
thumbnailloader.cpp
execfiledialog.cpp execfiledialog.cpp
appchoosercombobox.cpp appchoosercombobox.cpp
appmenuview.cpp appmenuview.cpp
appchooserdialog.cpp appchooserdialog.cpp
filesearchdialog.cpp filesearchdialog.cpp
filedialog.cpp
fm-search.c # might be moved to libfm later fm-search.c # might be moved to libfm later
xdndworkaround.cpp xdndworkaround.cpp
) )
@ -55,6 +92,7 @@ set(libfm_UIS
exec-file.ui exec-file.ui
app-chooser-dialog.ui app-chooser-dialog.ui
filesearch.ui filesearch.ui
filedialog.ui
) )
qt5_wrap_ui(libfm_UIS_H ${libfm_UIS}) qt5_wrap_ui(libfm_UIS_H ${libfm_UIS})
@ -80,11 +118,6 @@ add_library(${LIBFM_QT_LIBRARY_NAME} SHARED
${QM_FILES} ${QM_FILES}
) )
# only turn on custom actions support if it is enabled in libfm.
if(EXISTS "${FM_INCLUDE_DIR}/libfm/fm-actions.h")
target_compile_definitions(${LIBFM_QT_LIBRARY_NAME} PRIVATE CUSTOM_ACTIONS)
endif()
install(EXPORT install(EXPORT
"${LIBFM_QT_LIBRARY_NAME}-targets" "${LIBFM_QT_LIBRARY_NAME}-targets"
DESTINATION "${CMAKE_INSTALL_DATADIR}/cmake/${LIBFM_QT_LIBRARY_NAME}" DESTINATION "${CMAKE_INSTALL_DATADIR}/cmake/${LIBFM_QT_LIBRARY_NAME}"
@ -97,6 +130,7 @@ target_link_libraries(${LIBFM_QT_LIBRARY_NAME}
${FM_LIBRARIES} ${FM_LIBRARIES}
${MENUCACHE_LIBRARIES} ${MENUCACHE_LIBRARIES}
${XCB_LIBRARIES} ${XCB_LIBRARIES}
${EXIF_LIBRARIES}
) )
# set libtool soname # set libtool soname
@ -112,6 +146,7 @@ target_include_directories(${LIBFM_QT_LIBRARY_NAME}
"${FM_INCLUDE_DIR}/libfm" # to workaround incorrect #include in fm-actions. "${FM_INCLUDE_DIR}/libfm" # to workaround incorrect #include in fm-actions.
"${MENUCACHE_INCLUDE_DIRS}" "${MENUCACHE_INCLUDE_DIRS}"
"${XCB_INCLUDE_DIRS}" "${XCB_INCLUDE_DIRS}"
"${EXIF_INCLUDE_DIRS}"
INTERFACE INTERFACE
"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>" "$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
"$<BUILD_INTERFACE:${LIBFM_QT_INTREE_INCLUDE_DIR}>" "$<BUILD_INTERFACE:${LIBFM_QT_INTREE_INCLUDE_DIR}>"
@ -128,11 +163,11 @@ install(FILES
COMPONENT Devel COMPONENT Devel
) )
file(GLOB libfm_HS "${CMAKE_CURRENT_SOURCE_DIR}/*.h")
# install include header files (FIXME: can we make this cleaner? should dir name be versioned?) # install include header files (FIXME: can we make this cleaner? should dir name be versioned?)
install(FILES ${libfm_HS} install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/libfm-qt" DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/libfm-qt"
COMPONENT Devel COMPONENT Devel
FILES_MATCHING PATTERN "*.h"
) )
generate_export_header(${LIBFM_QT_LIBRARY_NAME} generate_export_header(${LIBFM_QT_LIBRARY_NAME}
@ -140,10 +175,14 @@ generate_export_header(${LIBFM_QT_LIBRARY_NAME}
) )
# InTree build # InTree build
file(COPY ${libfm_HS} ${CMAKE_CURRENT_BINARY_DIR}/${LIBFM_QT_LIBRARY_NAME}_export.h file(COPY ${CMAKE_CURRENT_BINARY_DIR}/${LIBFM_QT_LIBRARY_NAME}_export.h
DESTINATION "${LIBFM_QT_INTREE_INCLUDE_DIR}/libfm-qt" DESTINATION "${LIBFM_QT_INTREE_INCLUDE_DIR}/libfm-qt"
) )
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/
DESTINATION "${LIBFM_QT_INTREE_INCLUDE_DIR}/libfm-qt"
FILES_MATCHING PATTERN "*.h"
)
configure_package_config_file( configure_package_config_file(
"${PROJECT_SOURCE_DIR}/cmake/fm-qt-config.cmake.in" "${PROJECT_SOURCE_DIR}/cmake/fm-qt-config.cmake.in"
@ -194,3 +233,37 @@ endif()
# prevent the generated files from being deleted during make cleaner # prevent the generated files from being deleted during make cleaner
set_directory_properties(PROPERTIES CLEAN_NO_CUSTOM true) set_directory_properties(PROPERTIES CLEAN_NO_CUSTOM true)
set(TEST_LIBRARIES
Qt5::Core
Qt5::Widgets
${FM_LIBRARIES}
${LIBFM_QT_LIBRARY_NAME}
)
# some simple test cases
add_executable("test-folder"
tests/test-folder.cpp
)
target_link_libraries("test-folder" ${TEST_LIBRARIES})
add_executable("test-folderview"
tests/test-folderview.cpp
)
target_link_libraries("test-folderview" ${TEST_LIBRARIES})
add_executable("test-filedialog"
tests/test-filedialog.cpp
)
target_link_libraries("test-filedialog" ${TEST_LIBRARIES})
add_executable("test-volumemanager"
tests/test-volumemanager.cpp
)
target_link_libraries("test-volumemanager" ${TEST_LIBRARIES})
add_executable("test-placesview"
tests/test-placesview.cpp
)
target_link_libraries("test-placesview" ${TEST_LIBRARIES})

@ -21,124 +21,113 @@
#include "icontheme.h" #include "icontheme.h"
#include "appchooserdialog.h" #include "appchooserdialog.h"
#include "utilities.h" #include "utilities.h"
#include "core/iconinfo.h"
namespace Fm { namespace Fm {
AppChooserComboBox::AppChooserComboBox(QWidget* parent): AppChooserComboBox::AppChooserComboBox(QWidget* parent):
QComboBox(parent), QComboBox(parent),
mimeType_(NULL), defaultAppIndex_(-1),
appInfos_(NULL), prevIndex_(0),
defaultApp_(NULL), blockOnCurrentIndexChanged_(false) {
defaultAppIndex_(-1),
prevIndex_(0), // the new Qt5 signal/slot syntax cannot handle overloaded methods by default
blockOnCurrentIndexChanged_(false) { // hence a type-casting is needed here. really ugly!
// reference: http://qt-project.org/forums/viewthread/21513
// the new Qt5 signal/slot syntax cannot handle overloaded methods by default connect((QComboBox*)this, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &AppChooserComboBox::onCurrentIndexChanged);
// hence a type-casting is needed here. really ugly!
// reference: http://qt-project.org/forums/viewthread/21513
connect((QComboBox*)this, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &AppChooserComboBox::onCurrentIndexChanged);
} }
AppChooserComboBox::~AppChooserComboBox() { AppChooserComboBox::~AppChooserComboBox() {
if(mimeType_)
fm_mime_type_unref(mimeType_);
if(defaultApp_)
g_object_unref(defaultApp_);
// delete GAppInfo objects stored for Combobox
if(appInfos_) {
g_list_foreach(appInfos_, (GFunc)g_object_unref, NULL);
g_list_free(appInfos_);
}
} }
void AppChooserComboBox::setMimeType(FmMimeType* mimeType) { void AppChooserComboBox::setMimeType(std::shared_ptr<const Fm::MimeType> mimeType) {
clear(); clear();
if(mimeType_) defaultApp_.reset();
fm_mime_type_unref(mimeType_); appInfos_.clear();
mimeType_ = fm_mime_type_ref(mimeType); mimeType_ = std::move(mimeType);
if(mimeType_) { if(mimeType_) {
const char* typeName = fm_mime_type_get_type(mimeType_); const char* typeName = mimeType_->name();
defaultApp_ = g_app_info_get_default_for_type(typeName, FALSE); defaultApp_ = Fm::GAppInfoPtr{g_app_info_get_default_for_type(typeName, FALSE), false};
appInfos_ = g_app_info_get_all_for_type(typeName); GList* appInfos_glist = g_app_info_get_all_for_type(typeName);
int i = 0; int i = 0;
for(GList* l = appInfos_; l; l = l->next, ++i) { for(GList* l = appInfos_glist; l; l = l->next, ++i) {
GAppInfo* app = G_APP_INFO(l->data); Fm::GAppInfoPtr app{G_APP_INFO(l->data), false};
GIcon* gicon = g_app_info_get_icon(app); GIcon* gicon = g_app_info_get_icon(app.get());
QString name = QString::fromUtf8(g_app_info_get_name(app)); addItem(gicon ? Fm::IconInfo::fromGIcon(gicon)->qicon(): QIcon(), g_app_info_get_name(app.get()));
// QVariant data = qVariantFromValue<void*>(app); if(g_app_info_equal(app.get(), defaultApp_.get())) {
// addItem(IconTheme::icon(gicon), name, data); defaultAppIndex_ = i;
addItem(IconTheme::icon(gicon), name); }
if(g_app_info_equal(app, defaultApp_)) appInfos_.push_back(std::move(app));
defaultAppIndex_ = i; }
g_list_free(appInfos_glist);
}
// add "Other applications" item
insertSeparator(count());
addItem(tr("Customize"));
if(defaultAppIndex_ != -1) {
setCurrentIndex(defaultAppIndex_);
} }
}
// add "Other applications" item
insertSeparator(count());
addItem(tr("Customize"));
if(defaultAppIndex_ != -1)
setCurrentIndex(defaultAppIndex_);
} }
// returns the currently selected app. // returns the currently selected app.
GAppInfo* AppChooserComboBox::selectedApp() { Fm::GAppInfoPtr AppChooserComboBox::selectedApp() const {
return G_APP_INFO(g_list_nth_data(appInfos_, currentIndex())); int idx = currentIndex();
return idx >= 0 ? appInfos_[idx] : Fm::GAppInfoPtr{};
} }
bool AppChooserComboBox::isChanged() { bool AppChooserComboBox::isChanged() const {
return (defaultAppIndex_ != currentIndex()); return (defaultAppIndex_ != currentIndex());
} }
void AppChooserComboBox::onCurrentIndexChanged(int index) { void AppChooserComboBox::onCurrentIndexChanged(int index) {
if(index == -1 || index == prevIndex_ || blockOnCurrentIndexChanged_) if(index == -1 || index == prevIndex_ || blockOnCurrentIndexChanged_) {
return; return;
}
// the last item is "Customize"
if(index == (count() - 1)) { // the last item is "Customize"
/* TODO: let the user choose an app or add custom actions here. */ if(index == (count() - 1)) {
QWidget* toplevel = topLevelWidget(); /* TODO: let the user choose an app or add custom actions here. */
AppChooserDialog dlg(mimeType_, toplevel); QWidget* toplevel = topLevelWidget();
dlg.setWindowModality(Qt::WindowModal); AppChooserDialog dlg(mimeType_, toplevel);
dlg.setCanSetDefault(false); dlg.setWindowModality(Qt::WindowModal);
if(dlg.exec() == QDialog::Accepted) { dlg.setCanSetDefault(false);
GAppInfo* app = dlg.selectedApp(); if(dlg.exec() == QDialog::Accepted) {
if(app) { auto app = dlg.selectedApp();
/* see if it's already in the list to prevent duplication */ if(app) {
GList* found = NULL; /* see if it's already in the list to prevent duplication */
for(found = appInfos_; found; found = found->next) { auto found = std::find_if(appInfos_.cbegin(), appInfos_.cend(), [&](const Fm::GAppInfoPtr& item) {
if(g_app_info_equal(app, G_APP_INFO(found->data))) return g_app_info_equal(app.get(), item.get());
break; });
// inserting new items or change current index will recursively trigger onCurrentIndexChanged.
// we need to block our handler to prevent recursive calls.
blockOnCurrentIndexChanged_ = true;
/* if it's already in the list, select it */
if(found != appInfos_.cend()) {
auto pos = found - appInfos_.cbegin();
setCurrentIndex(pos);
}
else { /* if it's not found, add it to the list */
auto it = appInfos_.insert(appInfos_.cbegin(), std::move(app));
GIcon* gicon = g_app_info_get_icon(it->get());
insertItem(0, Fm::IconInfo::fromGIcon(gicon)->qicon(), g_app_info_get_name(it->get()));
setCurrentIndex(0);
}
blockOnCurrentIndexChanged_ = false;
return;
}
} }
// inserting new items or change current index will recursively trigger onCurrentIndexChanged. // block our handler to prevent recursive calls.
// we need to block our handler to prevent recursive calls.
blockOnCurrentIndexChanged_ = true; blockOnCurrentIndexChanged_ = true;
/* if it's already in the list, select it */ // restore to previously selected item
if(found) { setCurrentIndex(prevIndex_);
setCurrentIndex(g_list_position(appInfos_, found));
g_object_unref(app);
}
else { /* if it's not found, add it to the list */
appInfos_ = g_list_prepend(appInfos_, app);
GIcon* gicon = g_app_info_get_icon(app);
QString name = QString::fromUtf8(g_app_info_get_name(app));
insertItem(0, IconTheme::icon(gicon), name);
setCurrentIndex(0);
}
blockOnCurrentIndexChanged_ = false; blockOnCurrentIndexChanged_ = false;
return;
}
} }
else {
// block our handler to prevent recursive calls. prevIndex_ = index;
blockOnCurrentIndexChanged_ = true; }
// restore to previously selected item
setCurrentIndex(prevIndex_);
blockOnCurrentIndexChanged_ = false;
}
else {
prevIndex_ = index;
}
} }

@ -24,35 +24,40 @@
#include <QComboBox> #include <QComboBox>
#include <libfm/fm.h> #include <libfm/fm.h>
#include <vector>
#include "core/mimetype.h"
#include "core/gioptrs.h"
namespace Fm { namespace Fm {
class LIBFM_QT_API AppChooserComboBox : public QComboBox { class LIBFM_QT_API AppChooserComboBox : public QComboBox {
Q_OBJECT Q_OBJECT
public: public:
~AppChooserComboBox(); ~AppChooserComboBox();
AppChooserComboBox(QWidget* parent); explicit AppChooserComboBox(QWidget* parent);
void setMimeType(FmMimeType* mimeType); void setMimeType(std::shared_ptr<const Fm::MimeType> mimeType);
FmMimeType* mimeType() { const std::shared_ptr<const Fm::MimeType>& mimeType() const {
return mimeType_; return mimeType_;
} }
GAppInfo* selectedApp(); Fm::GAppInfoPtr selectedApp() const;
// const GList* customApps(); // const GList* customApps();
bool isChanged(); bool isChanged() const;
private Q_SLOTS: private Q_SLOTS:
void onCurrentIndexChanged(int index); void onCurrentIndexChanged(int index);
private: private:
FmMimeType* mimeType_; std::shared_ptr<const Fm::MimeType> mimeType_;
GList* appInfos_; // applications used to open the file type std::vector<Fm::GAppInfoPtr> appInfos_; // applications used to open the file type
GAppInfo* defaultApp_; // default application used to open the file type Fm::GAppInfoPtr defaultApp_; // default application used to open the file type
int defaultAppIndex_; int defaultAppIndex_;
int prevIndex_; int prevIndex_;
bool blockOnCurrentIndexChanged_; bool blockOnCurrentIndexChanged_;
}; };
} }

@ -25,262 +25,262 @@
namespace Fm { namespace Fm {
AppChooserDialog::AppChooserDialog(FmMimeType* mimeType, QWidget* parent, Qt::WindowFlags f): AppChooserDialog::AppChooserDialog(std::shared_ptr<const Fm::MimeType> mimeType, QWidget* parent, Qt::WindowFlags f):
QDialog(parent, f), QDialog(parent, f),
ui(new Ui::AppChooserDialog()), ui(new Ui::AppChooserDialog()),
mimeType_(NULL), mimeType_{std::move(mimeType)},
canSetDefault_(true), canSetDefault_(true) {
selectedApp_(NULL) { ui->setupUi(this);
ui->setupUi(this);
connect(ui->appMenuView, &AppMenuView::selectionChanged, this, &AppChooserDialog::onSelectionChanged); connect(ui->appMenuView, &AppMenuView::selectionChanged, this, &AppChooserDialog::onSelectionChanged);
connect(ui->tabWidget, &QTabWidget::currentChanged, this, &AppChooserDialog::onTabChanged); connect(ui->tabWidget, &QTabWidget::currentChanged, this, &AppChooserDialog::onTabChanged);
if(!ui->appMenuView->isAppSelected()) if(!ui->appMenuView->isAppSelected()) {
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); // disable OK button ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); // disable OK button
}
if(mimeType)
setMimeType(mimeType);
} }
AppChooserDialog::~AppChooserDialog() { AppChooserDialog::~AppChooserDialog() {
delete ui; delete ui;
if(mimeType_)
fm_mime_type_unref(mimeType_);
if(selectedApp_)
g_object_unref(selectedApp_);
} }
bool AppChooserDialog::isSetDefault() { bool AppChooserDialog::isSetDefault() const {
return ui->setDefault->isChecked(); return ui->setDefault->isChecked();
} }
static void on_temp_appinfo_destroy(gpointer data, GObject* /*objptr*/) {
static void on_temp_appinfo_destroy(gpointer data, GObject* objptr) { char* filename = (char*)data;
char* filename = (char*)data; if(g_unlink(filename) < 0) {
if(g_unlink(filename) < 0) g_critical("failed to remove %s", filename);
g_critical("failed to remove %s", filename); }
/* else /* else
qDebug("temp file %s removed", filename); */ qDebug("temp file %s removed", filename); */
g_free(filename); g_free(filename);
} }
static GAppInfo* app_info_create_from_commandline(const char* commandline, static GAppInfo* app_info_create_from_commandline(const char* commandline,
const char* application_name, const char* application_name,
const char* bin_name, const char* bin_name,
const char* mime_type, const char* mime_type,
gboolean terminal, gboolean keep) { gboolean terminal, gboolean keep) {
GAppInfo* app = NULL; GAppInfo* app = nullptr;
char* dirname = g_build_filename(g_get_user_data_dir(), "applications", NULL); char* dirname = g_build_filename(g_get_user_data_dir(), "applications", nullptr);
const char* app_basename = strrchr(bin_name, '/'); const char* app_basename = strrchr(bin_name, '/');
if(app_basename) if(app_basename) {
app_basename++; app_basename++;
else
app_basename = bin_name;
if(g_mkdir_with_parents(dirname, 0700) == 0) {
char* filename = g_strdup_printf("%s/userapp-%s-XXXXXX.desktop", dirname, app_basename);
int fd = g_mkstemp(filename);
if(fd != -1) {
GString* content = g_string_sized_new(256);
g_string_printf(content,
"[" G_KEY_FILE_DESKTOP_GROUP "]\n"
G_KEY_FILE_DESKTOP_KEY_TYPE "=" G_KEY_FILE_DESKTOP_TYPE_APPLICATION "\n"
G_KEY_FILE_DESKTOP_KEY_NAME "=%s\n"
G_KEY_FILE_DESKTOP_KEY_EXEC "=%s\n"
G_KEY_FILE_DESKTOP_KEY_CATEGORIES "=Other;\n"
G_KEY_FILE_DESKTOP_KEY_NO_DISPLAY "=true\n",
application_name,
commandline
);
if(mime_type)
g_string_append_printf(content,
G_KEY_FILE_DESKTOP_KEY_MIME_TYPE "=%s\n",
mime_type);
g_string_append_printf(content,
G_KEY_FILE_DESKTOP_KEY_TERMINAL "=%s\n",
terminal ? "true" : "false");
if(terminal)
g_string_append_printf(content, "X-KeepTerminal=%s\n",
keep ? "true" : "false");
close(fd); /* g_file_set_contents() may fail creating duplicate */
if(g_file_set_contents(filename, content->str, content->len, NULL)) {
char* fbname = g_path_get_basename(filename);
app = G_APP_INFO(g_desktop_app_info_new(fbname));
g_free(fbname);
/* if there is mime_type set then created application will be
saved for the mime type (see fm_choose_app_for_mime_type()
below) but if not then we should remove this temp. file */
if(!mime_type || !application_name[0])
/* save the name so this file will be removed later */
g_object_weak_ref(G_OBJECT(app), on_temp_appinfo_destroy,
g_strdup(filename));
}
else
g_unlink(filename);
g_string_free(content, TRUE);
} }
g_free(filename); else {
} app_basename = bin_name;
g_free(dirname); }
return app; if(g_mkdir_with_parents(dirname, 0700) == 0) {
char* filename = g_strdup_printf("%s/userapp-%s-XXXXXX.desktop", dirname, app_basename);
int fd = g_mkstemp(filename);
if(fd != -1) {
GString* content = g_string_sized_new(256);
g_string_printf(content,
"[" G_KEY_FILE_DESKTOP_GROUP "]\n"
G_KEY_FILE_DESKTOP_KEY_TYPE "=" G_KEY_FILE_DESKTOP_TYPE_APPLICATION "\n"
G_KEY_FILE_DESKTOP_KEY_NAME "=%s\n"
G_KEY_FILE_DESKTOP_KEY_EXEC "=%s\n"
G_KEY_FILE_DESKTOP_KEY_CATEGORIES "=Other;\n"
G_KEY_FILE_DESKTOP_KEY_NO_DISPLAY "=true\n",
application_name,
commandline
);
if(mime_type)
g_string_append_printf(content,
G_KEY_FILE_DESKTOP_KEY_MIME_TYPE "=%s\n",
mime_type);
g_string_append_printf(content,
G_KEY_FILE_DESKTOP_KEY_TERMINAL "=%s\n",
terminal ? "true" : "false");
if(terminal)
g_string_append_printf(content, "X-KeepTerminal=%s\n",
keep ? "true" : "false");
close(fd); /* g_file_set_contents() may fail creating duplicate */
if(g_file_set_contents(filename, content->str, content->len, nullptr)) {
char* fbname = g_path_get_basename(filename);
app = G_APP_INFO(g_desktop_app_info_new(fbname));
g_free(fbname);
/* if there is mime_type set then created application will be
saved for the mime type (see fm_choose_app_for_mime_type()
below) but if not then we should remove this temp. file */
if(!mime_type || !application_name[0])
/* save the name so this file will be removed later */
g_object_weak_ref(G_OBJECT(app), on_temp_appinfo_destroy,
g_strdup(filename));
}
else {
g_unlink(filename);
}
g_string_free(content, TRUE);
}
g_free(filename);
}
g_free(dirname);
return app;
} }
inline static char* get_binary(const char* cmdline, gboolean* arg_found) { inline static char* get_binary(const char* cmdline, gboolean* arg_found) {
/* see if command line contains %f, %F, %u, or %U. */
const char* p = strstr(cmdline, " %");
if(p) {
if(!strchr("fFuU", *(p + 2)))
p = NULL;
}
if(arg_found)
*arg_found = (p != NULL);
if(p)
return g_strndup(cmdline, p - cmdline);
else
return g_strdup(cmdline);
}
GAppInfo* AppChooserDialog::customCommandToApp() {
GAppInfo* app = NULL;
QByteArray cmdline = ui->cmdLine->text().toLocal8Bit();
QByteArray app_name = ui->appName->text().toUtf8();
if(!cmdline.isEmpty()) {
gboolean arg_found = FALSE;
char* bin1 = get_binary(cmdline.constData(), &arg_found);
qDebug("bin1 = %s", bin1);
/* see if command line contains %f, %F, %u, or %U. */ /* see if command line contains %f, %F, %u, or %U. */
if(!arg_found) { /* append %f if no %f, %F, %u, or %U was found. */ const char* p = strstr(cmdline, " %");
cmdline += " %f"; if(p) {
if(!strchr("fFuU", *(p + 2))) {
p = nullptr;
}
}
if(arg_found) {
*arg_found = (p != nullptr);
}
if(p) {
return g_strndup(cmdline, p - cmdline);
}
else {
return g_strdup(cmdline);
} }
}
/* FIXME: is there any better way to do this? */ GAppInfo* AppChooserDialog::customCommandToApp() {
/* We need to ensure that no duplicated items are added */ GAppInfo* app = nullptr;
if(mimeType_) { QByteArray cmdline = ui->cmdLine->text().toLocal8Bit();
MenuCache* menu_cache; QByteArray app_name = ui->appName->text().toUtf8();
/* see if the command is already in the list of known apps for this mime-type */ if(!cmdline.isEmpty()) {
GList* apps = g_app_info_get_all_for_type(fm_mime_type_get_type(mimeType_)); gboolean arg_found = FALSE;
GList* l; char* bin1 = get_binary(cmdline.constData(), &arg_found);
for(l = apps; l; l = l->next) { qDebug("bin1 = %s", bin1);
GAppInfo* app2 = G_APP_INFO(l->data); /* see if command line contains %f, %F, %u, or %U. */
const char* cmd = g_app_info_get_commandline(app2); if(!arg_found) { /* append %f if no %f, %F, %u, or %U was found. */
char* bin2 = get_binary(cmd, NULL); cmdline += " %f";
if(g_strcmp0(bin1, bin2) == 0) {
app = G_APP_INFO(g_object_ref(app2));
qDebug("found in app list");
g_free(bin2);
break;
} }
g_free(bin2);
}
g_list_foreach(apps, (GFunc)g_object_unref, NULL);
g_list_free(apps);
if(app)
goto _out;
/* see if this command can be found in menu cache */ /* FIXME: is there any better way to do this? */
menu_cache = menu_cache_lookup("applications.menu"); /* We need to ensure that no duplicated items are added */
if(menu_cache) { if(mimeType_) {
MenuCacheDir* root_dir = menu_cache_dup_root_dir(menu_cache); MenuCache* menu_cache;
if(root_dir) { /* see if the command is already in the list of known apps for this mime-type */
GSList* all_apps = menu_cache_list_all_apps(menu_cache); GList* apps = g_app_info_get_all_for_type(mimeType_->name());
GSList* l; GList* l;
for(l = all_apps; l; l = l->next) { for(l = apps; l; l = l->next) {
MenuCacheApp* ma = MENU_CACHE_APP(l->data); GAppInfo* app2 = G_APP_INFO(l->data);
const char* exec = menu_cache_app_get_exec(ma); const char* cmd = g_app_info_get_commandline(app2);
char* bin2; char* bin2 = get_binary(cmd, nullptr);
if(exec == NULL) { if(g_strcmp0(bin1, bin2) == 0) {
g_warning("application %s has no Exec statement", menu_cache_item_get_id(MENU_CACHE_ITEM(ma))); app = G_APP_INFO(g_object_ref(app2));
continue; qDebug("found in app list");
g_free(bin2);
break;
}
g_free(bin2);
} }
bin2 = get_binary(exec, NULL); g_list_foreach(apps, (GFunc)g_object_unref, nullptr);
if(g_strcmp0(bin1, bin2) == 0) { g_list_free(apps);
app = G_APP_INFO(g_desktop_app_info_new(menu_cache_item_get_id(MENU_CACHE_ITEM(ma)))); if(app) {
qDebug("found in menu cache"); goto _out;
menu_cache_item_unref(MENU_CACHE_ITEM(ma)); }
g_free(bin2);
break; /* see if this command can be found in menu cache */
menu_cache = menu_cache_lookup("applications.menu");
if(menu_cache) {
MenuCacheDir* root_dir = menu_cache_dup_root_dir(menu_cache);
if(root_dir) {
GSList* all_apps = menu_cache_list_all_apps(menu_cache);
GSList* l;
for(l = all_apps; l; l = l->next) {
MenuCacheApp* ma = MENU_CACHE_APP(l->data);
const char* exec = menu_cache_app_get_exec(ma);
char* bin2;
if(exec == nullptr) {
g_warning("application %s has no Exec statement", menu_cache_item_get_id(MENU_CACHE_ITEM(ma)));
continue;
}
bin2 = get_binary(exec, nullptr);
if(g_strcmp0(bin1, bin2) == 0) {
app = G_APP_INFO(g_desktop_app_info_new(menu_cache_item_get_id(MENU_CACHE_ITEM(ma))));
qDebug("found in menu cache");
menu_cache_item_unref(MENU_CACHE_ITEM(ma));
g_free(bin2);
break;
}
menu_cache_item_unref(MENU_CACHE_ITEM(ma));
g_free(bin2);
}
g_slist_free(all_apps);
menu_cache_item_unref(MENU_CACHE_ITEM(root_dir));
}
menu_cache_unref(menu_cache);
}
if(app) {
goto _out;
} }
menu_cache_item_unref(MENU_CACHE_ITEM(ma));
g_free(bin2);
}
g_slist_free(all_apps);
menu_cache_item_unref(MENU_CACHE_ITEM(root_dir));
} }
menu_cache_unref(menu_cache);
}
if(app)
goto _out;
}
/* FIXME: g_app_info_create_from_commandline force the use of %f or %u, so this is not we need */ /* FIXME: g_app_info_create_from_commandline force the use of %f or %u, so this is not we need */
app = app_info_create_from_commandline(cmdline.constData(), app_name.constData(), bin1, app = app_info_create_from_commandline(cmdline.constData(), app_name.constData(), bin1,
mimeType_ ? fm_mime_type_get_type(mimeType_) : NULL, mimeType_ ? mimeType_->name() : nullptr,
ui->useTerminal->isChecked(), ui->keepTermOpen->isChecked()); ui->useTerminal->isChecked(), ui->keepTermOpen->isChecked());
_out: _out:
g_free(bin1); g_free(bin1);
} }
return app; return app;
} }
void AppChooserDialog::accept() { void AppChooserDialog::accept() {
QDialog::accept(); QDialog::accept();
if(ui->tabWidget->currentIndex() == 0) { if(ui->tabWidget->currentIndex() == 0) {
selectedApp_ = ui->appMenuView->selectedApp(); selectedApp_ = ui->appMenuView->selectedApp();
} }
else { // custom command line else { // custom command line
selectedApp_ = customCommandToApp(); selectedApp_ = customCommandToApp();
} }
if(selectedApp_) { if(selectedApp_) {
if(mimeType_ && fm_mime_type_get_type(mimeType_) && g_app_info_get_name(selectedApp_)[0]) { if(mimeType_ && g_app_info_get_name(selectedApp_.get())) {
/* add this app to the mime-type */ /* add this app to the mime-type */
#if GLIB_CHECK_VERSION(2, 27, 6) #if GLIB_CHECK_VERSION(2, 27, 6)
g_app_info_set_as_last_used_for_type(selectedApp_, fm_mime_type_get_type(mimeType_), NULL); g_app_info_set_as_last_used_for_type(selectedApp_.get(), mimeType_->name(), nullptr);
#else #else
g_app_info_add_supports_type(selectedApp_, fm_mime_type_get_type(mimeType_), NULL); g_app_info_add_supports_type(selectedApp_.get(), mimeType_->name(), nullptr);
#endif #endif
/* if need to set default */ /* if need to set default */
if(ui->setDefault->isChecked()) if(ui->setDefault->isChecked()) {
g_app_info_set_as_default_for_type(selectedApp_, fm_mime_type_get_type(mimeType_), NULL); g_app_info_set_as_default_for_type(selectedApp_.get(), mimeType_->name(), nullptr);
}
}
} }
}
} }
void AppChooserDialog::onSelectionChanged() { void AppChooserDialog::onSelectionChanged() {
bool isAppSelected = ui->appMenuView->isAppSelected(); bool isAppSelected = ui->appMenuView->isAppSelected();
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(isAppSelected); ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(isAppSelected);
} }
void AppChooserDialog::setMimeType(FmMimeType* mimeType) { void AppChooserDialog::setMimeType(std::shared_ptr<const Fm::MimeType> mimeType) {
if(mimeType_) mimeType_ = std::move(mimeType);
fm_mime_type_unref(mimeType_); if(mimeType_) {
QString text = tr("Select an application to open \"%1\" files")
mimeType_ = mimeType ? fm_mime_type_ref(mimeType) : NULL; .arg(QString::fromUtf8(mimeType_->desc()));
if(mimeType_) { ui->fileTypeHeader->setText(text);
QString text = tr("Select an application to open \"%1\" files") }
.arg(QString::fromUtf8(fm_mime_type_get_desc(mimeType_))); else {
ui->fileTypeHeader->setText(text); ui->fileTypeHeader->hide();
} ui->setDefault->hide();
else { }
ui->fileTypeHeader->hide();
ui->setDefault->hide();
}
} }
void AppChooserDialog::setCanSetDefault(bool value) { void AppChooserDialog::setCanSetDefault(bool value) {
canSetDefault_ = value; canSetDefault_ = value;
ui->setDefault->setVisible(value); ui->setDefault->setVisible(value);
} }
void AppChooserDialog::onTabChanged(int index) { void AppChooserDialog::onTabChanged(int index) {
if(index == 0) { // app menu view if(index == 0) { // app menu view
onSelectionChanged(); onSelectionChanged();
} }
else if(index == 1) { // custom command else if(index == 1) { // custom command
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
} }
} }
} // namespace Fm } // namespace Fm

@ -25,48 +25,53 @@
#include "libfmqtglobals.h" #include "libfmqtglobals.h"
#include <libfm/fm.h> #include <libfm/fm.h>
#include "core/mimetype.h"
#include "core/gioptrs.h"
namespace Ui { namespace Ui {
class AppChooserDialog; class AppChooserDialog;
} }
namespace Fm { namespace Fm {
class LIBFM_QT_API AppChooserDialog : public QDialog { class LIBFM_QT_API AppChooserDialog : public QDialog {
Q_OBJECT Q_OBJECT
public: public:
explicit AppChooserDialog(FmMimeType* mimeType, QWidget* parent = NULL, Qt::WindowFlags f = 0); explicit AppChooserDialog(std::shared_ptr<const Fm::MimeType> mimeType, QWidget* parent = nullptr, Qt::WindowFlags f = 0);
~AppChooserDialog(); ~AppChooserDialog();
virtual void accept();
void setMimeType(std::shared_ptr<const Fm::MimeType> mimeType);
virtual void accept(); const std::shared_ptr<const Fm::MimeType>& mimeType() const {
return mimeType_;
}
void setMimeType(FmMimeType* mimeType); void setCanSetDefault(bool value);
FmMimeType* mimeType() {
return mimeType_;
}
void setCanSetDefault(bool value); bool canSetDefault() const {
bool canSetDefault() { return canSetDefault_;
return canSetDefault_; }
}
GAppInfo* selectedApp() { const Fm::GAppInfoPtr& selectedApp() const {
return G_APP_INFO(g_object_ref(selectedApp_)); return selectedApp_;
} }
bool isSetDefault(); bool isSetDefault() const;
private: private:
GAppInfo* customCommandToApp(); GAppInfo* customCommandToApp();
private Q_SLOTS: private Q_SLOTS:
void onSelectionChanged(); void onSelectionChanged();
void onTabChanged(int index); void onTabChanged(int index);
private: private:
Ui::AppChooserDialog* ui; Ui::AppChooserDialog* ui;
FmMimeType* mimeType_; std::shared_ptr<const Fm::MimeType> mimeType_;
bool canSetDefault_; bool canSetDefault_;
GAppInfo* selectedApp_; Fm::GAppInfoPtr selectedApp_;
}; };
} }

@ -28,17 +28,17 @@ typedef struct _FmAppLaunchContext {
G_DEFINE_TYPE(FmAppLaunchContext, fm_app_launch_context, G_TYPE_APP_LAUNCH_CONTEXT) G_DEFINE_TYPE(FmAppLaunchContext, fm_app_launch_context, G_TYPE_APP_LAUNCH_CONTEXT)
static char* fm_app_launch_context_get_display(GAppLaunchContext *context, GAppInfo *info, GList *files) { static char* fm_app_launch_context_get_display(GAppLaunchContext * /*context*/, GAppInfo * /*info*/, GList * /*files*/) {
Display* dpy = QX11Info::display(); Display* dpy = QX11Info::display();
if(dpy) { if(dpy) {
char* xstr = DisplayString(dpy); char* xstr = DisplayString(dpy);
return g_strdup(xstr); return g_strdup(xstr);
} }
return NULL; return nullptr;
} }
static char* fm_app_launch_context_get_startup_notify_id(GAppLaunchContext *context, GAppInfo *info, GList *files) { static char* fm_app_launch_context_get_startup_notify_id(GAppLaunchContext * /*context*/, GAppInfo * /*info*/, GList * /*files*/) {
return NULL; return nullptr;
} }
static void fm_app_launch_context_class_init(FmAppLaunchContextClass* klass) { static void fm_app_launch_context_class_init(FmAppLaunchContextClass* klass) {
@ -47,15 +47,15 @@ static void fm_app_launch_context_class_init(FmAppLaunchContextClass* klass) {
app_launch_class->get_startup_notify_id = fm_app_launch_context_get_startup_notify_id; app_launch_class->get_startup_notify_id = fm_app_launch_context_get_startup_notify_id;
} }
static void fm_app_launch_context_init(FmAppLaunchContext* context) { static void fm_app_launch_context_init(FmAppLaunchContext* /*context*/) {
} }
FmAppLaunchContext* fm_app_launch_context_new_for_widget(QWidget* widget) { FmAppLaunchContext* fm_app_launch_context_new_for_widget(QWidget* /*widget*/) {
FmAppLaunchContext* context = (FmAppLaunchContext*)g_object_new(FM_TYPE_APP_LAUNCH_CONTEXT, NULL); FmAppLaunchContext* context = (FmAppLaunchContext*)g_object_new(FM_TYPE_APP_LAUNCH_CONTEXT, nullptr);
return context; return context;
} }
FmAppLaunchContext* fm_app_launch_context_new() { FmAppLaunchContext* fm_app_launch_context_new() {
FmAppLaunchContext* context = (FmAppLaunchContext*)g_object_new(FM_TYPE_APP_LAUNCH_CONTEXT, NULL); FmAppLaunchContext* context = (FmAppLaunchContext*)g_object_new(FM_TYPE_APP_LAUNCH_CONTEXT, nullptr);
return context; return context;
} }

@ -26,133 +26,137 @@
namespace Fm { namespace Fm {
AppMenuView::AppMenuView(QWidget* parent): AppMenuView::AppMenuView(QWidget* parent):
QTreeView(parent), QTreeView(parent),
model_(new QStandardItemModel()), model_(new QStandardItemModel()),
menu_cache(NULL), menu_cache(nullptr),
menu_cache_reload_notify(NULL) { menu_cache_reload_notify(nullptr) {
setHeaderHidden(true); setHeaderHidden(true);
setSelectionMode(SingleSelection); setSelectionMode(SingleSelection);
// initialize model // initialize model
// TODO: share one model among all app menu view widgets // TODO: share one model among all app menu view widgets
// ensure that we're using lxmenu-data (FIXME: should we do this?) // ensure that we're using lxmenu-data (FIXME: should we do this?)
QByteArray oldenv = qgetenv("XDG_MENU_PREFIX"); QByteArray oldenv = qgetenv("XDG_MENU_PREFIX");
qputenv("XDG_MENU_PREFIX", "lxde-"); qputenv("XDG_MENU_PREFIX", "lxde-");
menu_cache = menu_cache_lookup("applications.menu"); menu_cache = menu_cache_lookup("applications.menu");
// if(!oldenv.isEmpty()) // if(!oldenv.isEmpty())
qputenv("XDG_MENU_PREFIX", oldenv); // restore the original value if needed qputenv("XDG_MENU_PREFIX", oldenv); // restore the original value if needed
if(menu_cache) { if(menu_cache) {
MenuCacheDir* dir = menu_cache_dup_root_dir(menu_cache); MenuCacheDir* dir = menu_cache_dup_root_dir(menu_cache);
menu_cache_reload_notify = menu_cache_add_reload_notify(menu_cache, _onMenuCacheReload, this); menu_cache_reload_notify = menu_cache_add_reload_notify(menu_cache, _onMenuCacheReload, this);
if(dir) { /* content of menu is already loaded */ if(dir) { /* content of menu is already loaded */
addMenuItems(NULL, dir); addMenuItems(nullptr, dir);
menu_cache_item_unref(MENU_CACHE_ITEM(dir)); menu_cache_item_unref(MENU_CACHE_ITEM(dir));
}
} }
} setModel(model_);
setModel(model_); connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &AppMenuView::selectionChanged);
connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &AppMenuView::selectionChanged); selectionModel()->select(model_->index(0, 0), QItemSelectionModel::SelectCurrent);
selectionModel()->select(model_->index(0, 0), QItemSelectionModel::SelectCurrent);
} }
AppMenuView::~AppMenuView() { AppMenuView::~AppMenuView() {
delete model_; delete model_;
if(menu_cache) { if(menu_cache) {
if(menu_cache_reload_notify) if(menu_cache_reload_notify) {
menu_cache_remove_reload_notify(menu_cache, menu_cache_reload_notify); menu_cache_remove_reload_notify(menu_cache, menu_cache_reload_notify);
menu_cache_unref(menu_cache); }
} menu_cache_unref(menu_cache);
}
} }
void AppMenuView::addMenuItems(QStandardItem* parentItem, MenuCacheDir* dir) { void AppMenuView::addMenuItems(QStandardItem* parentItem, MenuCacheDir* dir) {
GSList* l; GSList* l;
GSList* list; GSList* list;
/* Iterate over all menu items in this directory. */ /* Iterate over all menu items in this directory. */
for(l = list = menu_cache_dir_list_children(dir); l != NULL; l = l->next) { for(l = list = menu_cache_dir_list_children(dir); l != nullptr; l = l->next) {
/* Get the menu item. */ /* Get the menu item. */
MenuCacheItem* menuItem = MENU_CACHE_ITEM(l->data); MenuCacheItem* menuItem = MENU_CACHE_ITEM(l->data);
switch(menu_cache_item_get_type(menuItem)) { switch(menu_cache_item_get_type(menuItem)) {
case MENU_CACHE_TYPE_NONE: case MENU_CACHE_TYPE_NONE:
case MENU_CACHE_TYPE_SEP: case MENU_CACHE_TYPE_SEP:
break; break;
case MENU_CACHE_TYPE_APP: case MENU_CACHE_TYPE_APP:
case MENU_CACHE_TYPE_DIR: { case MENU_CACHE_TYPE_DIR: {
AppMenuViewItem* newItem = new AppMenuViewItem(menuItem); AppMenuViewItem* newItem = new AppMenuViewItem(menuItem);
if(parentItem) if(parentItem) {
parentItem->insertRow(parentItem->rowCount(), newItem); parentItem->insertRow(parentItem->rowCount(), newItem);
else }
model_->insertRow(model_->rowCount(), newItem); else {
model_->insertRow(model_->rowCount(), newItem);
if(menu_cache_item_get_type(menuItem) == MENU_CACHE_TYPE_DIR) }
addMenuItems(newItem, MENU_CACHE_DIR(menuItem));
break; if(menu_cache_item_get_type(menuItem) == MENU_CACHE_TYPE_DIR) {
} addMenuItems(newItem, MENU_CACHE_DIR(menuItem));
}
break;
}
}
} }
} g_slist_free_full(list, (GDestroyNotify)menu_cache_item_unref);
g_slist_free_full(list, (GDestroyNotify)menu_cache_item_unref);
} }
void AppMenuView::onMenuCacheReload(MenuCache* mc) { void AppMenuView::onMenuCacheReload(MenuCache* mc) {
MenuCacheDir* dir = menu_cache_dup_root_dir(mc); MenuCacheDir* dir = menu_cache_dup_root_dir(mc);
model_->clear(); model_->clear();
/* FIXME: preserve original selection */ /* FIXME: preserve original selection */
if(dir) { if(dir) {
addMenuItems(NULL, dir); addMenuItems(nullptr, dir);
menu_cache_item_unref(MENU_CACHE_ITEM(dir)); menu_cache_item_unref(MENU_CACHE_ITEM(dir));
selectionModel()->select(model_->index(0, 0), QItemSelectionModel::SelectCurrent); selectionModel()->select(model_->index(0, 0), QItemSelectionModel::SelectCurrent);
} }
} }
bool AppMenuView::isAppSelected() { bool AppMenuView::isAppSelected() const {
AppMenuViewItem* item = selectedItem(); AppMenuViewItem* item = selectedItem();
return (item && item->isApp()); return (item && item->isApp());
} }
AppMenuViewItem* AppMenuView::selectedItem() { AppMenuViewItem* AppMenuView::selectedItem() const {
QModelIndexList selected = selectedIndexes(); QModelIndexList selected = selectedIndexes();
if(!selected.isEmpty()) { if(!selected.isEmpty()) {
AppMenuViewItem* item = static_cast<AppMenuViewItem*>(model_->itemFromIndex(selected.first() AppMenuViewItem* item = static_cast<AppMenuViewItem*>(model_->itemFromIndex(selected.first()
)); ));
return item; return item;
} }
return NULL; return nullptr;
} }
GAppInfo* AppMenuView::selectedApp() { Fm::GAppInfoPtr AppMenuView::selectedApp() const {
const char* id = selectedAppDesktopId(); const char* id = selectedAppDesktopId();
return id ? G_APP_INFO(g_desktop_app_info_new(id)) : NULL; return Fm::GAppInfoPtr{id ? G_APP_INFO(g_desktop_app_info_new(id)) : nullptr, false};
} }
QByteArray AppMenuView::selectedAppDesktopFilePath() { QByteArray AppMenuView::selectedAppDesktopFilePath() const {
AppMenuViewItem* item = selectedItem(); AppMenuViewItem* item = selectedItem();
if(item && item->isApp()) { if(item && item->isApp()) {
char* path = menu_cache_item_get_file_path(item->item()); char* path = menu_cache_item_get_file_path(item->item());
QByteArray ret(path); QByteArray ret(path);
g_free(path); g_free(path);
return ret; return ret;
} }
return QByteArray(); return QByteArray();
} }
const char* AppMenuView::selectedAppDesktopId() { const char* AppMenuView::selectedAppDesktopId() const {
AppMenuViewItem* item = selectedItem(); AppMenuViewItem* item = selectedItem();
if(item && item->isApp()) { if(item && item->isApp()) {
return menu_cache_item_get_id(item->item()); return menu_cache_item_get_id(item->item());
} }
return NULL; return nullptr;
} }
FmPath* AppMenuView::selectedAppDesktopPath() { FmPath* AppMenuView::selectedAppDesktopPath() const {
AppMenuViewItem* item = selectedItem(); AppMenuViewItem* item = selectedItem();
if(item && item->isApp()) { if(item && item->isApp()) {
char* mpath = menu_cache_dir_make_path(MENU_CACHE_DIR(item)); char* mpath = menu_cache_dir_make_path(MENU_CACHE_DIR(item));
FmPath* path = fm_path_new_relative(fm_path_get_apps_menu(), FmPath* path = fm_path_new_relative(fm_path_get_apps_menu(),
mpath + 13 /* skip "/Applications" */); mpath + 13 /* skip "/Applications" */);
g_free(mpath); g_free(mpath);
return path; return path;
} }
return NULL; return nullptr;
} }
} // namespace Fm } // namespace Fm

@ -25,6 +25,8 @@
#include <libfm/fm.h> #include <libfm/fm.h>
#include <menu-cache/menu-cache.h> #include <menu-cache/menu-cache.h>
#include "core/gioptrs.h"
class QStandardItemModel; class QStandardItemModel;
class QStandardItem; class QStandardItem;
@ -33,38 +35,38 @@ namespace Fm {
class AppMenuViewItem; class AppMenuViewItem;
class LIBFM_QT_API AppMenuView : public QTreeView { class LIBFM_QT_API AppMenuView : public QTreeView {
Q_OBJECT Q_OBJECT
public: public:
explicit AppMenuView(QWidget* parent = NULL); explicit AppMenuView(QWidget* parent = nullptr);
~AppMenuView(); ~AppMenuView();
GAppInfo* selectedApp(); Fm::GAppInfoPtr selectedApp() const;
const char* selectedAppDesktopId(); const char* selectedAppDesktopId() const;
QByteArray selectedAppDesktopFilePath(); QByteArray selectedAppDesktopFilePath() const;
FmPath * selectedAppDesktopPath(); FmPath* selectedAppDesktopPath() const;
bool isAppSelected(); bool isAppSelected() const;
Q_SIGNALS: Q_SIGNALS:
void selectionChanged(); void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected);
private: private:
void addMenuItems(QStandardItem* parentItem, MenuCacheDir* dir); void addMenuItems(QStandardItem* parentItem, MenuCacheDir* dir);
void onMenuCacheReload(MenuCache* mc); void onMenuCacheReload(MenuCache* mc);
static void _onMenuCacheReload(MenuCache* mc, gpointer user_data) { static void _onMenuCacheReload(MenuCache* mc, gpointer user_data) {
static_cast<AppMenuView*>(user_data)->onMenuCacheReload(mc); static_cast<AppMenuView*>(user_data)->onMenuCacheReload(mc);
} }
AppMenuViewItem* selectedItem(); AppMenuViewItem* selectedItem() const;
private: private:
// gboolean fm_app_menu_view_is_item_app(, GtkTreeIter* it); // gboolean fm_app_menu_view_is_item_app(, GtkTreeIter* it);
QStandardItemModel* model_; QStandardItemModel* model_;
MenuCache* menu_cache; MenuCache* menu_cache;
MenuCacheNotifyId menu_cache_reload_notify; MenuCacheNotifyId menu_cache_reload_notify;
}; };
} }

@ -23,49 +23,48 @@
#include <QStandardItem> #include <QStandardItem>
#include <menu-cache/menu-cache.h> #include <menu-cache/menu-cache.h>
#include "icontheme.h" #include "icontheme.h"
#include "core/iconinfo.h"
namespace Fm { namespace Fm {
class AppMenuViewItem : public QStandardItem { class AppMenuViewItem : public QStandardItem {
public: public:
explicit AppMenuViewItem(MenuCacheItem* item): explicit AppMenuViewItem(MenuCacheItem* item):
item_(menu_cache_item_ref(item)) { item_(menu_cache_item_ref(item)) {
FmIcon* fmicon; std::shared_ptr<const Fm::IconInfo> icon;
if(menu_cache_item_get_icon(item)) if(menu_cache_item_get_icon(item)) {
fmicon = fm_icon_from_name(menu_cache_item_get_icon(item)); icon = Fm::IconInfo::fromName(menu_cache_item_get_icon(item));
else }
fmicon = nullptr; setText(menu_cache_item_get_name(item));
setText(QString::fromUtf8(menu_cache_item_get_name(item))); setEditable(false);
setEditable(false); setDragEnabled(false);
setDragEnabled(false); if(icon) {
if(fmicon) { setIcon(icon->qicon());
setIcon(IconTheme::icon(fmicon)); }
fm_icon_unref(fmicon);
} }
}
~AppMenuViewItem() { ~AppMenuViewItem() {
menu_cache_item_unref(item_); menu_cache_item_unref(item_);
} }
MenuCacheItem* item() { MenuCacheItem* item() {
return item_; return item_;
} }
MenuCacheType type() { int type() const {
return menu_cache_item_get_type(item_); return menu_cache_item_get_type(item_);
} }
bool isApp() { bool isApp() {
return type() == MENU_CACHE_TYPE_APP; return type() == MENU_CACHE_TYPE_APP;
} }
bool isDir() { bool isDir() {
return type() == MENU_CACHE_TYPE_DIR; return type() == MENU_CACHE_TYPE_DIR;
} }
private: private:
MenuCacheItem* item_; MenuCacheItem* item_;
}; };
} }

@ -40,7 +40,7 @@ public:
// move constructor // move constructor
Archiver(Archiver&& other) { Archiver(Archiver&& other) noexcept {
dataPtr_ = reinterpret_cast<FmArchiver*>(other.takeDataPtr()); dataPtr_ = reinterpret_cast<FmArchiver*>(other.takeDataPtr());
} }
@ -85,7 +85,7 @@ public:
// move assignment // move assignment
Archiver& operator=(Archiver&& other) { Archiver& operator=(Archiver&& other) noexcept {
dataPtr_ = reinterpret_cast<FmArchiver*>(other.takeDataPtr()); dataPtr_ = reinterpret_cast<FmArchiver*>(other.takeDataPtr());
return *this; return *this;
} }

@ -22,11 +22,11 @@
namespace Fm { namespace Fm {
BookmarkAction::BookmarkAction(FmBookmarkItem* item, QObject* parent): BookmarkAction::BookmarkAction(std::shared_ptr<const Fm::BookmarkItem> item, QObject* parent):
QAction(parent), QAction(parent),
item_(fm_bookmark_item_ref(item)) { item_(std::move(item)) {
setText(QString::fromUtf8(item->name)); setText(item_->name());
} }
} // namespace Fm } // namespace Fm

@ -23,30 +23,25 @@
#include "libfmqtglobals.h" #include "libfmqtglobals.h"
#include <QAction> #include <QAction>
#include <libfm/fm.h> #include "core/bookmarks.h"
namespace Fm { namespace Fm {
// action used to create bookmark menu items // action used to create bookmark menu items
class LIBFM_QT_API BookmarkAction : public QAction { class LIBFM_QT_API BookmarkAction : public QAction {
public: public:
explicit BookmarkAction(FmBookmarkItem* item, QObject* parent = 0); explicit BookmarkAction(std::shared_ptr<const Fm::BookmarkItem> item, QObject* parent = 0);
virtual ~BookmarkAction() { const std::shared_ptr<const Fm::BookmarkItem>& bookmark() const {
if(item_) return item_;
fm_bookmark_item_unref(item_); }
}
FmBookmarkItem* bookmark() { const Fm::FilePath& path() const {
return item_; return item_->path();
} }
FmPath* path() {
return item_->path;
}
private: private:
FmBookmarkItem* item_; std::shared_ptr<const Fm::BookmarkItem> item_;
}; };
} }

@ -1,163 +0,0 @@
/*
* Copyright (C) 2016 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
*
* 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_BOOKMARKS_H__
#define __LIBFM_QT_FM_BOOKMARKS_H__
#include <libfm/fm.h>
#include <QObject>
#include <QtGlobal>
#include "libfmqtglobals.h"
namespace Fm {
class LIBFM_QT_API Bookmarks {
public:
// default constructor
Bookmarks() {
dataPtr_ = nullptr;
}
Bookmarks(FmBookmarks* dataPtr){
dataPtr_ = dataPtr != nullptr ? reinterpret_cast<GObject*>(g_object_ref(dataPtr)) : nullptr;
}
// copy constructor
Bookmarks(const Bookmarks& other) {
dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<GObject*>(g_object_ref(other.dataPtr_)) : nullptr;
}
// move constructor
Bookmarks(Bookmarks&& other) {
dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
}
// destructor
virtual ~Bookmarks() {
if(dataPtr_ != nullptr) {
g_object_unref(dataPtr_);
}
}
// create a wrapper for the data pointer without increasing the reference count
static Bookmarks wrapPtr(FmBookmarks* dataPtr) {
Bookmarks obj;
obj.dataPtr_ = reinterpret_cast<GObject*>(dataPtr);
return obj;
}
// disown the managed data pointer
FmBookmarks* takeDataPtr() {
FmBookmarks* data = reinterpret_cast<FmBookmarks*>(dataPtr_);
dataPtr_ = nullptr;
return data;
}
// get the raw pointer wrapped
FmBookmarks* dataPtr() {
return reinterpret_cast<FmBookmarks*>(dataPtr_);
}
// automatic type casting
operator FmBookmarks*() {
return dataPtr();
}
// automatic type casting
operator void*() {
return dataPtr();
}
// copy assignment
Bookmarks& operator=(const Bookmarks& other) {
if(dataPtr_ != nullptr) {
g_object_unref(dataPtr_);
}
dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<GObject*>(g_object_ref(other.dataPtr_)) : nullptr;
return *this;
}
// move assignment
Bookmarks& operator=(Bookmarks&& other) {
dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
return *this;
}
bool isNull() {
return (dataPtr_ == nullptr);
}
// methods
GList* getAll(void) {
return fm_bookmarks_get_all(dataPtr());
}
void rename(FmBookmarkItem* item, const char* new_name) {
fm_bookmarks_rename(dataPtr(), item, new_name);
}
void reorder(FmBookmarkItem* item, int pos) {
fm_bookmarks_reorder(dataPtr(), item, pos);
}
void remove(FmBookmarkItem* item) {
fm_bookmarks_remove(dataPtr(), item);
}
FmBookmarkItem* insert(FmPath* path, const char* name, int pos) {
return fm_bookmarks_insert(dataPtr(), path, name, pos);
}
static Bookmarks dup(void ) {
return Bookmarks::wrapPtr(fm_bookmarks_dup());
}
// automatic type casting for GObject
operator GObject*() {
return reinterpret_cast<GObject*>(dataPtr_);
}
protected:
GObject* dataPtr_; // data pointer for the underlying C struct
};
}
#endif // __LIBFM_QT_FM_BOOKMARKS_H__

@ -23,67 +23,70 @@
namespace Fm { namespace Fm {
BrowseHistory::BrowseHistory(): BrowseHistory::BrowseHistory():
currentIndex_(0), currentIndex_(0),
maxCount_(10) { maxCount_(10) {
} }
BrowseHistory::~BrowseHistory() { BrowseHistory::~BrowseHistory() {
} }
void BrowseHistory::add(FmPath* path, int scrollPos) { void BrowseHistory::add(Fm::FilePath path, int scrollPos) {
int lastIndex = size() - 1; int lastIndex = items_.size() - 1;
if(currentIndex_ < lastIndex) { if(currentIndex_ < lastIndex) {
// if we're not at the last item, remove items after the current one. // if we're not at the last item, remove items after the current one.
erase(begin() + currentIndex_ + 1, end()); items_.erase(items_.cbegin() + currentIndex_ + 1, items_.cend());
} }
if(size() + 1 > maxCount_) { if(items_.size() + 1 > static_cast<size_t>(maxCount_)) {
// if there are too many items, remove the oldest one. // if there are too many items, remove the oldest one.
// FIXME: what if currentIndex_ == 0? remove the last item instead? // FIXME: what if currentIndex_ == 0? remove the last item instead?
if(currentIndex_ == 0) if(currentIndex_ == 0) {
remove(lastIndex); items_.erase(items_.cbegin() + lastIndex);
else { }
remove(0); else {
--currentIndex_; items_.erase(items_.cbegin());
--currentIndex_;
}
} }
} // add a path and current scroll position to browse history
// add a path and current scroll position to browse history items_.push_back(BrowseHistoryItem(path, scrollPos));
append(BrowseHistoryItem(path, scrollPos)); currentIndex_ = items_.size() - 1;
currentIndex_ = size() - 1;
} }
void BrowseHistory::setCurrentIndex(int index) { void BrowseHistory::setCurrentIndex(int index) {
if(index >= 0 && index < size()) { if(index >= 0 && static_cast<size_t>(index) < items_.size()) {
currentIndex_ = index; currentIndex_ = index;
// FIXME: should we emit a signal for the change? // FIXME: should we emit a signal for the change?
} }
} }
bool BrowseHistory::canBackward() const { bool BrowseHistory::canBackward() const {
return (currentIndex_ > 0); return (currentIndex_ > 0);
} }
int BrowseHistory::backward() { int BrowseHistory::backward() {
if(canBackward()) if(canBackward()) {
--currentIndex_; --currentIndex_;
return currentIndex_; }
return currentIndex_;
} }
bool BrowseHistory::canForward() const { bool BrowseHistory::canForward() const {
return (currentIndex_ + 1 < size()); return (static_cast<size_t>(currentIndex_) + 1 < items_.size());
} }
int BrowseHistory::forward() { int BrowseHistory::forward() {
if(canForward()) if(canForward()) {
++currentIndex_; ++currentIndex_;
return currentIndex_; }
return currentIndex_;
} }
void BrowseHistory::setMaxCount(int maxCount) { void BrowseHistory::setMaxCount(int maxCount) {
maxCount_ = maxCount; maxCount_ = maxCount;
if(size() > maxCount) { if(items_.size() > static_cast<size_t>(maxCount)) {
// TODO: remove some items // TODO: remove some items
} }
} }

@ -22,9 +22,11 @@
#define FM_BROWSEHISTORY_H #define FM_BROWSEHISTORY_H
#include "libfmqtglobals.h" #include "libfmqtglobals.h"
#include <QVector> #include <vector>
#include <libfm/fm.h> #include <libfm/fm.h>
#include "core/filepath.h"
namespace Fm { namespace Fm {
// class used to story browsing history of folder views // class used to story browsing history of folder views
@ -34,96 +36,95 @@ namespace Fm {
class LIBFM_QT_API BrowseHistoryItem { class LIBFM_QT_API BrowseHistoryItem {
public: public:
BrowseHistoryItem(): explicit BrowseHistoryItem():
path_(NULL), scrollPos_(0) {
scrollPos_(0) { }
}
explicit BrowseHistoryItem(Fm::FilePath path, int scrollPos = 0):
BrowseHistoryItem(FmPath* path, int scrollPos = 0): path_(std::move(path)),
path_(fm_path_ref(path)), scrollPos_(scrollPos) {
scrollPos_(scrollPos) { }
}
BrowseHistoryItem(const BrowseHistoryItem& other) = default;
BrowseHistoryItem(const BrowseHistoryItem& other):
path_(other.path_ ? fm_path_ref(other.path_) : NULL), ~BrowseHistoryItem() {
scrollPos_(other.scrollPos_) { }
}
BrowseHistoryItem& operator=(const BrowseHistoryItem& other) {
~BrowseHistoryItem() { path_ = other.path_;
if(path_) scrollPos_ = other.scrollPos_;
fm_path_unref(path_); return *this;
} }
BrowseHistoryItem& operator=(const BrowseHistoryItem& other) { Fm::FilePath path() const {
if(path_) return path_;
fm_path_unref(path_); }
path_ = other.path_ ? fm_path_ref(other.path_) : NULL;
scrollPos_ = other.scrollPos_; int scrollPos() const {
return *this; return scrollPos_;
} }
FmPath* path() const { void setScrollPos(int pos) {
return path_; scrollPos_ = pos;
} }
int scrollPos() const {
return scrollPos_;
}
void setScrollPos(int pos) {
scrollPos_ = pos;
}
private: private:
FmPath* path_; Fm::FilePath path_;
int scrollPos_; int scrollPos_;
// TODO: we may need to store current selection as well. reserve room for furutre expansion. // TODO: we may need to store current selection as well.
// void* reserved1;
// void* reserved2;
}; };
class LIBFM_QT_API BrowseHistory : public QVector<BrowseHistoryItem> { class LIBFM_QT_API BrowseHistory {
public: public:
BrowseHistory(); BrowseHistory();
virtual ~BrowseHistory(); virtual ~BrowseHistory();
int currentIndex() const {
return currentIndex_;
}
void setCurrentIndex(int index);
Fm::FilePath currentPath() const {
return items_[currentIndex_].path();
}
int currentIndex() const { int currentScrollPos() const {
return currentIndex_; return items_[currentIndex_].scrollPos();
} }
void setCurrentIndex(int index);
FmPath* currentPath() const { BrowseHistoryItem& currentItem() {
return at(currentIndex_).path(); return items_[currentIndex_];
} }
int currentScrollPos() const { size_t size() const {
return at(currentIndex_).scrollPos(); return items_.size();
} }
BrowseHistoryItem& currentItem() { BrowseHistoryItem& at(int index) {
return operator[](currentIndex_); return items_[index];
} }
void add(FmPath* path, int scrollPos = 0); void add(Fm::FilePath path, int scrollPos = 0);
bool canForward() const; bool canForward() const;
bool canBackward() const; bool canBackward() const;
int backward(); int backward();
int forward(); int forward();
int maxCount() const { int maxCount() const {
return maxCount_; return maxCount_;
} }
void setMaxCount(int maxCount); void setMaxCount(int maxCount);
private: private:
int currentIndex_; std::vector<BrowseHistoryItem> items_;
int maxCount_; int currentIndex_;
int maxCount_;
}; };
} }

@ -21,53 +21,46 @@
namespace Fm { namespace Fm {
static GQuark data_id = 0; CachedFolderModel::CachedFolderModel(const std::shared_ptr<Fm::Folder>& folder):
FolderModel(),
refCount(1) {
CachedFolderModel::CachedFolderModel(FmFolder* folder): FolderModel::setFolder(folder);
FolderModel(),
refCount(1) {
FolderModel::setFolder(folder);
} }
CachedFolderModel::~CachedFolderModel() { CachedFolderModel::~CachedFolderModel() {
// qDebug("delete CachedFolderModel");
} }
CachedFolderModel* CachedFolderModel::modelFromFolder(FmFolder* folder) { CachedFolderModel* CachedFolderModel::modelFromFolder(const std::shared_ptr<Fm::Folder>& folder) {
CachedFolderModel* model = NULL; QVariant cache = folder->property(cacheKey);
if(!data_id) CachedFolderModel* model = cache.value<CachedFolderModel*>();
data_id = g_quark_from_static_string("CachedFolderModel"); if(model) {
gpointer qdata = g_object_get_qdata(G_OBJECT(folder), data_id); model->ref();
model = reinterpret_cast<CachedFolderModel*>(qdata); }
if(model) { else {
// qDebug("cache found!!"); model = new CachedFolderModel(folder);
model->ref(); cache = QVariant::fromValue(model);
} folder->setProperty(cacheKey, cache);
else { }
model = new CachedFolderModel(folder); return model;
g_object_set_qdata(G_OBJECT(folder), data_id, model);
}
return model;
} }
CachedFolderModel* CachedFolderModel::modelFromPath(FmPath* path) { CachedFolderModel* CachedFolderModel::modelFromPath(const Fm::FilePath& path) {
FmFolder* folder = fm_folder_from_path(path); auto folder = Fm::Folder::fromPath(path);
if(folder) { if(folder) {
CachedFolderModel* model = modelFromFolder(folder); CachedFolderModel* model = modelFromFolder(folder);
g_object_unref(folder); return model;
return model; }
} return nullptr;
return NULL;
} }
void CachedFolderModel::unref() { void CachedFolderModel::unref() {
// qDebug("unref cache"); // qDebug("unref cache");
--refCount; --refCount;
if(refCount <= 0) { if(refCount <= 0) {
g_object_set_qdata(G_OBJECT(folder()), data_id, NULL); folder()->setProperty(cacheKey, QVariant());
deleteLater(); deleteLater();
} }
} }

@ -24,25 +24,29 @@
#include "libfmqtglobals.h" #include "libfmqtglobals.h"
#include "foldermodel.h" #include "foldermodel.h"
#include "core/folder.h"
namespace Fm { namespace Fm {
// FIXME: deprecate CachedFolderModel later (ugly API design with manual ref()/unref())
class LIBFM_QT_API CachedFolderModel : public FolderModel { class LIBFM_QT_API CachedFolderModel : public FolderModel {
Q_OBJECT Q_OBJECT
public: public:
CachedFolderModel(FmFolder* folder); explicit CachedFolderModel(const std::shared_ptr<Fm::Folder>& folder);
void ref() { void ref() {
++refCount; ++refCount;
} }
void unref(); void unref();
static CachedFolderModel* modelFromFolder(FmFolder* folder); static CachedFolderModel* modelFromFolder(const std::shared_ptr<Fm::Folder>& folder);
static CachedFolderModel* modelFromPath(FmPath* path); static CachedFolderModel* modelFromPath(const Fm::FilePath& path);
private: private:
virtual ~CachedFolderModel(); virtual ~CachedFolderModel();
void setFolder(FmFolder* folder); void setFolder(FmFolder* folder);
private: private:
int refCount; int refCount;
constexpr static const char* cacheKey = "CachedFolderModel";
}; };

@ -24,7 +24,7 @@
namespace Fm { namespace Fm {
ColorButton::ColorButton(QWidget* parent): QPushButton(parent) { ColorButton::ColorButton(QWidget* parent): QPushButton(parent) {
connect(this, &QPushButton::clicked, this, &ColorButton::onClicked); connect(this, &QPushButton::clicked, this, &ColorButton::onClicked);
} }
ColorButton::~ColorButton() { ColorButton::~ColorButton() {
@ -32,21 +32,21 @@ ColorButton::~ColorButton() {
} }
void ColorButton::onClicked() { void ColorButton::onClicked() {
QColorDialog dlg(color_); QColorDialog dlg(color_);
if(dlg.exec() == QDialog::Accepted) { if(dlg.exec() == QDialog::Accepted) {
setColor(dlg.selectedColor()); setColor(dlg.selectedColor());
} }
} }
void ColorButton::setColor(const QColor& color) { void ColorButton::setColor(const QColor& color) {
if(color != color_) { if(color != color_) {
color_ = color; color_ = color;
// use qss instead of QPalette to set the background color // use qss instead of QPalette to set the background color
// otherwise, this won't work when using the gtk style. // otherwise, this won't work when using the gtk style.
QString style = QString("QPushButton{background-color:%1;}").arg(color.name()); QString style = QString("QPushButton{background-color:%1;}").arg(color.name());
setStyleSheet(style); setStyleSheet(style);
Q_EMIT changed(); Q_EMIT changed();
} }
} }

@ -28,26 +28,26 @@
namespace Fm { namespace Fm {
class LIBFM_QT_API ColorButton : public QPushButton { class LIBFM_QT_API ColorButton : public QPushButton {
Q_OBJECT Q_OBJECT
public: public:
explicit ColorButton(QWidget* parent = 0); explicit ColorButton(QWidget* parent = 0);
virtual ~ColorButton(); virtual ~ColorButton();
void setColor(const QColor&); void setColor(const QColor&);
QColor color() const { QColor color() const {
return color_; return color_;
} }
Q_SIGNALS: Q_SIGNALS:
void changed(); void changed();
private Q_SLOTS: private Q_SLOTS:
void onClicked(); void onClicked();
private: private:
QColor color_; QColor color_;
}; };
} }

@ -1,152 +0,0 @@
/*
* Copyright (C) 2016 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
*
* 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_CONFIG_H__
#define __LIBFM_QT_FM_CONFIG_H__
#include <libfm/fm.h>
#include <QObject>
#include <QtGlobal>
#include "libfmqtglobals.h"
namespace Fm {
class LIBFM_QT_API Config {
public:
Config(void ) {
dataPtr_ = reinterpret_cast<GObject*>(fm_config_new());
}
Config(FmConfig* dataPtr){
dataPtr_ = dataPtr != nullptr ? reinterpret_cast<GObject*>(g_object_ref(dataPtr)) : nullptr;
}
// copy constructor
Config(const Config& other) {
dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<GObject*>(g_object_ref(other.dataPtr_)) : nullptr;
}
// move constructor
Config(Config&& other) {
dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
}
// destructor
virtual ~Config() {
if(dataPtr_ != nullptr) {
g_object_unref(dataPtr_);
}
}
// create a wrapper for the data pointer without increasing the reference count
static Config wrapPtr(FmConfig* dataPtr) {
Config obj;
obj.dataPtr_ = reinterpret_cast<GObject*>(dataPtr);
return obj;
}
// disown the managed data pointer
FmConfig* takeDataPtr() {
FmConfig* data = reinterpret_cast<FmConfig*>(dataPtr_);
dataPtr_ = nullptr;
return data;
}
// get the raw pointer wrapped
FmConfig* dataPtr() {
return reinterpret_cast<FmConfig*>(dataPtr_);
}
// automatic type casting
operator FmConfig*() {
return dataPtr();
}
// automatic type casting
operator void*() {
return dataPtr();
}
// copy assignment
Config& operator=(const Config& other) {
if(dataPtr_ != nullptr) {
g_object_unref(dataPtr_);
}
dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<GObject*>(g_object_ref(other.dataPtr_)) : nullptr;
return *this;
}
// move assignment
Config& operator=(Config&& other) {
dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
return *this;
}
bool isNull() {
return (dataPtr_ == nullptr);
}
// methods
void emitChanged(const char* changed_key) {
fm_config_emit_changed(dataPtr(), changed_key);
}
void save(const char* name) {
fm_config_save(dataPtr(), name);
}
void loadFromKeyFile(GKeyFile* kf) {
fm_config_load_from_key_file(dataPtr(), kf);
}
void loadFromFile(const char* name) {
fm_config_load_from_file(dataPtr(), name);
}
// automatic type casting for GObject
operator GObject*() {
return reinterpret_cast<GObject*>(dataPtr_);
}
protected:
GObject* dataPtr_; // data pointer for the underlying C struct
};
}
#endif // __LIBFM_QT_FM_CONFIG_H__

@ -0,0 +1,162 @@
#include "bookmarks.h"
#include "cstrptr.h"
#include <algorithm>
#include <QTimer>
namespace Fm {
std::weak_ptr<Bookmarks> Bookmarks::globalInstance_;
static inline CStrPtr get_legacy_bookmarks_file(void) {
return CStrPtr{g_build_filename(g_get_home_dir(), ".gtk-bookmarks", nullptr)};
}
static inline CStrPtr get_new_bookmarks_file(void) {
return CStrPtr{g_build_filename(g_get_user_config_dir(), "gtk-3.0", "bookmarks", nullptr)};
}
Bookmarks::Bookmarks(QObject* parent):
QObject(parent),
idle_handler{false} {
/* trying the gtk-3.0 first and use it if it exists */
auto fpath = get_new_bookmarks_file();
file = FilePath::fromLocalPath(fpath.get());
load();
if(items_.empty()) { /* not found, use legacy file */
fpath = get_legacy_bookmarks_file();
file = FilePath::fromLocalPath(fpath.get());
load();
}
mon = GObjectPtr<GFileMonitor>{g_file_monitor_file(file.gfile().get(), G_FILE_MONITOR_NONE, nullptr, nullptr), false};
if(mon) {
g_signal_connect(mon.get(), "changed", G_CALLBACK(_onFileChanged), this);
}
}
Bookmarks::~Bookmarks() {
if(mon) {
g_signal_handlers_disconnect_by_data(mon.get(), this);
}
}
const std::shared_ptr<const BookmarkItem>& Bookmarks::insert(const FilePath& path, const QString& name, int pos) {
const auto insert_pos = (pos < 0 || static_cast<size_t>(pos) > items_.size()) ? items_.cend() : items_.cbegin() + pos;
auto it = items_.insert(insert_pos, std::make_shared<const BookmarkItem>(path, name));
queueSave();
return *it;
}
void Bookmarks::remove(const std::shared_ptr<const BookmarkItem>& item) {
items_.erase(std::remove(items_.begin(), items_.end(), item), items_.end());
queueSave();
}
void Bookmarks::reorder(const std::shared_ptr<const BookmarkItem>& item, int pos) {
auto old_it = std::find(items_.cbegin(), items_.cend(), item);
if(old_it == items_.cend())
return;
std::shared_ptr<const BookmarkItem> newItem = item;
auto old_pos = old_it - items_.cbegin();
items_.erase(old_it);
if(old_pos < pos)
--pos;
auto new_it = items_.cbegin() + pos;
if(new_it > items_.cend())
new_it = items_.cend();
items_.insert(new_it, std::move(newItem));
queueSave();
}
void Bookmarks::rename(const std::shared_ptr<const BookmarkItem>& item, QString new_name) {
auto it = std::find_if(items_.cbegin(), items_.cend(), [item](const std::shared_ptr<const BookmarkItem>& elem) {
return elem->path() == item->path();
});
if(it != items_.cend()) {
// create a new item to replace the old one
// we do not modify the old item directly since this data structure is shared with others
it = items_.insert(it, std::make_shared<const BookmarkItem>(item->path(), new_name));
items_.erase(it + 1); // remove the old item
queueSave();
}
}
std::shared_ptr<Bookmarks> Bookmarks::globalInstance() {
auto bookmarks = globalInstance_.lock();
if(!bookmarks) {
bookmarks = std::make_shared<Bookmarks>();
globalInstance_ = bookmarks;
}
return bookmarks;
}
void Bookmarks::save() {
std::string buf;
// G_LOCK(bookmarks);
for(auto& item: items_) {
auto uri = item->path().uri();
buf += uri.get();
buf += ' ';
buf += item->name().toUtf8().constData();
buf += '\n';
}
idle_handler = false;
// G_UNLOCK(bookmarks);
GError* err = nullptr;
if(!g_file_replace_contents(file.gfile().get(), buf.c_str(), buf.length(), nullptr,
FALSE, G_FILE_CREATE_NONE, nullptr, nullptr, &err)) {
g_critical("%s", err->message);
g_error_free(err);
}
/* we changed bookmarks list, let inform who interested in that */
Q_EMIT changed();
}
void Bookmarks::load() {
auto fpath = file.localPath();
FILE* f;
char buf[1024];
/* load the file */
f = fopen(fpath.get(), "r");
if(f) {
while(fgets(buf, 1024, f)) {
// format of each line in the bookmark file:
// <URI> <name>\n
char* sep;
sep = strchr(buf, '\n');
if(sep) {
*sep = '\0';
}
QString name;
sep = strchr(buf, ' '); // find the separator between URI and name
if(sep) {
*sep = '\0';
name = sep + 1;
}
auto uri = buf;
if(uri[0] != '\0') {
items_.push_back(std::make_shared<BookmarkItem>(FilePath::fromUri(uri), name));
}
}
fclose(f);
}
}
void Bookmarks::onFileChanged(GFileMonitor* /*mon*/, GFile* /*gf*/, GFile* /*other*/, GFileMonitorEvent /*evt*/) {
// reload the bookmarks
items_.clear();
load();
Q_EMIT changed();
}
void Bookmarks::queueSave() {
if(!idle_handler) {
QTimer::singleShot(0, this, &Bookmarks::save);
idle_handler = true;
}
}
} // namespace Fm

@ -0,0 +1,96 @@
#ifndef FM2_BOOKMARKS_H
#define FM2_BOOKMARKS_H
#include "../libfmqtglobals.h"
#include <QObject>
#include "gobjectptr.h"
#include "fileinfo.h"
namespace Fm {
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();
}
}
const QString& name() const {
return name_;
}
const FilePath& path() const {
return path_;
}
const std::shared_ptr<const FmFileInfo>& info() const {
return info_;
}
private:
void setInfo(const std::shared_ptr<const FmFileInfo>& info) {
info_ = info;
}
void setName(const QString& name) {
name_ = name;
}
private:
FilePath path_;
QString name_;
std::shared_ptr<const FmFileInfo> info_;
};
class LIBFM_QT_API Bookmarks : public QObject {
Q_OBJECT
public:
explicit Bookmarks(QObject* parent = 0);
~Bookmarks();
const std::shared_ptr<const BookmarkItem> &insert(const FilePath& path, const QString& name, int pos);
void remove(const std::shared_ptr<const BookmarkItem>& item);
void reorder(const std::shared_ptr<const BookmarkItem> &item, int pos);
void rename(const std::shared_ptr<const BookmarkItem>& item, QString new_name);
const std::vector<std::shared_ptr<const BookmarkItem>>& items() const {
return items_;
}
static std::shared_ptr<Bookmarks> globalInstance();
Q_SIGNALS:
void changed();
private Q_SLOTS:
void save();
private:
void load();
void queueSave();
static void _onFileChanged(GFileMonitor* mon, GFile* gf, GFile* other, GFileMonitorEvent evt, Bookmarks* _this) {
_this->onFileChanged(mon, gf, other, evt);
}
void onFileChanged(GFileMonitor* mon, GFile* gf, GFile* other, GFileMonitorEvent evt);
private:
FilePath file;
GObjectPtr<GFileMonitor> mon;
std::vector<std::shared_ptr<const BookmarkItem>> items_;
static std::weak_ptr<Bookmarks> globalInstance_;
bool idle_handler;
};
} // namespace Fm
#endif // FM2_BOOKMARKS_H

@ -0,0 +1,53 @@
#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 <libfm/fm.h>
#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<const Fm::FileInfo>& 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

@ -0,0 +1,453 @@
#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

@ -0,0 +1,46 @@
#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

@ -0,0 +1,42 @@
#ifndef FM2_CSTRPTR_H
#define FM2_CSTRPTR_H
#include <memory>
#include <glib.h>
namespace Fm {
struct CStrDeleter {
void operator()(char* ptr) {
g_free(ptr);
}
};
// smart pointer for C string (char*) which should be freed by free()
typedef std::unique_ptr<char[], CStrDeleter> CStrPtr;
struct CStrHash {
std::size_t operator()(const char* str) const {
return g_str_hash(str);
}
};
struct CStrEqual {
bool operator()(const char* str1, const char* str2) const {
return g_str_equal(str1, str2);
}
};
struct CStrVDeleter {
void operator()(char** ptr) {
g_strfreev(ptr);
}
};
// smart pointer for C string array (char**) which should be freed by g_strfreev() of glib
typedef std::unique_ptr<char*[], CStrVDeleter> CStrArrayPtr;
} // namespace Fm
#endif // FM2_CSTRPTR_H

@ -0,0 +1,151 @@
#include "deletejob.h"
#include "totalsizejob.h"
#include "fileinfo_p.h"
namespace Fm {
bool DeleteJob::deleteFile(const FilePath& path, GFileInfoPtr inf) {
ErrorAction act = ErrorAction::CONTINUE;
while(!inf) {
GErrorPtr err;
inf = GFileInfoPtr{
g_file_query_info(path.gfile().get(), "standard::*",
G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
cancellable().get(), &err),
false
};
if(err) {
act = emitError(err, ErrorSeverity::SEVERE);
if(act == ErrorAction::ABORT) {
return false;
}
if(act != ErrorAction::RETRY) {
break;
}
}
}
/* currently processed file. */
setCurrentFile(path);
if(g_file_info_get_file_type(inf.get()) == G_FILE_TYPE_DIRECTORY) {
// delete the content of the dir prior to deleting itself
deleteDirContent(path, inf);
}
bool hasError = false;
while(!isCancelled()) {
GErrorPtr err;
// try to delete the path directly
if(g_file_delete(path.gfile().get(), cancellable().get(), &err)) {
break;
}
if(err) {
// FIXME: error handling
/* if it's non-empty dir then descent into it then try again */
/* trash root gives G_IO_ERROR_PERMISSION_DENIED */
if(err.domain() == G_IO_ERROR && err.code() == G_IO_ERROR_NOT_EMPTY) {
deleteDirContent(path, inf);
}
else if(err.domain() == G_IO_ERROR && err.code() == G_IO_ERROR_PERMISSION_DENIED) {
/* special case for trash:/// */
/* FIXME: is there any better way to handle this? */
auto scheme = path.uriScheme();
if(g_strcmp0(scheme.get(), "trash") == 0) {
break;
}
}
act = emitError(err, ErrorSeverity::MODERATE);
if(act != ErrorAction::RETRY) {
hasError = true;
break;
}
}
}
addFinishedAmount(g_file_info_get_size(inf.get()), 1);
return !hasError;
}
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_QUERY_INFO_NOFOLLOW_SYMLINKS,
cancellable().get(), &err),
false
};
if(!enu) {
emitError(err, ErrorSeverity::MODERATE);
return false;
}
bool hasError = false;
while(!isCancelled()) {
inf = GFileInfoPtr{
g_file_enumerator_next_file(enu.get(), cancellable().get(), &err),
false
};
if(inf) {
auto subPath = path.child(g_file_info_get_name(inf.get()));
if(!deleteFile(subPath, inf)) {
continue;
}
}
else {
if(err) {
emitError(err, ErrorSeverity::MODERATE);
/* ErrorAction::RETRY is not supported here */
hasError = true;
}
else { /* EOF */
}
break;
}
}
g_file_enumerator_close(enu.get(), nullptr, nullptr);
return !hasError;
}
void DeleteJob::exec() {
/* prepare the job, count total work needed with FmDeepCountJob */
TotalSizeJob totalSizeJob{paths_, TotalSizeJob::Flags::PREPARE_DELETE};
connect(&totalSizeJob, &TotalSizeJob::error, this, &DeleteJob::error);
connect(this, &DeleteJob::cancelled, &totalSizeJob, &TotalSizeJob::cancel);
totalSizeJob.run();
if(isCancelled()) {
return;
}
setTotalAmount(totalSizeJob.totalSize(), totalSizeJob.fileCount());
Q_EMIT preparedToRun();
for(auto& path : paths_) {
if(isCancelled()) {
break;
}
deleteFile(path, GFileInfoPtr{nullptr});
}
}
} // namespace Fm

@ -0,0 +1,36 @@
#ifndef FM2_DELETEJOB_H
#define FM2_DELETEJOB_H
#include "../libfmqtglobals.h"
#include "fileoperationjob.h"
#include "filepath.h"
#include "gioptrs.h"
namespace Fm {
class LIBFM_QT_API DeleteJob : public Fm::FileOperationJob {
Q_OBJECT
public:
explicit DeleteJob(const FilePathList& paths): paths_{paths} {
}
explicit DeleteJob(FilePathList&& paths): paths_{paths} {
}
~DeleteJob() {
}
protected:
void exec() override;
private:
bool deleteFile(const FilePath& path, GFileInfoPtr inf);
bool deleteDirContent(const FilePath& path, GFileInfoPtr inf);
private:
FilePathList paths_;
};
} // namespace Fm
#endif // FM2_DELETEJOB_H

@ -0,0 +1,178 @@
#include "dirlistjob.h"
#include <gio/gio.h>
#include "fileinfo_p.h"
#include "gioptrs.h"
#include <QDebug>
namespace Fm {
DirListJob::DirListJob(const FilePath& path, Flags _flags, const std::shared_ptr<const HashSet>& cutFilesHashSet):
dir_path{path}, flags{_flags}, cutFilesHashSet_{cutFilesHashSet} {
}
void DirListJob::exec() {
GErrorPtr err;
GFileInfoPtr dir_inf;
GFilePtr dir_gfile = dir_path.gfile();
// FIXME: these are hacks for search:/// URI implemented by libfm which contains some bugs
bool isFileSearch = dir_path.hasUriScheme("search");
if(isFileSearch) {
// NOTE: The GFile instance changes its URI during file enumeration (bad design).
// So we create a copy here to avoid channging the gfile stored in dir_path.
// FIXME: later we should refactor file search and remove this dirty hack.
dir_gfile = GFilePtr{g_file_dup(dir_gfile.get())};
}
_retry:
err.reset();
dir_inf = GFileInfoPtr{
g_file_query_info(dir_gfile.get(), gfile_info_query_attribs,
G_FILE_QUERY_INFO_NONE, cancellable().get(), &err),
false
};
if(!dir_inf) {
ErrorAction act = emitError(err, ErrorSeverity::MODERATE);
if(act == ErrorAction::RETRY) {
err.reset();
goto _retry;
}
return;
}
if(g_file_info_get_file_type(dir_inf.get()) != G_FILE_TYPE_DIRECTORY) {
auto path_str = dir_path.toString();
err = GErrorPtr{
G_IO_ERROR,
G_IO_ERROR_NOT_DIRECTORY,
tr("The specified directory '%1' is not valid").arg(path_str.get())
};
emitError(err, ErrorSeverity::CRITICAL);
return;
}
else {
std::lock_guard<std::mutex> lock{mutex_};
dir_fi = std::make_shared<FileInfo>(dir_inf, dir_path.parent());
}
FileInfoList foundFiles;
/* check if FS is R/O and set attr. into inf */
// 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_QUERY_INFO_NONE, cancellable().get(), &err),
false
};
if(enu) {
// qDebug() << "START LISTING:" << dir_path.toString().get();
while(!isCancelled()) {
err.reset();
GFileInfoPtr inf{g_file_enumerator_next_file(enu.get(), cancellable().get(), &err), false};
if(inf) {
#if 0
FmPath* dir, *sub;
GFile* child;
if(G_UNLIKELY(job->flags & FM_DIR_LIST_JOB_DIR_ONLY)) {
/* FIXME: handle symlinks */
if(g_file_info_get_file_type(inf) != G_FILE_TYPE_DIRECTORY) {
g_object_unref(inf);
continue;
}
}
#endif
// virtual folders may return children not within them
// For example: the search:/// URI implemented by libfm might return files from different folders during enumeration.
// So here we call g_file_enumerator_get_container() to get the real parent path rather than simply using dir_path.
// This is not the behaviour of gio, but the extensions by libfm might do this.
// FIXME: after we port these vfs implementation from libfm, we can redesign this.
FilePath realParentPath = FilePath{g_file_enumerator_get_container(enu.get()), true};
if(isFileSearch) { // this is a file sarch job (search:/// URI)
// FIXME: redesign file search and remove this dirty hack
// the libfm implementation of search:/// URI returns a customized GFile implementation that does not behave normally.
// let's get its actual URI and re-create a normal gio GFile instance from it.
realParentPath = FilePath::fromUri(realParentPath.uri().get());
}
#if 0
if(g_file_info_get_file_type(inf) == G_FILE_TYPE_DIRECTORY)
/* for dir: check if its FS is R/O and set attr. into inf */
{
_fm_file_info_job_update_fs_readonly(child, inf, nullptr, nullptr);
}
fi = fm_file_info_new_from_g_file_data(child, inf, sub);
#endif
auto fileInfo = std::make_shared<FileInfo>(inf, realParentPath);
if(emit_files_found) {
// Q_EMIT filesFound();
}
if(cutFilesHashSet_
&& cutFilesHashSet_->count(fileInfo->path().hash()) > 0) {
fileInfo->bindCutFiles(cutFilesHashSet_);
}
foundFiles.push_back(std::move(fileInfo));
}
else {
if(err) {
ErrorAction act = emitError(err, ErrorSeverity::MILD);
/* ErrorAction::RETRY is not supported. */
if(act == ErrorAction::ABORT) {
cancel();
}
}
/* otherwise it's EOL */
break;
}
}
err.reset();
g_file_enumerator_close(enu.get(), cancellable().get(), &err);
}
else {
emitError(err, ErrorSeverity::CRITICAL);
}
// qDebug() << "END LISTING:" << dir_path.toString().get();
if(!foundFiles.empty()) {
std::lock_guard<std::mutex> lock{mutex_};
files_.swap(foundFiles);
}
}
#if 0
//FIXME: incremental..
static gboolean emit_found_files(gpointer user_data) {
/* this callback is called from the main thread */
FmDirListJob* job = FM_DIR_LIST_JOB(user_data);
/* g_print("emit_found_files: %d\n", g_slist_length(job->files_to_add)); */
if(g_source_is_destroyed(g_main_current_source())) {
return FALSE;
}
g_signal_emit(job, signals[FILES_FOUND], 0, job->files_to_add);
g_slist_free_full(job->files_to_add, (GDestroyNotify)fm_file_info_unref);
job->files_to_add = nullptr;
job->delay_add_files_handler = 0;
return FALSE;
}
static gpointer queue_add_file(FmJob* fmjob, gpointer user_data) {
FmDirListJob* job = FM_DIR_LIST_JOB(fmjob);
FmFileInfo* file = FM_FILE_INFO(user_data);
/* this callback is called from the main thread */
/* g_print("queue_add_file: %s\n", fm_file_info_get_disp_name(file)); */
job->files_to_add = g_slist_prepend(job->files_to_add, fm_file_info_ref(file));
if(job->delay_add_files_handler == 0)
job->delay_add_files_handler = g_timeout_add_seconds_full(G_PRIORITY_LOW,
1, emit_found_files, g_object_ref(job), g_object_unref);
return nullptr;
}
void fm_dir_list_job_add_found_file(FmDirListJob* job, FmFileInfo* file) {
fm_file_info_list_push_tail(job->files, file);
if(G_UNLIKELY(job->emit_files_found)) {
fm_job_call_main_thread(FM_JOB(job), queue_add_file, file);
}
}
#endif
} // namespace Fm

@ -0,0 +1,65 @@
#ifndef FM2_DIRLISTJOB_H
#define FM2_DIRLISTJOB_H
#include "../libfmqtglobals.h"
#include <mutex>
#include "job.h"
#include "filepath.h"
#include "gobjectptr.h"
#include "fileinfo.h"
namespace Fm {
class LIBFM_QT_API DirListJob : public Job {
Q_OBJECT
public:
enum Flags {
FAST = 0,
DIR_ONLY = 1 << 0,
DETAILED = 1 << 1
};
explicit DirListJob(const FilePath& path, Flags flags, const std::shared_ptr<const HashSet>& cutFilesHashSet = nullptr);
FileInfoList& files() {
return files_;
}
void setIncremental(bool set);
bool incremental() const {
return emit_files_found;
}
FilePath dirPath() const {
std::lock_guard<std::mutex> lock{mutex_};
return dir_path;
}
std::shared_ptr<const FileInfo> dirInfo() const {
std::lock_guard<std::mutex> lock{mutex_};
return dir_fi;
}
Q_SIGNALS:
void filesFound(FileInfoList& foundFiles);
protected:
void exec() override;
private:
mutable std::mutex mutex_;
FilePath dir_path;
Flags flags;
std::shared_ptr<const FileInfo> dir_fi;
FileInfoList files_;
const std::shared_ptr<const HashSet> cutFilesHashSet_;
bool emit_files_found;
// guint delay_add_files_handler;
// GSList* files_to_add;
};
} // namespace Fm
#endif // FM2_DIRLISTJOB_H

@ -0,0 +1,9 @@
#include "filechangeattrjob.h"
namespace Fm {
FileChangeAttrJob::FileChangeAttrJob() {
}
} // namespace Fm

@ -0,0 +1,17 @@
#ifndef FM2_FILECHANGEATTRJOB_H
#define FM2_FILECHANGEATTRJOB_H
#include "../libfmqtglobals.h"
#include "fileoperationjob.h"
namespace Fm {
class LIBFM_QT_API FileChangeAttrJob : public Fm::FileOperationJob {
Q_OBJECT
public:
explicit FileChangeAttrJob();
};
} // namespace Fm
#endif // FM2_FILECHANGEATTRJOB_H

@ -0,0 +1,378 @@
#include "fileinfo.h"
#include "fileinfo_p.h"
#include <gio/gio.h>
namespace Fm {
const char gfile_info_query_attribs[] = "standard::*,"
"unix::*,"
"time::*,"
"access::*,"
"id::filesystem,"
"metadata::emblems";
FileInfo::FileInfo() {
// FIXME: initialize numeric data members
}
FileInfo::FileInfo(const GFileInfoPtr& inf, const FilePath& parentDirPath) {
setFromGFileInfo(inf, parentDirPath);
}
FileInfo::~FileInfo() {
}
void FileInfo::setFromGFileInfo(const GObjectPtr<GFileInfo>& inf, const FilePath& parentDirPath) {
dirPath_ = parentDirPath;
const char* tmp, *uri;
GIcon* gicon;
GFileType type;
name_ = g_file_info_get_name(inf.get());
dispName_ = g_file_info_get_display_name(inf.get());
size_ = g_file_info_get_size(inf.get());
tmp = g_file_info_get_content_type(inf.get());
if(!tmp) {
tmp = "application/octet-stream";
}
mimeType_ = MimeType::fromName(tmp);
mode_ = g_file_info_get_attribute_uint32(inf.get(), G_FILE_ATTRIBUTE_UNIX_MODE);
uid_ = gid_ = -1;
if(g_file_info_has_attribute(inf.get(), G_FILE_ATTRIBUTE_UNIX_UID)) {
uid_ = g_file_info_get_attribute_uint32(inf.get(), G_FILE_ATTRIBUTE_UNIX_UID);
}
if(g_file_info_has_attribute(inf.get(), G_FILE_ATTRIBUTE_UNIX_GID)) {
gid_ = g_file_info_get_attribute_uint32(inf.get(), G_FILE_ATTRIBUTE_UNIX_GID);
}
type = g_file_info_get_file_type(inf.get());
if(0 == mode_) { /* if UNIX file mode is not available, compose a fake one. */
switch(type) {
case G_FILE_TYPE_REGULAR:
mode_ |= S_IFREG;
break;
case G_FILE_TYPE_DIRECTORY:
mode_ |= S_IFDIR;
break;
case G_FILE_TYPE_SYMBOLIC_LINK:
mode_ |= S_IFLNK;
break;
case G_FILE_TYPE_SHORTCUT:
break;
case G_FILE_TYPE_MOUNTABLE:
break;
case G_FILE_TYPE_SPECIAL:
if(mode_) {
break;
}
/* if it's a special file but it doesn't have UNIX mode, compose a fake one. */
if(strcmp(tmp, "inode/chardevice") == 0) {
mode_ |= S_IFCHR;
}
else if(strcmp(tmp, "inode/blockdevice") == 0) {
mode_ |= S_IFBLK;
}
else if(strcmp(tmp, "inode/fifo") == 0) {
mode_ |= S_IFIFO;
}
#ifdef S_IFSOCK
else if(strcmp(tmp, "inode/socket") == 0) {
mode_ |= S_IFSOCK;
}
#endif
break;
case G_FILE_TYPE_UNKNOWN:
;
}
}
if(g_file_info_has_attribute(inf.get(), G_FILE_ATTRIBUTE_ACCESS_CAN_READ)) {
isAccessible_ = g_file_info_get_attribute_boolean(inf.get(), G_FILE_ATTRIBUTE_ACCESS_CAN_READ);
}
else
/* assume it's accessible */
{
isAccessible_ = true;
}
if(g_file_info_has_attribute(inf.get(), G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE)) {
isWritable_ = g_file_info_get_attribute_boolean(inf.get(), G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE);
}
else
/* assume it's writable */
{
isWritable_ = true;
}
if(g_file_info_has_attribute(inf.get(), G_FILE_ATTRIBUTE_ACCESS_CAN_DELETE)) {
isDeletable_ = g_file_info_get_attribute_boolean(inf.get(), G_FILE_ATTRIBUTE_ACCESS_CAN_DELETE);
}
else
/* assume it's deletable */
{
isDeletable_ = true;
}
/* special handling for symlinks */
if(g_file_info_get_is_symlink(inf.get())) {
mode_ &= ~S_IFMT; /* reset type */
mode_ |= S_IFLNK; /* set type to symlink */
goto _file_is_symlink;
}
isShortcut_ = false;
switch(type) {
case G_FILE_TYPE_SHORTCUT:
isShortcut_ = true;
case G_FILE_TYPE_MOUNTABLE:
uri = g_file_info_get_attribute_string(inf.get(), G_FILE_ATTRIBUTE_STANDARD_TARGET_URI);
if(uri) {
if(g_str_has_prefix(uri, "file:///")) {
auto filename = CStrPtr{g_filename_from_uri(uri, nullptr, nullptr)};
target_ = filename.get();
}
else {
target_ = uri;
}
if(!mimeType_) {
mimeType_ = MimeType::guessFromFileName(target_.c_str());
}
}
/* if the mime-type is not determined or is unknown */
if(G_UNLIKELY(!mimeType_ || mimeType_->isUnknownType())) {
/* FIXME: is this appropriate? */
if(type == G_FILE_TYPE_SHORTCUT) {
mimeType_ = MimeType::inodeShortcut();
}
else {
mimeType_ = MimeType::inodeMountPoint();
}
}
break;
case G_FILE_TYPE_DIRECTORY:
if(!mimeType_) {
mimeType_ = MimeType::inodeDirectory();
}
isReadOnly_ = false; /* default is R/W */
if(g_file_info_has_attribute(inf.get(), G_FILE_ATTRIBUTE_FILESYSTEM_READONLY)) {
isReadOnly_ = g_file_info_get_attribute_boolean(inf.get(), G_FILE_ATTRIBUTE_FILESYSTEM_READONLY);
}
/* directories should be writable to be deleted by user */
if(isReadOnly_ || !isWritable_) {
isDeletable_ = false;
}
break;
case G_FILE_TYPE_SYMBOLIC_LINK:
_file_is_symlink:
uri = g_file_info_get_symlink_target(inf.get());
if(uri) {
if(g_str_has_prefix(uri, "file:///")) {
auto filename = CStrPtr{g_filename_from_uri(uri, nullptr, nullptr)};
target_ = filename.get();
}
else {
target_ = uri;
}
if(!mimeType_) {
mimeType_ = MimeType::guessFromFileName(target_.c_str());
}
}
/* continue with absent mime type */
default: /* G_FILE_TYPE_UNKNOWN G_FILE_TYPE_REGULAR G_FILE_TYPE_SPECIAL */
if(G_UNLIKELY(!mimeType_)) {
if(!mimeType_) {
mimeType_ = MimeType::guessFromFileName(name_.c_str());
}
}
}
/* if there is a custom folder icon, use it */
if(isNative() && type == G_FILE_TYPE_DIRECTORY) {
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) {
auto dot_icon = IconInfo::fromName(icon_name.get());
if(dot_icon && dot_icon->isValid()) {
icon_ = dot_icon;
}
}
}
g_key_file_free(kf);
}
}
if(!icon_) {
/* try file-specific icon first */
gicon = g_file_info_get_icon(inf.get());
if(gicon) {
icon_ = IconInfo::fromGIcon(gicon);
}
}
#if 0
/* set "locked" icon on unaccesible folder */
else if(!accessible && type == G_FILE_TYPE_DIRECTORY) {
icon = g_object_ref(icon_locked_folder);
}
else {
icon = g_object_ref(fm_mime_type_get_icon(mime_type));
}
#endif
/* if the file has emblems, add them to the icon */
auto emblem_names = g_file_info_get_attribute_stringv(inf.get(), "metadata::emblems");
if(emblem_names) {
auto n_emblems = g_strv_length(emblem_names);
for(int i = n_emblems - 1; i >= 0; --i) {
emblems_.emplace_front(Fm::IconInfo::fromName(emblem_names[i]));
}
}
tmp = g_file_info_get_attribute_string(inf.get(), G_FILE_ATTRIBUTE_ID_FILESYSTEM);
filesystemId_ = g_intern_string(tmp);
mtime_ = g_file_info_get_attribute_uint64(inf.get(), G_FILE_ATTRIBUTE_TIME_MODIFIED);
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());
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)) {
isNameChangeable_ = g_file_info_get_attribute_boolean(inf.get(), G_FILE_ATTRIBUTE_ACCESS_CAN_RENAME);
}
// special handling for desktop entry files (show the name and icon defined in the desktop entry instead)
if(isNative() && G_UNLIKELY(isDesktopEntry())) {
auto local_path = path().localPath();
GKeyFile* kf = g_key_file_new();
if(g_key_file_load_from_file(kf, local_path.get(), G_KEY_FILE_NONE, nullptr)) {
/* check if type is correct and supported */
CStrPtr type{g_key_file_get_string(kf, "Desktop Entry", "Type", nullptr)};
if(type) {
// Type == "Link"
if(strcmp(type.get(), G_KEY_FILE_DESKTOP_TYPE_LINK) == 0) {
CStrPtr uri{g_key_file_get_string(kf, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_URL, nullptr)};
if(uri) {
isShortcut_ = true;
target_ = uri.get();
}
}
}
CStrPtr icon_name{g_key_file_get_string(kf, "Desktop Entry", "Icon", nullptr)};
if(icon_name) {
icon_ = IconInfo::fromName(icon_name.get());
}
/* Use title of the desktop entry for display */
CStrPtr displayName{g_key_file_get_locale_string(kf, "Desktop Entry", "Name", nullptr, nullptr)};
if(displayName) {
dispName_ = displayName.get();
}
/* handle 'Hidden' key to set hidden attribute */
if(!isHidden_) {
isHidden_ = g_key_file_get_boolean(kf, "Desktop Entry", "Hidden", nullptr);
}
}
g_key_file_free(kf);
}
if(!icon_ && mimeType_)
icon_ = mimeType_->icon();
#if 0
GFile* _gf = nullptr;
GFileAttributeInfoList* list;
auto list = g_file_query_settable_attributes(gf, nullptr, nullptr);
if(G_LIKELY(list)) {
if(g_file_attribute_info_list_lookup(list, G_FILE_ATTRIBUTE_STANDARD_ICON)) {
icon_is_changeable = true;
}
if(g_file_attribute_info_list_lookup(list, G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN)) {
hidden_is_changeable = true;
}
g_file_attribute_info_list_unref(list);
}
if(G_UNLIKELY(_gf)) {
g_object_unref(_gf);
}
#endif
}
void FileInfo::bindCutFiles(const std::shared_ptr<const HashSet>& cutFilesHashSet) {
cutFilesHashSet_ = cutFilesHashSet;
}
bool FileInfo::canThumbnail() const {
/* We cannot use S_ISREG here as this exclude all symlinks */
if(size_ == 0 || /* don't generate thumbnails for empty files */
!(mode_ & S_IFREG) ||
isDesktopEntry() ||
isUnknownType()) {
return false;
}
return true;
}
/* 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 */
/* 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 */
auto pathStr = path().toString();
int fd = open(pathStr.get(), O_RDONLY);
if(fd >= 0) {
char buf[2];
ssize_t rdlen = read(fd, &buf, 2);
close(fd);
if(rdlen == 2 && buf[0] == '#' && buf[1] == '!') {
return true;
}
}
}
return false;
}
return mimeType_->canBeExecutable();
}
bool FileInfoList::isSameType() const {
if(!empty()) {
auto& item = front();
for(auto it = cbegin() + 1; it != cend(); ++it) {
auto& item2 = *it;
if(item->mimeType() != item2->mimeType()) {
return false;
}
}
}
return true;
}
bool FileInfoList::isSameFilesystem() const {
if(!empty()) {
auto& item = front();
for(auto it = cbegin() + 1; it != cend(); ++it) {
auto& item2 = *it;
if(item->filesystemId() != item2->filesystemId()) {
return false;
}
}
}
return true;
}
} // namespace Fm

@ -0,0 +1,278 @@
/*
* Copyright (C) 2016 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
*
* 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_FM2_FILE_INFO_H__
#define __LIBFM_QT_FM2_FILE_INFO_H__
#include <libfm/fm.h>
#include <QObject>
#include <QtGlobal>
#include "../libfmqtglobals.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <vector>
#include <set>
#include <utility>
#include <string>
#include <forward_list>
#include "gioptrs.h"
#include "filepath.h"
#include "iconinfo.h"
#include "mimetype.h"
namespace Fm {
class FileInfoList;
typedef std::set<unsigned int> HashSet;
class LIBFM_QT_API FileInfo {
public:
explicit FileInfo();
explicit FileInfo(const GFileInfoPtr& inf, const FilePath& parentDirPath);
virtual ~FileInfo();
bool canSetHidden() const {
return isHiddenChangeable_;
}
bool canSetIcon() const {
return isIconChangeable_;
}
bool canSetName() const {
return isNameChangeable_;
}
bool canThumbnail() const;
gid_t gid() const {
return gid_;
}
uid_t uid() const {
return uid_;
}
const char* filesystemId() const {
return filesystemId_;
}
const std::shared_ptr<const IconInfo>& icon() const {
return icon_;
}
const std::shared_ptr<const MimeType>& mimeType() const {
return mimeType_;
}
time_t ctime() const {
return ctime_;
}
time_t atime() const {
return atime_;
}
time_t mtime() const {
return mtime_;
}
const std::string& target() const {
return target_;
}
bool isWritableDirectory() const {
return (!isReadOnly_ && isDir());
}
bool isAccessible() const {
return isAccessible_;
}
bool isWritable() const {
return isWritable_;
}
bool isDeletable() const {
return isDeletable_;
}
bool isExecutableType() const;
bool isBackup() const {
return isBackup_;
}
bool isHidden() const {
// FIXME: we might treat backup files as hidden
return isHidden_;
}
bool isUnknownType() const {
return mimeType_->isUnknownType();
}
bool isDesktopEntry() const {
return mimeType_->isDesktopEntry();
}
bool isText() const {
return mimeType_->isText();
}
bool isImage() const {
return mimeType_->isImage();
}
bool isMountable() const {
return mimeType_->isMountable();
}
bool isShortcut() const {
return isShortcut_;
}
bool isSymlink() const {
return S_ISLNK(mode_) ? true : false;
}
bool isDir() const {
return mimeType_->isDir();
}
bool isNative() const {
return dirPath_ ? dirPath_.isNative() : path().isNative();
}
bool isCut() const {
return !cutFilesHashSet_.expired();
}
mode_t mode() const {
return mode_;
}
uint64_t realSize() const {
return blksize_ *blocks_;
}
uint64_t size() const {
return size_;
}
const std::string& name() const {
return name_;
}
const QString& displayName() const {
return dispName_;
}
FilePath path() const {
return dirPath_ ? dirPath_.child(name_.c_str()) : FilePath::fromPathStr(name_.c_str());
}
const FilePath& dirPath() const {
return dirPath_;
}
void setFromGFileInfo(const GFileInfoPtr& inf, const FilePath& parentDirPath);
void bindCutFiles(const std::shared_ptr<const HashSet>& cutFilesHashSet);
const std::forward_list<std::shared_ptr<const IconInfo>>& emblems() const {
return emblems_;
}
private:
std::string name_;
QString dispName_;
FilePath dirPath_;
mode_t mode_;
const char* filesystemId_;
uid_t uid_;
gid_t gid_;
uint64_t size_;
time_t mtime_;
time_t atime_;
time_t ctime_;
uint64_t blksize_;
uint64_t blocks_;
std::shared_ptr<const MimeType> mimeType_;
std::shared_ptr<const IconInfo> icon_;
std::forward_list<std::shared_ptr<const IconInfo>> emblems_;
std::string target_; /* target of shortcut or mountable. */
bool isShortcut_ : 1; /* TRUE if file is shortcut type */
bool isAccessible_ : 1; /* TRUE if can be read by user */
bool isWritable_ : 1; /* TRUE if can be written to by user */
bool isDeletable_ : 1; /* TRUE if can be deleted by user */
bool isHidden_ : 1; /* TRUE if file is hidden */
bool isBackup_ : 1; /* TRUE if file is backup */
bool isNameChangeable_ : 1; /* TRUE if name can be changed */
bool isIconChangeable_ : 1; /* TRUE if icon can be changed */
bool isHiddenChangeable_ : 1; /* TRUE if hidden can be changed */
bool isReadOnly_ : 1; /* TRUE if host FS is R/O */
std::weak_ptr<const HashSet> cutFilesHashSet_;
// std::vector<std::tuple<int, void*, void(void*)>> extraData_;
};
class LIBFM_QT_API FileInfoList: public std::vector<std::shared_ptr<const FileInfo>> {
public:
bool isSameType() const;
bool isSameFilesystem() const;
FilePathList paths() const {
FilePathList ret;
for(auto& file: *this) {
ret.push_back(file->path());
}
return ret;
}
};
typedef std::pair<std::shared_ptr<const FileInfo>, std::shared_ptr<const FileInfo>> FileInfoPair;
}
Q_DECLARE_METATYPE(std::shared_ptr<const Fm::FileInfo>)
#endif // __LIBFM_QT_FM2_FILE_INFO_H__

@ -0,0 +1,10 @@
#ifndef FILEINFO_P_H
#define FILEINFO_P_H
namespace Fm {
extern const char gfile_info_query_attribs[];
} // namespace Fm
#endif // FILEINFO_P_H

@ -0,0 +1,42 @@
#include "fileinfojob.h"
#include "fileinfo_p.h"
namespace Fm {
FileInfoJob::FileInfoJob(FilePathList paths, FilePath commonDirPath, const std::shared_ptr<const HashSet>& cutFilesHashSet):
Job(),
paths_{std::move(paths)},
commonDirPath_{std::move(commonDirPath)},
cutFilesHashSet_{cutFilesHashSet} {
}
void FileInfoJob::exec() {
for(const auto& path: paths_) {
if(!isCancelled()) {
GErrorPtr err;
GFileInfoPtr inf{
g_file_query_info(path.gfile().get(), gfile_info_query_attribs,
G_FILE_QUERY_INFO_NONE, cancellable().get(), &err),
false
};
if(!inf)
return;
// 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();
FileInfo fileInfo(inf, dirPath);
if(cutFilesHashSet_
&& cutFilesHashSet_->count(fileInfo.path().hash())) {
fileInfo.bindCutFiles(cutFilesHashSet_);
}
auto fileInfoPtr = std::make_shared<const FileInfo>(fileInfo);
results_.push_back(fileInfoPtr);
Q_EMIT gotInfo(path, fileInfoPtr);
}
}
}
} // namespace Fm

@ -0,0 +1,41 @@
#ifndef FM2_FILEINFOJOB_H
#define FM2_FILEINFOJOB_H
#include "../libfmqtglobals.h"
#include "job.h"
#include "filepath.h"
#include "fileinfo.h"
namespace Fm {
class LIBFM_QT_API FileInfoJob : public Job {
Q_OBJECT
public:
explicit FileInfoJob(FilePathList paths, FilePath commonDirPath = FilePath(), const std::shared_ptr<const HashSet>& cutFilesHashSet = nullptr);
const FilePathList& paths() const {
return paths_;
}
const FileInfoList& files() const {
return results_;
}
Q_SIGNALS:
void gotInfo(const FilePath& path, std::shared_ptr<const FileInfo>& info);
protected:
void exec() override;
private:
FilePathList paths_;
FileInfoList results_;
FilePath commonDirPath_;
const std::shared_ptr<const HashSet> cutFilesHashSet_;
};
} // namespace Fm
#endif // FM2_FILEINFOJOB_H

@ -0,0 +1,9 @@
#include "filelinkjob.h"
namespace Fm {
FileLinkJob::FileLinkJob() {
}
} // namespace Fm

@ -0,0 +1,16 @@
#ifndef FM2_FILELINKJOB_H
#define FM2_FILELINKJOB_H
#include "../libfmqtglobals.h"
#include "fileoperationjob.h"
namespace Fm {
class LIBFM_QT_API FileLinkJob : public Fm::FileOperationJob {
public:
explicit FileLinkJob();
};
} // namespace Fm
#endif // FM2_FILELINKJOB_H

@ -0,0 +1,9 @@
#include "filemonitor.h"
namespace Fm {
FileMonitor::FileMonitor() {
}
} // namespace Fm

@ -0,0 +1,26 @@
#ifndef FM2_FILEMONITOR_H
#define FM2_FILEMONITOR_H
#include "../libfmqtglobals.h"
#include <QObject>
#include "gioptrs.h"
#include "filepath.h"
namespace Fm {
class LIBFM_QT_API FileMonitor: public QObject {
Q_OBJECT
public:
explicit FileMonitor();
Q_SIGNALS:
private:
GFileMonitorPtr monitor_;
};
} // namespace Fm
#endif // FM2_FILEMONITOR_H

@ -0,0 +1,79 @@
#include "fileoperationjob.h"
namespace Fm {
FileOperationJob::FileOperationJob():
hasTotalAmount_{false},
totalSize_{0},
totalCount_{0},
finishedSize_{0},
finishedCount_{0},
currentFileSize_{0},
currentFileFinished_{0} {
}
bool FileOperationJob::totalAmount(uint64_t& fileSize, uint64_t& fileCount) const {
std::lock_guard<std::mutex> lock{mutex_};
if(hasTotalAmount_) {
fileSize = totalSize_;
fileCount = totalCount_;
}
return hasTotalAmount_;
}
bool FileOperationJob::currentFileProgress(FilePath& path, uint64_t& totalSize, uint64_t& finishedSize) const {
std::lock_guard<std::mutex> lock{mutex_};
if(currentFile_.isValid()) {
path = currentFile_;
totalSize = currentFileSize_;
finishedSize = currentFileFinished_;
}
return currentFile_.isValid();
}
FileOperationJob::FileExistsAction FileOperationJob::askRename(const FileInfo &src, const FileInfo &dest, FilePath &newDest) {
FileExistsAction action = SKIP;
Q_EMIT fileExists(src, dest, action, newDest);
return action;
}
bool FileOperationJob::finishedAmount(uint64_t& finishedSize, uint64_t& finishedCount) const {
std::lock_guard<std::mutex> lock{mutex_};
if(hasTotalAmount_) {
finishedSize = finishedSize_;
finishedCount = finishedCount_;
}
return hasTotalAmount_;
}
void FileOperationJob::setTotalAmount(uint64_t fileSize, uint64_t fileCount) {
std::lock_guard<std::mutex> locl{mutex_};
hasTotalAmount_ = true;
totalSize_ = fileSize;
totalCount_ = fileCount;
}
void FileOperationJob::setFinishedAmount(uint64_t finishedSize, uint64_t finishedCount) {
std::lock_guard<std::mutex> locl{mutex_};
finishedSize_ = finishedSize;
finishedCount_ = finishedCount;
}
void FileOperationJob::addFinishedAmount(uint64_t finishedSize, uint64_t finishedCount) {
std::lock_guard<std::mutex> locl{mutex_};
finishedSize_ += finishedSize;
finishedCount_ += finishedCount;
}
void FileOperationJob::setCurrentFile(const FilePath& path) {
std::lock_guard<std::mutex> locl{mutex_};
currentFile_ = path;
}
void FileOperationJob::setCurrentFileProgress(uint64_t totalSize, uint64_t finishedSize) {
std::lock_guard<std::mutex> locl{mutex_};
currentFileSize_ = totalSize;
currentFileFinished_ = finishedSize;
}
} // namespace Fm

@ -0,0 +1,73 @@
#ifndef FM2_FILEOPERATIONJOB_H
#define FM2_FILEOPERATIONJOB_H
#include "../libfmqtglobals.h"
#include "job.h"
#include <string>
#include <mutex>
#include <cstdint>
#include "fileinfo.h"
#include "filepath.h"
namespace Fm {
class LIBFM_QT_API FileOperationJob : public Fm::Job {
Q_OBJECT
public:
enum FileExistsAction {
CANCEL = 0,
OVERWRITE = 1<<0,
RENAME = 1<<1,
SKIP = 1<<2,
SKIP_ERROR = 1<<3
};
explicit FileOperationJob();
bool totalAmount(std::uint64_t& fileSize, std::uint64_t& fileCount) const;
bool finishedAmount(std::uint64_t& finishedSize, std::uint64_t& finishedCount) const;
bool currentFileProgress(FilePath& path, std::uint64_t& totalSize, std::uint64_t& finishedSize) const;
Q_SIGNALS:
void preparedToRun();
// void currentFile(const char* file);
// void progress(uint32_t percent);
// to correctly handle the signal, connect with Qt::BlockingQueuedConnection so it's a sync call.
void fileExists(const FileInfo& src, const FileInfo& dest, FileExistsAction& response, FilePath& newDest);
protected:
FileExistsAction askRename(const FileInfo& src, const FileInfo& dest, FilePath& newDest);
void setTotalAmount(std::uint64_t fileSize, std::uint64_t fileCount);
void setFinishedAmount(std::uint64_t finishedSize, std::uint64_t finishedCount);
void addFinishedAmount(std::uint64_t finishedSize, std::uint64_t finishedCount);
void setCurrentFile(const FilePath &path);
void setCurrentFileProgress(uint64_t totalSize, uint64_t finishedSize);
private:
bool hasTotalAmount_;
std::uint64_t totalSize_;
std::uint64_t totalCount_;
std::uint64_t finishedSize_;
std::uint64_t finishedCount_;
FilePath currentFile_;
std::uint64_t currentFileSize_;
std::uint64_t currentFileFinished_;
mutable std::mutex mutex_;
};
} // namespace Fm
#endif // FM2_FILEOPERATIONJOB_H

@ -0,0 +1,21 @@
#include "filepath.h"
#include <cstdlib>
#include <utility>
#include <glib.h>
namespace Fm {
FilePath FilePath::homeDir_;
const FilePath &FilePath::homeDir() {
if(!homeDir_) {
const char* home = getenv("HOME");
if(!home) {
home = g_get_home_dir();
}
homeDir_ = FilePath::fromLocalPath(home);
}
return homeDir_;
}
} // namespace Fm

@ -0,0 +1,177 @@
#ifndef FM2_FILEPATH_H
#define FM2_FILEPATH_H
#include "../libfmqtglobals.h"
#include "gobjectptr.h"
#include "cstrptr.h"
#include <gio/gio.h>
#include <vector>
#include <QMetaType>
namespace Fm {
class LIBFM_QT_API FilePath {
public:
explicit FilePath() {
}
explicit FilePath(GFile* gfile, bool add_ref): gfile_{gfile, add_ref} {
}
FilePath(const FilePath& other): FilePath{} {
*this = other;
}
FilePath(FilePath&& other) noexcept: FilePath{} {
*this = other;
}
static FilePath fromUri(const char* uri) {
return FilePath{g_file_new_for_uri(uri), false};
}
static FilePath fromLocalPath(const char* path) {
return FilePath{g_file_new_for_path(path), false};
}
static FilePath fromDisplayName(const char* path) {
return FilePath{g_file_parse_name(path), false};
}
static FilePath fromPathStr(const char* path_str) {
return FilePath{g_file_new_for_commandline_arg(path_str), false};
}
bool isValid() const {
return gfile_ != nullptr;
}
unsigned int hash() const {
return g_file_hash(gfile_.get());
}
CStrPtr baseName() const {
return CStrPtr{g_file_get_basename(gfile_.get())};
}
CStrPtr localPath() const {
return CStrPtr{g_file_get_path(gfile_.get())};
}
CStrPtr uri() const {
return CStrPtr{g_file_get_uri(gfile_.get())};
}
CStrPtr toString() const {
if(isNative()) {
return localPath();
}
return uri();
}
// a human readable UTF-8 display name for the path
CStrPtr displayName() const {
return CStrPtr{g_file_get_parse_name(gfile_.get())};
}
FilePath parent() const {
return FilePath{g_file_get_parent(gfile_.get()), false};
}
bool hasParent() const {
return g_file_has_parent(gfile_.get(), nullptr);
}
bool isParentOf(const FilePath& other) {
return g_file_has_parent(other.gfile_.get(), gfile_.get());
}
bool isPrefixOf(const FilePath& other) {
return g_file_has_prefix(other.gfile_.get(), gfile_.get());
}
FilePath child(const char* name) const {
return FilePath{g_file_get_child(gfile_.get(), name), false};
}
CStrPtr relativePathStr(const FilePath& descendant) const {
return CStrPtr{g_file_get_relative_path(gfile_.get(), descendant.gfile_.get())};
}
FilePath relativePath(const char* relPath) const {
return FilePath{g_file_resolve_relative_path(gfile_.get(), relPath), false};
}
bool isNative() const {
return g_file_is_native(gfile_.get());
}
bool hasUriScheme(const char* scheme) const {
return g_file_has_uri_scheme(gfile_.get(), scheme);
}
CStrPtr uriScheme() const {
return CStrPtr{g_file_get_uri_scheme(gfile_.get())};
}
const GObjectPtr<GFile>& gfile() const {
return gfile_;
}
FilePath& operator = (const FilePath& other) {
gfile_ = other.gfile_;
return *this;
}
FilePath& operator = (const FilePath&& other) noexcept {
gfile_ = std::move(other.gfile_);
return *this;
}
bool operator == (const FilePath& other) const {
return operator==(other.gfile_.get());
}
bool operator == (GFile* other_gfile) const {
if(gfile_ == other_gfile) {
return true;
}
if(gfile_ && other_gfile) {
return g_file_equal(gfile_.get(), other_gfile);
}
return false;
}
bool operator != (const FilePath& other) const {
return !operator==(other);
}
bool operator != (std::nullptr_t) const {
return gfile_ != nullptr;
}
operator bool() const {
return gfile_ != nullptr;
}
static const FilePath& homeDir();
private:
GObjectPtr<GFile> gfile_;
static FilePath homeDir_;
};
struct FilePathHash {
std::size_t operator() (const FilePath& path) const {
return path.hash();
}
};
typedef std::vector<FilePath> FilePathList;
} // namespace Fm
Q_DECLARE_METATYPE(Fm::FilePath)
#endif // FM2_FILEPATH_H

@ -0,0 +1,24 @@
#include "filesysteminfojob.h"
#include "gobjectptr.h"
namespace Fm {
void FileSystemInfoJob::exec() {
GObjectPtr<GFileInfo> inf = GObjectPtr<GFileInfo>{
g_file_query_filesystem_info(
path_.gfile().get(),
G_FILE_ATTRIBUTE_FILESYSTEM_SIZE","
G_FILE_ATTRIBUTE_FILESYSTEM_FREE,
cancellable().get(), nullptr),
false
};
if(!inf)
return;
if(g_file_info_has_attribute(inf.get(), G_FILE_ATTRIBUTE_FILESYSTEM_SIZE)) {
size_ = g_file_info_get_attribute_uint64(inf.get(), G_FILE_ATTRIBUTE_FILESYSTEM_SIZE);
freeSize_ = g_file_info_get_attribute_uint64(inf.get(), G_FILE_ATTRIBUTE_FILESYSTEM_FREE);
isAvailable_ = true;
}
}
} // namespace Fm

@ -0,0 +1,45 @@
#ifndef FM2_FILESYSTEMINFOJOB_H
#define FM2_FILESYSTEMINFOJOB_H
#include "../libfmqtglobals.h"
#include "job.h"
#include "filepath.h"
namespace Fm {
class LIBFM_QT_API FileSystemInfoJob : public Job {
Q_OBJECT
public:
explicit FileSystemInfoJob(const FilePath& path):
path_{path},
isAvailable_{false},
size_{0},
freeSize_{0} {
}
bool isAvailable() const {
return isAvailable_;
}
uint64_t size() const {
return size_;
}
uint64_t freeSize() const {
return freeSize_;
}
protected:
void exec() override;
private:
FilePath path_;
bool isAvailable_;
uint64_t size_;
uint64_t freeSize_;
};
} // namespace Fm
#endif // FM2_FILESYSTEMINFOJOB_H

@ -0,0 +1,908 @@
/*
* fm-folder.c
*
* Copyright 2009 - 2012 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
* Copyright 2012-2016 Andriy Grytsenko (LStranger) <andrej@rep.kiev.ua>
*
* This file is a part of the Libfm library.
*
* 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 "folder.h"
#include <string.h>
#include <cassert>
#include <QTimer>
#include <QDebug>
#include "dirlistjob.h"
#include "filesysteminfojob.h"
#include "fileinfojob.h"
namespace Fm {
std::unordered_map<FilePath, std::weak_ptr<Folder>, FilePathHash> Folder::cache_;
FilePath Folder::cutFilesDirPath_;
FilePath Folder::lastCutFilesDirPath_;
std::shared_ptr<const HashSet> Folder::cutFilesHashSet_;
std::mutex Folder::mutex_;
Folder::Folder():
dirlist_job{nullptr},
fsInfoJob_{nullptr},
volumeManager_{VolumeManager::globalInstance()},
/* for file monitor */
has_idle_reload_handler{0},
has_idle_update_handler{false},
pending_change_notify{false},
filesystem_info_pending{false},
wants_incremental{false},
stop_emission{false}, /* don't set it 1 bit to not lock other bits */
/* filesystem info - set in query thread, read in main */
fs_total_size{0},
fs_free_size{0},
has_fs_info{false},
defer_content_test{false} {
connect(volumeManager_.get(), &VolumeManager::mountAdded, this, &Folder::onMountAdded);
connect(volumeManager_.get(), &VolumeManager::mountRemoved, this, &Folder::onMountRemoved);
}
Folder::Folder(const FilePath& path): Folder() {
dirPath_ = path;
}
Folder::~Folder() {
if(dirMonitor_) {
g_signal_handlers_disconnect_by_data(dirMonitor_.get(), this);
dirMonitor_.reset();
}
if(dirlist_job) {
dirlist_job->cancel();
}
// cancel any file info job in progress.
for(auto job: fileinfoJobs_) {
job->cancel();
}
if(fsInfoJob_) {
fsInfoJob_->cancel();
}
// We store a weak_ptr instead of shared_ptr in the hash table, so the hash table
// does not own a reference to the folder. When the last reference to Folder is
// freed, we need to remove its hash table entry.
std::lock_guard<std::mutex> lock{mutex_};
auto it = cache_.find(dirPath_);
if(it != cache_.end()) {
cache_.erase(it);
}
}
// static
std::shared_ptr<Folder> Folder::fromPath(const FilePath& path) {
std::lock_guard<std::mutex> lock{mutex_};
auto it = cache_.find(path);
if(it != cache_.end()) {
auto folder = it->second.lock();
if(folder) {
return folder;
}
else { // FIXME: is this possible?
cache_.erase(it);
}
}
auto folder = std::make_shared<Folder>(path);
folder->reload();
cache_.emplace(path, folder);
return folder;
}
bool Folder::makeDirectory(const char* /*name*/, GError** /*error*/) {
// TODO:
// FIXME: what the API is used for in the original libfm C API?
return false;
}
bool Folder::isIncremental() const {
return wants_incremental;
}
bool Folder::isValid() const {
return dirInfo_ != nullptr;
}
bool Folder::isLoaded() const {
return (dirlist_job == nullptr);
}
std::shared_ptr<const FileInfo> Folder::fileByName(const char* name) const {
auto it = files_.find(name);
if(it != files_.end()) {
return it->second;
}
return nullptr;
}
bool Folder::isEmpty() const {
return files_.empty();
}
FileInfoList Folder::files() const {
FileInfoList ret;
ret.reserve(files_.size());
for(const auto& item : files_) {
ret.push_back(item.second);
}
return ret;
}
const FilePath& Folder::path() const {
auto pathStr = dirPath_.toString();
// qDebug() << this << "FOLDER_PATH:" << pathStr.get() << dirPath_.gfile().get();
//assert(!g_str_has_prefix(pathStr.get(), "file:"));
return dirPath_;
}
const std::shared_ptr<const FileInfo>& Folder::info() const {
return dirInfo_;
}
#if 0
void Folder::init(FmFolder* folder) {
files = fm_file_info_list_new();
G_LOCK(hash);
if(G_UNLIKELY(hash_uses == 0)) {
hash = g_hash_table_new((GHashFunc)fm_path_hash, (GEqualFunc)fm_path_equal);
volume_monitor = g_volume_monitor_get();
if(G_LIKELY(volume_monitor)) {
g_signal_connect(volume_monitor, "mount-added", G_CALLBACK(on_mount_added), nullptr);
g_signal_connect(volume_monitor, "mount-removed", G_CALLBACK(on_mount_removed), nullptr);
}
}
hash_uses++;
G_UNLOCK(hash);
}
#endif
void Folder::onIdleReload() {
/* check if folder still exists */
reload();
// G_LOCK(query);
has_idle_reload_handler = false;
// G_UNLOCK(query);
}
void Folder::queueReload() {
// G_LOCK(query);
if(!has_idle_reload_handler) {
has_idle_reload_handler = true;
QTimer::singleShot(0, this, &Folder::onIdleReload);
}
// G_UNLOCK(query);
}
void Folder::onFileInfoFinished() {
FileInfoJob* job = static_cast<FileInfoJob*>(sender());
fileinfoJobs_.erase(std::find(fileinfoJobs_.cbegin(), fileinfoJobs_.cend(), job));
if(job->isCancelled())
return;
FileInfoList files_to_add;
std::vector<FileInfoPair> files_to_update;
const auto& paths = job->paths();
const auto& infos = job->files();
auto path_it = paths.cbegin();
auto info_it = infos.cbegin();
for(; path_it != paths.cend() && info_it != infos.cend(); ++path_it, ++info_it) {
const auto& path = *path_it;
const auto& info = *info_it;
if(path == dirPath_) { // got the info for the folder itself.
dirInfo_ = info;
}
else {
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));
}
else { // newly added
files_to_add.push_back(info);
}
files_[info->name()] = info;
}
}
if(!files_to_add.empty()) {
Q_EMIT filesAdded(files_to_add);
}
if(!files_to_update.empty()) {
Q_EMIT filesChanged(files_to_update);
}
Q_EMIT contentChanged();
}
void Folder::processPendingChanges() {
has_idle_update_handler = false;
// FmFileInfoJob* job = nullptr;
std::lock_guard<std::mutex> lock{mutex_};
// idle_handler = 0;
/* if we were asked to block updates let delay it for now */
if(stop_emission) {
return;
}
FileInfoJob* info_job = nullptr;
if(!paths_to_update.empty() || !paths_to_add.empty()) {
FilePathList paths;
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};
paths_to_update.clear();
paths_to_add.clear();
}
if(info_job) {
fileinfoJobs_.push_back(info_job);
info_job->setAutoDelete(true);
connect(info_job, &FileInfoJob::finished, this, &Folder::onFileInfoFinished, Qt::BlockingQueuedConnection);
info_job->runAsync();
#if 0
pending_jobs = g_slist_prepend(pending_jobs, job);
if(!fm_job_run_async(FM_JOB(job))) {
pending_jobs = g_slist_remove(pending_jobs, job);
g_object_unref(job);
g_critical("failed to start folder update job");
}
#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 */
queryFilesystemInfo();
pending_change_notify = false;
}
if(filesystem_info_pending) {
Q_EMIT fileSystemChanged();
filesystem_info_pending = false;
}
}
/* should be called only with G_LOCK(lists) on! */
void Folder::queueUpdate() {
// qDebug() << "queue_update:" << !has_idle_handler << paths_to_add.size() << paths_to_update.size() << paths_to_del.size();
if(!has_idle_update_handler) {
QTimer::singleShot(0, this, &Folder::processPendingChanges);
has_idle_update_handler = true;
}
}
/* returns true if reference was taken from path */
bool Folder::eventFileAdded(const FilePath &path) {
bool added = true;
// G_LOCK(lists);
/* make sure that the file is not already queued for addition. */
if(std::find(paths_to_add.cbegin(), paths_to_add.cend(), path) == paths_to_add.cend()) {
if(files_.find(path.baseName().get()) != files_.end()) { // the file already exists, update instead
if(std::find(paths_to_update.cbegin(), paths_to_update.cend(), path) == paths_to_update.cend()) {
paths_to_update.push_back(path);
}
}
else { // newly added file
paths_to_add.push_back(path);
}
/* bug #3591771: 'ln -fns . test' leave no file visible in folder.
If it is queued for deletion then cancel that operation */
paths_to_del.erase(std::remove(paths_to_del.begin(), paths_to_del.end(), path), paths_to_del.cend());
}
else
/* file already queued for adding, don't duplicate */
{
added = false;
}
if(added) {
queueUpdate();
}
// G_UNLOCK(lists);
return added;
}
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. */
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()) {
/* 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. */
paths_to_update.push_back(path);
added = true;
queueUpdate();
}
else {
added = false;
}
// G_UNLOCK(lists);
return added;
}
void Folder::eventFileDeleted(const FilePath& path) {
// qDebug() << "delete " << path.baseName().get();
// G_LOCK(lists);
if(files_.find(path.baseName().get()) != files_.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());
paths_to_update.erase(std::remove(paths_to_update.begin(), paths_to_update.end(), path), paths_to_update.cend());
queueUpdate();
// G_UNLOCK(lists);
}
void Folder::onDirChanged(GFileMonitorEvent evt) {
switch(evt) {
case G_FILE_MONITOR_EVENT_PRE_UNMOUNT:
/* g_debug("folder is going to be unmounted"); */
break;
case G_FILE_MONITOR_EVENT_UNMOUNTED:
Q_EMIT unmount();
/* g_debug("folder is unmounted"); */
queueReload();
break;
case G_FILE_MONITOR_EVENT_DELETED:
Q_EMIT removed();
/* g_debug("folder is deleted"); */
break;
case G_FILE_MONITOR_EVENT_CREATED:
queueReload();
break;
case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED:
case G_FILE_MONITOR_EVENT_CHANGED: {
std::lock_guard<std::mutex> lock{mutex_};
pending_change_notify = true;
if(std::find(paths_to_update.cbegin(), paths_to_update.cend(), dirPath_) != paths_to_update.cend()) {
paths_to_update.push_back(dirPath_);
queueUpdate();
}
/* g_debug("folder is changed"); */
break;
}
#if GLIB_CHECK_VERSION(2,24,0)
case G_FILE_MONITOR_EVENT_MOVED:
#endif
case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT:
;
default:
break;
}
}
void Folder::onFileChangeEvents(GFileMonitor* /*monitor*/, GFile* gf, GFile* /*other_file*/, GFileMonitorEvent evt) {
/* const char* names[]={
"G_FILE_MONITOR_EVENT_CHANGED",
"G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT",
"G_FILE_MONITOR_EVENT_DELETED",
"G_FILE_MONITOR_EVENT_CREATED",
"G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED",
"G_FILE_MONITOR_EVENT_PRE_UNMOUNT",
"G_FILE_MONITOR_EVENT_UNMOUNTED"
}; */
if(dirPath_ == gf) {
onDirChanged(evt);
return;
}
else {
std::lock_guard<std::mutex> lock{mutex_};
auto path = FilePath{gf, true};
/* NOTE: sometimes, for unknown reasons, GFileMonitor gives us the
* same event of the same file for multiple times. So we need to
* check for duplications ourselves here. */
switch(evt) {
case G_FILE_MONITOR_EVENT_CREATED:
eventFileAdded(path);
break;
case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED:
case G_FILE_MONITOR_EVENT_CHANGED:
eventFileChanged(path);
break;
case G_FILE_MONITOR_EVENT_DELETED:
eventFileDeleted(path);
break;
default:
return;
}
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();
return true;
}
return false;
}
bool Folder::hasCutFiles() {
return cutFilesHashSet_
&& !cutFilesHashSet_->empty()
&& cutFilesDirPath_ == dirPath_;
}
void Folder::setCutFiles(const std::shared_ptr<const HashSet>& cutFilesHashSet) {
if(cutFilesHashSet_ && !cutFilesHashSet_->empty()) {
lastCutFilesDirPath_ = cutFilesDirPath_;
}
cutFilesDirPath_ = dirPath_;
cutFilesHashSet_ = cutFilesHashSet;
}
void Folder::onDirListFinished() {
DirListJob* job = static_cast<DirListJob*>(sender());
if(job->isCancelled()) { // this is a cancelled job, ignore!
if(job == dirlist_job) {
dirlist_job = nullptr;
}
Q_EMIT finishLoading();
return;
}
dirInfo_ = job->dirInfo();
FileInfoList files_to_add;
std::vector<FileInfoPair> files_to_update;
const auto& infos = job->files();
// with "search://", there is no update for infos and all of them should be added
if(strcmp(dirPath_.uriScheme().get(), "search") == 0) {
files_to_add = infos;
for(auto& file: files_to_add) {
files_[file->name()] = file;
}
}
else {
auto info_it = infos.cbegin();
for(; info_it != infos.cend(); ++info_it) {
const auto& info = *info_it;
auto it = files_.find(info->name());
if(it != files_.end()) {
files_to_update.push_back(std::make_pair(it->second, info));
}
else {
files_to_add.push_back(info);
}
files_[info->name()] = info;
}
}
if(!files_to_add.empty()) {
Q_EMIT filesAdded(files_to_add);
}
if(!files_to_update.empty()) {
Q_EMIT filesChanged(files_to_update);
}
#if 0
if(dirlist_job->isCancelled() && !wants_incremental) {
GList* l;
for(l = fm_file_info_list_peek_head_link(job->files); l; l = l->next) {
FmFileInfo* inf = (FmFileInfo*)l->data;
files = g_slist_prepend(files, inf);
fm_file_info_list_push_tail(files, inf);
}
if(G_LIKELY(files)) {
GSList* l;
G_LOCK(lists);
if(defer_content_test && fm_path_is_native(dir_path))
/* we got only basic info on content, schedule update it now */
for(l = files; l; l = l->next)
files_to_update = g_slist_prepend(files_to_update,
fm_path_ref(fm_file_info_get_path(l->data)));
G_UNLOCK(lists);
g_signal_emit(folder, signals[FILES_ADDED], 0, files);
g_slist_free(files);
}
if(job->dir_fi) {
dir_fi = fm_file_info_ref(job->dir_fi);
}
/* Some new files are created while FmDirListJob is loading the folder. */
G_LOCK(lists);
if(G_UNLIKELY(files_to_add)) {
/* This should be a very rare case. Could this happen? */
GSList* l;
for(l = files_to_add; l;) {
FmPath* path = l->data;
GSList* next = l->next;
if(_Folder::get_file_by_path(folder, path)) {
/* we already have the file. remove it from files_to_add,
* and put it in files_to_update instead.
* No strdup for name is needed here. We steal
* the string from files_to_add.*/
files_to_update = g_slist_prepend(files_to_update, path);
files_to_add = g_slist_delete_link(files_to_add, l);
}
l = next;
}
}
G_UNLOCK(lists);
}
else if(!dir_fi && job->dir_fi)
/* we may need dir_fi for incremental folders too */
{
dir_fi = fm_file_info_ref(job->dir_fi);
}
g_object_unref(dirlist_job);
#endif
dirlist_job = nullptr;
Q_EMIT finishLoading();
}
#if 0
void on_dirlist_job_files_found(FmDirListJob* job, GSList* files, gpointer user_data) {
FmFolder* folder = FM_FOLDER(user_data);
GSList* l;
for(l = files; l; l = l->next) {
FmFileInfo* file = FM_FILE_INFO(l->data);
fm_file_info_list_push_tail(files, file);
}
if(G_UNLIKELY(!dir_fi && job->dir_fi))
/* we may want info while folder is still loading */
{
dir_fi = fm_file_info_ref(job->dir_fi);
}
g_signal_emit(folder, signals[FILES_ADDED], 0, files);
}
ErrorAction on_dirlist_job_error(FmDirListJob* job, GError* err, FmJobErrorSeverity severity, FmFolder* folder) {
guint ret;
/* it's possible that some signal handlers tries to free the folder
* when errors occurs, so let's g_object_ref here. */
g_object_ref(folder);
g_signal_emit(folder, signals[ERROR], 0, err, (guint)severity, &ret);
g_object_unref(folder);
return ret;
}
void free_dirlist_job(FmFolder* folder) {
if(wants_incremental) {
g_signal_handlers_disconnect_by_func(dirlist_job, on_dirlist_job_files_found, folder);
}
g_signal_handlers_disconnect_by_func(dirlist_job, on_dirlist_job_finished, folder);
g_signal_handlers_disconnect_by_func(dirlist_job, on_dirlist_job_error, folder);
fm_job_cancel(FM_JOB(dirlist_job));
g_object_unref(dirlist_job);
dirlist_job = nullptr;
}
#endif
void Folder::reload() {
// cancel in-progress jobs if there are any
GError* err = nullptr;
// cancel directory monitoring
if(dirMonitor_) {
g_signal_handlers_disconnect_by_data(dirMonitor_.get(), this);
dirMonitor_.reset();
}
/* clear all update-lists now, see SF bug #919 - if update comes before
listing job is finished, a duplicate may be created in the folder */
if(has_idle_update_handler) {
// FIXME: cancel the idle handler
paths_to_add.clear();
paths_to_update.clear();
paths_to_del.clear();
// cancel any file info job in progress.
for(auto job: fileinfoJobs_) {
job->cancel();
disconnect(job, &FileInfoJob::finished, this, &Folder::onFileInfoFinished);
}
fileinfoJobs_.clear();
}
/* remove all existing files */
if(!files_.empty()) {
// FIXME: this is not very efficient :(
auto tmp = files();
files_.clear();
Q_EMIT filesRemoved(tmp);
}
/* Tell the world that we're about to reload the folder.
* It might be a good idea for users of the folder to disconnect
* from the folder temporarily and reconnect to it again after
* the folder complete the loading. This might reduce some
* unnecessary signal handling and UI updates. */
Q_EMIT startLoading();
dirInfo_.reset(); // clear dir info
/* also re-create a new file monitor */
// mon = GFileMonitorPtr{fm_monitor_directory(dir_path.gfile().get(), &err), false};
// FIXME: should we make this cancellable?
dirMonitor_ = GFileMonitorPtr{
g_file_monitor_directory(dirPath_.gfile().get(), G_FILE_MONITOR_WATCH_MOUNTS, nullptr, &err),
false
};
if(dirMonitor_) {
g_signal_connect(dirMonitor_.get(), "changed", G_CALLBACK(_onFileChangeEvents), this);
}
else {
qDebug("file monitor cannot be created: %s", err->message);
g_error_free(err);
}
Q_EMIT contentChanged();
/* run a new dir listing job */
// FIXME:
// defer_content_test = fm_config->defer_content_test;
dirlist_job = new DirListJob(dirPath_, defer_content_test ? DirListJob::FAST : DirListJob::DETAILED,
hasCutFiles() ? cutFilesHashSet_ : nullptr);
dirlist_job->setAutoDelete(true);
connect(dirlist_job, &DirListJob::error, this, &Folder::error, Qt::BlockingQueuedConnection);
connect(dirlist_job, &DirListJob::finished, this, &Folder::onDirListFinished, Qt::BlockingQueuedConnection);
#if 0
if(wants_incremental) {
g_signal_connect(dirlist_job, "files-found", G_CALLBACK(on_dirlist_job_files_found), folder);
}
fm_dir_list_job_set_incremental(dirlist_job, wants_incremental);
#endif
dirlist_job->runAsync();
/* also reload filesystem info.
* FIXME: is this needed? */
queryFilesystemInfo();
}
#if 0
/**
* Folder::is_incremental
* @folder: folder to test
*
* Checks if a folder is incrementally loaded.
* After an FmFolder object is obtained from calling Folder::from_path(),
* if it's not yet loaded, it begins loading the content of the folder
* and emits "start-loading" signal. Most of the time, the info of the
* files in the folder becomes available only after the folder is fully
* loaded. That means, after the "finish-loading" signal is emitted.
* Before the loading is finished, Folder::get_files() returns nothing.
* You can tell if a folder is still being loaded with Folder::is_loaded().
*
* However, for some special FmFolder types, such as the ones handling
* search:// URIs, we want to access the file infos while the folder is
* still being loaded (the search is still ongoing).
* The content of the folder grows incrementally and Folder::get_files()
* returns files currently being loaded even when the folder is not
* fully loaded. This is what we called incremental.
* Folder::is_incremental() tells you if the FmFolder has this feature.
*
* Returns: %true if @folder is incrementally loaded
*
* Since: 1.0.2
*/
bool Folder::is_incremental(FmFolder* folder) {
return wants_incremental;
}
#endif
bool Folder::getFilesystemInfo(uint64_t* total_size, uint64_t* free_size) const {
if(has_fs_info) {
*total_size = fs_total_size;
*free_size = fs_free_size;
return true;
}
return false;
}
void Folder::onFileSystemInfoFinished() {
FileSystemInfoJob* job = static_cast<FileSystemInfoJob*>(sender());
if(job->isCancelled() || job != fsInfoJob_) { // this is a cancelled job, ignore!
fsInfoJob_ = nullptr;
has_fs_info = false;
return;
}
has_fs_info = job->isAvailable();
fs_total_size = job->size();
fs_free_size = job->freeSize();
filesystem_info_pending = true;
fsInfoJob_ = nullptr;
queueUpdate();
}
void Folder::queryFilesystemInfo() {
// G_LOCK(query);
if(fsInfoJob_)
return;
fsInfoJob_ = new FileSystemInfoJob{dirPath_};
fsInfoJob_->setAutoDelete(true);
connect(fsInfoJob_, &FileSystemInfoJob::finished, this, &Folder::onFileSystemInfoFinished, Qt::BlockingQueuedConnection);
fsInfoJob_->runAsync();
// G_UNLOCK(query);
}
#if 0
/**
* Folder::block_updates
* @folder: folder to apply
*
* Blocks emitting signals for changes in folder, i.e. if some file was
* added, changed, or removed in folder after this API, no signal will be
* sent until next call to Folder::unblock_updates().
*
* Since: 1.2.0
*/
void Folder::block_updates(FmFolder* folder) {
/* g_debug("Folder::block_updates %p", folder); */
G_LOCK(lists);
/* just set the flag */
stop_emission = true;
G_UNLOCK(lists);
}
/**
* Folder::unblock_updates
* @folder: folder to apply
*
* Unblocks emitting signals for changes in folder. If some changes were
* in folder after previous call to Folder::block_updates() then these
* changes will be sent after this call.
*
* Since: 1.2.0
*/
void Folder::unblock_updates(FmFolder* folder) {
/* g_debug("Folder::unblock_updates %p", folder); */
G_LOCK(lists);
stop_emission = false;
/* query update now */
queue_update(folder);
G_UNLOCK(lists);
/* g_debug("Folder::unblock_updates OK"); */
}
/**
* Folder::make_directory
* @folder: folder to apply
* @name: display name for new directory
* @error: (allow-none) (out): location to save error
*
* Creates new directory in given @folder.
*
* Returns: %true in case of success.
*
* Since: 1.2.0
*/
bool Folder::make_directory(FmFolder* folder, const char* name, GError** error) {
GFile* dir, *gf;
FmPath* path;
bool ok;
dir = fm_path_to_gfile(dir_path);
gf = g_file_get_child_for_display_name(dir, name, error);
g_object_unref(dir);
if(gf == nullptr) {
return false;
}
ok = g_file_make_directory(gf, nullptr, error);
if(ok) {
path = fm_path_new_for_gfile(gf);
if(!_Folder::event_file_added(folder, path)) {
fm_path_unref(path);
}
}
g_object_unref(gf);
return ok;
}
void Folder::content_changed(FmFolder* folder) {
if(has_fs_info && !fs_info_not_avail) {
Folder::query_filesystem_info(folder);
}
}
#endif
/* NOTE:
* GFileMonitor has some significant limitations:
* 1. Currently it can correctly emit unmounted event for a directory.
* 2. After a directory is unmounted, its content changes.
* Inotify does not fire events for this so a forced reload is needed.
* 3. If a folder is empty, and later a filesystem is mounted to the
* folder, its content should reflect the content of the newly mounted
* filesystem. However, GFileMonitor and inotify do not emit events
* for this case. A forced reload might be needed for this case as well.
* 4. Some limitations come from Linux/inotify. If FAM/gamin is used,
* the condition may be different. More testing is needed.
*/
void Folder::onMountAdded(const Mount& mnt) {
/* If a filesystem is mounted over an existing folder,
* we need to refresh the content of the folder to reflect
* the changes. Besides, we need to create a new GFileMonitor
* for the newly-mounted filesystem as the inode already changed.
* GFileMonitor cannot detect this kind of changes caused by mounting.
* So let's do it ourselves. */
auto mountRoot = mnt.root();
if(mountRoot.isPrefixOf(dirPath_)) {
queueReload();
}
/* g_debug("FmFolder::mount_added"); */
}
void Folder::onMountRemoved(const Mount& mnt) {
/* g_debug("FmFolder::mount_removed"); */
/* NOTE: gvfs does not emit unmount signals for remote folders since
* GFileMonitor does not support remote filesystems at all.
* So here is the side effect, no unmount notifications.
* We need to generate the signal ourselves. */
if(!dirMonitor_) {
// this is only needed when we don't have a GFileMonitor
auto mountRoot = mnt.root();
if(mountRoot.isPrefixOf(dirPath_)) {
// if the current folder is under the unmounted path, generate the event ourselves
onDirChanged(G_FILE_MONITOR_EVENT_UNMOUNTED);
}
}
}
} // namespace Fm

@ -0,0 +1,196 @@
/*
* Copyright (C) 2016 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
*
* 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 __LIBFM2_QT_FM_FOLDER_H__
#define __LIBFM2_QT_FM_FOLDER_H__
#include <gio/gio.h>
#include <cstdint>
#include <memory>
#include <vector>
#include <unordered_map>
#include <mutex>
#include <functional>
#include <QObject>
#include <QtGlobal>
#include "../libfmqtglobals.h"
#include "gioptrs.h"
#include "fileinfo.h"
#include "job.h"
#include "volumemanager.h"
namespace Fm {
class DirListJob;
class FileSystemInfoJob;
class FileInfoJob;
class LIBFM_QT_API Folder: public QObject {
Q_OBJECT
public:
explicit Folder();
explicit Folder(const FilePath& path);
virtual ~Folder();
static std::shared_ptr<Folder> fromPath(const FilePath& path);
bool makeDirectory(const char* name, GError** error);
void queryFilesystemInfo();
bool getFilesystemInfo(uint64_t* total_size, uint64_t* free_size) const;
void reload();
bool isIncremental() const;
bool isValid() const;
bool isLoaded() const;
std::shared_ptr<const FileInfo> fileByName(const char* name) const;
bool isEmpty() const;
FileInfoList files() const;
const FilePath& path() const;
const std::shared_ptr<const FileInfo> &info() const;
bool hadCutFilesUnset();
bool hasCutFiles();
void setCutFiles(const std::shared_ptr<const HashSet>& cutFilesHashSet);
void forEachFile(std::function<void (const std::shared_ptr<const FileInfo>&)> func) const {
std::lock_guard<std::mutex> lock{mutex_};
for(auto it = files_.begin(); it != files_.end(); ++it) {
func(it->second);
}
}
Q_SIGNALS:
void startLoading();
void finishLoading();
void filesAdded(FileInfoList& addedFiles);
void filesChanged(std::vector<FileInfoPair>& changePairs);
void filesRemoved(FileInfoList& removedFiles);
void removed();
void changed();
void unmount();
void contentChanged();
void fileSystemChanged();
// FIXME: this API design is bad. We leave this here to be compatible with the old libfm C API.
// It might be better to remember the error state while loading the folder, and let the user of the
// API handle the error on finish.
void error(const GErrorPtr& err, Job::ErrorSeverity severity, Job::ErrorAction& response);
private:
static void _onFileChangeEvents(GFileMonitor* monitor, GFile* file, GFile* other_file, GFileMonitorEvent event_type, Folder* _this) {
_this->onFileChangeEvents(monitor, file, other_file, event_type);
}
void onFileChangeEvents(GFileMonitor* monitor, GFile* file, GFile* other_file, GFileMonitorEvent event_type);
void onDirChanged(GFileMonitorEvent event_type);
void queueUpdate();
void queueReload();
bool eventFileAdded(const FilePath &path);
bool eventFileChanged(const FilePath &path);
void eventFileDeleted(const FilePath &path);
private Q_SLOTS:
void processPendingChanges();
void onDirListFinished();
void onFileSystemInfoFinished();
void onFileInfoFinished();
void onIdleReload();
void onMountAdded(const Mount& mnt);
void onMountRemoved(const Mount& mnt);
private:
FilePath dirPath_;
GFileMonitorPtr dirMonitor_;
std::shared_ptr<const FileInfo> dirInfo_;
DirListJob* dirlist_job;
std::vector<FileInfoJob*> fileinfoJobs_;
FileSystemInfoJob* fsInfoJob_;
std::shared_ptr<VolumeManager> volumeManager_;
/* for file monitor */
bool has_idle_reload_handler;
bool has_idle_update_handler;
std::vector<FilePath> paths_to_add;
std::vector<FilePath> paths_to_update;
std::vector<FilePath> paths_to_del;
// GSList* pending_jobs;
bool pending_change_notify;
bool filesystem_info_pending;
bool wants_incremental;
bool stop_emission; /* don't set it 1 bit to not lock other bits */
std::unordered_map<const std::string, std::shared_ptr<const FileInfo>, std::hash<std::string>> files_;
/* filesystem info - set in query thread, read in main */
uint64_t fs_total_size;
uint64_t fs_free_size;
GCancellablePtr fs_size_cancellable;
bool has_fs_info : 1;
bool defer_content_test : 1;
static std::unordered_map<FilePath, std::weak_ptr<Folder>, FilePathHash> cache_;
static FilePath cutFilesDirPath_;
static FilePath lastCutFilesDirPath_;
static std::shared_ptr<const HashSet> cutFilesHashSet_;
static std::mutex mutex_;
};
}
#endif // __LIBFM_QT_FM2_FOLDER_H__

@ -0,0 +1,137 @@
#ifndef GIOPTRS_H
#define GIOPTRS_H
// define smart pointers for GIO data types
#include <glib.h>
#include <gio/gio.h>
#include "gobjectptr.h"
#include "cstrptr.h"
namespace Fm {
typedef GObjectPtr<GFile> GFilePtr;
typedef GObjectPtr<GFileInfo> GFileInfoPtr;
typedef GObjectPtr<GFileMonitor> GFileMonitorPtr;
typedef GObjectPtr<GCancellable> GCancellablePtr;
typedef GObjectPtr<GFileEnumerator> GFileEnumeratorPtr;
typedef GObjectPtr<GInputStream> GInputStreamPtr;
typedef GObjectPtr<GFileInputStream> GFileInputStreamPtr;
typedef GObjectPtr<GOutputStream> GOutputStreamPtr;
typedef GObjectPtr<GFileOutputStream> GFileOutputStreamPtr;
typedef GObjectPtr<GIcon> GIconPtr;
typedef GObjectPtr<GVolumeMonitor> GVolumeMonitorPtr;
typedef GObjectPtr<GVolume> GVolumePtr;
typedef GObjectPtr<GMount> GMountPtr;
typedef GObjectPtr<GAppInfo> GAppInfoPtr;
class GErrorPtr {
public:
GErrorPtr(): err_{nullptr} {
}
GErrorPtr(GError*&& err) noexcept: err_{err} {
err = nullptr;
}
GErrorPtr(const GErrorPtr& other) = delete;
GErrorPtr(GErrorPtr&& other) noexcept: err_{other.err_} {
other.err_ = nullptr;
}
GErrorPtr(std::uint32_t domain, unsigned int code, const char* msg):
GErrorPtr{g_error_new_literal(domain, code, msg)} {
}
GErrorPtr(std::uint32_t domain, unsigned int code, const QString& msg):
GErrorPtr{domain, code, msg.toUtf8().constData()} {
}
~GErrorPtr() {
reset();
}
std::uint32_t domain() const {
if(err_ != nullptr) {
return err_->domain;
}
return 0;
}
unsigned int code() const {
if(err_ != nullptr) {
return err_->code;
}
return 0;
}
QString message() const {
if(err_ != nullptr) {
return err_->message;
}
return QString();
}
void reset() {
if(err_) {
g_error_free(err_);
}
err_ = nullptr;
}
GError* get() const {
return err_;
}
GErrorPtr& operator = (const GErrorPtr& other) = delete;
GErrorPtr& operator = (GErrorPtr&& other) noexcept {
reset();
err_ = other.err_;
other.err_ = nullptr;
return *this;
}
GErrorPtr& operator = (GError*&& err) {
reset();
err_ = err;
err_ = nullptr;
return *this;
}
GError** operator&() {
return &err_;
}
GError* operator->() {
return err_;
}
bool operator == (const GErrorPtr& other) const {
return err_ == other.err_;
}
bool operator == (GError* err) const {
return err_ == err;
}
bool operator != (std::nullptr_t) const {
return err_ != nullptr;
}
operator bool() const {
return err_ != nullptr;
}
private:
GError* err_;
};
} //namespace Fm
#endif // GIOPTRS_H

@ -0,0 +1,104 @@
#ifndef FM2_GOBJECTPTR_H
#define FM2_GOBJECTPTR_H
#include "../libfmqtglobals.h"
#include <glib.h>
#include <glib-object.h>
#include <cstddef>
#include <QDebug>
namespace Fm {
template <typename T>
class LIBFM_QT_API GObjectPtr {
public:
explicit GObjectPtr(): gobj_{nullptr} {
}
explicit GObjectPtr(T* gobj, bool add_ref = true): gobj_{gobj} {
if(gobj_ != nullptr && add_ref)
g_object_ref(gobj_);
}
GObjectPtr(const GObjectPtr& other): gobj_{other.gobj_ ? reinterpret_cast<T*>(g_object_ref(other.gobj_)) : nullptr} {
}
GObjectPtr(GObjectPtr&& other) noexcept: gobj_{other.release()} {
}
~GObjectPtr() {
if(gobj_ != nullptr)
g_object_unref(gobj_);
}
T* get() const {
return gobj_;
}
T* release() {
T* tmp = gobj_;
gobj_ = nullptr;
return tmp;
}
void reset() {
if(gobj_ != nullptr)
g_object_unref(gobj_);
gobj_ = nullptr;
}
GObjectPtr& operator = (const GObjectPtr& other) {
if (*this == other)
return *this;
if(gobj_ != nullptr)
g_object_unref(gobj_);
gobj_ = other.gobj_ ? reinterpret_cast<T*>(g_object_ref(other.gobj_)) : nullptr;
return *this;
}
GObjectPtr& operator = (GObjectPtr&& other) noexcept {
if (this == &other)
return *this;
if(gobj_ != nullptr)
g_object_unref(gobj_);
gobj_ = other.release();
return *this;
}
GObjectPtr& operator = (T* gobj) {
if (*this == gobj)
return *this;
if(gobj_ != nullptr)
g_object_unref(gobj_);
gobj_ = gobj ? reinterpret_cast<T*>(g_object_ref(gobj_)) : nullptr;
return *this;
}
bool operator == (const GObjectPtr& other) const {
return gobj_ == other.gobj_;
}
bool operator == (T* gobj) const {
return gobj_ == gobj;
}
bool operator != (std::nullptr_t) const {
return gobj_ != nullptr;
}
operator bool() const {
return gobj_ != nullptr;
}
private:
mutable T* gobj_;
};
} // namespace Fm
#endif // FM2_GOBJECTPTR_H

@ -0,0 +1,138 @@
#include "iconinfo.h"
#include "iconinfo_p.h"
namespace Fm {
std::unordered_map<GIcon*, std::shared_ptr<IconInfo>, IconInfo::GIconHash, IconInfo::GIconEqual> IconInfo::cache_;
std::mutex IconInfo::mutex_;
QIcon IconInfo::fallbackQicon_;
static const char* fallbackIconNames[] = {
"unknown",
"application-octet-stream",
"application-x-generic",
"text-x-generic",
nullptr
};
IconInfo::IconInfo(const char* name):
gicon_{g_themed_icon_new(name), false} {
}
IconInfo::IconInfo(const GIconPtr gicon):
gicon_{std::move(gicon)} {
}
IconInfo::~IconInfo() {
}
// static
std::shared_ptr<const IconInfo> IconInfo::fromName(const char* name) {
GObjectPtr<GIcon> gicon{g_themed_icon_new(name), false};
return fromGIcon(gicon);
}
// static
std::shared_ptr<const IconInfo> IconInfo::fromGIcon(GIconPtr gicon) {
if(Q_LIKELY(gicon)) {
std::lock_guard<std::mutex> lock{mutex_};
auto it = cache_.find(gicon.get());
if(it != cache_.end()) {
return it->second;
}
// not found in the cache, create a new entry for it.
auto icon = std::make_shared<IconInfo>(std::move(gicon));
cache_.insert(std::make_pair(icon->gicon_.get(), icon));
return icon;
}
return std::shared_ptr<const IconInfo>{};
}
void IconInfo::updateQIcons() {
std::lock_guard<std::mutex> lock{mutex_};
fallbackQicon_ = QIcon();
for(auto& elem: cache_) {
auto& info = elem.second;
info->internalQicon_ = QIcon();
}
}
QIcon IconInfo::qicon(const bool& transparent) const {
if(Q_LIKELY(!transparent)) {
if(Q_UNLIKELY(qicon_.isNull() && gicon_)) {
if(!G_IS_FILE_ICON(gicon_.get())) {
qicon_ = QIcon(new IconEngine{shared_from_this()});
}
else {
qicon_ = internalQicon_;
}
}
}
else { // transparent == true
if(Q_UNLIKELY(qiconTransparent_.isNull() && gicon_)) {
if(!G_IS_FILE_ICON(gicon_.get())) {
qiconTransparent_ = QIcon(new IconEngine{shared_from_this(), transparent});
}
else {
qiconTransparent_ = internalQicon_;
}
}
}
return !transparent ? qicon_ : qiconTransparent_;
}
QIcon IconInfo::qiconFromNames(const char* const* names) {
const gchar* const* name;
// qDebug("names: %p", names);
for(name = names; *name; ++name) {
// qDebug("icon name=%s", *name);
QIcon qicon = QIcon::fromTheme(*name);
if(!qicon.isNull()) {
return qicon;
}
}
return QIcon();
}
std::forward_list<std::shared_ptr<const IconInfo>> IconInfo::emblems() const {
std::forward_list<std::shared_ptr<const IconInfo>> result;
if(hasEmblems()) {
const GList* emblems_glist = g_emblemed_icon_get_emblems(G_EMBLEMED_ICON(gicon_.get()));
for(auto l = emblems_glist; l; l = l->next) {
auto gemblem = G_EMBLEM(l->data);
GIconPtr gemblem_icon{g_emblem_get_icon(gemblem), true};
result.emplace_front(fromGIcon(gemblem_icon));
}
result.reverse();
}
return result;
}
QIcon IconInfo::internalQicon() const {
if(Q_UNLIKELY(internalQicon_.isNull())) {
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);
}
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());
}
// fallback to default icon
if(Q_UNLIKELY(internalQicon_.isNull())) {
if(Q_UNLIKELY(fallbackQicon_.isNull())) {
fallbackQicon_ = qiconFromNames(fallbackIconNames);
}
internalQicon_ = fallbackQicon_;
}
}
return internalQicon_;
}
} // namespace Fm

@ -0,0 +1,112 @@
/*
* fm-icon.h
*
* Copyright 2009 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
* Copyright 2013 Andriy Grytsenko (LStranger) <andrej@rep.kiev.ua>
*
* This file is a part of the Libfm library.
*
* 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 __FM2_ICON_INFO_H__
#define __FM2_ICON_INFO_H__
#include "../libfmqtglobals.h"
#include <gio/gio.h>
#include "gioptrs.h"
#include <memory>
#include <mutex>
#include <unordered_map>
#include <forward_list>
#include <QIcon>
namespace Fm {
class LIBFM_QT_API IconInfo: public std::enable_shared_from_this<IconInfo> {
public:
friend class IconEngine;
explicit IconInfo() {}
explicit IconInfo(const char* name);
explicit IconInfo(const GIconPtr gicon);
~IconInfo();
static std::shared_ptr<const IconInfo> fromName(const char* name);
static std::shared_ptr<const IconInfo> fromGIcon(GIconPtr gicon);
static std::shared_ptr<const IconInfo> fromGIcon(GIcon* gicon) {
return fromGIcon(GIconPtr{gicon, true});
}
static void updateQIcons();
GIconPtr gicon() const {
return gicon_;
}
QIcon qicon(const bool& transparent = false) const;
bool hasEmblems() const {
return G_IS_EMBLEMED_ICON(gicon_.get());
}
std::forward_list<std::shared_ptr<const IconInfo>> emblems() const;
bool isValid() const {
return gicon_ != nullptr;
}
private:
static QIcon qiconFromNames(const char* const* names);
// actual QIcon loaded by QIcon::fromTheme
QIcon internalQicon() const;
struct GIconHash {
std::size_t operator()(GIcon* gicon) const {
return g_icon_hash(gicon);
}
};
struct GIconEqual {
bool operator()(GIcon* gicon1, GIcon* gicon2) const {
return g_icon_equal(gicon1, gicon2);
}
};
private:
GIconPtr gicon_;
mutable QIcon qicon_;
mutable QIcon qiconTransparent_;
mutable QIcon internalQicon_;
static std::unordered_map<GIcon*, std::shared_ptr<IconInfo>, GIconHash, GIconEqual> cache_;
static std::mutex mutex_;
static QIcon fallbackQicon_;
};
} // namespace Fm
Q_DECLARE_METATYPE(std::shared_ptr<const Fm::IconInfo>)
#endif /* __FM2_ICON_INFO_H__ */

@ -0,0 +1,126 @@
/*
* Copyright (C) 2016 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
*
* 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_ICONENGINE_H
#define FM_ICONENGINE_H
#include <QIconEngine>
#include <QPainter>
#include "../libfmqtglobals.h"
#include "iconinfo.h"
#include <gio/gio.h>
namespace Fm {
class IconEngine: public QIconEngine {
public:
IconEngine(std::shared_ptr<const Fm::IconInfo> info, const bool& transparent = false);
~IconEngine();
virtual QSize actualSize(const QSize& size, QIcon::Mode mode, QIcon::State state) override;
// not supported
virtual void addFile(const QString& /*fileName*/, const QSize& /*size*/, QIcon::Mode /*mode*/, QIcon::State /*state*/) override {}
// not supported
virtual void addPixmap(const QPixmap& /*pixmap*/, QIcon::Mode /*mode*/, QIcon::State /*state*/) override {}
virtual QIconEngine* clone() const override;
virtual QString key() const override;
virtual void paint(QPainter* painter, const QRect& rect, QIcon::Mode mode, QIcon::State state) override;
virtual QPixmap pixmap(const QSize& size, QIcon::Mode mode, QIcon::State state) override;
virtual void virtual_hook(int id, void* data) override;
private:
std::weak_ptr<const Fm::IconInfo> info_;
bool transparent_;
};
IconEngine::IconEngine(std::shared_ptr<const IconInfo> info, const bool& transparent):
info_{info}, transparent_{transparent} {
}
IconEngine::~IconEngine() {
}
QSize IconEngine::actualSize(const QSize& size, QIcon::Mode mode, QIcon::State state) {
auto info = info_.lock();
return info ? info->internalQicon().actualSize(size, mode, state) : QSize{};
}
QIconEngine* IconEngine::clone() const {
IconEngine* engine = new IconEngine(info_.lock());
return engine;
}
QString IconEngine::key() const {
return QStringLiteral("Fm::IconEngine");
}
void IconEngine::paint(QPainter* painter, const QRect& rect, QIcon::Mode mode, QIcon::State state) {
auto info = info_.lock();
if(info) {
if(transparent_) {
painter->save();
painter->setOpacity(0.45);
}
info->internalQicon().paint(painter, rect, Qt::AlignCenter, mode, state);
if(transparent_) {
painter->restore();
}
}
}
QPixmap IconEngine::pixmap(const QSize& size, QIcon::Mode mode, QIcon::State state) {
auto info = info_.lock();
return info ? info->internalQicon().pixmap(size, mode, state) : QPixmap{};
}
void IconEngine::virtual_hook(int id, void* data) {
auto info = info_.lock();
switch(id) {
case QIconEngine::AvailableSizesHook: {
auto* args = reinterpret_cast<QIconEngine::AvailableSizesArgument*>(data);
args->sizes = info ? info->internalQicon().availableSizes(args->mode, args->state) : QList<QSize>{};
break;
}
case QIconEngine::IconNameHook: {
QString* result = reinterpret_cast<QString*>(data);
*result = info ? info->internalQicon().name() : QString{};
break;
}
#if QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)
case QIconEngine::IsNullHook: {
bool* result = reinterpret_cast<bool*>(data);
*result = info ? info->internalQicon().isNull() : true;
break;
}
#endif
}
}
} // namespace Fm
#endif // FM_ICONENGINE_H

@ -0,0 +1,57 @@
#include "job.h"
#include "job_p.h"
namespace Fm {
Job::Job():
paused_{false},
cancellable_{g_cancellable_new(), false},
cancellableHandler_{g_signal_connect(cancellable_.get(), "cancelled", G_CALLBACK(_onCancellableCancelled), this)} {
}
Job::~Job() {
if(cancellable_) {
g_cancellable_disconnect(cancellable_.get(), cancellableHandler_);
}
}
void Job::runAsync(QThread::Priority priority) {
auto thread = new JobThread(this);
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
if(autoDelete()) {
connect(this, &Job::finished, this, &Job::deleteLater);
}
thread->start(priority);
}
void Job::cancel() {
g_cancellable_cancel(cancellable_.get());
}
void Job::run() {
exec();
Q_EMIT finished();
}
Job::ErrorAction Job::emitError(const GErrorPtr &err, Job::ErrorSeverity severity) {
ErrorAction response = ErrorAction::CONTINUE;
// if the error is already handled, don't emit it.
if(err.domain() == G_IO_ERROR && err.code() == G_IO_ERROR_FAILED_HANDLED) {
return response;
}
Q_EMIT error(err, severity, response);
if(severity == ErrorSeverity::CRITICAL || response == ErrorAction::ABORT) {
cancel();
}
else if(response == ErrorAction::RETRY ) {
/* If the job is already cancelled, retry is not allowed. */
if(isCancelled() || (err.domain() == G_IO_ERROR && err.code() == G_IO_ERROR_CANCELLED)) {
response = ErrorAction::CONTINUE;
}
}
return response;
}
} // namespace Fm

@ -0,0 +1,120 @@
/*
* Copyright (C) 2016 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
*
* 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_JOB_H__
#define __LIBFM_QT_FM_JOB_H__
#include <libfm/fm.h>
#include <QObject>
#include <QtGlobal>
#include <QThread>
#include <QRunnable>
#include <memory>
#include <gio/gio.h>
#include "gobjectptr.h"
#include "gioptrs.h"
#include "../libfmqtglobals.h"
namespace Fm {
/*
* Fm::Job can be used in several different modes.
* 1. run with QThreadPool::start()
* 2. call runAsync(), which will create a new QThread and move the object to the thread.
* 3. create a new QThread, and connect the started() signal to the slot Job::run()
* 4. Directly call Job::run(), which executes synchrounously as a normal blocking call
*/
class LIBFM_QT_API Job: public QObject, public QRunnable {
Q_OBJECT
public:
enum class ErrorAction{
CONTINUE,
RETRY,
ABORT
};
enum class ErrorSeverity {
UNKNOWN,
WARNING,
MILD,
MODERATE,
SEVERE,
CRITICAL
};
explicit Job();
virtual ~Job();
bool isCancelled() const {
return g_cancellable_is_cancelled(cancellable_.get());
}
void runAsync(QThread::Priority priority = QThread::InheritPriority);
bool pause();
void resume();
const GCancellablePtr& cancellable() const {
return cancellable_;
}
Q_SIGNALS:
void cancelled();
void finished();
// this signal should be connected with Qt::BlockingQueuedConnection
void error(const GErrorPtr& err, ErrorSeverity severity, ErrorAction& response);
public Q_SLOTS:
void cancel();
void run() override;
protected:
ErrorAction emitError(const GErrorPtr& err, ErrorSeverity severity = ErrorSeverity::MODERATE);
// all derived job subclasses should do their work in this method.
virtual void exec() = 0;
private:
static void _onCancellableCancelled(GCancellable* cancellable, Job* _this) {
_this->onCancellableCancelled(cancellable);
}
void onCancellableCancelled(GCancellable* /*cancellable*/) {
Q_EMIT cancelled();
}
private:
bool paused_;
GCancellablePtr cancellable_;
gulong cancellableHandler_;
};
}
#endif // __LIBFM_QT_FM_JOB_H__

@ -0,0 +1,26 @@
#ifndef JOB_P_H
#define JOB_P_H
#include <QThread>
#include "job.h"
namespace Fm {
class JobThread: public QThread {
Q_OBJECT
public:
JobThread(Job* job): job_{job} {
}
protected:
void run() override {
job_->run();
}
Job* job_;
};
} // namespace Fm
#endif // JOB_P_H

@ -0,0 +1,64 @@
#include "mimetype.h"
#include <cstring>
#include <glib.h>
#include <gio/gio.h>
using namespace std;
namespace Fm {
std::unordered_map<const char*, std::shared_ptr<const MimeType>, CStrHash, CStrEqual> MimeType::cache_;
std::mutex MimeType::mutex_;
std::shared_ptr<const MimeType> MimeType::inodeDirectory_; // inode/directory
std::shared_ptr<const MimeType> MimeType::inodeShortcut_; // inode/x-shortcut
std::shared_ptr<const MimeType> MimeType::inodeMountPoint_; // inode/mount-point
std::shared_ptr<const MimeType> MimeType::desktopEntry_; // application/x-desktop
MimeType::MimeType(const char* typeName):
name_{g_strdup(typeName)},
desc_{nullptr} {
GObjectPtr<GIcon> gicon{g_content_type_get_icon(typeName), false};
if(strcmp(typeName, "inode/directory") == 0)
g_themed_icon_prepend_name(G_THEMED_ICON(gicon.get()), "folder");
else if(g_content_type_can_be_executable(typeName))
g_themed_icon_append_name(G_THEMED_ICON(gicon.get()), "application-x-executable");
icon_ = IconInfo::fromGIcon(gicon);
}
MimeType::~MimeType () {
}
//static
std::shared_ptr<const MimeType> MimeType::fromName(const char* typeName) {
std::shared_ptr<const MimeType> ret;
std::lock_guard<std::mutex> lock(mutex_);
auto it = cache_.find(typeName);
if(it == cache_.end()) {
ret = std::make_shared<MimeType>(typeName);
cache_.insert(std::make_pair(ret->name_.get(), ret));
}
else {
ret = it->second;
}
return ret;
}
// static
std::shared_ptr<const MimeType> MimeType::guessFromFileName(const char* fileName) {
gboolean uncertain;
/* let skip scheme and host from non-native names */
auto uri_scheme = g_strstr_len(fileName, -1, "://");
if(uri_scheme)
fileName = strchr(uri_scheme + 3, '/');
if(fileName == nullptr)
fileName = "unknown";
auto type = CStrPtr{g_content_type_guess(fileName, nullptr, 0, &uncertain)};
return fromName(type.get());
}
} // namespace Fm

@ -0,0 +1,172 @@
/*
* fm-mime-type.h
*
* Copyright 2009 - 2012 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
*
* This file is a part of the Libfm library.
*
* 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 _FM2_MIME_TYPE_H_
#define _FM2_MIME_TYPE_H_
#include "../libfmqtglobals.h"
#include <glib.h>
#include <gio/gio.h>
#include <memory>
#include <vector>
#include <string>
#include <mutex>
#include <cstring>
#include <forward_list>
#include <functional>
#include "cstrptr.h"
#include "gobjectptr.h"
#include "iconinfo.h"
#include "thumbnailer.h"
namespace Fm {
class LIBFM_QT_API MimeType {
public:
friend class Thumbnailer;
explicit MimeType(const char* typeName);
MimeType() = delete;
~MimeType();
std::shared_ptr<const Thumbnailer> firstThumbnailer() const {
std::lock_guard<std::mutex> lock{mutex_};
return thumbnailers_.empty() ? nullptr : thumbnailers_.front();
}
void forEachThumbnailer(std::function<bool(const std::shared_ptr<const Thumbnailer>&)> func) const {
std::lock_guard<std::mutex> lock{mutex_};
for(auto& thumbnailer: thumbnailers_) {
if(func(thumbnailer)) {
break;
}
}
}
const std::shared_ptr<const IconInfo>& icon() const {
return icon_;
}
const char* name() const {
return name_.get();
}
const char* desc() const {
if(!desc_) {
desc_ = CStrPtr{g_content_type_get_description(name_.get())};
}
return desc_.get();
}
static std::shared_ptr<const MimeType> fromName(const char* typeName);
static std::shared_ptr<const MimeType> guessFromFileName(const char* fileName);
bool isUnknownType() const {
return g_content_type_is_unknown(name_.get());
}
bool isDesktopEntry() const {
return this == desktopEntry().get();
}
bool isText() const {
return g_content_type_is_a(name_.get(), "text/plain");
}
bool isImage() const {
return !std::strncmp("image/", name_.get(), 6);
}
bool isMountable() const {
return this == inodeMountPoint().get();
}
bool isShortcut() const {
return this == inodeShortcut().get();
}
bool isDir() const {
return this == inodeDirectory().get();
}
bool canBeExecutable() const {
return g_content_type_can_be_executable(name_.get());
}
static std::shared_ptr<const MimeType> inodeDirectory() { // inode/directory
if(!inodeDirectory_)
inodeDirectory_ = fromName("inode/directory");
return inodeDirectory_;
}
static std::shared_ptr<const MimeType> inodeShortcut() { // inode/x-shortcut
if(!inodeShortcut_)
inodeShortcut_ = fromName("inode/x-shortcut");
return inodeShortcut_;
}
static std::shared_ptr<const MimeType> inodeMountPoint() { // inode/mount-point
if(!inodeMountPoint_)
inodeMountPoint_ = fromName("inode/mount-point");
return inodeMountPoint_;
}
static std::shared_ptr<const MimeType> desktopEntry() { // application/x-desktop
if(!desktopEntry_)
desktopEntry_ = fromName("application/x-desktop");
return desktopEntry_;
}
private:
void removeThumbnailer(std::shared_ptr<const Thumbnailer>& thumbnailer) {
std::lock_guard<std::mutex> lock{mutex_};
thumbnailers_.remove(thumbnailer);
}
void addThumbnailer(std::shared_ptr<const Thumbnailer> thumbnailer) {
std::lock_guard<std::mutex> lock{mutex_};
thumbnailers_.push_front(std::move(thumbnailer));
}
private:
std::shared_ptr<const IconInfo> icon_;
CStrPtr name_;
mutable CStrPtr desc_;
std::forward_list<std::shared_ptr<const Thumbnailer>> thumbnailers_;
static std::unordered_map<const char*, std::shared_ptr<const MimeType>, CStrHash, CStrEqual> cache_;
static std::mutex mutex_;
static std::shared_ptr<const MimeType> inodeDirectory_; // inode/directory
static std::shared_ptr<const MimeType> inodeShortcut_; // inode/x-shortcut
static std::shared_ptr<const MimeType> inodeMountPoint_; // inode/mount-point
static std::shared_ptr<const MimeType> desktopEntry_; // application/x-desktop
};
} // namespace Fm
#endif

@ -0,0 +1,127 @@
#include "terminal.h"
namespace Fm {
#include <glib.h>
#include <gio/gdesktopappinfo.h>
#include <string.h>
#include <unistd.h>
#if !GLIB_CHECK_VERSION(2, 28, 0) && !HAVE_DECL_ENVIRON
extern char** environ;
#endif
static void child_setup(gpointer user_data) {
/* Move child to grandparent group so it will not die with parent */
setpgid(0, (pid_t)(gsize)user_data);
}
bool launchTerminal(const char* programName, const FilePath& workingDir, Fm::GErrorPtr& error) {
/* read system terminals file */
GKeyFile* kf = g_key_file_new();
if(!g_key_file_load_from_file(kf, LIBFM_QT_DATA_DIR "/terminals.list", G_KEY_FILE_NONE, &error)) {
g_key_file_free(kf);
return false;
}
auto launch = g_key_file_get_string(kf, programName, "launch", nullptr);
auto desktop_id = g_key_file_get_string(kf, programName, "desktop_id", nullptr);
GDesktopAppInfo* appinfo = nullptr;
if(desktop_id) {
appinfo = g_desktop_app_info_new(desktop_id);
}
const gchar* cmd;
gchar* _cmd = nullptr;
if(appinfo) {
cmd = g_app_info_get_commandline(G_APP_INFO(appinfo));
}
else if(launch) {
cmd = _cmd = g_strdup_printf("%s %s", programName, launch);
}
else {
cmd = programName;
}
#if 0 // FIXME: what's this?
if(custom_args) {
cmd = g_strdup_printf("%s %s", cmd, custom_args);
g_free(_cmd);
_cmd = (char*)cmd;
}
#endif
char** argv;
int argc;
if(!g_shell_parse_argv(cmd, &argc, &argv, nullptr)) {
argv = nullptr;
}
g_free(_cmd);
if(appinfo) {
g_object_unref(appinfo);
}
if(!argv) { /* parsing failed */
return false;
}
char** envp;
#if GLIB_CHECK_VERSION(2, 28, 0)
envp = g_get_environ();
#else
envp = g_strdupv(environ);
#endif
auto dir = workingDir ? workingDir.localPath() : nullptr;
if(dir) {
#if GLIB_CHECK_VERSION(2, 32, 0)
envp = g_environ_setenv(envp, "PWD", dir.get(), TRUE);
#else
char** env = envp;
if(env) while(*env != nullptr) {
if(strncmp(*env, "PWD=", 4) == 0) {
break;
}
env++;
}
if(env == nullptr || *env == nullptr) {
gint length;
length = envp ? g_strv_length(envp) : 0;
envp = g_renew(gchar*, envp, length + 2);
env = &envp[length];
env[1] = nullptr;
}
else {
g_free(*env);
}
*env = g_strdup_printf("PWD=%s", dir);
#endif
}
bool ret = g_spawn_async(dir.get(), argv, envp, G_SPAWN_SEARCH_PATH,
child_setup, (gpointer)(gsize)getpgid(getppid()),
nullptr, &error);
g_strfreev(argv);
g_strfreev(envp);
g_key_file_free(kf);
return ret;
}
std::vector<CStrPtr> allKnownTerminals() {
std::vector<CStrPtr> terminals;
GKeyFile* kf = g_key_file_new();
if(g_key_file_load_from_file(kf, LIBFM_QT_DATA_DIR "/terminals.list", G_KEY_FILE_NONE, nullptr)) {
gsize n;
auto programs = g_key_file_get_groups(kf, &n);
terminals.reserve(n);
for(auto name = programs; *name; ++name) {
terminals.emplace_back(*name);
}
g_free(programs);
}
g_key_file_free(kf);
return terminals;
}
} // namespace Fm

@ -0,0 +1,17 @@
#ifndef TERMINAL_H
#define TERMINAL_H
#include "../libfmqtglobals.h"
#include "gioptrs.h"
#include "filepath.h"
#include <vector>
namespace Fm {
LIBFM_QT_API bool launchTerminal(const char* programName, const FilePath& workingDir, GErrorPtr& error);
LIBFM_QT_API std::vector<CStrPtr> allKnownTerminals();
} // namespace Fm
#endif // TERMINAL_H

@ -0,0 +1,140 @@
#include "thumbnailer.h"
#include "mimetype.h"
#include <string>
#include <QDebug>
#include <sys/stat.h>
#include <sys/types.h>
namespace Fm {
std::mutex Thumbnailer::mutex_;
std::vector<std::shared_ptr<Thumbnailer>> Thumbnailer::allThumbnailers_;
Thumbnailer::Thumbnailer(const char* id, GKeyFile* kf):
id_{g_strdup(id)},
try_exec_{g_key_file_get_string(kf, "Thumbnailer Entry", "TryExec", nullptr)},
exec_{g_key_file_get_string(kf, "Thumbnailer Entry", "Exec", nullptr)} {
}
CStrPtr Thumbnailer::commandForUri(const char* uri, const char* output_file, guint size) const {
if(exec_) {
/* 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
*/
GString* cmd_line = g_string_sized_new(1024);
const char* p;
for(p = exec_.get(); *p; ++p) {
if(G_LIKELY(*p != '%')) {
g_string_append_c(cmd_line, *p);
}
else {
char* quoted;
++p;
switch(*p) {
case '\0':
break;
case 's':
g_string_append_printf(cmd_line, "%d", size);
break;
case 'i': {
char* src_path = g_filename_from_uri(uri, nullptr, nullptr);
if(src_path) {
quoted = g_shell_quote(src_path);
g_string_append(cmd_line, quoted);
g_free(quoted);
g_free(src_path);
}
break;
}
case 'u':
quoted = g_shell_quote(uri);
g_string_append(cmd_line, quoted);
g_free(quoted);
break;
case 'o':
g_string_append(cmd_line, output_file);
break;
default:
g_string_append_c(cmd_line, '%');
if(*p != '%') {
g_string_append_c(cmd_line, *p);
}
}
}
}
return CStrPtr{g_string_free(cmd_line, FALSE)};
}
return nullptr;
}
bool Thumbnailer::run(const char* uri, const char* output_file, int size) const {
auto cmd = commandForUri(uri, output_file, size);
qDebug() << cmd.get();
int status;
bool ret = g_spawn_command_line_sync(cmd.get(), nullptr, nullptr, &status, nullptr);
return ret && status == 0;
}
static void find_thumbnailers_in_data_dir(std::unordered_map<std::string, const char*>& hash, const char* data_dir) {
CStrPtr dir_path{g_build_filename(data_dir, "thumbnailers", nullptr)};
GDir* dir = g_dir_open(dir_path.get(), 0, nullptr);
if(dir) {
const char* basename;
while((basename = g_dir_read_name(dir)) != nullptr) {
/* we only want filenames with .thumbnailer extension */
if(G_LIKELY(g_str_has_suffix(basename, ".thumbnailer"))) {
hash.insert(std::make_pair(basename, data_dir));
}
}
g_dir_close(dir);
}
}
void Thumbnailer::loadAll() {
const gchar* const* data_dirs = g_get_system_data_dirs();
const gchar* const* data_dir;
/* use a temporary hash table to collect thumbnailer basenames
* key: basename of thumbnailer entry file
* value: data dir the thumbnailer entry file is in */
std::unordered_map<std::string, const char*> hash;
/* load user-specific thumbnailers */
find_thumbnailers_in_data_dir(hash, g_get_user_data_dir());
/* load system-wide thumbnailers */
for(data_dir = data_dirs; *data_dir; ++data_dir) {
find_thumbnailers_in_data_dir(hash, *data_dir);
}
/* load all found thumbnailers */
if(!hash.empty()) {
std::lock_guard<std::mutex> lock{mutex_};
GKeyFile* kf = g_key_file_new();
for(auto& item: hash) {
auto& base_name = item.first;
auto& dir_path = item.second;
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<Thumbnailer>(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<MimeType>(mime_type)->addThumbnailer(thumbnailer);
}
}
g_strfreev(mime_types);
}
allThumbnailers_.push_back(std::move(thumbnailer));
}
}
g_key_file_free(kf);
}
}
} // namespace Fm

@ -0,0 +1,37 @@
#ifndef FM2_THUMBNAILER_H
#define FM2_THUMBNAILER_H
#include "../libfmqtglobals.h"
#include "cstrptr.h"
#include <unordered_map>
#include <vector>
#include <memory>
#include <mutex>
namespace Fm {
class MimeType;
class LIBFM_QT_API Thumbnailer {
public:
explicit Thumbnailer(const char *id, GKeyFile *kf);
CStrPtr commandForUri(const char* uri, const char* output_file, guint size) const;
bool run(const char* uri, const char* output_file, int size) const;
static void loadAll();
private:
CStrPtr id_;
CStrPtr try_exec_; /* FIXME: is this useful? */
CStrPtr exec_;
std::vector<std::shared_ptr<const MimeType>> mimeTypes_;
static std::mutex mutex_;
static std::vector<std::shared_ptr<Thumbnailer>> allThumbnailers_;
};
} // namespace Fm
#endif // FM2_THUMBNAILER_H

@ -0,0 +1,266 @@
#include "thumbnailjob.h"
#include <string>
#include <memory>
#include <algorithm>
#include <libexif/exif-loader.h>
#include <QImageReader>
#include <QDir>
#include "thumbnailer.h"
namespace Fm {
QThreadPool* ThumbnailJob::threadPool_ = nullptr;
bool ThumbnailJob::localFilesOnly_ = true;
int ThumbnailJob::maxThumbnailFileSize_ = 0;
ThumbnailJob::ThumbnailJob(FileInfoList files, int size):
files_{std::move(files)},
size_{size},
md5Calc_{g_checksum_new(G_CHECKSUM_MD5)} {
}
ThumbnailJob::~ThumbnailJob() {
g_checksum_free(md5Calc_);
// qDebug("delete ThumbnailJob");
}
void ThumbnailJob::exec() {
for(auto& file: files_) {
if(isCancelled()) {
break;
}
auto image = loadForFile(file);
Q_EMIT thumbnailLoaded(file, size_, image);
results_.emplace_back(std::move(image));
}
}
QImage ThumbnailJob::readImageFromStream(GInputStream* stream, size_t len) {
// FIXME: should we set a limit here? Otherwise if len is too large, we can run out of memory.
std::unique_ptr<unsigned char[]> buffer{new unsigned char[len]}; // allocate enough buffer
unsigned char* pbuffer = buffer.get();
size_t totalReadSize = 0;
while(!isCancelled() && totalReadSize < len) {
size_t bytesToRead = totalReadSize + 4096 > len ? len - totalReadSize : 4096;
gssize readSize = g_input_stream_read(stream, pbuffer, bytesToRead, cancellable_.get(), nullptr);
if(readSize == 0) { // end of file
break;
}
else if(readSize == -1) { // error
return QImage();
}
totalReadSize += readSize;
pbuffer += readSize;
}
QImage image;
image.loadFromData(buffer.get(), totalReadSize);
return image;
}
QImage ThumbnailJob::loadForFile(const std::shared_ptr<const FileInfo> &file) {
if(!file->canThumbnail()) {
return QImage();
}
// thumbnails are stored in $XDG_CACHE_HOME/thumbnails/large|normal|failed
QString thumbnailDir{g_get_user_cache_dir()};
thumbnailDir += "/thumbnails/";
// don't make thumbnails for files inside the thumbnail directory
if(FilePath::fromLocalPath(thumbnailDir.toLocal8Bit().constData()).isParentOf(file->dirPath())) {
return QImage();
}
const char* subdir = size_ > 128 ? "large" : "normal";
thumbnailDir += subdir;
// generate base name of the thumbnail => {md5 of uri}.png
auto origPath = file->path();
auto uri = origPath.uri();
char thumbnailName[32 + 5];
// calculate md5 hash for the uri of the original file
g_checksum_update(md5Calc_, reinterpret_cast<const unsigned char*>(uri.get()), -1);
memcpy(thumbnailName, g_checksum_get_string(md5Calc_), 32);
mempcpy(thumbnailName + 32, ".png", 5);
g_checksum_reset(md5Calc_); // reset the checksum calculator for next use
QString thumbnailFilename = thumbnailDir;
thumbnailFilename += '/';
thumbnailFilename += thumbnailName;
// qDebug() << "thumbnail:" << file->getName().c_str() << thumbnailFilename;
// try to load the thumbnail file if it exists
QImage thumbnail{thumbnailFilename};
if(thumbnail.isNull() || isThumbnailOutdated(file, thumbnail)) {
// the existing thumbnail cannot be loaded, generate a new one
// create the thumbnail dir as needd (FIXME: Qt file I/O is slow)
QDir().mkpath(thumbnailDir);
thumbnail = generateThumbnail(file, origPath, uri.get(), thumbnailFilename);
}
// resize to the size we need
if(thumbnail.width() > size_ || thumbnail.height() > size_) {
thumbnail = thumbnail.scaled(size_, size_, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
return thumbnail;
}
bool ThumbnailJob::isSupportedImageType(const std::shared_ptr<const MimeType>& mimeType) const {
if(mimeType->isImage()) {
auto supportedTypes = QImageReader::supportedMimeTypes();
auto found = std::find(supportedTypes.cbegin(), supportedTypes.cend(), mimeType->name());
if(found != supportedTypes.cend())
return true;
}
return false;
}
bool ThumbnailJob::isThumbnailOutdated(const std::shared_ptr<const FileInfo>& file, const QImage &thumbnail) const {
QString thumb_mtime = thumbnail.text("Thumb::MTime");
return (thumb_mtime.isEmpty() || thumb_mtime.toInt() != file->mtime());
}
bool ThumbnailJob::readJpegExif(GInputStream *stream, QImage& thumbnail, int& rotate_degrees) {
/* try to extract thumbnails embedded in jpeg files */
ExifLoader* exif_loader = exif_loader_new();
while(!isCancelled()) {
unsigned char buf[4096];
gssize read_size = g_input_stream_read(stream, buf, 4096, cancellable_.get(), nullptr);
if(read_size <= 0) { // EOF or error
break;
}
if(exif_loader_write(exif_loader, buf, read_size) == 0) {
break; // no more EXIF data
}
}
ExifData* exif_data = exif_loader_get_data(exif_loader);
exif_loader_unref(exif_loader);
if(exif_data) {
/* reference for EXIF orientation tag:
* http://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;
ExifByteOrder bo = exif_data_get_byte_order(exif_data);
/* bo == EXIF_BYTE_ORDER_INTEL ; */
orient = exif_get_short(orient_ent->data, bo);
switch(orient) {
case 1: /* no rotation */
rotate_degrees = 0;
break;
case 8:
rotate_degrees = 90;
break;
case 3:
rotate_degrees = 180;
break;
case 6:
rotate_degrees = 270;
break;
}
}
if(exif_data->data) { // if an embedded thumbnail is available, load it
thumbnail.loadFromData(exif_data->data, exif_data->size);
}
exif_data_unref(exif_data);
}
return !thumbnail.isNull();
}
QImage ThumbnailJob::generateThumbnail(const std::shared_ptr<const FileInfo>& file, const FilePath& origPath, const char* uri, const QString& thumbnailFilename) {
QImage result;
auto mime_type = file->mimeType();
if(isSupportedImageType(mime_type)) {
GFileInputStreamPtr ins{g_file_read(origPath.gfile().get(), cancellable_.get(), nullptr), false};
if(!ins)
return QImage();
bool fromExif = false;
int rotate_degrees = 0;
if(strcmp(mime_type->name(), "image/jpeg") == 0) { // if this is a jpeg file
// try to get the thumbnail embedded in EXIF data
if(readJpegExif(G_INPUT_STREAM(ins.get()), result, rotate_degrees)) {
fromExif = true;
}
}
if(!fromExif) { // not able to generate a thumbnail from the EXIF data
// load the original file and do the scaling ourselves
g_seekable_seek(G_SEEKABLE(ins.get()), 0, G_SEEK_SET, cancellable_.get(), nullptr);
result = readImageFromStream(G_INPUT_STREAM(ins.get()), file->size());
}
g_input_stream_close(G_INPUT_STREAM(ins.get()), nullptr, nullptr);
if(!result.isNull()) { // the image is successfully loaded
// scale the image as needed
int target_size = size_ > 128 ? 256 : 128;
// only scale the original image if it's too large
if(result.width() > target_size || result.height() > target_size) {
result = result.scaled(target_size, target_size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
if(rotate_degrees != 0) {
// degree values are 0, 90, 180, and 270 counterclockwise.
// In Qt, QMatrix does rotation counterclockwise as well.
// However, because the y axis of widget coordinate system is downward,
// the real effect of the coordinate transformation becomes clockwise rotation.
// So we need to use (360 - degree) here.
// Quote from QMatrix API doc:
// Note that if you apply a QMatrix to a point defined in widget
// coordinates, the direction of the rotation will be clockwise because
// the y-axis points downwards.
result = result.transformed(QMatrix().rotate(360 - rotate_degrees));
}
// save the generated thumbnail to disk (don't save png thumbnails for JPEG EXIF thumbnails since loading them is cheap)
if(!fromExif) {
result.setText("Thumb::MTime", QString::number(file->mtime()));
result.setText("Thumb::URI", uri);
result.save(thumbnailFilename, "PNG");
}
// qDebug() << "save thumbnail:" << thumbnailFilename;
}
}
else { // the image format is not supported, try to find an external thumbnailer
// try all available external thumbnailers for it until sucess
int target_size = size_ > 128 ? 256 : 128;
file->mimeType()->forEachThumbnailer([&](const std::shared_ptr<const Thumbnailer>& thumbnailer) {
if(thumbnailer->run(uri, thumbnailFilename.toLocal8Bit().constData(), target_size)) {
result = QImage(thumbnailFilename);
}
return !result.isNull(); // return true on success, and forEachThumbnailer() will stop.
});
if(!result.isNull()) {
// Some thumbnailers did not write the proper metadata required by the xdg spec to the output (such as evince-thumbnailer)
// Here we waste some time to fix them so next time we don't need to re-generate these thumbnails. :-(
bool changed = false;
if(Q_UNLIKELY(result.text("Thumb::MTime").isEmpty())) {
result.setText("Thumb::MTime", QString::number(file->mtime()));
changed = true;
}
if(Q_UNLIKELY(result.text("Thumb::URI").isEmpty())) {
result.setText("Thumb::URI", uri);
changed = true;
}
if(Q_UNLIKELY(changed)) {
// save the modified PNG file containing metadata to a file.
result.save(thumbnailFilename, "PNG");
}
}
}
return result;
}
QThreadPool* ThumbnailJob::threadPool() {
if(Q_UNLIKELY(threadPool_ == nullptr)) {
threadPool_ = new QThreadPool();
threadPool_->setMaxThreadCount(1);
}
return threadPool_;
}
} // namespace Fm

@ -0,0 +1,89 @@
#ifndef FM2_THUMBNAILJOB_H
#define FM2_THUMBNAILJOB_H
#include "../libfmqtglobals.h"
#include "fileinfo.h"
#include "gioptrs.h"
#include "job.h"
#include <QThreadPool>
namespace Fm {
class LIBFM_QT_API ThumbnailJob: public Job {
Q_OBJECT
public:
explicit ThumbnailJob(FileInfoList files, int size);
~ThumbnailJob();
int size() const {
return size_;
}
static QThreadPool* threadPool();
static void setLocalFilesOnly(bool value) {
localFilesOnly_ = value;
if(fm_config) {
fm_config->thumbnail_local = localFilesOnly_;
}
}
static bool localFilesOnly() {
return localFilesOnly_;
}
static int maxThumbnailFileSize() {
return maxThumbnailFileSize_;
}
static void setMaxThumbnailFileSize(int size) {
maxThumbnailFileSize_ = size;
if(fm_config) {
fm_config->thumbnail_max = maxThumbnailFileSize_;
}
}
const std::vector<QImage>& results() const {
return results_;
}
Q_SIGNALS:
void thumbnailLoaded(const std::shared_ptr<const FileInfo>& file, int size, QImage thumbnail);
protected:
void exec() override;
private:
bool isSupportedImageType(const std::shared_ptr<const MimeType>& mimeType) const;
bool isThumbnailOutdated(const std::shared_ptr<const FileInfo>& file, const QImage& thumbnail) const;
QImage generateThumbnail(const std::shared_ptr<const FileInfo>& file, const FilePath& origPath, const char* uri, const QString& thumbnailFilename);
QImage readImageFromStream(GInputStream* stream, size_t len);
QImage loadForFile(const std::shared_ptr<const FileInfo>& file);
bool readJpegExif(GInputStream* stream, QImage& thumbnail, int& rotate_degrees);
private:
FileInfoList files_;
int size_;
std::vector<QImage> results_;
GCancellablePtr cancellable_;
GChecksum* md5Calc_;
static QThreadPool* threadPool_;
static bool localFilesOnly_;
static int maxThumbnailFileSize_;
};
} // namespace Fm
#endif // FM2_THUMBNAILJOB_H

@ -0,0 +1,144 @@
#include "totalsizejob.h"
namespace Fm {
static const char query_str[] =
G_FILE_ATTRIBUTE_STANDARD_TYPE","
G_FILE_ATTRIBUTE_STANDARD_NAME","
G_FILE_ATTRIBUTE_STANDARD_IS_VIRTUAL","
G_FILE_ATTRIBUTE_STANDARD_SIZE","
G_FILE_ATTRIBUTE_STANDARD_ALLOCATED_SIZE","
G_FILE_ATTRIBUTE_ID_FILESYSTEM;
TotalSizeJob::TotalSizeJob(FilePathList paths, Flags flags):
paths_{std::move(paths)},
flags_{flags},
totalSize_{0},
totalOndiskSize_{0},
fileCount_{0},
dest_fs_id{nullptr} {
}
void TotalSizeJob::exec(FilePath path, GFileInfoPtr inf) {
GFileType type;
const char* fs_id;
bool descend;
_retry_query_info:
if(!inf) {
GErrorPtr err;
inf = GFileInfoPtr {
g_file_query_info(path.gfile().get(), query_str,
(flags_ & FOLLOW_LINKS) ? G_FILE_QUERY_INFO_NONE : G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
cancellable().get(), &err),
false
};
if(!inf) {
ErrorAction act = emitError( err, ErrorSeverity::MILD);
err = nullptr;
if(act == ErrorAction::RETRY) {
goto _retry_query_info;
}
return;
}
}
if(isCancelled()) {
return;
}
type = g_file_info_get_file_type(inf.get());
descend = true;
++fileCount_;
/* SF bug #892: dir file size is not relevant in the summary */
if(type != G_FILE_TYPE_DIRECTORY) {
totalSize_ += g_file_info_get_size(inf.get());
}
totalOndiskSize_ += g_file_info_get_attribute_uint64(inf.get(), G_FILE_ATTRIBUTE_STANDARD_ALLOCATED_SIZE);
/* 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) {
/* files on different device requires an additional 'delete' for the source file. */
++totalSize_; /* this is for the additional delete */
++totalOndiskSize_;
++fileCount_;
}
else {
descend = false;
}
}
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)) {
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);
descend = (g_strcmp0(fs_id, dest_fs_id) == 0);
}
}
fm_path_unref(fm_path);
#endif
inf = nullptr;
if(descend) {
_retry_enum_children:
GErrorPtr err;
auto enu = GFileEnumeratorPtr {
g_file_enumerate_children(path.gfile().get(), query_str,
G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
cancellable().get(), &err),
false
};
if(enu) {
while(!isCancelled()) {
inf = GFileInfoPtr{g_file_enumerator_next_file(enu.get(), cancellable().get(), &err), false};
if(inf) {
FilePath child = path.child(g_file_info_get_name(inf.get()));
exec(std::move(child), std::move(inf));
}
else {
if(err) { /* error! */
/* ErrorAction::RETRY is not supported */
emitError( err, ErrorSeverity::MILD);
err = nullptr;
}
else {
/* EOF is reached, do nothing. */
break;
}
}
}
g_file_enumerator_close(enu.get(), nullptr, nullptr);
}
else {
ErrorAction act = emitError( err, ErrorSeverity::MILD);
err = nullptr;
if(act == ErrorAction::RETRY) {
goto _retry_enum_children;
}
}
}
}
}
void TotalSizeJob::exec() {
for(auto& path : paths_) {
exec(path, GFileInfoPtr{});
}
}
} // namespace Fm

@ -0,0 +1,56 @@
#ifndef FM2_TOTALSIZEJOB_H
#define FM2_TOTALSIZEJOB_H
#include "../libfmqtglobals.h"
#include "fileoperationjob.h"
#include "filepath.h"
#include <cstdint>
#include "gioptrs.h"
namespace Fm {
class LIBFM_QT_API TotalSizeJob : public Fm::FileOperationJob {
Q_OBJECT
public:
enum Flags {
DEFAULT = 0,
FOLLOW_LINKS = 1 << 0,
SAME_FS = 1 << 1,
PREPARE_MOVE = 1 << 2,
PREPARE_DELETE = 1 << 3
};
explicit TotalSizeJob(FilePathList paths = FilePathList{}, Flags flags = DEFAULT);
std::uint64_t totalSize() const {
return totalSize_;
}
std::uint64_t totalOnDiskSize() const {
return totalOndiskSize_;
}
unsigned int fileCount() const {
return fileCount_;
}
protected:
void exec() override;
private:
void exec(FilePath path, GFileInfoPtr inf);
private:
FilePathList paths_;
int flags_;
std::uint64_t totalSize_;
std::uint64_t totalOndiskSize_;
unsigned int fileCount_;
const char* dest_fs_id;
};
} // namespace Fm
#endif // FM2_TOTALSIZEJOB_H

@ -0,0 +1,73 @@
#include "trashjob.h"
namespace Fm {
TrashJob::TrashJob(const FilePathList& paths): paths_{paths} {
}
TrashJob::TrashJob(const FilePathList&& paths): paths_{paths} {
}
void TrashJob::exec() {
setTotalAmount(paths_.size(), paths_.size());
Q_EMIT preparedToRun();
/* FIXME: we shouldn't trash a file already in trash:/// */
for(auto& path : paths_) {
if(isCancelled()) {
break;
}
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
};
bool ret = FALSE;
if(fm_config->no_usb_trash) {
err.reset();
GMountPtr mnt{g_file_find_enclosing_mount(gf, nullptr, &err), false};
if(mnt) {
ret = g_mount_can_unmount(mnt.get()); /* TRUE if it's removable media */
if(ret) {
unsupportedFiles_.push_back(path);
}
}
}
if(!ret) {
err.reset();
ret = g_file_trash(gf, cancellable().get(), &err);
}
if(!ret) {
/* 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);
}
else {
ErrorAction act = emitError(err, ErrorSeverity::MODERATE);
if(act == ErrorAction::RETRY) {
err.reset();
}
else if(act == ErrorAction::ABORT) {
cancel();
return;
}
else {
break;
}
}
}
}
addFinishedAmount(1, 1);
}
}
} // namespace Fm

@ -0,0 +1,31 @@
#ifndef FM2_TRASHJOB_H
#define FM2_TRASHJOB_H
#include "../libfmqtglobals.h"
#include "fileoperationjob.h"
#include "filepath.h"
namespace Fm {
class LIBFM_QT_API TrashJob : public Fm::FileOperationJob {
Q_OBJECT
public:
explicit TrashJob(const FilePathList& paths);
explicit TrashJob(const FilePathList&& paths);
FilePathList unsupportedFiles() const {
return unsupportedFiles_;
}
protected:
void exec() override;
private:
FilePathList paths_;
FilePathList unsupportedFiles_;
};
} // namespace Fm
#endif // FM2_TRASHJOB_H

@ -0,0 +1,132 @@
#include "untrashjob.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;
}
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;
}
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));
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);
}
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;
}
}
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);
if(err) {
ErrorAction act = emitError( err, ErrorSeverity::MODERATE);
g_error_free(err);
err = nullptr;
if(act == ErrorAction::RETRY) {
goto _retry_get_orig_path;
}
else if(act == ErrorAction::ABORT) {
g_object_unref(gf);
return FALSE;
}
}
}
g_object_unref(gf);
++job->finished;
fm_file_ops_job_emit_percent(job);
}
#endif
}
} // namespace Fm

@ -0,0 +1,22 @@
#ifndef FM2_UNTRASHJOB_H
#define FM2_UNTRASHJOB_H
#include "../libfmqtglobals.h"
#include "fileoperationjob.h"
namespace Fm {
class LIBFM_QT_API UntrashJob : public Fm::FileOperationJob {
public:
explicit UntrashJob();
protected:
void exec() override;
private:
bool ensure_parent_dir(GFile *orig_path);
};
} // namespace Fm
#endif // FM2_UNTRASHJOB_H

@ -0,0 +1,47 @@
#include "userinfocache.h"
#include <pwd.h>
#include <grp.h>
namespace Fm {
UserInfoCache* UserInfoCache::globalInstance_ = nullptr;
std::mutex UserInfoCache::mutex_;
UserInfoCache::UserInfoCache() : QObject() {
}
const std::shared_ptr<const UserInfo>& UserInfoCache::userFromId(uid_t uid) {
std::lock_guard<std::mutex> lock{mutex_};
auto it = users_.find(uid);
if(it != users_.end())
return it->second;
std::shared_ptr<const UserInfo> user;
auto pw = getpwuid(uid);
if(pw) {
user = std::make_shared<UserInfo>(uid, pw->pw_name, pw->pw_gecos);
}
return (users_[uid] = user);
}
const std::shared_ptr<const GroupInfo>& UserInfoCache::groupFromId(gid_t gid) {
std::lock_guard<std::mutex> lock{mutex_};
auto it = groups_.find(gid);
if(it != groups_.end())
return it->second;
std::shared_ptr<const GroupInfo> group;
auto gr = getgrgid(gid);
if(gr) {
group = std::make_shared<GroupInfo>(gid, gr->gr_name);
}
return (groups_[gid] = group);
}
// static
UserInfoCache* UserInfoCache::globalInstance() {
std::lock_guard<std::mutex> lock{mutex_};
if(!globalInstance_)
globalInstance_ = new UserInfoCache();
return globalInstance_;
}
} // namespace Fm

@ -0,0 +1,82 @@
#ifndef FM2_USERINFOCACHE_H
#define FM2_USERINFOCACHE_H
#include "../libfmqtglobals.h"
#include <QObject>
#include <string>
#include <unordered_map>
#include <sys/types.h>
#include <memory>
#include <mutex>
namespace Fm {
class LIBFM_QT_API UserInfo {
public:
explicit UserInfo(uid_t uid, const char* name, const char* realName):
uid_{uid}, name_{name}, realName_{realName} {
}
uid_t uid() const {
return uid_;
}
const QString& name() const {
return name_;
}
const QString& realName() const {
return realName_;
}
private:
uid_t uid_;
QString name_;
QString realName_;
};
class LIBFM_QT_API GroupInfo {
public:
explicit GroupInfo(gid_t gid, const char* name): gid_{gid}, name_{name} {
}
gid_t gid() const {
return gid_;
}
const QString& name() const {
return name_;
}
private:
gid_t gid_;
QString name_;
};
// FIXME: handle file changes
class LIBFM_QT_API UserInfoCache : public QObject {
Q_OBJECT
public:
explicit UserInfoCache();
const std::shared_ptr<const UserInfo>& userFromId(uid_t uid);
const std::shared_ptr<const GroupInfo>& groupFromId(gid_t gid);
static UserInfoCache* globalInstance();
Q_SIGNALS:
void changed();
private:
std::unordered_map<uid_t, std::shared_ptr<const UserInfo>> users_;
std::unordered_map<gid_t, std::shared_ptr<const GroupInfo>> groups_;
static UserInfoCache* globalInstance_;
static std::mutex mutex_;
};
} // namespace Fm
#endif // FM2_USERINFOCACHE_H

@ -0,0 +1,111 @@
#include "volumemanager.h"
namespace Fm {
std::mutex VolumeManager::mutex_;
std::weak_ptr<VolumeManager> VolumeManager::globalInstance_;
VolumeManager::VolumeManager():
QObject(),
monitor_{g_volume_monitor_get(), false} {
// connect gobject signal handlers
g_signal_connect(monitor_.get(), "volume-added", G_CALLBACK(_onGVolumeAdded), this);
g_signal_connect(monitor_.get(), "volume-removed", G_CALLBACK(_onGVolumeRemoved), this);
g_signal_connect(monitor_.get(), "volume-changed", G_CALLBACK(_onGVolumeChanged), this);
g_signal_connect(monitor_.get(), "mount-added", G_CALLBACK(_onGMountAdded), this);
g_signal_connect(monitor_.get(), "mount-removed", G_CALLBACK(_onGMountRemoved), this);
g_signal_connect(monitor_.get(), "mount-changed", G_CALLBACK(_onGMountChanged), this);
// g_get_volume_monitor() is a slow blocking call, so call it in a low priority thread
auto job = new GetGVolumeMonitorJob();
job->setAutoDelete(true);
connect(job, &GetGVolumeMonitorJob::finished, this, &VolumeManager::onGetGVolumeMonitorFinished, Qt::BlockingQueuedConnection);
job->runAsync(QThread::LowPriority);
}
VolumeManager::~VolumeManager() {
if(monitor_) {
g_signal_handlers_disconnect_by_data(monitor_.get(), this);
}
}
std::shared_ptr<VolumeManager> VolumeManager::globalInstance() {
std::lock_guard<std::mutex> lock{mutex_};
auto mon = globalInstance_.lock();
if(mon == nullptr) {
mon = std::make_shared<VolumeManager>();
globalInstance_ = mon;
}
return mon;
}
void VolumeManager::onGetGVolumeMonitorFinished() {
auto job = static_cast<GetGVolumeMonitorJob*>(sender());
monitor_ = std::move(job->monitor_);
GList* vols = g_volume_monitor_get_volumes(monitor_.get());
for(GList* l = vols; l != nullptr; l = l->next) {
volumes_.push_back(Volume{G_VOLUME(l->data), false});
Q_EMIT volumeAdded(volumes_.back());
}
g_list_free(vols);
GList* mnts = g_volume_monitor_get_mounts(monitor_.get());
for(GList* l = mnts; l != nullptr; l = l->next) {
mounts_.push_back(Mount{G_MOUNT(l->data), false});
Q_EMIT mountAdded(mounts_.back());
}
g_list_free(mnts);
}
void VolumeManager::onGVolumeAdded(GVolume* vol) {
if(std::find(volumes_.cbegin(), volumes_.cend(), vol) != volumes_.cend())
return;
volumes_.push_back(Volume{vol, true});
Q_EMIT volumeAdded(volumes_.back());
}
void VolumeManager::onGVolumeRemoved(GVolume* vol) {
auto it = std::find(volumes_.begin(), volumes_.end(), vol);
if(it == volumes_.end())
return;
Q_EMIT volumeRemoved(*it);
volumes_.erase(it);
}
void VolumeManager::onGVolumeChanged(GVolume* vol) {
auto it = std::find(volumes_.begin(), volumes_.end(), vol);
if(it == volumes_.end())
return;
Q_EMIT volumeChanged(*it);
}
void VolumeManager::onGMountAdded(GMount* mnt) {
if(std::find(mounts_.cbegin(), mounts_.cend(), mnt) != mounts_.cend())
return;
mounts_.push_back(Mount{mnt, true});
Q_EMIT mountAdded(mounts_.back());
}
void VolumeManager::onGMountRemoved(GMount* mnt) {
auto it = std::find(mounts_.begin(), mounts_.end(), mnt);
if(it == mounts_.end())
return;
Q_EMIT mountRemoved(*it);
mounts_.erase(it);
}
void VolumeManager::onGMountChanged(GMount* mnt) {
auto it = std::find(mounts_.begin(), mounts_.end(), mnt);
if(it == mounts_.end())
return;
Q_EMIT mountChanged(*it);
}
void VolumeManager::GetGVolumeMonitorJob::exec() {
monitor_ = GVolumeMonitorPtr{g_volume_monitor_get(), false};
}
} // namespace Fm

@ -0,0 +1,237 @@
#ifndef FM2_VOLUMEMANAGER_H
#define FM2_VOLUMEMANAGER_H
#include "../libfmqtglobals.h"
#include <QObject>
#include <gio/gio.h>
#include "gioptrs.h"
#include "filepath.h"
#include "iconinfo.h"
#include "job.h"
#include <vector>
#include <mutex>
namespace Fm {
class LIBFM_QT_API Volume: public GVolumePtr {
public:
explicit Volume(GVolume* gvol, bool addRef): GVolumePtr{gvol, addRef} {
}
explicit Volume(GVolumePtr gvol): GVolumePtr{std::move(gvol)} {
}
CStrPtr name() const {
return CStrPtr{g_volume_get_name(get())};
}
CStrPtr uuid() const {
return CStrPtr{g_volume_get_uuid(get())};
}
std::shared_ptr<const IconInfo> icon() const {
return IconInfo::fromGIcon(GIconPtr{g_volume_get_icon(get()), false});
}
// GDrive * g_volume_get_drive(get());
GMountPtr mount() const {
return GMountPtr{g_volume_get_mount(get()), false};
}
bool canMount() const {
return g_volume_can_mount(get());
}
bool shouldAutoMount() const {
return g_volume_should_automount(get());
}
FilePath activationRoot() const {
return FilePath{g_volume_get_activation_root(get()), false};
}
/*
void g_volume_mount(get());
gboolean g_volume_mount_finish(get());
*/
bool canEject() const {
return g_volume_can_eject(get());
}
/*
void g_volume_eject(get());
gboolean g_volume_eject_finish(get());
void g_volume_eject_with_operation(get());
gboolean g_volume_eject_with_operation_finish(get());
char ** g_volume_enumerate_identifiers(get());
char * g_volume_get_identifier(get());
const gchar * g_volume_get_sort_key(get());
*/
CStrPtr device() const {
return CStrPtr{g_volume_get_identifier(get(), G_VOLUME_IDENTIFIER_KIND_UNIX_DEVICE)};
}
CStrPtr label() const {
return CStrPtr{g_volume_get_identifier(get(), G_VOLUME_IDENTIFIER_KIND_LABEL)};
}
};
class LIBFM_QT_API Mount: public GMountPtr {
public:
explicit Mount(GMount* mnt, bool addRef): GMountPtr{mnt, addRef} {
}
explicit Mount(GMountPtr gmnt): GMountPtr{std::move(gmnt)} {
}
CStrPtr name() const {
return CStrPtr{g_mount_get_name(get())};
}
CStrPtr uuid() const {
return CStrPtr{g_mount_get_uuid(get())};
}
std::shared_ptr<const IconInfo> icon() const {
return IconInfo::fromGIcon(GIconPtr{g_mount_get_icon(get()), false});
}
// GIcon * g_mount_get_symbolic_icon(get());
// GDrive * g_mount_get_drive(get());
FilePath root() const {
return FilePath{g_mount_get_root(get()), false};
}
GVolumePtr volume() const {
return GVolumePtr{g_mount_get_volume(get()), false};
}
FilePath defaultLocation() const {
return FilePath{g_mount_get_default_location(get()), false};
}
bool canUnmount() const {
return g_mount_can_unmount(get());
}
/*
void g_mount_unmount(get());
gboolean g_mount_unmount_finish(get());
void g_mount_unmount_with_operation(get());
gboolean g_mount_unmount_with_operation_finish(get());
void g_mount_remount(get());
gboolean g_mount_remount_finish(get());
*/
bool canEject() const {
return g_mount_can_eject(get());
}
/*
void g_mount_eject(get());
gboolean g_mount_eject_finish(get());
void g_mount_eject_with_operation(get());
gboolean g_mount_eject_with_operation_finish(get());
*/
// void g_mount_guess_content_type(get());
// gchar ** g_mount_guess_content_type_finish(get());
// gchar ** g_mount_guess_content_type_sync(get());
bool isShadowed() const {
return g_mount_is_shadowed(get());
}
// void g_mount_shadow(get());
// void g_mount_unshadow(get());
// const gchar * g_mount_get_sort_key(get());
};
class LIBFM_QT_API VolumeManager : public QObject {
Q_OBJECT
public:
explicit VolumeManager();
~VolumeManager();
const std::vector<Volume>& volumes() const {
return volumes_;
}
const std::vector<Mount>& mounts() const {
return mounts_;
}
static std::shared_ptr<VolumeManager> globalInstance();
Q_SIGNALS:
void volumeAdded(const Volume& vol);
void volumeRemoved(const Volume& vol);
void volumeChanged(const Volume& vol);
void mountAdded(const Mount& mnt);
void mountRemoved(const Mount& mnt);
void mountChanged(const Mount& mnt);
public Q_SLOTS:
void onGetGVolumeMonitorFinished();
private:
class GetGVolumeMonitorJob: public Job {
public:
GetGVolumeMonitorJob() {}
GVolumeMonitorPtr monitor_;
protected:
void exec() override;
};
static void _onGVolumeAdded(GVolumeMonitor* /*mon*/, GVolume* vol, VolumeManager* _this) {
_this->onGVolumeAdded(vol);
}
void onGVolumeAdded(GVolume* vol);
static void _onGVolumeRemoved(GVolumeMonitor* /*mon*/, GVolume* vol, VolumeManager* _this) {
_this->onGVolumeRemoved(vol);
}
void onGVolumeRemoved(GVolume* vol);
static void _onGVolumeChanged(GVolumeMonitor* /*mon*/, GVolume* vol, VolumeManager* _this) {
_this->onGVolumeChanged(vol);
}
void onGVolumeChanged(GVolume* vol);
static void _onGMountAdded(GVolumeMonitor* /*mon*/, GMount* mnt, VolumeManager* _this) {
_this->onGMountAdded(mnt);
}
void onGMountAdded(GMount* mnt);
static void _onGMountRemoved(GVolumeMonitor* /*mon*/, GMount* mnt, VolumeManager* _this) {
_this->onGMountRemoved(mnt);
}
void onGMountRemoved(GMount* mnt);
static void _onGMountChanged(GVolumeMonitor* /*mon*/, GMount* mnt, VolumeManager* _this) {
_this->onGMountChanged(mnt);
}
void onGMountChanged(GMount* mnt);
private:
GVolumeMonitorPtr monitor_;
std::vector<Volume> volumes_;
std::vector<Mount> mounts_;
static std::mutex mutex_;
static std::weak_ptr<VolumeManager> globalInstance_;
};
} // namespace Fm
#endif // FM2_VOLUMEMANAGER_H

@ -21,70 +21,76 @@
#include "folderview.h" #include "folderview.h"
#include "icontheme.h" #include "icontheme.h"
#include "utilities.h" #include "utilities.h"
#include "core/iconinfo.h"
namespace Fm { namespace Fm {
CreateNewMenu::CreateNewMenu(QWidget* dialogParent, FmPath* dirPath, QWidget* parent): CreateNewMenu::CreateNewMenu(QWidget* dialogParent, Fm::FilePath dirPath, QWidget* parent):
QMenu(parent), dialogParent_(dialogParent), dirPath_(dirPath) { QMenu(parent), dialogParent_(dialogParent), dirPath_(std::move(dirPath)) {
QAction* action = new QAction(QIcon::fromTheme("folder-new"), tr("Folder"), this); QAction* action = new QAction(QIcon::fromTheme("folder-new"), tr("Folder"), this);
connect(action, &QAction::triggered, this, &CreateNewMenu::onCreateNewFolder); connect(action, &QAction::triggered, this, &CreateNewMenu::onCreateNewFolder);
addAction(action); addAction(action);
action = new QAction(QIcon::fromTheme("document-new"), tr("Blank File"), this); action = new QAction(QIcon::fromTheme("document-new"), tr("Blank File"), this);
connect(action, &QAction::triggered, this, &CreateNewMenu::onCreateNewFile); connect(action, &QAction::triggered, this, &CreateNewMenu::onCreateNewFile);
addAction(action); addAction(action);
// add more items to "Create New" menu from templates // add more items to "Create New" menu from templates
GList* templates = fm_template_list_all(fm_config->only_user_templates); GList* templates = fm_template_list_all(fm_config->only_user_templates);
if(templates) { if(templates) {
addSeparator(); addSeparator();
for(GList* l = templates; l; l = l->next) { for(GList* l = templates; l; l = l->next) {
FmTemplate* templ = (FmTemplate*)l->data; FmTemplate* templ = (FmTemplate*)l->data;
/* we support directories differently */ /* we support directories differently */
if(fm_template_is_directory(templ)) if(fm_template_is_directory(templ)) {
continue; continue;
FmMimeType* mime_type = fm_template_get_mime_type(templ); }
const char* label = fm_template_get_label(templ); FmMimeType* mime_type = fm_template_get_mime_type(templ);
QString text = QString("%1 (%2)").arg(QString::fromUtf8(label)).arg(QString::fromUtf8(fm_mime_type_get_desc(mime_type))); const char* label = fm_template_get_label(templ);
FmIcon* icon = fm_template_get_icon(templ); QString text = QString("%1 (%2)").arg(QString::fromUtf8(label)).arg(QString::fromUtf8(fm_mime_type_get_desc(mime_type)));
if(!icon) FmIcon* icon = fm_template_get_icon(templ);
icon = fm_mime_type_get_icon(mime_type); if(!icon) {
QAction* action = addAction(IconTheme::icon(icon), text); icon = fm_mime_type_get_icon(mime_type);
action->setObjectName(QString::fromUtf8(fm_template_get_name(templ, NULL))); }
connect(action, &QAction::triggered, this, &CreateNewMenu::onCreateNew); 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);
}
} }
}
} }
CreateNewMenu::~CreateNewMenu() { CreateNewMenu::~CreateNewMenu() {
} }
void CreateNewMenu::onCreateNewFile() { void CreateNewMenu::onCreateNewFile() {
if(dirPath_) if(dirPath_) {
createFileOrFolder(CreateNewTextFile, dirPath_); createFileOrFolder(CreateNewTextFile, dirPath_);
}
} }
void CreateNewMenu::onCreateNewFolder() { void CreateNewMenu::onCreateNewFolder() {
if(dirPath_) if(dirPath_) {
createFileOrFolder(CreateNewFolder, dirPath_); createFileOrFolder(CreateNewFolder, dirPath_);
}
} }
void CreateNewMenu::onCreateNew() { void CreateNewMenu::onCreateNew() {
QAction* action = static_cast<QAction*>(sender()); QAction* action = static_cast<QAction*>(sender());
QByteArray name = action->objectName().toUtf8(); QByteArray name = action->objectName().toUtf8();
GList* templates = fm_template_list_all(fm_config->only_user_templates); GList* templates = fm_template_list_all(fm_config->only_user_templates);
FmTemplate* templ = NULL; FmTemplate* templ = nullptr;
for(GList* l = templates; l; l = l->next) { for(GList* l = templates; l; l = l->next) {
FmTemplate* t = (FmTemplate*)l->data; FmTemplate* t = (FmTemplate*)l->data;
if(name == fm_template_get_name(t, NULL)) { if(name == fm_template_get_name(t, nullptr)) {
templ = t; templ = t;
break; break;
}
}
if(templ) { // template found
if(dirPath_) {
createFileOrFolder(CreateWithTemplate, dirPath_, templ, dialogParent_);
}
} }
}
if(templ) { // template found
if(dirPath_)
createFileOrFolder(CreateWithTemplate, dirPath_, templ, dialogParent_);
}
} }
} // namespace Fm } // namespace Fm

@ -24,26 +24,27 @@
#include <QMenu> #include <QMenu>
#include <libfm/fm.h> #include <libfm/fm.h>
#include "core/filepath.h"
namespace Fm { namespace Fm {
class FolderView; class FolderView;
class LIBFM_QT_API CreateNewMenu : public QMenu { class LIBFM_QT_API CreateNewMenu : public QMenu {
Q_OBJECT Q_OBJECT
public: public:
explicit CreateNewMenu(QWidget* dialogParent, FmPath* dirPath, explicit CreateNewMenu(QWidget* dialogParent, Fm::FilePath dirPath, QWidget* parent = 0);
QWidget* parent = 0); virtual ~CreateNewMenu();
virtual ~CreateNewMenu();
protected Q_SLOTS: protected Q_SLOTS:
void onCreateNewFolder(); void onCreateNewFolder();
void onCreateNewFile(); void onCreateNewFile();
void onCreateNew(); void onCreateNew();
private: private:
QWidget* dialogParent_; QWidget* dialogParent_;
FmPath* dirPath_; Fm::FilePath dirPath_;
}; };
} }

@ -20,28 +20,31 @@
#ifndef FM_CUSTOMACTION_P_H #ifndef FM_CUSTOMACTION_P_H
#define FM_CUSTOMACTION_P_H #define FM_CUSTOMACTION_P_H
#include <QAction>
#include "customactions/fileaction.h"
namespace Fm { namespace Fm {
class CustomAction : public QAction { class CustomAction : public QAction {
public: public:
explicit CustomAction(FmFileActionItem* item, QObject* parent = NULL): explicit CustomAction(std::shared_ptr<const FileActionItem> item, QObject* parent = nullptr):
QAction(QString::fromUtf8(fm_file_action_item_get_name(item)), parent), QAction{QString::fromStdString(item->get_name()), parent},
item_(reinterpret_cast<FmFileActionItem*>(fm_file_action_item_ref(item))) { item_{item} {
const char* icon_name = fm_file_action_item_get_icon(item); auto& icon_name = item->get_icon();
if(icon_name) if(!icon_name.empty()) {
setIcon(QIcon::fromTheme(icon_name)); setIcon(QIcon::fromTheme(icon_name.c_str()));
} }
}
virtual ~CustomAction() {
fm_file_action_item_unref(item_); virtual ~CustomAction() {
} }
FmFileActionItem* item() { const std::shared_ptr<const FileActionItem>& item() const {
return item_; return item_;
} }
private: private:
FmFileActionItem* item_; std::shared_ptr<const FileActionItem> item_;
}; };
} // namespace Fm } // namespace Fm

@ -0,0 +1,615 @@
#include "fileaction.h"
#include <unordered_map>
#include <QDebug>
using namespace std;
namespace Fm {
static const char* desktop_env = nullptr; // current desktop environment
static bool actions_loaded = false; // all actions are loaded?
static unordered_map<const char*, shared_ptr<FileActionObject>, CStrHash, CStrEqual> all_actions; // cache all loaded actions
FileActionObject::FileActionObject() {
}
FileActionObject::FileActionObject(GKeyFile* kf) {
name = CStrPtr{g_key_file_get_locale_string(kf, "Desktop Entry", "Name", nullptr, nullptr)};
tooltip = CStrPtr{g_key_file_get_locale_string(kf, "Desktop Entry", "Tooltip", nullptr, nullptr)};
icon = CStrPtr{g_key_file_get_locale_string(kf, "Desktop Entry", "Icon", nullptr, nullptr)};
desc = CStrPtr{g_key_file_get_locale_string(kf, "Desktop Entry", "Description", nullptr, nullptr)};
GErrorPtr err;
enabled = g_key_file_get_boolean(kf, "Desktop Entry", "Enabled", &err);
if(err) { // key not found, default to true
err.reset();
enabled = true;
}
hidden = g_key_file_get_boolean(kf, "Desktop Entry", "Hidden", nullptr);
suggested_shortcut = CStrPtr{g_key_file_get_string(kf, "Desktop Entry", "SuggestedShortcut", nullptr)};
condition = unique_ptr<FileActionCondition> {new FileActionCondition(kf, "Desktop Entry")};
has_parent = false;
}
FileActionObject::~FileActionObject() {
}
//static
bool FileActionObject::is_plural_exec(const char* exec) {
if(!exec) {
return false;
}
// the first relevent code encountered in Exec parameter
// determines whether the command accepts singular or plural forms
for(int i = 0; exec[i]; ++i) {
char ch = exec[i];
if(ch == '%') {
++i;
ch = exec[i];
switch(ch) {
case 'B':
case 'D':
case 'F':
case 'M':
case 'O':
case 'U':
case 'W':
case 'X':
return true; // plural
case 'b':
case 'd':
case 'f':
case 'm':
case 'o':
case 'u':
case 'w':
case 'x':
return false; // singular
default:
// irrelevent code, skip
break;
}
}
}
return false; // singular form by default
}
std::string FileActionObject::expand_str(const char* templ, const FileInfoList& files, bool for_display, std::shared_ptr<const FileInfo> first_file) {
if(!templ) {
return string{};
}
string result;
if(!first_file) {
first_file = files.front();
}
for(int i = 0; templ[i]; ++i) {
char ch = templ[i];
if(ch == '%') {
++i;
ch = templ[i];
switch(ch) {
case 'b': // (first) basename
if(for_display) {
result += first_file->name();
}
else {
CStrPtr quoted{g_shell_quote(first_file->name().c_str())};
result += quoted.get();
}
break;
case 'B': // space-separated list of basenames
for(auto& fi : files) {
if(for_display) {
result += fi->name();
}
else {
CStrPtr quoted{g_shell_quote(fi->name().c_str())};
result += quoted.get();
}
result += ' ';
}
if(result[result.length() - 1] == ' ') { // remove trailing space
result.erase(result.length() - 1);
}
break;
case 'c': // count of selected items
result += to_string(files.size());
break;
case 'd': { // (first) base directory
// FIXME: should the base dir be a URI?
auto base_dir = first_file->dirPath();
auto str = base_dir.toString();
if(for_display) {
// FIXME: str = Filename.display_name(str);
}
CStrPtr quoted{g_shell_quote(str.get())};
result += quoted.get();
break;
}
case 'D': // space-separated list of base directory of each selected items
for(auto& fi : files) {
auto base_dir = fi->dirPath();
auto str = base_dir.toString();
if(for_display) {
// str = Filename.display_name(str);
}
CStrPtr quoted{g_shell_quote(str.get())};
result += quoted.get();
result += ' ';
}
if(result[result.length() - 1] == ' ') { // remove trailing space
result.erase(result.length() - 1);
}
break;
case 'f': { // (first) file name
auto filename = first_file->path().toString();
if(for_display) {
// filename = Filename.display_name(filename);
}
CStrPtr quoted{g_shell_quote(filename.get())};
result += quoted.get();
break;
}
case 'F': // space-separated list of selected file names
for(auto& fi : files) {
auto filename = fi->path().toString();
if(for_display) {
// filename = Filename.display_name(filename);
}
CStrPtr quoted{g_shell_quote(filename.get())};
result += quoted.get();
result += ' ';
}
if(result[result.length() - 1] == ' ') { // remove trailing space
result.erase(result.length() - 1);
}
break;
case 'h': // hostname of the (first) URI
// FIXME: how to support this correctly?
// FIXME: currently we pass g_get_host_name()
result += g_get_host_name();
break;
case 'm': // mimetype of the (first) selected item
result += first_file->mimeType()->name();
break;
case 'M': // space-separated list of the mimetypes of the selected items
for(auto& fi : files) {
result += fi->mimeType()->name();
result += ' ';
}
break;
case 'n': // username of the (first) URI
// FIXME: how to support this correctly?
result += g_get_user_name();
break;
case 'o': // no-op operator which forces a singular form of execution when specified as first parameter,
case 'O': // no-op operator which forces a plural form of execution when specified as first parameter,
break;
case 'p': // port number of the (first) URI
// FIXME: how to support this correctly?
// result.append("0");
break;
case 's': // scheme of the (first) URI
result += first_file->path().uriScheme().get();
break;
case 'u': // (first) URI
result += first_file->path().uri().get();
break;
case 'U': // space-separated list of selected URIs
for(auto& fi : files) {
result += fi->path().uri().get();
result += ' ';
}
if(result[result.length() - 1] == ' ') { // remove trailing space
result.erase(result.length() - 1);
}
break;
case 'w': { // (first) basename without the extension
auto basename = first_file->name();
int pos = basename.rfind('.');
// FIXME: handle non-UTF8 filenames
if(pos != -1) {
basename.erase(pos, string::npos);
}
CStrPtr quoted{g_shell_quote(basename.c_str())};
result += quoted.get();
break;
}
case 'W': // space-separated list of basenames without their extension
for(auto& fi : files) {
auto basename = fi->name();
int pos = basename.rfind('.');
// FIXME: for_display ? Shell.quote(str) : str);
if(pos != -1) {
basename.erase(pos, string::npos);
}
CStrPtr quoted{g_shell_quote(basename.c_str())};
result += quoted.get();
result += ' ';
}
if(result[result.length() - 1] == ' ') { // remove trailing space
result.erase(result.length() - 1);
}
break;
case 'x': { // (first) extension
auto basename = first_file->name();
int pos = basename.rfind('.');
const char* ext = "";
if(pos >= 0) {
ext = basename.c_str() + pos + 1;
}
CStrPtr quoted{g_shell_quote(ext)};
result += quoted.get();
break;
}
case 'X': // space-separated list of extensions
for(auto& fi : files) {
auto basename = fi->name();
int pos = basename.rfind('.');
const char* ext = "";
if(pos >= 0) {
ext = basename.c_str() + pos + 1;
}
CStrPtr quoted{g_shell_quote(ext)};
result += quoted.get();
result += ' ';
}
if(result[result.length() - 1] == ' ') { // remove trailing space
result.erase(result.length() - 1);
}
break;
case '%': // the % character
result += '%';
break;
case '\0':
break;
}
}
else {
result += ch;
}
}
return result;
}
FileAction::FileAction(GKeyFile* kf): FileActionObject{kf}, target{FILE_ACTION_TARGET_CONTEXT} {
type = FileActionType::ACTION;
GErrorPtr err;
if(g_key_file_get_boolean(kf, "Desktop Entry", "TargetContext", &err)) { // default to true
target |= FILE_ACTION_TARGET_CONTEXT;
}
else if(!err) { // error means the key is abscent
target &= ~FILE_ACTION_TARGET_CONTEXT;
}
if(g_key_file_get_boolean(kf, "Desktop Entry", "TargetLocation", nullptr)) {
target |= FILE_ACTION_TARGET_LOCATION;
}
if(g_key_file_get_boolean(kf, "Desktop Entry", "TargetToolbar", nullptr)) {
target |= FILE_ACTION_TARGET_TOOLBAR;
}
toolbar_label = CStrPtr{g_key_file_get_locale_string(kf, "Desktop Entry", "ToolbarLabel", nullptr, nullptr)};
auto profile_names = CStrArrayPtr{g_key_file_get_string_list(kf, "Desktop Entry", "Profiles", nullptr, nullptr)};
if(profile_names != nullptr) {
for(auto profile_name = profile_names.get(); *profile_name; ++profile_name) {
// stdout.printf("%s", profile);
profiles.push_back(make_shared<FileActionProfile>(kf, *profile_name));
}
}
}
std::shared_ptr<FileActionProfile> FileAction::match(const FileInfoList& files) const {
//qDebug() << "FileAction.match: " << id.get();
if(hidden || !enabled) {
return nullptr;
}
if(!condition->match(files)) {
return nullptr;
}
for(const auto& profile : profiles) {
if(profile->match(files)) {
//qDebug() << " profile matched!\n\n";
return profile;
}
}
// stdout.printf("\n");
return nullptr;
}
FileActionMenu::FileActionMenu(GKeyFile* kf): FileActionObject{kf} {
type = FileActionType::MENU;
items_list = CStrArrayPtr{g_key_file_get_string_list(kf, "Desktop Entry", "ItemsList", nullptr, nullptr)};
}
bool FileActionMenu::match(const FileInfoList& files) const {
// stdout.printf("FileActionMenu.match: %s\n", id);
if(hidden || !enabled) {
return false;
}
if(!condition->match(files)) {
return false;
}
// stdout.printf("menu matched!: %s\n\n", id);
return true;
}
void FileActionMenu::cache_children(const FileInfoList& files, const char** items_list) {
for(; *items_list; ++items_list) {
const char* item_id_prefix = *items_list;
size_t len = strlen(item_id_prefix);
if(item_id_prefix[0] == '[' && item_id_prefix[len - 1] == ']') {
// runtime dynamic item list
char* output;
int exit_status;
string prefix{item_id_prefix + 1, len - 2}; // skip [ and ]
auto command = expand_str(prefix.c_str(), files);
if(g_spawn_command_line_sync(command.c_str(), &output, nullptr, &exit_status, nullptr) && exit_status == 0) {
CStrArrayPtr item_ids{g_strsplit(output, ";", -1)};
g_free(output);
cache_children(files, (const char**)item_ids.get());
}
}
else if(strcmp(item_id_prefix, "SEPARATOR") == 0) {
// separator item
cached_children.push_back(nullptr);
}
else {
CStrPtr item_id{g_strconcat(item_id_prefix, ".desktop", nullptr)};
auto it = all_actions.find(item_id.get());
if(it != all_actions.end()) {
auto child_action = it->second;
child_action->has_parent = true;
cached_children.push_back(child_action);
// stdout.printf("add child: %s to menu: %s\n", item_id, id);
}
}
}
}
std::shared_ptr<FileActionItem> FileActionItem::fromActionObject(std::shared_ptr<FileActionObject> action_obj, const FileInfoList& files) {
std::shared_ptr<FileActionItem> item;
if(action_obj->type == FileActionType::MENU) {
auto menu = static_pointer_cast<FileActionMenu>(action_obj);
if(menu->match(files)) {
item = make_shared<FileActionItem>(menu, files);
// eliminate empty menus
if(item->children.empty()) {
item = nullptr;
}
}
}
else {
// handle profiles here
auto action = static_pointer_cast<FileAction>(action_obj);
auto profile = action->match(files);
if(profile) {
item = make_shared<FileActionItem>(action, profile, files);
}
}
return item;
}
FileActionItem::FileActionItem(std::shared_ptr<FileAction> _action, std::shared_ptr<FileActionProfile> _profile, const FileInfoList& files):
FileActionItem{static_pointer_cast<FileActionObject>(_action), files} {
profile = _profile;
}
FileActionItem::FileActionItem(std::shared_ptr<FileActionMenu> menu, const FileInfoList& files):
FileActionItem{static_pointer_cast<FileActionObject>(menu), files} {
for(auto& action_obj : menu->cached_children) {
if(action_obj == nullptr) { // separator
children.push_back(nullptr);
}
else { // action item or menu
auto subitem = fromActionObject(action_obj, files);
if(subitem != nullptr) {
children.push_back(subitem);
}
}
}
}
FileActionItem::FileActionItem(std::shared_ptr<FileActionObject> _action, const FileInfoList& files) {
action = std::move(_action);
name = FileActionObject::expand_str(action->name.get(), files, true);
desc = FileActionObject::expand_str(action->desc.get(), files, true);
icon = FileActionObject::expand_str(action->icon.get(), files, false);
}
bool FileActionItem::launch(GAppLaunchContext* ctx, const FileInfoList& files, CStrPtr& output) const {
if(action->type == FileActionType::ACTION) {
if(profile != nullptr) {
profile->launch(ctx, files, output);
}
return true;
}
return false;
}
static void load_actions_from_dir(const char* dirname, const char* id_prefix) {
//qDebug() << "loading from: " << dirname << endl;
auto dir = g_dir_open(dirname, 0, nullptr);
if(dir != nullptr) {
for(;;) {
const char* name = g_dir_read_name(dir);
if(name == nullptr) {
break;
}
// found a file in file-manager/actions dir, get its full path
CStrPtr full_path{g_build_filename(dirname, name, nullptr)};
// stdout.printf("\nfound %s\n", full_path);
// see if it's a sub dir
if(g_file_test(full_path.get(), G_FILE_TEST_IS_DIR)) {
// load sub dirs recursively
CStrPtr new_id_prefix;
if(id_prefix) {
new_id_prefix = CStrPtr{g_strconcat(id_prefix, "-", name, nullptr)};
}
load_actions_from_dir(full_path.get(), id_prefix ? new_id_prefix.get() : name);
}
else if(g_str_has_suffix(name, ".desktop")) {
CStrPtr new_id_prefix;
if(id_prefix) {
new_id_prefix = CStrPtr{g_strconcat(id_prefix, "-", name, nullptr)};
}
const char* id = id_prefix ? new_id_prefix.get() : name;
// ensure that it's not already in the cache
if(all_actions.find(id) == all_actions.cend()) {
auto kf = g_key_file_new();
if(g_key_file_load_from_file(kf, full_path.get(), G_KEY_FILE_NONE, nullptr)) {
auto type = CStrPtr{g_key_file_get_string(kf, "Desktop Entry", "Type", nullptr)};
if(!type) {
continue;
}
std::shared_ptr<FileActionObject> action;
if(strcmp(type.get(), "Action") == 0) {
action = static_pointer_cast<FileActionObject>(make_shared<FileAction>(kf));
// stdout.printf("load action: %s\n", id);
}
else if(strcmp(type.get(), "Menu") == 0) {
action = static_pointer_cast<FileActionObject>(make_shared<FileActionMenu>(kf));
// stdout.printf("load menu: %s\n", id);
}
else {
continue;
}
action->setId(id);
all_actions.insert(make_pair(action->id.get(), action)); // add the id/action pair to hash table
// stdout.printf("add to cache %s\n", id);
}
g_key_file_free(kf);
}
else {
// stdout.printf("cache found for action: %s\n", id);
}
}
}
g_dir_close(dir);
}
}
void file_actions_set_desktop_env(const char* env) {
desktop_env = env;
}
static void load_all_actions() {
all_actions.clear();
auto dirs = g_get_system_data_dirs();
for(auto dir = dirs; *dir; ++dir) {
CStrPtr dir_path{g_build_filename(*dir, "file-manager/actions", nullptr)};
load_actions_from_dir(dir_path.get(), nullptr);
}
CStrPtr dir_path{g_build_filename(g_get_user_data_dir(), "file-manager/actions", nullptr)};
load_actions_from_dir(dir_path.get(), nullptr);
actions_loaded = true;
}
bool FileActionItem::compare_items(std::shared_ptr<const FileActionItem> a, std::shared_ptr<const FileActionItem> b)
{
// first get the list of level-zero item names (http://www.nautilus-actions.org/?q=node/377)
static QStringList itemNamesList;
static bool level_zero_checked = false;
if(!level_zero_checked) {
level_zero_checked = true;
auto level_zero = CStrPtr{g_build_filename(g_get_user_data_dir(),
"file-manager/actions/level-zero.directory", nullptr)};
if(g_file_test(level_zero.get(), G_FILE_TEST_IS_REGULAR)) {
GKeyFile* kf = g_key_file_new();
if(g_key_file_load_from_file(kf, level_zero.get(), G_KEY_FILE_NONE, nullptr)) {
auto itemsList = CStrArrayPtr{g_key_file_get_string_list(kf,
"Desktop Entry",
"ItemsList", nullptr, nullptr)};
if(itemsList) {
for(uint i = 0; i < g_strv_length(itemsList.get()); ++i) {
CStrPtr desktop_file_name{g_strconcat(itemsList.get()[i], ".desktop", nullptr)};
auto desktop_file = CStrPtr{g_build_filename(g_get_user_data_dir(),
"file-manager/actions",
desktop_file_name.get(), nullptr)};
GKeyFile* desktop_file_key = g_key_file_new();
if(g_key_file_load_from_file(desktop_file_key, desktop_file.get(), G_KEY_FILE_NONE, nullptr)) {
auto actionName = CStrPtr{g_key_file_get_string(desktop_file_key,
"Desktop Entry",
"Name", NULL)};
if(actionName) {
itemNamesList << QString::fromUtf8(actionName.get());
}
}
g_key_file_free(desktop_file_key);
}
}
}
g_key_file_free(kf);
}
}
if(!itemNamesList.isEmpty()) {
int first = itemNamesList.indexOf(QString::fromStdString(a->get_name()));
int second = itemNamesList.indexOf(QString::fromStdString(b->get_name()));
if(first > -1) {
if(second > -1) {
return (first < second);
}
else {
return true; // list items have priority
}
}
else if(second > -1) {
return false;
}
}
return (a->get_name().compare(b->get_name()) < 0);
}
FileActionItemList FileActionItem::get_actions_for_files(const FileInfoList& files) {
if(!actions_loaded) {
load_all_actions();
}
// Iterate over all actions to establish association between parent menu
// and children actions, and to find out toplevel ones which are not
// attached to any parent menu
for(auto& item : all_actions) {
auto& action_obj = item.second;
// stdout.printf("id = %s\n", action_obj.id);
if(action_obj->type == FileActionType::MENU) { // this is a menu
auto menu = static_pointer_cast<FileActionMenu>(action_obj);
// stdout.printf("menu: %s\n", menu.name);
// associate child items with menus
menu->cache_children(files, (const char**)menu->items_list.get());
}
}
// Output the menus
FileActionItemList items;
for(auto& item : all_actions) {
auto& action_obj = item.second;
// only output toplevel items here
if(action_obj->has_parent == false) { // this is a toplevel item
auto item = FileActionItem::fromActionObject(action_obj, files);
if(item != nullptr) {
items.push_back(item);
}
}
}
// cleanup temporary data cached during menu generation
for(auto& item : all_actions) {
auto& action_obj = item.second;
action_obj->has_parent = false;
if(action_obj->type == FileActionType::MENU) {
auto menu = static_pointer_cast<FileActionMenu>(action_obj);
menu->cached_children.clear();
}
}
std::sort(items.begin(), items.end(), compare_items);
return items;
}
} // namespace Fm

@ -0,0 +1,156 @@
#ifndef FILEACTION_H
#define FILEACTION_H
#include <glib.h>
#include <string>
#include "../core/fileinfo.h"
#include "fileactioncondition.h"
#include "fileactionprofile.h"
namespace Fm {
enum class FileActionType {
NONE,
ACTION,
MENU
};
enum FileActionTarget {
FILE_ACTION_TARGET_NONE,
FILE_ACTION_TARGET_CONTEXT = 1,
FILE_ACTION_TARGET_LOCATION = 1 << 1,
FILE_ACTION_TARGET_TOOLBAR = 1 << 2
};
class FileActionObject {
public:
explicit FileActionObject();
explicit FileActionObject(GKeyFile* kf);
virtual ~FileActionObject();
void setId(const char* _id) {
id = CStrPtr{g_strdup(_id)};
}
static bool is_plural_exec(const char* exec);
static std::string expand_str(const char* templ, const FileInfoList& files, bool for_display = false, std::shared_ptr<const FileInfo> first_file = nullptr);
FileActionType type;
CStrPtr id;
CStrPtr name;
CStrPtr tooltip;
CStrPtr icon;
CStrPtr desc;
bool enabled;
bool hidden;
CStrPtr suggested_shortcut;
std::unique_ptr<FileActionCondition> condition;
// values cached during menu generation
bool has_parent;
};
class FileAction: public FileActionObject {
public:
FileAction(GKeyFile* kf);
std::shared_ptr<FileActionProfile> match(const FileInfoList& files) const;
int target; // bitwise or of FileActionTarget
CStrPtr toolbar_label;
// FIXME: currently we don't support dynamic profiles
std::vector<std::shared_ptr<FileActionProfile>> profiles;
};
class FileActionMenu : public FileActionObject {
public:
FileActionMenu(GKeyFile* kf);
bool match(const FileInfoList &files) const;
// called during menu generation
void cache_children(const FileInfoList &files, const char** items_list);
CStrArrayPtr items_list;
// values cached during menu generation
std::vector<std::shared_ptr<FileActionObject>> cached_children;
};
class FileActionItem {
public:
static std::shared_ptr<FileActionItem> fromActionObject(std::shared_ptr<FileActionObject> action_obj, const FileInfoList &files);
FileActionItem(std::shared_ptr<FileAction> _action, std::shared_ptr<FileActionProfile> _profile, const FileInfoList& files);
FileActionItem(std::shared_ptr<FileActionMenu> menu, const FileInfoList& files);
FileActionItem(std::shared_ptr<FileActionObject> _action, const FileInfoList& files);
const std::string& get_name() const {
return name;
}
const std::string& get_desc() const {
return desc;
}
const std::string& get_icon() const {
return icon;
}
const char* get_id() const {
return action->id.get();
}
FileActionTarget get_target() const {
if(action->type == FileActionType::ACTION) {
return FileActionTarget(static_cast<FileAction*>(action.get())->target);
}
return FILE_ACTION_TARGET_CONTEXT;
}
bool is_menu() const {
return (action->type == FileActionType::MENU);
}
bool is_action() const {
return (action->type == FileActionType::ACTION);
}
bool launch(GAppLaunchContext *ctx, const FileInfoList &files, CStrPtr &output) const;
const std::vector<std::shared_ptr<const FileActionItem>>& get_sub_items() const {
return children;
}
static bool compare_items(std::shared_ptr<const FileActionItem> a, std::shared_ptr<const FileActionItem> b);
static std::vector<std::shared_ptr<const FileActionItem>> get_actions_for_files(const FileInfoList& files);
std::string name;
std::string desc;
std::string icon;
std::shared_ptr<FileActionObject> action;
std::shared_ptr<FileActionProfile> profile; // only used by action item
std::vector<std::shared_ptr<const FileActionItem>> children; // only used by menu
};
typedef std::vector<std::shared_ptr<const FileActionItem>> FileActionItemList;
} // namespace Fm
#endif // FILEACTION_H

@ -0,0 +1,503 @@
#include "fileactioncondition.h"
#include "fileaction.h"
#include <string>
using namespace std;
namespace Fm {
FileActionCondition::FileActionCondition(GKeyFile *kf, const char* group) {
only_show_in = CStrArrayPtr{g_key_file_get_string_list(kf, group, "OnlyShowIn", nullptr, nullptr)};
not_show_in = CStrArrayPtr{g_key_file_get_string_list(kf, group, "NotShowIn", nullptr, nullptr)};
try_exec = CStrPtr{g_key_file_get_string(kf, group, "TryExec", nullptr)};
show_if_registered = CStrPtr{g_key_file_get_string(kf, group, "ShowIfRegistered", nullptr)};
show_if_true = CStrPtr{g_key_file_get_string(kf, group, "ShowIfTrue", nullptr)};
show_if_running = CStrPtr{g_key_file_get_string(kf, group, "ShowIfRunning", nullptr)};
mime_types = CStrArrayPtr{g_key_file_get_string_list(kf, group, "MimeTypes", nullptr, nullptr)};
base_names = CStrArrayPtr{g_key_file_get_string_list(kf, group, "Basenames", nullptr, nullptr)};
match_case = g_key_file_get_boolean(kf, group, "Matchcase", nullptr);
CStrPtr selection_count_str{g_key_file_get_string(kf, group, "SelectionCount", nullptr)};
if(selection_count_str != nullptr) {
switch(selection_count_str[0]) {
case '<':
case '>':
case '=':
selection_count_cmp = selection_count_str[0];
selection_count = atoi(selection_count_str.get() + 1);
break;
default:
selection_count_cmp = '>';
selection_count = 0;
break;
}
}
else {
selection_count_cmp = '>';
selection_count = 0;
}
schemes = CStrArrayPtr{g_key_file_get_string_list(kf, group, "Schemes", nullptr, nullptr)};
folders = CStrArrayPtr{g_key_file_get_string_list(kf, group, "Folders", nullptr, nullptr)};
auto caps = CStrArrayPtr{g_key_file_get_string_list(kf, group, "Capabilities", nullptr, nullptr)};
// FIXME: implement Capabilities support
}
bool FileActionCondition::match_try_exec(const FileInfoList& files) {
if(try_exec != nullptr) {
// stdout.printf(" TryExec: %s\n", try_exec);
CStrPtr exec_path{g_find_program_in_path(FileActionObject::expand_str(try_exec.get(), files).c_str())};
if(!g_file_test(exec_path.get(), G_FILE_TEST_IS_EXECUTABLE)) {
return false;
}
}
return true;
}
bool FileActionCondition::match_show_if_registered(const FileInfoList& files) {
if(show_if_registered != nullptr) {
// stdout.printf(" ShowIfRegistered: %s\n", show_if_registered);
auto service = FileActionObject::expand_str(show_if_registered.get(), files);
// References:
// http://people.freedesktop.org/~david/eggdbus-20091014/eggdbus-interface-org.freedesktop.DBus.html#eggdbus-method-org.freedesktop.DBus.NameHasOwner
// glib source code: gio/tests/gdbus-names.c
auto con = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr);
auto result = g_dbus_connection_call_sync(con,
"org.freedesktop.DBus",
"/org/freedesktop/DBus",
"org.freedesktop.DBus",
"NameHasOwner",
g_variant_new("(s)", service.c_str()),
g_variant_type_new("(b)"),
G_DBUS_CALL_FLAGS_NONE,
-1, nullptr, nullptr);
bool name_has_owner;
g_variant_get(result, "(b)", &name_has_owner);
g_variant_unref(result);
// stdout.printf("check if service: %s is in use: %d\n", service, (int)name_has_owner);
if(!name_has_owner) {
return false;
}
}
return true;
}
bool FileActionCondition::match_show_if_true(const FileInfoList& files) {
if(show_if_true != nullptr) {
auto cmd = FileActionObject::expand_str(show_if_true.get(), files);
int exit_status;
// FIXME: Process.spawn cannot handle shell commands. Use Posix.system() instead.
//if(!Process.spawn_command_line_sync(cmd, nullptr, nullptr, out exit_status)
// || exit_status != 0)
// return false;
exit_status = system(cmd.c_str());
if(exit_status != 0) {
return false;
}
}
return true;
}
bool FileActionCondition::match_show_if_running(const FileInfoList& files) {
if(show_if_running != nullptr) {
auto process_name = FileActionObject::expand_str(show_if_running.get(), files);
CStrPtr pgrep{g_find_program_in_path("pgrep")};
bool running = false;
// pgrep is not fully portable, but we don't have better options here
if(pgrep != nullptr) {
int exit_status;
// cmd = "$pgrep -x '$process_name'"
string cmd = pgrep.get();
cmd += " -x \'";
cmd += process_name;
cmd += "\'";
if(g_spawn_command_line_sync(cmd.c_str(), nullptr, nullptr, &exit_status, nullptr)) {
if(exit_status == 0) {
running = true;
}
}
}
if(!running) {
return false;
}
}
return true;
}
bool FileActionCondition::match_mime_type(const FileInfoList& files, const char* type, bool negated) {
// stdout.printf("match_mime_type: %s, neg: %d\n", type, (int)negated);
if(strcmp(type, "all/all") == 0 || strcmp(type, "*") == 0) {
return negated ? false : true;
}
else if(strcmp(type, "all/allfiles") == 0) {
// see if all fileinfos are files
if(negated) { // all fileinfos should not be files
for(auto& fi: files) {
if(!fi->isDir()) { // at least 1 of the fileinfos is a file.
return false;
}
}
}
else { // all fileinfos should be files
for(auto& fi: files) {
if(fi->isDir()) { // at least 1 of the fileinfos is a file.
return false;
}
}
}
}
else if(g_str_has_suffix(type, "/*")) {
// check if all are subtypes of allowed_type
string prefix{type};
prefix.erase(prefix.length() - 1); // remove the last char
if(negated) { // all files should not have the prefix
for(auto& fi: files) {
if(g_str_has_prefix(fi->mimeType()->name(), prefix.c_str())) {
return false;
}
}
}
else { // all files should have the prefix
for(auto& fi: files) {
if(!g_str_has_prefix(fi->mimeType()->name(), prefix.c_str())) {
return false;
}
}
}
}
else {
if(negated) { // all files should not be of the type
for(auto& fi: files) {
if(strcmp(fi->mimeType()->name(),type) == 0) {
// if(ContentType.is_a(type, fi.get_mime_type().get_type())) {
return false;
}
}
}
else { // all files should be of the type
for(auto& fi: files) {
// stdout.printf("get_type: %s, type: %s\n", fi.get_mime_type().get_type(), type);
if(strcmp(fi->mimeType()->name(),type) != 0) {
// if(!ContentType.is_a(type, fi.get_mime_type().get_type())) {
return false;
}
}
}
}
return true;
}
bool FileActionCondition::match_mime_types(const FileInfoList& files) {
if(mime_types != nullptr) {
bool allowed = false;
// FIXME: this is inefficient, but easier to implement
// check if all of the mime_types are allowed
for(auto mime_type = mime_types.get(); *mime_type; ++mime_type) {
const char* allowed_type = *mime_type;
const char* type;
bool negated;
if(allowed_type[0] == '!') {
type = allowed_type + 1;
negated = true;
}
else {
type = allowed_type;
negated = false;
}
if(negated) { // negated mime_type rules are ANDed
bool type_is_allowed = match_mime_type(files, type, negated);
if(!type_is_allowed) { // so any mismatch is not allowed
return false;
}
}
else { // other mime_type rules are ORed
// matching any one of the mime_type is enough
if(!allowed) { // if no rule is matched yet
allowed = match_mime_type(files, type, false);
}
}
}
return allowed;
}
return true;
}
bool FileActionCondition::match_base_name(const FileInfoList& files, const char* base_name, bool negated) {
// see if all files has the base_name
// FIXME: this is inefficient, some optimization is needed later
GPatternSpec* pattern;
if(match_case) {
pattern = g_pattern_spec_new(base_name);
}
else {
CStrPtr case_fold{g_utf8_casefold(base_name, -1)};
pattern = g_pattern_spec_new(case_fold.get()); // FIXME: is this correct?
}
for(auto& fi: files) {
const char* name = fi->name().c_str();
if(match_case) {
if(g_pattern_match_string(pattern, name)) {
// at least 1 file has the base_name
if(negated) {
return false;
}
}
else {
// at least 1 file does not has the scheme
if(!negated) {
return false;
}
}
}
else {
CStrPtr case_fold{g_utf8_casefold(name, -1)};
if(g_pattern_match_string(pattern, case_fold.get())) {
// at least 1 file has the base_name
if(negated) {
return false;
}
}
else {
// at least 1 file does not has the scheme
if(!negated) {
return false;
}
}
}
}
return true;
}
bool FileActionCondition::match_base_names(const FileInfoList& files) {
if(base_names != nullptr) {
bool allowed = false;
// FIXME: this is inefficient, but easier to implement
// check if all of the base_names are allowed
for(auto it = base_names.get(); *it; ++it) {
auto allowed_name = *it;
const char* name;
bool negated;
if(allowed_name[0] == '!') {
name = allowed_name + 1;
negated = true;
}
else {
name = allowed_name;
negated = false;
}
if(negated) { // negated base_name rules are ANDed
bool name_is_allowed = match_base_name(files, name, negated);
if(!name_is_allowed) { // so any mismatch is not allowed
return false;
}
}
else { // other base_name rules are ORed
// matching any one of the base_name is enough
if(!allowed) { // if no rule is matched yet
allowed = match_base_name(files, name, false);
}
}
}
return allowed;
}
return true;
}
bool FileActionCondition::match_scheme(const FileInfoList& files, const char* scheme, bool negated) {
// FIXME: this is inefficient, some optimization is needed later
// see if all files has the scheme
for(auto& fi: files) {
if(fi->path().hasUriScheme(scheme)) {
// at least 1 file has the scheme
if(negated) {
return false;
}
}
else {
// at least 1 file does not has the scheme
if(!negated) {
return false;
}
}
}
return true;
}
bool FileActionCondition::match_schemes(const FileInfoList& files) {
if(schemes != nullptr) {
bool allowed = false;
// FIXME: this is inefficient, but easier to implement
// check if all of the schemes are allowed
for(auto it = schemes.get(); *it; ++it) {
auto allowed_scheme = *it;
const char* scheme;
bool negated;
if(allowed_scheme[0] == '!') {
scheme = allowed_scheme + 1;
negated = true;
}
else {
scheme = allowed_scheme;
negated = false;
}
if(negated) { // negated scheme rules are ANDed
bool scheme_is_allowed = match_scheme(files, scheme, negated);
if(!scheme_is_allowed) { // so any mismatch is not allowed
return false;
}
}
else { // other scheme rules are ORed
// matching any one of the scheme is enough
if(!allowed) { // if no rule is matched yet
allowed = match_scheme(files, scheme, false);
}
}
}
return allowed;
}
return true;
}
bool FileActionCondition::match_folder(const FileInfoList& files, const char* folder, bool negated) {
// trailing /* should always be implied.
// FIXME: this is inefficient, some optimization is needed later
GPatternSpec* pattern;
if(g_str_has_suffix(folder, "/*")) {
pattern = g_pattern_spec_new(folder);
}
else {
auto pat_str = 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
if(negated) {
return false;
}
}
else {
if(!negated) {
return false;
}
}
}
return true;
}
bool FileActionCondition::match_folders(const FileInfoList& files) {
if(folders != nullptr) {
bool allowed = false;
// FIXME: this is inefficient, but easier to implement
// check if all of the schemes are allowed
for(auto it = folders.get(); *it; ++it) {
auto allowed_folder = *it;
const char* folder;
bool negated;
if(allowed_folder[0] == '!') {
folder = allowed_folder + 1;
negated = true;
}
else {
folder = allowed_folder;
negated = false;
}
if(negated) { // negated folder rules are ANDed
bool folder_is_allowed = match_folder(files, folder, negated);
if(!folder_is_allowed) { // so any mismatch is not allowed
return false;
}
}
else { // other folder rules are ORed
// matching any one of the folder is enough
if(!allowed) { // if no rule is matched yet
allowed = match_folder(files, folder, false);
}
}
}
return allowed;
}
return true;
}
bool FileActionCondition::match_selection_count(const FileInfoList& files) {
const int n_files = files.size();
switch(selection_count_cmp) {
case '<':
if(n_files >= selection_count) {
return false;
}
break;
case '=':
if(n_files != selection_count) {
return false;
}
break;
case '>':
if(n_files <= selection_count) {
return false;
}
break;
}
return true;
}
bool FileActionCondition::match_capabilities(const FileInfoList& /*files*/) {
// TODO
return true;
}
bool FileActionCondition::match(const FileInfoList& files) {
// all of the condition are combined with AND
// So, if any one of the conditions is not matched, we quit.
// TODO: OnlyShowIn, NotShowIn
if(!match_try_exec(files)) {
return false;
}
if(!match_mime_types(files)) {
return false;
}
if(!match_base_names(files)) {
return false;
}
if(!match_selection_count(files)) {
return false;
}
if(!match_schemes(files)) {
return false;
}
if(!match_folders(files)) {
return false;
}
// TODO: Capabilities
// currently, due to limitations of Fm.FileInfo, this cannot
// be implemanted correctly.
if(!match_capabilities(files)) {
return false;
}
if(!match_show_if_registered(files)) {
return false;
}
if(!match_show_if_true(files)) {
return false;
}
if(!match_show_if_running(files)) {
return false;
}
return true;
}
} // namespace Fm

@ -0,0 +1,123 @@
#ifndef FILEACTIONCONDITION_H
#define FILEACTIONCONDITION_H
#include <glib.h>
#include "../core/gioptrs.h"
#include "../core/fileinfo.h"
namespace Fm {
// FIXME: we can use getgroups() to get groups of current process
// then, call stat() and stat.st_gid to handle capabilities
// in this way, we don't have to call euidaccess
enum class FileActionCapability {
OWNER = 0,
READABLE = 1 << 1,
WRITABLE = 1 << 2,
EXECUTABLE = 1 << 3,
LOCAL = 1 << 4
};
class FileActionCondition {
public:
explicit FileActionCondition(GKeyFile* kf, const char* group);
#if 0
bool match_base_name_(const FileInfoList& files, const char* allowed_base_name) {
// all files should match the base_name pattern.
bool allowed = true;
if(allowed_base_name.index_of_char('*') >= 0) {
string allowed_base_name_ci;
if(!match_case) {
allowed_base_name_ci = allowed_base_name.casefold(); // FIXME: is this ok?
allowed_base_name = allowed_base_name_ci;
}
var pattern = new PatternSpec(allowed_base_name);
foreach(unowned FileInfo fi in files) {
unowned string name = fi.get_name();
if(match_case) {
if(!pattern.match_string(name)) {
allowed = false;
break;
}
}
else {
if(!pattern.match_string(name.casefold())) {
allowed = false;
break;
}
}
}
}
else {
foreach(unowned FileInfo fi in files) {
unowned string name = fi.get_name();
if(match_case) {
if(allowed_base_name != name) {
allowed = false;
break;
}
}
else {
if(allowed_base_name.collate(name) != 0) {
allowed = false;
break;
}
}
}
}
return allowed;
}
#endif
bool match_try_exec(const FileInfoList& files);
bool match_show_if_registered(const FileInfoList& files);
bool match_show_if_true(const FileInfoList& files);
bool match_show_if_running(const FileInfoList& files);
bool match_mime_type(const FileInfoList& files, const char* type, bool negated);
bool match_mime_types(const FileInfoList& files);
bool match_base_name(const FileInfoList& files, const char* base_name, bool negated);
bool match_base_names(const FileInfoList& files);
static bool match_scheme(const FileInfoList& files, const char* scheme, bool negated);
bool match_schemes(const FileInfoList& files);
static bool match_folder(const FileInfoList& files, const char* folder, bool negated);
bool match_folders(const FileInfoList& files);
bool match_selection_count(const FileInfoList &files);
bool match_capabilities(const FileInfoList& files);
bool match(const FileInfoList& files);
CStrArrayPtr only_show_in;
CStrArrayPtr not_show_in;
CStrPtr try_exec;
CStrPtr show_if_registered;
CStrPtr show_if_true;
CStrPtr show_if_running;
CStrArrayPtr mime_types;
CStrArrayPtr base_names;
bool match_case;
char selection_count_cmp;
int selection_count;
CStrArrayPtr schemes;
CStrArrayPtr folders;
FileActionCapability capabilities;
};
}
#endif // FILEACTIONCONDITION_H

@ -0,0 +1,121 @@
#include "fileactionprofile.h"
#include "fileaction.h"
#include <QDebug>
using namespace std;
namespace Fm {
FileActionProfile::FileActionProfile(GKeyFile *kf, const char* profile_name) {
id = profile_name;
std::string group_name = "X-Action-Profile " + id;
name = CStrPtr{g_key_file_get_string(kf, group_name.c_str(), "Name", nullptr)};
exec = CStrPtr{g_key_file_get_string(kf, group_name.c_str(), "Exec", nullptr)};
// stdout.printf("id: %s, Exec: %s\n", id, exec);
path = CStrPtr{g_key_file_get_string(kf, group_name.c_str(), "Path", nullptr)};
auto s = CStrPtr{g_key_file_get_string(kf, group_name.c_str(), "ExecutionMode", nullptr)};
if(s) {
if(strcmp(s.get(), "Normal") == 0) {
exec_mode = FileActionExecMode::NORMAL;
}
else if(strcmp(s.get(), "Terminal") == 0) {
exec_mode = FileActionExecMode::TERMINAL;
}
else if(strcmp(s.get(), "Embedded") == 0) {
exec_mode = FileActionExecMode::EMBEDDED;
}
else if(strcmp(s.get(), "DisplayOutput") == 0) {
exec_mode = FileActionExecMode::DISPLAY_OUTPUT;
}
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)};
exec_as = CStrPtr{g_key_file_get_string(kf, group_name.c_str(), "ExecuteAs", nullptr)};
condition = make_shared<FileActionCondition>(kf, group_name.c_str());
}
bool FileActionProfile::launch_once(GAppLaunchContext* /*ctx*/, std::shared_ptr<const FileInfo> first_file, const FileInfoList& files, CStrPtr& output) {
if(exec == nullptr) {
return false;
}
auto exec_cmd = FileActionObject::expand_str(exec.get(), files, false, first_file);
bool ret = false;
if(exec_mode == FileActionExecMode::DISPLAY_OUTPUT) {
int exit_status;
char* output_buf = nullptr;
ret = g_spawn_command_line_sync(exec_cmd.c_str(), &output_buf, nullptr, &exit_status, nullptr);
if(ret) {
ret = (exit_status == 0);
}
output = CStrPtr{output_buf};
}
else {
/*
AppInfoCreateFlags flags = AppInfoCreateFlags.NONE;
if(startup_notify)
flags |= AppInfoCreateFlags.SUPPORTS_STARTUP_NOTIFICATION;
if(exec_mode == FileActionExecMode::TERMINAL ||
exec_mode == FileActionExecMode::EMBEDDED)
flags |= AppInfoCreateFlags.NEEDS_TERMINAL;
GLib.AppInfo app = Fm.AppInfo.create_from_commandline(exec, nullptr, flags);
stdout.printf("Execute command line: %s\n\n", exec);
ret = app.launch(nullptr, ctx);
*/
// NOTE: we cannot use GAppInfo here since GAppInfo does
// command line parsing which involving %u, %f, and other
// code defined in desktop entry spec.
// This may conflict with DES EMA parameters.
// FIXME: so how to handle this cleaner?
// Maybe we should leave all %% alone and don't translate
// them to %. Then GAppInfo will translate them to %, not
// codes specified in DES.
ret = g_spawn_command_line_async(exec_cmd.c_str(), nullptr);
}
return ret;
}
bool FileActionProfile::launch(GAppLaunchContext* ctx, const FileInfoList& files, CStrPtr& output) {
bool plural_form = FileActionObject::is_plural_exec(exec.get());
bool ret;
if(plural_form) { // plural form command, handle all files at a time
ret = launch_once(ctx, files.front(), files, output);
}
else { // singular form command, run once for each file
GString* all_output = g_string_sized_new(1024);
bool show_output = false;
for(auto& fi: files) {
CStrPtr one_output;
launch_once(ctx, fi, files, one_output);
if(one_output) {
show_output = true;
// FIXME: how to handle multiple output std::strings properly?
g_string_append(all_output, one_output.get());
g_string_append(all_output, "\n");
}
}
if(show_output) {
output = CStrPtr{g_string_free(all_output, false)};
}
else {
g_string_free(all_output, true);
}
ret = true;
}
return ret;
}
bool FileActionProfile::match(FileInfoList files) {
// stdout.printf(" match profile: %s\n", id);
return condition->match(files);
}
}

@ -0,0 +1,45 @@
#ifndef FILEACTIONPROFILE_H
#define FILEACTIONPROFILE_H
#include <string>
#include <glib.h>
#include <gio/gio.h>
#include "../core/fileinfo.h"
#include "fileactioncondition.h"
namespace Fm {
enum class FileActionExecMode {
NORMAL,
TERMINAL,
EMBEDDED,
DISPLAY_OUTPUT
};
class FileActionProfile {
public:
explicit FileActionProfile(GKeyFile* kf, const char* profile_name);
bool launch_once(GAppLaunchContext* ctx, std::shared_ptr<const FileInfo> first_file, const FileInfoList& files, CStrPtr& output);
bool launch(GAppLaunchContext* ctx, const FileInfoList& files, CStrPtr& output);
bool match(FileInfoList files);
std::string id;
CStrPtr name;
CStrPtr exec;
CStrPtr path;
FileActionExecMode exec_mode;
bool startup_notify;
CStrPtr startup_wm_class;
CStrPtr exec_as;
std::shared_ptr<FileActionCondition> condition;
};
} // namespace Fm
#endif // FILEACTIONPROFILE_H

@ -1,128 +0,0 @@
/*
* Copyright (C) 2016 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
*
* 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_DEEP_COUNT_JOB_H__
#define __LIBFM_QT_FM_DEEP_COUNT_JOB_H__
#include <libfm/fm.h>
#include <QObject>
#include <QtGlobal>
#include "libfmqtglobals.h"
#include "job.h"
namespace Fm {
class LIBFM_QT_API DeepCountJob: public Job {
public:
DeepCountJob(FmPathList* paths, FmDeepCountJobFlags flags) {
dataPtr_ = reinterpret_cast<GObject*>(fm_deep_count_job_new(paths, flags));
}
// default constructor
DeepCountJob() {
dataPtr_ = nullptr;
}
DeepCountJob(FmDeepCountJob* dataPtr){
dataPtr_ = dataPtr != nullptr ? reinterpret_cast<GObject*>(g_object_ref(dataPtr)) : nullptr;
}
// copy constructor
DeepCountJob(const DeepCountJob& other) {
dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<GObject*>(g_object_ref(other.dataPtr_)) : nullptr;
}
// move constructor
DeepCountJob(DeepCountJob&& other) {
dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
}
// create a wrapper for the data pointer without increasing the reference count
static DeepCountJob wrapPtr(FmDeepCountJob* dataPtr) {
DeepCountJob obj;
obj.dataPtr_ = reinterpret_cast<GObject*>(dataPtr);
return obj;
}
// disown the managed data pointer
FmDeepCountJob* takeDataPtr() {
FmDeepCountJob* data = reinterpret_cast<FmDeepCountJob*>(dataPtr_);
dataPtr_ = nullptr;
return data;
}
// get the raw pointer wrapped
FmDeepCountJob* dataPtr() {
return reinterpret_cast<FmDeepCountJob*>(dataPtr_);
}
// automatic type casting
operator FmDeepCountJob*() {
return dataPtr();
}
// automatic type casting
operator void*() {
return dataPtr();
}
// copy assignment
DeepCountJob& operator=(const DeepCountJob& other) {
if(dataPtr_ != nullptr) {
g_object_unref(dataPtr_);
}
dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<GObject*>(g_object_ref(other.dataPtr_)) : nullptr;
return *this;
}
// move assignment
DeepCountJob& operator=(DeepCountJob&& other) {
dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
return *this;
}
bool isNull() {
return (dataPtr_ == nullptr);
}
// methods
void setDest(dev_t dev, const char* fs_id) {
fm_deep_count_job_set_dest(dataPtr(), dev, fs_id);
}
};
}
#endif // __LIBFM_QT_FM_DEEP_COUNT_JOB_H__

@ -1,148 +0,0 @@
/*
* Copyright (C) 2016 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
*
* 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_DIR_LIST_JOB_H__
#define __LIBFM_QT_FM_DIR_LIST_JOB_H__
#include <libfm/fm.h>
#include <QObject>
#include <QtGlobal>
#include "libfmqtglobals.h"
#include "job.h"
namespace Fm {
class LIBFM_QT_API DirListJob: public Job {
public:
DirListJob(FmPath* path, gboolean dir_only) {
dataPtr_ = reinterpret_cast<GObject*>(fm_dir_list_job_new(path, dir_only));
}
// default constructor
DirListJob() {
dataPtr_ = nullptr;
}
DirListJob(FmDirListJob* dataPtr){
dataPtr_ = dataPtr != nullptr ? reinterpret_cast<GObject*>(g_object_ref(dataPtr)) : nullptr;
}
// copy constructor
DirListJob(const DirListJob& other) {
dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<GObject*>(g_object_ref(other.dataPtr_)) : nullptr;
}
// move constructor
DirListJob(DirListJob&& other) {
dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
}
// create a wrapper for the data pointer without increasing the reference count
static DirListJob wrapPtr(FmDirListJob* dataPtr) {
DirListJob obj;
obj.dataPtr_ = reinterpret_cast<GObject*>(dataPtr);
return obj;
}
// disown the managed data pointer
FmDirListJob* takeDataPtr() {
FmDirListJob* data = reinterpret_cast<FmDirListJob*>(dataPtr_);
dataPtr_ = nullptr;
return data;
}
// get the raw pointer wrapped
FmDirListJob* dataPtr() {
return reinterpret_cast<FmDirListJob*>(dataPtr_);
}
// automatic type casting
operator FmDirListJob*() {
return dataPtr();
}
// automatic type casting
operator void*() {
return dataPtr();
}
// copy assignment
DirListJob& operator=(const DirListJob& other) {
if(dataPtr_ != nullptr) {
g_object_unref(dataPtr_);
}
dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<GObject*>(g_object_ref(other.dataPtr_)) : nullptr;
return *this;
}
// move assignment
DirListJob& operator=(DirListJob&& other) {
dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
return *this;
}
bool isNull() {
return (dataPtr_ == nullptr);
}
// methods
void addFoundFile(FmFileInfo* file) {
fm_dir_list_job_add_found_file(dataPtr(), file);
}
void setIncremental(gboolean set) {
fm_dir_list_job_set_incremental(dataPtr(), set);
}
FmFileInfoList* getFiles(void) {
return fm_dir_list_job_get_files(dataPtr());
}
static DirListJob newForGfile(GFile* gf) {
return DirListJob::wrapPtr(fm_dir_list_job_new_for_gfile(gf));
}
static DirListJob new2(FmPath* path, FmDirListJobFlags flags) {
return DirListJob::wrapPtr(fm_dir_list_job_new2(path, flags));
}
};
}
#endif // __LIBFM_QT_FM_DIR_LIST_JOB_H__

@ -20,185 +20,213 @@
#include "dirtreemodel.h" #include "dirtreemodel.h"
#include "dirtreemodelitem.h" #include "dirtreemodelitem.h"
#include <QDebug> #include <QDebug>
#include "core/fileinfojob.h"
namespace Fm { namespace Fm {
DirTreeModel::DirTreeModel(QObject* parent): DirTreeModel::DirTreeModel(QObject* parent):
showHidden_(false) { QAbstractItemModel(parent),
showHidden_(false) {
} }
DirTreeModel::~DirTreeModel() { DirTreeModel::~DirTreeModel() {
} }
void DirTreeModel::addRoots(Fm::FilePathList rootPaths) {
auto job = new Fm::FileInfoJob{std::move(rootPaths)};
job->setAutoDelete(true);
connect(job, &Fm::FileInfoJob::finished, this, &DirTreeModel::onFileInfoJobFinished, Qt::BlockingQueuedConnection);
job->runAsync();
}
void DirTreeModel::onFileInfoJobFinished() {
auto job = static_cast<Fm::FileInfoJob*>(sender());
for(auto file: job->files()) {
addRoot(std::move(file));
}
}
// QAbstractItemModel implementation // QAbstractItemModel implementation
Qt::ItemFlags DirTreeModel::flags(const QModelIndex& index) const { Qt::ItemFlags DirTreeModel::flags(const QModelIndex& index) const {
DirTreeModelItem* item = itemFromIndex(index); DirTreeModelItem* item = itemFromIndex(index);
if(item && item->isPlaceHolder()) if(item && item->isPlaceHolder()) {
return Qt::ItemIsEnabled; return Qt::ItemIsEnabled;
return QAbstractItemModel::flags(index); }
return QAbstractItemModel::flags(index);
} }
QVariant DirTreeModel::data(const QModelIndex& index, int role) const { QVariant DirTreeModel::data(const QModelIndex& index, int role) const {
if(!index.isValid() || index.column() > 1) { if(!index.isValid() || index.column() > 1) {
return QVariant(); return QVariant();
} }
DirTreeModelItem* item = itemFromIndex(index); DirTreeModelItem* item = itemFromIndex(index);
if(item) { if(item) {
FmFileInfo* info = item->fileInfo_; auto info = item->fileInfo_;
switch(role) { switch(role) {
case Qt::ToolTipRole: case Qt::ToolTipRole:
return QVariant(item->displayName_); return QVariant(item->displayName_);
case Qt::DisplayRole: case Qt::DisplayRole:
return QVariant(item->displayName_); return QVariant(item->displayName_);
case Qt::DecorationRole: case Qt::DecorationRole:
return QVariant(item->icon_); return QVariant(item->icon_);
case FileInfoRole: case FileInfoRole: {
return qVariantFromValue((void*)info); QVariant v;
v.setValue(info);
return v;
}
}
} }
} return QVariant();
return QVariant();
} }
int DirTreeModel::columnCount(const QModelIndex& parent) const { int DirTreeModel::columnCount(const QModelIndex& /*parent*/) const {
return 1; return 1;
} }
int DirTreeModel::rowCount(const QModelIndex& parent) const { int DirTreeModel::rowCount(const QModelIndex& parent) const {
if(!parent.isValid()) if(!parent.isValid()) {
return rootItems_.count(); return rootItems_.size();
DirTreeModelItem* item = itemFromIndex(parent); }
if(item) DirTreeModelItem* item = itemFromIndex(parent);
return item->children_.count(); if(item) {
return 0; return item->children_.size();
}
return 0;
} }
QModelIndex DirTreeModel::parent(const QModelIndex& child) const { QModelIndex DirTreeModel::parent(const QModelIndex& child) const {
DirTreeModelItem* item = itemFromIndex(child); DirTreeModelItem* item = itemFromIndex(child);
if(item && item->parent_) { if(item && item->parent_) {
item = item->parent_; // go to parent item item = item->parent_; // go to parent item
if(item) { if(item) {
const QList<DirTreeModelItem*>& items = item->parent_ ? item->parent_->children_ : rootItems_; const auto& items = item->parent_ ? item->parent_->children_ : rootItems_;
int row = items.indexOf(item); // this is Q(n) and may be slow :-( auto it = std::find(items.cbegin(), items.cend(), item);
if(row >= 0) if(it != items.cend()) {
return createIndex(row, 0, (void*)item); int row = it - items.cbegin();
return createIndex(row, 0, (void*)item);
}
}
} }
} return QModelIndex();
return QModelIndex();
} }
QModelIndex DirTreeModel::index(int row, int column, const QModelIndex& parent) const { QModelIndex DirTreeModel::index(int row, int column, const QModelIndex& parent) const {
if(row >= 0 && column >= 0 && column == 0) { if(row >= 0 && column >= 0 && column == 0) {
if(!parent.isValid()) { // root items if(!parent.isValid()) { // root items
if(row < rootItems_.count()) { if(static_cast<size_t>(row) < rootItems_.size()) {
const DirTreeModelItem* item = rootItems_.at(row); const DirTreeModelItem* item = rootItems_.at(row);
return createIndex(row, column, (void*)item); return createIndex(row, column, (void*)item);
} }
}
else { // child items
DirTreeModelItem* parentItem = itemFromIndex(parent);
if(static_cast<size_t>(row) < parentItem->children_.size()) {
const DirTreeModelItem* item = parentItem->children_.at(row);
return createIndex(row, column, (void*)item);
}
}
} }
else { // child items return QModelIndex(); // invalid index
DirTreeModelItem* parentItem = itemFromIndex(parent);
if(row < parentItem->children_.count()) {
const DirTreeModelItem* item = parentItem->children_.at(row);
return createIndex(row, column, (void*)item);
}
}
}
return QModelIndex(); // invalid index
} }
bool DirTreeModel::hasChildren(const QModelIndex& parent) const { bool DirTreeModel::hasChildren(const QModelIndex& parent) const {
DirTreeModelItem* item = itemFromIndex(parent); DirTreeModelItem* item = itemFromIndex(parent);
return item ? !item->isPlaceHolder() : true; return item ? !item->isPlaceHolder() : true;
} }
QModelIndex DirTreeModel::indexFromItem(DirTreeModelItem* item) const { QModelIndex DirTreeModel::indexFromItem(DirTreeModelItem* item) const {
Q_ASSERT(item); Q_ASSERT(item);
const QList<DirTreeModelItem*>& items = item->parent_ ? item->parent_->children_ : rootItems_; const auto& items = item->parent_ ? item->parent_->children_ : rootItems_;
int row = items.indexOf(item); auto it = std::find(items.cbegin(), items.cend(), item);
if(row >= 0) if(it != items.cend()) {
return createIndex(row, 0, (void*)item); int row = it - items.cbegin();
return QModelIndex(); return createIndex(row, 0, (void*)item);
}
return QModelIndex();
} }
// public APIs // public APIs
QModelIndex DirTreeModel::addRoot(FmFileInfo* root) { QModelIndex DirTreeModel::addRoot(std::shared_ptr<const Fm::FileInfo> root) {
DirTreeModelItem* item = new DirTreeModelItem(root, this); DirTreeModelItem* item = new DirTreeModelItem(std::move(root), this);
int row = rootItems_.count(); int row = rootItems_.size();
beginInsertRows(QModelIndex(), row, row); beginInsertRows(QModelIndex(), row, row);
item->fileInfo_ = fm_file_info_ref(root); rootItems_.push_back(item);
rootItems_.append(item); // add_place_holder_child_item(model, item_l, nullptr, FALSE);
// add_place_holder_child_item(model, item_l, NULL, FALSE); endInsertRows();
endInsertRows(); return createIndex(row, 0, (void*)item);
return QModelIndex();
} }
DirTreeModelItem* DirTreeModel::itemFromIndex(const QModelIndex& index) const { DirTreeModelItem* DirTreeModel::itemFromIndex(const QModelIndex& index) const {
return reinterpret_cast<DirTreeModelItem*>(index.internalPointer()); return reinterpret_cast<DirTreeModelItem*>(index.internalPointer());
} }
QModelIndex DirTreeModel::indexFromPath(FmPath* path) const { QModelIndex DirTreeModel::indexFromPath(const Fm::FilePath &path) const {
DirTreeModelItem* item = itemFromPath(path); DirTreeModelItem* item = itemFromPath(path);
return item ? item->index() : QModelIndex(); return item ? item->index() : QModelIndex();
} }
DirTreeModelItem* DirTreeModel::itemFromPath(FmPath* path) const { DirTreeModelItem* DirTreeModel::itemFromPath(const Fm::FilePath &path) const {
Q_FOREACH(DirTreeModelItem* item, rootItems_) { Q_FOREACH(DirTreeModelItem* item, rootItems_) {
if(item->fileInfo_ && fm_path_equal(path, fm_file_info_get_path(item->fileInfo_))) { if(item->fileInfo_ && path == item->fileInfo_->path()) {
return item; return item;
} }
else { else {
DirTreeModelItem* child = item->childFromPath(path, true); DirTreeModelItem* child = item->childFromPath(path, true);
if(child) if(child) {
return child; return child;
}
}
} }
} return nullptr;
return NULL;
} }
void DirTreeModel::loadRow(const QModelIndex& index) { void DirTreeModel::loadRow(const QModelIndex& index) {
DirTreeModelItem* item = itemFromIndex(index); DirTreeModelItem* item = itemFromIndex(index);
Q_ASSERT(item); Q_ASSERT(item);
if(item && !item->isPlaceHolder()) if(item && !item->isPlaceHolder()) {
item->loadFolder(); item->loadFolder();
}
} }
void DirTreeModel::unloadRow(const QModelIndex& index) { void DirTreeModel::unloadRow(const QModelIndex& index) {
DirTreeModelItem* item = itemFromIndex(index); DirTreeModelItem* item = itemFromIndex(index);
if(item && !item->isPlaceHolder()) if(item && !item->isPlaceHolder()) {
item->unloadFolder(); item->unloadFolder();
}
} }
bool DirTreeModel::isLoaded(const QModelIndex& index) { bool DirTreeModel::isLoaded(const QModelIndex& index) {
DirTreeModelItem* item = itemFromIndex(index); DirTreeModelItem* item = itemFromIndex(index);
return item ? item->loaded_ : false; return item ? item->loaded_ : false;
} }
QIcon DirTreeModel::icon(const QModelIndex& index) { QIcon DirTreeModel::icon(const QModelIndex& index) {
DirTreeModelItem* item = itemFromIndex(index); DirTreeModelItem* item = itemFromIndex(index);
return item ? item->icon_ : QIcon(); return item ? item->icon_ : QIcon();
} }
FmFileInfo* DirTreeModel::fileInfo(const QModelIndex& index) { std::shared_ptr<const Fm::FileInfo> DirTreeModel::fileInfo(const QModelIndex& index) {
DirTreeModelItem* item = itemFromIndex(index); DirTreeModelItem* item = itemFromIndex(index);
return item ? item->fileInfo_ : NULL; return item ? item->fileInfo_ : nullptr;
} }
FmPath* DirTreeModel::filePath(const QModelIndex& index) { Fm::FilePath DirTreeModel::filePath(const QModelIndex& index) {
DirTreeModelItem* item = itemFromIndex(index); DirTreeModelItem* item = itemFromIndex(index);
return item && item->fileInfo_ ? fm_file_info_get_path(item->fileInfo_) : NULL; return (item && item->fileInfo_) ? item->fileInfo_->path() : Fm::FilePath{};
} }
QString DirTreeModel::dispName(const QModelIndex& index) { QString DirTreeModel::dispName(const QModelIndex& index) {
DirTreeModelItem* item = itemFromIndex(index); DirTreeModelItem* item = itemFromIndex(index);
return item ? item->displayName_ : QString(); return item ? item->displayName_ : QString();
} }
void DirTreeModel::setShowHidden(bool show_hidden) { void DirTreeModel::setShowHidden(bool show_hidden) {
showHidden_ = show_hidden; showHidden_ = show_hidden;
Q_FOREACH(DirTreeModelItem* item, rootItems_) { Q_FOREACH(DirTreeModelItem* item, rootItems_) {
item->setShowHidden(show_hidden); item->setShowHidden(show_hidden);
} }
} }

@ -27,6 +27,10 @@
#include <QList> #include <QList>
#include <QSharedPointer> #include <QSharedPointer>
#include <libfm/fm.h> #include <libfm/fm.h>
#include <vector>
#include "core/fileinfo.h"
#include "core/filepath.h"
namespace Fm { namespace Fm {
@ -34,56 +38,63 @@ class DirTreeModelItem;
class DirTreeView; class DirTreeView;
class LIBFM_QT_API DirTreeModel : public QAbstractItemModel { class LIBFM_QT_API DirTreeModel : public QAbstractItemModel {
Q_OBJECT Q_OBJECT
public: public:
friend class DirTreeModelItem; // allow direct access of private members in DirTreeModelItem friend class DirTreeModelItem; // allow direct access of private members in DirTreeModelItem
friend class DirTreeView; // allow direct access of private members in DirTreeView friend class DirTreeView; // allow direct access of private members in DirTreeView
enum Role { enum Role {
FileInfoRole = Qt::UserRole FileInfoRole = Qt::UserRole
}; };
explicit DirTreeModel(QObject* parent); explicit DirTreeModel(QObject* parent);
~DirTreeModel(); ~DirTreeModel();
QModelIndex addRoot(FmFileInfo* root); void addRoots(Fm::FilePathList rootPaths);
void loadRow(const QModelIndex& index);
void unloadRow(const QModelIndex& index);
bool isLoaded(const QModelIndex& index); void loadRow(const QModelIndex& index);
QIcon icon(const QModelIndex& index); void unloadRow(const QModelIndex& index);
FmFileInfo* fileInfo(const QModelIndex& index);
FmPath* filePath(const QModelIndex& index);
QString dispName(const QModelIndex& index);
void setShowHidden(bool show_hidden); bool isLoaded(const QModelIndex& index);
bool showHidden() const { QIcon icon(const QModelIndex& index);
return showHidden_; std::shared_ptr<const Fm::FileInfo> fileInfo(const QModelIndex& index);
} Fm::FilePath filePath(const QModelIndex& index);
QString dispName(const QModelIndex& index);
QModelIndex indexFromPath(FmPath* path) const; void setShowHidden(bool show_hidden);
bool showHidden() const {
return showHidden_;
}
virtual Qt::ItemFlags flags(const QModelIndex& index) const; QModelIndex indexFromPath(const Fm::FilePath& path) const;
virtual QVariant data(const QModelIndex& index, int role) const;
virtual int columnCount(const QModelIndex& parent) const;
virtual int rowCount(const QModelIndex& parent) const;
virtual QModelIndex parent(const QModelIndex& child) const;
virtual QModelIndex index(int row, int column, const QModelIndex& parent) const;
virtual bool hasChildren(const QModelIndex& parent = QModelIndex()) const;
private: virtual Qt::ItemFlags flags(const QModelIndex& index) const;
DirTreeModelItem* itemFromPath(FmPath* path) const; virtual QVariant data(const QModelIndex& index, int role) const;
DirTreeModelItem* itemFromIndex(const QModelIndex& index) const; virtual int columnCount(const QModelIndex& parent) const;
QModelIndex indexFromItem(DirTreeModelItem* item) const; virtual int rowCount(const QModelIndex& parent) const;
virtual QModelIndex parent(const QModelIndex& child) const;
virtual QModelIndex index(int row, int column, const QModelIndex& parent) const;
virtual bool hasChildren(const QModelIndex& parent = QModelIndex()) const;
Q_SIGNALS: Q_SIGNALS:
void rowLoaded(const QModelIndex& index); void rowLoaded(const QModelIndex& index);
private Q_SLOTS:
void onFileInfoJobFinished();
private: private:
bool showHidden_; QModelIndex addRoot(std::shared_ptr<const Fm::FileInfo> root);
QList<DirTreeModelItem*> rootItems_;
DirTreeModelItem* itemFromPath(const Fm::FilePath& path) const;
DirTreeModelItem* itemFromIndex(const QModelIndex& index) const;
QModelIndex indexFromItem(DirTreeModelItem* item) const;
private:
bool showHidden_;
std::vector<DirTreeModelItem*> rootItems_;
}; };
} }
#endif // FM_DIRTREEMODEL_H #endif // FM_DIRTREEMODEL_H

@ -25,313 +25,393 @@
namespace Fm { namespace Fm {
DirTreeModelItem::DirTreeModelItem(): DirTreeModelItem::DirTreeModelItem():
fileInfo_(nullptr), fileInfo_(nullptr),
folder_(nullptr), folder_(nullptr),
expanded_(false), expanded_(false),
loaded_(false), loaded_(false),
parent_(nullptr), parent_(nullptr),
placeHolderChild_(nullptr), placeHolderChild_(nullptr),
model_(nullptr) { model_(nullptr),
queuedForDeletion_(false) {
} }
DirTreeModelItem::DirTreeModelItem(FmFileInfo* info, DirTreeModel* model, DirTreeModelItem* parent): DirTreeModelItem::DirTreeModelItem(std::shared_ptr<const Fm::FileInfo> info, DirTreeModel* model, DirTreeModelItem* parent):
fileInfo_(fm_file_info_ref(info)), fileInfo_{std::move(info)},
folder_(nullptr), expanded_(false),
displayName_(QString::fromUtf8(fm_file_info_get_disp_name(info))), loaded_(false),
icon_(IconTheme::icon(fm_file_info_get_icon(info))), parent_(parent),
expanded_(false), placeHolderChild_(nullptr),
loaded_(false), model_(model),
parent_(parent), queuedForDeletion_(false) {
placeHolderChild_(nullptr),
model_(model) { if(fileInfo_) {
displayName_ = fileInfo_->displayName();
if(info) icon_ = fileInfo_->icon()->qicon();
addPlaceHolderChild(); addPlaceHolderChild();
}
} }
DirTreeModelItem::~DirTreeModelItem() { DirTreeModelItem::~DirTreeModelItem() {
if(fileInfo_)
fm_file_info_unref(fileInfo_);
if(folder_)
freeFolder(); freeFolder();
// delete child items if needed
// delete child items if needed if(!children_.empty()) {
if(!children_.isEmpty()) { Q_FOREACH(DirTreeModelItem* item, children_) {
Q_FOREACH(DirTreeModelItem* item, children_) { delete item;
delete item; }
} }
} if(!hiddenChildren_.empty()) {
if(!hiddenChildren_.isEmpty()) { Q_FOREACH(DirTreeModelItem* item, hiddenChildren_) {
Q_FOREACH(DirTreeModelItem* item, hiddenChildren_) { delete item;
delete item; }
} }
} /*if(queuedForDeletion_)
qDebug() << "queued deletion done";*/
} }
void DirTreeModelItem::addPlaceHolderChild() { void DirTreeModelItem::freeFolder() {
placeHolderChild_ = new DirTreeModelItem(); if(folder_) {
placeHolderChild_->parent_ = this; QObject::disconnect(onFolderFinishLoadingConn_);
placeHolderChild_->model_ = model_; QObject::disconnect(onFolderFilesAddedConn_);
placeHolderChild_->displayName_ = DirTreeModel::tr("Loading..."); QObject::disconnect(onFolderFilesRemovedConn_);
children_.append(placeHolderChild_); QObject::disconnect(onFolderFilesChangedConn_);
folder_.reset();
}
} }
void DirTreeModelItem::freeFolder() { void DirTreeModelItem::addPlaceHolderChild() {
if(folder_) { placeHolderChild_ = new DirTreeModelItem();
g_signal_handlers_disconnect_by_func(folder_, gpointer(onFolderFinishLoading), this); placeHolderChild_->parent_ = this;
g_signal_handlers_disconnect_by_func(folder_, gpointer(onFolderFilesAdded), this); placeHolderChild_->model_ = model_;
g_signal_handlers_disconnect_by_func(folder_, gpointer(onFolderFilesRemoved), this); placeHolderChild_->displayName_ = DirTreeModel::tr("Loading...");
g_signal_handlers_disconnect_by_func(folder_, gpointer(onFolderFilesChanged), this); children_.push_back(placeHolderChild_);
g_object_unref(folder_);
folder_ = nullptr;
}
} }
void DirTreeModelItem::loadFolder() { void DirTreeModelItem::loadFolder() {
if(!expanded_) { if(!expanded_) {
/* dynamically load content of the folder. */ /* dynamically load content of the folder. */
folder_ = fm_folder_from_path(fm_file_info_get_path(fileInfo_)); folder_ = Fm::Folder::fromPath(fileInfo_->path());
/* g_debug("fm_dir_tree_model_load_row()"); */ /* g_debug("fm_dir_tree_model_load_row()"); */
/* associate the data with loaded handler */ /* associate the data with loaded handler */
g_signal_connect(folder_, "finish-loading", G_CALLBACK(onFolderFinishLoading), this);
g_signal_connect(folder_, "files-added", G_CALLBACK(onFolderFilesAdded), this); onFolderFinishLoadingConn_ = QObject::connect(folder_.get(), &Fm::Folder::finishLoading, model_, [=]() {
g_signal_connect(folder_, "files-removed", G_CALLBACK(onFolderFilesRemoved), this); onFolderFinishLoading();
g_signal_connect(folder_, "files-changed", G_CALLBACK(onFolderFilesChanged), this); });
onFolderFilesAddedConn_ = QObject::connect(folder_.get(), &Fm::Folder::filesAdded, model_, [=](Fm::FileInfoList files) {
/* set 'expanded' flag beforehand as callback may check it */ onFolderFilesAdded(files);
expanded_ = true; });
/* if the folder is already loaded, call "loaded" handler ourselves */ onFolderFilesRemovedConn_ = QObject::connect(folder_.get(), &Fm::Folder::filesRemoved, model_, [=](Fm::FileInfoList files) {
if(fm_folder_is_loaded(folder_)) { // already loaded onFolderFilesRemoved(files);
GList* file_l; });
FmFileInfoList* files = fm_folder_get_files(folder_); onFolderFilesChangedConn_ = QObject::connect(folder_.get(), &Fm::Folder::filesChanged, model_, [=](std::vector<Fm::FileInfoPair>& changes) {
for(file_l = fm_file_info_list_peek_head_link(files); file_l; file_l = file_l->next) { onFolderFilesChanged(changes);
FmFileInfo* fi = FM_FILE_INFO(file_l->data); });
if(fm_file_info_is_dir(fi)) {
insertFileInfo(fi); /* set 'expanded' flag beforehand as callback may check it */
expanded_ = true;
/* if the folder is already loaded, call "loaded" handler ourselves */
if(folder_->isLoaded()) { // already loaded
insertFiles(folder_->files());
onFolderFinishLoading();
} }
}
onFolderFinishLoading(folder_, this);
} }
}
} }
void DirTreeModelItem::unloadFolder() { void DirTreeModelItem::unloadFolder() {
if(expanded_) { /* do some cleanup */ if(expanded_) { /* do some cleanup */
/* remove all children, and replace them with a dummy child /* remove all children, and replace them with a dummy child
* item to keep expander in the tree view around. */ * item to keep expander in the tree view around. */
// delete all visible child items // delete all visible child items
model_->beginRemoveRows(index(), 0, children_.count() - 1); model_->beginRemoveRows(index(), 0, children_.size() - 1);
if(!children_.isEmpty()) { if(!children_.empty()) {
Q_FOREACH(DirTreeModelItem* item, children_) { Q_FOREACH(DirTreeModelItem* item, children_) {
delete item; delete item;
} }
children_.clear(); children_.clear();
} }
model_->endRemoveRows(); model_->endRemoveRows();
// remove hidden children // remove hidden children
if(!hiddenChildren_.isEmpty()) { if(!hiddenChildren_.empty()) {
Q_FOREACH(DirTreeModelItem* item, hiddenChildren_) { Q_FOREACH(DirTreeModelItem* item, hiddenChildren_) {
delete item; delete item;
} }
hiddenChildren_.clear(); hiddenChildren_.clear();
} }
/* now, we have no child since all child items are removed. /* now, we have no child since all child items are removed.
* So we add a place holder child item to keep the expander around. */ * So we add a place holder child item to keep the expander around. */
addPlaceHolderChild(); addPlaceHolderChild();
/* deactivate folder since it will be reactivated on expand */ /* deactivate folder since it will be reactivated on expand */
freeFolder(); freeFolder();
expanded_ = false; expanded_ = false;
loaded_ = false; loaded_ = false;
} }
} }
QModelIndex DirTreeModelItem::index() { QModelIndex DirTreeModelItem::index() {
Q_ASSERT(model_); Q_ASSERT(model_);
return model_->indexFromItem(this); return model_->indexFromItem(this);
}
/* Add file info to parent node to proper position. */
DirTreeModelItem* DirTreeModelItem::insertFile(std::shared_ptr<const Fm::FileInfo> fi) {
// qDebug() << "insertFileInfo: " << fm_file_info_get_disp_name(fi);
DirTreeModelItem* item = new DirTreeModelItem(std::move(fi), model_);
insertItem(item);
return item;
} }
/* Add file info to parent node to proper position. /* Add file info to parent node to proper position. */
* GtkTreePath tp is the tree path of parent node. */ void DirTreeModelItem::insertFiles(Fm::FileInfoList files) {
DirTreeModelItem* DirTreeModelItem::insertFileInfo(FmFileInfo* fi) { if(children_.size() == 1 && placeHolderChild_) {
// qDebug() << "insertFileInfo: " << fm_file_info_get_disp_name(fi); // the list is empty, add them all at once and do sort
DirTreeModelItem* item = new DirTreeModelItem(fi, model_); if(!model_->showHidden()) { // need to separate visible and hidden items
insertItem(item); // insert hidden files into the "hiddenChildren_" list and remove them from "files" list
return item; // WARNING: "std::remove_if" shouldn't be used to work on the "removed" items because, as
// docs say, the elements between the returned and the end iterators are in an unspecified
// state and, as far as I (@tsujan) have tested, some of them announce themselves as null.
for(auto it = files.begin(); it != files.end();) {
auto file = *it;
if(file->isHidden()) {
hiddenChildren_.push_back(new DirTreeModelItem{std::move(file), model_});
it = files.erase(it);
}
else {
++it;
}
}
}
// sort the remaining visible files by name
std::sort(files.begin(), files.end(), [](const std::shared_ptr<const Fm::FileInfo>& a, const std::shared_ptr<const Fm::FileInfo>& b) {
return QString::localeAwareCompare(a->displayName(), b->displayName()) < 0;
});
// insert the files into the visible children list at once
model_->beginInsertRows(index(), 1, files.size() + 1); // the first item is the placeholder item, so we start from row 1
for(auto& file: files) {
if(file->isDir()) {
DirTreeModelItem* newItem = new DirTreeModelItem(std::move(file), model_);
newItem->parent_ = this;
children_.push_back(newItem);
}
}
model_->endInsertRows();
// remove the place holder if a folder is added
if(children_.size() > 1) {
auto it = std::find(children_.cbegin(), children_.cend(), placeHolderChild_);
if(it != children_.cend()) {
auto pos = it - children_.cbegin();
model_->beginRemoveRows(index(), pos, pos);
children_.erase(it);
delete placeHolderChild_;
model_->endRemoveRows();
placeHolderChild_ = nullptr;
}
}
}
else {
// the list already contain some items, insert new items one by one so they can be sorted.
for(auto& file: files) {
if(file->isDir()) {
insertFile(std::move(file));
}
}
}
} }
// find a good position to insert the new item // find a good position to insert the new item
// FIXME: insert one item at a time is slow. Insert multiple items at once and then sort is faster.
int DirTreeModelItem::insertItem(DirTreeModelItem* newItem) { int DirTreeModelItem::insertItem(DirTreeModelItem* newItem) {
if(model_->showHidden() || !newItem->fileInfo_ || !fm_file_info_is_hidden(newItem->fileInfo_)) { if(!newItem->fileInfo_ || !newItem->fileInfo_->isDir()) {
const char* new_key = fm_file_info_get_collate_key(newItem->fileInfo_); // don't insert placeholders or non-directory files
int pos = 0; return -1;
QList<DirTreeModelItem*>::iterator it; }
for(it = children_.begin(); it != children_.end(); ++it) { if(model_->showHidden() || !newItem->fileInfo_ || !newItem->fileInfo_->isHidden()) {
DirTreeModelItem* child = *it; auto it = std::lower_bound(children_.cbegin(), children_.cend(), newItem, [=](const DirTreeModelItem* a, const DirTreeModelItem* b) {
if(G_UNLIKELY(!child->fileInfo_)) if(Q_UNLIKELY(!a->fileInfo_)) {
continue; return true; // this is a placeholder item which will be removed so the order doesn't matter.
const char* key = fm_file_info_get_collate_key(child->fileInfo_); }
if(strcmp(new_key, key) <= 0) if(Q_UNLIKELY(!b->fileInfo_)) {
break; return false;
++pos; }
return QString::localeAwareCompare(a->fileInfo_->displayName(), b->fileInfo_->displayName()) < 0;
});
// inform the world that we're about to insert the item
auto position = it - children_.begin();
model_->beginInsertRows(index(), position, position);
newItem->parent_ = this;
children_.insert(it, newItem);
model_->endInsertRows();
return position;
} }
// inform the world that we're about to insert the item else { // hidden folder
model_->beginInsertRows(index(), pos, pos); hiddenChildren_.push_back(newItem);
newItem->parent_ = this; }
children_.insert(it, newItem); return -1;
model_->endInsertRows();
return pos;
}
else { // hidden folder
hiddenChildren_.append(newItem);
}
return -1;
} }
// FmFolder signal handlers // FmFolder signal handlers
// static void DirTreeModelItem::onFolderFinishLoading() {
void DirTreeModelItem::onFolderFinishLoading(FmFolder* folder, gpointer user_data) { DirTreeModel* model = model_;
DirTreeModelItem* _this = (DirTreeModelItem*)user_data; /* set 'loaded' flag beforehand as callback may check it */
DirTreeModel* model = _this->model_; loaded_ = true;
/* set 'loaded' flag beforehand as callback may check it */ QModelIndex idx = index();
_this->loaded_ = true; //qDebug() << "folder loaded";
QModelIndex index = _this->index(); // remove the placeholder child if needed
qDebug() << "folder loaded"; // (a check for its existence is necessary; see insertItem)
// remove the placeholder child if needed if(placeHolderChild_) {
if(_this->children_.count() == 1) { // we have no other child other than the place holder item, leave it if(children_.size() == 1) { // we have no other child other than the place holder item, leave it
_this->placeHolderChild_->displayName_ = DirTreeModel::tr("<No sub folders>"); placeHolderChild_->displayName_ = DirTreeModel::tr("<No sub folders>");
QModelIndex placeHolderIndex = _this->placeHolderChild_->index(); QModelIndex placeHolderIndex = placeHolderChild_->index();
// qDebug() << "placeHolderIndex: "<<placeHolderIndex; // qDebug() << "placeHolderIndex: "<<placeHolderIndex;
Q_EMIT model->dataChanged(placeHolderIndex, placeHolderIndex); Q_EMIT model->dataChanged(placeHolderIndex, placeHolderIndex);
} }
else { else {
int pos = _this->children_.indexOf(_this->placeHolderChild_); auto it = std::find(children_.cbegin(), children_.cend(), placeHolderChild_);
model->beginRemoveRows(index, pos, pos); if(it != children_.cend()) {
_this->children_.removeAt(pos); auto pos = it - children_.cbegin();
delete _this->placeHolderChild_; model->beginRemoveRows(idx, pos, pos);
model->endRemoveRows(); children_.erase(it);
_this->placeHolderChild_ = nullptr; delete placeHolderChild_;
} model->endRemoveRows();
placeHolderChild_ = nullptr;
Q_EMIT model->rowLoaded(index); }
}
}
Q_EMIT model->rowLoaded(idx);
} }
// static void DirTreeModelItem::onFolderFilesAdded(Fm::FileInfoList& files) {
void DirTreeModelItem::onFolderFilesAdded(FmFolder* folder, GSList* files, gpointer user_data) { insertFiles(files);
GSList* l;
DirTreeModelItem* _this = (DirTreeModelItem*)user_data;
for(l = files; l; l = l->next) {
FmFileInfo* fi = FM_FILE_INFO(l->data);
if(fm_file_info_is_dir(fi)) { /* FIXME: maybe adding files can be allowed later */
/* Ideally FmFolder should not emit files-added signals for files that
* already exists. So there is no need to check for duplication here. */
_this->insertFileInfo(fi);
}
}
} }
// static void DirTreeModelItem::onFolderFilesRemoved(Fm::FileInfoList& files) {
void DirTreeModelItem::onFolderFilesRemoved(FmFolder* folder, GSList* files, gpointer user_data) { DirTreeModel* model = model_;
DirTreeModelItem* _this = (DirTreeModelItem*)user_data;
DirTreeModel* model = _this->model_; for(auto& fi: files) {
int pos;
for(GSList* l = files; l; l = l->next) { DirTreeModelItem* child = childFromName(fi->name().c_str(), &pos);
FmFileInfo* fi = FM_FILE_INFO(l->data); if(child) {
int pos; // The item shouldn't be deleted now but after its row is removed from QTreeView;
DirTreeModelItem* child = _this->childFromName(fm_file_info_get_name(fi), &pos); // otherwise a freeze will happen when it has a child item (its row is expanded).
if(child) { child->queuedForDeletion_ = true;
model->beginRemoveRows(_this->index(), pos, pos); model->beginRemoveRows(index(), pos, pos);
_this->children_.removeAt(pos); children_.erase(children_.cbegin() + pos);
delete child; model->endRemoveRows();
model->endRemoveRows();
}
}
if(children_.empty()) { // no visible children, add a placeholder item to keep the row expanded
addPlaceHolderChild();
placeHolderChild_->displayName_ = DirTreeModel::tr("<No sub folders>");
} }
}
} }
// static void DirTreeModelItem::onFolderFilesChanged(std::vector<Fm::FileInfoPair> &changes) {
void DirTreeModelItem::onFolderFilesChanged(FmFolder* folder, GSList* files, gpointer user_data) { DirTreeModel* model = model_;
DirTreeModelItem* _this = (DirTreeModelItem*)user_data; for(auto& changePair: changes) {
DirTreeModel* model = _this->model_; int pos;
auto& changedFile = changePair.first;
for(GSList* l = files; l; l = l->next) { DirTreeModelItem* child = childFromName(changedFile->name().c_str(), &pos);
FmFileInfo* changedFile = FM_FILE_INFO(l->data); if(child) {
int pos; QModelIndex childIndex = child->index();
DirTreeModelItem* child = _this->childFromName(fm_file_info_get_name(changedFile), &pos); Q_EMIT model->dataChanged(childIndex, childIndex);
if(child) { }
QModelIndex childIndex = child->index();
Q_EMIT model->dataChanged(childIndex, childIndex);
} }
}
} }
DirTreeModelItem* DirTreeModelItem::childFromName(const char* utf8_name, int* pos) { DirTreeModelItem* DirTreeModelItem::childFromName(const char* utf8_name, int* pos) {
int i = 0; int i = 0;
for (const auto item : children_) { for(const auto item : children_) {
if(item->fileInfo_ && strcmp(fm_file_info_get_name(item->fileInfo_), utf8_name) == 0) { if(item->fileInfo_ && item->fileInfo_->name() == utf8_name) {
if(pos) if(pos) {
*pos = i; *pos = i;
return item; }
return item;
}
++i;
} }
++i; return nullptr;
}
return nullptr;
} }
DirTreeModelItem* DirTreeModelItem::childFromPath(FmPath* path, bool recursive) const { DirTreeModelItem* DirTreeModelItem::childFromPath(Fm::FilePath path, bool recursive) const {
Q_ASSERT(path != nullptr); Q_ASSERT(path != nullptr);
Q_FOREACH(DirTreeModelItem* item, children_) { Q_FOREACH(DirTreeModelItem* item, children_) {
// if(item->fileInfo_) // if(item->fileInfo_)
// qDebug() << "child: " << QString::fromUtf8(fm_file_info_get_disp_name(item->fileInfo_)); // qDebug() << "child: " << QString::fromUtf8(fm_file_info_get_disp_name(item->fileInfo_));
if(item->fileInfo_ && fm_path_equal(fm_file_info_get_path(item->fileInfo_), path)) { if(item->fileInfo_ && item->fileInfo_->path() == path) {
return item; return item;
} else if(recursive) { }
DirTreeModelItem* child = item->childFromPath(path, true); else if(recursive) {
if(child) DirTreeModelItem* child = item->childFromPath(std::move(path), true);
return child; if(child) {
return child;
}
}
} }
} return nullptr;
return nullptr;
} }
void DirTreeModelItem::setShowHidden(bool show) { void DirTreeModelItem::setShowHidden(bool show) {
if(show) { if(show) {
// move all hidden children to visible list // move all hidden children to visible list
Q_FOREACH(DirTreeModelItem* item, hiddenChildren_) { for(auto item: hiddenChildren_) {
insertItem(item); insertItem(item);
}
hiddenChildren_.clear();
// remove the placeholder if needed
if(children_.size() > 1) {
auto it = std::find(children_.cbegin(), children_.cend(), placeHolderChild_);
if(it != children_.cend()) {
auto pos = it - children_.cbegin();
model_->beginRemoveRows(index(), pos, pos);
children_.erase(it);
delete placeHolderChild_;
model_->endRemoveRows();
placeHolderChild_ = nullptr;
}
}
// recursively show children of children, etc.
for(auto item: children_) {
item->setShowHidden(true);
}
} }
hiddenChildren_.clear(); else { // hide hidden folders
} QModelIndex _index = index();
else { // hide hidden folders int pos = 0;
QModelIndex _index = index(); for(auto it = children_.begin(); it != children_.end(); ++pos) {
QList<DirTreeModelItem*>::iterator it, next; DirTreeModelItem* item = *it;
int pos = 0; if(item->fileInfo_) {
for(it = children_.begin(); it != children_.end(); ++pos) { if(item->fileInfo_->isHidden()) { // hidden folder
DirTreeModelItem* item = *it; // remove from the model and add to the hiddenChildren_ list
next = it + 1; model_->beginRemoveRows(_index, pos, pos);
if(item->fileInfo_) { it = children_.erase(it);
if(fm_file_info_is_hidden(item->fileInfo_)) { // hidden folder hiddenChildren_.push_back(item);
// remove from the model and add to the hiddenChildren_ list model_->endRemoveRows();
model_->beginRemoveRows(_index, pos, pos); }
children_.erase(it); else { // visible folder, recursively filter its children
hiddenChildren_.append(item); item->setShowHidden(show);
model_->endRemoveRows(); ++it;
}
}
else {
++it;
}
} }
else { // visible folder, recursively filter its children if(children_.empty()) { // no visible children, add a placeholder item to keep the row expanded
item->setShowHidden(show); addPlaceHolderChild();
placeHolderChild_->displayName_ = DirTreeModel::tr("<No sub folders>");
} }
}
it = next;
} }
}
} }
} // namespace Fm } // namespace Fm

@ -22,10 +22,13 @@
#include "libfmqtglobals.h" #include "libfmqtglobals.h"
#include <libfm/fm.h> #include <libfm/fm.h>
#include <vector>
#include <QIcon> #include <QIcon>
#include <QList>
#include <QModelIndex> #include <QModelIndex>
#include "core/fileinfo.h"
#include "core/folder.h"
namespace Fm { namespace Fm {
class DirTreeModel; class DirTreeModel;
@ -33,49 +36,61 @@ class DirTreeView;
class LIBFM_QT_API DirTreeModelItem { class LIBFM_QT_API DirTreeModelItem {
public: public:
friend class DirTreeModel; // allow direct access of private members in DirTreeModel friend class DirTreeModel; // allow direct access of private members in DirTreeModel
friend class DirTreeView; // allow direct access of private members in DirTreeView friend class DirTreeView; // allow direct access of private members in DirTreeView
explicit DirTreeModelItem();
explicit DirTreeModelItem(std::shared_ptr<const Fm::FileInfo> info, DirTreeModel* model, DirTreeModelItem* parent = nullptr);
~DirTreeModelItem();
void loadFolder();
void unloadFolder();
explicit DirTreeModelItem(); inline bool isPlaceHolder() const {
explicit DirTreeModelItem(FmFileInfo* info, DirTreeModel* model, DirTreeModelItem* parent = nullptr); return (fileInfo_ == nullptr);
~DirTreeModelItem(); }
void loadFolder(); void setShowHidden(bool show);
void unloadFolder();
inline bool isPlaceHolder() const { bool isQueuedForDeletion() {
return (fileInfo_ == nullptr); return queuedForDeletion_;
} }
void setShowHidden(bool show);
private: private:
void freeFolder(); void freeFolder();
void addPlaceHolderChild(); void addPlaceHolderChild();
DirTreeModelItem* childFromName(const char* utf8_name, int* pos); DirTreeModelItem* childFromName(const char* utf8_name, int* pos);
DirTreeModelItem* childFromPath(FmPath* path, bool recursive) const; DirTreeModelItem* childFromPath(Fm::FilePath path, bool recursive) const;
DirTreeModelItem* insertFileInfo(FmFileInfo* fi); DirTreeModelItem* insertFile(std::shared_ptr<const Fm::FileInfo> fi);
int insertItem(Fm::DirTreeModelItem* newItem); void insertFiles(Fm::FileInfoList files);
QModelIndex index(); int insertItem(Fm::DirTreeModelItem* newItem);
QModelIndex index();
static void onFolderFinishLoading(FmFolder* folder, gpointer user_data); void onFolderFinishLoading();
static void onFolderFilesAdded(FmFolder* folder, GSList* files, gpointer user_data); void onFolderFilesAdded(Fm::FileInfoList &files);
static void onFolderFilesRemoved(FmFolder* folder, GSList* files, gpointer user_data); void onFolderFilesRemoved(Fm::FileInfoList &files);
static void onFolderFilesChanged(FmFolder* folder, GSList* files, gpointer user_data); void onFolderFilesChanged(std::vector<Fm::FileInfoPair>& changes);
private: private:
FmFileInfo* fileInfo_; std::shared_ptr<const Fm::FileInfo> fileInfo_;
FmFolder* folder_; std::shared_ptr<Fm::Folder> folder_;
QString displayName_ ; QString displayName_ ;
QIcon icon_; QIcon icon_;
bool expanded_; bool expanded_;
bool loaded_; bool loaded_;
DirTreeModelItem* parent_; DirTreeModelItem* parent_;
DirTreeModelItem* placeHolderChild_; DirTreeModelItem* placeHolderChild_;
QList<DirTreeModelItem*> children_; std::vector<DirTreeModelItem*> children_;
QList<DirTreeModelItem*> hiddenChildren_; std::vector<DirTreeModelItem*> hiddenChildren_;
DirTreeModel* model_; DirTreeModel* model_;
bool queuedForDeletion_;
// signal connections
QMetaObject::Connection onFolderFinishLoadingConn_;
QMetaObject::Connection onFolderFilesAddedConn_;
QMetaObject::Connection onFolderFilesRemovedConn_;
QMetaObject::Connection onFolderFilesChangedConn_;
}; };
} }

@ -23,6 +23,7 @@
#include <QItemSelection> #include <QItemSelection>
#include <QGuiApplication> #include <QGuiApplication>
#include <QMouseEvent> #include <QMouseEvent>
#include <QTimer>
#include "dirtreemodel.h" #include "dirtreemodel.h"
#include "dirtreemodelitem.h" #include "dirtreemodelitem.h"
#include "filemenu.h" #include "filemenu.h"
@ -30,268 +31,309 @@
namespace Fm { namespace Fm {
DirTreeView::DirTreeView(QWidget* parent): DirTreeView::DirTreeView(QWidget* parent):
currentPath_(NULL), QTreeView(parent),
currentExpandingItem_(NULL) { currentExpandingItem_(nullptr) {
setSelectionMode(QAbstractItemView::SingleSelection); setSelectionMode(QAbstractItemView::SingleSelection);
setHeaderHidden(true); setHeaderHidden(true);
setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
header()->setStretchLastSection(false); header()->setStretchLastSection(false);
connect(this, &DirTreeView::collapsed, this, &DirTreeView::onCollapsed); connect(this, &DirTreeView::collapsed, this, &DirTreeView::onCollapsed);
connect(this, &DirTreeView::expanded, this, &DirTreeView::onExpanded); connect(this, &DirTreeView::expanded, this, &DirTreeView::onExpanded);
setContextMenuPolicy(Qt::CustomContextMenu); setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, &DirTreeView::customContextMenuRequested, connect(this, &DirTreeView::customContextMenuRequested,
this, &DirTreeView::onCustomContextMenuRequested); this, &DirTreeView::onCustomContextMenuRequested);
} }
DirTreeView::~DirTreeView() { DirTreeView::~DirTreeView() {
if(currentPath_)
fm_path_unref(currentPath_);
} }
void DirTreeView::cancelPendingChdir() { void DirTreeView::cancelPendingChdir() {
if(!pathsToExpand_.isEmpty()) { if(!pathsToExpand_.empty()) {
pathsToExpand_.clear(); pathsToExpand_.clear();
if(!currentExpandingItem_) if(!currentExpandingItem_) {
return; return;
DirTreeModel* _model = static_cast<DirTreeModel*>(model()); }
disconnect(_model, &DirTreeModel::rowLoaded, this, &DirTreeView::onRowLoaded); DirTreeModel* _model = static_cast<DirTreeModel*>(model());
currentExpandingItem_ = NULL; disconnect(_model, &DirTreeModel::rowLoaded, this, &DirTreeView::onRowLoaded);
} currentExpandingItem_ = nullptr;
}
} }
void DirTreeView::expandPendingPath() { void DirTreeView::expandPendingPath() {
if(pathsToExpand_.isEmpty()) if(pathsToExpand_.empty()) {
return; return;
}
FmPath* path = pathsToExpand_.first();
// qDebug() << "expanding: " << Path(path).displayBasename(); auto path = pathsToExpand_.front();
DirTreeModel* _model = static_cast<DirTreeModel*>(model()); // qDebug() << "expanding: " << Path(path).displayBasename();
DirTreeModelItem* item = _model->itemFromPath(path); DirTreeModel* _model = static_cast<DirTreeModel*>(model());
// qDebug() << "findItem: " << item; DirTreeModelItem* item = _model->itemFromPath(path);
if(item) { // qDebug() << "findItem: " << item;
currentExpandingItem_ = item; if(item) {
connect(_model, &DirTreeModel::rowLoaded, this, &DirTreeView::onRowLoaded); currentExpandingItem_ = item;
if(item->loaded_) { // the node is already loaded connect(_model, &DirTreeModel::rowLoaded, this, &DirTreeView::onRowLoaded);
onRowLoaded(item->index()); if(item->loaded_) { // the node is already loaded
onRowLoaded(item->index());
}
else {
// _model->loadRow(item->index());
item->loadFolder();
}
} }
else { else {
// _model->loadRow(item->index()); selectionModel()->clear();
item->loadFolder(); /* since we never get it loaded we need to update cwd here */
currentPath_ = path;
cancelPendingChdir(); // FIXME: is this correct? this is not done in the gtk+ version of libfm.
} }
}
else {
selectionModel()->clear();
/* since we never get it loaded we need to update cwd here */
if(currentPath_)
fm_path_unref(currentPath_);
currentPath_ = fm_path_ref(path);
cancelPendingChdir(); // FIXME: is this correct? this is not done in the gtk+ version of libfm.
}
} }
void DirTreeView::onRowLoaded(const QModelIndex& index) { void DirTreeView::onRowLoaded(const QModelIndex& index) {
DirTreeModel* _model = static_cast<DirTreeModel*>(model()); DirTreeModel* _model = static_cast<DirTreeModel*>(model());
if(!currentExpandingItem_) if(!currentExpandingItem_) {
return; return;
if(currentExpandingItem_ != _model->itemFromIndex(index)) { }
return; if(currentExpandingItem_ != _model->itemFromIndex(index)) {
} return;
/* disconnect the handler since we only need it once */ }
disconnect(_model, &DirTreeModel::rowLoaded, this, &DirTreeView::onRowLoaded); /* disconnect the handler since we only need it once */
disconnect(_model, &DirTreeModel::rowLoaded, this, &DirTreeView::onRowLoaded);
// DirTreeModelItem* item = _model->itemFromIndex(index);
// qDebug() << "row loaded: " << item->displayName_; // DirTreeModelItem* item = _model->itemFromIndex(index);
/* after the folder is loaded, the files should have been added to // qDebug() << "row loaded: " << item->displayName_;
* the tree model */ /* after the folder is loaded, the files should have been added to
expand(index); * the tree model */
expand(index);
/* remove the expanded path from pending list */
pathsToExpand_.removeFirst(); /* remove the expanded path from pending list */
if(pathsToExpand_.isEmpty()) { /* this is the last one and we're done, select the item */ pathsToExpand_.erase(pathsToExpand_.begin());
// qDebug() << "Done!"; if(pathsToExpand_.empty()) { /* this is the last one and we're done, select the item */
selectionModel()->select(index, QItemSelectionModel::SelectCurrent|QItemSelectionModel::Clear); // qDebug() << "Done!";
scrollTo(index, QAbstractItemView::EnsureVisible); selectionModel()->select(index, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Clear);
} scrollTo(index, QAbstractItemView::EnsureVisible);
else { /* continue expanding next pending path */ }
expandPendingPath(); else { /* continue expanding next pending path */
} expandPendingPath();
}
} }
void DirTreeView::setCurrentPath(FmPath* path) { void DirTreeView::setCurrentPath(Fm::FilePath path) {
DirTreeModel* _model = static_cast<DirTreeModel*>(model()); DirTreeModel* _model = static_cast<DirTreeModel*>(model());
if(!_model) if(!_model) {
return; return;
int rowCount = _model->rowCount(QModelIndex());
if(rowCount <= 0 || fm_path_equal(currentPath_, path))
return;
if(currentPath_)
fm_path_unref(currentPath_);
currentPath_ = fm_path_ref(path);
// NOTE: The content of each node is loaded on demand dynamically.
// So, when we ask for a chdir operation, some nodes do not exists yet.
// We have to wait for the loading of child nodes and continue the
// pending chdir operation after the child nodes become available.
// cancel previous pending tree expansion
cancelPendingChdir();
/* find a root item containing this path */
FmPath* root;
for(int row = 0; row < rowCount; ++row) {
QModelIndex index = _model->index(row, 0, QModelIndex());
root = _model->filePath(index);
if(fm_path_has_prefix(path, root))
break;
root = NULL;
}
if(root) { /* root item is found */
do { /* add path elements one by one to a list */
pathsToExpand_.prepend(path);
// qDebug() << "prepend path: " << Path(path).displayBasename();
if(fm_path_equal(path, root))
break;
path = fm_path_get_parent(path);
} }
while(path); int rowCount = _model->rowCount(QModelIndex());
if(rowCount <= 0 || currentPath_ == path) {
return;
}
currentPath_ = std::move(path);
// NOTE: The content of each node is loaded on demand dynamically.
// So, when we ask for a chdir operation, some nodes do not exists yet.
// We have to wait for the loading of child nodes and continue the
// pending chdir operation after the child nodes become available.
expandPendingPath(); // cancel previous pending tree expansion
} cancelPendingChdir();
/* find a root item containing this path */
Fm::FilePath root;
for(int row = 0; row < rowCount; ++row) {
QModelIndex index = _model->index(row, 0, QModelIndex());
auto row_path = _model->filePath(index);
if(row_path.isPrefixOf(currentPath_)) {
root = row_path;
break;
}
}
if(root) { /* root item is found */
path = currentPath_;
do { /* add path elements one by one to a list */
pathsToExpand_.insert(pathsToExpand_.cbegin(), path);
// qDebug() << "prepend path: " << Path(path).displayBasename();
if(path == root) {
break;
}
path = path.parent();
}
while(path);
expandPendingPath();
}
} }
void DirTreeView::setModel(QAbstractItemModel* model) { void DirTreeView::setModel(QAbstractItemModel* model) {
Q_ASSERT(model->inherits("Fm::DirTreeModel")); Q_ASSERT(model->inherits("Fm::DirTreeModel"));
if(!pathsToExpand_.isEmpty()) // if a chdir request is in progress, cancel it if(!pathsToExpand_.empty()) { // if a chdir request is in progress, cancel it
cancelPendingChdir(); cancelPendingChdir();
}
QTreeView::setModel(model); QTreeView::setModel(model);
header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &DirTreeView::onSelectionChanged); connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &DirTreeView::onSelectionChanged);
} }
void DirTreeView::mousePressEvent(QMouseEvent* event) { void DirTreeView::mousePressEvent(QMouseEvent* event) {
if(event && event->button() == Qt::RightButton && if(event && event->button() == Qt::RightButton &&
event->type() == QEvent::MouseButtonPress) { event->type() == QEvent::MouseButtonPress) {
// Do not change the selection when the context menu is activated. // Do not change the selection when the context menu is activated.
return; return;
} }
QTreeView::mousePressEvent(event); QTreeView::mousePressEvent(event);
} }
void DirTreeView::onCustomContextMenuRequested(const QPoint& pos) { void DirTreeView::onCustomContextMenuRequested(const QPoint& pos) {
QModelIndex index = indexAt(pos); QModelIndex index = indexAt(pos);
if(index.isValid()) { if(index.isValid()) {
QVariant data = index.data(DirTreeModel::FileInfoRole); QVariant data = index.data(DirTreeModel::FileInfoRole);
FmFileInfo* fileInfo = reinterpret_cast<FmFileInfo*>(data.value<void*>()); auto fileInfo = data.value<std::shared_ptr<const Fm::FileInfo>>();
if(fileInfo) { if(fileInfo) {
FmPath* path = fm_file_info_get_path(fileInfo); auto path = fileInfo->path();
FmFileInfoList* files = fm_file_info_list_new(); Fm::FileInfoList files ;
fm_file_info_list_push_tail(files, fileInfo); files.push_back(fileInfo);
Fm::FileMenu* menu = new Fm::FileMenu(files, fileInfo, path); Fm::FileMenu* menu = new Fm::FileMenu(files, fileInfo, path);
// FIXME: apply some settings to the menu and set a proper file launcher to it // FIXME: apply some settings to the menu and set a proper file launcher to it
Q_EMIT prepareFileMenu(menu); Q_EMIT prepareFileMenu(menu);
fm_file_info_list_unref(files);
QVariant pathData = qVariantFromValue(reinterpret_cast<void*>(path)); QVariant pathData = qVariantFromValue<Fm::FilePath>(path);
QAction* action = menu->openAction(); QAction* action = menu->openAction();
action->disconnect(); action->disconnect();
action->setData(index); action->setData(index);
connect(action, &QAction::triggered, this, &DirTreeView::onOpen); connect(action, &QAction::triggered, this, &DirTreeView::onOpen);
action = new QAction(QIcon::fromTheme("window-new"), tr("Open in New T&ab"), menu); action = new QAction(QIcon::fromTheme("window-new"), tr("Open in New T&ab"), menu);
action->setData(pathData); action->setData(pathData);
connect(action, &QAction::triggered, this, &DirTreeView::onNewTab); connect(action, &QAction::triggered, this, &DirTreeView::onNewTab);
menu->insertAction(menu->separator1(), action); menu->insertAction(menu->separator1(), action);
action = new QAction(QIcon::fromTheme("window-new"), tr("Open in New Win&dow"), menu); action = new QAction(QIcon::fromTheme("window-new"), tr("Open in New Win&dow"), menu);
action->setData(pathData); action->setData(pathData);
connect(action, &QAction::triggered, this, &DirTreeView::onNewWindow); connect(action, &QAction::triggered, this, &DirTreeView::onNewWindow);
menu->insertAction(menu->separator1(), action); menu->insertAction(menu->separator1(), action);
if(fm_file_info_is_native(fileInfo)) { if(fileInfo->isNative()) {
action = new QAction(QIcon::fromTheme("utilities-terminal"), tr("Open in Termina&l"), menu); action = new QAction(QIcon::fromTheme("utilities-terminal"), tr("Open in Termina&l"), menu);
action->setData(pathData); action->setData(pathData);
connect(action, &QAction::triggered, this, &DirTreeView::onOpenInTerminal); connect(action, &QAction::triggered, this, &DirTreeView::onOpenInTerminal);
menu->insertAction(menu->separator1(), action); menu->insertAction(menu->separator1(), action);
} }
menu->exec(mapToGlobal(pos)); menu->exec(mapToGlobal(pos));
delete menu; delete menu;
}
} }
}
} }
void DirTreeView::onOpen() { void DirTreeView::onOpen() {
if(QAction* action = qobject_cast<QAction*>(sender())) { if(QAction* action = qobject_cast<QAction*>(sender())) {
setCurrentIndex(action->data().toModelIndex()); setCurrentIndex(action->data().toModelIndex());
} }
} }
void DirTreeView::onNewWindow() { void DirTreeView::onNewWindow() {
if(QAction* action = qobject_cast<QAction*>(sender())) { if(QAction* action = qobject_cast<QAction*>(sender())) {
FmPath* path = reinterpret_cast<FmPath*>(action->data().value<void*>()); auto path = action->data().value<Fm::FilePath>();
Q_EMIT openFolderInNewWindowRequested(path); Q_EMIT openFolderInNewWindowRequested(path);
} }
} }
void DirTreeView::onNewTab() { void DirTreeView::onNewTab() {
if(QAction* action = qobject_cast<QAction*>(sender())) { if(QAction* action = qobject_cast<QAction*>(sender())) {
FmPath* path = reinterpret_cast<FmPath*>(action->data().value<void*>()); auto path = action->data().value<Fm::FilePath>();
Q_EMIT openFolderInNewTabRequested(path); Q_EMIT openFolderInNewTabRequested(path);
} }
} }
void DirTreeView::onOpenInTerminal() { void DirTreeView::onOpenInTerminal() {
if(QAction* action = qobject_cast<QAction*>(sender())) { if(QAction* action = qobject_cast<QAction*>(sender())) {
FmPath* path = reinterpret_cast<FmPath*>(action->data().value<void*>()); auto path = action->data().value<Fm::FilePath>();
Q_EMIT openFolderInTerminalRequested(path); Q_EMIT openFolderInTerminalRequested(path);
} }
} }
void DirTreeView::onNewFolder() { void DirTreeView::onNewFolder() {
if(QAction* action = qobject_cast<QAction*>(sender())) { if(QAction* action = qobject_cast<QAction*>(sender())) {
FmPath* path = reinterpret_cast<FmPath*>(action->data().value<void*>()); auto path = action->data().value<Fm::FilePath>();
Q_EMIT createNewFolderRequested(path); Q_EMIT createNewFolderRequested(path);
} }
} }
void DirTreeView::onCollapsed(const QModelIndex& index) { void DirTreeView::onCollapsed(const QModelIndex& index) {
DirTreeModel* treeModel = static_cast<DirTreeModel*>(model()); DirTreeModel* treeModel = static_cast<DirTreeModel*>(model());
if(treeModel) { if(treeModel) {
treeModel->unloadRow(index); treeModel->unloadRow(index);
} }
} }
void DirTreeView::onExpanded(const QModelIndex& index) { void DirTreeView::onExpanded(const QModelIndex& index) {
DirTreeModel* treeModel = static_cast<DirTreeModel*>(model()); DirTreeModel* treeModel = static_cast<DirTreeModel*>(model());
if(treeModel) { if(treeModel) {
treeModel->loadRow(index); treeModel->loadRow(index);
} }
}
void DirTreeView::rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) {
// see if to-be-removed items are queued for deletion
// and also clear selection if one of them is selected (otherwise a freeze will occur)
QModelIndex selIndex;
if(selectionModel()->selectedRows().size() == 1) {
selIndex = selectionModel()->selectedRows().at(0);
}
for (int i = start; i <= end; ++i) {
QModelIndex index = parent.child(i, 0);
if(index.isValid()) {
if(index == selIndex) {
selectionModel()->clear();
}
DirTreeModelItem* item = reinterpret_cast<DirTreeModelItem*>(index.internalPointer());
if (item->isQueuedForDeletion()) {
queuedForDeletion_.push_back(item);
}
}
}
QTreeView::rowsAboutToBeRemoved (parent, start, end);
} }
void DirTreeView::onSelectionChanged(const QItemSelection & selected, const QItemSelection & deselected) { void DirTreeView::rowsRemoved(const QModelIndex& parent, int start, int end) {
if(!selected.isEmpty()) { QTreeView::rowsRemoved (parent, start, end);
QModelIndex index = selected.first().topLeft(); // do the queued deletions only after all rows are removed (otherwise a freeze might occur)
DirTreeModel* _model = static_cast<DirTreeModel*>(model()); QTimer::singleShot(0, this, SLOT (doQueuedDeletions()));
FmPath* path = _model->filePath(index); }
if(path && currentPath_ && fm_path_equal(path, currentPath_))
return; void DirTreeView::doQueuedDeletions() {
cancelPendingChdir(); if(!queuedForDeletion_.empty()) {
if(!path) Q_FOREACH(DirTreeModelItem* item, queuedForDeletion_) {
return; delete item;
if(currentPath_) }
fm_path_unref(currentPath_); queuedForDeletion_.clear();
currentPath_ = fm_path_ref(path); }
}
// FIXME: use enums for type rather than hard-coded values 0 or 1
int type = 0; void DirTreeView::onSelectionChanged(const QItemSelection& selected, const QItemSelection& /*deselected*/) {
if(QGuiApplication::mouseButtons() & Qt::MiddleButton) if(!selected.isEmpty()) {
type = 1; QModelIndex index = selected.first().topLeft();
Q_EMIT chdirRequested(type, path); DirTreeModel* _model = static_cast<DirTreeModel*>(model());
} auto path = _model->filePath(index);
if(path && currentPath_ && path == currentPath_) {
return;
}
cancelPendingChdir();
if(!path) {
return;
}
currentPath_ = std::move(path);
// FIXME: use enums for type rather than hard-coded values 0 or 1
int type = 0;
if(QGuiApplication::mouseButtons() & Qt::MiddleButton) {
type = 1;
}
Q_EMIT chdirRequested(type, currentPath_);
}
} }

@ -23,7 +23,8 @@
#include "libfmqtglobals.h" #include "libfmqtglobals.h"
#include <QTreeView> #include <QTreeView>
#include <libfm/fm.h> #include <libfm/fm.h>
#include "path.h"
#include "core/filepath.h"
class QItemSelection; class QItemSelection;
@ -33,60 +34,59 @@ class FileMenu;
class DirTreeModelItem; class DirTreeModelItem;
class LIBFM_QT_API DirTreeView : public QTreeView { class LIBFM_QT_API DirTreeView : public QTreeView {
Q_OBJECT Q_OBJECT
public: public:
DirTreeView(QWidget* parent); explicit DirTreeView(QWidget* parent);
~DirTreeView(); ~DirTreeView();
FmPath* currentPath() {
return currentPath_;
}
void setCurrentPath(FmPath* path); const Fm::FilePath& currentPath() const {
return currentPath_;
}
// libfm-gtk compatible alias void setCurrentPath(Fm::FilePath path);
FmPath* getCwd() {
return currentPath();
}
void chdir(FmPath* path) { void chdir(Fm::FilePath path) {
setCurrentPath(path); setCurrentPath(std::move(path));
} }
virtual void setModel(QAbstractItemModel* model); virtual void setModel(QAbstractItemModel* model);
protected: protected:
virtual void mousePressEvent(QMouseEvent* event); virtual void mousePressEvent(QMouseEvent* event);
virtual void rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end);
private: private:
void cancelPendingChdir(); void cancelPendingChdir();
void expandPendingPath(); void expandPendingPath();
Q_SIGNALS: Q_SIGNALS:
void chdirRequested(int type, FmPath* path); void chdirRequested(int type, const Fm::FilePath& path);
void openFolderInNewWindowRequested(FmPath* path); void openFolderInNewWindowRequested(const Fm::FilePath& path);
void openFolderInNewTabRequested(FmPath* path); void openFolderInNewTabRequested(const Fm::FilePath& path);
void openFolderInTerminalRequested(FmPath* path); void openFolderInTerminalRequested(const Fm::FilePath& path);
void createNewFolderRequested(FmPath* path); void createNewFolderRequested(const Fm::FilePath& path);
void prepareFileMenu(Fm::FileMenu* menu); // emit before showing a Fm::FileMenu void prepareFileMenu(Fm::FileMenu* menu); // emit before showing a Fm::FileMenu
protected Q_SLOTS: protected Q_SLOTS:
void onCollapsed(const QModelIndex & index); void onCollapsed(const QModelIndex& index);
void onExpanded(const QModelIndex & index); void onExpanded(const QModelIndex& index);
void onRowLoaded(const QModelIndex& index); void onRowLoaded(const QModelIndex& index);
void onSelectionChanged(const QItemSelection & selected, const QItemSelection & deselected); void onSelectionChanged(const QItemSelection& selected, const QItemSelection& deselected);
void onCustomContextMenuRequested(const QPoint& pos); void onCustomContextMenuRequested(const QPoint& pos);
void onOpen(); void onOpen();
void onNewWindow(); void onNewWindow();
void onNewTab(); void onNewTab();
void onOpenInTerminal(); void onOpenInTerminal();
void onNewFolder(); void onNewFolder();
void rowsRemoved(const QModelIndex& parent, int start, int end);
void doQueuedDeletions();
private: private:
FmPath* currentPath_; Fm::FilePath currentPath_;
QList<Path> pathsToExpand_; Fm::FilePathList pathsToExpand_;
DirTreeModelItem* currentExpandingItem_; DirTreeModelItem* currentExpandingItem_;
std::vector<DirTreeModelItem*> queuedForDeletion_;
}; };
} }

@ -23,20 +23,22 @@
namespace Fm { namespace Fm {
DndActionMenu::DndActionMenu(Qt::DropActions possibleActions, QWidget* parent) DndActionMenu::DndActionMenu(Qt::DropActions possibleActions, QWidget* parent)
: QMenu(parent) : QMenu(parent)
, copyAction(nullptr) , copyAction(nullptr)
, moveAction(nullptr) , moveAction(nullptr)
, linkAction(nullptr) , linkAction(nullptr)
, cancelAction(nullptr) , cancelAction(nullptr) {
{ if(possibleActions.testFlag(Qt::CopyAction)) {
if (possibleActions.testFlag(Qt::CopyAction)) copyAction = addAction(QIcon::fromTheme("edit-copy"), tr("Copy here"));
copyAction = addAction(QIcon::fromTheme("edit-copy"), tr("Copy here")); }
if (possibleActions.testFlag(Qt::MoveAction)) if(possibleActions.testFlag(Qt::MoveAction)) {
moveAction = addAction(tr("Move here")); moveAction = addAction(tr("Move here"));
if (possibleActions.testFlag(Qt::LinkAction)) }
linkAction = addAction(tr("Create symlink here")); if(possibleActions.testFlag(Qt::LinkAction)) {
addSeparator(); linkAction = addAction(tr("Create symlink here"));
cancelAction = addAction(tr("Cancel")); }
addSeparator();
cancelAction = addAction(tr("Cancel"));
} }
DndActionMenu::~DndActionMenu() { DndActionMenu::~DndActionMenu() {
@ -44,19 +46,21 @@ DndActionMenu::~DndActionMenu() {
} }
Qt::DropAction DndActionMenu::askUser(Qt::DropActions possibleActions, QPoint pos) { Qt::DropAction DndActionMenu::askUser(Qt::DropActions possibleActions, QPoint pos) {
Qt::DropAction result = Qt::IgnoreAction; Qt::DropAction result = Qt::IgnoreAction;
DndActionMenu menu{possibleActions}; DndActionMenu menu{possibleActions};
QAction* action = menu.exec(pos); QAction* action = menu.exec(pos);
if (nullptr != action) if(nullptr != action) {
{ if(action == menu.copyAction) {
if(action == menu.copyAction) result = Qt::CopyAction;
result = Qt::CopyAction; }
else if(action == menu.moveAction) else if(action == menu.moveAction) {
result = Qt::MoveAction; result = Qt::MoveAction;
else if(action == menu.linkAction) }
result = Qt::LinkAction; else if(action == menu.linkAction) {
} result = Qt::LinkAction;
return result; }
}
return result;
} }

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save