Cherry-picking upstream release 0.12.0.

* Switched to experimental
* Bumped Standards to 4.1.1
* Fixed Symbols
* Added build dependency libexif-dev
* Added dependencies libexif-dev and libmenu-cache-dev to libfm-qt-dev
* Added override for dh_missing
* Bumped years in copyright
ubuntu/cosmic debian/0.12.0-1
Alf Gaida 7 years ago
parent 2e496b302c
commit ea757e4af0

@ -3,4 +3,4 @@ Upstream Authors:
Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
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)
* bump patch version (#56)
* 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_VERSION_MAJOR 0)
set(LIBFM_QT_VERSION_MINOR 11)
set(LIBFM_QT_VERSION_PATCH 2)
set(LIBFM_QT_VERSION_MINOR 12)
set(LIBFM_QT_VERSION_PATCH 0)
set(LIBFM_QT_VERSION ${LIBFM_QT_VERSION_MAJOR}.${LIBFM_QT_VERSION_MINOR}.${LIBFM_QT_VERSION_PATCH})
list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")
@ -24,7 +24,7 @@ set(LIBFM_QT_LIB_SOVERSION "3")
set(REQUIRED_QT_VERSION "5.2")
set(REQUIRED_LIBFM_VERSION "1.2.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)
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(Fm "${REQUIRED_LIBFM_VERSION}" REQUIRED)
find_package(MenuCache "${REQUIRED_LIBMENUCACHE_VERSION}" REQUIRED)
find_package(Exif 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)
include(GNUInstallDirs)
@ -65,6 +66,7 @@ install(FILES
)
add_subdirectory(src)
add_subdirectory(data)
# add Doxygen support to generate API docs
# 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

13
debian/changelog vendored

@ -1,3 +1,16 @@
libfm-qt (0.12.0-1) experimental; urgency=medium
* Cherry-picking upstream release 0.12.0.
* Switched to experimental
* Bumped Standards to 4.1.1
* Fixed Symbols
* Added build dependency libexif-dev
* Added dependencies libexif-dev and libmenu-cache-dev to libfm-qt-dev
* Added override for dh_missing
* Bumped years in copyright
-- Alf Gaida <agaida@siduction.org> Sun, 22 Oct 2017 16:34:45 +0200
libfm-qt (0.11.2-2) unstable; urgency=medium
* Bumped Standards to 4.0.0

13
debian/control vendored

@ -7,6 +7,7 @@ Uploaders: Alf Gaida <agaida@siduction.org>,
Section: x11
Priority: optional
Build-Depends: debhelper (>= 10),
libexif-dev,
libfm-dev (>= 1.2.0),
libglib2.0-dev,
libkf5windowsystem-dev,
@ -14,11 +15,11 @@ Build-Depends: debhelper (>= 10),
libqt5svg5-dev,
libqt5x11extras5-dev,
libx11-dev,
lxqt-build-tools (>= 0.3.0),
lxqt-build-tools (>= 0.4.0),
qtbase5-private-dev
Standards-Version: 4.0.0
Vcs-Browser: https://anonscm.debian.org/cgit/pkg-lxqt/libfm-qt.git/?h=debian/sid
Vcs-Git: https://anonscm.debian.org/git/pkg-lxqt/libfm-qt.git -b debian/sid
Standards-Version: 4.1.1
Vcs-Browser: https://anonscm.debian.org/cgit/pkg-lxqt/libfm-qt.git/?h=debian/experimental
Vcs-Git: https://anonscm.debian.org/git/pkg-lxqt/libfm-qt.git -b debian/experimental
Homepage: https://github.com/lxde/libfm-qt
Package: libfm-qt3
@ -48,8 +49,10 @@ Breaks: libfm-qt5-dev
Architecture: any
Section: libdevel
Depends: ${misc:Depends},
libexif-dev,
libfm-dev (>= 1.2.0),
libfm-qt3 (= ${binary:Version})
libfm-qt3 (= ${binary:Version}),
libmenu-cache-dev
Description: file management support library for pcmanfm-qt (development files)
Libfm-Qt is a companion library providing components to build desktop file
managers.

6
debian/copyright vendored

@ -3,8 +3,8 @@ Upstream-Name: libfm-qt
Source: https://github.com/lxde/libfm-qt
Files: *
Copyright: 2013-2016 LXQt team
2013-2016 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
Copyright: 2013-2017 LXQt team
2013-2017 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
2012-2013 Andriy Grytsenko (LStranger) <andrej@rep.kiev.ua>
2014 Kuzma Shapran <kuzma.shapran@gmail.com>
License: LGPL-2.1+
@ -16,7 +16,7 @@ License: BSD-3-Clause
Files: debian/*
Copyright: 2014-2015 Wen Liao <wen.cf83@gmail.com>
2014-2016 ChangZhuo Chen (陳昌倬) <czchen@debian.org>
2013-2016 Alf Gaida <agaida@siduction.org>
2013-2017 Alf Gaida <agaida@siduction.org>
2015 Andrew Lee (李健秋) <ajqlee@debian.org>
License: LGPL-2.1+

@ -3,3 +3,6 @@ usr/lib/*/*.so
usr/lib/*/pkgconfig/*
usr/share/cmake/fm-qt/*.cmake
usr/include/libfm-qt/customactions/*.h
usr/include/libfm-qt/core/*.h

@ -1 +1,4 @@
usr/lib/*/*.so.*
usr/share/libfm-qt/terminals.list
usr/share/libfm-qt/archivers.list
usr/share/mime/packages/libfm-qt-mimetypes.xml

File diff suppressed because it is too large Load Diff

6
debian/rules vendored

@ -6,8 +6,10 @@ export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed
export DEB_BUILD_MAINT_OPTIONS = hardening=+all
%:
dh ${@} --buildsystem cmake \
--fail-missing
dh ${@} --buildsystem cmake
override_dh_missing:
dh_missing --fail-missing
override_dh_auto_configure:
dh_auto_configure -- \

@ -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
${libfm_core_SRCS}
libfmqt.cpp
bookmarkaction.cpp
sidepane.cpp
@ -36,12 +73,12 @@ set(libfm_SRCS
utilities.cpp
dndactionmenu.cpp
editbookmarksdialog.cpp
thumbnailloader.cpp
execfiledialog.cpp
appchoosercombobox.cpp
appmenuview.cpp
appchooserdialog.cpp
filesearchdialog.cpp
filedialog.cpp
fm-search.c # might be moved to libfm later
xdndworkaround.cpp
)
@ -55,6 +92,7 @@ set(libfm_UIS
exec-file.ui
app-chooser-dialog.ui
filesearch.ui
filedialog.ui
)
qt5_wrap_ui(libfm_UIS_H ${libfm_UIS})
@ -80,11 +118,6 @@ add_library(${LIBFM_QT_LIBRARY_NAME} SHARED
${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
"${LIBFM_QT_LIBRARY_NAME}-targets"
DESTINATION "${CMAKE_INSTALL_DATADIR}/cmake/${LIBFM_QT_LIBRARY_NAME}"
@ -97,6 +130,7 @@ target_link_libraries(${LIBFM_QT_LIBRARY_NAME}
${FM_LIBRARIES}
${MENUCACHE_LIBRARIES}
${XCB_LIBRARIES}
${EXIF_LIBRARIES}
)
# 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.
"${MENUCACHE_INCLUDE_DIRS}"
"${XCB_INCLUDE_DIRS}"
"${EXIF_INCLUDE_DIRS}"
INTERFACE
"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
"$<BUILD_INTERFACE:${LIBFM_QT_INTREE_INCLUDE_DIR}>"
@ -128,11 +163,11 @@ install(FILES
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(FILES ${libfm_HS}
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/libfm-qt"
COMPONENT Devel
FILES_MATCHING PATTERN "*.h"
)
generate_export_header(${LIBFM_QT_LIBRARY_NAME}
@ -140,10 +175,14 @@ generate_export_header(${LIBFM_QT_LIBRARY_NAME}
)
# 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"
)
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/
DESTINATION "${LIBFM_QT_INTREE_INCLUDE_DIR}/libfm-qt"
FILES_MATCHING PATTERN "*.h"
)
configure_package_config_file(
"${PROJECT_SOURCE_DIR}/cmake/fm-qt-config.cmake.in"
@ -194,3 +233,37 @@ endif()
# prevent the generated files from being deleted during make cleaner
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 "appchooserdialog.h"
#include "utilities.h"
#include "core/iconinfo.h"
namespace Fm {
AppChooserComboBox::AppChooserComboBox(QWidget* parent):
QComboBox(parent),
mimeType_(NULL),
appInfos_(NULL),
defaultApp_(NULL),
defaultAppIndex_(-1),
prevIndex_(0),
blockOnCurrentIndexChanged_(false) {
// the new Qt5 signal/slot syntax cannot handle overloaded methods by default
// hence a type-casting is needed here. really ugly!
// reference: http://qt-project.org/forums/viewthread/21513
connect((QComboBox*)this, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &AppChooserComboBox::onCurrentIndexChanged);
QComboBox(parent),
defaultAppIndex_(-1),
prevIndex_(0),
blockOnCurrentIndexChanged_(false) {
// the new Qt5 signal/slot syntax cannot handle overloaded methods by default
// hence a type-casting is needed here. really ugly!
// reference: http://qt-project.org/forums/viewthread/21513
connect((QComboBox*)this, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &AppChooserComboBox::onCurrentIndexChanged);
}
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) {
clear();
if(mimeType_)
fm_mime_type_unref(mimeType_);
mimeType_ = fm_mime_type_ref(mimeType);
if(mimeType_) {
const char* typeName = fm_mime_type_get_type(mimeType_);
defaultApp_ = g_app_info_get_default_for_type(typeName, FALSE);
appInfos_ = g_app_info_get_all_for_type(typeName);
int i = 0;
for(GList* l = appInfos_; l; l = l->next, ++i) {
GAppInfo* app = G_APP_INFO(l->data);
GIcon* gicon = g_app_info_get_icon(app);
QString name = QString::fromUtf8(g_app_info_get_name(app));
// QVariant data = qVariantFromValue<void*>(app);
// addItem(IconTheme::icon(gicon), name, data);
addItem(IconTheme::icon(gicon), name);
if(g_app_info_equal(app, defaultApp_))
defaultAppIndex_ = i;
void AppChooserComboBox::setMimeType(std::shared_ptr<const Fm::MimeType> mimeType) {
clear();
defaultApp_.reset();
appInfos_.clear();
mimeType_ = std::move(mimeType);
if(mimeType_) {
const char* typeName = mimeType_->name();
defaultApp_ = Fm::GAppInfoPtr{g_app_info_get_default_for_type(typeName, FALSE), false};
GList* appInfos_glist = g_app_info_get_all_for_type(typeName);
int i = 0;
for(GList* l = appInfos_glist; l; l = l->next, ++i) {
Fm::GAppInfoPtr app{G_APP_INFO(l->data), false};
GIcon* gicon = g_app_info_get_icon(app.get());
addItem(gicon ? Fm::IconInfo::fromGIcon(gicon)->qicon(): QIcon(), g_app_info_get_name(app.get()));
if(g_app_info_equal(app.get(), defaultApp_.get())) {
defaultAppIndex_ = i;
}
appInfos_.push_back(std::move(app));
}
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.
GAppInfo* AppChooserComboBox::selectedApp() {
return G_APP_INFO(g_list_nth_data(appInfos_, currentIndex()));
Fm::GAppInfoPtr AppChooserComboBox::selectedApp() const {
int idx = currentIndex();
return idx >= 0 ? appInfos_[idx] : Fm::GAppInfoPtr{};
}
bool AppChooserComboBox::isChanged() {
return (defaultAppIndex_ != currentIndex());
bool AppChooserComboBox::isChanged() const {
return (defaultAppIndex_ != currentIndex());
}
void AppChooserComboBox::onCurrentIndexChanged(int index) {
if(index == -1 || index == prevIndex_ || blockOnCurrentIndexChanged_)
return;
// the last item is "Customize"
if(index == (count() - 1)) {
/* TODO: let the user choose an app or add custom actions here. */
QWidget* toplevel = topLevelWidget();
AppChooserDialog dlg(mimeType_, toplevel);
dlg.setWindowModality(Qt::WindowModal);
dlg.setCanSetDefault(false);
if(dlg.exec() == QDialog::Accepted) {
GAppInfo* app = dlg.selectedApp();
if(app) {
/* see if it's already in the list to prevent duplication */
GList* found = NULL;
for(found = appInfos_; found; found = found->next) {
if(g_app_info_equal(app, G_APP_INFO(found->data)))
break;
if(index == -1 || index == prevIndex_ || blockOnCurrentIndexChanged_) {
return;
}
// the last item is "Customize"
if(index == (count() - 1)) {
/* TODO: let the user choose an app or add custom actions here. */
QWidget* toplevel = topLevelWidget();
AppChooserDialog dlg(mimeType_, toplevel);
dlg.setWindowModality(Qt::WindowModal);
dlg.setCanSetDefault(false);
if(dlg.exec() == QDialog::Accepted) {
auto app = dlg.selectedApp();
if(app) {
/* see if it's already in the list to prevent duplication */
auto found = std::find_if(appInfos_.cbegin(), appInfos_.cend(), [&](const Fm::GAppInfoPtr& item) {
return g_app_info_equal(app.get(), item.get());
});
// 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.
// we need to block our handler to prevent recursive calls.
// block our handler to prevent recursive calls.
blockOnCurrentIndexChanged_ = true;
/* if it's already in the list, select it */
if(found) {
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);
}
// restore to previously selected item
setCurrentIndex(prevIndex_);
blockOnCurrentIndexChanged_ = false;
return;
}
}
// block our handler to prevent recursive calls.
blockOnCurrentIndexChanged_ = true;
// restore to previously selected item
setCurrentIndex(prevIndex_);
blockOnCurrentIndexChanged_ = false;
}
else {
prevIndex_ = index;
}
else {
prevIndex_ = index;
}
}

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

@ -25,262 +25,262 @@
namespace Fm {
AppChooserDialog::AppChooserDialog(FmMimeType* mimeType, QWidget* parent, Qt::WindowFlags f):
QDialog(parent, f),
ui(new Ui::AppChooserDialog()),
mimeType_(NULL),
canSetDefault_(true),
selectedApp_(NULL) {
ui->setupUi(this);
AppChooserDialog::AppChooserDialog(std::shared_ptr<const Fm::MimeType> mimeType, QWidget* parent, Qt::WindowFlags f):
QDialog(parent, f),
ui(new Ui::AppChooserDialog()),
mimeType_{std::move(mimeType)},
canSetDefault_(true) {
ui->setupUi(this);
connect(ui->appMenuView, &AppMenuView::selectionChanged, this, &AppChooserDialog::onSelectionChanged);
connect(ui->tabWidget, &QTabWidget::currentChanged, this, &AppChooserDialog::onTabChanged);
connect(ui->appMenuView, &AppMenuView::selectionChanged, this, &AppChooserDialog::onSelectionChanged);
connect(ui->tabWidget, &QTabWidget::currentChanged, this, &AppChooserDialog::onTabChanged);
if(!ui->appMenuView->isAppSelected())
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); // disable OK button
if(mimeType)
setMimeType(mimeType);
if(!ui->appMenuView->isAppSelected()) {
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); // disable OK button
}
}
AppChooserDialog::~AppChooserDialog() {
delete ui;
if(mimeType_)
fm_mime_type_unref(mimeType_);
if(selectedApp_)
g_object_unref(selectedApp_);
delete ui;
}
bool AppChooserDialog::isSetDefault() {
return ui->setDefault->isChecked();
bool AppChooserDialog::isSetDefault() const {
return ui->setDefault->isChecked();
}
static void on_temp_appinfo_destroy(gpointer data, GObject* objptr) {
char* filename = (char*)data;
if(g_unlink(filename) < 0)
g_critical("failed to remove %s", filename);
/* else
qDebug("temp file %s removed", filename); */
g_free(filename);
static void on_temp_appinfo_destroy(gpointer data, GObject* /*objptr*/) {
char* filename = (char*)data;
if(g_unlink(filename) < 0) {
g_critical("failed to remove %s", filename);
}
/* else
qDebug("temp file %s removed", filename); */
g_free(filename);
}
static GAppInfo* app_info_create_from_commandline(const char* commandline,
const char* application_name,
const char* bin_name,
const char* mime_type,
gboolean terminal, gboolean keep) {
GAppInfo* app = NULL;
char* dirname = g_build_filename(g_get_user_data_dir(), "applications", NULL);
const char* app_basename = strrchr(bin_name, '/');
const char* application_name,
const char* bin_name,
const char* mime_type,
gboolean terminal, gboolean keep) {
GAppInfo* app = nullptr;
char* dirname = g_build_filename(g_get_user_data_dir(), "applications", nullptr);
const char* app_basename = strrchr(bin_name, '/');
if(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);
if(app_basename) {
app_basename++;
}
g_free(filename);
}
g_free(dirname);
return app;
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, 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) {
/* 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. */
if(!arg_found) { /* append %f if no %f, %F, %u, or %U was found. */
cmdline += " %f";
const char* p = strstr(cmdline, " %");
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? */
/* We need to ensure that no duplicated items are added */
if(mimeType_) {
MenuCache* menu_cache;
/* see if the command is already in the list of known apps for this mime-type */
GList* apps = g_app_info_get_all_for_type(fm_mime_type_get_type(mimeType_));
GList* l;
for(l = apps; l; l = l->next) {
GAppInfo* app2 = G_APP_INFO(l->data);
const char* cmd = g_app_info_get_commandline(app2);
char* bin2 = get_binary(cmd, NULL);
if(g_strcmp0(bin1, bin2) == 0) {
app = G_APP_INFO(g_object_ref(app2));
qDebug("found in app list");
g_free(bin2);
break;
GAppInfo* AppChooserDialog::customCommandToApp() {
GAppInfo* app = nullptr;
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. */
if(!arg_found) { /* append %f if no %f, %F, %u, or %U was found. */
cmdline += " %f";
}
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 */
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 == NULL) {
g_warning("application %s has no Exec statement", menu_cache_item_get_id(MENU_CACHE_ITEM(ma)));
continue;
/* FIXME: is there any better way to do this? */
/* We need to ensure that no duplicated items are added */
if(mimeType_) {
MenuCache* menu_cache;
/* see if the command is already in the list of known apps for this mime-type */
GList* apps = g_app_info_get_all_for_type(mimeType_->name());
GList* l;
for(l = apps; l; l = l->next) {
GAppInfo* app2 = G_APP_INFO(l->data);
const char* cmd = g_app_info_get_commandline(app2);
char* bin2 = get_binary(cmd, nullptr);
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);
}
bin2 = get_binary(exec, NULL);
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;
g_list_foreach(apps, (GFunc)g_object_unref, nullptr);
g_list_free(apps);
if(app) {
goto _out;
}
/* 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 */
app = app_info_create_from_commandline(cmdline.constData(), app_name.constData(), bin1,
mimeType_ ? fm_mime_type_get_type(mimeType_) : NULL,
ui->useTerminal->isChecked(), ui->keepTermOpen->isChecked());
_out:
g_free(bin1);
}
return app;
/* 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,
mimeType_ ? mimeType_->name() : nullptr,
ui->useTerminal->isChecked(), ui->keepTermOpen->isChecked());
_out:
g_free(bin1);
}
return app;
}
void AppChooserDialog::accept() {
QDialog::accept();
QDialog::accept();
if(ui->tabWidget->currentIndex() == 0) {
selectedApp_ = ui->appMenuView->selectedApp();
}
else { // custom command line
selectedApp_ = customCommandToApp();
}
if(ui->tabWidget->currentIndex() == 0) {
selectedApp_ = ui->appMenuView->selectedApp();
}
else { // custom command line
selectedApp_ = customCommandToApp();
}
if(selectedApp_) {
if(mimeType_ && fm_mime_type_get_type(mimeType_) && g_app_info_get_name(selectedApp_)[0]) {
/* add this app to the mime-type */
if(selectedApp_) {
if(mimeType_ && g_app_info_get_name(selectedApp_.get())) {
/* add this app to the mime-type */
#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
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
/* if need to set default */
if(ui->setDefault->isChecked())
g_app_info_set_as_default_for_type(selectedApp_, fm_mime_type_get_type(mimeType_), NULL);
/* if need to set default */
if(ui->setDefault->isChecked()) {
g_app_info_set_as_default_for_type(selectedApp_.get(), mimeType_->name(), nullptr);
}
}
}
}
}
void AppChooserDialog::onSelectionChanged() {
bool isAppSelected = ui->appMenuView->isAppSelected();
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(isAppSelected);
bool isAppSelected = ui->appMenuView->isAppSelected();
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(isAppSelected);
}
void AppChooserDialog::setMimeType(FmMimeType* mimeType) {
if(mimeType_)
fm_mime_type_unref(mimeType_);
mimeType_ = mimeType ? fm_mime_type_ref(mimeType) : NULL;
if(mimeType_) {
QString text = tr("Select an application to open \"%1\" files")
.arg(QString::fromUtf8(fm_mime_type_get_desc(mimeType_)));
ui->fileTypeHeader->setText(text);
}
else {
ui->fileTypeHeader->hide();
ui->setDefault->hide();
}
void AppChooserDialog::setMimeType(std::shared_ptr<const Fm::MimeType> mimeType) {
mimeType_ = std::move(mimeType);
if(mimeType_) {
QString text = tr("Select an application to open \"%1\" files")
.arg(QString::fromUtf8(mimeType_->desc()));
ui->fileTypeHeader->setText(text);
}
else {
ui->fileTypeHeader->hide();
ui->setDefault->hide();
}
}
void AppChooserDialog::setCanSetDefault(bool value) {
canSetDefault_ = value;
ui->setDefault->setVisible(value);
canSetDefault_ = value;
ui->setDefault->setVisible(value);
}
void AppChooserDialog::onTabChanged(int index) {
if(index == 0) { // app menu view
onSelectionChanged();
}
else if(index == 1) { // custom command
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
}
if(index == 0) { // app menu view
onSelectionChanged();
}
else if(index == 1) { // custom command
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
}
}
} // namespace Fm

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

@ -28,17 +28,17 @@ typedef struct _FmAppLaunchContext {
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();
if(dpy) {
char* xstr = DisplayString(dpy);
return g_strdup(xstr);
}
return NULL;
return nullptr;
}
static char* fm_app_launch_context_get_startup_notify_id(GAppLaunchContext *context, GAppInfo *info, GList *files) {
return NULL;
static char* fm_app_launch_context_get_startup_notify_id(GAppLaunchContext * /*context*/, GAppInfo * /*info*/, GList * /*files*/) {
return nullptr;
}
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;
}
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* context = (FmAppLaunchContext*)g_object_new(FM_TYPE_APP_LAUNCH_CONTEXT, NULL);
FmAppLaunchContext* fm_app_launch_context_new_for_widget(QWidget* /*widget*/) {
FmAppLaunchContext* context = (FmAppLaunchContext*)g_object_new(FM_TYPE_APP_LAUNCH_CONTEXT, nullptr);
return context;
}
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;
}

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

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

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

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

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

@ -23,30 +23,25 @@
#include "libfmqtglobals.h"
#include <QAction>
#include <libfm/fm.h>
#include "core/bookmarks.h"
namespace Fm {
// action used to create bookmark menu items
class LIBFM_QT_API BookmarkAction : public QAction {
public:
explicit BookmarkAction(FmBookmarkItem* item, QObject* parent = 0);
explicit BookmarkAction(std::shared_ptr<const Fm::BookmarkItem> item, QObject* parent = 0);
virtual ~BookmarkAction() {
if(item_)
fm_bookmark_item_unref(item_);
}
const std::shared_ptr<const Fm::BookmarkItem>& bookmark() const {
return item_;
}
FmBookmarkItem* bookmark() {
return item_;
}
FmPath* path() {
return item_->path;
}
const Fm::FilePath& path() const {
return item_->path();
}
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 {
BrowseHistory::BrowseHistory():
currentIndex_(0),
maxCount_(10) {
currentIndex_(0),
maxCount_(10) {
}
BrowseHistory::~BrowseHistory() {
}
void BrowseHistory::add(FmPath* path, int scrollPos) {
int lastIndex = size() - 1;
if(currentIndex_ < lastIndex) {
// if we're not at the last item, remove items after the current one.
erase(begin() + currentIndex_ + 1, end());
}
void BrowseHistory::add(Fm::FilePath path, int scrollPos) {
int lastIndex = items_.size() - 1;
if(currentIndex_ < lastIndex) {
// if we're not at the last item, remove items after the current one.
items_.erase(items_.cbegin() + currentIndex_ + 1, items_.cend());
}
if(size() + 1 > maxCount_) {
// if there are too many items, remove the oldest one.
// FIXME: what if currentIndex_ == 0? remove the last item instead?
if(currentIndex_ == 0)
remove(lastIndex);
else {
remove(0);
--currentIndex_;
if(items_.size() + 1 > static_cast<size_t>(maxCount_)) {
// if there are too many items, remove the oldest one.
// FIXME: what if currentIndex_ == 0? remove the last item instead?
if(currentIndex_ == 0) {
items_.erase(items_.cbegin() + lastIndex);
}
else {
items_.erase(items_.cbegin());
--currentIndex_;
}
}
}
// add a path and current scroll position to browse history
append(BrowseHistoryItem(path, scrollPos));
currentIndex_ = size() - 1;
// add a path and current scroll position to browse history
items_.push_back(BrowseHistoryItem(path, scrollPos));
currentIndex_ = items_.size() - 1;
}
void BrowseHistory::setCurrentIndex(int index) {
if(index >= 0 && index < size()) {
currentIndex_ = index;
// FIXME: should we emit a signal for the change?
}
if(index >= 0 && static_cast<size_t>(index) < items_.size()) {
currentIndex_ = index;
// FIXME: should we emit a signal for the change?
}
}
bool BrowseHistory::canBackward() const {
return (currentIndex_ > 0);
return (currentIndex_ > 0);
}
int BrowseHistory::backward() {
if(canBackward())
--currentIndex_;
return currentIndex_;
if(canBackward()) {
--currentIndex_;
}
return currentIndex_;
}
bool BrowseHistory::canForward() const {
return (currentIndex_ + 1 < size());
return (static_cast<size_t>(currentIndex_) + 1 < items_.size());
}
int BrowseHistory::forward() {
if(canForward())
++currentIndex_;
return currentIndex_;
if(canForward()) {
++currentIndex_;
}
return currentIndex_;
}
void BrowseHistory::setMaxCount(int maxCount) {
maxCount_ = maxCount;
if(size() > maxCount) {
// TODO: remove some items
}
maxCount_ = maxCount;
if(items_.size() > static_cast<size_t>(maxCount)) {
// TODO: remove some items
}
}

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

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

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

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

@ -28,26 +28,26 @@
namespace Fm {
class LIBFM_QT_API ColorButton : public QPushButton {
Q_OBJECT
Q_OBJECT
public:
explicit ColorButton(QWidget* parent = 0);
virtual ~ColorButton();
explicit ColorButton(QWidget* parent = 0);
virtual ~ColorButton();
void setColor(const QColor&);
void setColor(const QColor&);
QColor color() const {
return color_;
}
QColor color() const {
return color_;
}
Q_SIGNALS:
void changed();
void changed();
private Q_SLOTS:
void onClicked();
void onClicked();
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 "icontheme.h"
#include "utilities.h"
#include "core/iconinfo.h"
namespace Fm {
CreateNewMenu::CreateNewMenu(QWidget* dialogParent, FmPath* dirPath, QWidget* parent):
QMenu(parent), dialogParent_(dialogParent), dirPath_(dirPath) {
QAction* action = new QAction(QIcon::fromTheme("folder-new"), tr("Folder"), this);
connect(action, &QAction::triggered, this, &CreateNewMenu::onCreateNewFolder);
addAction(action);
CreateNewMenu::CreateNewMenu(QWidget* dialogParent, Fm::FilePath dirPath, QWidget* parent):
QMenu(parent), dialogParent_(dialogParent), dirPath_(std::move(dirPath)) {
QAction* action = new QAction(QIcon::fromTheme("folder-new"), tr("Folder"), this);
connect(action, &QAction::triggered, this, &CreateNewMenu::onCreateNewFolder);
addAction(action);
action = new QAction(QIcon::fromTheme("document-new"), tr("Blank File"), this);
connect(action, &QAction::triggered, this, &CreateNewMenu::onCreateNewFile);
addAction(action);
action = new QAction(QIcon::fromTheme("document-new"), tr("Blank File"), this);
connect(action, &QAction::triggered, this, &CreateNewMenu::onCreateNewFile);
addAction(action);
// add more items to "Create New" menu from templates
GList* templates = fm_template_list_all(fm_config->only_user_templates);
if(templates) {
addSeparator();
for(GList* l = templates; l; l = l->next) {
FmTemplate* templ = (FmTemplate*)l->data;
/* we support directories differently */
if(fm_template_is_directory(templ))
continue;
FmMimeType* mime_type = fm_template_get_mime_type(templ);
const char* label = fm_template_get_label(templ);
QString text = QString("%1 (%2)").arg(QString::fromUtf8(label)).arg(QString::fromUtf8(fm_mime_type_get_desc(mime_type)));
FmIcon* icon = fm_template_get_icon(templ);
if(!icon)
icon = fm_mime_type_get_icon(mime_type);
QAction* action = addAction(IconTheme::icon(icon), text);
action->setObjectName(QString::fromUtf8(fm_template_get_name(templ, NULL)));
connect(action, &QAction::triggered, this, &CreateNewMenu::onCreateNew);
// add more items to "Create New" menu from templates
GList* templates = fm_template_list_all(fm_config->only_user_templates);
if(templates) {
addSeparator();
for(GList* l = templates; l; l = l->next) {
FmTemplate* templ = (FmTemplate*)l->data;
/* we support directories differently */
if(fm_template_is_directory(templ)) {
continue;
}
FmMimeType* mime_type = fm_template_get_mime_type(templ);
const char* label = fm_template_get_label(templ);
QString text = QString("%1 (%2)").arg(QString::fromUtf8(label)).arg(QString::fromUtf8(fm_mime_type_get_desc(mime_type)));
FmIcon* icon = fm_template_get_icon(templ);
if(!icon) {
icon = fm_mime_type_get_icon(mime_type);
}
QAction* action = addAction(Fm::IconInfo::fromGIcon(G_ICON(icon))->qicon(), text);
action->setObjectName(QString::fromUtf8(fm_template_get_name(templ, nullptr)));
connect(action, &QAction::triggered, this, &CreateNewMenu::onCreateNew);
}
}
}
}
CreateNewMenu::~CreateNewMenu() {
}
void CreateNewMenu::onCreateNewFile() {
if(dirPath_)
createFileOrFolder(CreateNewTextFile, dirPath_);
if(dirPath_) {
createFileOrFolder(CreateNewTextFile, dirPath_);
}
}
void CreateNewMenu::onCreateNewFolder() {
if(dirPath_)
createFileOrFolder(CreateNewFolder, dirPath_);
if(dirPath_) {
createFileOrFolder(CreateNewFolder, dirPath_);
}
}
void CreateNewMenu::onCreateNew() {
QAction* action = static_cast<QAction*>(sender());
QByteArray name = action->objectName().toUtf8();
GList* templates = fm_template_list_all(fm_config->only_user_templates);
FmTemplate* templ = NULL;
for(GList* l = templates; l; l = l->next) {
FmTemplate* t = (FmTemplate*)l->data;
if(name == fm_template_get_name(t, NULL)) {
templ = t;
break;
QAction* action = static_cast<QAction*>(sender());
QByteArray name = action->objectName().toUtf8();
GList* templates = fm_template_list_all(fm_config->only_user_templates);
FmTemplate* templ = nullptr;
for(GList* l = templates; l; l = l->next) {
FmTemplate* t = (FmTemplate*)l->data;
if(name == fm_template_get_name(t, nullptr)) {
templ = t;
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

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

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

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

Loading…
Cancel
Save