From 18aae1b51c4ce360cf7fa35af019639043628156 Mon Sep 17 00:00:00 2001 From: Alf Gaida Date: Sun, 22 Oct 2017 15:34:45 +0100 Subject: [PATCH] Import libfm-qt_0.12.0.orig.tar.xz [dgit import orig libfm-qt_0.12.0.orig.tar.xz] --- AUTHORS | 6 + CHANGELOG | 591 +++++++ CMakeLists.txt | 89 + Doxyfile.in | 1890 +++++++++++++++++++++ LICENSE | 458 +++++ README.md | 27 + cmake/fm-qt-config.cmake.in | 41 + data/CMakeLists.txt | 10 + data/archivers.list | 35 + data/libfm-qt-mimetypes.xml | 52 + data/terminals.list | 77 + src/CMakeLists.txt | 269 +++ src/app-chooser-dialog.ui | 183 ++ src/appchoosercombobox.cpp | 142 ++ src/appchoosercombobox.h | 65 + src/appchooserdialog.cpp | 286 ++++ src/appchooserdialog.h | 79 + src/applaunchcontext.cpp | 61 + src/applaunchcontext.h | 50 + src/appmenuview.cpp | 162 ++ src/appmenuview.h | 74 + src/appmenuview_p.h | 72 + src/archiver.h | 143 ++ src/bookmarkaction.cpp | 32 + src/bookmarkaction.h | 49 + src/browsehistory.cpp | 93 + src/browsehistory.h | 132 ++ src/cachedfoldermodel.cpp | 67 + src/cachedfoldermodel.h | 55 + src/colorbutton.cpp | 53 + src/colorbutton.h | 55 + src/core/bookmarks.cpp | 162 ++ src/core/bookmarks.h | 96 ++ src/core/compat_p.h | 53 + src/core/copyjob.cpp | 453 +++++ src/core/copyjob.h | 46 + src/core/cstrptr.h | 42 + src/core/deletejob.cpp | 151 ++ src/core/deletejob.h | 36 + src/core/dirlistjob.cpp | 178 ++ src/core/dirlistjob.h | 65 + src/core/filechangeattrjob.cpp | 9 + src/core/filechangeattrjob.h | 17 + src/core/fileinfo.cpp | 378 +++++ src/core/fileinfo.h | 278 +++ src/core/fileinfo_p.h | 10 + src/core/fileinfojob.cpp | 42 + src/core/fileinfojob.h | 41 + src/core/filelinkjob.cpp | 9 + src/core/filelinkjob.h | 16 + src/core/filemonitor.cpp | 9 + src/core/filemonitor.h | 26 + src/core/fileoperationjob.cpp | 79 + src/core/fileoperationjob.h | 73 + src/core/filepath.cpp | 21 + src/core/filepath.h | 177 ++ src/core/filesysteminfojob.cpp | 24 + src/core/filesysteminfojob.h | 45 + src/core/folder.cpp | 908 ++++++++++ src/core/folder.h | 196 +++ src/core/gioptrs.h | 137 ++ src/core/gobjectptr.h | 104 ++ src/core/iconinfo.cpp | 138 ++ src/core/iconinfo.h | 112 ++ src/core/iconinfo_p.h | 126 ++ src/core/job.cpp | 57 + src/core/job.h | 120 ++ src/core/job_p.h | 26 + src/core/mimetype.cpp | 64 + src/core/mimetype.h | 172 ++ src/core/terminal.cpp | 127 ++ src/core/terminal.h | 17 + src/core/thumbnailer.cpp | 140 ++ src/core/thumbnailer.h | 37 + src/core/thumbnailjob.cpp | 266 +++ src/core/thumbnailjob.h | 89 + src/core/totalsizejob.cpp | 144 ++ src/core/totalsizejob.h | 56 + src/core/trashjob.cpp | 73 + src/core/trashjob.h | 31 + src/core/untrashjob.cpp | 132 ++ src/core/untrashjob.h | 22 + src/core/userinfocache.cpp | 47 + src/core/userinfocache.h | 82 + src/core/volumemanager.cpp | 111 ++ src/core/volumemanager.h | 237 +++ src/createnewmenu.cpp | 96 ++ src/createnewmenu.h | 52 + src/customaction_p.h | 52 + src/customactions/fileaction.cpp | 615 +++++++ src/customactions/fileaction.h | 156 ++ src/customactions/fileactioncondition.cpp | 503 ++++++ src/customactions/fileactioncondition.h | 123 ++ src/customactions/fileactionprofile.cpp | 121 ++ src/customactions/fileactionprofile.h | 45 + src/dirtreemodel.cpp | 233 +++ src/dirtreemodel.h | 100 ++ src/dirtreemodelitem.cpp | 417 +++++ src/dirtreemodelitem.h | 98 ++ src/dirtreeview.cpp | 340 ++++ src/dirtreeview.h | 94 + src/dndactionmenu.cpp | 67 + src/dndactionmenu.h | 47 + src/dnddest.cpp | 71 + src/dnddest.h | 52 + src/edit-bookmarks.ui | 143 ++ src/editbookmarksdialog.cpp | 112 ++ src/editbookmarksdialog.h | 53 + src/exec-file.ui | 163 ++ src/execfiledialog.cpp | 74 + src/execfiledialog_p.h | 53 + src/file-operation-dialog.ui | 185 ++ src/file-props.ui | 736 ++++++++ src/filedialog.cpp | 967 +++++++++++ src/filedialog.h | 213 +++ src/filedialog.ui | 151 ++ src/filelauncher.cpp | 145 ++ src/filelauncher.h | 84 + src/filemenu.cpp | 418 +++++ src/filemenu.h | 212 +++ src/filemenu_p.h | 57 + src/fileoperation.cpp | 318 ++++ src/fileoperation.h | 163 ++ src/fileoperationdialog.cpp | 194 +++ src/fileoperationdialog.h | 68 + src/filepropsdialog.cpp | 560 ++++++ src/filepropsdialog.h | 100 ++ src/filesearch.ui | 574 +++++++ src/filesearchdialog.cpp | 200 +++ src/filesearchdialog.h | 73 + src/fm-search.c | 317 ++++ src/fm-search.h | 89 + src/folderconfig.h | 201 +++ src/folderitemdelegate.cpp | 431 +++++ src/folderitemdelegate.h | 124 ++ src/foldermenu.cpp | 302 ++++ src/foldermenu.h | 132 ++ src/foldermodel.cpp | 546 ++++++ src/foldermodel.h | 145 ++ src/foldermodelitem.cpp | 149 ++ src/foldermodelitem.h | 92 + src/folderview.cpp | 1242 ++++++++++++++ src/folderview.h | 188 ++ src/folderview_p.h | 118 ++ src/fontbutton.cpp | 58 + src/fontbutton.h | 54 + src/icontheme.cpp | 80 + src/icontheme.h | 53 + src/libfm-qt.pc.in | 12 + src/libfmqt.cpp | 95 ++ src/libfmqt.h | 47 + src/libfmqtglobals.h | 25 + src/mount-operation-password.ui | 215 +++ src/mountoperation.cpp | 232 +++ src/mountoperation.h | 157 ++ src/mountoperationpassworddialog.cpp | 138 ++ src/mountoperationpassworddialog_p.h | 64 + src/mountoperationquestiondialog.cpp | 68 + src/mountoperationquestiondialog_p.h | 50 + src/path.h | 441 +++++ src/pathbar.cpp | 350 ++++ src/pathbar.h | 89 + src/pathbar_p.h | 78 + src/pathedit.cpp | 245 +++ src/pathedit.h | 65 + src/pathedit_p.h | 55 + src/placesmodel.cpp | 612 +++++++ src/placesmodel.h | 147 ++ src/placesmodelitem.cpp | 153 ++ src/placesmodelitem.h | 134 ++ src/placesview.cpp | 434 +++++ src/placesview.h | 108 ++ src/proxyfoldermodel.cpp | 303 ++++ src/proxyfoldermodel.h | 114 ++ src/rename-dialog.ui | 204 +++ src/renamedialog.cpp | 140 ++ src/renamedialog.h | 83 + src/sidepane.cpp | 211 +++ src/sidepane.h | 129 ++ src/templates.h | 158 ++ src/tests/test-filedialog.cpp | 48 + src/tests/test-folder.cpp | 44 + src/tests/test-folderview.cpp | 48 + src/tests/test-placesview.cpp | 20 + src/tests/test-volumemanager.cpp | 24 + src/utilities.cpp | 367 ++++ src/utilities.h | 88 + src/utilities_p.h | 79 + src/utils.h | 35 + src/xdndworkaround.cpp | 292 ++++ src/xdndworkaround.h | 89 + 191 files changed, 31814 insertions(+) create mode 100644 AUTHORS create mode 100644 CHANGELOG create mode 100644 CMakeLists.txt create mode 100644 Doxyfile.in create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cmake/fm-qt-config.cmake.in create mode 100644 data/CMakeLists.txt create mode 100644 data/archivers.list create mode 100644 data/libfm-qt-mimetypes.xml create mode 100644 data/terminals.list create mode 100644 src/CMakeLists.txt create mode 100644 src/app-chooser-dialog.ui create mode 100644 src/appchoosercombobox.cpp create mode 100644 src/appchoosercombobox.h create mode 100644 src/appchooserdialog.cpp create mode 100644 src/appchooserdialog.h create mode 100644 src/applaunchcontext.cpp create mode 100644 src/applaunchcontext.h create mode 100644 src/appmenuview.cpp create mode 100644 src/appmenuview.h create mode 100644 src/appmenuview_p.h create mode 100644 src/archiver.h create mode 100644 src/bookmarkaction.cpp create mode 100644 src/bookmarkaction.h create mode 100644 src/browsehistory.cpp create mode 100644 src/browsehistory.h create mode 100644 src/cachedfoldermodel.cpp create mode 100644 src/cachedfoldermodel.h create mode 100644 src/colorbutton.cpp create mode 100644 src/colorbutton.h create mode 100644 src/core/bookmarks.cpp create mode 100644 src/core/bookmarks.h create mode 100644 src/core/compat_p.h create mode 100644 src/core/copyjob.cpp create mode 100644 src/core/copyjob.h create mode 100644 src/core/cstrptr.h create mode 100644 src/core/deletejob.cpp create mode 100644 src/core/deletejob.h create mode 100644 src/core/dirlistjob.cpp create mode 100644 src/core/dirlistjob.h create mode 100644 src/core/filechangeattrjob.cpp create mode 100644 src/core/filechangeattrjob.h create mode 100644 src/core/fileinfo.cpp create mode 100644 src/core/fileinfo.h create mode 100644 src/core/fileinfo_p.h create mode 100644 src/core/fileinfojob.cpp create mode 100644 src/core/fileinfojob.h create mode 100644 src/core/filelinkjob.cpp create mode 100644 src/core/filelinkjob.h create mode 100644 src/core/filemonitor.cpp create mode 100644 src/core/filemonitor.h create mode 100644 src/core/fileoperationjob.cpp create mode 100644 src/core/fileoperationjob.h create mode 100644 src/core/filepath.cpp create mode 100644 src/core/filepath.h create mode 100644 src/core/filesysteminfojob.cpp create mode 100644 src/core/filesysteminfojob.h create mode 100644 src/core/folder.cpp create mode 100644 src/core/folder.h create mode 100644 src/core/gioptrs.h create mode 100644 src/core/gobjectptr.h create mode 100644 src/core/iconinfo.cpp create mode 100644 src/core/iconinfo.h create mode 100644 src/core/iconinfo_p.h create mode 100644 src/core/job.cpp create mode 100644 src/core/job.h create mode 100644 src/core/job_p.h create mode 100644 src/core/mimetype.cpp create mode 100644 src/core/mimetype.h create mode 100644 src/core/terminal.cpp create mode 100644 src/core/terminal.h create mode 100644 src/core/thumbnailer.cpp create mode 100644 src/core/thumbnailer.h create mode 100644 src/core/thumbnailjob.cpp create mode 100644 src/core/thumbnailjob.h create mode 100644 src/core/totalsizejob.cpp create mode 100644 src/core/totalsizejob.h create mode 100644 src/core/trashjob.cpp create mode 100644 src/core/trashjob.h create mode 100644 src/core/untrashjob.cpp create mode 100644 src/core/untrashjob.h create mode 100644 src/core/userinfocache.cpp create mode 100644 src/core/userinfocache.h create mode 100644 src/core/volumemanager.cpp create mode 100644 src/core/volumemanager.h create mode 100644 src/createnewmenu.cpp create mode 100644 src/createnewmenu.h create mode 100644 src/customaction_p.h create mode 100644 src/customactions/fileaction.cpp create mode 100644 src/customactions/fileaction.h create mode 100644 src/customactions/fileactioncondition.cpp create mode 100644 src/customactions/fileactioncondition.h create mode 100644 src/customactions/fileactionprofile.cpp create mode 100644 src/customactions/fileactionprofile.h create mode 100644 src/dirtreemodel.cpp create mode 100644 src/dirtreemodel.h create mode 100644 src/dirtreemodelitem.cpp create mode 100644 src/dirtreemodelitem.h create mode 100644 src/dirtreeview.cpp create mode 100644 src/dirtreeview.h create mode 100644 src/dndactionmenu.cpp create mode 100644 src/dndactionmenu.h create mode 100644 src/dnddest.cpp create mode 100644 src/dnddest.h create mode 100644 src/edit-bookmarks.ui create mode 100644 src/editbookmarksdialog.cpp create mode 100644 src/editbookmarksdialog.h create mode 100644 src/exec-file.ui create mode 100644 src/execfiledialog.cpp create mode 100644 src/execfiledialog_p.h create mode 100644 src/file-operation-dialog.ui create mode 100644 src/file-props.ui create mode 100644 src/filedialog.cpp create mode 100644 src/filedialog.h create mode 100644 src/filedialog.ui create mode 100644 src/filelauncher.cpp create mode 100644 src/filelauncher.h create mode 100644 src/filemenu.cpp create mode 100644 src/filemenu.h create mode 100644 src/filemenu_p.h create mode 100644 src/fileoperation.cpp create mode 100644 src/fileoperation.h create mode 100644 src/fileoperationdialog.cpp create mode 100644 src/fileoperationdialog.h create mode 100644 src/filepropsdialog.cpp create mode 100644 src/filepropsdialog.h create mode 100644 src/filesearch.ui create mode 100644 src/filesearchdialog.cpp create mode 100644 src/filesearchdialog.h create mode 100644 src/fm-search.c create mode 100644 src/fm-search.h create mode 100644 src/folderconfig.h create mode 100644 src/folderitemdelegate.cpp create mode 100644 src/folderitemdelegate.h create mode 100644 src/foldermenu.cpp create mode 100644 src/foldermenu.h create mode 100644 src/foldermodel.cpp create mode 100644 src/foldermodel.h create mode 100644 src/foldermodelitem.cpp create mode 100644 src/foldermodelitem.h create mode 100644 src/folderview.cpp create mode 100644 src/folderview.h create mode 100644 src/folderview_p.h create mode 100644 src/fontbutton.cpp create mode 100644 src/fontbutton.h create mode 100644 src/icontheme.cpp create mode 100644 src/icontheme.h create mode 100644 src/libfm-qt.pc.in create mode 100644 src/libfmqt.cpp create mode 100644 src/libfmqt.h create mode 100644 src/libfmqtglobals.h create mode 100644 src/mount-operation-password.ui create mode 100644 src/mountoperation.cpp create mode 100644 src/mountoperation.h create mode 100644 src/mountoperationpassworddialog.cpp create mode 100644 src/mountoperationpassworddialog_p.h create mode 100644 src/mountoperationquestiondialog.cpp create mode 100644 src/mountoperationquestiondialog_p.h create mode 100644 src/path.h create mode 100644 src/pathbar.cpp create mode 100644 src/pathbar.h create mode 100644 src/pathbar_p.h create mode 100644 src/pathedit.cpp create mode 100644 src/pathedit.h create mode 100644 src/pathedit_p.h create mode 100644 src/placesmodel.cpp create mode 100644 src/placesmodel.h create mode 100644 src/placesmodelitem.cpp create mode 100644 src/placesmodelitem.h create mode 100644 src/placesview.cpp create mode 100644 src/placesview.h create mode 100644 src/proxyfoldermodel.cpp create mode 100644 src/proxyfoldermodel.h create mode 100644 src/rename-dialog.ui create mode 100644 src/renamedialog.cpp create mode 100644 src/renamedialog.h create mode 100644 src/sidepane.cpp create mode 100644 src/sidepane.h create mode 100644 src/templates.h create mode 100644 src/tests/test-filedialog.cpp create mode 100644 src/tests/test-folder.cpp create mode 100644 src/tests/test-folderview.cpp create mode 100644 src/tests/test-placesview.cpp create mode 100644 src/tests/test-volumemanager.cpp create mode 100644 src/utilities.cpp create mode 100644 src/utilities.h create mode 100644 src/utilities_p.h create mode 100644 src/utils.h create mode 100644 src/xdndworkaround.cpp create mode 100644 src/xdndworkaround.h diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..40631a6 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,6 @@ +Upstream Authors: + LXQt team: http://lxqt.org + Hong Jen Yee (PCMan) + +Copyright: + Copyright (c) 2013-2017 LXQt team diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..55392c4 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,591 @@ + +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) + * Updates lxqt-build-tools required version + * Bump ABI so version numbers preparing for a new release. + * Fix Pathbar Paint on Menu Pop-Up + * Code cleanup and refactor for Fm::PathBar. + * Added another condition + * Added a missing condition (typo) + * Scroll Pathbar with Mouse Wheel + * Reduct flickering of the path bar when creating path buttons. + * Code simplification by removing unnecessary signal/slot usage. + * Path Button Middle Click + * Enable auto-repeat for pathbar scroll buttons. + * Make the path bar buttons aware of style changes. + * Use widget style instead of app style + * Align Path Buttons + * Move FindXCB.cmake to lxqt-build-tools + * Adds superbuild/intree support + * Removes not needed dependency check + * Set CMP0024 policy usage to NEW + * Updates target_include_directories() stuff + * Drops GLib library detection + * Use the new FindMenuCache CMake module + * Use the new FindFm CMake module + * Check for FolderModelItem info (and FmPath) + * Add Fm::PathBar::editingFinished() signal. + * Select the current path when editing the path bar. + * Enable path editing and popup menu for the button-style path bar. + * Properly set styles of path buttons. + * Remove unnecessary debug messages. + * Try to implement the Fm::PathBar which shows a filesystem path as buttons. + * Adds Build PROJECT_NAME with Qt version message + * Move LIBFM_DATA_DIR to pcmanfm repo. + * Refactors CUSTOM_ACTIONS compile definition + * Refactors LIBFM_DATA_DIR compile definition + * Drop add_definitions. Use target_compile_definitions. + * Removes duplicated symbols visibility settings + * README.md: Add build dependency lxqt-build-tools + * Use the new lxqt-build-tools package + * Restore symlink emblem + * Remove empty files + * Try to refactor the emblemed icon support in a more generalized way. Reuse FolderItemDelegate to paint the emblemed icons in Fm::PlacesView to prevent code duplication. APIs changes: * Add Fm::IconTheme::emblems() and cache emblem info in the cache. * Add Fm::FolderItemDelegate::setFileInfoRole() and Fm::FolderItemDelegate::setFmIconRole() * Cache multiple emblems rather than getting the first one only (but only paint the first one now). * Remove icon sizes from Fm::PlacesModel and Fm::PlacesModelItems to maintain MVC design pattern and backward incompatibility. * Expose two role IDs in Fm::PlacesModel: FileInfoRole and FmIconRole so the views can access these data. + * Show File Emblems + * Emblem For (Encrypted) Volume Icons + * Remove cpack (#44) + * Also Consider GEmblemedIcon (#41) + +0.11.1 / 2016-09-24 +=================== + + * Release 0.11.1: Add changelog + * Bump version to 0.11.1 (#39) + * Fix Custom Actions Submenu (#38) + * Extend README.md + * Add C++ wrappers for libfm C APIs. + * Correct positioning of small icons in icon/thumbnail mode (#32) + * Silence new compiler warnings (#36) + * Adapt to QT_USE_QSTRINGBUILDER macro + * Fixes xcb linking error + * Use LXQtCompilerSettings cmake module + * Replaces deprecated QStyleOptionViewItemV4 + * Fix item text direction with RTL layout (#33) + * Set tabstops for mount operation password dialog (#31) + * Fix memory leak in file open menu (#29) + * Fixes https://github.com/lxde/pcmanfm-qt/issues/351. (#27) + * build: Use external translations + * ts-file removal (#26) + * Fix memory leaks in file menu (#25) + * Merge pull request #24 from philippwiesemann/fix-folder-menu-leak + * translations: fix four typos in messages (#23) + * Fix memory leak if restoring files from trash + * Fix rename dialog displaying source info twice + * Enable renaming in Properties dialog Fixes https://github.com/lxde/pcmanfm-qt/issues/324. + * Cancel indexing job when closing filepropsdialog + * Bump year + +0.11.0 / 2016-02-27 +=================== + + * Bump version number and ABI version and preparing for the initial release. + * Redirect focus operation on the folderview to the actual widget + * Use grid layout to have proper properties alignment + * Focus automatically on the first field of the filesearch dialog + * Add (folder) custom actions to foldermenu. + * Update czech translation (by Petr Balíček ) + * Fix compiling with Qt < 5.4 + * Add supports for freedesktop Xdnd direct save (XDS) protocol (closes #pcmanfm-qt/298). * Move Xdnd workarounds from pcmanfm-qt to libfm-qt. + * Protected FolderView methods for getting and setting item delegate margins + * Perform auto-completion for the path bar when the user press Tab key. This closes pcmanfm-qt/#201. + * Disable unused options in file search dialog + * Let the style engine draw the text selection rectangle. + * Fix file search with smaller than option + * Fix target language in Lithuanian translation file + * Fix memory leak if reading stream for image failed + * Update German translation + * Polish translation updated + * Polish translation updated + * Add a missing type casting to fix a compiler warning. + * Relicense libfm-qt to LGPL v.2.1. + * Avoid using blocking calls when initializing the trash can to speed up startup. + * Updated Russian translation Removed ru_RU file + * Create DnD menu with supported actions only + * make createAction public so can be hidden from view + * Adds Runtime and Devel install COMPONENT + * Quote everything that could break due to whitespaces + * Use CMake GenerateExportHeader to generate ABI header file + * Use no exceptions also with Clang + * Uses CMAKE_VISIBILITY and CMAKE_CXX_VISIBILITY flags + * Use QString() instead of "" + * Fix duplicated mtp mounts in the side pane by adding workarounds for potential bugs of gvfs. + * Replace g_io_scheduler which is deprecated with QThread in Fm::PathEdit. * Fix compiler warnings. + * Adds .gitignore + * Makes Release the default BUILD_TYPE + * Adds INSTALL package configuration file + * Removes XCB from the dependencies + * Adds target_include_directories() for the INSTALL_INTERFACE. + * Removes CMAKE_CURRENT_BINARY_DIR usage + * Removes QTX_INCLUDE_DIRS + * Removes Qt Creator .user file + * Updates libraries dependencies + * Adds REQUIRED_PACKAGE_VERSION variables + * Adds generation of TARGETS CMake file + * Creates and installs an package version file + * Renames and moves LIBRARY_NAME variable to the top CMakeLists + * Removes sub-project + * Rename the library to libfm-qt and fix installed header files. + * Split pcmanfm-qt into two parts and move libfm-qt to its own repository. + * Update French translation + * Italian translation updates + * Fix a crash triggered when unmounting a volume from the side pane. + * Avoid duplicated volumes and mounts in the side panel. (This fixes the mtp:// problem.) + * Fix missing null pointer check in Fm::Path::parent() and use nullptr instead of NULL in path.h. + * Code cleanup, «using» directive only if necessary + * Upgrade of pcmanfm-qt to C++11 + * hu translations updated + * Fixed several problems with item selection and alignment + * Remove unnecessary qDebug traces + * Update translations. + * The signal QAbstractItemView::iconSizeChanged is only available after Qt 5.5. Add workarounds for older Qt versions. + * Add more null pointer checks in the thumbnail loader to avoid crashes caused by older versions of libfm. + * Update translations + * Avoid the column resizing tricks for the detailed list view when the view contains no columns. + * Improve the column width computation for the detailed view + * PlacesView: activate on click on the second column + * SidePane: reduce size of button's column width + * Added a filterbar + Handle virtually hidden files + * Russian translation update + * Update cs_CZ translation with the strings provided by petrbal in pull request #218. + * Allow adding or removing paths in the file search dialog. Fix bugs in searching for documents. + * Try to implement file searching by using the new Fm::FileSearchDialog class. + * Fix a incorrecy free() in fm_search_free() API. + * Add Fm::Path::take() API to take the ownership of a raw FmPath pointer. + * Add class Fm::FileSearchDialog used to show a dialog for searching files. + * Add FmSearch API which is used to build a search:// URI. (implemented in C and might become part of libfm later). + * Fix #195 - Crash when rightclick on item in trash. + * Add a null check for FmFileInfo in Fm::ProxyFolderModel::lessThan(). This closes #205. + * Fix (workaround) for right-click crash in placesview. + * Russian translation: update + * Italian translation: add desktop entry files, adjust TS files + * placesview: middle-click correct item to activate (fix of segfault) + * Check for null pointers. + * Select the folder from where we have gone up. + * Paste into folder from its context menu. + * libfm-qt: updated german translation + * libfm-qt: lupdated translation files + * Add Greek (el) translation + * added support for mouse back/forward buttons + * Update German translation + * Add new signal prepareFileMenu() to Fm::SidePane and Fm::DirTree so there's a chance to customize the file menu before its shown. + * Port some missing config options from the gtk+ version of pcmanfm. + * Also show hidden dirs in the directory tree when the "Show Hidden" option in the menu is turned on. + * Fix #190 - Column layout is not always updated. + * Create New menu actions, context menu in tree side pane, #163. + * Store side pane mode setting, #157. + * Fixes an translation regression + * Updates translations + * Uses LXQt lxqt_translate_ts() to handle translations + * Fix lxde/lxqt#447 - missing actions in Places' context menus + * Remove trailing whitespaces + * polishing German translation + * Add menu items and shortcuts for new folder and blank file, fixes #163. + * Display folders first when active and sort order descending, fixes #179. + * Avoid space wasted by incorrect decoration in detailed view columns, fixes #177. + * Avoid flickering column header while resizing manually, fixes #176. + * Hungarian translation + * Fix #627 - long startup time. (This blocking is caused by checking the availability of "network:///".) + * Enable text selection in file properties dialog + * Fix some memory leaks reported by valgrind. + * Fix warnings reported by cppcheck. + * Fix warnings reported by scan-build. + * Sort indicators in detailed view, store column and order in settings, fixes #109. + * Fix lxde/lxqt#512 - pcmanfm-qt: cannot delete to trash. + * Polish translations added + * Use 'user-trash' icon for 'Move to Trash' + * The "Custom" option in the application chooser combo box inside file properties dialog is broken. Fix by preventing recursive signal handler invocation. + * The file property dialog does not show correct default applications. Fix a bug in AppChooserComboBox::setMimeType() that does incorrect app comparison. + * When converting an UID or GID to its name, show the number as string when the user or group does not exists. + * Add more null checks. + * Portuguese update + * Add very basic "remaining time" display to the progress dialog. Fix lxde/lxqt#463 - Progress dialog of pcmanfm-qt does not show remaining time. + * Fix lxde/pcmanfm-qt#120 - Foucs "Rename" button when file name changed. + * Remove unnecessary '\n' charactor from the translated strings. + * Fix translations (the newly added string comes from the translation of libfm). + * Improve trash can handling: provide an option to delete the files if moving to trashcan fails. + * Fix broken filenames of translation files. + * More migration to Qt5 new signal/slot syntax for better type safety & speed. + * Migrade to new Qt5 signal/slot syntax for better type safety and speed. + * Fix the broken sorting option. + * Fix lxde/lxqt#448 - PCmanFM-QT renaming place bookmarks does nothing. + * Support linguistic sorting of file names. This fixes #105. + * Update the folder model & view after files are changed. + * Open folders in new tabs by middle clicking on items in the side pane. + * Portuguese update + * Fix a crash of the context menu of places view caused by change of items. + * Save the result of "Edit bookmarks" to gtk+3 bookmarks file instead of the deprecated ~/.gtkbookmarks file. This fixes bug #112 partially. + * Add spanish translations + * Update Japanese translation + * Add German translation + * add Japanese translation + * Implement "UnTrash" for files in trash:/// and close lxde/lxqt#136. + * Add Russian translation + * Drop Qt4 support in code + * Clean up CMakeLists.txt and drop Qt4 support + * New files added from LXDE Pootle server based on templates + * Commit from LXDE Pootle server by user Julius22.: 1007 of 1008 strings translated (2 need review). + * Commit from LXDE Pootle server by user Julius22.: 709 of 1008 strings translated (2 need review). + * Commit from LXDE Pootle server by user mbouzada.: 364 of 364 strings translated (0 need review). + * New files added from LXDE Pootle server based on templates + * Add cs_CZ translation for libfm-qt. + * Commit from LXDE Pootle server by user dforsi.: 364 of 364 strings translated (0 need review). + * Commit from LXDE Pootle server by user dforsi.: 358 of 364 strings translated (0 need review). + * Bump package version number and library soversion to prepare for 0.2 release. + * Fix #85 - Scrolling doesn't work in compact view. + * Hide UI elements that are not usable and disable trash can when gvfs is not available. * Add new API Fm::isUriSchemeSupported(). + * Avoid showing the popup menu when moving desktop items. + * Improve handling of file selection and fixing FolderView::selectAll(), which is reported in #45. Delay the handling of selectionChanged() signal to avoid too frequent UI updates. + * Little adjustment for the grid of the folder view to decrease unnecessary margins. + * Use a new way to optimize the size of filename display based on current view mode and font size. This also fixes lxde/lxde-qt #198 - PCmanFM-qt incorrectly displays 256x256 Thumbnails. + * Fully support single click activation and auto-selection with associated options added to the preference dialog. + * Add single click and auto-selection on hover support to Fm::FolderView. + * New files added from LXDE Pootle server based on templates + * New files added from LXDE Pootle server based on templates + * Improve update of translations to avoid unnecessary regeneration of ts files. + * Improve handling of fallback icons. This closes bug #57. + * Translations are lost accidentally in a previous commit. Restore them all. + * Fix a crash in Fm::PlacesModel when gvfs is not available. This closes bug #35 - Ctrl+W closes all windows. + * Do not detect filename extension and select the whole filename by default while renaming directories. This closes bug #71 - Don't try to detect extensions on directories. * API changed: Fm::renameFile() now accepect FmFileInfo as its first parameter. + * Fix bug #80 - make execute in context menu doesn't do change permissions. + * Revert "fixed selection issue #45" This patch breaks copying files by DND in icon view mode and moving desktop icons. + * Use qss instead of QPalette to set the background color of ColorButton. This fixed bug #192 of lxde-qt. + * Rename the library from libfm-qt to libfm-qt5 when built with Qt5. + * fixed selection issue #45 + * Fix middle click position calculation in detailed view mode + * Fix crash when context menu is requested but selection is empty + * Activate view items only if clicked with left mouse button + * Do not emit activated signal when keyboard modifiers are on. + * Splits the checks for needed libraries + * Removes duplicated include_directories() entry + * Make sure clang compiler does not complain + * Install pkgconfig file of libfm-qt to correct location in FreeBSD + * Fix missing return values in several methods. + * Avoid endless popups of error dialogs when there are errors launching files. + * Save thumbnails as png files correctly. + * Remember custom positions for desktop icons and fix #29. + * Add template support to the folder context menus and fix #39. + * Show "owner" in the detailed list view mode. * Fix a crash when switching to detailed list mode in qt5. + * Use xcb to set EWMH window type hint to the desktop window in Qt5. * Some more cleanup for the CMakeList.txt files + * Add initial support for Qt5. + * Try to fix #36 again. + * Fix a seg fault caused by the widget being deleted during glib signal handling. + * Code cleanup, removing unnecessary header inclusion to speed up compilation. + * Avoid further handling of MountOperation in the gio finished callback if the object is deleted. + * Use modeless dialogs for app chooser and error reporting in Fm::FileLauncher and Fm::FileMenu. + * Add an small emblem for symlinks (using icon name "emblem-symbolic-link"). Fix bug #27. + * Add missing file to git. + * Move internal implementation details to private headers which are not installed to the system. + * Implement Fm::AppChooserDialog and Fm::AppMenuView classes. * Add / menu item to Fm::FileMenu. * Add custom app to Fm::AppChooserComboBox. + * Add Fm::AppChooserComboBox and use it in Fm::FilePropsDialog. + * Redesign Fm::FileLauncher APIs to make it more usable. * Add Fm::FileMenu::setFileLauncher() and Fm::FolderView::setFileLauncher() APIs. * Move PCManFM::View::onFileClick() and other popup menu handling to Fm::FolderView. + * Improve Fm::FileLaucher to make it easy to derive subclasses. * Implement a special dialog for opening executable files (Fix bug #13 - it does not launch executables) + * Fix bug #28 - Tash can icon does not refresh when the trash can changes its empty/full status + * Load autocompletion list for Fm::PathEdit only when the widget has the keyboard focus to avoid unnecessary I/O. + * Add proper popup menu items for selected folders and fix #20 and #19. * Some code refactors, adding openFolders() and openFolderInTerminal() to Application class. + * Fix #25 - Amount of items in the folder is not refreshed when the folder content changes. * Update status bar text properly on switching tab pages, selection changes, and folder content changes. + * Fix the broken compiler definitions caused by previous commit. + * Fix bug #22 - Do not select file extension by default on file rename. * Avoid installing private headers (*_p.h files) + * Setup pcmanfm-qt to support optional Custom Actions Menubar detects if libfm was built with vala or not if so a fm-actions.h will exist and support will be compiled in if not, will still compile with no actions menu + * Allow installation path configuration with standard CMake X_INSTALL_DIR + * Support reordering bookmark items in the places view with DND. + * Support adding bookmarks to the places view using drag and drop + * Preparing for implementing dnd for places view. + * Improve the usability of icon view mode, fixing github bug #24. + * Fix crashes caused by invalid pointer access. + * Switch current dir of the folder view correctly with dir tree view in the side pane. + * Finish chdir operation for Fm::DirTreeView. + * Support hiding hidden folders from DirTreeModel. + * Move some methods from DirTreeModel to DirTreeModelItem and fix some row updating problems. + * Implement dynamic folder loading/unloading when expanding or collapsing dir tree nodes. * Enable horizontal scrollbar of dir tree view. + * Move some code from Fm::DirTreeModel to Fm::DirTreeModelItem. + * Partially implement Fm::DirTreeView and Fm::DirTreeModel. (not finished yet) + * Fix an invalid pointer + * Implment different modes for Fm::SidePane, matching libfm-qtk design. * Add basic skeleton for dir tree view/model. + * Fix the cosmetic defect introduced by the eject buttons in the places view. + * Add eject buttons to mounted volumes in the places side pane. + * Add a wrapper class Fm::Path for FmPath C struct. + * Initialize icon_ member of PlacesModelItem correctly. + * Fix fallback icon when a platform plugin is abscent. * Make Fm::IconTheme::checkUpdate() a static function. + * Remove xsettings support. Use a cleaner way to detect config changes by monitor StyleChange events. * Add Fm::IconTheme::changed() signal which is emitted when the icon theme name is changed. * Replace nested Fm::PlacesModel::Item and related classes with their own separate toplevel classes. + * Fix the icon for files of unknown mime types, again. + * Fix the icon for files of unknown mime types. + * Add DES-EMA custom actions to the popup menus. + * Make it safe to create multiple Fm::LibFmQt objects and only initialize libfm once. + * Fix incorrect export symbols and use GNUInstallDirs for installation destination + * Use the latest libfm thumbnailer APIs. + * Fix #3614873 - Thumbnails in icon view shown upside down for some jpegs. + * Adopt recent changes of libfm. FmIcon is essentially identical to GIcon now. + * Add a UI file for application chooser dialog. + * Correctly handle display names of folders in path entry auto-completion. + * Add a global header and add proper definition for LIBFM_QT_API macro. + * Add "Empty trash" and fix a memory leak. + * Fix memory leaks for bookmarks. Fix the broken "Network" item in places. + * Reduce memory usage: Paint the folder items with our own code instead of using a dirty hacks duplicating pixmaps. + * Reduce of size of QPixmapCache in the hope of decreasing memory usage. + * Add fallback icons for places item "applications" and "network". + * Add class Fm::CachedFolderModel, a convinient way to share Fm::FolderModel objects and reduce memory usage. + * Resize the columns of detailed list view when items are inserted or removed. + * Optimize column widths in detailed list mode when the view is resized. + * Only show thumbnails for the first column in detailed list mode. + * Use new "automoc" feature of cmake 2.8.6 and remove cumbersome #include "*.moc" code. + * Trivial fix. + * Add additional custom filter support to ProxyFolderModel. + * Fix some memory leaks. + * Fix some compiler errors and update translations. + * Support the latest libfm trunk. Remove -fpermissive compiler flag and fix compiler errors/warnings. + * Adopt new libfm thumbnail APIs. + * Add soname 0.0.0 for libfm-qt, preparing for 0.1 release. + * Fix crashes caused by incorrect deletion of dialog objects. + * Enable thumbnail related settings. + * Update zh_TW translations and translation templates. + * Add Portuguese translation (pt). + * Add Lithuanian translation (lt_LT). + * Adopt the latest thumbnail API in libfm (thumbnail branch) to speed up loading. + * Workardound incorrect thumbnail painting caused by bug of QStyledItemDelegate. :-( + * Fix a crash caused by accessing data for invalid model index. + * Fix a crash caused by accessing data for invalid model index. + * Add basic thumbnail support (need the latest thumbnail branch of libfm). + * Add archiver integration for file context menus. + * Add archiver integration to file context menus. + * Add mnemonics for menu items. Make confirm dialog before delete and trash can optional. + * Update side pane according to current dir. Little fix. + * Implement "Open in Terminal" and "Open as Root". + * Implement "Auto Run" for newly inserted removable devices. + * Add "Edit Bookmarks" dialog. + * Implement "Invert Selection". Little fix of UI, add a Tool menu to main window. + * Implement "Create New" menu in the folder popup menu. + * Modify make rules for translations. Avoid deleting generated ts files when "make clean". Fix a small error in zh_TW translation. + * Add auto-completion to path entry bar. + * Rename Fm::Application to Fm::LibFmQt to decrease confusion. Set required Qt version to 4.6. + * Load translation files correctly for pcmanfm-qt and libfm-qt. + * Add basic skeleton for i18n (using Qt QTranslator & Qt Linguist). + * Add separate CMakeLists.txt files for pcmanfm and libfm-qt. Hide more implementation details from libfm-qt headers. + * Fix copyright notice in all source files. + * Install a pkgconfig file for libfm-qt for use in other projects. + * Fix a memory error caused by incorrect array size. Fix incorrect spacing of icons. + * Finish chown and chmod supports. + * Try to add file opermission settings UI. + * Implement very basic drag and drop support. + * Supress the incorrect default dnd handling of QListView. + * Try to implement Dnd. + * Finish desktop preferences. + * Improve desktop preferences and apply settings (partially done). + * Add desktop preferences dialog. + * Apply side pane icon size correctly. Add basic skeleton for archiver integration. + * Set shortcuts for frequently used menu options. Implement "rename file" support. Hide tabs when there is only one tab left (optional). + * Delete windows properly when they're closed with setAttribute(Qt::WA_DeleteOnClose); Apply settings to windows after clicking OK in the preference dialog. + * Improve preferences dialog. Change base class of SidePane to QWidget. + * Sync the state of folder popup menu and main menu bar. + * Implement sort options for main window. + * Fix file sorting options for Fm::FolderMenu. + * Correctly implement browse history and fix crashes. + * Add very simple browse history (back/forward) handling. + * Apply gcc visiblility attributes to export less symbols. + * Correctly handle file rename/overwrite during file operations. + * Exclude unnecessary files from CPack. + * Improve folder popup menu. + * Add folder popup menu. Some UI polishing. + * Fix a little crash. + * Fix crashes when turning off desktop manager. + * Show popup menu for blank area of folders. + * Do some refactor to make Fm::FolderView cleaner. Add PCManFM::View to do file manager-specific operations. + * Move files for libfm-qt and pcmanfm-qt to separate subdirs. diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..064bdd7 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,89 @@ +cmake_minimum_required(VERSION 3.0.2) +project(libfm-qt) + +set(LIBFM_QT_LIBRARY_NAME "fm-qt" CACHE STRING "fm-qt") + +set(LIBFM_QT_VERSION_MAJOR 0) +set(LIBFM_QT_VERSION_MINOR 12) +set(LIBFM_QT_VERSION_PATCH 0) +set(LIBFM_QT_VERSION ${LIBFM_QT_VERSION_MAJOR}.${LIBFM_QT_VERSION_MINOR}.${LIBFM_QT_VERSION_PATCH}) + +list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") + +# We use the libtool versioning scheme for the internal so name, "current:revision:age" +# http://www.gnu.org/software/libtool/manual/html_node/Updating-version-info.html#Updating-version-info +# https://www.sourceware.org/autobook/autobook/autobook_91.html +# http://pusling.com/blog/?p=352 +# Actually, libtool uses different ways on different operating systems. So there is no +# universal way to translate a libtool version-info to a cmake version. +# We use "(current-age).age.revision" as the cmake version. +# current: 4, revision: 0, age: 1 => version: 3.1.0 +set(LIBFM_QT_LIB_VERSION "3.1.0") +set(LIBFM_QT_LIB_SOVERSION "3") + +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.4.0") + +if (NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + +find_package(Qt5Widgets "${REQUIRED_QT_VERSION}" REQUIRED) +find_package(Qt5LinguistTools "${REQUIRED_QT_VERSION}" REQUIRED) +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}") + +option(UPDATE_TRANSLATIONS "Update source translation translations/*.ts files" OFF) +include(GNUInstallDirs) +include(GenerateExportHeader) +include(CMakePackageConfigHelpers) +include(LXQtTranslateTs) +include(LXQtTranslateDesktop) +include(LXQtCompilerSettings NO_POLICY_SCOPE) + +set(CMAKE_AUTOMOC TRUE) +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +write_basic_package_version_file( + "${CMAKE_BINARY_DIR}/${LIBFM_QT_LIBRARY_NAME}-config-version.cmake" + VERSION ${LIBFM_QT_LIB_VERSION} + COMPATIBILITY AnyNewerVersion +) + +install(FILES + "${CMAKE_BINARY_DIR}/${LIBFM_QT_LIBRARY_NAME}-config-version.cmake" + DESTINATION "${CMAKE_INSTALL_DATADIR}/cmake/${LIBFM_QT_LIBRARY_NAME}" + COMPONENT Devel +) + +add_subdirectory(src) +add_subdirectory(data) + +# add Doxygen support to generate API docs +# References: +# http://majewsky.wordpress.com/2010/08/14/tip-of-the-day-cmake-and-doxygen/ +# http://www.bluequartz.net/projects/EIM_Segmentation/SoftwareDocumentation/html/usewithcmakeproject.html +option(BUILD_DOCUMENTATION "Use Doxygen to create the HTML based API documentation" OFF) +if(BUILD_DOCUMENTATION) + find_package(Doxygen REQUIRED) + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in" "${CMAKE_CURRENT_BINARY_DIR}/Doxyfile" @ONLY) + add_custom_target(doc ALL + ${DOXYGEN_EXECUTABLE} "${CMAKE_CURRENT_BINARY_DIR}/Doxyfile" + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" + COMMENT "Generating API documentation with Doxygen" VERBATIM + ) + install(DIRECTORY + "${CMAKE_CURRENT_BINARY_DIR}/docs" + DESTINATION "${CMAKE_INSTALL_DOCDIR}" + COMPONENT Devel + ) +endif() diff --git a/Doxyfile.in b/Doxyfile.in new file mode 100644 index 0000000..b7961be --- /dev/null +++ b/Doxyfile.in @@ -0,0 +1,1890 @@ +# Doxyfile 1.8.4 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed +# in front of the TAG it is preceding . +# All text after a hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists items can also be appended using: +# TAG + = value [value, ...] +# Values that contain spaces should be placed between quotes (" "). + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the config file +# that follow. The default is UTF-8 which is also the encoding used for all +# text before the first occurrence of this tag. Doxygen uses libiconv (or the +# iconv built into libc) for the transcoding. See +# http://www.gnu.org/software/libiconv for the list of possible encodings. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or sequence of words) that should +# identify the project. Note that if you do not use Doxywizard you need +# to put quotes around the project name if it contains spaces. + +PROJECT_NAME = "libfm-qt" + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. +# This could be handy for archiving the generated documentation or +# if some version control system is used. + +PROJECT_NUMBER = + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer +# a quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = + +# With the PROJECT_LOGO tag one can specify an logo or icon that is +# included in the documentation. The maximum height of the logo should not +# exceed 55 pixels and the maximum width should not exceed 200 pixels. +# Doxygen will copy the logo to the output directory. + +PROJECT_LOGO = + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) +# base path where the generated documentation will be put. +# If a relative path is entered, it will be relative to the location +# where doxygen was started. If left blank the current directory will be used. + +OUTPUT_DIRECTORY = @PROJECT_BINARY_DIR@/docs + +# If the CREATE_SUBDIRS tag is set to YES, then doxygen will create +# 4096 sub-directories (in 2 levels) under the output directory of each output +# format and will distribute the generated files over these directories. +# Enabling this option can be useful when feeding doxygen a huge amount of +# source files, where putting all generated files in the same directory would +# otherwise cause performance problems for the file system. + +CREATE_SUBDIRS = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# The default language is English, other supported languages are: +# Afrikaans, Arabic, Brazilian, Catalan, Chinese, Chinese-Traditional, +# Croatian, Czech, Danish, Dutch, Esperanto, Farsi, Finnish, French, German, +# Greek, Hungarian, Italian, Japanese, Japanese-en (Japanese with English +# messages), Korean, Korean-en, Latvian, Lithuanian, Norwegian, Macedonian, +# Persian, Polish, Portuguese, Romanian, Russian, Serbian, Serbian-Cyrillic, +# Slovak, Slovene, Spanish, Swedish, Ukrainian, and Vietnamese. + +OUTPUT_LANGUAGE = English + +# If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will +# include brief member descriptions after the members that are listed in +# the file and class documentation (similar to JavaDoc). +# Set to NO to disable this. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend +# the brief description of a member or function before the detailed description. +# Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator +# that is used to form the text in various listings. Each string +# in this list, if found as the leading text of the brief description, will be +# stripped from the text and the result after processing the whole list, is +# used as the annotated text. Otherwise, the brief description is used as-is. +# If left blank, the following values are used ("$name" is automatically +# replaced with the name of the entity): "The $name class" "The $name widget" +# "The $name file" "is" "provides" "specifies" "contains" +# "represents" "a" "an" "the" + +ABBREVIATE_BRIEF = + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# Doxygen will generate a detailed section even if there is only a brief +# description. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full +# path before files name in the file list and in the header files. If set +# to NO the shortest path that makes the file name unique will be used. + +FULL_PATH_NAMES = NO + +# If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag +# can be used to strip a user-defined part of the path. Stripping is +# only done if one of the specified strings matches the left-hand part of +# the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the +# path to strip. Note that you specify absolute paths here, but also +# relative paths, which will be relative from the directory where doxygen is +# started. + +STRIP_FROM_PATH = + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of +# the path mentioned in the documentation of a class, which tells +# the reader which header file to include in order to use a class. +# If left blank only the name of the header file containing the class +# definition is used. Otherwise one should specify the include paths that +# are normally passed to the compiler using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter +# (but less readable) file names. This can be useful if your file system +# doesn't support long names like on DOS, Mac, or CD-ROM. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen +# will interpret the first line (until the first dot) of a JavaDoc-style +# comment as the brief description. If set to NO, the JavaDoc +# comments will behave just like regular Qt-style comments +# (thus requiring an explicit @brief command for a brief description.) + +JAVADOC_AUTOBRIEF = NO + +# If the QT_AUTOBRIEF tag is set to YES then Doxygen will +# interpret the first line (until the first dot) of a Qt-style +# comment as the brief description. If set to NO, the comments +# will behave just like regular Qt-style comments (thus requiring +# an explicit \brief command for a brief description.) + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen +# treat a multi-line C++ special comment block (i.e. a block of //! or /// +# comments) as a brief description. This used to be the default behaviour. +# The new default is to treat a multi-line C++ comment block as a detailed +# description. Set this tag to YES if you prefer the old behaviour instead. + +MULTILINE_CPP_IS_BRIEF = NO + +# If the INHERIT_DOCS tag is set to YES (the default) then an undocumented +# member inherits the documentation from any documented member that it +# re-implements. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce +# a new page for each member. If set to NO, the documentation of a member will +# be part of the file/class/namespace that contains it. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. +# Doxygen uses this value to replace tabs by spaces in code fragments. + +TAB_SIZE = 4 + +# This tag can be used to specify a number of aliases that acts +# as commands in the documentation. An alias has the form "name = value". +# For example adding "sideeffect = \par Side Effects:\n" will allow you to +# put the command \sideeffect (or @sideeffect) in the documentation, which +# will result in a user-defined paragraph with heading "Side Effects:". +# You can put \n's in the value part of an alias to insert newlines. + +ALIASES = + +# This tag can be used to specify a number of word-keyword mappings (TCL only). +# A mapping has the form "name = value". For example adding +# "class = itcl::class" will allow you to use the command class in the +# itcl::class meaning. + +TCL_SUBST = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C +# sources only. Doxygen will then generate output that is more tailored for C. +# For instance, some of the names that are used will be different. The list +# of all members will be omitted, etc. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java +# sources only. Doxygen will then generate output that is more tailored for +# Java. For instance, namespaces will be presented as packages, qualified +# scopes will look different, etc. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources only. Doxygen will then generate output that is more tailored for +# Fortran. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for +# VHDL. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given +# extension. Doxygen has a built-in mapping, but you can override or extend it +# using this tag. The format is ext = language, where ext is a file extension, +# and language is one of the parsers supported by doxygen: IDL, Java, +# Javascript, CSharp, C, C++, D, PHP, Objective-C, Python, Fortran, VHDL, C, +# C++. For instance to make doxygen treat .inc files as Fortran files (default +# is PHP), and .f files as C (default is Fortran), use: inc = Fortran f = C. Note +# that for custom extensions you also need to set FILE_PATTERNS otherwise the +# files are not read by doxygen. + +EXTENSION_MAPPING = + +# If MARKDOWN_SUPPORT is enabled (the default) then doxygen pre-processes all +# comments according to the Markdown format, which allows for more readable +# documentation. See http://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by doxygen, so you +# can mix doxygen, HTML, and XML commands with Markdown formatting. +# Disable only in case of backward compatibilities issues. + +MARKDOWN_SUPPORT = YES + +# When enabled doxygen tries to link words that correspond to documented +# classes, or namespaces to their corresponding documentation. Such a link can +# be prevented in individual cases by by putting a % sign in front of the word +# or globally by setting AUTOLINK_SUPPORT to NO. + +AUTOLINK_SUPPORT = YES + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should +# set this tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); v.s. +# func(std::string) {}). This also makes the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. + +BUILTIN_STL_SUPPORT = NO + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip sources only. +# Doxygen will parse them like normal C++ but will assume all classes use public +# instead of private inheritance when no explicit protection keyword is present. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate +# getter and setter methods for a property. Setting this option to YES (the +# default) will make doxygen replace the get and set methods by a property in +# the documentation. This will only work if the methods are indeed getting or +# setting a simple type. If this is not the case, or you want to show the +# methods anyway, you should set this option to NO. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES, then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. + +DISTRIBUTE_GROUP_DOC = NO + +# Set the SUBGROUPING tag to YES (the default) to allow class member groups of +# the same type (for instance a group of public functions) to be put as a +# subgroup of that type (e.g. under the Public Functions section). Set it to +# NO to prevent subgrouping. Alternatively, this can be done per class using +# the \nosubgrouping command. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and +# unions are shown inside the group in which they are included (e.g. using +# @ingroup) instead of on a separate page (for HTML and Man pages) or +# section (for LaTeX and RTF). + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and +# unions with only public data fields or simple typedef fields will be shown +# inline in the documentation of the scope in which they are defined (i.e. file, +# namespace, or group documentation), provided this scope is documented. If set +# to NO (the default), structs, classes, and unions are shown on a separate +# page (for HTML and Man pages) or section (for LaTeX and RTF). + +INLINE_SIMPLE_STRUCTS = NO + +# When TYPEDEF_HIDES_STRUCT is enabled, a typedef of a struct, union, or enum +# is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically +# be useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. + +TYPEDEF_HIDES_STRUCT = NO + +# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This +# cache is used to resolve symbols given their name and scope. Since this can +# be an expensive process and often the same symbol appear multiple times in +# the code, doxygen keeps a cache of pre-resolved symbols. If the cache is too +# small doxygen will become slower. If the cache is too large, memory is wasted. +# The cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid +# range is 0..9, the default is 0, corresponding to a cache size of 2^16 = 65536 +# symbols. + +LOOKUP_CACHE_SIZE = 0 + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in +# documentation are documented, even if no documentation was available. +# Private class members and static file members will be hidden unless +# the EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES + +EXTRACT_ALL = NO + +# If the EXTRACT_PRIVATE tag is set to YES all private members of a class +# will be included in the documentation. + +EXTRACT_PRIVATE = NO + +# If the EXTRACT_PACKAGE tag is set to YES all members with package or internal +# scope will be included in the documentation. + +EXTRACT_PACKAGE = NO + +# If the EXTRACT_STATIC tag is set to YES all static members of a file +# will be included in the documentation. + +EXTRACT_STATIC = NO + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) +# defined locally in source files will be included in the documentation. +# If set to NO only classes defined in header files are included. + +EXTRACT_LOCAL_CLASSES = YES + +# This flag is only useful for Objective-C code. When set to YES local +# methods, which are defined in the implementation section but not in +# the interface are included in the documentation. +# If set to NO (the default) only methods in the interface are included. + +EXTRACT_LOCAL_METHODS = NO + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base +# name of the file that contains the anonymous namespace. By default +# anonymous namespaces are hidden. + +EXTRACT_ANON_NSPACES = NO + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all +# undocumented members of documented classes, files or namespaces. +# If set to NO (the default) these members will be included in the +# various overviews, but no documentation section is generated. +# This option has no effect if EXTRACT_ALL is enabled. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. +# If set to NO (the default) these classes will be included in the various +# overviews. This option has no effect if EXTRACT_ALL is enabled. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all +# friend (class|struct|union) declarations. +# If set to NO (the default) these declarations will be included in the +# documentation. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any +# documentation blocks found inside the body of a function. +# If set to NO (the default) these blocks will be appended to the +# function's detailed documentation block. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation +# that is typed after a \internal command is included. If the tag is set +# to NO (the default) then the documentation will be excluded. +# Set it to YES to include the internal documentation. + +INTERNAL_DOCS = NO + +# If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate +# file names in lower-case letters. If set to YES upper-case letters are also +# allowed. This is useful if you have classes or files whose names only differ +# in case and if your file system supports case sensitive file names. Windows +# and Mac users are advised to set this option to NO. + +CASE_SENSE_NAMES = YES + +# If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen +# will show members with their full class and namespace scopes in the +# documentation. If set to YES the scope will be hidden. + +HIDE_SCOPE_NAMES = NO + +# If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen +# will put a list of the files that are included by a file in the documentation +# of that file. + +SHOW_INCLUDE_FILES = YES + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then Doxygen +# will list include files with double quotes in the documentation +# rather than with sharp brackets. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES (the default) then a tag [inline] +# is inserted in the documentation for inline members. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen +# will sort the (detailed) documentation of file and class members +# alphabetically by member name. If set to NO the members will appear in +# declaration order. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the +# brief documentation of file, namespace and class members alphabetically +# by member name. If set to NO (the default) the members will appear in +# declaration order. + +SORT_BRIEF_DOCS = NO + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen +# will sort the (brief and detailed) documentation of class members so that +# constructors and destructors are listed first. If set to NO (the default) +# the constructors will appear in the respective orders defined by +# SORT_MEMBER_DOCS and SORT_BRIEF_DOCS. +# This tag will be ignored for brief docs if SORT_BRIEF_DOCS is set to NO +# and ignored for detailed docs if SORT_MEMBER_DOCS is set to NO. + +SORT_MEMBERS_CTORS_1ST = NO + +# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the +# hierarchy of group names into alphabetical order. If set to NO (the default) +# the group names will appear in their defined order. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be +# sorted by fully-qualified names, including namespaces. If set to +# NO (the default), the class list will be sorted only by class name, +# not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the +# alphabetical list. + +SORT_BY_SCOPE_NAME = NO + +# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to +# do proper type resolution of all parameters of a function it will reject a +# match between the prototype and the implementation of a member function even +# if there is only one candidate or it is obvious which candidate to choose +# by doing a simple string match. By disabling STRICT_PROTO_MATCHING doxygen +# will still accept a match between prototype and implementation in such cases. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable (YES) or +# disable (NO) the todo list. This list is created by putting \todo +# commands in the documentation. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable (YES) or +# disable (NO) the test list. This list is created by putting \test +# commands in the documentation. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or +# disable (NO) the bug list. This list is created by putting \bug +# commands in the documentation. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or +# disable (NO) the deprecated list. This list is created by putting +# \deprecated commands in the documentation. + +GENERATE_DEPRECATEDLIST = YES + +# The ENABLED_SECTIONS tag can be used to enable conditional +# documentation sections, marked by \if section-label ... \endif +# and \cond section-label ... \endcond blocks. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines +# the initial value of a variable or macro consists of for it to appear in +# the documentation. If the initializer consists of more lines than specified +# here it will be hidden. Use a value of 0 to hide initializers completely. +# The appearance of the initializer of individual variables and macros in the +# documentation can be controlled using \showinitializer or \hideinitializer +# command in the documentation regardless of this setting. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated +# at the bottom of the documentation of classes and structs. If set to YES the +# list will mention the files that were used to generate the documentation. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. +# This will remove the Files entry from the Quick Index and from the +# Folder Tree View (if specified). The default is YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the +# Namespaces page. +# This will remove the Namespaces entry from the Quick Index +# and from the Folder Tree View (if specified). The default is YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command , where is the value of +# the FILE_VERSION_FILTER tag, and is the name of an input file +# provided by doxygen. Whatever the program writes to standard output +# is used as the file version. See the manual for examples. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents doxygen's defaults, run doxygen with the -l option. +# You can optionally specify a file name after the option, if omitted +# DoxygenLayout.xml will be used as the name of the layout file. + +LAYOUT_FILE = + +# The CITE_BIB_FILES tag can be used to specify one or more bib files +# containing the references data. This must be a list of .bib files. The +# .bib extension is automatically appended if omitted. Using this command +# requires the bibtex tool to be installed. See also +# http://en.wikipedia.org/wiki/BibTeX for more info. For LaTeX the style +# of the bibliography can be controlled using LATEX_BIB_STYLE. To use this +# feature you need bibtex and perl available in the search path. Do not use +# file names with spaces, bibtex cannot handle them. + +CITE_BIB_FILES = + +#--------------------------------------------------------------------------- +# configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated +# by doxygen. Possible values are YES and NO. If left blank NO is used. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated by doxygen. Possible values are YES and NO. If left blank +# NO is used. + +WARNINGS = YES + +# If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings +# for undocumented members. If EXTRACT_ALL is set to YES then this flag will +# automatically be disabled. + +WARN_IF_UNDOCUMENTED = YES + +# If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as not documenting some +# parameters in a documented function, or documenting parameters that +# don't exist or using markup commands wrongly. + +WARN_IF_DOC_ERROR = YES + +# The WARN_NO_PARAMDOC option can be enabled to get warnings for +# functions that are documented, but have no documentation for their parameters +# or return value. If set to NO (the default) doxygen will only warn about +# wrong or incomplete parameter documentation, but not about the absence of +# documentation. + +WARN_NO_PARAMDOC = NO + +# The WARN_FORMAT tag determines the format of the warning messages that +# doxygen can produce. The string should contain the $file, $line, and $text +# tags, which will be replaced by the file and line number from which the +# warning originated and the warning text. Optionally the format may contain +# $version, which will be replaced by the version of the file (if it could +# be obtained via FILE_VERSION_FILTER) + +WARN_FORMAT = "$file:$line: $text" + +# The WARN_LOGFILE tag can be used to specify a file to which warning +# and error messages should be written. If left blank the output is written +# to stderr. + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag can be used to specify the files and/or directories that contain +# documented source files. You may enter file names like "myfile.cpp" or +# directories like "/usr/src/myproject". Separate the files or directories +# with spaces. + +INPUT = "@PROJECT_SOURCE_DIR@/src" + +# This tag can be used to specify the character encoding of the source files +# that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is +# also the default input encoding. Doxygen uses libiconv (or the iconv built +# into libc) for the transcoding. See http://www.gnu.org/software/libiconv for +# the list of possible encodings. + +INPUT_ENCODING = UTF-8 + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp +# and *.h) to filter out the source-files in the directories. If left +# blank the following patterns are tested: +# *.c *.cc *.cxx *.cpp *.c++ *.d *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh +# *.hxx *.hpp *.h++ *.idl *.odl *.cs *.php *.php3 *.inc *.m *.mm *.dox *.py +# *.f90 *.f *.for *.vhd *.vhdl + +FILE_PATTERNS = + +# The RECURSIVE tag can be used to turn specify whether or not subdirectories +# should be searched for input files as well. Possible values are YES and NO. +# If left blank NO is used. + +RECURSIVE = NO + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# Note that relative paths are relative to the directory from which doxygen is +# run. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. Note that the wildcards are matched +# against the file with absolute path, so to exclude all test directories +# for example use the pattern */test/* + +EXCLUDE_PATTERNS = */*_p.h + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# AClass::ANamespace, ANamespace::*Test + +EXCLUDE_SYMBOLS = LibFmQtData + +# The EXAMPLE_PATH tag can be used to specify one or more files or +# directories that contain example code fragments that are included (see +# the \include command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp +# and *.h) to filter out the source-files in the directories. If left +# blank all files are included. + +EXAMPLE_PATTERNS = + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude +# commands irrespective of the value of the RECURSIVE tag. +# Possible values are YES and NO. If left blank NO is used. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or +# directories that contain image that are included in the documentation (see +# the \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command , where +# is the value of the INPUT_FILTER tag, and is the name of an +# input file. Doxygen will then use the output that the filter program writes +# to standard output. +# If FILTER_PATTERNS is specified, this tag will be ignored. +# Note that the filter must not add or remove lines; it is applied before the +# code is scanned, but not when the output code is generated. If lines are added +# or removed, the anchors will not be placed correctly. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. +# Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. +# The filters are a list of the form: +# pattern = filter (like *.cpp = my_cpp_filter). See INPUT_FILTER for further +# info on how filters are used. If FILTER_PATTERNS is empty or if +# non of the patterns match the file name, INPUT_FILTER is applied. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will be used to filter the input files when producing source +# files to browse (i.e. when SOURCE_BROWSER is set to YES). + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) +# and it is also possible to disable source filtering for a specific pattern +# using *.ext = (so without naming a filter). This option only has effect when +# FILTER_SOURCE_FILES is enabled. + +FILTER_SOURCE_PATTERNS = + +# If the USE_MD_FILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want reuse the introduction page also for the doxygen output. + +USE_MDFILE_AS_MAINPAGE = + +#--------------------------------------------------------------------------- +# configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will +# be generated. Documented entities will be cross-referenced with these sources. +# Note: To get rid of all source code in the generated output, make sure also +# VERBATIM_HEADERS is set to NO. + +SOURCE_BROWSER = NO + +# Setting the INLINE_SOURCES tag to YES will include the body +# of functions and classes directly in the documentation. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct +# doxygen to hide any special comment blocks from generated source code +# fragments. Normal C, C++ and Fortran comments will always remain visible. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES +# then for each documented function all documented +# functions referencing it will be listed. + +REFERENCED_BY_RELATION = NO + +# If the REFERENCES_RELATION tag is set to YES +# then for each documented function all documented entities +# called/used by that function will be listed. + +REFERENCES_RELATION = NO + +# If the REFERENCES_LINK_SOURCE tag is set to YES (the default) +# and SOURCE_BROWSER tag is set to YES, then the hyperlinks from +# functions in REFERENCES_RELATION and REFERENCED_BY_RELATION lists will +# link to the source code. +# Otherwise they will link to the documentation. + +REFERENCES_LINK_SOURCE = YES + +# If the USE_HTAGS tag is set to YES then the references to source code +# will point to the HTML generated by the htags(1) tool instead of doxygen +# built-in source browser. The htags tool is part of GNU's global source +# tagging system (see http://www.gnu.org/software/global/global.html). You +# will need version 4.8.6 or higher. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen +# will generate a verbatim copy of the header file for each class for +# which an include is specified. Set to NO to disable this. + +VERBATIM_HEADERS = YES + +#--------------------------------------------------------------------------- +# configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index +# of all compounds will be generated. Enable this if the project +# contains a lot of classes, structs, unions or interfaces. + +ALPHABETICAL_INDEX = YES + +# If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then +# the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns +# in which this list will be split (can be a number in the range [1..20]) + +COLS_IN_ALPHA_INDEX = 5 + +# In case all classes in a project start with a common prefix, all +# classes will be put under the same header in the alphabetical index. +# The IGNORE_PREFIX tag can be used to specify one or more prefixes that +# should be ignored while generating the index headers. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES (the default) Doxygen will +# generate HTML output. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `html' will be used as the default path. + +HTML_OUTPUT = html + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for +# each generated HTML page (for example: .htm,.php,.asp). If it is left blank +# doxygen will generate files with .html extension. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a personal HTML header for +# each generated HTML page. If it is left blank doxygen will generate a +# standard header. Note that when using a custom header you are responsible +# for the proper inclusion of any scripts and style sheets that doxygen +# needs, which is dependent on the configuration options used. +# It is advised to generate a default header using "doxygen -w html +# header.html footer.html stylesheet.css YourConfigFile" and then modify +# that header. Note that the header is subject to change so you typically +# have to redo this when upgrading to a newer version of doxygen or when +# changing the value of configuration settings such as GENERATE_TREEVIEW! + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a personal HTML footer for +# each generated HTML page. If it is left blank doxygen will generate a +# standard footer. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading +# style sheet that is used by each HTML page. It can be used to +# fine-tune the look of the HTML output. If left blank doxygen will +# generate a default style sheet. Note that it is recommended to use +# HTML_EXTRA_STYLESHEET instead of this one, as it is more robust and this +# tag will in the future become obsolete. + +HTML_STYLESHEET = + +# The HTML_EXTRA_STYLESHEET tag can be used to specify an additional +# user-defined cascading style sheet that is included after the standard +# style sheets created by doxygen. Using this option one can overrule +# certain style aspects. This is preferred over using HTML_STYLESHEET +# since it does not replace the standard style sheet and is therefor more +# robust against future updates. Doxygen will copy the style sheet file to +# the output directory. + +HTML_EXTRA_STYLESHEET = + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that +# the files will be copied as-is; there are no commands or markers available. + +HTML_EXTRA_FILES = + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. +# Doxygen will adjust the colors in the style sheet and background images +# according to this color. Hue is specified as an angle on a colorwheel, +# see http://en.wikipedia.org/wiki/Hue for more information. +# For instance the value 0 represents red, 60 is yellow, 120 is green, +# 180 is cyan, 240 is blue, 300 purple, and 360 is red again. +# The allowed range is 0 to 359. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of +# the colors in the HTML output. For a value of 0 the output will use +# grayscales only. A value of 255 will produce the most vivid colors. + +HTML_COLORSTYLE_SAT = 100 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to +# the luminance component of the colors in the HTML output. Values below +# 100 gradually make the output lighter, whereas values above 100 make +# the output darker. The value divided by 100 is the actual gamma applied, +# so 80 represents a gamma of 0.8, The value 220 represents a gamma of 2.2, +# and 100 does not change the gamma. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML +# page will contain the date and time when the page was generated. Setting +# this to NO can help when comparing the output of multiple runs. + +HTML_TIMESTAMP = YES + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. + +HTML_DYNAMIC_SECTIONS = NO + +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of +# entries shown in the various tree structured indices initially; the user +# can expand and collapse entries dynamically later on. Doxygen will expand +# the tree to such a level that at most the specified number of entries are +# visible (unless a fully collapsed tree already exceeds this amount). +# So setting the number of entries 1 will produce a full collapsed tree by +# default. 0 is a special value representing an infinite number of entries +# and will result in a full expanded tree by default. + +HTML_INDEX_NUM_ENTRIES = 100 + +# If the GENERATE_DOCSET tag is set to YES, additional index files +# will be generated that can be used as input for Apple's Xcode 3 +# integrated development environment, introduced with OSX 10.5 (Leopard). +# To create a documentation set, doxygen will generate a Makefile in the +# HTML output directory. Running make will produce the docset in that +# directory and running "make install" will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find +# it at startup. +# See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html +# for more information. + +GENERATE_DOCSET = NO + +# When GENERATE_DOCSET tag is set to YES, this tag determines the name of the +# feed. A documentation feed provides an umbrella under which multiple +# documentation sets from a single provider (such as a company or product suite) +# can be grouped. + +DOCSET_FEEDNAME = "Doxygen generated docs" + +# When GENERATE_DOCSET tag is set to YES, this tag specifies a string that +# should uniquely identify the documentation set bundle. This should be a +# reverse domain-name style string, e.g. com.mycompany.MyDocSet. Doxygen +# will append .docset to the name. + +DOCSET_BUNDLE_ID = org.doxygen.Project + +# When GENERATE_PUBLISHER_ID tag specifies a string that should uniquely +# identify the documentation publisher. This should be a reverse domain-name +# style string, e.g. com.mycompany.MyDocSet.documentation. + +DOCSET_PUBLISHER_ID = org.doxygen.Publisher + +# The GENERATE_PUBLISHER_NAME tag identifies the documentation publisher. + +DOCSET_PUBLISHER_NAME = Publisher + +# If the GENERATE_HTMLHELP tag is set to YES, additional index files +# will be generated that can be used as input for tools like the +# Microsoft HTML help workshop to generate a compiled HTML help file (.chm) +# of the generated HTML documentation. + +GENERATE_HTMLHELP = NO + +# If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can +# be used to specify the file name of the resulting .chm file. You +# can add a path in front of the file if the result should not be +# written to the html output directory. + +CHM_FILE = + +# If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can +# be used to specify the location (absolute path including file name) of +# the HTML help compiler (hhc.exe). If non-empty doxygen will try to run +# the HTML help compiler on the generated index.hhp. + +HHC_LOCATION = + +# If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag +# controls if a separate .chi index file is generated (YES) or that +# it should be included in the master .chm file (NO). + +GENERATE_CHI = NO + +# If the GENERATE_HTMLHELP tag is set to YES, the CHM_INDEX_ENCODING +# is used to encode HtmlHelp index (hhk), content (hhc) and project file +# content. + +CHM_INDEX_ENCODING = + +# If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag +# controls whether a binary table of contents is generated (YES) or a +# normal table of contents (NO) in the .chm file. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members +# to the contents of the HTML help documentation and to the tree view. + +TOC_EXPAND = NO + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated +# that can be used as input for Qt's qhelpgenerator to generate a +# Qt Compressed Help (.qch) of the generated HTML documentation. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can +# be used to specify the file name of the resulting .qch file. +# The path specified is relative to the HTML output folder. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating +# Qt Help Project output. For more information please see +# http://doc.trolltech.com/qthelpproject.html#namespace + +QHP_NAMESPACE = org.doxygen.Project + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating +# Qt Help Project output. For more information please see +# http://doc.trolltech.com/qthelpproject.html#virtual-folders + +QHP_VIRTUAL_FOLDER = doc + +# If QHP_CUST_FILTER_NAME is set, it specifies the name of a custom filter to +# add. For more information please see +# http://doc.trolltech.com/qthelpproject.html#custom-filters + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILT_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see +# +# Qt Help Project / Custom Filters. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's +# filter section matches. +# +# Qt Help Project / Filter Attributes. + +QHP_SECT_FILTER_ATTRS = + +# If the GENERATE_QHP tag is set to YES, the QHG_LOCATION tag can +# be used to specify the location of Qt's qhelpgenerator. +# If non-empty doxygen will try to run qhelpgenerator on the generated +# .qhp file. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files +# will be generated, which together with the HTML files, form an Eclipse help +# plugin. To install this plugin and make it available under the help contents +# menu in Eclipse, the contents of the directory containing the HTML and XML +# files needs to be copied into the plugins directory of eclipse. The name of +# the directory within the plugins directory should be the same as +# the ECLIPSE_DOC_ID value. After copying Eclipse needs to be restarted before +# the help appears. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have +# this name. + +ECLIPSE_DOC_ID = org.doxygen.Project + +# The DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) +# at top of each HTML page. The value NO (the default) enables the index and +# the value YES disables it. Since the tabs have the same information as the +# navigation tree you can set this option to NO if you already set +# GENERATE_TREEVIEW to YES. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. +# If the tag value is set to YES, a side panel will be generated +# containing a tree-like index structure (just like the one that +# is generated for HTML Help). For this to work a browser that supports +# JavaScript, DHTML, CSS and frames is required (i.e. any modern browser). +# Windows users are probably better off using the HTML help feature. +# Since the tree basically has the same information as the tab index you +# could consider to set DISABLE_INDEX to NO when enabling this option. + +GENERATE_TREEVIEW = NO + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values +# (range [0,1..20]) that doxygen will group on one line in the generated HTML +# documentation. Note that a value of 0 will completely suppress the enum +# values from appearing in the overview section. + +ENUM_VALUES_PER_LINE = 4 + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be +# used to set the initial width (in pixels) of the frame in which the tree +# is shown. + +TREEVIEW_WIDTH = 250 + +# When the EXT_LINKS_IN_WINDOW option is set to YES doxygen will open +# links to external symbols imported via tag files in a separate window. + +EXT_LINKS_IN_WINDOW = NO + +# Use this tag to change the font size of Latex formulas included +# as images in the HTML documentation. The default is 10. Note that +# when you change the font size after a successful doxygen run you need +# to manually remove any form_*.png images from the HTML output directory +# to force them to be regenerated. + +FORMULA_FONTSIZE = 10 + +# Use the FORMULA_TRANPARENT tag to determine whether or not the images +# generated for formulas are transparent PNGs. Transparent PNGs are +# not supported properly for IE 6.0, but are supported on all modern browsers. +# Note that when changing this option you need to delete any form_*.png files +# in the HTML output before the changes have effect. + +FORMULA_TRANSPARENT = YES + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax +# (see http://www.mathjax.org) which uses client side Javascript for the +# rendering instead of using prerendered bitmaps. Use this if you do not +# have LaTeX installed or if you want to formulas look prettier in the HTML +# output. When enabled you may also need to install MathJax separately and +# configure the path to it using the MATHJAX_RELPATH option. + +USE_MATHJAX = NO + +# When MathJax is enabled you can set the default output format to be used for +# the MathJax output. Supported types are HTML-CSS, NativeMML (i.e. MathML) and +# SVG. The default value is HTML-CSS, which is slower, but has the best +# compatibility. + +MATHJAX_FORMAT = HTML-CSS + +# When MathJax is enabled you need to specify the location relative to the +# HTML output directory using the MATHJAX_RELPATH option. The destination +# directory should contain the MathJax.js script. For instance, if the mathjax +# directory is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to +# the MathJax Content Delivery Network so you can quickly see the result without +# installing MathJax. +# However, it is strongly recommended to install a local +# copy of MathJax from http://www.mathjax.org before deployment. + +MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest + +# The MATHJAX_EXTENSIONS tag can be used to specify one or MathJax extension +# names that should be enabled during MathJax rendering. + +MATHJAX_EXTENSIONS = + +# The MATHJAX_CODEFILE tag can be used to specify a file with javascript +# pieces of code that will be used on startup of the MathJax code. + +MATHJAX_CODEFILE = + +# When the SEARCHENGINE tag is enabled doxygen will generate a search box +# for the HTML output. The underlying search engine uses javascript +# and DHTML and should work on any modern browser. Note that when using +# HTML help (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets +# (GENERATE_DOCSET) there is already a search function so this one should +# typically be disabled. For large projects the javascript based search engine +# can be slow, then enabling SERVER_BASED_SEARCH may provide a better solution. + +SEARCHENGINE = YES + +# When the SERVER_BASED_SEARCH tag is enabled the search engine will be +# implemented using a web server instead of a web client using Javascript. +# There are two flavours of web server based search depending on the +# EXTERNAL_SEARCH setting. When disabled, doxygen will generate a PHP script for +# searching and an index file used by the script. When EXTERNAL_SEARCH is +# enabled the indexing and searching needs to be provided by external tools. +# See the manual for details. + +SERVER_BASED_SEARCH = NO + +# When EXTERNAL_SEARCH is enabled doxygen will no longer generate the PHP +# script for searching. Instead the search results are written to an XML file +# which needs to be processed by an external indexer. Doxygen will invoke an +# external search engine pointed to by the SEARCHENGINE_URL option to obtain +# the search results. Doxygen ships with an example indexer (doxyindexer) and +# search engine (doxysearch.cgi) which are based on the open source search +# engine library Xapian. See the manual for configuration details. + +EXTERNAL_SEARCH = NO + +# The SEARCHENGINE_URL should point to a search engine hosted by a web server +# which will returned the search results when EXTERNAL_SEARCH is enabled. +# Doxygen ships with an example search engine (doxysearch) which is based on +# the open source search engine library Xapian. See the manual for configuration +# details. + +SEARCHENGINE_URL = + +# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed +# search data is written to a file for indexing by an external tool. With the +# SEARCHDATA_FILE tag the name of this file can be specified. + +SEARCHDATA_FILE = searchdata.xml + +# When SERVER_BASED_SEARCH AND EXTERNAL_SEARCH are both enabled the +# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is +# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple +# projects and redirect the results back to the right project. + +EXTERNAL_SEARCH_ID = + +# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen +# projects other than the one defined by this configuration file, but that are +# all added to the same external search index. Each project needs to have a +# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id +# of to a relative location where the documentation can be found. +# The format is: EXTRA_SEARCH_MAPPINGS = id1 = loc1 id2 = loc2 ... + +EXTRA_SEARCH_MAPPINGS = + +#--------------------------------------------------------------------------- +# configuration options related to the LaTeX output +#--------------------------------------------------------------------------- + +# If the GENERATE_LATEX tag is set to YES (the default) Doxygen will +# generate Latex output. + +GENERATE_LATEX = NO + +# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `latex' will be used as the default path. + +LATEX_OUTPUT = latex + +# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be +# invoked. If left blank `latex' will be used as the default command name. +# Note that when enabling USE_PDFLATEX this option is only used for +# generating bitmaps for formulas in the HTML output, but not in the +# Makefile that is written to the output directory. + +LATEX_CMD_NAME = latex + +# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to +# generate index for LaTeX. If left blank `makeindex' will be used as the +# default command name. + +MAKEINDEX_CMD_NAME = makeindex + +# If the COMPACT_LATEX tag is set to YES Doxygen generates more compact +# LaTeX documents. This may be useful for small projects and may help to +# save some trees in general. + +COMPACT_LATEX = NO + +# The PAPER_TYPE tag can be used to set the paper type that is used +# by the printer. Possible values are: a4, letter, legal and +# executive. If left blank a4 will be used. + +PAPER_TYPE = a4 + +# The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX +# packages that should be included in the LaTeX output. + +EXTRA_PACKAGES = + +# The LATEX_HEADER tag can be used to specify a personal LaTeX header for +# the generated latex document. The header should contain everything until +# the first chapter. If it is left blank doxygen will generate a +# standard header. Notice: only use this tag if you know what you are doing! + +LATEX_HEADER = + +# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for +# the generated latex document. The footer should contain everything after +# the last chapter. If it is left blank doxygen will generate a +# standard footer. Notice: only use this tag if you know what you are doing! + +LATEX_FOOTER = + +# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images +# or other source files which should be copied to the LaTeX output directory. +# Note that the files will be copied as-is; there are no commands or markers +# available. + +LATEX_EXTRA_FILES = + +# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated +# is prepared for conversion to pdf (using ps2pdf). The pdf file will +# contain links (just like the HTML output) instead of page references +# This makes the output suitable for online browsing using a pdf viewer. + +PDF_HYPERLINKS = YES + +# If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of +# plain latex in the generated Makefile. Set this option to YES to get a +# higher quality PDF documentation. + +USE_PDFLATEX = YES + +# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode. +# command to the generated LaTeX files. This will instruct LaTeX to keep +# running if errors occur, instead of asking the user for help. +# This option is also used when generating formulas in HTML. + +LATEX_BATCHMODE = NO + +# If LATEX_HIDE_INDICES is set to YES then doxygen will not +# include the index chapters (such as File Index, Compound Index, etc.) +# in the output. + +LATEX_HIDE_INDICES = NO + +# If LATEX_SOURCE_CODE is set to YES then doxygen will include +# source code with syntax highlighting in the LaTeX output. +# Note that which sources are shown also depends on other settings +# such as SOURCE_BROWSER. + +LATEX_SOURCE_CODE = NO + +# The LATEX_BIB_STYLE tag can be used to specify the style to use for the +# bibliography, e.g. plainnat, or ieeetr. The default style is "plain". See +# http://en.wikipedia.org/wiki/BibTeX for more info. + +LATEX_BIB_STYLE = plain + +#--------------------------------------------------------------------------- +# configuration options related to the RTF output +#--------------------------------------------------------------------------- + +# If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output +# The RTF output is optimized for Word 97 and may not look very pretty with +# other RTF readers or editors. + +GENERATE_RTF = NO + +# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `rtf' will be used as the default path. + +RTF_OUTPUT = rtf + +# If the COMPACT_RTF tag is set to YES Doxygen generates more compact +# RTF documents. This may be useful for small projects and may help to +# save some trees in general. + +COMPACT_RTF = NO + +# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated +# will contain hyperlink fields. The RTF file will +# contain links (just like the HTML output) instead of page references. +# This makes the output suitable for online browsing using WORD or other +# programs which support those fields. +# Note: wordpad (write) and others do not support links. + +RTF_HYPERLINKS = NO + +# Load style sheet definitions from file. Syntax is similar to doxygen's +# config file, i.e. a series of assignments. You only have to provide +# replacements, missing definitions are set to their default value. + +RTF_STYLESHEET_FILE = + +# Set optional variables used in the generation of an rtf document. +# Syntax is similar to doxygen's config file. + +RTF_EXTENSIONS_FILE = + +#--------------------------------------------------------------------------- +# configuration options related to the man page output +#--------------------------------------------------------------------------- + +# If the GENERATE_MAN tag is set to YES (the default) Doxygen will +# generate man pages + +GENERATE_MAN = NO + +# The MAN_OUTPUT tag is used to specify where the man pages will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `man' will be used as the default path. + +MAN_OUTPUT = man + +# The MAN_EXTENSION tag determines the extension that is added to +# the generated man pages (default is the subroutine's section .3) + +MAN_EXTENSION = .3 + +# If the MAN_LINKS tag is set to YES and Doxygen generates man output, +# then it will generate one additional man file for each entity +# documented in the real man page(s). These additional files +# only source the real man page, but without them the man command +# would be unable to find the correct page. The default is NO. + +MAN_LINKS = NO + +#--------------------------------------------------------------------------- +# configuration options related to the XML output +#--------------------------------------------------------------------------- + +# If the GENERATE_XML tag is set to YES Doxygen will +# generate an XML file that captures the structure of +# the code including all documentation. + +GENERATE_XML = NO + +# The XML_OUTPUT tag is used to specify where the XML pages will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `xml' will be used as the default path. + +XML_OUTPUT = xml + +# The XML_SCHEMA tag can be used to specify an XML schema, +# which can be used by a validating XML parser to check the +# syntax of the XML files. + +XML_SCHEMA = + +# The XML_DTD tag can be used to specify an XML DTD, +# which can be used by a validating XML parser to check the +# syntax of the XML files. + +XML_DTD = + +# If the XML_PROGRAMLISTING tag is set to YES Doxygen will +# dump the program listings (including syntax highlighting +# and cross-referencing information) to the XML output. Note that +# enabling this will significantly increase the size of the XML output. + +XML_PROGRAMLISTING = YES + +#--------------------------------------------------------------------------- +# configuration options related to the DOCBOOK output +#--------------------------------------------------------------------------- + +# If the GENERATE_DOCBOOK tag is set to YES Doxygen will generate DOCBOOK files +# that can be used to generate PDF. + +GENERATE_DOCBOOK = NO + +# The DOCBOOK_OUTPUT tag is used to specify where the DOCBOOK pages will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in +# front of it. If left blank docbook will be used as the default path. + +DOCBOOK_OUTPUT = docbook + +#--------------------------------------------------------------------------- +# configuration options for the AutoGen Definitions output +#--------------------------------------------------------------------------- + +# If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will +# generate an AutoGen Definitions (see autogen.sf.net) file +# that captures the structure of the code including all +# documentation. Note that this feature is still experimental +# and incomplete at the moment. + +GENERATE_AUTOGEN_DEF = NO + +#--------------------------------------------------------------------------- +# configuration options related to the Perl module output +#--------------------------------------------------------------------------- + +# If the GENERATE_PERLMOD tag is set to YES Doxygen will +# generate a Perl module file that captures the structure of +# the code including all documentation. Note that this +# feature is still experimental and incomplete at the +# moment. + +GENERATE_PERLMOD = NO + +# If the PERLMOD_LATEX tag is set to YES Doxygen will generate +# the necessary Makefile rules, Perl scripts and LaTeX code to be able +# to generate PDF and DVI output from the Perl module output. + +PERLMOD_LATEX = NO + +# If the PERLMOD_PRETTY tag is set to YES the Perl module output will be +# nicely formatted so it can be parsed by a human reader. +# This is useful +# if you want to understand what is going on. +# On the other hand, if this +# tag is set to NO the size of the Perl module output will be much smaller +# and Perl will parse it just the same. + +PERLMOD_PRETTY = YES + +# The names of the make variables in the generated doxyrules.make file +# are prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. +# This is useful so different doxyrules.make files included by the same +# Makefile don't overwrite each other's variables. + +PERLMOD_MAKEVAR_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the preprocessor +#--------------------------------------------------------------------------- + +# If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will +# evaluate all C-preprocessor directives found in the sources and include +# files. + +ENABLE_PREPROCESSING = YES + +# If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro +# names in the source code. If set to NO (the default) only conditional +# compilation will be performed. Macro expansion can be done in a controlled +# way by setting EXPAND_ONLY_PREDEF to YES. + +MACRO_EXPANSION = NO + +# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES +# then the macro expansion is limited to the macros specified with the +# PREDEFINED and EXPAND_AS_DEFINED tags. + +EXPAND_ONLY_PREDEF = NO + +# If the SEARCH_INCLUDES tag is set to YES (the default) the includes files +# pointed to by INCLUDE_PATH will be searched when a #include is found. + +SEARCH_INCLUDES = YES + +# The INCLUDE_PATH tag can be used to specify one or more directories that +# contain include files that are not input files but should be processed by +# the preprocessor. + +INCLUDE_PATH = + +# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard +# patterns (like *.h and *.hpp) to filter out the header-files in the +# directories. If left blank, the patterns specified with FILE_PATTERNS will +# be used. + +INCLUDE_FILE_PATTERNS = + +# The PREDEFINED tag can be used to specify one or more macro names that +# are defined before the preprocessor is started (similar to the -D option of +# gcc). The argument of the tag is a list of macros of the form: name +# or name = definition (no spaces). If the definition and the = are +# omitted = 1 is assumed. To prevent a macro definition from being +# undefined via #undef or recursively expanded use the : = operator +# instead of the = operator. + +PREDEFINED = + +# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then +# this tag can be used to specify a list of macro names that should be expanded. +# The macro definition that is found in the sources will be used. +# Use the PREDEFINED tag if you want to use a different macro definition that +# overrules the definition found in the source code. + +EXPAND_AS_DEFINED = + +# If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then +# doxygen's preprocessor will remove all references to function-like macros +# that are alone on a line, have an all uppercase name, and do not end with a +# semicolon, because these will confuse the parser if not removed. + +SKIP_FUNCTION_MACROS = YES + +#--------------------------------------------------------------------------- +# Configuration::additions related to external references +#--------------------------------------------------------------------------- + +# The TAGFILES option can be used to specify one or more tagfiles. For each +# tag file the location of the external documentation should be added. The +# format of a tag file without this location is as follows: +# +# TAGFILES = file1 file2 ... +# Adding location for the tag files is done as follows: +# +# TAGFILES = file1 = loc1 "file2 = loc2" ... +# where "loc1" and "loc2" can be relative or absolute paths +# or URLs. Note that each tag file must have a unique name (where the name does +# NOT include the path). If a tag file is not located in the directory in which +# doxygen is run, you must also specify the path to the tagfile here. + +TAGFILES = + +# When a file name is specified after GENERATE_TAGFILE, doxygen will create +# a tag file that is based on the input files it reads. + +GENERATE_TAGFILE = + +# If the ALLEXTERNALS tag is set to YES all external classes will be listed +# in the class index. If set to NO only the inherited external classes +# will be listed. + +ALLEXTERNALS = NO + +# If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed +# in the modules index. If set to NO, only the current project's groups will +# be listed. + +EXTERNAL_GROUPS = YES + +# If the EXTERNAL_PAGES tag is set to YES all external pages will be listed +# in the related pages index. If set to NO, only the current project's +# pages will be listed. + +EXTERNAL_PAGES = YES + +# The PERL_PATH should be the absolute path and name of the perl script +# interpreter (i.e. the result of `which perl'). + +PERL_PATH = /usr/bin/perl + +#--------------------------------------------------------------------------- +# Configuration options related to the dot tool +#--------------------------------------------------------------------------- + +# If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will +# generate a inheritance diagram (in HTML, RTF and LaTeX) for classes with base +# or super classes. Setting the tag to NO turns the diagrams off. Note that +# this option also works with HAVE_DOT disabled, but it is recommended to +# install and use dot, since it yields more powerful graphs. + +CLASS_DIAGRAMS = YES + +# You can define message sequence charts within doxygen comments using the \msc +# command. Doxygen will then run the mscgen tool (see +# http://www.mcternan.me.uk/mscgen/) to produce the chart and insert it in the +# documentation. The MSCGEN_PATH tag allows you to specify the directory where +# the mscgen tool resides. If left empty the tool is assumed to be found in the +# default search path. + +MSCGEN_PATH = + +# If set to YES, the inheritance and collaboration graphs will hide +# inheritance and usage relations if the target is undocumented +# or is not a class. + +HIDE_UNDOC_RELATIONS = YES + +# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is +# available from the path. This tool is part of Graphviz, a graph visualization +# toolkit from AT&T and Lucent Bell Labs. The other options in this section +# have no effect if this option is set to NO (the default) + +HAVE_DOT = NO + +# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is +# allowed to run in parallel. When set to 0 (the default) doxygen will +# base this on the number of processors available in the system. You can set it +# explicitly to a value larger than 0 to get control over the balance +# between CPU load and processing speed. + +DOT_NUM_THREADS = 0 + +# By default doxygen will use the Helvetica font for all dot files that +# doxygen generates. When you want a differently looking font you can specify +# the font name using DOT_FONTNAME. You need to make sure dot is able to find +# the font, which can be done by putting it in a standard location or by setting +# the DOTFONTPATH environment variable or by setting DOT_FONTPATH to the +# directory containing the font. + +DOT_FONTNAME = Helvetica + +# The DOT_FONTSIZE tag can be used to set the size of the font of dot graphs. +# The default size is 10pt. + +DOT_FONTSIZE = 10 + +# By default doxygen will tell dot to use the Helvetica font. +# If you specify a different font using DOT_FONTNAME you can use DOT_FONTPATH to +# set the path where dot can find it. + +DOT_FONTPATH = + +# If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for each documented class showing the direct and +# indirect inheritance relations. Setting this tag to YES will force the +# CLASS_DIAGRAMS tag to NO. + +CLASS_GRAPH = YES + +# If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for each documented class showing the direct and +# indirect implementation dependencies (inheritance, containment, and +# class references variables) of the class with other documented classes. + +COLLABORATION_GRAPH = YES + +# If the GROUP_GRAPHS and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for groups, showing the direct groups dependencies + +GROUP_GRAPHS = YES + +# If the UML_LOOK tag is set to YES doxygen will generate inheritance and +# collaboration diagrams in a style similar to the OMG's Unified Modeling +# Language. + +UML_LOOK = NO + +# If the UML_LOOK tag is enabled, the fields and methods are shown inside +# the class node. If there are many fields or methods and many nodes the +# graph may become too big to be useful. The UML_LIMIT_NUM_FIELDS +# threshold limits the number of items for each type to make the size more +# manageable. Set this to 0 for no limit. Note that the threshold may be +# exceeded by 50% before the limit is enforced. + +UML_LIMIT_NUM_FIELDS = 10 + +# If set to YES, the inheritance and collaboration graphs will show the +# relations between templates and their instances. + +TEMPLATE_RELATIONS = NO + +# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT +# tags are set to YES then doxygen will generate a graph for each documented +# file showing the direct and indirect include dependencies of the file with +# other documented files. + +INCLUDE_GRAPH = YES + +# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and +# HAVE_DOT tags are set to YES then doxygen will generate a graph for each +# documented header file showing the documented files that directly or +# indirectly include this file. + +INCLUDED_BY_GRAPH = YES + +# If the CALL_GRAPH and HAVE_DOT options are set to YES then +# doxygen will generate a call dependency graph for every global function +# or class method. Note that enabling this option will significantly increase +# the time of a run. So in most cases it will be better to enable call graphs +# for selected functions only using the \callgraph command. + +CALL_GRAPH = NO + +# If the CALLER_GRAPH and HAVE_DOT tags are set to YES then +# doxygen will generate a caller dependency graph for every global function +# or class method. Note that enabling this option will significantly increase +# the time of a run. So in most cases it will be better to enable caller +# graphs for selected functions only using the \callergraph command. + +CALLER_GRAPH = NO + +# If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen +# will generate a graphical hierarchy of all classes instead of a textual one. + +GRAPHICAL_HIERARCHY = YES + +# If the DIRECTORY_GRAPH and HAVE_DOT tags are set to YES +# then doxygen will show the dependencies a directory has on other directories +# in a graphical way. The dependency relations are determined by the #include +# relations between the files in the directories. + +DIRECTORY_GRAPH = YES + +# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images +# generated by dot. Possible values are svg, png, jpg, or gif. +# If left blank png will be used. If you choose svg you need to set +# HTML_FILE_EXTENSION to xhtml in order to make the SVG files +# visible in IE 9+ (other browsers do not have this requirement). + +DOT_IMAGE_FORMAT = png + +# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to +# enable generation of interactive SVG images that allow zooming and panning. +# Note that this requires a modern browser other than Internet Explorer. +# Tested and working are Firefox, Chrome, Safari, and Opera. For IE 9+ you +# need to set HTML_FILE_EXTENSION to xhtml in order to make the SVG files +# visible. Older versions of IE do not have SVG support. + +INTERACTIVE_SVG = NO + +# The tag DOT_PATH can be used to specify the path where the dot tool can be +# found. If left blank, it is assumed the dot tool can be found in the path. + +DOT_PATH = + +# The DOTFILE_DIRS tag can be used to specify one or more directories that +# contain dot files that are included in the documentation (see the +# \dotfile command). + +DOTFILE_DIRS = + +# The MSCFILE_DIRS tag can be used to specify one or more directories that +# contain msc files that are included in the documentation (see the +# \mscfile command). + +MSCFILE_DIRS = + +# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of +# nodes that will be shown in the graph. If the number of nodes in a graph +# becomes larger than this value, doxygen will truncate the graph, which is +# visualized by representing a node as a red box. Note that doxygen if the +# number of direct children of the root node in a graph is already larger than +# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note +# that the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH. + +DOT_GRAPH_MAX_NODES = 50 + +# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the +# graphs generated by dot. A depth value of 3 means that only nodes reachable +# from the root by following a path via at most 3 edges will be shown. Nodes +# that lay further from the root node will be omitted. Note that setting this +# option to 1 or 2 may greatly reduce the computation time needed for large +# code bases. Also note that the size of a graph can be further restricted by +# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction. + +MAX_DOT_GRAPH_DEPTH = 0 + +# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent +# background. This is disabled by default, because dot on Windows does not +# seem to support this out of the box. Warning: Depending on the platform used, +# enabling this option may lead to badly anti-aliased labels on the edges of +# a graph (i.e. they become hard to read). + +DOT_TRANSPARENT = NO + +# Set the DOT_MULTI_TARGETS tag to YES allow dot to generate multiple output +# files in one run (i.e. multiple -o and -T options on the command line). This +# makes dot run faster, but since only newer versions of dot (>1.8.10) +# support this, this feature is disabled by default. + +DOT_MULTI_TARGETS = YES + +# If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will +# generate a legend page explaining the meaning of the various boxes and +# arrows in the dot generated graphs. + +GENERATE_LEGEND = YES + +# If the DOT_CLEANUP tag is set to YES (the default) Doxygen will +# remove the intermediate dot files that are used to generate +# the various graphs. + +DOT_CLEANUP = YES diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..20fb9c7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,458 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS diff --git a/README.md b/README.md new file mode 100644 index 0000000..29ec1b8 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# libfm-qt + +## Overview + +libfm-qt is the Qt port of libfm, a library providing components to build desktop file managers which belongs to [LXDE](http://lxde.org). + +libfm-qt is licensed under the terms of the [LGPLv2.1](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html) or any later version. See file LICENSE for its full text. + +## Installation + +### Compiling source code + +Runtime dependencies are Qt X11 Extras and libfm ≥ 1,2 (not all features are provided by libfm-qt yet). +Additional build dependencies are CMake, [lxqt-build-tools](https://github.com/lxde/lxqt-build-tools) and optionally Git to pull latest VCS checkouts. The localization files were outsourced to repository [lxqt-l10n](https://github.com/lxde/lxqt-l10n) so the corresponding dependencies are needed, too. Please refer to this repository's `README.md` for further information. + +Code configuration is handled by CMake. CMake variable `CMAKE_INSTALL_PREFIX` has to be set to `/usr` on most operating systems, depending on the way library paths are dealt with on 64bit systems variables like `CMAKE_INSTALL_LIBDIR` may have to be set as well. + +To build run `make`, to install `make install` which accepts variable `DESTDIR` as usual. + +### Binary packages + +Official binary packages are available in Arch Linux, Debian (as of Debian stretch) and openSUSE (Leap 42.1 and Tumbleweed). +The library is still missing in Fedora which is providing version 0.10.0 of PCManFM-Qt only so far. This version was still including the code outsourced into libfm-qt later so libfm-qt will have to be provided by Fedora, too, as soon as the distribution upgrades to PCManFM-Qt ≥ 0.10.1. + +## Development + +Issues should go to the tracker of PCManFM-Qt at https://github.com/lxde/pcmanfm-qt/issues. diff --git a/cmake/fm-qt-config.cmake.in b/cmake/fm-qt-config.cmake.in new file mode 100644 index 0000000..a44783f --- /dev/null +++ b/cmake/fm-qt-config.cmake.in @@ -0,0 +1,41 @@ +#============================================================================= +# Copyright 2015 Luís Pereira +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#============================================================================= + +@PACKAGE_INIT@ + +if (CMAKE_VERSION VERSION_LESS 3.0.2) + message(FATAL_ERROR \"fm-qt requires at least CMake version 3.0.2\") +endif() + +include(CMakeFindDependencyMacro) + +if (NOT TARGET @LIBFM_QT_LIBRARY_NAME@) + if (POLICY CMP0024) + cmake_policy(SET CMP0024 NEW) + endif() + include("${CMAKE_CURRENT_LIST_DIR}/@LIBFM_QT_LIBRARY_NAME@-targets.cmake") +endif() diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt new file mode 100644 index 0000000..9e9cd2c --- /dev/null +++ b/data/CMakeLists.txt @@ -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" +) diff --git a/data/archivers.list b/data/archivers.list new file mode 100644 index 0000000..317953a --- /dev/null +++ b/data/archivers.list @@ -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 diff --git a/data/libfm-qt-mimetypes.xml b/data/libfm-qt-mimetypes.xml new file mode 100644 index 0000000..7f509a9 --- /dev/null +++ b/data/libfm-qt-mimetypes.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + Windows installer + Windows 安裝程式 + + + + + MS VBScript + + + + + C# source + C# 程式碼 + + + + + 應用程式捷徑 + + + + + + + + + + + + + + diff --git a/data/terminals.list b/data/terminals.list new file mode 100644 index 0000000..ec6d202 --- /dev/null +++ b/data/terminals.list @@ -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 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..b586381 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,269 @@ +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 + icontheme.cpp + filelauncher.cpp + foldermodel.cpp + foldermodelitem.cpp + cachedfoldermodel.cpp + proxyfoldermodel.cpp + folderview.cpp + folderitemdelegate.cpp + createnewmenu.cpp + filemenu.cpp + foldermenu.cpp + filepropsdialog.cpp + applaunchcontext.cpp + placesview.cpp + placesmodel.cpp + placesmodelitem.cpp + dirtreeview.cpp + dirtreemodel.cpp + dirtreemodelitem.cpp + dnddest.cpp + mountoperation.cpp + mountoperationpassworddialog.cpp + mountoperationquestiondialog.cpp + fileoperation.cpp + fileoperationdialog.cpp + renamedialog.cpp + pathedit.cpp + pathbar.cpp + colorbutton.cpp + fontbutton.cpp + browsehistory.cpp + utilities.cpp + dndactionmenu.cpp + editbookmarksdialog.cpp + execfiledialog.cpp + appchoosercombobox.cpp + appmenuview.cpp + appchooserdialog.cpp + filesearchdialog.cpp + filedialog.cpp + fm-search.c # might be moved to libfm later + xdndworkaround.cpp +) + +set(libfm_UIS + file-props.ui + file-operation-dialog.ui + rename-dialog.ui + mount-operation-password.ui + edit-bookmarks.ui + exec-file.ui + app-chooser-dialog.ui + filesearch.ui + filedialog.ui +) + +qt5_wrap_ui(libfm_UIS_H ${libfm_UIS}) + + +set(LIBFM_QT_DATA_DIR "${CMAKE_INSTALL_FULL_DATADIR}/libfm-qt") +set(LIBFM_QT_INTREE_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/include") + +# add translation for libfm-qt +lxqt_translate_ts(QM_FILES + UPDATE_TRANSLATIONS ${UPDATE_TRANSLATIONS} + SOURCES ${libfm_SRCS} ${libfm_UIS} + INSTALL_DIR "${LIBFM_QT_DATA_DIR}/translations" + PULL_TRANSLATIONS ${PULL_TRANSLATIONS} + CLEAN_TRANSLATIONS ${CLEAN_TRANSLATIONS} + TRANSLATIONS_REPO ${TRANSLATIONS_REPO} + TRANSLATIONS_REFSPEC ${TRANSLATIONS_REFSPEC} +) + +add_library(${LIBFM_QT_LIBRARY_NAME} SHARED + ${libfm_SRCS} + ${libfm_UIS_H} + ${QM_FILES} +) + +install(EXPORT + "${LIBFM_QT_LIBRARY_NAME}-targets" + DESTINATION "${CMAKE_INSTALL_DATADIR}/cmake/${LIBFM_QT_LIBRARY_NAME}" + COMPONENT Devel +) + +target_link_libraries(${LIBFM_QT_LIBRARY_NAME} + Qt5::Widgets + Qt5::X11Extras + ${FM_LIBRARIES} + ${MENUCACHE_LIBRARIES} + ${XCB_LIBRARIES} + ${EXIF_LIBRARIES} +) + +# set libtool soname +set_target_properties(${LIBFM_QT_LIBRARY_NAME} PROPERTIES + VERSION ${LIBFM_QT_LIB_VERSION} + SOVERSION ${LIBFM_QT_LIB_SOVERSION} +) + +target_include_directories(${LIBFM_QT_LIBRARY_NAME} + PRIVATE "${Qt5Gui_PRIVATE_INCLUDE_DIRS}" + PUBLIC + "${FM_INCLUDE_DIRS}" + "${FM_INCLUDE_DIR}/libfm" # to workaround incorrect #include in fm-actions. + "${MENUCACHE_INCLUDE_DIRS}" + "${XCB_INCLUDE_DIRS}" + "${EXIF_INCLUDE_DIRS}" + INTERFACE + "$" + "$" +) + +target_compile_definitions(${LIBFM_QT_LIBRARY_NAME} + PRIVATE "LIBFM_QT_DATA_DIR=\"${LIBFM_QT_DATA_DIR}\"" + PUBLIC "QT_NO_KEYWORDS" +) + +install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/${LIBFM_QT_LIBRARY_NAME}_export.h" + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/libfm-qt" + COMPONENT Devel +) + +# install include header files (FIXME: can we make this cleaner? should dir name be versioned?) +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} + EXPORT_MACRO_NAME LIBFM_QT_API +) + +# InTree build +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" + "${CMAKE_BINARY_DIR}/${LIBFM_QT_LIBRARY_NAME}-config.cmake" + INSTALL_DESTINATION "${CMAKE_INSTALL_DATADIR}/cmake/${LIBFM_QT_LIBRARY_NAME}" +) + +install(FILES + "${CMAKE_BINARY_DIR}/${LIBFM_QT_LIBRARY_NAME}-config.cmake" + DESTINATION "${CMAKE_INSTALL_DATADIR}/cmake/${LIBFM_QT_LIBRARY_NAME}" + COMPONENT Devel +) + +# FIXME: add libtool version to the lib (soname) later. +# FIXME: only export public symbols + +install(TARGETS ${LIBFM_QT_LIBRARY_NAME} + DESTINATION "${CMAKE_INSTALL_LIBDIR}" + EXPORT "${LIBFM_QT_LIBRARY_NAME}-targets" + LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" + PUBLIC_HEADER + COMPONENT Runtime +) + +export(TARGETS ${LIBFM_QT_LIBRARY_NAME} + FILE "${CMAKE_BINARY_DIR}/${LIBFM_QT_LIBRARY_NAME}-targets.cmake" + EXPORT_LINK_INTERFACE_LIBRARIES +) + +# install a pkgconfig file for libfm-qt +set(REQUIRED_QT "Qt5Widgets >= ${REQUIRED_QT_VERSION} Qt5X11Extras >= ${REQUIRED_QT_VERSION}") +configure_file(libfm-qt.pc.in lib${LIBFM_QT_LIBRARY_NAME}.pc @ONLY) +# FreeBSD loves to install files to different locations +# http://www.freebsd.org/doc/handbook/dirstructure.html +if(${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") + install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/lib${LIBFM_QT_LIBRARY_NAME}.pc" + DESTINATION libdata/pkgconfig + COMPONENT Devel + ) +else() + install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/lib${LIBFM_QT_LIBRARY_NAME}.pc" + DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig" + COMPONENT Devel + ) +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}) + diff --git a/src/app-chooser-dialog.ui b/src/app-chooser-dialog.ui new file mode 100644 index 0000000..ef1ebee --- /dev/null +++ b/src/app-chooser-dialog.ui @@ -0,0 +1,183 @@ + + + AppChooserDialog + + + + 0 + 0 + 432 + 387 + + + + Choose an Application + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + + + + + 0 + 1 + + + + 0 + + + + Installed Applications + + + + + + + + + + Custom Command + + + + + + Command line to execute: + + + + + + + + + + Application name: + + + + + + + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + + + Qt::RichText + + + + + + + false + + + Keep terminal window open after command execution + + + + + + + Execute in terminal emulator + + + + + + + + + + + Set selected application as default action of this file type + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + Fm::AppMenuView + QTreeView +
appmenuview.h
+
+
+ + + + buttonBox + accepted() + AppChooserDialog + accept() + + + 227 + 359 + + + 157 + 274 + + + + + buttonBox + rejected() + AppChooserDialog + reject() + + + 295 + 365 + + + 286 + 274 + + + + + useTerminal + toggled(bool) + keepTermOpen + setEnabled(bool) + + + 72 + 260 + + + 79 + 282 + + + + +
diff --git a/src/appchoosercombobox.cpp b/src/appchoosercombobox.cpp new file mode 100644 index 0000000..bf7d0f1 --- /dev/null +++ b/src/appchoosercombobox.cpp @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "appchoosercombobox.h" +#include "icontheme.h" +#include "appchooserdialog.h" +#include "utilities.h" +#include "core/iconinfo.h" + +namespace Fm { + +AppChooserComboBox::AppChooserComboBox(QWidget* parent): + 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(&QComboBox::currentIndexChanged), this, &AppChooserComboBox::onCurrentIndexChanged); +} + +AppChooserComboBox::~AppChooserComboBox() { +} + +void AppChooserComboBox::setMimeType(std::shared_ptr 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_); + } +} + +// returns the currently selected app. +Fm::GAppInfoPtr AppChooserComboBox::selectedApp() const { + int idx = currentIndex(); + return idx >= 0 ? appInfos_[idx] : Fm::GAppInfoPtr{}; +} + +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) { + 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; + } + } + + // block our handler to prevent recursive calls. + blockOnCurrentIndexChanged_ = true; + // restore to previously selected item + setCurrentIndex(prevIndex_); + blockOnCurrentIndexChanged_ = false; + } + else { + prevIndex_ = index; + } +} + + +#if 0 +/* get a list of custom apps added with app-chooser. +* the returned GList is owned by the combo box and shouldn't be freed. */ +const GList* AppChooserComboBox::customApps() { + +} +#endif + +} // namespace Fm diff --git a/src/appchoosercombobox.h b/src/appchoosercombobox.h new file mode 100644 index 0000000..03149e0 --- /dev/null +++ b/src/appchoosercombobox.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_APPCHOOSERCOMBOBOX_H +#define FM_APPCHOOSERCOMBOBOX_H + +#include "libfmqtglobals.h" +#include +#include + +#include + +#include "core/mimetype.h" +#include "core/gioptrs.h" + +namespace Fm { + +class LIBFM_QT_API AppChooserComboBox : public QComboBox { + Q_OBJECT +public: + ~AppChooserComboBox(); + explicit AppChooserComboBox(QWidget* parent); + + void setMimeType(std::shared_ptr mimeType); + + const std::shared_ptr& mimeType() const { + return mimeType_; + } + + Fm::GAppInfoPtr selectedApp() const; + // const GList* customApps(); + + bool isChanged() const; + +private Q_SLOTS: + void onCurrentIndexChanged(int index); + +private: + std::shared_ptr mimeType_; + std::vector 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_; +}; + +} + +#endif // FM_APPCHOOSERCOMBOBOX_H diff --git a/src/appchooserdialog.cpp b/src/appchooserdialog.cpp new file mode 100644 index 0000000..0b508a2 --- /dev/null +++ b/src/appchooserdialog.cpp @@ -0,0 +1,286 @@ +/* + * Copyright 2010-2014 Hong Jen Yee (PCMan) + * Copyright 2012-2013 Andriy Grytsenko (LStranger) + * + * 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 "appchooserdialog.h" +#include "ui_app-chooser-dialog.h" +#include +#include + +namespace Fm { + +AppChooserDialog::AppChooserDialog(std::shared_ptr 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); + + if(!ui->appMenuView->isAppSelected()) { + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); // disable OK button + } +} + +AppChooserDialog::~AppChooserDialog() { + delete ui; +} + +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 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 = 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, 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 = nullptr; + } + } + if(arg_found) { + *arg_found = (p != nullptr); + } + if(p) { + return g_strndup(cmdline, p - cmdline); + } + else { + return g_strdup(cmdline); + } +} + +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"; + } + + /* 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); + } + 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; + } + } + + /* 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(); + + if(ui->tabWidget->currentIndex() == 0) { + selectedApp_ = ui->appMenuView->selectedApp(); + } + else { // custom command line + selectedApp_ = customCommandToApp(); + } + + 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_.get(), mimeType_->name(), nullptr); +#else + 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_.get(), mimeType_->name(), nullptr); + } + } + } +} + +void AppChooserDialog::onSelectionChanged() { + bool isAppSelected = ui->appMenuView->isAppSelected(); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(isAppSelected); +} + +void AppChooserDialog::setMimeType(std::shared_ptr 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); +} + +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); + } +} + +} // namespace Fm diff --git a/src/appchooserdialog.h b/src/appchooserdialog.h new file mode 100644 index 0000000..ef36dfc --- /dev/null +++ b/src/appchooserdialog.h @@ -0,0 +1,79 @@ +/* + * Copyright 2010-2014 Hong Jen Yee (PCMan) + * Copyright 2012-2013 Andriy Grytsenko (LStranger) + * + * 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_APPCHOOSERDIALOG_H +#define FM_APPCHOOSERDIALOG_H + +#include +#include "libfmqtglobals.h" +#include + +#include "core/mimetype.h" +#include "core/gioptrs.h" + +namespace Ui { +class AppChooserDialog; +} + +namespace Fm { + +class LIBFM_QT_API AppChooserDialog : public QDialog { + Q_OBJECT +public: + explicit AppChooserDialog(std::shared_ptr mimeType, QWidget* parent = nullptr, Qt::WindowFlags f = 0); + ~AppChooserDialog(); + + virtual void accept(); + + void setMimeType(std::shared_ptr mimeType); + + const std::shared_ptr& mimeType() const { + return mimeType_; + } + + void setCanSetDefault(bool value); + + bool canSetDefault() const { + return canSetDefault_; + } + + const Fm::GAppInfoPtr& selectedApp() const { + return selectedApp_; + } + + bool isSetDefault() const; + +private: + GAppInfo* customCommandToApp(); + +private Q_SLOTS: + void onSelectionChanged(); + void onTabChanged(int index); + +private: + Ui::AppChooserDialog* ui; + std::shared_ptr mimeType_; + bool canSetDefault_; + Fm::GAppInfoPtr selectedApp_; +}; + +} + +#endif // FM_APPCHOOSERDIALOG_H diff --git a/src/applaunchcontext.cpp b/src/applaunchcontext.cpp new file mode 100644 index 0000000..e599485 --- /dev/null +++ b/src/applaunchcontext.cpp @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "applaunchcontext.h" +#include +#include + +typedef struct _FmAppLaunchContext { + GAppLaunchContext parent; +}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*/) { + Display* dpy = QX11Info::display(); + if(dpy) { + char* xstr = DisplayString(dpy); + return g_strdup(xstr); + } + return nullptr; +} + +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) { + GAppLaunchContextClass* app_launch_class = G_APP_LAUNCH_CONTEXT_CLASS(klass); + app_launch_class->get_display = fm_app_launch_context_get_display; + app_launch_class->get_startup_notify_id = fm_app_launch_context_get_startup_notify_id; +} + +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, nullptr); + return context; +} + +FmAppLaunchContext* fm_app_launch_context_new() { + FmAppLaunchContext* context = (FmAppLaunchContext*)g_object_new(FM_TYPE_APP_LAUNCH_CONTEXT, nullptr); + return context; +} diff --git a/src/applaunchcontext.h b/src/applaunchcontext.h new file mode 100644 index 0000000..606c3b6 --- /dev/null +++ b/src/applaunchcontext.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_APP_LAUNCHCONTEXT_H +#define FM_APP_LAUNCHCONTEXT_H + +#include "libfmqtglobals.h" +#include +#include + +#define FM_TYPE_APP_LAUNCH_CONTEXT (fm_app_launch_context_get_type()) +#define FM_APP_LAUNCH_CONTEXT(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj),\ + FM_TYPE_APP_LAUNCH_CONTEXT, FmAppLaunchContext)) +#define FM_APP_LAUNCH_CONTEXT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass),\ + FM_TYPE_APP_LAUNCH_CONTEXT, FmAppLaunchContextClass)) +#define FM_IS_APP_LAUNCH_CONTEXT(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj),\ + FM_TYPE_APP_LAUNCH_CONTEXT)) +#define FM_IS_APP_LAUNCH_CONTEXT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass),\ + FM_TYPE_APP_LAUNCH_CONTEXT)) +#define FM_APP_LAUNCH_CONTEXT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj),\ + FM_TYPE_APP_LAUNCH_CONTEXT, FmAppLaunchContextClass)) + +typedef struct _FmAppLaunchContext FmAppLaunchContext; + +typedef struct _FmAppLaunchContextClass { + GAppLaunchContextClass parent; +}FmAppLaunchContextClass; + +FmAppLaunchContext* fm_app_launch_context_new(); +FmAppLaunchContext* fm_app_launch_context_new_for_widget(QWidget* widget); +GType fm_app_launch_context_get_type(); + +#endif // FM_APPLAUNCHCONTEXT_H diff --git a/src/appmenuview.cpp b/src/appmenuview.cpp new file mode 100644 index 0000000..cc669d1 --- /dev/null +++ b/src/appmenuview.cpp @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "appmenuview.h" +#include +#include "icontheme.h" +#include "appmenuview_p.h" +#include + +namespace Fm { + +AppMenuView::AppMenuView(QWidget* parent): + 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); +} + +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); + } +} + +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 != 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); +} + +void AppMenuView::onMenuCacheReload(MenuCache* mc) { + 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() const { + AppMenuViewItem* item = selectedItem(); + return (item && item->isApp()); +} + +AppMenuViewItem* AppMenuView::selectedItem() const { + QModelIndexList selected = selectedIndexes(); + if(!selected.isEmpty()) { + AppMenuViewItem* item = static_cast(model_->itemFromIndex(selected.first() + )); + return item; + } + return nullptr; +} + +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() 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() const { + AppMenuViewItem* item = selectedItem(); + if(item && item->isApp()) { + return menu_cache_item_get_id(item->item()); + } + return nullptr; +} + +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 diff --git a/src/appmenuview.h b/src/appmenuview.h new file mode 100644 index 0000000..12d42a7 --- /dev/null +++ b/src/appmenuview.h @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_APPMENUVIEW_H +#define FM_APPMENUVIEW_H + +#include +#include "libfmqtglobals.h" +#include +#include + +#include "core/gioptrs.h" + +class QStandardItemModel; +class QStandardItem; + +namespace Fm { + +class AppMenuViewItem; + +class LIBFM_QT_API AppMenuView : public QTreeView { + Q_OBJECT +public: + explicit AppMenuView(QWidget* parent = nullptr); + ~AppMenuView(); + + Fm::GAppInfoPtr selectedApp() const; + + const char* selectedAppDesktopId() const; + + QByteArray selectedAppDesktopFilePath() const; + + FmPath* selectedAppDesktopPath() const; + + bool isAppSelected() const; + +Q_SIGNALS: + 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(user_data)->onMenuCacheReload(mc); + } + + AppMenuViewItem* selectedItem() const; + +private: + // gboolean fm_app_menu_view_is_item_app(, GtkTreeIter* it); + QStandardItemModel* model_; + MenuCache* menu_cache; + MenuCacheNotifyId menu_cache_reload_notify; +}; + +} + +#endif // FM_APPMENUVIEW_H diff --git a/src/appmenuview_p.h b/src/appmenuview_p.h new file mode 100644 index 0000000..815c84e --- /dev/null +++ b/src/appmenuview_p.h @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_APPMENUVIEW_P_H +#define FM_APPMENUVIEW_P_H + +#include +#include +#include "icontheme.h" +#include "core/iconinfo.h" + +namespace Fm { + +class AppMenuViewItem : public QStandardItem { +public: + explicit AppMenuViewItem(MenuCacheItem* item): + item_(menu_cache_item_ref(item)) { + std::shared_ptr 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_); + } + + MenuCacheItem* item() { + return item_; + } + + int type() const { + return menu_cache_item_get_type(item_); + } + + bool isApp() { + return type() == MENU_CACHE_TYPE_APP; + } + + bool isDir() { + return type() == MENU_CACHE_TYPE_DIR; + } + +private: + MenuCacheItem* item_; +}; + +} + +#endif // FM_APPMENUVIEW_P_H diff --git a/src/archiver.h b/src/archiver.h new file mode 100644 index 0000000..216a32c --- /dev/null +++ b/src/archiver.h @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2016 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef __LIBFM_QT_FM_ARCHIVER_H__ +#define __LIBFM_QT_FM_ARCHIVER_H__ + +#include +#include +#include +#include "libfmqtglobals.h" + + +namespace Fm { + + +class LIBFM_QT_API Archiver { +public: + + + // default constructor + Archiver() { + dataPtr_ = nullptr; + } + + + // move constructor + Archiver(Archiver&& other) noexcept { + dataPtr_ = reinterpret_cast(other.takeDataPtr()); + } + + + // destructor + ~Archiver() { + if(dataPtr_ != nullptr) { + (dataPtr_); + } + } + + + // create a wrapper for the data pointer without increasing the reference count + static Archiver wrapPtr(FmArchiver* dataPtr) { + Archiver obj; + obj.dataPtr_ = reinterpret_cast(dataPtr); + return obj; + } + + // disown the managed data pointer + FmArchiver* takeDataPtr() { + FmArchiver* data = reinterpret_cast(dataPtr_); + dataPtr_ = nullptr; + return data; + } + + // get the raw pointer wrapped + FmArchiver* dataPtr() { + return reinterpret_cast(dataPtr_); + } + + // automatic type casting + operator FmArchiver*() { + return dataPtr(); + } + + // automatic type casting + operator void*() { + return dataPtr(); + } + + + + // move assignment + Archiver& operator=(Archiver&& other) noexcept { + dataPtr_ = reinterpret_cast(other.takeDataPtr()); + return *this; + } + + bool isNull() { + return (dataPtr_ == nullptr); + } + + // methods + + void setDefault(void) { + fm_archiver_set_default(dataPtr()); + } + + + static Archiver getDefault( ) { + return wrapPtr(fm_archiver_get_default()); + } + + + bool extractArchivesTo(GAppLaunchContext* ctx, FmPathList* files, FmPath* dest_dir) { + return fm_archiver_extract_archives_to(dataPtr(), ctx, files, dest_dir); + } + + + bool extractArchives(GAppLaunchContext* ctx, FmPathList* files) { + return fm_archiver_extract_archives(dataPtr(), ctx, files); + } + + + bool createArchive(GAppLaunchContext* ctx, FmPathList* files) { + return fm_archiver_create_archive(dataPtr(), ctx, files); + } + + + bool isMimeTypeSupported(const char* type) { + return fm_archiver_is_mime_type_supported(dataPtr(), type); + } + + +// the wrapped object cannot be copied. +private: + Archiver(const Archiver& other) = delete; + Archiver& operator=(const Archiver& other) = delete; + + +private: + FmArchiver* dataPtr_; // data pointer for the underlying C struct + +}; + + +} + +#endif // __LIBFM_QT_FM_ARCHIVER_H__ diff --git a/src/bookmarkaction.cpp b/src/bookmarkaction.cpp new file mode 100644 index 0000000..502126a --- /dev/null +++ b/src/bookmarkaction.cpp @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "bookmarkaction.h" + +namespace Fm { + +BookmarkAction::BookmarkAction(std::shared_ptr item, QObject* parent): + QAction(parent), + item_(std::move(item)) { + + setText(item_->name()); +} + +} // namespace Fm diff --git a/src/bookmarkaction.h b/src/bookmarkaction.h new file mode 100644 index 0000000..08fc73f --- /dev/null +++ b/src/bookmarkaction.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef BOOKMARKACTION_H +#define BOOKMARKACTION_H + +#include "libfmqtglobals.h" +#include +#include "core/bookmarks.h" + +namespace Fm { + +// action used to create bookmark menu items +class LIBFM_QT_API BookmarkAction : public QAction { +public: + explicit BookmarkAction(std::shared_ptr item, QObject* parent = 0); + + const std::shared_ptr& bookmark() const { + return item_; + } + + const Fm::FilePath& path() const { + return item_->path(); + } + +private: + std::shared_ptr item_; +}; + +} + +#endif // BOOKMARKACTION_H diff --git a/src/browsehistory.cpp b/src/browsehistory.cpp new file mode 100644 index 0000000..54671fb --- /dev/null +++ b/src/browsehistory.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "browsehistory.h" + +namespace Fm { + +BrowseHistory::BrowseHistory(): + currentIndex_(0), + maxCount_(10) { +} + +BrowseHistory::~BrowseHistory() { +} + +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(items_.size() + 1 > static_cast(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 + items_.push_back(BrowseHistoryItem(path, scrollPos)); + currentIndex_ = items_.size() - 1; +} + +void BrowseHistory::setCurrentIndex(int index) { + if(index >= 0 && static_cast(index) < items_.size()) { + currentIndex_ = index; + // FIXME: should we emit a signal for the change? + } +} + +bool BrowseHistory::canBackward() const { + return (currentIndex_ > 0); +} + +int BrowseHistory::backward() { + if(canBackward()) { + --currentIndex_; + } + return currentIndex_; +} + +bool BrowseHistory::canForward() const { + return (static_cast(currentIndex_) + 1 < items_.size()); +} + +int BrowseHistory::forward() { + if(canForward()) { + ++currentIndex_; + } + return currentIndex_; +} + +void BrowseHistory::setMaxCount(int maxCount) { + maxCount_ = maxCount; + if(items_.size() > static_cast(maxCount)) { + // TODO: remove some items + } +} + + +} // namespace Fm diff --git a/src/browsehistory.h b/src/browsehistory.h new file mode 100644 index 0000000..6ea570e --- /dev/null +++ b/src/browsehistory.h @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_BROWSEHISTORY_H +#define FM_BROWSEHISTORY_H + +#include "libfmqtglobals.h" +#include +#include + +#include "core/filepath.h" + +namespace Fm { + +// class used to story browsing history of folder views +// We use this class to replace FmNavHistory provided by libfm since +// the original Libfm API is hard to use and confusing. + +class LIBFM_QT_API BrowseHistoryItem { +public: + + 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: + Fm::FilePath path_; + int scrollPos_; + // TODO: we may need to store current selection as well. +}; + +class LIBFM_QT_API BrowseHistory { + +public: + BrowseHistory(); + virtual ~BrowseHistory(); + + int currentIndex() const { + return currentIndex_; + } + void setCurrentIndex(int index); + + Fm::FilePath currentPath() const { + return items_[currentIndex_].path(); + } + + int currentScrollPos() const { + return items_[currentIndex_].scrollPos(); + } + + BrowseHistoryItem& currentItem() { + return items_[currentIndex_]; + } + + size_t size() const { + return items_.size(); + } + + BrowseHistoryItem& at(int index) { + return items_[index]; + } + + void add(Fm::FilePath path, int scrollPos = 0); + + bool canForward() const; + + bool canBackward() const; + + int backward(); + + int forward(); + + int maxCount() const { + return maxCount_; + } + + void setMaxCount(int maxCount); + +private: + std::vector items_; + int currentIndex_; + int maxCount_; +}; + +} + +#endif // FM_BROWSEHISTORY_H diff --git a/src/cachedfoldermodel.cpp b/src/cachedfoldermodel.cpp new file mode 100644 index 0000000..c594cb9 --- /dev/null +++ b/src/cachedfoldermodel.cpp @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "cachedfoldermodel.h" + +namespace Fm { + +CachedFolderModel::CachedFolderModel(const std::shared_ptr& folder): + FolderModel(), + refCount(1) { + FolderModel::setFolder(folder); +} + +CachedFolderModel::~CachedFolderModel() { + // qDebug("delete CachedFolderModel"); +} + +CachedFolderModel* CachedFolderModel::modelFromFolder(const std::shared_ptr& folder) { + QVariant cache = folder->property(cacheKey); + CachedFolderModel* model = cache.value(); + if(model) { + model->ref(); + } + else { + model = new CachedFolderModel(folder); + cache = QVariant::fromValue(model); + folder->setProperty(cacheKey, cache); + } + return model; +} + +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) { + folder()->setProperty(cacheKey, QVariant()); + deleteLater(); + } +} + + +} // namespace Fm diff --git a/src/cachedfoldermodel.h b/src/cachedfoldermodel.h new file mode 100644 index 0000000..1c22489 --- /dev/null +++ b/src/cachedfoldermodel.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_CACHEDFOLDERMODEL_H +#define FM_CACHEDFOLDERMODEL_H + +#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 +public: + explicit CachedFolderModel(const std::shared_ptr& folder); + void ref() { + ++refCount; + } + void unref(); + + static CachedFolderModel* modelFromFolder(const std::shared_ptr& folder); + static CachedFolderModel* modelFromPath(const Fm::FilePath& path); + +private: + virtual ~CachedFolderModel(); + void setFolder(FmFolder* folder); +private: + int refCount; + constexpr static const char* cacheKey = "CachedFolderModel"; +}; + + +} + +#endif // FM_CACHEDFOLDERMODEL_H diff --git a/src/colorbutton.cpp b/src/colorbutton.cpp new file mode 100644 index 0000000..ac95b45 --- /dev/null +++ b/src/colorbutton.cpp @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "colorbutton.h" +#include + +namespace Fm { + +ColorButton::ColorButton(QWidget* parent): QPushButton(parent) { + connect(this, &QPushButton::clicked, this, &ColorButton::onClicked); +} + +ColorButton::~ColorButton() { + +} + +void ColorButton::onClicked() { + 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(); + } +} + + +} // namespace Fm diff --git a/src/colorbutton.h b/src/colorbutton.h new file mode 100644 index 0000000..d5fa89d --- /dev/null +++ b/src/colorbutton.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_COLORBUTTON_H +#define FM_COLORBUTTON_H + +#include "libfmqtglobals.h" +#include +#include + +namespace Fm { + +class LIBFM_QT_API ColorButton : public QPushButton { + Q_OBJECT + +public: + explicit ColorButton(QWidget* parent = 0); + virtual ~ColorButton(); + + void setColor(const QColor&); + + QColor color() const { + return color_; + } + +Q_SIGNALS: + void changed(); + +private Q_SLOTS: + void onClicked(); + +private: + QColor color_; +}; + +} + +#endif // FM_COLORBUTTON_H diff --git a/src/core/bookmarks.cpp b/src/core/bookmarks.cpp new file mode 100644 index 0000000..8397550 --- /dev/null +++ b/src/core/bookmarks.cpp @@ -0,0 +1,162 @@ +#include "bookmarks.h" +#include "cstrptr.h" +#include +#include + +namespace Fm { + +std::weak_ptr 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{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& Bookmarks::insert(const FilePath& path, const QString& name, int pos) { + const auto insert_pos = (pos < 0 || static_cast(pos) > items_.size()) ? items_.cend() : items_.cbegin() + pos; + auto it = items_.insert(insert_pos, std::make_shared(path, name)); + queueSave(); + return *it; +} + +void Bookmarks::remove(const std::shared_ptr& item) { + items_.erase(std::remove(items_.begin(), items_.end(), item), items_.end()); + queueSave(); +} + +void Bookmarks::reorder(const std::shared_ptr& item, int pos) { + auto old_it = std::find(items_.cbegin(), items_.cend(), item); + if(old_it == items_.cend()) + return; + std::shared_ptr 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& item, QString new_name) { + auto it = std::find_if(items_.cbegin(), items_.cend(), [item](const std::shared_ptr& 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(item->path(), new_name)); + items_.erase(it + 1); // remove the old item + queueSave(); + } +} + +std::shared_ptr Bookmarks::globalInstance() { + auto bookmarks = globalInstance_.lock(); + if(!bookmarks) { + bookmarks = std::make_shared(); + 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: + // \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(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 diff --git a/src/core/bookmarks.h b/src/core/bookmarks.h new file mode 100644 index 0000000..5c19d2f --- /dev/null +++ b/src/core/bookmarks.h @@ -0,0 +1,96 @@ +#ifndef FM2_BOOKMARKS_H +#define FM2_BOOKMARKS_H + +#include "../libfmqtglobals.h" +#include +#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& info() const { + return info_; + } + +private: + void setInfo(const std::shared_ptr& info) { + info_ = info; + } + + void setName(const QString& name) { + name_ = name; + } + +private: + FilePath path_; + QString name_; + std::shared_ptr info_; +}; + + +class LIBFM_QT_API Bookmarks : public QObject { + Q_OBJECT +public: + explicit Bookmarks(QObject* parent = 0); + + ~Bookmarks(); + + const std::shared_ptr &insert(const FilePath& path, const QString& name, int pos); + + void remove(const std::shared_ptr& item); + + void reorder(const std::shared_ptr &item, int pos); + + void rename(const std::shared_ptr& item, QString new_name); + + const std::vector>& items() const { + return items_; + } + + static std::shared_ptr 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 mon; + std::vector> items_; + static std::weak_ptr globalInstance_; + bool idle_handler; +}; + +} // namespace Fm + +#endif // FM2_BOOKMARKS_H diff --git a/src/core/compat_p.h b/src/core/compat_p.h new file mode 100644 index 0000000..86d550f --- /dev/null +++ b/src/core/compat_p.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 +#include "path.h" + +// compatibility functions bridging the old libfm C APIs and new C++ APIs. + +namespace Fm { + +inline FM_QT_DEPRECATED Fm::Path _convertPath(const Fm::FilePath& path) { + return Fm::Path::newForGfile(path.gfile().get()); +} + +inline FM_QT_DEPRECATED Fm::PathList _convertPathList(const Fm::FilePathList& srcFiles) { + Fm::PathList ret; + for(auto& file: srcFiles) { + ret.pushTail(_convertPath(file)); + } + return ret; +} + +inline FM_QT_DEPRECATED FmFileInfo* _convertFileInfo(const std::shared_ptr& info) { + // conver to GFileInfo first + GFileInfoPtr ginfo{g_file_info_new(), false}; + g_file_info_set_name(ginfo.get(), info->name().c_str()); + g_file_info_set_display_name(ginfo.get(), info->displayName().toUtf8().constData()); + g_file_info_set_content_type(ginfo.get(), info->mimeType()->name()); + + auto mode = info->mode(); + g_file_info_set_attribute_uint32(ginfo.get(), G_FILE_ATTRIBUTE_UNIX_MODE, mode); + GFileType ftype = info->isDir() ? G_FILE_TYPE_DIRECTORY : G_FILE_TYPE_REGULAR; // FIXME: generate more accurate type + g_file_info_set_file_type(ginfo.get(), ftype); + g_file_info_set_size(ginfo.get(), info->size()); + g_file_info_set_icon(ginfo.get(), info->icon()->gicon().get()); + + g_file_info_set_attribute_uint64(ginfo.get(), G_FILE_ATTRIBUTE_TIME_MODIFIED, info->mtime()); + g_file_info_set_attribute_uint64(ginfo.get(), G_FILE_ATTRIBUTE_TIME_ACCESS, info->atime()); + g_file_info_set_attribute_uint64(ginfo.get(), G_FILE_ATTRIBUTE_TIME_CHANGED, info->ctime()); + + auto gf = info->path().gfile(); + return fm_file_info_new_from_g_file_data(gf.get(), ginfo.get(), nullptr); +} + +} + +#endif // LIBFM_QT_COMPAT_P_H diff --git a/src/core/copyjob.cpp b/src/core/copyjob.cpp new file mode 100644 index 0000000..73ef3cf --- /dev/null +++ b/src/core/copyjob.cpp @@ -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 diff --git a/src/core/copyjob.h b/src/core/copyjob.h new file mode 100644 index 0000000..1cdbbe5 --- /dev/null +++ b/src/core/copyjob.h @@ -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 diff --git a/src/core/cstrptr.h b/src/core/cstrptr.h new file mode 100644 index 0000000..cc12a0f --- /dev/null +++ b/src/core/cstrptr.h @@ -0,0 +1,42 @@ +#ifndef FM2_CSTRPTR_H +#define FM2_CSTRPTR_H + +#include +#include + +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 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 CStrArrayPtr; + + +} // namespace Fm + +#endif // FM2_CSTRPTR_H diff --git a/src/core/deletejob.cpp b/src/core/deletejob.cpp new file mode 100644 index 0000000..b2c518f --- /dev/null +++ b/src/core/deletejob.cpp @@ -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 diff --git a/src/core/deletejob.h b/src/core/deletejob.h new file mode 100644 index 0000000..4560098 --- /dev/null +++ b/src/core/deletejob.h @@ -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 diff --git a/src/core/dirlistjob.cpp b/src/core/dirlistjob.cpp new file mode 100644 index 0000000..d782ede --- /dev/null +++ b/src/core/dirlistjob.cpp @@ -0,0 +1,178 @@ +#include "dirlistjob.h" +#include +#include "fileinfo_p.h" +#include "gioptrs.h" +#include + +namespace Fm { + +DirListJob::DirListJob(const FilePath& path, Flags _flags, const std::shared_ptr& 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 lock{mutex_}; + dir_fi = std::make_shared(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(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 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 diff --git a/src/core/dirlistjob.h b/src/core/dirlistjob.h new file mode 100644 index 0000000..e578521 --- /dev/null +++ b/src/core/dirlistjob.h @@ -0,0 +1,65 @@ +#ifndef FM2_DIRLISTJOB_H +#define FM2_DIRLISTJOB_H + +#include "../libfmqtglobals.h" +#include +#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& cutFilesHashSet = nullptr); + + FileInfoList& files() { + return files_; + } + + void setIncremental(bool set); + + bool incremental() const { + return emit_files_found; + } + + FilePath dirPath() const { + std::lock_guard lock{mutex_}; + return dir_path; + } + + std::shared_ptr dirInfo() const { + std::lock_guard 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 dir_fi; + FileInfoList files_; + const std::shared_ptr cutFilesHashSet_; + bool emit_files_found; + // guint delay_add_files_handler; + // GSList* files_to_add; +}; + +} // namespace Fm + +#endif // FM2_DIRLISTJOB_H diff --git a/src/core/filechangeattrjob.cpp b/src/core/filechangeattrjob.cpp new file mode 100644 index 0000000..5ebf43d --- /dev/null +++ b/src/core/filechangeattrjob.cpp @@ -0,0 +1,9 @@ +#include "filechangeattrjob.h" + +namespace Fm { + +FileChangeAttrJob::FileChangeAttrJob() { + +} + +} // namespace Fm diff --git a/src/core/filechangeattrjob.h b/src/core/filechangeattrjob.h new file mode 100644 index 0000000..2b0aea4 --- /dev/null +++ b/src/core/filechangeattrjob.h @@ -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 diff --git a/src/core/fileinfo.cpp b/src/core/fileinfo.cpp new file mode 100644 index 0000000..0258c67 --- /dev/null +++ b/src/core/fileinfo.cpp @@ -0,0 +1,378 @@ +#include "fileinfo.h" +#include "fileinfo_p.h" +#include + +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& 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& 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 diff --git a/src/core/fileinfo.h b/src/core/fileinfo.h new file mode 100644 index 0000000..1208e1e --- /dev/null +++ b/src/core/fileinfo.h @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2016 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef __LIBFM_QT_FM2_FILE_INFO_H__ +#define __LIBFM_QT_FM2_FILE_INFO_H__ + +#include +#include +#include +#include "../libfmqtglobals.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "gioptrs.h" +#include "filepath.h" +#include "iconinfo.h" +#include "mimetype.h" + + +namespace Fm { + +class FileInfoList; +typedef std::set 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& icon() const { + return icon_; + } + + const std::shared_ptr& 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& cutFilesHashSet); + + const std::forward_list>& 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 mimeType_; + std::shared_ptr icon_; + std::forward_list> 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 cutFilesHashSet_; + // std::vector> extraData_; +}; + + +class LIBFM_QT_API FileInfoList: public std::vector> { +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> FileInfoPair; + + +} + +Q_DECLARE_METATYPE(std::shared_ptr) + + +#endif // __LIBFM_QT_FM2_FILE_INFO_H__ diff --git a/src/core/fileinfo_p.h b/src/core/fileinfo_p.h new file mode 100644 index 0000000..37553fa --- /dev/null +++ b/src/core/fileinfo_p.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 diff --git a/src/core/fileinfojob.cpp b/src/core/fileinfojob.cpp new file mode 100644 index 0000000..75142a4 --- /dev/null +++ b/src/core/fileinfojob.cpp @@ -0,0 +1,42 @@ +#include "fileinfojob.h" +#include "fileinfo_p.h" + +namespace Fm { + +FileInfoJob::FileInfoJob(FilePathList paths, FilePath commonDirPath, const std::shared_ptr& 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(fileInfo); + + results_.push_back(fileInfoPtr); + Q_EMIT gotInfo(path, fileInfoPtr); + } + } +} + +} // namespace Fm diff --git a/src/core/fileinfojob.h b/src/core/fileinfojob.h new file mode 100644 index 0000000..ba59eb7 --- /dev/null +++ b/src/core/fileinfojob.h @@ -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& cutFilesHashSet = nullptr); + + const FilePathList& paths() const { + return paths_; + } + + const FileInfoList& files() const { + return results_; + } + +Q_SIGNALS: + void gotInfo(const FilePath& path, std::shared_ptr& info); + +protected: + void exec() override; + +private: + FilePathList paths_; + FileInfoList results_; + FilePath commonDirPath_; + const std::shared_ptr cutFilesHashSet_; +}; + +} // namespace Fm + +#endif // FM2_FILEINFOJOB_H diff --git a/src/core/filelinkjob.cpp b/src/core/filelinkjob.cpp new file mode 100644 index 0000000..253768e --- /dev/null +++ b/src/core/filelinkjob.cpp @@ -0,0 +1,9 @@ +#include "filelinkjob.h" + +namespace Fm { + +FileLinkJob::FileLinkJob() { + +} + +} // namespace Fm diff --git a/src/core/filelinkjob.h b/src/core/filelinkjob.h new file mode 100644 index 0000000..965d089 --- /dev/null +++ b/src/core/filelinkjob.h @@ -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 diff --git a/src/core/filemonitor.cpp b/src/core/filemonitor.cpp new file mode 100644 index 0000000..68c6b6b --- /dev/null +++ b/src/core/filemonitor.cpp @@ -0,0 +1,9 @@ +#include "filemonitor.h" + +namespace Fm { + +FileMonitor::FileMonitor() { + +} + +} // namespace Fm diff --git a/src/core/filemonitor.h b/src/core/filemonitor.h new file mode 100644 index 0000000..9a1485d --- /dev/null +++ b/src/core/filemonitor.h @@ -0,0 +1,26 @@ +#ifndef FM2_FILEMONITOR_H +#define FM2_FILEMONITOR_H + +#include "../libfmqtglobals.h" +#include +#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 diff --git a/src/core/fileoperationjob.cpp b/src/core/fileoperationjob.cpp new file mode 100644 index 0000000..346da42 --- /dev/null +++ b/src/core/fileoperationjob.cpp @@ -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 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 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 lock{mutex_}; + if(hasTotalAmount_) { + finishedSize = finishedSize_; + finishedCount = finishedCount_; + } + return hasTotalAmount_; +} + +void FileOperationJob::setTotalAmount(uint64_t fileSize, uint64_t fileCount) { + std::lock_guard locl{mutex_}; + hasTotalAmount_ = true; + totalSize_ = fileSize; + totalCount_ = fileCount; +} + +void FileOperationJob::setFinishedAmount(uint64_t finishedSize, uint64_t finishedCount) { + std::lock_guard locl{mutex_}; + finishedSize_ = finishedSize; + finishedCount_ = finishedCount; +} + +void FileOperationJob::addFinishedAmount(uint64_t finishedSize, uint64_t finishedCount) { + std::lock_guard locl{mutex_}; + finishedSize_ += finishedSize; + finishedCount_ += finishedCount; +} + +void FileOperationJob::setCurrentFile(const FilePath& path) { + std::lock_guard locl{mutex_}; + currentFile_ = path; +} + +void FileOperationJob::setCurrentFileProgress(uint64_t totalSize, uint64_t finishedSize) { + std::lock_guard locl{mutex_}; + currentFileSize_ = totalSize; + currentFileFinished_ = finishedSize; +} + +} // namespace Fm diff --git a/src/core/fileoperationjob.h b/src/core/fileoperationjob.h new file mode 100644 index 0000000..c9ce569 --- /dev/null +++ b/src/core/fileoperationjob.h @@ -0,0 +1,73 @@ +#ifndef FM2_FILEOPERATIONJOB_H +#define FM2_FILEOPERATIONJOB_H + +#include "../libfmqtglobals.h" +#include "job.h" +#include +#include +#include +#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 diff --git a/src/core/filepath.cpp b/src/core/filepath.cpp new file mode 100644 index 0000000..36a7910 --- /dev/null +++ b/src/core/filepath.cpp @@ -0,0 +1,21 @@ +#include "filepath.h" +#include +#include +#include + +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 diff --git a/src/core/filepath.h b/src/core/filepath.h new file mode 100644 index 0000000..d33658b --- /dev/null +++ b/src/core/filepath.h @@ -0,0 +1,177 @@ +#ifndef FM2_FILEPATH_H +#define FM2_FILEPATH_H + +#include "../libfmqtglobals.h" +#include "gobjectptr.h" +#include "cstrptr.h" +#include +#include +#include + +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() 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_; + static FilePath homeDir_; +}; + +struct FilePathHash { + std::size_t operator() (const FilePath& path) const { + return path.hash(); + } +}; + +typedef std::vector FilePathList; + +} // namespace Fm + +Q_DECLARE_METATYPE(Fm::FilePath) + +#endif // FM2_FILEPATH_H diff --git a/src/core/filesysteminfojob.cpp b/src/core/filesysteminfojob.cpp new file mode 100644 index 0000000..6c3aa69 --- /dev/null +++ b/src/core/filesysteminfojob.cpp @@ -0,0 +1,24 @@ +#include "filesysteminfojob.h" +#include "gobjectptr.h" + +namespace Fm { + +void FileSystemInfoJob::exec() { + GObjectPtr inf = GObjectPtr{ + 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 diff --git a/src/core/filesysteminfojob.h b/src/core/filesysteminfojob.h new file mode 100644 index 0000000..59c963a --- /dev/null +++ b/src/core/filesysteminfojob.h @@ -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 diff --git a/src/core/folder.cpp b/src/core/folder.cpp new file mode 100644 index 0000000..d91600a --- /dev/null +++ b/src/core/folder.cpp @@ -0,0 +1,908 @@ +/* + * fm-folder.c + * + * Copyright 2009 - 2012 Hong Jen Yee (PCMan) + * Copyright 2012-2016 Andriy Grytsenko (LStranger) + * + * 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 +#include +#include +#include + +#include "dirlistjob.h" +#include "filesysteminfojob.h" +#include "fileinfojob.h" + +namespace Fm { + +std::unordered_map, FilePathHash> Folder::cache_; +FilePath Folder::cutFilesDirPath_; +FilePath Folder::lastCutFilesDirPath_; +std::shared_ptr 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 lock{mutex_}; + auto it = cache_.find(dirPath_); + if(it != cache_.end()) { + cache_.erase(it); + } +} + +// static +std::shared_ptr Folder::fromPath(const FilePath& path) { + std::lock_guard 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(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 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& 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(sender()); + fileinfoJobs_.erase(std::find(fileinfoJobs_.cbegin(), fileinfoJobs_.cend(), job)); + + if(job->isCancelled()) + return; + + FileInfoList files_to_add; + std::vector 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 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 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 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& cutFilesHashSet) { + if(cutFilesHashSet_ && !cutFilesHashSet_->empty()) { + lastCutFilesDirPath_ = cutFilesDirPath_; + } + cutFilesDirPath_ = dirPath_; + cutFilesHashSet_ = cutFilesHashSet; +} + +void Folder::onDirListFinished() { + DirListJob* job = static_cast(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 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(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 diff --git a/src/core/folder.h b/src/core/folder.h new file mode 100644 index 0000000..b4b1f39 --- /dev/null +++ b/src/core/folder.h @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2016 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef __LIBFM2_QT_FM_FOLDER_H__ +#define __LIBFM2_QT_FM_FOLDER_H__ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#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 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 fileByName(const char* name) const; + + bool isEmpty() const; + + FileInfoList files() const; + + const FilePath& path() const; + + const std::shared_ptr &info() const; + + bool hadCutFilesUnset(); + + bool hasCutFiles(); + + void setCutFiles(const std::shared_ptr& cutFilesHashSet); + + void forEachFile(std::function&)> func) const { + std::lock_guard 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& 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 dirInfo_; + DirListJob* dirlist_job; + std::vector fileinfoJobs_; + FileSystemInfoJob* fsInfoJob_; + + std::shared_ptr volumeManager_; + + /* for file monitor */ + bool has_idle_reload_handler; + bool has_idle_update_handler; + std::vector paths_to_add; + std::vector paths_to_update; + std::vector 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, std::hash> 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, FilePathHash> cache_; + static FilePath cutFilesDirPath_; + static FilePath lastCutFilesDirPath_; + static std::shared_ptr cutFilesHashSet_; + static std::mutex mutex_; +}; + +} + +#endif // __LIBFM_QT_FM2_FOLDER_H__ diff --git a/src/core/gioptrs.h b/src/core/gioptrs.h new file mode 100644 index 0000000..401424b --- /dev/null +++ b/src/core/gioptrs.h @@ -0,0 +1,137 @@ +#ifndef GIOPTRS_H +#define GIOPTRS_H +// define smart pointers for GIO data types + +#include +#include +#include "gobjectptr.h" +#include "cstrptr.h" + +namespace Fm { + +typedef GObjectPtr GFilePtr; +typedef GObjectPtr GFileInfoPtr; +typedef GObjectPtr GFileMonitorPtr; +typedef GObjectPtr GCancellablePtr; +typedef GObjectPtr GFileEnumeratorPtr; + +typedef GObjectPtr GInputStreamPtr; +typedef GObjectPtr GFileInputStreamPtr; +typedef GObjectPtr GOutputStreamPtr; +typedef GObjectPtr GFileOutputStreamPtr; + +typedef GObjectPtr GIconPtr; + +typedef GObjectPtr GVolumeMonitorPtr; +typedef GObjectPtr GVolumePtr; +typedef GObjectPtr GMountPtr; + +typedef GObjectPtr 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 diff --git a/src/core/gobjectptr.h b/src/core/gobjectptr.h new file mode 100644 index 0000000..333bcc2 --- /dev/null +++ b/src/core/gobjectptr.h @@ -0,0 +1,104 @@ +#ifndef FM2_GOBJECTPTR_H +#define FM2_GOBJECTPTR_H + +#include "../libfmqtglobals.h" +#include +#include +#include +#include + +namespace Fm { + +template +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(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(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(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 diff --git a/src/core/iconinfo.cpp b/src/core/iconinfo.cpp new file mode 100644 index 0000000..5ec2abb --- /dev/null +++ b/src/core/iconinfo.cpp @@ -0,0 +1,138 @@ +#include "iconinfo.h" +#include "iconinfo_p.h" + +namespace Fm { + +std::unordered_map, 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 IconInfo::fromName(const char* name) { + GObjectPtr gicon{g_themed_icon_new(name), false}; + return fromGIcon(gicon); +} + +// static +std::shared_ptr IconInfo::fromGIcon(GIconPtr gicon) { + if(Q_LIKELY(gicon)) { + std::lock_guard 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(std::move(gicon)); + cache_.insert(std::make_pair(icon->gicon_.get(), icon)); + return icon; + } + return std::shared_ptr{}; +} + +void IconInfo::updateQIcons() { + std::lock_guard 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> IconInfo::emblems() const { + std::forward_list> 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 diff --git a/src/core/iconinfo.h b/src/core/iconinfo.h new file mode 100644 index 0000000..60924b1 --- /dev/null +++ b/src/core/iconinfo.h @@ -0,0 +1,112 @@ +/* + * fm-icon.h + * + * Copyright 2009 Hong Jen Yee (PCMan) + * Copyright 2013 Andriy Grytsenko (LStranger) + * + * 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 +#include "gioptrs.h" +#include +#include +#include +#include +#include + + +namespace Fm { + +class LIBFM_QT_API IconInfo: public std::enable_shared_from_this { +public: + friend class IconEngine; + + explicit IconInfo() {} + + explicit IconInfo(const char* name); + + explicit IconInfo(const GIconPtr gicon); + + ~IconInfo(); + + static std::shared_ptr fromName(const char* name); + + static std::shared_ptr fromGIcon(GIconPtr gicon); + + static std::shared_ptr 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> 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, GIconHash, GIconEqual> cache_; + static std::mutex mutex_; + static QIcon fallbackQicon_; +}; + +} // namespace Fm + +Q_DECLARE_METATYPE(std::shared_ptr) + +#endif /* __FM2_ICON_INFO_H__ */ diff --git a/src/core/iconinfo_p.h b/src/core/iconinfo_p.h new file mode 100644 index 0000000..dd03207 --- /dev/null +++ b/src/core/iconinfo_p.h @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2016 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_ICONENGINE_H +#define FM_ICONENGINE_H + +#include +#include +#include "../libfmqtglobals.h" +#include "iconinfo.h" +#include + +namespace Fm { + +class IconEngine: public QIconEngine { +public: + + IconEngine(std::shared_ptr 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 info_; + bool transparent_; +}; + +IconEngine::IconEngine(std::shared_ptr 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(data); + args->sizes = info ? info->internalQicon().availableSizes(args->mode, args->state) : QList{}; + break; + } + case QIconEngine::IconNameHook: { + QString* result = reinterpret_cast(data); + *result = info ? info->internalQicon().name() : QString{}; + break; + } +#if QT_VERSION >= QT_VERSION_CHECK(5, 7, 0) + case QIconEngine::IsNullHook: { + bool* result = reinterpret_cast(data); + *result = info ? info->internalQicon().isNull() : true; + break; + } +#endif + } +} + +} // namespace Fm + +#endif // FM_ICONENGINE_H diff --git a/src/core/job.cpp b/src/core/job.cpp new file mode 100644 index 0000000..e597975 --- /dev/null +++ b/src/core/job.cpp @@ -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 diff --git a/src/core/job.h b/src/core/job.h new file mode 100644 index 0000000..00ca4a5 --- /dev/null +++ b/src/core/job.h @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2016 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef __LIBFM_QT_FM_JOB_H__ +#define __LIBFM_QT_FM_JOB_H__ + +#include +#include +#include +#include +#include +#include +#include +#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__ diff --git a/src/core/job_p.h b/src/core/job_p.h new file mode 100644 index 0000000..d893d5f --- /dev/null +++ b/src/core/job_p.h @@ -0,0 +1,26 @@ +#ifndef JOB_P_H +#define JOB_P_H + +#include +#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 diff --git a/src/core/mimetype.cpp b/src/core/mimetype.cpp new file mode 100644 index 0000000..19345bb --- /dev/null +++ b/src/core/mimetype.cpp @@ -0,0 +1,64 @@ +#include "mimetype.h" +#include + +#include +#include + +using namespace std; + +namespace Fm { + +std::unordered_map, CStrHash, CStrEqual> MimeType::cache_; +std::mutex MimeType::mutex_; + +std::shared_ptr MimeType::inodeDirectory_; // inode/directory +std::shared_ptr MimeType::inodeShortcut_; // inode/x-shortcut +std::shared_ptr MimeType::inodeMountPoint_; // inode/mount-point +std::shared_ptr MimeType::desktopEntry_; // application/x-desktop + + +MimeType::MimeType(const char* typeName): + name_{g_strdup(typeName)}, + desc_{nullptr} { + + GObjectPtr 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 MimeType::fromName(const char* typeName) { + std::shared_ptr ret; + std::lock_guard lock(mutex_); + auto it = cache_.find(typeName); + if(it == cache_.end()) { + ret = std::make_shared(typeName); + cache_.insert(std::make_pair(ret->name_.get(), ret)); + } + else { + ret = it->second; + } + return ret; +} + +// static +std::shared_ptr 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 diff --git a/src/core/mimetype.h b/src/core/mimetype.h new file mode 100644 index 0000000..887ce08 --- /dev/null +++ b/src/core/mimetype.h @@ -0,0 +1,172 @@ +/* + * fm-mime-type.h + * + * Copyright 2009 - 2012 Hong Jen Yee (PCMan) + * + * 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 +#include + +#include +#include +#include +#include +#include +#include +#include + +#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 firstThumbnailer() const { + std::lock_guard lock{mutex_}; + return thumbnailers_.empty() ? nullptr : thumbnailers_.front(); + } + + void forEachThumbnailer(std::function&)> func) const { + std::lock_guard lock{mutex_}; + for(auto& thumbnailer: thumbnailers_) { + if(func(thumbnailer)) { + break; + } + } + } + + const std::shared_ptr& 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 fromName(const char* typeName); + + static std::shared_ptr 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 inodeDirectory() { // inode/directory + if(!inodeDirectory_) + inodeDirectory_ = fromName("inode/directory"); + return inodeDirectory_; + } + + static std::shared_ptr inodeShortcut() { // inode/x-shortcut + if(!inodeShortcut_) + inodeShortcut_ = fromName("inode/x-shortcut"); + return inodeShortcut_; + } + + static std::shared_ptr inodeMountPoint() { // inode/mount-point + if(!inodeMountPoint_) + inodeMountPoint_ = fromName("inode/mount-point"); + return inodeMountPoint_; + } + + static std::shared_ptr desktopEntry() { // application/x-desktop + if(!desktopEntry_) + desktopEntry_ = fromName("application/x-desktop"); + return desktopEntry_; + } + +private: + void removeThumbnailer(std::shared_ptr& thumbnailer) { + std::lock_guard lock{mutex_}; + thumbnailers_.remove(thumbnailer); + } + + void addThumbnailer(std::shared_ptr thumbnailer) { + std::lock_guard lock{mutex_}; + thumbnailers_.push_front(std::move(thumbnailer)); + } + +private: + std::shared_ptr icon_; + CStrPtr name_; + mutable CStrPtr desc_; + std::forward_list> thumbnailers_; + static std::unordered_map, CStrHash, CStrEqual> cache_; + static std::mutex mutex_; + + static std::shared_ptr inodeDirectory_; // inode/directory + static std::shared_ptr inodeShortcut_; // inode/x-shortcut + static std::shared_ptr inodeMountPoint_; // inode/mount-point + static std::shared_ptr desktopEntry_; // application/x-desktop +}; + + +} // namespace Fm + +#endif diff --git a/src/core/terminal.cpp b/src/core/terminal.cpp new file mode 100644 index 0000000..ae65f3d --- /dev/null +++ b/src/core/terminal.cpp @@ -0,0 +1,127 @@ +#include "terminal.h" + +namespace Fm { + +#include +#include +#include +#include + +#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 allKnownTerminals() { + std::vector 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 diff --git a/src/core/terminal.h b/src/core/terminal.h new file mode 100644 index 0000000..86c3f4b --- /dev/null +++ b/src/core/terminal.h @@ -0,0 +1,17 @@ +#ifndef TERMINAL_H +#define TERMINAL_H + +#include "../libfmqtglobals.h" +#include "gioptrs.h" +#include "filepath.h" +#include + +namespace Fm { + +LIBFM_QT_API bool launchTerminal(const char* programName, const FilePath& workingDir, GErrorPtr& error); + +LIBFM_QT_API std::vector allKnownTerminals(); + +} // namespace Fm + +#endif // TERMINAL_H diff --git a/src/core/thumbnailer.cpp b/src/core/thumbnailer.cpp new file mode 100644 index 0000000..8aedd75 --- /dev/null +++ b/src/core/thumbnailer.cpp @@ -0,0 +1,140 @@ +#include "thumbnailer.h" +#include "mimetype.h" +#include +#include +#include +#include + +namespace Fm { + +std::mutex Thumbnailer::mutex_; +std::vector> 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& 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 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 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(base_name.c_str(), kf); + char** mime_types = g_key_file_get_string_list(kf, "Thumbnailer Entry", "MimeType", nullptr, nullptr); + if(mime_types && thumbnailer->exec_) { + for(char** name = mime_types; *name; ++name) { + auto mime_type = MimeType::fromName(*name); + if(mime_type) { + thumbnailer->mimeTypes_.push_back(mime_type); + std::const_pointer_cast(mime_type)->addThumbnailer(thumbnailer); + } + } + g_strfreev(mime_types); + } + allThumbnailers_.push_back(std::move(thumbnailer)); + } + } + g_key_file_free(kf); + } +} + +} // namespace Fm diff --git a/src/core/thumbnailer.h b/src/core/thumbnailer.h new file mode 100644 index 0000000..c1a7328 --- /dev/null +++ b/src/core/thumbnailer.h @@ -0,0 +1,37 @@ +#ifndef FM2_THUMBNAILER_H +#define FM2_THUMBNAILER_H + +#include "../libfmqtglobals.h" +#include "cstrptr.h" +#include +#include +#include +#include + +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> mimeTypes_; + + static std::mutex mutex_; + static std::vector> allThumbnailers_; +}; + +} // namespace Fm + +#endif // FM2_THUMBNAILER_H diff --git a/src/core/thumbnailjob.cpp b/src/core/thumbnailjob.cpp new file mode 100644 index 0000000..61eccff --- /dev/null +++ b/src/core/thumbnailjob.cpp @@ -0,0 +1,266 @@ +#include "thumbnailjob.h" +#include +#include +#include +#include +#include +#include +#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 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 &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(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& 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& 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& 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& 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 diff --git a/src/core/thumbnailjob.h b/src/core/thumbnailjob.h new file mode 100644 index 0000000..33dbe0a --- /dev/null +++ b/src/core/thumbnailjob.h @@ -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 + +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& results() const { + return results_; + } + +Q_SIGNALS: + void thumbnailLoaded(const std::shared_ptr& file, int size, QImage thumbnail); + +protected: + + void exec() override; + +private: + + bool isSupportedImageType(const std::shared_ptr& mimeType) const; + + bool isThumbnailOutdated(const std::shared_ptr& file, const QImage& thumbnail) const; + + QImage generateThumbnail(const std::shared_ptr& file, const FilePath& origPath, const char* uri, const QString& thumbnailFilename); + + QImage readImageFromStream(GInputStream* stream, size_t len); + + QImage loadForFile(const std::shared_ptr& file); + + bool readJpegExif(GInputStream* stream, QImage& thumbnail, int& rotate_degrees); + +private: + FileInfoList files_; + int size_; + std::vector results_; + GCancellablePtr cancellable_; + GChecksum* md5Calc_; + + static QThreadPool* threadPool_; + + static bool localFilesOnly_; + static int maxThumbnailFileSize_; +}; + +} // namespace Fm + +#endif // FM2_THUMBNAILJOB_H diff --git a/src/core/totalsizejob.cpp b/src/core/totalsizejob.cpp new file mode 100644 index 0000000..81ff0db --- /dev/null +++ b/src/core/totalsizejob.cpp @@ -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 diff --git a/src/core/totalsizejob.h b/src/core/totalsizejob.h new file mode 100644 index 0000000..43c19c0 --- /dev/null +++ b/src/core/totalsizejob.h @@ -0,0 +1,56 @@ +#ifndef FM2_TOTALSIZEJOB_H +#define FM2_TOTALSIZEJOB_H + +#include "../libfmqtglobals.h" +#include "fileoperationjob.h" +#include "filepath.h" +#include +#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 diff --git a/src/core/trashjob.cpp b/src/core/trashjob.cpp new file mode 100644 index 0000000..31a5c3c --- /dev/null +++ b/src/core/trashjob.cpp @@ -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 diff --git a/src/core/trashjob.h b/src/core/trashjob.h new file mode 100644 index 0000000..519fdd8 --- /dev/null +++ b/src/core/trashjob.h @@ -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 diff --git a/src/core/untrashjob.cpp b/src/core/untrashjob.cpp new file mode 100644 index 0000000..14148af --- /dev/null +++ b/src/core/untrashjob.cpp @@ -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 diff --git a/src/core/untrashjob.h b/src/core/untrashjob.h new file mode 100644 index 0000000..1574abd --- /dev/null +++ b/src/core/untrashjob.h @@ -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 diff --git a/src/core/userinfocache.cpp b/src/core/userinfocache.cpp new file mode 100644 index 0000000..1c237f0 --- /dev/null +++ b/src/core/userinfocache.cpp @@ -0,0 +1,47 @@ +#include "userinfocache.h" +#include +#include + +namespace Fm { + +UserInfoCache* UserInfoCache::globalInstance_ = nullptr; +std::mutex UserInfoCache::mutex_; + +UserInfoCache::UserInfoCache() : QObject() { +} + +const std::shared_ptr& UserInfoCache::userFromId(uid_t uid) { + std::lock_guard lock{mutex_}; + auto it = users_.find(uid); + if(it != users_.end()) + return it->second; + std::shared_ptr user; + auto pw = getpwuid(uid); + if(pw) { + user = std::make_shared(uid, pw->pw_name, pw->pw_gecos); + } + return (users_[uid] = user); +} + +const std::shared_ptr& UserInfoCache::groupFromId(gid_t gid) { + std::lock_guard lock{mutex_}; + auto it = groups_.find(gid); + if(it != groups_.end()) + return it->second; + std::shared_ptr group; + auto gr = getgrgid(gid); + if(gr) { + group = std::make_shared(gid, gr->gr_name); + } + return (groups_[gid] = group); +} + +// static +UserInfoCache* UserInfoCache::globalInstance() { + std::lock_guard lock{mutex_}; + if(!globalInstance_) + globalInstance_ = new UserInfoCache(); + return globalInstance_; +} + +} // namespace Fm diff --git a/src/core/userinfocache.h b/src/core/userinfocache.h new file mode 100644 index 0000000..7338463 --- /dev/null +++ b/src/core/userinfocache.h @@ -0,0 +1,82 @@ +#ifndef FM2_USERINFOCACHE_H +#define FM2_USERINFOCACHE_H + +#include "../libfmqtglobals.h" +#include +#include +#include +#include +#include +#include + +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& userFromId(uid_t uid); + + const std::shared_ptr& groupFromId(gid_t gid); + + static UserInfoCache* globalInstance(); + +Q_SIGNALS: + void changed(); + +private: + std::unordered_map> users_; + std::unordered_map> groups_; + static UserInfoCache* globalInstance_; + static std::mutex mutex_; +}; + +} // namespace Fm + +#endif // FM2_USERINFOCACHE_H diff --git a/src/core/volumemanager.cpp b/src/core/volumemanager.cpp new file mode 100644 index 0000000..522c38c --- /dev/null +++ b/src/core/volumemanager.cpp @@ -0,0 +1,111 @@ +#include "volumemanager.h" + +namespace Fm { + +std::mutex VolumeManager::mutex_; +std::weak_ptr 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::globalInstance() { + std::lock_guard lock{mutex_}; + auto mon = globalInstance_.lock(); + if(mon == nullptr) { + mon = std::make_shared(); + globalInstance_ = mon; + } + return mon; +} + +void VolumeManager::onGetGVolumeMonitorFinished() { + auto job = static_cast(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 diff --git a/src/core/volumemanager.h b/src/core/volumemanager.h new file mode 100644 index 0000000..642adc8 --- /dev/null +++ b/src/core/volumemanager.h @@ -0,0 +1,237 @@ +#ifndef FM2_VOLUMEMANAGER_H +#define FM2_VOLUMEMANAGER_H + +#include "../libfmqtglobals.h" +#include +#include +#include "gioptrs.h" +#include "filepath.h" +#include "iconinfo.h" +#include "job.h" +#include +#include + +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 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 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& volumes() const { + return volumes_; + } + + const std::vector& mounts() const { + return mounts_; + } + + static std::shared_ptr 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 volumes_; + std::vector mounts_; + + static std::mutex mutex_; + static std::weak_ptr globalInstance_; +}; + +} // namespace Fm + +#endif // FM2_VOLUMEMANAGER_H diff --git a/src/createnewmenu.cpp b/src/createnewmenu.cpp new file mode 100644 index 0000000..9191201 --- /dev/null +++ b/src/createnewmenu.cpp @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "createnewmenu.h" +#include "folderview.h" +#include "icontheme.h" +#include "utilities.h" +#include "core/iconinfo.h" + +namespace Fm { + +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); + + // 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_); + } +} + +void CreateNewMenu::onCreateNewFolder() { + if(dirPath_) { + createFileOrFolder(CreateNewFolder, dirPath_); + } +} + +void CreateNewMenu::onCreateNew() { + QAction* action = static_cast(sender()); + QByteArray name = action->objectName().toUtf8(); + GList* templates = fm_template_list_all(fm_config->only_user_templates); + FmTemplate* templ = nullptr; + for(GList* l = templates; l; l = l->next) { + FmTemplate* t = (FmTemplate*)l->data; + if(name == fm_template_get_name(t, nullptr)) { + templ = t; + break; + } + } + if(templ) { // template found + if(dirPath_) { + createFileOrFolder(CreateWithTemplate, dirPath_, templ, dialogParent_); + } + } +} + +} // namespace Fm diff --git a/src/createnewmenu.h b/src/createnewmenu.h new file mode 100644 index 0000000..d2e7282 --- /dev/null +++ b/src/createnewmenu.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_CREATENEWMENU_H +#define FM_CREATENEWMENU_H + +#include "libfmqtglobals.h" +#include +#include + +#include "core/filepath.h" + +namespace Fm { + +class FolderView; + +class LIBFM_QT_API CreateNewMenu : public QMenu { + Q_OBJECT + +public: + explicit CreateNewMenu(QWidget* dialogParent, Fm::FilePath dirPath, QWidget* parent = 0); + virtual ~CreateNewMenu(); + +protected Q_SLOTS: + void onCreateNewFolder(); + void onCreateNewFile(); + void onCreateNew(); + +private: + QWidget* dialogParent_; + Fm::FilePath dirPath_; +}; + +} + +#endif // FM_CREATENEWMENU_H diff --git a/src/customaction_p.h b/src/customaction_p.h new file mode 100644 index 0000000..7aba3f9 --- /dev/null +++ b/src/customaction_p.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_CUSTOMACTION_P_H +#define FM_CUSTOMACTION_P_H + +#include +#include "customactions/fileaction.h" + +namespace Fm { + +class CustomAction : public QAction { +public: + explicit CustomAction(std::shared_ptr 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& item() const { + return item_; + } + +private: + std::shared_ptr item_; +}; + +} // namespace Fm + +#endif diff --git a/src/customactions/fileaction.cpp b/src/customactions/fileaction.cpp new file mode 100644 index 0000000..31d5e3a --- /dev/null +++ b/src/customactions/fileaction.cpp @@ -0,0 +1,615 @@ +#include "fileaction.h" +#include +#include + +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, 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 {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 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(kf, *profile_name)); + } + } +} + +std::shared_ptr 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::fromActionObject(std::shared_ptr action_obj, const FileInfoList& files) { + std::shared_ptr item; + if(action_obj->type == FileActionType::MENU) { + auto menu = static_pointer_cast(action_obj); + if(menu->match(files)) { + item = make_shared(menu, files); + // eliminate empty menus + if(item->children.empty()) { + item = nullptr; + } + } + } + else { + // handle profiles here + auto action = static_pointer_cast(action_obj); + auto profile = action->match(files); + if(profile) { + item = make_shared(action, profile, files); + } + } + return item; +} + +FileActionItem::FileActionItem(std::shared_ptr _action, std::shared_ptr _profile, const FileInfoList& files): + FileActionItem{static_pointer_cast(_action), files} { + profile = _profile; +} + +FileActionItem::FileActionItem(std::shared_ptr menu, const FileInfoList& files): + FileActionItem{static_pointer_cast(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 _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 action; + if(strcmp(type.get(), "Action") == 0) { + action = static_pointer_cast(make_shared(kf)); + // stdout.printf("load action: %s\n", id); + } + else if(strcmp(type.get(), "Menu") == 0) { + action = static_pointer_cast(make_shared(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 a, std::shared_ptr 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(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(action_obj); + menu->cached_children.clear(); + } + } + + std::sort(items.begin(), items.end(), compare_items); + return items; +} + +} // namespace Fm diff --git a/src/customactions/fileaction.h b/src/customactions/fileaction.h new file mode 100644 index 0000000..4c2a8bc --- /dev/null +++ b/src/customactions/fileaction.h @@ -0,0 +1,156 @@ +#ifndef FILEACTION_H +#define FILEACTION_H + +#include +#include + +#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 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 condition; + + // values cached during menu generation + bool has_parent; +}; + + +class FileAction: public FileActionObject { +public: + + FileAction(GKeyFile* kf); + + std::shared_ptr match(const FileInfoList& files) const; + + int target; // bitwise or of FileActionTarget + CStrPtr toolbar_label; + + // FIXME: currently we don't support dynamic profiles + std::vector> 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> cached_children; +}; + + +class FileActionItem { +public: + + static std::shared_ptr fromActionObject(std::shared_ptr action_obj, const FileInfoList &files); + + FileActionItem(std::shared_ptr _action, std::shared_ptr _profile, const FileInfoList& files); + + FileActionItem(std::shared_ptr menu, const FileInfoList& files); + + FileActionItem(std::shared_ptr _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(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>& get_sub_items() const { + return children; + } + + static bool compare_items(std::shared_ptr a, std::shared_ptr b); + static std::vector> get_actions_for_files(const FileInfoList& files); + + std::string name; + std::string desc; + std::string icon; + std::shared_ptr action; + std::shared_ptr profile; // only used by action item + std::vector> children; // only used by menu +}; + +typedef std::vector> FileActionItemList; + +} // namespace Fm + + +#endif // FILEACTION_H diff --git a/src/customactions/fileactioncondition.cpp b/src/customactions/fileactioncondition.cpp new file mode 100644 index 0000000..16e545b --- /dev/null +++ b/src/customactions/fileactioncondition.cpp @@ -0,0 +1,503 @@ +#include "fileactioncondition.h" +#include "fileaction.h" +#include + + +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 diff --git a/src/customactions/fileactioncondition.h b/src/customactions/fileactioncondition.h new file mode 100644 index 0000000..5390cdd --- /dev/null +++ b/src/customactions/fileactioncondition.h @@ -0,0 +1,123 @@ +#ifndef FILEACTIONCONDITION_H +#define FILEACTIONCONDITION_H + +#include +#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 diff --git a/src/customactions/fileactionprofile.cpp b/src/customactions/fileactionprofile.cpp new file mode 100644 index 0000000..e356703 --- /dev/null +++ b/src/customactions/fileactionprofile.cpp @@ -0,0 +1,121 @@ +#include "fileactionprofile.h" +#include "fileaction.h" +#include + +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(kf, group_name.c_str()); +} + + +bool FileActionProfile::launch_once(GAppLaunchContext* /*ctx*/, std::shared_ptr 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); +} + +} diff --git a/src/customactions/fileactionprofile.h b/src/customactions/fileactionprofile.h new file mode 100644 index 0000000..18b959f --- /dev/null +++ b/src/customactions/fileactionprofile.h @@ -0,0 +1,45 @@ +#ifndef FILEACTIONPROFILE_H +#define FILEACTIONPROFILE_H + + +#include +#include +#include + +#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 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 condition; +}; + +} // namespace Fm + +#endif // FILEACTIONPROFILE_H diff --git a/src/dirtreemodel.cpp b/src/dirtreemodel.cpp new file mode 100644 index 0000000..2d87de9 --- /dev/null +++ b/src/dirtreemodel.cpp @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "dirtreemodel.h" +#include "dirtreemodelitem.h" +#include +#include "core/fileinfojob.h" + +namespace Fm { + +DirTreeModel::DirTreeModel(QObject* parent): + QAbstractItemModel(parent), + showHidden_(false) { +} + +DirTreeModel::~DirTreeModel() { +} + +void DirTreeModel::addRoots(Fm::FilePathList rootPaths) { + auto job = new Fm::FileInfoJob{std::move(rootPaths)}; + job->setAutoDelete(true); + connect(job, &Fm::FileInfoJob::finished, this, &DirTreeModel::onFileInfoJobFinished, Qt::BlockingQueuedConnection); + job->runAsync(); +} + +void DirTreeModel::onFileInfoJobFinished() { + auto job = static_cast(sender()); + for(auto file: job->files()) { + addRoot(std::move(file)); + } +} + +// QAbstractItemModel implementation + +Qt::ItemFlags DirTreeModel::flags(const QModelIndex& index) const { + DirTreeModelItem* item = itemFromIndex(index); + if(item && item->isPlaceHolder()) { + return Qt::ItemIsEnabled; + } + return QAbstractItemModel::flags(index); +} + +QVariant DirTreeModel::data(const QModelIndex& index, int role) const { + if(!index.isValid() || index.column() > 1) { + return QVariant(); + } + DirTreeModelItem* item = itemFromIndex(index); + if(item) { + auto info = item->fileInfo_; + switch(role) { + case Qt::ToolTipRole: + return QVariant(item->displayName_); + case Qt::DisplayRole: + return QVariant(item->displayName_); + case Qt::DecorationRole: + return QVariant(item->icon_); + case FileInfoRole: { + QVariant v; + v.setValue(info); + return v; + } + } + } + return QVariant(); +} + +int DirTreeModel::columnCount(const QModelIndex& /*parent*/) const { + return 1; +} + +int DirTreeModel::rowCount(const QModelIndex& parent) const { + if(!parent.isValid()) { + return rootItems_.size(); + } + DirTreeModelItem* item = itemFromIndex(parent); + if(item) { + return item->children_.size(); + } + return 0; +} + +QModelIndex DirTreeModel::parent(const QModelIndex& child) const { + DirTreeModelItem* item = itemFromIndex(child); + if(item && item->parent_) { + item = item->parent_; // go to parent item + if(item) { + const auto& items = item->parent_ ? item->parent_->children_ : rootItems_; + auto it = std::find(items.cbegin(), items.cend(), item); + if(it != items.cend()) { + int row = it - items.cbegin(); + return createIndex(row, 0, (void*)item); + } + } + } + return QModelIndex(); +} + +QModelIndex DirTreeModel::index(int row, int column, const QModelIndex& parent) const { + if(row >= 0 && column >= 0 && column == 0) { + if(!parent.isValid()) { // root items + if(static_cast(row) < rootItems_.size()) { + const DirTreeModelItem* item = rootItems_.at(row); + return createIndex(row, column, (void*)item); + } + } + else { // child items + DirTreeModelItem* parentItem = itemFromIndex(parent); + if(static_cast(row) < parentItem->children_.size()) { + const DirTreeModelItem* item = parentItem->children_.at(row); + return createIndex(row, column, (void*)item); + } + } + } + return QModelIndex(); // invalid index +} + +bool DirTreeModel::hasChildren(const QModelIndex& parent) const { + DirTreeModelItem* item = itemFromIndex(parent); + return item ? !item->isPlaceHolder() : true; +} + +QModelIndex DirTreeModel::indexFromItem(DirTreeModelItem* item) const { + Q_ASSERT(item); + const auto& items = item->parent_ ? item->parent_->children_ : rootItems_; + auto it = std::find(items.cbegin(), items.cend(), item); + if(it != items.cend()) { + int row = it - items.cbegin(); + return createIndex(row, 0, (void*)item); + } + return QModelIndex(); +} + +// public APIs +QModelIndex DirTreeModel::addRoot(std::shared_ptr root) { + DirTreeModelItem* item = new DirTreeModelItem(std::move(root), this); + int row = rootItems_.size(); + beginInsertRows(QModelIndex(), row, row); + rootItems_.push_back(item); + // add_place_holder_child_item(model, item_l, nullptr, FALSE); + endInsertRows(); + return createIndex(row, 0, (void*)item); +} + +DirTreeModelItem* DirTreeModel::itemFromIndex(const QModelIndex& index) const { + return reinterpret_cast(index.internalPointer()); +} + +QModelIndex DirTreeModel::indexFromPath(const Fm::FilePath &path) const { + DirTreeModelItem* item = itemFromPath(path); + return item ? item->index() : QModelIndex(); +} + +DirTreeModelItem* DirTreeModel::itemFromPath(const Fm::FilePath &path) const { + Q_FOREACH(DirTreeModelItem* item, rootItems_) { + if(item->fileInfo_ && path == item->fileInfo_->path()) { + return item; + } + else { + DirTreeModelItem* child = item->childFromPath(path, true); + if(child) { + return child; + } + } + } + return nullptr; +} + + +void DirTreeModel::loadRow(const QModelIndex& index) { + DirTreeModelItem* item = itemFromIndex(index); + Q_ASSERT(item); + if(item && !item->isPlaceHolder()) { + item->loadFolder(); + } +} + +void DirTreeModel::unloadRow(const QModelIndex& index) { + DirTreeModelItem* item = itemFromIndex(index); + if(item && !item->isPlaceHolder()) { + item->unloadFolder(); + } +} + +bool DirTreeModel::isLoaded(const QModelIndex& index) { + DirTreeModelItem* item = itemFromIndex(index); + return item ? item->loaded_ : false; +} + +QIcon DirTreeModel::icon(const QModelIndex& index) { + DirTreeModelItem* item = itemFromIndex(index); + return item ? item->icon_ : QIcon(); +} + +std::shared_ptr DirTreeModel::fileInfo(const QModelIndex& index) { + DirTreeModelItem* item = itemFromIndex(index); + return item ? item->fileInfo_ : nullptr; +} + +Fm::FilePath DirTreeModel::filePath(const QModelIndex& index) { + DirTreeModelItem* item = itemFromIndex(index); + return (item && item->fileInfo_) ? item->fileInfo_->path() : Fm::FilePath{}; +} + +QString DirTreeModel::dispName(const QModelIndex& index) { + DirTreeModelItem* item = itemFromIndex(index); + return item ? item->displayName_ : QString(); +} + +void DirTreeModel::setShowHidden(bool show_hidden) { + showHidden_ = show_hidden; + Q_FOREACH(DirTreeModelItem* item, rootItems_) { + item->setShowHidden(show_hidden); + } +} + + +} // namespace Fm diff --git a/src/dirtreemodel.h b/src/dirtreemodel.h new file mode 100644 index 0000000..c254713 --- /dev/null +++ b/src/dirtreemodel.h @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_DIRTREEMODEL_H +#define FM_DIRTREEMODEL_H + +#include "libfmqtglobals.h" +#include +#include +#include +#include +#include +#include +#include + +#include "core/fileinfo.h" +#include "core/filepath.h" + +namespace Fm { + +class DirTreeModelItem; +class DirTreeView; + +class LIBFM_QT_API DirTreeModel : public QAbstractItemModel { + Q_OBJECT + +public: + friend class DirTreeModelItem; // allow direct access of private members in DirTreeModelItem + friend class DirTreeView; // allow direct access of private members in DirTreeView + + enum Role { + FileInfoRole = Qt::UserRole + }; + + explicit DirTreeModel(QObject* parent); + ~DirTreeModel(); + + void addRoots(Fm::FilePathList rootPaths); + + void loadRow(const QModelIndex& index); + void unloadRow(const QModelIndex& index); + + bool isLoaded(const QModelIndex& index); + QIcon icon(const QModelIndex& index); + std::shared_ptr fileInfo(const QModelIndex& index); + Fm::FilePath filePath(const QModelIndex& index); + QString dispName(const QModelIndex& index); + + void setShowHidden(bool show_hidden); + bool showHidden() const { + return showHidden_; + } + + QModelIndex indexFromPath(const Fm::FilePath& path) const; + + virtual Qt::ItemFlags flags(const QModelIndex& index) const; + virtual QVariant data(const QModelIndex& index, int role) const; + virtual int columnCount(const QModelIndex& parent) const; + virtual int rowCount(const QModelIndex& parent) const; + virtual QModelIndex parent(const QModelIndex& child) const; + virtual QModelIndex index(int row, int column, const QModelIndex& parent) const; + virtual bool hasChildren(const QModelIndex& parent = QModelIndex()) const; + +Q_SIGNALS: + void rowLoaded(const QModelIndex& index); + +private Q_SLOTS: + void onFileInfoJobFinished(); + +private: + QModelIndex addRoot(std::shared_ptr root); + + DirTreeModelItem* itemFromPath(const Fm::FilePath& path) const; + DirTreeModelItem* itemFromIndex(const QModelIndex& index) const; + QModelIndex indexFromItem(DirTreeModelItem* item) const; + +private: + bool showHidden_; + std::vector rootItems_; +}; + +} + +#endif // FM_DIRTREEMODEL_H diff --git a/src/dirtreemodelitem.cpp b/src/dirtreemodelitem.cpp new file mode 100644 index 0000000..52dc1dd --- /dev/null +++ b/src/dirtreemodelitem.cpp @@ -0,0 +1,417 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "dirtreemodelitem.h" +#include "dirtreemodel.h" +#include "icontheme.h" +#include + +namespace Fm { + +DirTreeModelItem::DirTreeModelItem(): + fileInfo_(nullptr), + folder_(nullptr), + expanded_(false), + loaded_(false), + parent_(nullptr), + placeHolderChild_(nullptr), + model_(nullptr), + queuedForDeletion_(false) { +} + +DirTreeModelItem::DirTreeModelItem(std::shared_ptr info, DirTreeModel* model, DirTreeModelItem* parent): + fileInfo_{std::move(info)}, + expanded_(false), + loaded_(false), + parent_(parent), + placeHolderChild_(nullptr), + model_(model), + queuedForDeletion_(false) { + + if(fileInfo_) { + displayName_ = fileInfo_->displayName(); + icon_ = fileInfo_->icon()->qicon(); + addPlaceHolderChild(); + } +} + +DirTreeModelItem::~DirTreeModelItem() { + freeFolder(); + // delete child items if needed + if(!children_.empty()) { + Q_FOREACH(DirTreeModelItem* item, children_) { + delete item; + } + } + if(!hiddenChildren_.empty()) { + Q_FOREACH(DirTreeModelItem* item, hiddenChildren_) { + delete item; + } + } + /*if(queuedForDeletion_) + qDebug() << "queued deletion done";*/ +} + +void DirTreeModelItem::freeFolder() { + if(folder_) { + QObject::disconnect(onFolderFinishLoadingConn_); + QObject::disconnect(onFolderFilesAddedConn_); + QObject::disconnect(onFolderFilesRemovedConn_); + QObject::disconnect(onFolderFilesChangedConn_); + folder_.reset(); + } +} + +void DirTreeModelItem::addPlaceHolderChild() { + placeHolderChild_ = new DirTreeModelItem(); + placeHolderChild_->parent_ = this; + placeHolderChild_->model_ = model_; + placeHolderChild_->displayName_ = DirTreeModel::tr("Loading..."); + children_.push_back(placeHolderChild_); +} + +void DirTreeModelItem::loadFolder() { + if(!expanded_) { + /* dynamically load content of the folder. */ + folder_ = Fm::Folder::fromPath(fileInfo_->path()); + /* g_debug("fm_dir_tree_model_load_row()"); */ + /* associate the data with loaded handler */ + + onFolderFinishLoadingConn_ = QObject::connect(folder_.get(), &Fm::Folder::finishLoading, model_, [=]() { + onFolderFinishLoading(); + }); + onFolderFilesAddedConn_ = QObject::connect(folder_.get(), &Fm::Folder::filesAdded, model_, [=](Fm::FileInfoList files) { + onFolderFilesAdded(files); + }); + onFolderFilesRemovedConn_ = QObject::connect(folder_.get(), &Fm::Folder::filesRemoved, model_, [=](Fm::FileInfoList files) { + onFolderFilesRemoved(files); + }); + onFolderFilesChangedConn_ = QObject::connect(folder_.get(), &Fm::Folder::filesChanged, model_, [=](std::vector& changes) { + onFolderFilesChanged(changes); + }); + + /* set 'expanded' flag beforehand as callback may check it */ + expanded_ = true; + /* if the folder is already loaded, call "loaded" handler ourselves */ + if(folder_->isLoaded()) { // already loaded + insertFiles(folder_->files()); + onFolderFinishLoading(); + } + } +} + +void DirTreeModelItem::unloadFolder() { + if(expanded_) { /* do some cleanup */ + /* remove all children, and replace them with a dummy child + * item to keep expander in the tree view around. */ + + // delete all visible child items + model_->beginRemoveRows(index(), 0, children_.size() - 1); + if(!children_.empty()) { + Q_FOREACH(DirTreeModelItem* item, children_) { + delete item; + } + children_.clear(); + } + model_->endRemoveRows(); + + // remove hidden children + if(!hiddenChildren_.empty()) { + Q_FOREACH(DirTreeModelItem* item, hiddenChildren_) { + delete item; + } + hiddenChildren_.clear(); + } + + /* now, we have no child since all child items are removed. + * So we add a place holder child item to keep the expander around. */ + addPlaceHolderChild(); + /* deactivate folder since it will be reactivated on expand */ + freeFolder(); + expanded_ = false; + loaded_ = false; + } +} + +QModelIndex DirTreeModelItem::index() { + Q_ASSERT(model_); + return model_->indexFromItem(this); +} + +/* Add file info to parent node to proper position. */ +DirTreeModelItem* DirTreeModelItem::insertFile(std::shared_ptr fi) { + // qDebug() << "insertFileInfo: " << fm_file_info_get_disp_name(fi); + DirTreeModelItem* item = new DirTreeModelItem(std::move(fi), model_); + insertItem(item); + return item; +} + +/* Add file info to parent node to proper position. */ +void DirTreeModelItem::insertFiles(Fm::FileInfoList files) { + if(children_.size() == 1 && placeHolderChild_) { + // the list is empty, add them all at once and do sort + if(!model_->showHidden()) { // need to separate visible and hidden items + // insert hidden files into the "hiddenChildren_" list and remove them from "files" list + // WARNING: "std::remove_if" shouldn't be used to work on the "removed" items because, as + // docs say, the elements between the returned and the end iterators are in an unspecified + // state and, as far as I (@tsujan) have tested, some of them announce themselves as null. + for(auto it = files.begin(); it != files.end();) { + auto file = *it; + if(file->isHidden()) { + hiddenChildren_.push_back(new DirTreeModelItem{std::move(file), model_}); + it = files.erase(it); + } + else { + ++it; + } + } + + } + // sort the remaining visible files by name + std::sort(files.begin(), files.end(), [](const std::shared_ptr& a, const std::shared_ptr& b) { + return QString::localeAwareCompare(a->displayName(), b->displayName()) < 0; + }); + // insert the files into the visible children list at once + model_->beginInsertRows(index(), 1, files.size() + 1); // the first item is the placeholder item, so we start from row 1 + for(auto& file: files) { + if(file->isDir()) { + DirTreeModelItem* newItem = new DirTreeModelItem(std::move(file), model_); + newItem->parent_ = this; + children_.push_back(newItem); + } + } + model_->endInsertRows(); + + // remove the place holder if a folder is added + if(children_.size() > 1) { + auto it = std::find(children_.cbegin(), children_.cend(), placeHolderChild_); + if(it != children_.cend()) { + auto pos = it - children_.cbegin(); + model_->beginRemoveRows(index(), pos, pos); + children_.erase(it); + delete placeHolderChild_; + model_->endRemoveRows(); + placeHolderChild_ = nullptr; + } + } + } + else { + // the list already contain some items, insert new items one by one so they can be sorted. + for(auto& file: files) { + if(file->isDir()) { + insertFile(std::move(file)); + } + } + } +} + +// find a good position to insert the new item +// FIXME: insert one item at a time is slow. Insert multiple items at once and then sort is faster. +int DirTreeModelItem::insertItem(DirTreeModelItem* newItem) { + if(!newItem->fileInfo_ || !newItem->fileInfo_->isDir()) { + // don't insert placeholders or non-directory files + return -1; + } + if(model_->showHidden() || !newItem->fileInfo_ || !newItem->fileInfo_->isHidden()) { + auto it = std::lower_bound(children_.cbegin(), children_.cend(), newItem, [=](const DirTreeModelItem* a, const DirTreeModelItem* b) { + if(Q_UNLIKELY(!a->fileInfo_)) { + return true; // this is a placeholder item which will be removed so the order doesn't matter. + } + if(Q_UNLIKELY(!b->fileInfo_)) { + return false; + } + return QString::localeAwareCompare(a->fileInfo_->displayName(), b->fileInfo_->displayName()) < 0; + }); + // inform the world that we're about to insert the item + auto position = it - children_.begin(); + model_->beginInsertRows(index(), position, position); + newItem->parent_ = this; + children_.insert(it, newItem); + model_->endInsertRows(); + return position; + } + else { // hidden folder + hiddenChildren_.push_back(newItem); + } + return -1; +} + + +// FmFolder signal handlers + +void DirTreeModelItem::onFolderFinishLoading() { + DirTreeModel* model = model_; + /* set 'loaded' flag beforehand as callback may check it */ + loaded_ = true; + QModelIndex idx = index(); + //qDebug() << "folder loaded"; + // remove the placeholder child if needed + // (a check for its existence is necessary; see insertItem) + if(placeHolderChild_) { + if(children_.size() == 1) { // we have no other child other than the place holder item, leave it + placeHolderChild_->displayName_ = DirTreeModel::tr(""); + QModelIndex placeHolderIndex = placeHolderChild_->index(); + // qDebug() << "placeHolderIndex: "<dataChanged(placeHolderIndex, placeHolderIndex); + } + else { + auto it = std::find(children_.cbegin(), children_.cend(), placeHolderChild_); + if(it != children_.cend()) { + auto pos = it - children_.cbegin(); + model->beginRemoveRows(idx, pos, pos); + children_.erase(it); + delete placeHolderChild_; + model->endRemoveRows(); + placeHolderChild_ = nullptr; + } + } + } + + Q_EMIT model->rowLoaded(idx); +} + +void DirTreeModelItem::onFolderFilesAdded(Fm::FileInfoList& files) { + insertFiles(files); +} + +void DirTreeModelItem::onFolderFilesRemoved(Fm::FileInfoList& files) { + DirTreeModel* model = model_; + + for(auto& fi: files) { + int pos; + DirTreeModelItem* child = childFromName(fi->name().c_str(), &pos); + if(child) { + // The item shouldn't be deleted now but after its row is removed from QTreeView; + // otherwise a freeze will happen when it has a child item (its row is expanded). + child->queuedForDeletion_ = true; + model->beginRemoveRows(index(), pos, pos); + children_.erase(children_.cbegin() + pos); + model->endRemoveRows(); + + } + } + + if(children_.empty()) { // no visible children, add a placeholder item to keep the row expanded + addPlaceHolderChild(); + placeHolderChild_->displayName_ = DirTreeModel::tr(""); + } +} + +void DirTreeModelItem::onFolderFilesChanged(std::vector &changes) { + DirTreeModel* model = model_; + for(auto& changePair: changes) { + int pos; + auto& changedFile = changePair.first; + DirTreeModelItem* child = childFromName(changedFile->name().c_str(), &pos); + if(child) { + QModelIndex childIndex = child->index(); + Q_EMIT model->dataChanged(childIndex, childIndex); + } + } +} + +DirTreeModelItem* DirTreeModelItem::childFromName(const char* utf8_name, int* pos) { + int i = 0; + for(const auto item : children_) { + if(item->fileInfo_ && item->fileInfo_->name() == utf8_name) { + if(pos) { + *pos = i; + } + return item; + } + ++i; + } + return nullptr; +} + +DirTreeModelItem* DirTreeModelItem::childFromPath(Fm::FilePath path, bool recursive) const { + Q_ASSERT(path != nullptr); + + Q_FOREACH(DirTreeModelItem* item, children_) { + // if(item->fileInfo_) + // qDebug() << "child: " << QString::fromUtf8(fm_file_info_get_disp_name(item->fileInfo_)); + if(item->fileInfo_ && item->fileInfo_->path() == path) { + return item; + } + else if(recursive) { + DirTreeModelItem* child = item->childFromPath(std::move(path), true); + if(child) { + return child; + } + } + } + return nullptr; +} + +void DirTreeModelItem::setShowHidden(bool show) { + if(show) { + // move all hidden children to visible list + for(auto item: hiddenChildren_) { + insertItem(item); + } + hiddenChildren_.clear(); + // remove the placeholder if needed + if(children_.size() > 1) { + auto it = std::find(children_.cbegin(), children_.cend(), placeHolderChild_); + if(it != children_.cend()) { + auto pos = it - children_.cbegin(); + model_->beginRemoveRows(index(), pos, pos); + children_.erase(it); + delete placeHolderChild_; + model_->endRemoveRows(); + placeHolderChild_ = nullptr; + } + } + // recursively show children of children, etc. + for(auto item: children_) { + item->setShowHidden(true); + } + } + else { // hide hidden folders + QModelIndex _index = index(); + int pos = 0; + for(auto it = children_.begin(); it != children_.end(); ++pos) { + DirTreeModelItem* item = *it; + if(item->fileInfo_) { + if(item->fileInfo_->isHidden()) { // hidden folder + // remove from the model and add to the hiddenChildren_ list + model_->beginRemoveRows(_index, pos, pos); + it = children_.erase(it); + hiddenChildren_.push_back(item); + model_->endRemoveRows(); + } + else { // visible folder, recursively filter its children + item->setShowHidden(show); + ++it; + } + } + else { + ++it; + } + } + if(children_.empty()) { // no visible children, add a placeholder item to keep the row expanded + addPlaceHolderChild(); + placeHolderChild_->displayName_ = DirTreeModel::tr(""); + } + } +} + + + +} // namespace Fm diff --git a/src/dirtreemodelitem.h b/src/dirtreemodelitem.h new file mode 100644 index 0000000..9b6e8d0 --- /dev/null +++ b/src/dirtreemodelitem.h @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_DIRTREEMODELITEM_H +#define FM_DIRTREEMODELITEM_H + +#include "libfmqtglobals.h" +#include +#include +#include +#include + +#include "core/fileinfo.h" +#include "core/folder.h" + +namespace Fm { + +class DirTreeModel; +class DirTreeView; + +class LIBFM_QT_API DirTreeModelItem { +public: + friend class DirTreeModel; // allow direct access of private members in DirTreeModel + friend class DirTreeView; // allow direct access of private members in DirTreeView + + explicit DirTreeModelItem(); + explicit DirTreeModelItem(std::shared_ptr info, DirTreeModel* model, DirTreeModelItem* parent = nullptr); + ~DirTreeModelItem(); + + void loadFolder(); + void unloadFolder(); + + inline bool isPlaceHolder() const { + return (fileInfo_ == nullptr); + } + + void setShowHidden(bool show); + + bool isQueuedForDeletion() { + return queuedForDeletion_; + } + + +private: + void freeFolder(); + void addPlaceHolderChild(); + DirTreeModelItem* childFromName(const char* utf8_name, int* pos); + DirTreeModelItem* childFromPath(Fm::FilePath path, bool recursive) const; + + DirTreeModelItem* insertFile(std::shared_ptr fi); + void insertFiles(Fm::FileInfoList files); + int insertItem(Fm::DirTreeModelItem* newItem); + QModelIndex index(); + + void onFolderFinishLoading(); + void onFolderFilesAdded(Fm::FileInfoList &files); + void onFolderFilesRemoved(Fm::FileInfoList &files); + void onFolderFilesChanged(std::vector& changes); + +private: + std::shared_ptr fileInfo_; + std::shared_ptr folder_; + QString displayName_ ; + QIcon icon_; + bool expanded_; + bool loaded_; + DirTreeModelItem* parent_; + DirTreeModelItem* placeHolderChild_; + std::vector children_; + std::vector hiddenChildren_; + DirTreeModel* model_; + bool queuedForDeletion_; + // signal connections + QMetaObject::Connection onFolderFinishLoadingConn_; + QMetaObject::Connection onFolderFilesAddedConn_; + QMetaObject::Connection onFolderFilesRemovedConn_; + QMetaObject::Connection onFolderFilesChangedConn_; +}; + +} + +#endif // FM_DIRTREEMODELITEM_H diff --git a/src/dirtreeview.cpp b/src/dirtreeview.cpp new file mode 100644 index 0000000..a16c7df --- /dev/null +++ b/src/dirtreeview.cpp @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "dirtreeview.h" +#include +#include +#include +#include +#include +#include +#include "dirtreemodel.h" +#include "dirtreemodelitem.h" +#include "filemenu.h" + +namespace Fm { + +DirTreeView::DirTreeView(QWidget* parent): + QTreeView(parent), + currentExpandingItem_(nullptr) { + + setSelectionMode(QAbstractItemView::SingleSelection); + setHeaderHidden(true); + setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); + header()->setStretchLastSection(false); + + connect(this, &DirTreeView::collapsed, this, &DirTreeView::onCollapsed); + connect(this, &DirTreeView::expanded, this, &DirTreeView::onExpanded); + + setContextMenuPolicy(Qt::CustomContextMenu); + connect(this, &DirTreeView::customContextMenuRequested, + this, &DirTreeView::onCustomContextMenuRequested); +} + +DirTreeView::~DirTreeView() { +} + +void DirTreeView::cancelPendingChdir() { + if(!pathsToExpand_.empty()) { + pathsToExpand_.clear(); + if(!currentExpandingItem_) { + return; + } + DirTreeModel* _model = static_cast(model()); + disconnect(_model, &DirTreeModel::rowLoaded, this, &DirTreeView::onRowLoaded); + currentExpandingItem_ = nullptr; + } +} + +void DirTreeView::expandPendingPath() { + if(pathsToExpand_.empty()) { + return; + } + + auto path = pathsToExpand_.front(); + // qDebug() << "expanding: " << Path(path).displayBasename(); + DirTreeModel* _model = static_cast(model()); + DirTreeModelItem* item = _model->itemFromPath(path); + // qDebug() << "findItem: " << item; + if(item) { + currentExpandingItem_ = item; + connect(_model, &DirTreeModel::rowLoaded, this, &DirTreeView::onRowLoaded); + if(item->loaded_) { // the node is already loaded + onRowLoaded(item->index()); + } + else { + // _model->loadRow(item->index()); + item->loadFolder(); + } + } + else { + selectionModel()->clear(); + /* since we never get it loaded we need to update cwd here */ + currentPath_ = path; + + cancelPendingChdir(); // FIXME: is this correct? this is not done in the gtk+ version of libfm. + } +} + +void DirTreeView::onRowLoaded(const QModelIndex& index) { + DirTreeModel* _model = static_cast(model()); + if(!currentExpandingItem_) { + return; + } + if(currentExpandingItem_ != _model->itemFromIndex(index)) { + return; + } + /* disconnect the handler since we only need it once */ + disconnect(_model, &DirTreeModel::rowLoaded, this, &DirTreeView::onRowLoaded); + + // DirTreeModelItem* item = _model->itemFromIndex(index); + // qDebug() << "row loaded: " << item->displayName_; + /* after the folder is loaded, the files should have been added to + * the tree model */ + expand(index); + + /* remove the expanded path from pending list */ + pathsToExpand_.erase(pathsToExpand_.begin()); + if(pathsToExpand_.empty()) { /* this is the last one and we're done, select the item */ + // qDebug() << "Done!"; + selectionModel()->select(index, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Clear); + scrollTo(index, QAbstractItemView::EnsureVisible); + } + else { /* continue expanding next pending path */ + expandPendingPath(); + } +} + + +void DirTreeView::setCurrentPath(Fm::FilePath path) { + DirTreeModel* _model = static_cast(model()); + if(!_model) { + return; + } + int rowCount = _model->rowCount(QModelIndex()); + if(rowCount <= 0 || currentPath_ == path) { + return; + } + + currentPath_ = std::move(path); + + // NOTE: The content of each node is loaded on demand dynamically. + // So, when we ask for a chdir operation, some nodes do not exists yet. + // We have to wait for the loading of child nodes and continue the + // pending chdir operation after the child nodes become available. + + // cancel previous pending tree expansion + cancelPendingChdir(); + + /* find a root item containing this path */ + Fm::FilePath root; + for(int row = 0; row < rowCount; ++row) { + QModelIndex index = _model->index(row, 0, QModelIndex()); + auto row_path = _model->filePath(index); + if(row_path.isPrefixOf(currentPath_)) { + root = row_path; + break; + } + } + + if(root) { /* root item is found */ + path = currentPath_; + do { /* add path elements one by one to a list */ + pathsToExpand_.insert(pathsToExpand_.cbegin(), path); + // qDebug() << "prepend path: " << Path(path).displayBasename(); + if(path == root) { + break; + } + path = path.parent(); + } + while(path); + + expandPendingPath(); + } +} + +void DirTreeView::setModel(QAbstractItemModel* model) { + Q_ASSERT(model->inherits("Fm::DirTreeModel")); + + if(!pathsToExpand_.empty()) { // if a chdir request is in progress, cancel it + cancelPendingChdir(); + } + + QTreeView::setModel(model); + header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &DirTreeView::onSelectionChanged); +} + +void DirTreeView::mousePressEvent(QMouseEvent* event) { + if(event && event->button() == Qt::RightButton && + event->type() == QEvent::MouseButtonPress) { + // Do not change the selection when the context menu is activated. + return; + } + QTreeView::mousePressEvent(event); +} + +void DirTreeView::onCustomContextMenuRequested(const QPoint& pos) { + QModelIndex index = indexAt(pos); + if(index.isValid()) { + QVariant data = index.data(DirTreeModel::FileInfoRole); + auto fileInfo = data.value>(); + if(fileInfo) { + auto path = fileInfo->path(); + Fm::FileInfoList files ; + files.push_back(fileInfo); + Fm::FileMenu* menu = new Fm::FileMenu(files, fileInfo, path); + // FIXME: apply some settings to the menu and set a proper file launcher to it + Q_EMIT prepareFileMenu(menu); + + QVariant pathData = qVariantFromValue(path); + QAction* action = menu->openAction(); + action->disconnect(); + action->setData(index); + connect(action, &QAction::triggered, this, &DirTreeView::onOpen); + action = new QAction(QIcon::fromTheme("window-new"), tr("Open in New T&ab"), menu); + action->setData(pathData); + connect(action, &QAction::triggered, this, &DirTreeView::onNewTab); + menu->insertAction(menu->separator1(), action); + action = new QAction(QIcon::fromTheme("window-new"), tr("Open in New Win&dow"), menu); + action->setData(pathData); + connect(action, &QAction::triggered, this, &DirTreeView::onNewWindow); + menu->insertAction(menu->separator1(), action); + if(fileInfo->isNative()) { + action = new QAction(QIcon::fromTheme("utilities-terminal"), tr("Open in Termina&l"), menu); + action->setData(pathData); + connect(action, &QAction::triggered, this, &DirTreeView::onOpenInTerminal); + menu->insertAction(menu->separator1(), action); + } + menu->exec(mapToGlobal(pos)); + delete menu; + } + } +} + +void DirTreeView::onOpen() { + if(QAction* action = qobject_cast(sender())) { + setCurrentIndex(action->data().toModelIndex()); + } +} + +void DirTreeView::onNewWindow() { + if(QAction* action = qobject_cast(sender())) { + auto path = action->data().value(); + Q_EMIT openFolderInNewWindowRequested(path); + } +} + +void DirTreeView::onNewTab() { + if(QAction* action = qobject_cast(sender())) { + auto path = action->data().value(); + Q_EMIT openFolderInNewTabRequested(path); + } +} + +void DirTreeView::onOpenInTerminal() { + if(QAction* action = qobject_cast(sender())) { + auto path = action->data().value(); + Q_EMIT openFolderInTerminalRequested(path); + } +} + +void DirTreeView::onNewFolder() { + if(QAction* action = qobject_cast(sender())) { + auto path = action->data().value(); + Q_EMIT createNewFolderRequested(path); + } +} + +void DirTreeView::onCollapsed(const QModelIndex& index) { + DirTreeModel* treeModel = static_cast(model()); + if(treeModel) { + treeModel->unloadRow(index); + } +} + +void DirTreeView::onExpanded(const QModelIndex& index) { + DirTreeModel* treeModel = static_cast(model()); + if(treeModel) { + treeModel->loadRow(index); + } +} +void DirTreeView::rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { + // see if to-be-removed items are queued for deletion + // and also clear selection if one of them is selected (otherwise a freeze will occur) + QModelIndex selIndex; + if(selectionModel()->selectedRows().size() == 1) { + selIndex = selectionModel()->selectedRows().at(0); + } + for (int i = start; i <= end; ++i) { + QModelIndex index = parent.child(i, 0); + if(index.isValid()) { + if(index == selIndex) { + selectionModel()->clear(); + } + DirTreeModelItem* item = reinterpret_cast(index.internalPointer()); + if (item->isQueuedForDeletion()) { + queuedForDeletion_.push_back(item); + } + } + } + + QTreeView::rowsAboutToBeRemoved (parent, start, end); +} + +void DirTreeView::rowsRemoved(const QModelIndex& parent, int start, int end) { + QTreeView::rowsRemoved (parent, start, end); + // do the queued deletions only after all rows are removed (otherwise a freeze might occur) + QTimer::singleShot(0, this, SLOT (doQueuedDeletions())); +} + +void DirTreeView::doQueuedDeletions() { + if(!queuedForDeletion_.empty()) { + Q_FOREACH(DirTreeModelItem* item, queuedForDeletion_) { + delete item; + } + queuedForDeletion_.clear(); + } +} + +void DirTreeView::onSelectionChanged(const QItemSelection& selected, const QItemSelection& /*deselected*/) { + if(!selected.isEmpty()) { + QModelIndex index = selected.first().topLeft(); + DirTreeModel* _model = static_cast(model()); + auto path = _model->filePath(index); + if(path && currentPath_ && path == currentPath_) { + return; + } + cancelPendingChdir(); + if(!path) { + return; + } + currentPath_ = std::move(path); + + // FIXME: use enums for type rather than hard-coded values 0 or 1 + int type = 0; + if(QGuiApplication::mouseButtons() & Qt::MiddleButton) { + type = 1; + } + Q_EMIT chdirRequested(type, currentPath_); + } +} + + +} // namespace Fm diff --git a/src/dirtreeview.h b/src/dirtreeview.h new file mode 100644 index 0000000..b32bd2e --- /dev/null +++ b/src/dirtreeview.h @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_DIRTREEVIEW_H +#define FM_DIRTREEVIEW_H + +#include "libfmqtglobals.h" +#include +#include + +#include "core/filepath.h" + +class QItemSelection; + +namespace Fm { + +class FileMenu; +class DirTreeModelItem; + +class LIBFM_QT_API DirTreeView : public QTreeView { + Q_OBJECT + +public: + explicit DirTreeView(QWidget* parent); + ~DirTreeView(); + + const Fm::FilePath& currentPath() const { + return currentPath_; + } + + void setCurrentPath(Fm::FilePath path); + + void chdir(Fm::FilePath path) { + setCurrentPath(std::move(path)); + } + + virtual void setModel(QAbstractItemModel* model); + +protected: + virtual void mousePressEvent(QMouseEvent* event); + virtual void rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end); + +private: + void cancelPendingChdir(); + void expandPendingPath(); + +Q_SIGNALS: + void chdirRequested(int type, const Fm::FilePath& path); + void openFolderInNewWindowRequested(const Fm::FilePath& path); + void openFolderInNewTabRequested(const Fm::FilePath& path); + void openFolderInTerminalRequested(const Fm::FilePath& path); + void createNewFolderRequested(const Fm::FilePath& path); + void prepareFileMenu(Fm::FileMenu* menu); // emit before showing a Fm::FileMenu + +protected Q_SLOTS: + void onCollapsed(const QModelIndex& index); + void onExpanded(const QModelIndex& index); + void onRowLoaded(const QModelIndex& index); + void onSelectionChanged(const QItemSelection& selected, const QItemSelection& deselected); + void onCustomContextMenuRequested(const QPoint& pos); + void onOpen(); + void onNewWindow(); + void onNewTab(); + void onOpenInTerminal(); + void onNewFolder(); + void rowsRemoved(const QModelIndex& parent, int start, int end); + void doQueuedDeletions(); + +private: + Fm::FilePath currentPath_; + Fm::FilePathList pathsToExpand_; + DirTreeModelItem* currentExpandingItem_; + std::vector queuedForDeletion_; +}; + +} + +#endif // FM_DIRTREEVIEW_H diff --git a/src/dndactionmenu.cpp b/src/dndactionmenu.cpp new file mode 100644 index 0000000..2688376 --- /dev/null +++ b/src/dndactionmenu.cpp @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "dndactionmenu.h" + +namespace Fm { + +DndActionMenu::DndActionMenu(Qt::DropActions possibleActions, QWidget* parent) + : QMenu(parent) + , copyAction(nullptr) + , moveAction(nullptr) + , linkAction(nullptr) + , cancelAction(nullptr) { + if(possibleActions.testFlag(Qt::CopyAction)) { + copyAction = addAction(QIcon::fromTheme("edit-copy"), tr("Copy here")); + } + if(possibleActions.testFlag(Qt::MoveAction)) { + moveAction = addAction(tr("Move here")); + } + if(possibleActions.testFlag(Qt::LinkAction)) { + linkAction = addAction(tr("Create symlink here")); + } + addSeparator(); + cancelAction = addAction(tr("Cancel")); +} + +DndActionMenu::~DndActionMenu() { + +} + +Qt::DropAction DndActionMenu::askUser(Qt::DropActions possibleActions, QPoint pos) { + Qt::DropAction result = Qt::IgnoreAction; + DndActionMenu menu{possibleActions}; + QAction* action = menu.exec(pos); + if(nullptr != action) { + if(action == menu.copyAction) { + result = Qt::CopyAction; + } + else if(action == menu.moveAction) { + result = Qt::MoveAction; + } + else if(action == menu.linkAction) { + result = Qt::LinkAction; + } + } + return result; +} + + +} // namespace Fm diff --git a/src/dndactionmenu.h b/src/dndactionmenu.h new file mode 100644 index 0000000..d4dd1bd --- /dev/null +++ b/src/dndactionmenu.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_DNDACTIONMENU_H +#define FM_DNDACTIONMENU_H + +#include "libfmqtglobals.h" +#include +#include + +namespace Fm { + +class DndActionMenu : public QMenu { + Q_OBJECT +public: + explicit DndActionMenu(Qt::DropActions possibleActions, QWidget* parent = 0); + virtual ~DndActionMenu(); + + static Qt::DropAction askUser(Qt::DropActions possibleActions, QPoint pos); + +private: + QAction* copyAction; + QAction* moveAction; + QAction* linkAction; + QAction* cancelAction; +}; + +} + +#endif // FM_DNDACTIONMENU_H diff --git a/src/dnddest.cpp b/src/dnddest.cpp new file mode 100644 index 0000000..be20b66 --- /dev/null +++ b/src/dnddest.cpp @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "dnddest.h" +#include "fileoperation.h" +#include "utilities.h" + +namespace Fm { + +const char* supportedMimeTypes[] = { + "text/uri-list" + "XdndDirectSave0"/* X direct save */ + /* TODO: add more targets to support: text types, _NETSCAPE_URL, property/bgimage ... */ +}; + +DndDest::DndDest() { + +} + +DndDest::~DndDest() { + +} + +bool DndDest::dropMimeData(const QMimeData* data, Qt::DropAction action) { + // FIXME: should we put this in dropEvent handler of FolderView instead? + if(data->hasUrls()) { + qDebug("drop action: %d", action); + auto srcPaths = pathListFromQUrls(data->urls()); + switch(action) { + case Qt::CopyAction: + FileOperation::copyFiles(srcPaths, destPath_); + break; + case Qt::MoveAction: + FileOperation::moveFiles(srcPaths, destPath_); + break; + case Qt::LinkAction: + FileOperation::symlinkFiles(srcPaths, destPath_); + default: + return false; + } + return true; + } + return false; +} + +bool DndDest::isSupported(const QMimeData* /*data*/) { + return false; +} + +bool DndDest::isSupported(QString /*mimeType*/) { + return false; +} + + +} // namespace Fm diff --git a/src/dnddest.h b/src/dnddest.h new file mode 100644 index 0000000..3efafd4 --- /dev/null +++ b/src/dnddest.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_DNDDEST_H +#define FM_DNDDEST_H + +#include +#include "core/filepath.h" + +namespace Fm { + +class DndDest { +public: + explicit DndDest(); + ~DndDest(); + + void setDestPath(Fm::FilePath dest) { + destPath_ = std::move(dest); + } + + const Fm::FilePath& destPath() { + return destPath_; + } + + bool isSupported(const QMimeData* data); + bool isSupported(QString mimeType); + + bool dropMimeData(const QMimeData* data, Qt::DropAction action); + +private: + Fm::FilePath destPath_; +}; + +} + +#endif // FM_DNDDEST_H diff --git a/src/edit-bookmarks.ui b/src/edit-bookmarks.ui new file mode 100644 index 0000000..8d989e6 --- /dev/null +++ b/src/edit-bookmarks.ui @@ -0,0 +1,143 @@ + + + EditBookmarksDialog + + + + 0 + 0 + 480 + 320 + + + + Edit Bookmarks + + + + + + true + + + true + + + QAbstractItemView::InternalMove + + + Qt::MoveAction + + + false + + + false + + + 100 + + + + Name + + + + + Location + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + &Add Item + + + + + + + + + + &Remove Item + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Use drag and drop to reorder the items + + + + + + + + + buttonBox + accepted() + EditBookmarksDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + EditBookmarksDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/editbookmarksdialog.cpp b/src/editbookmarksdialog.cpp new file mode 100644 index 0000000..36e0958 --- /dev/null +++ b/src/editbookmarksdialog.cpp @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "editbookmarksdialog.h" +#include "ui_edit-bookmarks.h" +#include +#include +#include +#include +#include + +namespace Fm { + +EditBookmarksDialog::EditBookmarksDialog(FmBookmarks* bookmarks, QWidget* parent, Qt::WindowFlags f): + QDialog(parent, f), + ui(new Ui::EditBookmarksDialog()), + bookmarks_(FM_BOOKMARKS(g_object_ref(bookmarks))) { + + ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose); // auto delete on close + + // load bookmarks + GList* allBookmarks = fm_bookmarks_get_all(bookmarks_); + for(GList* l = allBookmarks; l; l = l->next) { + FmBookmarkItem* bookmark = reinterpret_cast(l->data); + QTreeWidgetItem* item = new QTreeWidgetItem(); + char* path_str = fm_path_display_name(bookmark->path, false); + item->setData(0, Qt::DisplayRole, QString::fromUtf8(bookmark->name)); + item->setData(1, Qt::DisplayRole, QString::fromUtf8(path_str)); + item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsDragEnabled | Qt::ItemIsEnabled); + g_free(path_str); + ui->treeWidget->addTopLevelItem(item); + } + g_list_free_full(allBookmarks, (GDestroyNotify)fm_bookmark_item_unref); + + connect(ui->addItem, &QPushButton::clicked, this, &EditBookmarksDialog::onAddItem); + connect(ui->removeItem, &QPushButton::clicked, this, &EditBookmarksDialog::onRemoveItem); +} + +EditBookmarksDialog::~EditBookmarksDialog() { + g_object_unref(bookmarks_); + delete ui; +} + +void EditBookmarksDialog::accept() { + // save bookmarks + // it's easier to recreate the whole bookmark file than + // to manipulate FmBookmarks object. So here we generate the file directly. + // FIXME: maybe in the future we should add a libfm API to easily replace all FmBookmarks. + // Here we use gtk+ 3.0 bookmarks rather than the gtk+ 2.0 one. + // Since gtk+ 2.24.12, gtk+2 reads gtk+3 bookmarks file if it exists. + // So it's safe to only save gtk+3 bookmarks file. + QString path = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation); + path += QLatin1String("/gtk-3.0"); + if(!QDir().mkpath(path)) { + return; // fail to create ~/.config/gtk-3.0 dir + } + path += QLatin1String("/bookmarks"); + QSaveFile file(path); // use QSaveFile for atomic file operation + if(file.open(QIODevice::WriteOnly)) { + for(int row = 0; ; ++row) { + QTreeWidgetItem* item = ui->treeWidget->topLevelItem(row); + if(!item) { + break; + } + QString name = item->data(0, Qt::DisplayRole).toString(); + QUrl url = QUrl::fromUserInput(item->data(1, Qt::DisplayRole).toString()); + file.write(url.toEncoded()); + file.write(" "); + file.write(name.toUtf8()); + file.write("\n"); + } + // FIXME: should we support Qt or KDE specific bookmarks in the future? + file.commit(); + } + QDialog::accept(); +} + +void EditBookmarksDialog::onAddItem() { + QTreeWidgetItem* item = new QTreeWidgetItem(); + item->setData(0, Qt::DisplayRole, tr("New bookmark")); + item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsDragEnabled | Qt::ItemIsEnabled); + ui->treeWidget->addTopLevelItem(item); + ui->treeWidget->editItem(item); +} + +void EditBookmarksDialog::onRemoveItem() { + QList sels = ui->treeWidget->selectedItems(); + Q_FOREACH(QTreeWidgetItem* item, sels) { + delete item; + } +} + + +} // namespace Fm diff --git a/src/editbookmarksdialog.h b/src/editbookmarksdialog.h new file mode 100644 index 0000000..a3f2318 --- /dev/null +++ b/src/editbookmarksdialog.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_EDITBOOKMARKSDIALOG_H +#define FM_EDITBOOKMARKSDIALOG_H + +#include "libfmqtglobals.h" +#include +#include + +namespace Ui { +class EditBookmarksDialog; +} + +namespace Fm { + +class LIBFM_QT_API EditBookmarksDialog : public QDialog { + Q_OBJECT +public: + explicit EditBookmarksDialog(FmBookmarks* bookmarks, QWidget* parent = 0, Qt::WindowFlags f = 0); + virtual ~EditBookmarksDialog(); + + virtual void accept(); + +private Q_SLOTS: + void onAddItem(); + void onRemoveItem(); + +private: + Ui::EditBookmarksDialog* ui; + FmBookmarks* bookmarks_; +}; + +} + +#endif // FM_EDITBOOKMARKSDIALOG_H diff --git a/src/exec-file.ui b/src/exec-file.ui new file mode 100644 index 0000000..c5a9ea3 --- /dev/null +++ b/src/exec-file.ui @@ -0,0 +1,163 @@ + + + ExecFileDialog + + + + 0 + 0 + 487 + 58 + + + + Execute file + + + + + + + + + + + + + + true + + + + + + + + + + + &Open + + + + + + true + + + + + + + E&xecute + + + + + + + + + + Execute in &Terminal + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Cancel + + + + + + + + + + + + + + cancel + clicked() + ExecFileDialog + reject() + + + 341 + 39 + + + 196 + 28 + + + + + exec + clicked() + ExecFileDialog + accept() + + + 56 + 39 + + + 196 + 28 + + + + + execTerm + clicked() + ExecFileDialog + accept() + + + 201 + 39 + + + 196 + 28 + + + + + open + clicked() + ExecFileDialog + accept() + + + 346 + 39 + + + 250 + 28 + + + + + diff --git a/src/execfiledialog.cpp b/src/execfiledialog.cpp new file mode 100644 index 0000000..a1ec05f --- /dev/null +++ b/src/execfiledialog.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "execfiledialog_p.h" +#include "ui_exec-file.h" +#include "icontheme.h" +#include "core/iconinfo.h" + +namespace Fm { + +ExecFileDialog::ExecFileDialog(FmFileInfo* file, QWidget* parent, Qt::WindowFlags f): + QDialog(parent, f), + ui(new Ui::ExecFileDialog()), + fileInfo_(fm_file_info_ref(file)), + result_(FM_FILE_LAUNCHER_EXEC_CANCEL) { + + ui->setupUi(this); + // show file icon + GIcon* gicon = G_ICON(fm_file_info_get_icon(fileInfo_)); + ui->icon->setPixmap(Fm::IconInfo::fromGIcon(gicon)->qicon().pixmap(QSize(48, 48))); + + QString msg; + if(fm_file_info_is_text(file)) { + msg = tr("This text file '%1' seems to be an executable script.\nWhat do you want to do with it?") + .arg(QString::fromUtf8(fm_file_info_get_disp_name(file))); + ui->execTerm->setDefault(true); + } + else { + msg = tr("This file '%1' is executable. Do you want to execute it?") + .arg(QString::fromUtf8(fm_file_info_get_disp_name(file))); + ui->exec->setDefault(true); + ui->open->hide(); + } + ui->msg->setText(msg); +} + +ExecFileDialog::~ExecFileDialog() { + delete ui; + if(fileInfo_) { + fm_file_info_unref(fileInfo_); + } +} + +void ExecFileDialog::accept() { + QObject* _sender = sender(); + if(_sender == ui->exec) { + result_ = FM_FILE_LAUNCHER_EXEC; + } + else if(_sender == ui->execTerm) { + result_ = FM_FILE_LAUNCHER_EXEC_IN_TERMINAL; + } + else if(_sender == ui->open) { + result_ = FM_FILE_LAUNCHER_EXEC_OPEN; + } + QDialog::accept(); +} + +} // namespace Fm diff --git a/src/execfiledialog_p.h b/src/execfiledialog_p.h new file mode 100644 index 0000000..10af394 --- /dev/null +++ b/src/execfiledialog_p.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_EXECFILEDIALOG_H +#define FM_EXECFILEDIALOG_H + +#include +#include + +namespace Ui { + class ExecFileDialog; +} + +namespace Fm { + +class ExecFileDialog : public QDialog { + Q_OBJECT +public: + ~ExecFileDialog(); + ExecFileDialog(FmFileInfo* fileInfo, QWidget* parent = 0, Qt::WindowFlags f = 0); + + FmFileLauncherExecAction result() { + return result_; + } + +protected: + virtual void accept(); + +private: + Ui::ExecFileDialog* ui; + FmFileInfo* fileInfo_; + FmFileLauncherExecAction result_; +}; + +} + +#endif // FM_EXECFILEDIALOG_H diff --git a/src/file-operation-dialog.ui b/src/file-operation-dialog.ui new file mode 100644 index 0000000..f000afd --- /dev/null +++ b/src/file-operation-dialog.ui @@ -0,0 +1,185 @@ + + + FileOperationDialog + + + + 0 + 0 + 450 + 246 + + + + + + + + + + + + + + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + Destination: + + + + + + + + 0 + 0 + + + + + + + true + + + + + + + Processing: + + + + + + + + 0 + 0 + + + + Preparing... + + + + + + + Progress + + + + + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + Time remaining: + + + + + + + + 0 + 0 + + + + + + + + + + + + 0 + 0 + + + + + + + + Data transferred: + + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel + + + + + + + + + buttonBox + accepted() + FileOperationDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + FileOperationDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/file-props.ui b/src/file-props.ui new file mode 100644 index 0000000..23dd661 --- /dev/null +++ b/src/file-props.ui @@ -0,0 +1,736 @@ + + + FilePropsDialog + + + + 0 + 0 + 424 + 456 + + + + File Properties + + + + + + + + + 10 + + + 10 + + + 10 + + + 10 + + + + + 0 + + + + General + + + + 12 + + + 6 + + + + + + 0 + 0 + + + + + + + + + + + + + 32 + 32 + + + + + + + + + + + Location: + + + + + + + + 0 + 0 + + + + + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + File type: + + + + + + + + 0 + 0 + + + + + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + MIME type: + + + + + + + + 0 + 0 + + + + + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + File size: + + + + + + + + 0 + 0 + + + + + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + On-disk size: + + + + + + + + 0 + 0 + + + + + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Last modified: + + + + + + + + 0 + 0 + + + + + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Link target: + + + + + + + + 0 + 0 + + + + + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Open With: + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + 0 + 0 + + + + + + + + Last accessed: + + + + + + + + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + Permissions + + + + 6 + + + + + + 0 + 0 + + + + Ownership + + + + 12 + + + 6 + + + + + + + + + + + + 0 + 0 + + + + Group: + + + + + + + + 0 + 0 + + + + Owner: + + + + + + + + + + + 0 + 0 + + + + Access Control + + + + + + + 0 + 0 + + + + 0 + + + + + + + Owner: + + + + + + + + 0 + 0 + + + + + + + + Group: + + + + + + + + 0 + 0 + + + + + + + + Other: + + + + + + + + 0 + 0 + + + + + + + + Make the file executable + + + true + + + + + + + + + + + 0 + + + 6 + + + + + + 0 + 0 + + + + Owner: + + + + + + + + 0 + 0 + + + + Read + + + + + + + + 0 + 0 + + + + Write + + + + + + + + 0 + 0 + + + + Execute + + + + + + + + 0 + 0 + + + + Group: + + + + + + + + 0 + 0 + + + + Read + + + + + + + + 0 + 0 + + + + Write + + + + + + + + 0 + 0 + + + + Execute + + + + + + + + 0 + 0 + + + + Other: + + + + + + + + 0 + 0 + + + + Read + + + + + + + + 0 + 0 + + + + Write + + + + + + + + 0 + 0 + + + + Execute + + + + + + + + + Sticky + + + + + + + SetUID + + + + + + + SetGID + + + + + + + Qt::Vertical + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + Advanced Mode + + + true + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + Fm::AppChooserComboBox + QComboBox +
appchoosercombobox.h
+
+
+ + + + buttonBox + accepted() + FilePropsDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + FilePropsDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +
diff --git a/src/filedialog.cpp b/src/filedialog.cpp new file mode 100644 index 0000000..3d05650 --- /dev/null +++ b/src/filedialog.cpp @@ -0,0 +1,967 @@ +#include "filedialog.h" +#include "cachedfoldermodel.h" +#include "proxyfoldermodel.h" +#include "utilities.h" +#include "core/fileinfojob.h" +#include "ui_filedialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Fm { + + +FileDialog::FileDialog(QWidget* parent, FilePath path) : + QDialog(parent), + ui{new Ui::FileDialog()}, + folderModel_{nullptr}, + proxyModel_{nullptr}, + folder_{nullptr}, + options_{0}, + viewMode_{FolderView::DetailedListMode}, + fileMode_{QFileDialog::AnyFile}, + acceptMode_{QFileDialog::AcceptOpen}, + confirmOverwrite_{true}, + modelFilter_{this} { + + ui->setupUi(this); + + // path bar + connect(ui->location, &PathBar::chdir, [this](const FilePath &path) { + setDirectoryPath(path); + }); + + // side pane + ui->sidePane->setMode(Fm::SidePane::ModePlaces); + connect(ui->sidePane, &SidePane::chdirRequested, [this](int /*type*/, const FilePath &path) { + setDirectoryPath(path); + }); + + // folder view + proxyModel_ = new ProxyFolderModel(this); + proxyModel_->sort(FolderModel::ColumnFileName, Qt::AscendingOrder); + proxyModel_->setThumbnailSize(64); + proxyModel_->setShowThumbnails(true); + + proxyModel_->addFilter(&modelFilter_); + + connect(ui->folderView, &FolderView::clicked, this, &FileDialog::onFileClicked); + ui->folderView->setModel(proxyModel_); + ui->folderView->setAutoSelectionDelay(0); + // set the completer + QCompleter* completer = new QCompleter(this); + completer->setModel(proxyModel_); + ui->fileName->setCompleter(completer); + connect(completer, static_cast(&QCompleter::activated), [this](const QString &text) { + ui->folderView->selectionModel()->clearSelection(); + selectFilePath(directoryPath_.child(text.toLocal8Bit().constData())); + }); + // select typed paths if it they exist + connect(ui->fileName, &QLineEdit::textEdited, [this](const QString& /*text*/) { + disconnect(ui->folderView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &FileDialog::onSelectionChanged); + ui->folderView->selectionModel()->clearSelection(); + QStringList parsedNames = parseNames(); + for(auto& name: parsedNames) { + selectFilePath(directoryPath_.child(name.toLocal8Bit().constData())); + } + updateAcceptButtonState(); + updateSaveButtonText(false); + connect(ui->folderView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &FileDialog::onSelectionChanged); + }); + // update selection mode for the view + updateSelectionMode(); + + // file type + connect(ui->fileTypeCombo, &QComboBox::currentTextChanged, [this](const QString& text) { + selectNameFilter(text); + }); + ui->fileTypeCombo->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLength); + ui->fileTypeCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + ui->fileTypeCombo->setCurrentIndex(0); + + QShortcut* shortcut = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_H), this); + connect(shortcut, &QShortcut::activated, [this]() { + proxyModel_->setShowHidden(!proxyModel_->showHidden()); + }); + + // setup toolbar buttons + auto toolbar = new QToolBar(this); + // back button + backAction_ = toolbar->addAction(QIcon::fromTheme("go-previous"), tr("Go Back")); + backAction_->setShortcut(QKeySequence(tr("Alt+Left", "Go Back"))); + connect(backAction_, &QAction::triggered, [this]() { + history_.backward(); + setDirectoryPath(history_.currentPath(), FilePath(), false); + }); + // forward button + forwardAction_ = toolbar->addAction(QIcon::fromTheme("go-next"), tr("Go Forward")); + forwardAction_->setShortcut(QKeySequence(tr("Alt+Right", "Go Forward"))); + connect(forwardAction_, &QAction::triggered, [this]() { + history_.forward(); + setDirectoryPath(history_.currentPath(), FilePath(), false); + }); + toolbar->addSeparator(); + // reload button + auto reloadAction = toolbar->addAction(QIcon::fromTheme("view-refresh"), tr("Reload")); + reloadAction->setShortcut(QKeySequence(tr("F5", "Reload"))); + connect(reloadAction, &QAction::triggered, [this]() { + if(folder_ && folder_->isLoaded()) { + QObject::disconnect(lambdaConnection_); + auto selFiles = ui->folderView->selectedFiles(); + ui->folderView->selectionModel()->clear(); + // reselect files on reloading + if(!selFiles.empty() + && selFiles.size() <= 50) { // otherwise senseless and CPU-intensive + lambdaConnection_ = QObject::connect(folder_.get(), &Fm::Folder::finishLoading, [this, selFiles]() { + selectFilesOnReload(selFiles); + }); + } + folder_->reload(); + } + }); + // new folder button + auto newFolderAction = toolbar->addAction(QIcon::fromTheme("folder-new"), tr("Create Folder")); + connect(newFolderAction, &QAction::triggered, this, &FileDialog::onNewFolder); + toolbar->addSeparator(); + // view buttons + auto viewModeGroup = new QActionGroup(this); + iconViewAction_ = toolbar->addAction(style()->standardIcon(QStyle::SP_FileDialogContentsView), tr("Icon View")); + iconViewAction_->setCheckable(true); + connect(iconViewAction_, &QAction::toggled, this, &FileDialog::onViewModeToggled); + viewModeGroup->addAction(iconViewAction_); + thumbnailViewAction_ = toolbar->addAction(style()->standardIcon(QStyle::SP_FileDialogInfoView), tr("Thumbnail View")); + thumbnailViewAction_->setCheckable(true); + connect(thumbnailViewAction_, &QAction::toggled, this, &FileDialog::onViewModeToggled); + viewModeGroup->addAction(thumbnailViewAction_); + compactViewAction_ = toolbar->addAction(style()->standardIcon(QStyle::SP_FileDialogListView), tr("Compact View")); + compactViewAction_->setCheckable(true); + connect(compactViewAction_, &QAction::toggled, this, &FileDialog::onViewModeToggled); + viewModeGroup->addAction(compactViewAction_); + detailedViewAction_ = toolbar->addAction(style()->standardIcon(QStyle::SP_FileDialogDetailedView), tr("Detailed List View")); + detailedViewAction_->setCheckable(true); + connect(detailedViewAction_, &QAction::toggled, this, &FileDialog::onViewModeToggled); + viewModeGroup->addAction(detailedViewAction_); + ui->toolbarLayout->addWidget(toolbar); + + setViewMode(viewMode_); + + // set the default splitter position + setSplitterPos(200); + + // browse to the directory + if(path.isValid()) { + setDirectoryPath(path); + } + else { + goHome(); + } + + // focus the text entry on showing the dialog + QTimer::singleShot(0, ui->fileName, SLOT(setFocus())); +} + +FileDialog::~FileDialog() { + freeFolder(); +} + +int FileDialog::splitterPos() const { + return ui->splitter->sizes().at(0); +} + +void FileDialog::setSplitterPos(int pos) { + QList sizes; + sizes.append(qMax(pos, 0)); + sizes.append(320); + ui->splitter->setSizes(sizes); +} + +// This should always be used instead of getting text directly from the entry. +QStringList FileDialog::parseNames() const { + // parse the file names from the text entry + QStringList parsedNames; + auto fileNames = ui->fileName->text(); + if(!fileNames.isEmpty()) { + /* check if there are multiple file names (containing "), + considering the fact that inside quotes were escaped by \ */ + auto firstQuote = fileNames.indexOf(QLatin1Char('\"')); + auto lastQuote = fileNames.lastIndexOf(QLatin1Char('\"')); + if(firstQuote != -1 && lastQuote != -1 + && firstQuote != lastQuote + && (firstQuote == 0 || fileNames.at(firstQuote - 1) != QLatin1Char('\\')) + && fileNames.at(lastQuote - 1) != QLatin1Char('\\')) { + // split the names + QRegExp sep{"\"\\s+\""}; // separated with " " + parsedNames = fileNames.mid(firstQuote + 1, lastQuote - firstQuote - 1).split(sep); + parsedNames.replaceInStrings(QLatin1String("\\\""), QLatin1String("\"")); + } + else { + parsedNames << fileNames.replace(QLatin1String("\\\""), QLatin1String("\"")); + } + } + return parsedNames; +} + +std::shared_ptr FileDialog::firstSelectedDir() const { + std::shared_ptr selectedFolder = nullptr; + auto list = ui->folderView->selectedFiles(); + for(auto it = list.cbegin(); it != list.cend(); ++it) { + auto& item = *it; + if(item->isDir()) { + selectedFolder = item; + break; + } + } + return selectedFolder; +} + +void FileDialog::accept() { + // handle selected filenames + selectedFiles_.clear(); + + // if a folder is selected in file mode, chdir into it (as QFileDialog does) + // by giving priority to the current index and, if it isn't a folder, + // to the first selected folder + if(fileMode_ != QFileDialog::Directory) { + std::shared_ptr selectedFolder = nullptr; + // check if the current index is a folder + QItemSelectionModel* selModel = ui->folderView->selectionModel(); + QModelIndex cur = selModel->currentIndex(); + if(cur.isValid() && selModel->isSelected(cur)) { + auto file = proxyModel_->fileInfoFromIndex(cur); + if(file && file->isDir()) { + selectedFolder = file; + } + } + if(!selectedFolder) { + selectedFolder = firstSelectedDir(); + } + if(selectedFolder) { + setDirectoryPath(selectedFolder->path()); + return; + } + } + + QStringList parsedNames = parseNames(); + if(parsedNames.isEmpty()) { + // when selecting a dir and the name is not provided, just select current dir in the view + if(fileMode_ == QFileDialog::Directory) { + auto localPath = directoryPath_.localPath(); + if(localPath) { + selectedFiles_.append(QUrl::fromLocalFile(localPath.get())); + } + else { + selectedFiles_.append(directory()); + } + } + else { + QMessageBox::critical(this, tr("Error"), tr("Please select a file")); + return; + } + } + else { + if(fileMode_ != QFileDialog::Directory) { + auto firstName = parsedNames.at(0); + auto childPath = directoryPath_.child(firstName.toLocal8Bit().constData()); + auto info = proxyModel_->fileInfoFromPath(childPath); + if(info) { + // if the typed name belongs to a (nonselected) directory, chdir into it + if(info->isDir()) { + setDirectoryPath(childPath); + return; + } + // overwrite prompt (as in QFileDialog::accept) + if(fileMode_ == QFileDialog::AnyFile + && acceptMode_ != QFileDialog::AcceptOpen + && confirmOverwrite_) { + if (QMessageBox::warning(this, windowTitle(), + tr("%1 already exists.\nDo you want to replace it?") + .arg(firstName), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + == QMessageBox::No) { + return; + } + } + } + } + + // get full paths for the filenames and convert them to URLs + for(auto& name: parsedNames) { + // add default filename extension as needed + if(!defaultSuffix_.isEmpty() && name.lastIndexOf('.') == -1) { + name += '.'; + name += defaultSuffix_; + } + auto fullPath = directoryPath_.child(name.toLocal8Bit().constData()); + auto localPath = fullPath.localPath(); + /* add the local path if it exists; otherwise, add the uri */ + if(localPath) { + selectedFiles_.append(QUrl::fromLocalFile(localPath.get())); + } + else { + selectedFiles_.append(QUrl::fromEncoded(fullPath.uri().get())); + } + } + } + + // check existence of the selected files and if their types are correct + // async operation, call doAccept() in the callback. + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + + auto pathList = pathListFromQUrls(selectedFiles_); + auto job = new FileInfoJob(pathList); + job->setAutoDelete(true); + connect(job, &Job::finished, this, &FileDialog::onFileInfoJobFinished); + job->runAsync(); +} + +void FileDialog::reject() { + QDialog::reject(); +} + +void FileDialog::setDirectory(const QUrl &directory) { + auto path = Fm::FilePath::fromUri(directory.toEncoded().constData()); + setDirectoryPath(path); +} + +// interface for QPlatformFileDialogHelper + +void FileDialog::freeFolder() { + if(folder_) { + QObject::disconnect(lambdaConnection_); // lambdaConnection_ can be invalid + disconnect(folder_.get(), nullptr, this, nullptr); + folder_ = nullptr; + } +} + +void FileDialog::goHome() { + setDirectoryPath(FilePath::homeDir()); +} + +void FileDialog::setDirectoryPath(FilePath directory, FilePath selectedPath, bool addHistory) { + if(!directory.isValid() || directoryPath_ == directory) { + updateAcceptButtonState(); // FIXME: is this needed? + return; + } + + if(folder_) { + if(folderModel_) { + proxyModel_->setSourceModel(nullptr); + folderModel_->unref(); // unref the cached model + folderModel_ = nullptr; + } + freeFolder(); + } + + directoryPath_ = std::move(directory); + + ui->location->setPath(directoryPath_); + ui->sidePane->chdir(directoryPath_); + if(addHistory) { + history_.add(directoryPath_); + } + backAction_->setEnabled(history_.canBackward()); + forwardAction_->setEnabled(history_.canForward()); + + folder_ = Fm::Folder::fromPath(directoryPath_); + folderModel_ = CachedFolderModel::modelFromFolder(folder_); + proxyModel_->setSourceModel(folderModel_); + + // no lambda in these connections for easy disconnection + connect(folder_.get(), &Fm::Folder::removed, this, &FileDialog::goHome); + connect(folder_.get(), &Fm::Folder::unmount, this, &FileDialog::goHome); + + QUrl uri = QUrl::fromEncoded(directory.uri().get()); + Q_EMIT directoryEntered(uri); + + // select the path if valid + if(selectedPath.isValid()) { + if(folder_->isLoaded()) { + selectFilePathWithDelay(selectedPath); + } + else { + lambdaConnection_ = QObject::connect(folder_.get(), &Fm::Folder::finishLoading, [this, selectedPath]() { + selectFilePathWithDelay(selectedPath); + }); + } + } + else { + updateAcceptButtonState(); + updateSaveButtonText(false); + } + +} + +void FileDialog::selectFilePath(const FilePath &path) { + auto idx = proxyModel_->indexFromPath(path); + if(!idx.isValid()) { + return; + } + + // FIXME: add a method to Fm::FolderView to select files + + // FIXME: need to add this for detailed list + QItemSelectionModel::SelectionFlags flags = QItemSelectionModel::Select; + if(viewMode_ == FolderView::DetailedListMode) { + flags |= QItemSelectionModel::Rows; + } + QItemSelectionModel* selModel = ui->folderView->selectionModel(); + selModel->select(idx, flags); + selModel->setCurrentIndex(idx, QItemSelectionModel::Current); + QTimer::singleShot(0, [this, idx]() { + ui->folderView->childView()->scrollTo(idx, QAbstractItemView::PositionAtCenter); + }); +} + +void FileDialog::selectFilePathWithDelay(const FilePath &path) { + QTimer::singleShot(0, [this, path]() { + if(acceptMode_ == QFileDialog::AcceptSave) { + // with a save dialog, always put the base name in line-edit, regardless of selection + ui->fileName->setText(path.baseName().get()); + } + // update "accept" button because there might be no selection later + updateAcceptButtonState(); + updateSaveButtonText(false); + // try to select path + selectFilePath(path); + }); +} + +void FileDialog::selectFilesOnReload(const Fm::FileInfoList& infos) { + QObject::disconnect(lambdaConnection_); + QTimer::singleShot(0, [this, infos]() { + for(auto& fileInfo: infos) { + selectFilePath(fileInfo->path()); + } + }); +} + +void FileDialog::onCurrentRowChanged(const QModelIndex ¤t, const QModelIndex& /*previous*/) { + // emit currentChanged signal + QUrl currentUrl; + if(current.isValid()) { + // emit changed siangl for newly selected items + auto fi = proxyModel_->fileInfoFromIndex(current); + if(fi) { + currentUrl = QUrl::fromEncoded(fi->path().uri().get()); + } + } + Q_EMIT currentChanged(currentUrl); +} + +void FileDialog::onSelectionChanged(const QItemSelection& /*selected*/, const QItemSelection& /*deselected*/) { + auto selFiles = ui->folderView->selectedFiles(); + if(selFiles.empty()) { + updateAcceptButtonState(); + updateSaveButtonText(false); + return; + } + bool multiple(selFiles.size() > 1); + bool hasDir(false); + QString fileNames; + for(auto& fileInfo: selFiles) { + if(fileMode_ == QFileDialog::Directory) { + // if we want to select dir, ignore selected files + if(!fileInfo->isDir()) { + continue; + } + } + else if(fileInfo->isDir()) { + // if we want to select files, ignore selected dirs + hasDir = true; + continue; + } + + auto baseName = fileInfo->path().baseName(); + if(multiple) { + // support multiple selection + if(!fileNames.isEmpty()) { + fileNames += ' '; + } + fileNames += QLatin1Char('\"'); + // escape inside quotes with \ to distinguish between them + // and the quotes used for separating file names from each other + QString name(baseName.get()); + fileNames += name.replace(QLatin1String("\""), QLatin1String("\\\"")); + fileNames += QLatin1Char('\"'); + } + else { + // support single selection only + QString name(baseName.get()); + fileNames = name.replace(QLatin1String("\""), QLatin1String("\\\"")); + break; + } + } + // put the selection list in the text entry + if(!fileNames.isEmpty()) { + ui->fileName->setText(fileNames); + } + updateSaveButtonText(hasDir); + updateAcceptButtonState(); +} + +void FileDialog::onFileClicked(int type, const std::shared_ptr &file) { + bool canAccept = false; + if(file && type == FolderView::ActivatedClick) { + if(file->isDir()) { + if(fileMode_ == QFileDialog::Directory) { + ui->fileName->clear(); + } + // chdir into the activated dir + setDirectoryPath(file->path()); + } + else if(fileMode_ != QFileDialog::Directory) { + // select file(s) and a file item is activated + canAccept = true; + } + } + + if(canAccept) { + selectFilePath(file->path()); + accept(); + } +} + +void FileDialog::onNewFolder() { + createFileOrFolder(CreateNewFolder, directoryPath_, nullptr, this); +} + +void FileDialog::onViewModeToggled(bool active) { + if(active) { + auto action = static_cast(sender()); + FolderView::ViewMode newMode; + if(action == iconViewAction_) { + newMode = FolderView::IconMode; + } + else if(action == thumbnailViewAction_) { + newMode = FolderView::ThumbnailMode; + } + else if(action == compactViewAction_) { + newMode = FolderView::CompactMode; + } + else if(action == detailedViewAction_) { + newMode = FolderView::DetailedListMode; + } + else { + return; + } + setViewMode(newMode); + } +} + +void FileDialog::updateSelectionMode() { + // enable multiple selection? + ui->folderView->childView()->setSelectionMode(fileMode_ == QFileDialog::ExistingFiles ? QAbstractItemView::ExtendedSelection : QAbstractItemView::SingleSelection); +} + +void FileDialog::doAccept() { + + Q_EMIT filesSelected(selectedFiles_); + + if(selectedFiles_.size() == 1) { + Q_EMIT fileSelected(selectedFiles_[0]); + } + + QDialog::accept(); +} + +void FileDialog::onFileInfoJobFinished() { + auto job = static_cast(sender()); + if(job->isCancelled()) { + selectedFiles_.clear(); + reject(); + } + else { + QString error; + // check if the files exist and their types are correct + auto paths = job->paths(); + auto files = job->files(); + for(size_t i = 0; i < paths.size(); ++i) { + const auto& path = paths[i]; + if(i >= files.size() || files[i]->path() != path) { + // the file path is not found and does not have file info + if(fileMode_ != QFileDialog::AnyFile) { + // if we do not allow non-existent file, this is an error. + error = tr("Path \"%1\" does not exist").arg(path.displayName().get()); + break; + } + ++i; // skip the file + continue; + } + + // FIXME: currently, if a path is not found, FmFileInfoJob does not return its file info object. + // This is bad API design. We may return nullptr for the failed file info query instead. + const auto& file = files[i]; + // check if the file type is correct + if(fileMode_ == QFileDialog::Directory) { + if(!file->isDir()) { + // we're selecting dirs, but the selected file path does not point to a dir + error = tr("\"%1\" is not a directory").arg(path.displayName().get()); + break; + } + } + else if(file->isDir() || file->isShortcut()) { + // we're selecting files, but the selected file path refers to a dir or shortcut (such as computer:///) + error = tr("\"%1\" is not a file").arg(path.displayName().get());; + break; + } + } + + if(error.isEmpty()) { + // no error! + doAccept(); + } + else { + QMessageBox::critical(this, tr("Error"), error); + selectedFiles_.clear(); + } + } + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); +} + +QUrl FileDialog::directory() const { + QUrl url{directoryPath_.uri().get()}; + return url; +} + +void FileDialog::selectFile(const QUrl& filename) { + auto urlStr = filename.toEncoded(); + auto path = FilePath::fromUri(urlStr.constData()); + auto parent = path.parent(); + if(parent.isValid() && parent != directoryPath_) { + // chdir into file's parent if it isn't the current directory + setDirectoryPath(parent, path); + } + else { + selectFilePathWithDelay(path); + } +} + +QList FileDialog::selectedFiles() { + return selectedFiles_; +} + +void FileDialog::selectNameFilter(const QString& filter) { + if(filter != currentNameFilter_) { + currentNameFilter_ = filter; + ui->fileTypeCombo->setCurrentText(filter); + + modelFilter_.update(); + proxyModel_->invalidate(); + Q_EMIT filterSelected(filter); + } +} + +void FileDialog::selectMimeTypeFilter(const QString &filter) { + auto idx = mimeTypeFilters_.indexOf(filter); + if(idx != -1) { + ui->fileTypeCombo->setCurrentIndex(idx); + } +} + +QString FileDialog::selectedMimeTypeFilter() const { + QString filter; + auto idx = mimeTypeFilters_.indexOf(filter); + if(idx >= 0 && idx < mimeTypeFilters_.size()) { + filter = mimeTypeFilters_[idx]; + } + return filter; +} + +bool FileDialog::isSupportedUrl(const QUrl& url) { + auto scheme = url.scheme().toLocal8Bit(); + // FIXME: this is not reliable due to the bug of gvfs. + return Fm::isUriSchemeSupported(scheme.constData()); +} + + +// options + +void FileDialog::setFilter(QDir::Filters filters) { + filters_ = filters; + // TODO: +} + +void FileDialog::setViewMode(FolderView::ViewMode mode) { + viewMode_ = mode; + + // Since setModel() is called by FolderView::setViewMode(), the selectionModel will be replaced by one + // created by the view. So, we need to deal with selection changes again after setting the view mode. + disconnect(ui->folderView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &FileDialog::onCurrentRowChanged); + disconnect(ui->folderView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &FileDialog::onSelectionChanged); + + ui->folderView->setViewMode(mode); + switch(mode) { + case FolderView::IconMode: + iconViewAction_->setChecked(true); + break; + case FolderView::ThumbnailMode: + thumbnailViewAction_->setChecked(true); + break; + case FolderView::CompactMode: + compactViewAction_->setChecked(true); + break; + case FolderView::DetailedListMode: + detailedViewAction_->setChecked(true); + break; + default: + break; + } + // selection changes + connect(ui->folderView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &FileDialog::onCurrentRowChanged); + connect(ui->folderView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &FileDialog::onSelectionChanged); + // update selection mode for the view + updateSelectionMode(); +} + + +void FileDialog::setFileMode(QFileDialog::FileMode mode) { + if(mode == QFileDialog::DirectoryOnly) { + // directly only is deprecated and not allowed. + mode = QFileDialog::Directory; + } + fileMode_ = mode; + + // enable multiple selection? + updateSelectionMode(); +} + + +void FileDialog::setAcceptMode(QFileDialog::AcceptMode mode) { + acceptMode_ = mode; + // set "open/save" label if it isn't set explicitly + if(isLabelExplicitlySet(QFileDialog::Accept)) { + return; + } + if(acceptMode_ == QFileDialog::AcceptOpen) { + setLabelTextControl(QFileDialog::Accept, tr("&Open")); + } + else if(acceptMode_ == QFileDialog::AcceptSave) { + setLabelTextControl(QFileDialog::Accept, tr("&Save")); + } +} + +void FileDialog::setNameFilters(const QStringList& filters) { + if(filters.isEmpty()) { + // default filename pattern + nameFilters_ = (QStringList() << tr("All Files (*)")); + } + else { + nameFilters_ = filters; + } + ui->fileTypeCombo->clear(); + ui->fileTypeCombo->addItems(nameFilters_); +} + +void FileDialog::setMimeTypeFilters(const QStringList& filters) { + mimeTypeFilters_ = filters; + + QStringList nameFilters; + QMimeDatabase db; + for(const auto& filter: filters) { + auto mimeType = db.mimeTypeForName(filter); + auto nameFilter = mimeType.comment(); + if(!mimeType.suffixes().empty()) { + nameFilter + " ("; + for(const auto& suffix: mimeType.suffixes()) { + nameFilter += "*."; + nameFilter += suffix; + nameFilter += ' '; + } + nameFilter[nameFilter.length() - 1] = ')'; + } + nameFilters << nameFilter; + } + setNameFilters(nameFilters); +} + +void FileDialog::setLabelTextControl(QFileDialog::DialogLabel label, const QString& text) { + switch(label) { + case QFileDialog::LookIn: + ui->lookInLabel->setText(text); + break; + case QFileDialog::FileName: + ui->fileNameLabel->setText(text); + break; + case QFileDialog::FileType: + ui->fileTypeLabel->setText(text); + break; + case QFileDialog::Accept: + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(text); + break; + case QFileDialog::Reject: + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(text); + break; + default: + break; + } +} + +void FileDialog::setLabelText(QFileDialog::DialogLabel label, const QString& text) { + setLabelExplicitly(label, text); + setLabelTextControl(label, text); +} + +QString FileDialog::labelText(QFileDialog::DialogLabel label) const { + QString text; + switch(label) { + case QFileDialog::LookIn: + text = ui->lookInLabel->text(); + break; + case QFileDialog::FileName: + text = ui->fileNameLabel->text(); + break; + case QFileDialog::FileType: + text = ui->fileTypeLabel->text(); + break; + case QFileDialog::Accept: + ui->buttonBox->button(QDialogButtonBox::Ok)->text(); + break; + case QFileDialog::Reject: + ui->buttonBox->button(QDialogButtonBox::Cancel)->text(); + break; + default: + break; + } + return text; +} + +void FileDialog::updateSaveButtonText(bool saveOnFolder) { + if(fileMode_ != QFileDialog::Directory + && acceptMode_ == QFileDialog::AcceptSave) { + // change save button to open button when there is a dir with the save name, + // otherwise restore it to a save button again + if(!saveOnFolder) { + QStringList parsedNames = parseNames(); + if(!parsedNames.isEmpty()) { + auto info = proxyModel_->fileInfoFromPath(directoryPath_.child(parsedNames.at(0).toLocal8Bit().constData())); + if(info && info->isDir()) { + saveOnFolder = true; + } + } + } + if(saveOnFolder) { + setLabelTextControl(QFileDialog::Accept, tr("&Open")); + } + else { + // restore save button text appropriately + if(isLabelExplicitlySet(QFileDialog::Accept)) { + setLabelTextControl(QFileDialog::Accept, explicitLabels_[QFileDialog::Accept]); + } + else { + setLabelTextControl(QFileDialog::Accept, tr("&Save")); + } + } + } +} + +void FileDialog::updateAcceptButtonState() { + bool enable(false); + if(fileMode_ != QFileDialog::Directory) { + if(acceptMode_ == QFileDialog::AcceptOpen) + { + if(firstSelectedDir()) { + // enable "open" button if a dir is selected + enable = true; + } + else { + // enable "open" button when there is a file whose name is listed + QStringList parsedNames = parseNames(); + for(auto& name: parsedNames) { + if(proxyModel_->indexFromPath(directoryPath_.child(name.toLocal8Bit().constData())).isValid()) { + enable = true; + break; + } + } + } + } + else if(acceptMode_ == QFileDialog::AcceptSave) { + // enable "save" button when there is a name or a dir selection + if(!ui->fileName->text().isEmpty()) { + enable = true; + } + else if(firstSelectedDir()) { + enable = true; + } + } + } + else if(fileMode_ == QFileDialog::Directory + && acceptMode_ != QFileDialog::AcceptSave) { + QStringList parsedNames = parseNames(); + if(parsedNames.isEmpty()) { + // in the dir mode, the current dir will be opened + // if no dir is selected and the name list is empty + enable = true; + } + else { + for(auto& name: parsedNames) { + auto info = proxyModel_->fileInfoFromPath(directoryPath_.child(name.toLocal8Bit().constData())); + if(info && info->isDir()) { + // the name of a dir is listed + enable = true; + break; + } + } + } + } + else { + enable = true; + } + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(enable); +} + +bool FileDialog::FileDialogFilter::filterAcceptsRow(const ProxyFolderModel* /*model*/, const std::shared_ptr &info) const { + if(dlg_->fileMode_ == QFileDialog::Directory) { + // we only want to select directories + if(!info->isDir()) { // not a dir + // NOTE: here we ignore dlg_->options_& QFileDialog::ShowDirsOnly option. + return false; + } + } + else { + // we want to select files, so all directories can be shown regardless of their names + if(info->isDir()) { + return true; + } + } + + bool nameMatched = false; + auto& name = info->displayName(); + for(const auto& pattern: patterns_) { + if(pattern.exactMatch(name)) { + nameMatched = true; + break; + } + } + return nameMatched; +} + +void FileDialog::FileDialogFilter::update() { + // update filename patterns + patterns_.clear(); + QString nameFilter = dlg_->currentNameFilter_; + // if the filter contains (...), only get the part between the parenthesis. + auto left = nameFilter.indexOf('('); + if(left != -1) { + ++left; + auto right = nameFilter.indexOf(')', left); + if(right == -1) { + right = nameFilter.length(); + } + nameFilter = nameFilter.mid(left, right - left); + } + // parse the "*.ext1 *.ext2 *.ext3 ..." list into QRegExp objects + auto globs = nameFilter.simplified().split(' '); + for(const auto& glob: globs) { + patterns_.emplace_back(QRegExp(glob, Qt::CaseInsensitive, QRegExp::Wildcard)); + } +} + +} // namespace Fm diff --git a/src/filedialog.h b/src/filedialog.h new file mode 100644 index 0000000..207d00d --- /dev/null +++ b/src/filedialog.h @@ -0,0 +1,213 @@ +#ifndef FM_FILEDIALOG_H +#define FM_FILEDIALOG_H + +#include "libfmqtglobals.h" +#include "core/filepath.h" + +#include +#include +#include +#include +#include "folderview.h" +#include "browsehistory.h" + +namespace Ui { +class FileDialog; +} + +namespace Fm { + +class CachedFolderModel; +class ProxyFolderModel; + +class LIBFM_QT_API FileDialog : public QDialog { + Q_OBJECT +public: + explicit FileDialog(QWidget *parent = 0, FilePath path = FilePath::homeDir()); + + ~FileDialog(); + + // Some QFileDialog compatible interface + void accept() override; + + void reject() override; + + QFileDialog::Options options() const { + return options_; + } + + void setOptions(QFileDialog::Options options) { + options_ = options; + } + + // interface for QPlatformFileDialogHelper + + void setDirectory(const QUrl &directory); + + QUrl directory() const; + + void selectFile(const QUrl &filename); + + QList selectedFiles(); + + void selectNameFilter(const QString &filter); + + QString selectedNameFilter() const { + return currentNameFilter_; + } + + void selectMimeTypeFilter(const QString &filter); + + QString selectedMimeTypeFilter() const; + + bool isSupportedUrl(const QUrl &url); + + // options + + // not yet supported + QDir::Filters filter() const { + return filters_; + } + // not yet supported + void setFilter(QDir::Filters filters); + + void setViewMode(FolderView::ViewMode mode); + FolderView::ViewMode viewMode() const { + return viewMode_; + } + + void setFileMode(QFileDialog::FileMode mode); + QFileDialog::FileMode fileMode() const { + return fileMode_; + } + + void setAcceptMode(QFileDialog::AcceptMode mode); + QFileDialog::AcceptMode acceptMode() const { + return acceptMode_; + } + + void setNameFilters(const QStringList &filters); + QStringList nameFilters() const { + return nameFilters_; + } + + void setMimeTypeFilters(const QStringList &filters); + QStringList mimeTypeFilters() const { + return mimeTypeFilters_; + } + + void setDefaultSuffix(const QString &suffix) { + if(!suffix.isEmpty() && suffix[0] == '.') { + // if the first char is dot, remove it. + defaultSuffix_ = suffix.mid(1); + } + else { + defaultSuffix_ = suffix; + } + } + QString defaultSuffix() const { + return defaultSuffix_; + } + + void setConfirmOverwrite(bool enabled) { + confirmOverwrite_ = enabled; + } + bool confirmOverwrite() const { + return confirmOverwrite_; + } + + void setLabelText(QFileDialog::DialogLabel label, const QString &text); + QString labelText(QFileDialog::DialogLabel label) const; + + int splitterPos() const; + void setSplitterPos(int pos); + +private Q_SLOTS: + void onCurrentRowChanged(const QModelIndex ¤t, const QModelIndex& /*previous*/); + void onSelectionChanged(const QItemSelection& /*selected*/, const QItemSelection& /*deselected*/); + void onFileClicked(int type, const std::shared_ptr& file); + void onNewFolder(); + void onViewModeToggled(bool active); + void goHome(); + +Q_SIGNALS: + // emitted when the dialog is accepted and some files are selected + void fileSelected(const QUrl &file); + void filesSelected(const QList &files); + + // emitted whenever selection changes (including no selected files) + void currentChanged(const QUrl &path); + + void directoryEntered(const QUrl &directory); + void filterSelected(const QString &filter); + +private: + + class FileDialogFilter: public ProxyFolderModelFilter { + public: + FileDialogFilter(FileDialog* dlg): dlg_{dlg} {} + virtual bool filterAcceptsRow(const ProxyFolderModel* /*model*/, const std::shared_ptr& info) const override; + void update(); + + FileDialog* dlg_; + std::vector patterns_; + }; + + bool isLabelExplicitlySet(QFileDialog::DialogLabel label) const { + return !explicitLabels_[label].isEmpty(); + } + void setLabelExplicitly(QFileDialog::DialogLabel label, const QString& text) { + explicitLabels_[label] = text; + } + void setLabelTextControl(QFileDialog::DialogLabel label, const QString &text); + void updateSaveButtonText(bool saveOnFolder); + void updateAcceptButtonState(); + + std::shared_ptr firstSelectedDir() const; + void selectFilePath(const FilePath& path); + void selectFilePathWithDelay(const FilePath& path); + void selectFilesOnReload(const Fm::FileInfoList& infos); + void setDirectoryPath(FilePath directory, FilePath selectedPath = FilePath(), bool addHistory = true); + void updateSelectionMode(); + void doAccept(); + void onFileInfoJobFinished(); + void freeFolder(); + QStringList parseNames() const; + +private: + std::unique_ptr ui; + CachedFolderModel* folderModel_; + ProxyFolderModel* proxyModel_; + FilePath directoryPath_; + std::shared_ptr folder_; + Fm::BrowseHistory history_; + + QFileDialog::Options options_; + QDir::Filters filters_; + FolderView::ViewMode viewMode_; + QFileDialog::FileMode fileMode_; + QFileDialog::AcceptMode acceptMode_; + bool confirmOverwrite_; + QStringList nameFilters_; + QStringList mimeTypeFilters_; + QString defaultSuffix_; + FileDialogFilter modelFilter_; + QString currentNameFilter_; + QList selectedFiles_; + // view modes: + QAction* iconViewAction_; + QAction* thumbnailViewAction_; + QAction* compactViewAction_; + QAction* detailedViewAction_; + // back and forward buttons: + QAction* backAction_; + QAction* forwardAction_; + // dialog labels that can be set explicitly: + QString explicitLabels_[5]; + // needed for disconnecting Fm::Folder signal from lambda: + QMetaObject::Connection lambdaConnection_; +}; + + +} // namespace Fm +#endif // FM_FILEDIALOG_H diff --git a/src/filedialog.ui b/src/filedialog.ui new file mode 100644 index 0000000..cc7d65e --- /dev/null +++ b/src/filedialog.ui @@ -0,0 +1,151 @@ + + + FileDialog + + + + 0 + 0 + 700 + 500 + + + + + + + + + + + + Location: + + + + + + + + + + + + Qt::Horizontal + + + + + 0 + 0 + + + + + 120 + 0 + + + + + + + 1 + 0 + + + + + + + + + + + File name: + + + + + + + + + + File type: + + + + + + + + + + Qt::Vertical + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + Fm::SidePane + QWidget +
sidepane.h
+ 1 +
+ + Fm::FolderView + QWidget +
folderview.h
+ 1 +
+ + Fm::PathBar + QWidget +
pathbar.h
+ 1 +
+
+ + + + buttonBox + accepted() + FileDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + FileDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +
diff --git a/src/filelauncher.cpp b/src/filelauncher.cpp new file mode 100644 index 0000000..6131c01 --- /dev/null +++ b/src/filelauncher.cpp @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "filelauncher.h" +#include "applaunchcontext.h" +#include +#include +#include "execfiledialog_p.h" +#include "appchooserdialog.h" +#include "utilities.h" + +namespace Fm { + +FmFileLauncher FileLauncher::funcs = { + FileLauncher::_getApp, + /* gboolean (*before_open)(GAppLaunchContext* ctx, GList* folder_infos, gpointer user_data); */ + (FmLaunchFolderFunc)FileLauncher::_openFolder, + FileLauncher::_execFile, + FileLauncher::_error, + FileLauncher::_ask, + nullptr +}; + +FileLauncher::FileLauncher(): + quickExec_(false) { +} + +FileLauncher::~FileLauncher() { +} + +bool FileLauncher::launchFiles(QWidget *parent, Fm::FileInfoList file_infos) { + // FIXME: rewrite + return launchPaths(parent, file_infos.paths()); +} + +bool FileLauncher::launchPaths(QWidget *parent, Fm::FilePathList paths) { + // FIXME: rewrite, port to new api + GList* tmp = nullptr; + for(auto& path: paths) { + auto fmpath = fm_path_new_for_gfile(path.gfile().get()); + tmp = g_list_prepend(tmp, fmpath); + } + tmp = g_list_reverse(tmp); + bool ret = launchPaths(parent, tmp); + g_list_free(tmp); + return ret; +} + +bool FileLauncher::launchFiles(QWidget* parent, GList* file_infos) { + FmAppLaunchContext* context = fm_app_launch_context_new_for_widget(parent); + bool ret = fm_launch_files(G_APP_LAUNCH_CONTEXT(context), file_infos, &funcs, this); + g_object_unref(context); + return ret; +} + +bool FileLauncher::launchPaths(QWidget* parent, GList* paths) { + FmAppLaunchContext* context = fm_app_launch_context_new_for_widget(parent); + bool ret = fm_launch_paths(G_APP_LAUNCH_CONTEXT(context), paths, &funcs, this); + g_object_unref(context); + return ret; +} + +GAppInfo* FileLauncher::getApp(GList* /*file_infos*/, FmMimeType* mime_type, GError** /*err*/) { + AppChooserDialog dlg(nullptr); + if(mime_type) { + dlg.setMimeType(Fm::MimeType::fromName(fm_mime_type_get_type(mime_type))); + } + else { + dlg.setCanSetDefault(false); + } + // FIXME: show error properly? + if(execModelessDialog(&dlg) == QDialog::Accepted) { + auto app = dlg.selectedApp(); + return app.release(); + } + return nullptr; +} + +bool FileLauncher::openFolder(GAppLaunchContext* /*ctx*/, GList* folder_infos, GError** /*err*/) { + for(GList* l = folder_infos; l; l = l->next) { + FmFileInfo* fi = FM_FILE_INFO(l->data); + qDebug() << " folder:" << QString::fromUtf8(fm_file_info_get_disp_name(fi)); + } + return false; +} + +FmFileLauncherExecAction FileLauncher::execFile(FmFileInfo* file) { + if(quickExec_) { + /* SF bug#838: open terminal for each script may be just a waste. + User should open a terminal and start the script there + in case if user wants to see the script output anyway. + if (fm_file_info_is_text(file)) + return FM_FILE_LAUNCHER_EXEC_IN_TERMINAL; */ + return FM_FILE_LAUNCHER_EXEC; + } + + FmFileLauncherExecAction res = FM_FILE_LAUNCHER_EXEC_CANCEL; + ExecFileDialog dlg(file); + if(execModelessDialog(&dlg) == QDialog::Accepted) { + res = dlg.result(); + } + return res; +} + +int FileLauncher::ask(const char* /*msg*/, char* const* /*btn_labels*/, int /*default_btn*/) { + /* FIXME: set default button properly */ + // return fm_askv(data->parent, nullptr, msg, btn_labels); + return -1; +} + +bool FileLauncher::error(GAppLaunchContext* /*ctx*/, GError* err, FmPath* path) { + /* ask for mount if trying to launch unmounted path */ + if(err->domain == G_IO_ERROR) { + if(path && err->code == G_IO_ERROR_NOT_MOUNTED) { + //if(fm_mount_path(data->parent, path, TRUE)) + // return FALSE; /* ask to retry */ + } + else if(err->code == G_IO_ERROR_FAILED_HANDLED) { + return true; /* don't show error message */ + } + } + QMessageBox dlg(QMessageBox::Critical, QObject::tr("Error"), QString::fromUtf8(err->message), QMessageBox::Ok); + execModelessDialog(&dlg); + return true; +} + + +} // namespace Fm diff --git a/src/filelauncher.h b/src/filelauncher.h new file mode 100644 index 0000000..9ebcfe6 --- /dev/null +++ b/src/filelauncher.h @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_FILELAUNCHER_H +#define FM_FILELAUNCHER_H + +#include "libfmqtglobals.h" +#include +#include +#include "core/fileinfo.h" + +namespace Fm { + +class LIBFM_QT_API FileLauncher { +public: + explicit FileLauncher(); + virtual ~FileLauncher(); + + bool launchFiles(QWidget* parent, Fm::FileInfoList file_infos); + + bool launchPaths(QWidget* parent, Fm::FilePathList paths); + + bool quickExec() const { + return quickExec_; + } + + void setQuickExec(bool value) { + quickExec_ = value; + } + +protected: + + virtual GAppInfo* getApp(GList* file_infos, FmMimeType* mime_type, GError** err); + virtual bool openFolder(GAppLaunchContext* ctx, GList* folder_infos, GError** err); + virtual FmFileLauncherExecAction execFile(FmFileInfo* file); + virtual bool error(GAppLaunchContext* ctx, GError* err, FmPath* path); + virtual int ask(const char* msg, char* const* btn_labels, int default_btn); + +private: + bool launchFiles(QWidget* parent, GList* file_infos); + + bool launchPaths(QWidget* parent, GList* paths); + + static GAppInfo* _getApp(GList* file_infos, FmMimeType* mime_type, gpointer user_data, GError** err) { + return reinterpret_cast(user_data)->getApp(file_infos, mime_type, err); + } + static gboolean _openFolder(GAppLaunchContext* ctx, GList* folder_infos, gpointer user_data, GError** err) { + return reinterpret_cast(user_data)->openFolder(ctx, folder_infos, err); + } + static FmFileLauncherExecAction _execFile(FmFileInfo* file, gpointer user_data) { + return reinterpret_cast(user_data)->execFile(file); + } + static gboolean _error(GAppLaunchContext* ctx, GError* err, FmPath* file, gpointer user_data) { + return reinterpret_cast(user_data)->error(ctx, err, file); + } + static int _ask(const char* msg, char* const* btn_labels, int default_btn, gpointer user_data) { + return reinterpret_cast(user_data)->ask(msg, btn_labels, default_btn); + } + +private: + static FmFileLauncher funcs; + bool quickExec_; // Don't ask options on launch executable file +}; + +} + +#endif // FM_FILELAUNCHER_H diff --git a/src/filemenu.cpp b/src/filemenu.cpp new file mode 100644 index 0000000..e3942a3 --- /dev/null +++ b/src/filemenu.cpp @@ -0,0 +1,418 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "filemenu.h" +#include "createnewmenu.h" +#include "icontheme.h" +#include "filepropsdialog.h" +#include "utilities.h" +#include "fileoperation.h" +#include "filelauncher.h" +#include "appchooserdialog.h" + +#include "customactions/fileaction.h" +#include "customaction_p.h" + +#include +#include +#include +#include "filemenu_p.h" + +#include "core/compat_p.h" + +namespace Fm { + +FileMenu::FileMenu(Fm::FileInfoList files, std::shared_ptr info, Fm::FilePath cwd, bool isWritableDir, const QString& title, QWidget* parent): + QMenu(title, parent), + files_{std::move(files)}, + info_{std::move(info)}, + cwd_{std::move(cwd)}, + unTrashAction_(nullptr), + fileLauncher_(nullptr) { + + useTrash_ = true; + confirmDelete_ = true; + confirmTrash_ = false; // Confirm before moving files into "trash can" + + openAction_ = nullptr; + openWithMenuAction_ = nullptr; + openWithAction_ = nullptr; + separator1_ = nullptr; + cutAction_ = nullptr; + copyAction_ = nullptr; + pasteAction_ = nullptr; + deleteAction_ = nullptr; + unTrashAction_ = nullptr; + renameAction_ = nullptr; + separator2_ = nullptr; + propertiesAction_ = nullptr; + + auto mime_type = info_->mimeType(); + Fm::FilePath path = info_->path(); + + // check if the files are of the same type + sameType_ = files_.isSameType(); + // check if the files are on the same filesystem + sameFilesystem_ = files_.isSameFilesystem(); + // check if the files are all virtual + + // FIXME: allVirtual_ = sameFilesystem_ && fm_path_is_virtual(path); + allVirtual_ = false; + + // check if the files are all in the trash can + allTrash_ = sameFilesystem_ && path.hasUriScheme("trash"); + + openAction_ = new QAction(QIcon::fromTheme("document-open"), tr("Open"), this); + connect(openAction_, &QAction::triggered, this, &FileMenu::onOpenTriggered); + addAction(openAction_); + + openWithMenuAction_ = new QAction(tr("Open With..."), this); + addAction(openWithMenuAction_); + // create the "Open with..." sub menu + QMenu* menu = new QMenu(this); + openWithMenuAction_->setMenu(menu); + + if(sameType_) { /* add specific menu items for this mime type */ + if(mime_type && !allVirtual_) { /* the file has a valid mime-type and its not virtual */ + GList* apps = g_app_info_get_all_for_type(mime_type->name()); + GList* l; + for(l = apps; l; l = l->next) { + Fm::GAppInfoPtr app{G_APP_INFO(l->data), false}; + // check if the command really exists + gchar* program_path = g_find_program_in_path(g_app_info_get_executable(app.get())); + if(!program_path) { + continue; + } + g_free(program_path); + + // create a QAction for the application. + AppInfoAction* action = new AppInfoAction(std::move(app), menu); + connect(action, &QAction::triggered, this, &FileMenu::onApplicationTriggered); + menu->addAction(action); + } + g_list_free(apps); + } + } + menu->addSeparator(); + openWithAction_ = new QAction(tr("Other Applications"), this); + connect(openWithAction_, &QAction::triggered, this, &FileMenu::onOpenWithTriggered); + menu->addAction(openWithAction_); + + separator1_ = addSeparator(); + + createAction_ = new QAction(tr("Create &New"), this); + Fm::FilePath dirPath = files_.size() == 1 && info_->isDir() ? path : cwd_; + createAction_->setMenu(new CreateNewMenu(nullptr, dirPath, this)); + addAction(createAction_); + + separator2_ = addSeparator(); + + if(allTrash_) { // all selected files are in trash:/// + bool can_restore = true; + /* only immediate children of trash:/// can be restored. */ + auto trash_root = Fm::FilePath::fromUri("trash:///"); + for(auto& file: files_) { + Fm::FilePath trash_path = file->path(); + if(!trash_root.isParentOf(trash_path)) { + can_restore = false; + break; + } + } + if(can_restore) { + unTrashAction_ = new QAction(tr("&Restore"), this); + connect(unTrashAction_, &QAction::triggered, this, &FileMenu::onUnTrashTriggered); + addAction(unTrashAction_); + } + } + else { // ordinary files + cutAction_ = new QAction(QIcon::fromTheme("edit-cut"), tr("Cut"), this); + connect(cutAction_, &QAction::triggered, this, &FileMenu::onCutTriggered); + addAction(cutAction_); + + copyAction_ = new QAction(QIcon::fromTheme("edit-copy"), tr("Copy"), this); + connect(copyAction_, &QAction::triggered, this, &FileMenu::onCopyTriggered); + addAction(copyAction_); + + pasteAction_ = new QAction(QIcon::fromTheme("edit-paste"), tr("Paste"), this); + connect(pasteAction_, &QAction::triggered, this, &FileMenu::onPasteTriggered); + addAction(pasteAction_); + + deleteAction_ = new QAction(QIcon::fromTheme("user-trash"), tr("&Move to Trash"), this); + connect(deleteAction_, &QAction::triggered, this, &FileMenu::onDeleteTriggered); + addAction(deleteAction_); + + renameAction_ = new QAction(tr("Rename"), this); + connect(renameAction_, &QAction::triggered, this, &FileMenu::onRenameTriggered); + addAction(renameAction_); + + // disable actons that can't be used + bool hasAccessible(false); + bool hasDeletable(false); + bool hasRenamable(false); + for(auto& file: files_) { + if(file->isAccessible()) { + hasAccessible = true; + } + if(file->isDeletable()) { + hasDeletable = true; + } + if(file->canSetName()) { + hasRenamable = true; + } + if (hasAccessible && hasDeletable && hasRenamable) { + break; + } + } + copyAction_->setEnabled(hasAccessible); + cutAction_->setEnabled(hasDeletable); + deleteAction_->setEnabled(hasDeletable); + renameAction_->setEnabled(hasRenamable); + if(!(sameType_ && info_->isDir() + && (files_.size() > 1 ? isWritableDir : info_->isWritable()))) { + pasteAction_->setEnabled(false); + } + } + + // DES-EMA custom actions integration + // FIXME: port these parts to Fm API + auto custom_actions = FileActionItem::get_actions_for_files(files_); + for(auto& item: custom_actions) { + if(item && !(item->get_target() & FILE_ACTION_TARGET_CONTEXT)) { + continue; // this item is not for context menu + } + if(item == custom_actions.front() && !item->is_action()) { + addSeparator(); // before all custom actions + } + addCustomActionItem(this, item); + } + + // archiver integration + // FIXME: we need to modify upstream libfm to include some Qt-based archiver programs. + if(!allVirtual_) { + if(sameType_) { + // FIXME: port these parts to Fm API + FmArchiver* archiver = fm_archiver_get_default(); + if(archiver) { + if(fm_archiver_is_mime_type_supported(archiver, mime_type->name())) { + QAction* archiverSeparator = nullptr; + if(cwd_ && archiver->extract_to_cmd) { + archiverSeparator = addSeparator(); + QAction* action = new QAction(tr("Extract to..."), this); + connect(action, &QAction::triggered, this, &FileMenu::onExtract); + addAction(action); + } + if(archiver->extract_cmd) { + if(!archiverSeparator) { + addSeparator(); + } + QAction* action = new QAction(tr("Extract Here"), this); + connect(action, &QAction::triggered, this, &FileMenu::onExtractHere); + addAction(action); + } + } + else { + addSeparator(); + QAction* action = new QAction(tr("Compress"), this); + connect(action, &QAction::triggered, this, &FileMenu::onCompress); + addAction(action); + } + } + } + } + + separator3_ = addSeparator(); + + propertiesAction_ = new QAction(QIcon::fromTheme("document-properties"), tr("Properties"), this); + connect(propertiesAction_, &QAction::triggered, this, &FileMenu::onFilePropertiesTriggered); + addAction(propertiesAction_); +} + +FileMenu::~FileMenu() { +} + + +void FileMenu::addCustomActionItem(QMenu* menu, std::shared_ptr item) { + if(!item) { // separator + addSeparator(); + return; + } + + // this action is not for context menu + if(item->is_action() && !(item->get_target() & FILE_ACTION_TARGET_CONTEXT)) { + return; + } + + CustomAction* action = new CustomAction(item, menu); + menu->addAction(action); + if(item->is_menu()) { + auto& subitems = item->get_sub_items(); + if(!subitems.empty()) { + QMenu* submenu = new QMenu(menu); + for(auto& subitem: subitems) { + addCustomActionItem(submenu, subitem); + } + action->setMenu(submenu); + } + } + else if(item->is_action()) { + connect(action, &QAction::triggered, this, &FileMenu::onCustomActionTrigerred); + } +} + +void FileMenu::onOpenTriggered() { + if(fileLauncher_) { + fileLauncher_->launchFiles(nullptr, files_); + } + else { // use the default launcher + Fm::FileLauncher launcher; + launcher.launchFiles(nullptr, files_); + } +} + +void FileMenu::onOpenWithTriggered() { + AppChooserDialog dlg(nullptr); + if(sameType_) { + dlg.setMimeType(info_->mimeType()); + } + else { // we can only set the selected app as default if all files are of the same type + dlg.setCanSetDefault(false); + } + + if(execModelessDialog(&dlg) == QDialog::Accepted) { + auto app = dlg.selectedApp(); + if(app) { + openFilesWithApp(app.get()); + } + } +} + +void FileMenu::openFilesWithApp(GAppInfo* app) { + GList* uris = nullptr; + for(auto& file: files_) { + auto uri = file->path().uri(); + uris = g_list_prepend(uris, uri.release()); + } + fm_app_info_launch_uris(app, uris, nullptr, nullptr); + g_list_foreach(uris, (GFunc)g_free, nullptr); + g_list_free(uris); +} + +void FileMenu::onApplicationTriggered() { + AppInfoAction* action = static_cast(sender()); + openFilesWithApp(action->appInfo().get()); +} + +void FileMenu::onCustomActionTrigerred() { + CustomAction* action = static_cast(sender()); + auto& item = action->item(); + /* g_debug("item: %s is activated, id:%s", fm_file_action_item_get_name(item), + fm_file_action_item_get_id(item)); */ + CStrPtr output; + item->launch(nullptr, files_, output); + if(output) { + QMessageBox::information(this, tr("Output"), output.get()); + } +} + +void FileMenu::onFilePropertiesTriggered() { + FilePropsDialog::showForFiles(files_); +} + +void FileMenu::onCopyTriggered() { + Fm::copyFilesToClipboard(files_.paths()); +} + +void FileMenu::onCutTriggered() { + Fm::cutFilesToClipboard(files_.paths()); +} + +void FileMenu::onDeleteTriggered() { + auto paths = files_.paths(); + if(useTrash_) { + FileOperation::trashFiles(paths, confirmTrash_); + } + else { + FileOperation::deleteFiles(paths, confirmDelete_); + } +} + +void FileMenu::onUnTrashTriggered() { + FileOperation::unTrashFiles(files_.paths()); +} + +void FileMenu::onPasteTriggered() { + Fm::pasteFilesFromClipboard(cwd_); +} + +void FileMenu::onRenameTriggered() { + // if there is a view and this is a single file, just edit the current index + if(files_.size() == 1) { + if (QAbstractItemView* view = qobject_cast(parentWidget())) { + QModelIndexList selIndexes = view->selectionModel()->selectedIndexes(); + if(selIndexes.size() > 1) { // in the detailed list mode, only the first index is editable + view->setCurrentIndex(selIndexes.at(0)); + } + if (view->currentIndex().isValid()) { + view->edit(view->currentIndex()); + return; + } + } + } + for(auto& info: files_) { + Fm::renameFile(info, nullptr); + } +} + +void FileMenu::setUseTrash(bool trash) { + if(useTrash_ != trash) { + useTrash_ = trash; + if(deleteAction_) { + deleteAction_->setText(useTrash_ ? tr("&Move to Trash") : tr("&Delete")); + deleteAction_->setIcon(useTrash_ ? QIcon::fromTheme("user-trash") : QIcon::fromTheme("edit-delete")); + } + } +} + +void FileMenu::onCompress() { + FmArchiver* archiver = fm_archiver_get_default(); + if(archiver) { + auto paths = Fm::_convertPathList(files_.paths()); + fm_archiver_create_archive(archiver, nullptr, paths.dataPtr()); + } +} + +void FileMenu::onExtract() { + FmArchiver* archiver = fm_archiver_get_default(); + if(archiver) { + auto paths = Fm::_convertPathList(files_.paths()); + fm_archiver_extract_archives(archiver, nullptr, paths.dataPtr()); + } +} + +void FileMenu::onExtractHere() { + FmArchiver* archiver = fm_archiver_get_default(); + if(archiver) { + auto paths = Fm::_convertPathList(files_.paths()); + auto cwd = Fm::_convertPath(cwd_); + fm_archiver_extract_archives_to(archiver, nullptr, paths.dataPtr(), cwd); + } +} + +} // namespace Fm diff --git a/src/filemenu.h b/src/filemenu.h new file mode 100644 index 0000000..c683d84 --- /dev/null +++ b/src/filemenu.h @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_FILEMENU_H +#define FM_FILEMENU_H + +#include "libfmqtglobals.h" +#include +#include +#include +#include "core/fileinfo.h" + +class QAction; + +namespace Fm { + +class FileLauncher; +class FileActionItem; + +class LIBFM_QT_API FileMenu : public QMenu { + Q_OBJECT + +public: + explicit FileMenu(Fm::FileInfoList files, std::shared_ptr info, Fm::FilePath cwd, bool isWritableDir = true, const QString& title = QString(), QWidget* parent = nullptr); + ~FileMenu(); + + bool useTrash() { + return useTrash_; + } + + void setUseTrash(bool trash); + + bool confirmDelete() { + return confirmDelete_; + } + + void setConfirmDelete(bool confirm) { + confirmDelete_ = confirm; + } + + QAction* openAction() { + return openAction_; + } + + QAction* openWithMenuAction() { + return openWithMenuAction_; + } + + QAction* openWithAction() { + return openWithAction_; + } + + QAction* separator1() { + return separator1_; + } + + QAction* createAction() { + return createAction_; + } + + QAction* separator2() { + return separator2_; + } + + QAction* cutAction() { + return cutAction_; + } + + QAction* copyAction() { + return copyAction_; + } + + QAction* pasteAction() { + return pasteAction_; + } + + QAction* deleteAction() { + return deleteAction_; + } + + QAction* unTrashAction() { + return unTrashAction_; + } + + QAction* renameAction() { + return renameAction_; + } + + QAction* separator3() { + return separator3_; + } + + QAction* propertiesAction() { + return propertiesAction_; + } + + const Fm::FileInfoList& files() const { + return files_; + } + + const std::shared_ptr& firstFile() const { + return info_; + } + + const Fm::FilePath& cwd() const { + return cwd_; + } + + void setFileLauncher(FileLauncher* launcher) { + fileLauncher_ = launcher; + } + + FileLauncher* fileLauncher() { + return fileLauncher_; + } + + bool sameType() const { + return sameType_; + } + + bool sameFilesystem() const { + return sameFilesystem_; + } + + bool allVirtual() const { + return allVirtual_; + } + + bool allTrash() const { + return allTrash_; + } + + bool confirmTrash() const { + return confirmTrash_; + } + + void setConfirmTrash(bool value) { + confirmTrash_ = value; + } + +protected: + void addCustomActionItem(QMenu* menu, std::shared_ptr item); + void openFilesWithApp(GAppInfo* app); + +protected Q_SLOTS: + void onOpenTriggered(); + void onOpenWithTriggered(); + void onFilePropertiesTriggered(); + void onApplicationTriggered(); + void onCustomActionTrigerred(); + void onCompress(); + void onExtract(); + void onExtractHere(); + + void onCutTriggered(); + void onCopyTriggered(); + void onPasteTriggered(); + void onRenameTriggered(); + void onDeleteTriggered(); + void onUnTrashTriggered(); + +private: + Fm::FileInfoList files_; + std::shared_ptr info_; + Fm::FilePath cwd_; + bool useTrash_; + bool confirmDelete_; + bool confirmTrash_; // Confirm before moving files into "trash can" + + bool sameType_; + bool sameFilesystem_; + bool allVirtual_; + bool allTrash_; + + QAction* openAction_; + QAction* openWithMenuAction_; + QAction* openWithAction_; + QAction* separator1_; + QAction* createAction_; + QAction* separator2_; + QAction* cutAction_; + QAction* copyAction_; + QAction* pasteAction_; + QAction* deleteAction_; + QAction* unTrashAction_; + QAction* renameAction_; + QAction* separator3_; + QAction* propertiesAction_; + + FileLauncher* fileLauncher_; +}; + +} + +#endif // FM_FILEMENU_H diff --git a/src/filemenu_p.h b/src/filemenu_p.h new file mode 100644 index 0000000..c257dc0 --- /dev/null +++ b/src/filemenu_p.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_FILEMENU_P_H +#define FM_FILEMENU_P_H + +#include "icontheme.h" +#include +#include "core/gioptrs.h" +#include "core/iconinfo.h" + +namespace Fm { + +class AppInfoAction : public QAction { + Q_OBJECT +public: + explicit AppInfoAction(Fm::GAppInfoPtr app, QObject* parent = 0): + QAction(QString::fromUtf8(g_app_info_get_name(app.get())), parent), + appInfo_{std::move(app)} { + setToolTip(QString::fromUtf8(g_app_info_get_description(appInfo_.get()))); + GIcon* gicon = g_app_info_get_icon(appInfo_.get()); + const auto icnInfo = Fm::IconInfo::fromGIcon(gicon); + if(icnInfo) { + setIcon(icnInfo->qicon()); + } + } + + virtual ~AppInfoAction() { + } + + const Fm::GAppInfoPtr& appInfo() const { + return appInfo_; + } + +private: + Fm::GAppInfoPtr appInfo_; +}; + +} // namespace Fm + +#endif diff --git a/src/fileoperation.cpp b/src/fileoperation.cpp new file mode 100644 index 0000000..af0ad7c --- /dev/null +++ b/src/fileoperation.cpp @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "fileoperation.h" +#include "fileoperationdialog.h" +#include +#include +#include +#include +#include "path.h" + +#include "core/compat_p.h" + +namespace Fm { + +#define SHOW_DLG_DELAY 1000 + +FileOperation::FileOperation(Type type, Fm::FilePathList srcFiles, QObject* parent): + QObject(parent), + job_{fm_file_ops_job_new((FmFileOpType)type, Fm::_convertPathList(srcFiles))}, + dlg{nullptr}, + srcPaths{std::move(srcFiles)}, + uiTimer(nullptr), + elapsedTimer_(nullptr), + lastElapsed_(0), + updateRemainingTime_(true), + autoDestroy_(true) { + + g_signal_connect(job_, "ask", G_CALLBACK(onFileOpsJobAsk), this); + g_signal_connect(job_, "ask-rename", G_CALLBACK(onFileOpsJobAskRename), this); + g_signal_connect(job_, "error", G_CALLBACK(onFileOpsJobError), this); + g_signal_connect(job_, "prepared", G_CALLBACK(onFileOpsJobPrepared), this); + g_signal_connect(job_, "cur-file", G_CALLBACK(onFileOpsJobCurFile), this); + g_signal_connect(job_, "percent", G_CALLBACK(onFileOpsJobPercent), this); + g_signal_connect(job_, "finished", G_CALLBACK(onFileOpsJobFinished), this); + g_signal_connect(job_, "cancelled", G_CALLBACK(onFileOpsJobCancelled), this); +} + +void FileOperation::disconnectJob() { + g_signal_handlers_disconnect_by_func(job_, (gpointer)G_CALLBACK(onFileOpsJobAsk), this); + g_signal_handlers_disconnect_by_func(job_, (gpointer)G_CALLBACK(onFileOpsJobAskRename), this); + g_signal_handlers_disconnect_by_func(job_, (gpointer)G_CALLBACK(onFileOpsJobError), this); + g_signal_handlers_disconnect_by_func(job_, (gpointer)G_CALLBACK(onFileOpsJobPrepared), this); + g_signal_handlers_disconnect_by_func(job_, (gpointer)G_CALLBACK(onFileOpsJobCurFile), this); + g_signal_handlers_disconnect_by_func(job_, (gpointer)G_CALLBACK(onFileOpsJobPercent), this); + g_signal_handlers_disconnect_by_func(job_, (gpointer)G_CALLBACK(onFileOpsJobFinished), this); + g_signal_handlers_disconnect_by_func(job_, (gpointer)G_CALLBACK(onFileOpsJobCancelled), this); +} + +FileOperation::~FileOperation() { + if(uiTimer) { + uiTimer->stop(); + delete uiTimer; + uiTimer = nullptr; + } + if(elapsedTimer_) { + delete elapsedTimer_; + elapsedTimer_ = nullptr; + } + + if(job_) { + disconnectJob(); + g_object_unref(job_); + } +} + +void FileOperation::setDestination(Fm::FilePath dest) { + destPath = std::move(dest); + auto tmp = Fm::Path::newForGfile(dest.gfile().get()); + fm_file_ops_job_set_dest(job_, tmp.dataPtr()); +} + +bool FileOperation::run() { + delete uiTimer; + // run the job + uiTimer = new QTimer(); + uiTimer->start(SHOW_DLG_DELAY); + connect(uiTimer, &QTimer::timeout, this, &FileOperation::onUiTimeout); + + return fm_job_run_async(FM_JOB(job_)); +} + +void FileOperation::onUiTimeout() { + if(dlg) { + dlg->setCurFile(curFile); + // estimate remaining time based on past history + // FIXME: avoid directly access data member of FmFileOpsJob + if(Q_LIKELY(job_->percent > 0 && updateRemainingTime_)) { + gint64 remaining = elapsedTime() * ((double(100 - job_->percent) / job_->percent) / 1000); + dlg->setRemainingTime(remaining); + } + // this timeout slot is called every 0.5 second. + // by adding this flag, we can update remaining time every 1 second. + updateRemainingTime_ = !updateRemainingTime_; + } + else { + showDialog(); + } +} + +void FileOperation::showDialog() { + if(!dlg) { + dlg = new FileOperationDialog(this); + dlg->setSourceFiles(srcPaths); + + if(destPath) { + dlg->setDestPath(destPath); + } + + if(curFile.isEmpty()) { + dlg->setPrepared(); + dlg->setCurFile(curFile); + } + uiTimer->setInterval(500); // change the interval of the timer + // now the timer is used to update current file display + dlg->show(); + } +} + +gint FileOperation::onFileOpsJobAsk(FmFileOpsJob* /*job*/, const char* question, char* const* options, FileOperation* pThis) { + pThis->pauseElapsedTimer(); + pThis->showDialog(); + int ret = pThis->dlg->ask(QString::fromUtf8(question), options); + pThis->resumeElapsedTimer(); + return ret; +} + +gint FileOperation::onFileOpsJobAskRename(FmFileOpsJob* /*job*/, FmFileInfo* src, FmFileInfo* dest, char** new_name, FileOperation* pThis) { + pThis->pauseElapsedTimer(); + pThis->showDialog(); + QString newName; + int ret = pThis->dlg->askRename(src, dest, newName); + if(!newName.isEmpty()) { + *new_name = g_strdup(newName.toUtf8().constData()); + } + pThis->resumeElapsedTimer(); + return ret; +} + +void FileOperation::onFileOpsJobCancelled(FmFileOpsJob* /*job*/, FileOperation* /*pThis*/) { + qDebug("file operation is cancelled!"); +} + +void FileOperation::onFileOpsJobCurFile(FmFileOpsJob* /*job*/, const char* cur_file, FileOperation* pThis) { + pThis->curFile = QString::fromUtf8(cur_file); + + // We update the current file name in a timeout slot because drawing a string + // in the UI is expansive. Updating the label text too often cause + // significant impact on performance. + // if(pThis->dlg) + // pThis->dlg->setCurFile(pThis->curFile); +} + +FmJobErrorAction FileOperation::onFileOpsJobError(FmFileOpsJob* /*job*/, GError* err, FmJobErrorSeverity severity, FileOperation* pThis) { + pThis->pauseElapsedTimer(); + pThis->showDialog(); + FmJobErrorAction act = pThis->dlg->error(err, severity); + pThis->resumeElapsedTimer(); + return act; +} + +void FileOperation::onFileOpsJobFinished(FmFileOpsJob* /*job*/, FileOperation* pThis) { + pThis->handleFinish(); +} + +void FileOperation::onFileOpsJobPercent(FmFileOpsJob* job, guint percent, FileOperation* pThis) { + if(pThis->dlg) { + pThis->dlg->setPercent(percent); + pThis->dlg->setDataTransferred(job->finished, job->total); + } +} + +void FileOperation::onFileOpsJobPrepared(FmFileOpsJob* /*job*/, FileOperation* pThis) { + if(!pThis->elapsedTimer_) { + pThis->elapsedTimer_ = new QElapsedTimer(); + pThis->elapsedTimer_->start(); + } + if(pThis->dlg) { + pThis->dlg->setPrepared(); + } +} + +void FileOperation::handleFinish() { + disconnectJob(); + + if(uiTimer) { + uiTimer->stop(); + delete uiTimer; + uiTimer = nullptr; + } + + if(dlg) { + dlg->done(QDialog::Accepted); + delete dlg; + dlg = nullptr; + } + Q_EMIT finished(); + + /* sepcial handling for trash + * FIXME: need to refactor this to use a more elegant way later. */ + if(job_->type == FM_FILE_OP_TRASH) { /* FIXME: direct access to job struct! */ + auto unable_to_trash = static_cast(g_object_get_data(G_OBJECT(job_), "trash-unsupported")); + /* some files cannot be trashed because underlying filesystems don't support it. */ + if(unable_to_trash) { /* delete them instead */ + Fm::FilePathList filesToDel; + for(GList* l = fm_path_list_peek_head_link(unable_to_trash); l; l = l->next) { + filesToDel.push_back(Fm::FilePath{fm_path_to_gfile(FM_PATH(l->data)), false}); + } + /* FIXME: parent window might be already destroyed! */ + QWidget* parent = nullptr; // FIXME: currently, parent window is not set + if(QMessageBox::question(parent, tr("Error"), + tr("Some files cannot be moved to trash can because " + "the underlying file systems don't support this operation.\n" + "Do you want to delete them instead?")) == QMessageBox::Yes) { + deleteFiles(std::move(filesToDel), false); + } + } + } + g_object_unref(job_); + job_ = nullptr; + + if(autoDestroy_) { + delete this; + } +} + +// static +FileOperation* FileOperation::copyFiles(Fm::FilePathList srcFiles, Fm::FilePath dest, QWidget* parent) { + FileOperation* op = new FileOperation(FileOperation::Copy, std::move(srcFiles), parent); + op->setDestination(dest); + op->run(); + return op; +} + +// static +FileOperation* FileOperation::moveFiles(Fm::FilePathList srcFiles, Fm::FilePath dest, QWidget* parent) { + FileOperation* op = new FileOperation(FileOperation::Move, std::move(srcFiles), parent); + op->setDestination(dest); + op->run(); + return op; +} + +//static +FileOperation* FileOperation::symlinkFiles(Fm::FilePathList srcFiles, Fm::FilePath dest, QWidget* parent) { + FileOperation* op = new FileOperation(FileOperation::Link, std::move(srcFiles), parent); + op->setDestination(dest); + op->run(); + return op; +} + +//static +FileOperation* FileOperation::deleteFiles(Fm::FilePathList srcFiles, bool prompt, QWidget* parent) { + if(prompt) { + int result = QMessageBox::warning(parent, tr("Confirm"), + tr("Do you want to delete the selected files?"), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No); + if(result != QMessageBox::Yes) { + return nullptr; + } + } + + FileOperation* op = new FileOperation(FileOperation::Delete, std::move(srcFiles)); + op->run(); + return op; +} + +//static +FileOperation* FileOperation::trashFiles(Fm::FilePathList srcFiles, bool prompt, QWidget* parent) { + if(prompt) { + int result = QMessageBox::warning(parent, tr("Confirm"), + tr("Do you want to move the selected files to trash can?"), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No); + if(result != QMessageBox::Yes) { + return nullptr; + } + } + + FileOperation* op = new FileOperation(FileOperation::Trash, std::move(srcFiles)); + op->run(); + return op; +} + +//static +FileOperation* FileOperation::unTrashFiles(Fm::FilePathList srcFiles, QWidget* parent) { + FileOperation* op = new FileOperation(FileOperation::UnTrash, std::move(srcFiles), parent); + op->run(); + return op; +} + +// static +FileOperation* FileOperation::changeAttrFiles(Fm::FilePathList srcFiles, QWidget* parent) { + //TODO + FileOperation* op = new FileOperation(FileOperation::ChangeAttr, std::move(srcFiles), parent); + op->run(); + return op; +} + + +} // namespace Fm diff --git a/src/fileoperation.h b/src/fileoperation.h new file mode 100644 index 0000000..33f7fea --- /dev/null +++ b/src/fileoperation.h @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_FILEOPERATION_H +#define FM_FILEOPERATION_H + +#include "libfmqtglobals.h" +#include +#include +#include +#include "core/filepath.h" + +class QTimer; + +namespace Fm { + +class FileOperationDialog; + +class LIBFM_QT_API FileOperation : public QObject { + Q_OBJECT +public: + enum Type { + Copy = FM_FILE_OP_COPY, + Move = FM_FILE_OP_MOVE, + Link = FM_FILE_OP_LINK, + Delete = FM_FILE_OP_DELETE, + Trash = FM_FILE_OP_TRASH, + UnTrash = FM_FILE_OP_UNTRASH, + ChangeAttr = FM_FILE_OP_CHANGE_ATTR + }; + +public: + explicit FileOperation(Type type, Fm::FilePathList srcFiles, QObject* parent = 0); + virtual ~FileOperation(); + + void setDestination(Fm::FilePath dest); + + void setChmod(mode_t newMode, mode_t newModeMask) { + fm_file_ops_job_set_chmod(job_, newMode, newModeMask); + } + + void setChown(gint uid, gint gid) { + fm_file_ops_job_set_chown(job_, uid, gid); + } + + // This only work for change attr jobs. + void setRecursiveChattr(bool recursive) { + fm_file_ops_job_set_recursive(job_, (gboolean)recursive); + } + + bool run(); + + void cancel() { + if(job_) { + fm_job_cancel(FM_JOB(job_)); + } + } + + bool isRunning() const { + return job_ ? fm_job_is_running(FM_JOB(job_)) : false; + } + + bool isCancelled() const { + return job_ ? fm_job_is_cancelled(FM_JOB(job_)) : false; + } + + FmFileOpsJob* job() { + return job_; + } + + bool autoDestroy() { + return autoDestroy_; + } + void setAutoDestroy(bool destroy = true) { + autoDestroy_ = destroy; + } + + Type type() { + return (Type)job_->type; + } + + // convinient static functions + static FileOperation* copyFiles(Fm::FilePathList srcFiles, Fm::FilePath dest, QWidget* parent = 0); + static FileOperation* moveFiles(Fm::FilePathList srcFiles, Fm::FilePath dest, QWidget* parent = 0); + static FileOperation* symlinkFiles(Fm::FilePathList srcFiles, Fm::FilePath dest, QWidget* parent = 0); + static FileOperation* deleteFiles(Fm::FilePathList srcFiles, bool promp = true, QWidget* parent = 0); + static FileOperation* trashFiles(Fm::FilePathList srcFiles, bool promp = true, QWidget* parent = 0); + static FileOperation* unTrashFiles(Fm::FilePathList srcFiles, QWidget* parent = 0); + static FileOperation* changeAttrFiles(Fm::FilePathList srcFiles, QWidget* parent = 0); + +Q_SIGNALS: + void finished(); + +private: + static gint onFileOpsJobAsk(FmFileOpsJob* job, const char* question, char* const* options, FileOperation* pThis); + static gint onFileOpsJobAskRename(FmFileOpsJob* job, FmFileInfo* src, FmFileInfo* dest, char** new_name, FileOperation* pThis); + static FmJobErrorAction onFileOpsJobError(FmFileOpsJob* job, GError* err, FmJobErrorSeverity severity, FileOperation* pThis); + static void onFileOpsJobPrepared(FmFileOpsJob* job, FileOperation* pThis); + static void onFileOpsJobCurFile(FmFileOpsJob* job, const char* cur_file, FileOperation* pThis); + static void onFileOpsJobPercent(FmFileOpsJob* job, guint percent, FileOperation* pThis); + static void onFileOpsJobFinished(FmFileOpsJob* job, FileOperation* pThis); + static void onFileOpsJobCancelled(FmFileOpsJob* job, FileOperation* pThis); + + void handleFinish(); + void disconnectJob(); + void showDialog(); + + void pauseElapsedTimer() { + if(Q_LIKELY(elapsedTimer_ != nullptr)) { + lastElapsed_ += elapsedTimer_->elapsed(); + elapsedTimer_->invalidate(); + } + } + + void resumeElapsedTimer() { + if(Q_LIKELY(elapsedTimer_ != nullptr)) { + elapsedTimer_->start(); + } + } + + qint64 elapsedTime() { + if(Q_LIKELY(elapsedTimer_ != nullptr)) { + return lastElapsed_ + elapsedTimer_->elapsed(); + } + return 0; + } + +private Q_SLOTS: + void onUiTimeout(); + +private: + FmFileOpsJob* job_; + FileOperationDialog* dlg; + Fm::FilePath destPath; + Fm::FilePathList srcPaths; + QTimer* uiTimer; + QElapsedTimer* elapsedTimer_; + qint64 lastElapsed_; + bool updateRemainingTime_; + QString curFile; + bool autoDestroy_; +}; + +} + +#endif // FM_FILEOPERATION_H diff --git a/src/fileoperationdialog.cpp b/src/fileoperationdialog.cpp new file mode 100644 index 0000000..0b26fcd --- /dev/null +++ b/src/fileoperationdialog.cpp @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "fileoperationdialog.h" +#include "fileoperation.h" +#include "renamedialog.h" +#include +#include +#include +#include "utilities.h" +#include "ui_file-operation-dialog.h" + +namespace Fm { + +FileOperationDialog::FileOperationDialog(FileOperation* _operation): + QDialog(nullptr), + operation(_operation), + defaultOption(-1), + ignoreNonCriticalErrors_(false) { + + ui = new Ui::FileOperationDialog(); + ui->setupUi(this); + + QString title; + QString message; + switch(_operation->type()) { + case FM_FILE_OP_MOVE: + title = tr("Move files"); + message = tr("Moving the following files to destination folder:"); + break; + case FM_FILE_OP_COPY: + title = tr("Copy Files"); + message = tr("Copying the following files to destination folder:"); + break; + case FM_FILE_OP_TRASH: + title = tr("Trash Files"); + message = tr("Moving the following files to trash can:"); + break; + case FM_FILE_OP_DELETE: + title = tr("Delete Files"); + message = tr("Deleting the following files:"); + ui->dest->hide(); + ui->destLabel->hide(); + break; + case FM_FILE_OP_LINK: + title = tr("Create Symlinks"); + message = tr("Creating symlinks for the following files:"); + break; + case FM_FILE_OP_CHANGE_ATTR: + title = tr("Change Attributes"); + message = tr("Changing attributes of the following files:"); + ui->dest->hide(); + ui->destLabel->hide(); + break; + case FM_FILE_OP_UNTRASH: + title = tr("Restore Trashed Files"); + message = tr("Restoring the following files from trash can:"); + ui->dest->hide(); + ui->destLabel->hide(); + break; + } + ui->message->setText(message); + setWindowTitle(title); +} + + +FileOperationDialog::~FileOperationDialog() { + delete ui; +} + +void FileOperationDialog::setDestPath(const Fm::FilePath &dest) { + ui->dest->setText(dest.displayName().get()); +} + +void FileOperationDialog::setSourceFiles(const Fm::FilePathList &srcFiles) { + for(auto& srcFile : srcFiles) { + ui->sourceFiles->addItem(srcFile.displayName().get()); + } +} + +int FileOperationDialog::ask(QString /*question*/, char* const* /*options*/) { + // TODO: implement FileOperationDialog::ask() + return 0; +} + +int FileOperationDialog::askRename(FmFileInfo* src, FmFileInfo* dest, QString& new_name) { + int ret; + if(defaultOption == -1) { // default action is not set, ask the user + RenameDialog dlg(src, dest, this); + dlg.exec(); + switch(dlg.action()) { + case RenameDialog::ActionOverwrite: + ret = FM_FILE_OP_OVERWRITE; + if(dlg.applyToAll()) { + defaultOption = ret; + } + break; + case RenameDialog::ActionRename: + ret = FM_FILE_OP_RENAME; + new_name = dlg.newName(); + break; + case RenameDialog::ActionIgnore: + ret = FM_FILE_OP_SKIP; + if(dlg.applyToAll()) { + defaultOption = ret; + } + break; + default: + ret = FM_FILE_OP_CANCEL; + break; + } + } + else { + ret = defaultOption; + } + return ret; +} + +FmJobErrorAction FileOperationDialog::error(GError* err, FmJobErrorSeverity severity) { + if(severity >= FM_JOB_ERROR_MODERATE) { + if(severity == FM_JOB_ERROR_CRITICAL) { + QMessageBox::critical(this, tr("Error"), QString::fromUtf8(err->message)); + return FM_JOB_ABORT; + } + if (ignoreNonCriticalErrors_) { + return FM_JOB_CONTINUE; + } + QMessageBox::StandardButton stb = QMessageBox::critical(this, tr("Error"), QString::fromUtf8(err->message), + QMessageBox::Ok | QMessageBox::Ignore); + if (stb == QMessageBox::Ignore) { + ignoreNonCriticalErrors_ = true; + } + } + return FM_JOB_CONTINUE; +} + +void FileOperationDialog::setCurFile(QString cur_file) { + ui->curFile->setText(cur_file); +} + +void FileOperationDialog::setDataTransferred(uint64_t finishedSize, std::uint64_t totalSize) { + ui->dataTransferred->setText(QString("%1 / %2") + .arg(formatFileSize(finishedSize, fm_config->si_unit)) + .arg(formatFileSize(totalSize, fm_config->si_unit))); +} + +void FileOperationDialog::setPercent(unsigned int percent) { + ui->progressBar->setValue(percent); +} + +void FileOperationDialog::setRemainingTime(unsigned int sec) { + unsigned int min = 0; + unsigned int hr = 0; + if(sec > 60) { + min = sec / 60; + sec %= 60; + if(min > 60) { + hr = min / 60; + min %= 60; + } + } + ui->timeRemaining->setText(QString("%1:%2:%3") + .arg(hr, 2, 10, QChar('0')) + .arg(min, 2, 10, QChar('0')) + .arg(sec, 2, 10, QChar('0'))); +} + +void FileOperationDialog::setPrepared() { +} + +void FileOperationDialog::reject() { + operation->cancel(); + QDialog::reject(); +} + + +} // namespace Fm diff --git a/src/fileoperationdialog.h b/src/fileoperationdialog.h new file mode 100644 index 0000000..8f21493 --- /dev/null +++ b/src/fileoperationdialog.h @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_FILEOPERATIONDIALOG_H +#define FM_FILEOPERATIONDIALOG_H + +#include "libfmqtglobals.h" +#include +#include +#include +#include "core/filepath.h" +#include "core/fileinfo.h" + +namespace Ui { +class FileOperationDialog; +} + +namespace Fm { + +class FileOperation; + +class LIBFM_QT_API FileOperationDialog : public QDialog { + Q_OBJECT +public: + explicit FileOperationDialog(FileOperation* _operation); + virtual ~FileOperationDialog(); + + void setSourceFiles(const Fm::FilePathList& srcFiles); + void setDestPath(const Fm::FilePath& dest); + + int ask(QString question, char* const* options); + int askRename(FmFileInfo* src, FmFileInfo* dest, QString& new_name); + FmJobErrorAction error(GError* err, FmJobErrorSeverity severity); + void setPrepared(); + void setCurFile(QString cur_file); + void setPercent(unsigned int percent); + void setDataTransferred(std::uint64_t finishedSize, std::uint64_t totalSize); + void setRemainingTime(unsigned int sec); + + virtual void reject(); + +private: + Ui::FileOperationDialog* ui; + FileOperation* operation; + int defaultOption; + bool ignoreNonCriticalErrors_; +}; + +} + +#endif // FM_FILEOPERATIONDIALOG_H diff --git a/src/filepropsdialog.cpp b/src/filepropsdialog.cpp new file mode 100644 index 0000000..858aadf --- /dev/null +++ b/src/filepropsdialog.cpp @@ -0,0 +1,560 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "filepropsdialog.h" +#include "ui_file-props.h" +#include "icontheme.h" +#include "utilities.h" +#include "fileoperation.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "core/totalsizejob.h" +#include "core/folder.h" + +#define DIFFERENT_UIDS ((uid)-1) +#define DIFFERENT_GIDS ((gid)-1) +#define DIFFERENT_PERMS ((mode_t)-1) + +namespace Fm { + +enum { + ACCESS_NO_CHANGE = 0, + ACCESS_READ_ONLY, + ACCESS_READ_WRITE, + ACCESS_FORBID +}; + +FilePropsDialog::FilePropsDialog(Fm::FileInfoList files, QWidget* parent, Qt::WindowFlags f): + QDialog(parent, f), + fileInfos_{std::move(files)}, + fileInfo{fileInfos_.front()}, + singleType(fileInfos_.isSameType()), + singleFile(fileInfos_.size() == 1 ? true : false) { + + setAttribute(Qt::WA_DeleteOnClose); + + ui = new Ui::FilePropsDialog(); + ui->setupUi(this); + + if(singleType) { + mimeType = fileInfo->mimeType(); + } + + totalSizeJob = new Fm::TotalSizeJob(fileInfos_.paths(), Fm::TotalSizeJob::DEFAULT); + + initGeneralPage(); + initPermissionsPage(); +} + +FilePropsDialog::~FilePropsDialog() { + // Stop the timer if it's still running + if(fileSizeTimer) { + fileSizeTimer->stop(); + delete fileSizeTimer; + fileSizeTimer = nullptr; + } + + // Cancel the indexing job if it hasn't finished + if(totalSizeJob) { + totalSizeJob->cancel(); + totalSizeJob = nullptr; + } + + // And finally delete the dialog's UI + delete ui; +} + +void FilePropsDialog::initApplications() { + if(singleType && mimeType && !fileInfo->isDir()) { + ui->openWith->setMimeType(mimeType); + } + else { + ui->openWith->hide(); + ui->openWithLabel->hide(); + } +} + +void FilePropsDialog::initPermissionsPage() { + // ownership handling + // get owner/group and mode of the first file in the list + uid = fileInfo->uid(); + gid = fileInfo->gid(); + mode_t mode = fileInfo->mode(); + ownerPerm = (mode & (S_IRUSR | S_IWUSR | S_IXUSR)); + groupPerm = (mode & (S_IRGRP | S_IWGRP | S_IXGRP)); + otherPerm = (mode & (S_IROTH | S_IWOTH | S_IXOTH)); + execPerm = (mode & (S_IXUSR | S_IXGRP | S_IXOTH)); + allNative = fileInfo->isNative(); + hasDir = S_ISDIR(mode); + + // check if all selected files belongs to the same owner/group or have the same mode + // at the same time, check if all files are on native unix filesystems + for(auto& fi: fileInfos_) { + if(allNative && !fi->isNative()) { + allNative = false; // not all of the files are native + } + + mode_t fi_mode = fi->mode(); + if(S_ISDIR(fi_mode)) { + hasDir = true; // the files list contains dir(s) + } + + if(uid != DIFFERENT_UIDS && static_cast(uid) != fi->uid()) { + uid = DIFFERENT_UIDS; // not all files have the same owner + } + if(gid != DIFFERENT_GIDS && static_cast(gid) != fi->gid()) { + gid = DIFFERENT_GIDS; // not all files have the same owner group + } + + if(ownerPerm != DIFFERENT_PERMS && ownerPerm != (fi_mode & (S_IRUSR | S_IWUSR | S_IXUSR))) { + ownerPerm = DIFFERENT_PERMS; // not all files have the same permission for owner + } + if(groupPerm != DIFFERENT_PERMS && groupPerm != (fi_mode & (S_IRGRP | S_IWGRP | S_IXGRP))) { + groupPerm = DIFFERENT_PERMS; // not all files have the same permission for grop + } + if(otherPerm != DIFFERENT_PERMS && otherPerm != (fi_mode & (S_IROTH | S_IWOTH | S_IXOTH))) { + otherPerm = DIFFERENT_PERMS; // not all files have the same permission for other + } + if(execPerm != DIFFERENT_PERMS && execPerm != (fi_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) { + execPerm = DIFFERENT_PERMS; // not all files have the same executable permission + } + } + + // init owner/group + initOwner(); + + // if all files are of the same type, and some of them are dirs => all of the items are dirs + // rwx values have different meanings for dirs + // Let's make it clear for the users + // init combo boxes for file permissions here + QStringList comboItems; + comboItems.append("---"); // no change + if(singleType && hasDir) { // all files are dirs + comboItems.append(tr("View folder content")); + comboItems.append(tr("View and modify folder content")); + ui->executable->hide(); + } + else { //not all of the files are dirs + comboItems.append(tr("Read")); + comboItems.append(tr("Read and write")); + } + comboItems.append(tr("Forbidden")); + QStringListModel* comboModel = new QStringListModel(comboItems, this); + ui->ownerPerm->setModel(comboModel); + ui->groupPerm->setModel(comboModel); + ui->otherPerm->setModel(comboModel); + + // owner + ownerPermSel = ACCESS_NO_CHANGE; + if(ownerPerm != DIFFERENT_PERMS) { // permissions for owner are the same among all files + if(ownerPerm & S_IRUSR) { // can read + if(ownerPerm & S_IWUSR) { // can write + ownerPermSel = ACCESS_READ_WRITE; + } + else { + ownerPermSel = ACCESS_READ_ONLY; + } + } + else { + if((ownerPerm & S_IWUSR) == 0) { // cannot read or write + ownerPermSel = ACCESS_FORBID; + } + } + } + ui->ownerPerm->setCurrentIndex(ownerPermSel); + + // owner and group + groupPermSel = ACCESS_NO_CHANGE; + if(groupPerm != DIFFERENT_PERMS) { // permissions for owner are the same among all files + if(groupPerm & S_IRGRP) { // can read + if(groupPerm & S_IWGRP) { // can write + groupPermSel = ACCESS_READ_WRITE; + } + else { + groupPermSel = ACCESS_READ_ONLY; + } + } + else { + if((groupPerm & S_IWGRP) == 0) { // cannot read or write + groupPermSel = ACCESS_FORBID; + } + } + } + ui->groupPerm->setCurrentIndex(groupPermSel); + + // other + otherPermSel = ACCESS_NO_CHANGE; + if(otherPerm != DIFFERENT_PERMS) { // permissions for owner are the same among all files + if(otherPerm & S_IROTH) { // can read + if(otherPerm & S_IWOTH) { // can write + otherPermSel = ACCESS_READ_WRITE; + } + else { + otherPermSel = ACCESS_READ_ONLY; + } + } + else { + if((otherPerm & S_IWOTH) == 0) { // cannot read or write + otherPermSel = ACCESS_FORBID; + } + } + + } + ui->otherPerm->setCurrentIndex(otherPermSel); + + // set the checkbox to partially checked state + // when owner, group, and other have different executable flags set. + // some of them have exec, and others do not have. + execCheckState = Qt::PartiallyChecked; + if(execPerm != DIFFERENT_PERMS) { // if all files have the same executable permission + // check if the files are all executable + if((mode & (S_IXUSR | S_IXGRP | S_IXOTH)) == (S_IXUSR | S_IXGRP | S_IXOTH)) { + // owner, group, and other all have exec permission. + ui->executable->setTristate(false); + execCheckState = Qt::Checked; + } + else if((mode & (S_IXUSR | S_IXGRP | S_IXOTH)) == 0) { + // owner, group, and other all have no exec permission + ui->executable->setTristate(false); + execCheckState = Qt::Unchecked; + } + } + ui->executable->setCheckState(execCheckState); +} + +void FilePropsDialog::initGeneralPage() { + // update UI + if(singleType) { // all files are of the same mime-type + std::shared_ptr icon; + // FIXME: handle custom icons for some files + // FIXME: display special property pages for special files or + // some specified mime-types. + if(singleFile) { // only one file is selected. + icon = fileInfo->icon(); + } + if(mimeType) { + if(!icon) { // get an icon from mime type if needed + icon = mimeType->icon(); + } + ui->fileType->setText(mimeType->desc()); + ui->mimeType->setText(mimeType->name()); + } + if(icon) { + ui->iconButton->setIcon(icon->qicon()); + } + + if(singleFile && fileInfo->isSymlink()) { + ui->target->setText(QString::fromStdString(fileInfo->target())); + } + else { + ui->target->hide(); + ui->targetLabel->hide(); + } + if(fileInfo->isDir() && fileInfo->isNative()) { // all files are native dirs + connect(ui->iconButton, &QAbstractButton::clicked, this, &FilePropsDialog::onIconButtonclicked); + } + } // end if(singleType) + else { // not singleType, multiple files are selected at the same time + ui->fileType->setText(tr("Files of different types")); + ui->target->hide(); + ui->targetLabel->hide(); + } + + // FIXME: check if all files has the same parent dir, mtime, or atime + if(singleFile) { // only one file is selected + auto parent_path = fileInfo->path().parent(); + auto parent_str = parent_path ? parent_path.displayName(): nullptr; + + ui->fileName->setText(fileInfo->displayName()); + if(parent_str) { + ui->location->setText(parent_str.get()); + } + else { + ui->location->clear(); + } + auto mtime = QDateTime::fromMSecsSinceEpoch(fileInfo->mtime() * 1000); + ui->lastModified->setText(mtime.toString(Qt::SystemLocaleShortDate)); + auto atime = QDateTime::fromMSecsSinceEpoch(fileInfo->atime() * 1000); + ui->lastAccessed->setText(atime.toString(Qt::SystemLocaleShortDate)); + } + else { + ui->fileName->setText(tr("Multiple Files")); + ui->fileName->setEnabled(false); + } + + initApplications(); // init applications combo box + + // calculate total file sizes + fileSizeTimer = new QTimer(this); + connect(fileSizeTimer, &QTimer::timeout, this, &FilePropsDialog::onFileSizeTimerTimeout); + fileSizeTimer->start(600); + + connect(totalSizeJob, &Fm::TotalSizeJob::finished, this, &FilePropsDialog::onDeepCountJobFinished, Qt::BlockingQueuedConnection); + totalSizeJob->setAutoDelete(true); + totalSizeJob->runAsync(); +} + +void FilePropsDialog::onDeepCountJobFinished() { + onFileSizeTimerTimeout(); // update file size display + + totalSizeJob = nullptr; + + // stop the timer + if(fileSizeTimer) { + fileSizeTimer->stop(); + delete fileSizeTimer; + fileSizeTimer = nullptr; + } +} + +void FilePropsDialog::onFileSizeTimerTimeout() { + if(totalSizeJob && !totalSizeJob->isCancelled()) { + // FIXME: + // OMG! It's really unbelievable that Qt developers only implement + // QObject::tr(... int n). GNU gettext developers are smarter and + // they use unsigned long instead of int. + // We cannot use Qt here to handle plural forms. So sad. :-( + QString str = Fm::formatFileSize(totalSizeJob->totalSize(), fm_config->si_unit) % + QString(" (%1 B)").arg(totalSizeJob->totalSize()); + // tr(" (%n) byte(s)", "", deepCountJob->total_size); + ui->fileSize->setText(str); + + str = Fm::formatFileSize(totalSizeJob->totalOnDiskSize(), fm_config->si_unit) % + QString(" (%1 B)").arg(totalSizeJob->totalOnDiskSize()); + // tr(" (%n) byte(s)", "", deepCountJob->total_ondisk_size); + ui->onDiskSize->setText(str); + } +} + +void FilePropsDialog::onIconButtonclicked() { + QString iconDir; + QString iconThemeName = QIcon::themeName(); + QStringList icons = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, + "icons", + QStandardPaths::LocateDirectory); + for (QStringList::ConstIterator it = icons.constBegin(); it != icons.constEnd(); ++it) { + QString iconThemeFolder = *it + '/' + iconThemeName; + if (QDir(iconThemeFolder).exists() && QFileInfo(iconThemeFolder).permission(QFileDevice::ReadUser)) { + // give priority to the "places" folder + const QString places = iconThemeFolder + QLatin1String("/places"); + if (QDir(places).exists() && QFileInfo(places).permission(QFileDevice::ReadUser)) { + iconDir = places; + } + else { + iconDir = iconThemeFolder; + } + break; + } + } + if(iconDir.isEmpty()) { + iconDir = QStandardPaths::locate(QStandardPaths::GenericDataLocation, + "icons", + QStandardPaths::LocateDirectory); + if(iconDir.isEmpty()) { + return; + } + } + const QString iconPath = QFileDialog::getOpenFileName(this, tr("Select an icon"), + iconDir, + tr("Images (*.png *.xpm *.svg *.svgz )")); + if(!iconPath.isEmpty()) { + QStringList parts = iconPath.split("/", QString::SkipEmptyParts); + if(!parts.isEmpty()) { + QString iconName = parts.at(parts.count() - 1); + int ln = iconName.lastIndexOf("."); + if(ln > -1) { + iconName.remove(ln, iconName.length() - ln); + customIcon = QIcon::fromTheme(iconName); + ui->iconButton->setIcon(customIcon); + } + } + } +} + +void FilePropsDialog::accept() { + + // applications + if(mimeType && ui->openWith->isChanged()) { + auto currentApp = ui->openWith->selectedApp(); + g_app_info_set_as_default_for_type(currentApp.get(), mimeType->name(), nullptr); + } + + // check if chown or chmod is needed + gint32 newUid = uidFromName(ui->owner->text()); + gint32 newGid = gidFromName(ui->ownerGroup->text()); + bool needChown = (newUid != -1 && newUid != uid) || (newGid != -1 && newGid != gid); + + int newOwnerPermSel = ui->ownerPerm->currentIndex(); + int newGroupPermSel = ui->groupPerm->currentIndex(); + int newOtherPermSel = ui->otherPerm->currentIndex(); + Qt::CheckState newExecCheckState = ui->executable->checkState(); + bool needChmod = ((newOwnerPermSel != ownerPermSel) || + (newGroupPermSel != groupPermSel) || + (newOtherPermSel != otherPermSel) || + (newExecCheckState != execCheckState)); + + if(needChmod || needChown) { + FileOperation* op = new FileOperation(FileOperation::ChangeAttr, fileInfos_.paths()); + if(needChown) { + // don't do chown if new uid/gid and the original ones are actually the same. + if(newUid == uid) { + newUid = -1; + } + if(newGid == gid) { + newGid = -1; + } + op->setChown(newUid, newGid); + } + if(needChmod) { + mode_t newMode = 0; + mode_t newModeMask = 0; + // FIXME: we need to make sure that folders with "r" permission also have "x" + // at the same time. Otherwise, it's not able to browse the folder later. + if(newOwnerPermSel != ownerPermSel && newOwnerPermSel != ACCESS_NO_CHANGE) { + // owner permission changed + newModeMask |= (S_IRUSR | S_IWUSR); // affect user bits + if(newOwnerPermSel == ACCESS_READ_ONLY) { + newMode |= S_IRUSR; + } + else if(newOwnerPermSel == ACCESS_READ_WRITE) { + newMode |= (S_IRUSR | S_IWUSR); + } + } + if(newGroupPermSel != groupPermSel && newGroupPermSel != ACCESS_NO_CHANGE) { + qDebug("newGroupPermSel: %d", newGroupPermSel); + // group permission changed + newModeMask |= (S_IRGRP | S_IWGRP); // affect group bits + if(newGroupPermSel == ACCESS_READ_ONLY) { + newMode |= S_IRGRP; + } + else if(newGroupPermSel == ACCESS_READ_WRITE) { + newMode |= (S_IRGRP | S_IWGRP); + } + } + if(newOtherPermSel != otherPermSel && newOtherPermSel != ACCESS_NO_CHANGE) { + // other permission changed + newModeMask |= (S_IROTH | S_IWOTH); // affect other bits + if(newOtherPermSel == ACCESS_READ_ONLY) { + newMode |= S_IROTH; + } + else if(newOtherPermSel == ACCESS_READ_WRITE) { + newMode |= (S_IROTH | S_IWOTH); + } + } + if(newExecCheckState != execCheckState && newExecCheckState != Qt::PartiallyChecked) { + // executable state changed + newModeMask |= (S_IXUSR | S_IXGRP | S_IXOTH); + if(newExecCheckState == Qt::Checked) { + newMode |= (S_IXUSR | S_IXGRP | S_IXOTH); + } + } + op->setChmod(newMode, newModeMask); + + if(hasDir) { // if there are some dirs in our selected files + QMessageBox::StandardButton r = QMessageBox::question(this, + tr("Apply changes"), + tr("Do you want to recursively apply these changes to all files and sub-folders?"), + QMessageBox::Yes | QMessageBox::No); + if(r == QMessageBox::Yes) { + op->setRecursiveChattr(true); + } + } + } + op->setAutoDestroy(true); + op->run(); + } + + // Renaming + if(singleFile) { + QString new_name = ui->fileName->text(); + if(fileInfo->displayName() != new_name) { + auto path = fileInfo->path(); + auto parent_path = path.parent(); + auto dest = parent_path.child(new_name.toLocal8Bit().constData()); + Fm::GErrorPtr err; + if(!g_file_move(path.gfile().get(), dest.gfile().get(), + GFileCopyFlags(G_FILE_COPY_ALL_METADATA | + G_FILE_COPY_NO_FALLBACK_FOR_MOVE | + G_FILE_COPY_NOFOLLOW_SYMLINKS), + nullptr, nullptr, nullptr, &err)) { + QMessageBox::critical(this, QObject::tr("Error"), err.message()); + } + } + } + + // Custom (folder) icon + if(!customIcon.isNull()) { + bool reloadNeeded(false); + QString iconNamne = customIcon.name(); + for(auto& fi: fileInfos_) { + std::shared_ptr icon = fi->icon(); + if (!fi->icon() || fi->icon()->qicon().name() != iconNamne) { + auto dot_dir = CStrPtr{g_build_filename(fi->path().localPath().get(), ".directory", nullptr)}; + GKeyFile* kf = g_key_file_new(); + g_key_file_set_string(kf, "Desktop Entry", "Icon", iconNamne.toLocal8Bit().constData()); + Fm::GErrorPtr err; + if (!g_key_file_save_to_file(kf, dot_dir.get(), &err)) { + QMessageBox::critical(this, QObject::tr("Custom Icon Error"), err.message()); + } + else { + reloadNeeded = true; + } + g_key_file_free(kf); + } + } + if(reloadNeeded) { + // since there can be only one parent dir, only one reload is needed + auto parent = fileInfo->path().parent(); + if(parent.isValid()) { + auto folder = Fm::Folder::fromPath(parent); + if(folder->isLoaded()) { + folder->reload(); + } + } + } + } + + QDialog::accept(); +} + +void FilePropsDialog::initOwner() { + if(allNative) { + if(uid != DIFFERENT_UIDS) { + ui->owner->setText(uidToName(uid)); + } + if(gid != DIFFERENT_GIDS) { + ui->ownerGroup->setText(gidToName(gid)); + } + + if(geteuid() != 0) { // on local filesystems, only root can do chown. + ui->owner->setEnabled(false); + ui->ownerGroup->setEnabled(false); + } + } +} + + +} // namespace Fm diff --git a/src/filepropsdialog.h b/src/filepropsdialog.h new file mode 100644 index 0000000..d155c24 --- /dev/null +++ b/src/filepropsdialog.h @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_FILEPROPSDIALOG_H +#define FM_FILEPROPSDIALOG_H + +#include "libfmqtglobals.h" +#include +#include +#include + +#include "core/fileinfo.h" +#include "core/totalsizejob.h" + +namespace Ui { +class FilePropsDialog; +} + +namespace Fm { + +class LIBFM_QT_API FilePropsDialog : public QDialog { + Q_OBJECT + +public: + explicit FilePropsDialog(Fm::FileInfoList files, QWidget* parent = 0, Qt::WindowFlags f = 0); + virtual ~FilePropsDialog(); + + virtual void accept(); + + static FilePropsDialog* showForFile(std::shared_ptr file, QWidget* parent = 0) { + Fm::FileInfoList files; + files.push_back(std::move(file)); + FilePropsDialog* dlg = showForFiles(files, parent); + return dlg; + } + + static FilePropsDialog* showForFiles(Fm::FileInfoList files, QWidget* parent = 0) { + FilePropsDialog* dlg = new FilePropsDialog(std::move(files), parent); + dlg->show(); + return dlg; + } + +private: + void initGeneralPage(); + void initApplications(); + void initPermissionsPage(); + void initOwner(); + +private Q_SLOTS: + void onDeepCountJobFinished(); + void onFileSizeTimerTimeout(); + void onIconButtonclicked(); + +private: + Ui::FilePropsDialog* ui; + Fm::FileInfoList fileInfos_; // list of all file infos + std::shared_ptr fileInfo; // file info of the first file in the list + bool singleType; // all files are of the same type? + bool singleFile; // only one file is selected? + bool hasDir; // is there any dir in the files? + bool allNative; // all files are on native UNIX filesystems (not virtual or remote) + QIcon customIcon; // custom (folder) icon (wiil be checked for its nullity) + + std::shared_ptr mimeType; // mime type of the files + + gint32 uid; // owner uid of the files, -1 means all files do not have the same uid + gint32 gid; // owner gid of the files, -1 means all files do not have the same uid + mode_t ownerPerm; // read permission of the files, -1 means not all files have the same value + int ownerPermSel; + mode_t groupPerm; // read permission of the files, -1 means not all files have the same value + int groupPermSel; + mode_t otherPerm; // read permission of the files, -1 means not all files have the same value + int otherPermSel; + mode_t execPerm; // exec permission of the files + Qt::CheckState execCheckState; + + Fm::TotalSizeJob* totalSizeJob; // job used to count total size + QTimer* fileSizeTimer; +}; + +} + +#endif // FM_FILEPROPSDIALOG_H diff --git a/src/filesearch.ui b/src/filesearch.ui new file mode 100644 index 0000000..d8c0e1a --- /dev/null +++ b/src/filesearch.ui @@ -0,0 +1,574 @@ + + + SearchDialog + + + + 0 + 0 + 512 + 420 + + + + Search Files + + + + + + + + + + + 0 + + + + Name/Location + + + + + + File Name Patterns: + + + + + + * + + + + + + + Case insensitive + + + + + + + Use regular expression + + + true + + + + + + + + + + Places to Search: + + + + + + + + + + + + + &Add + + + + + + + + + + + + &Remove + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + Search in sub directories + + + + + + + Search for hidden files + + + + + + + + + + + File Type + + + + + + Only search for files of following types: + + + + + + Text files + + + + + + + Image files + + + + + + + Audio files + + + + + + + Video files + + + + + + + Documents + + + + + + + Folders + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Content + + + + + + File contains: + + + + + + + + + Case insensiti&ve + + + + + + + &Use regular expression + + + true + + + + + + + + + + Qt::Vertical + + + + 20 + 186 + + + + + + + + + Properties + + + + + + File Size: + + + + + + + + Smaller than: + + + + + + + false + + + + + + + false + + + + + + + Larger than: + + + + + + + false + + + 2 + + + + Bytes + + + + + KiB + + + + + MiB + + + + + GiB + + + + + + + + false + + + 2 + + + + Bytes + + + + + KiB + + + + + MiB + + + + + GiB + + + + + + + + + + + + + Qt::LeftToRight + + + Last Modified Time: + + + + + + + + Earlier than: + + + + + + + Later than: + + + + + + + false + + + true + + + + + + + false + + + true + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + SearchDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + SearchDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + largerThan + toggled(bool) + minSizeUnit + setEnabled(bool) + + + 93 + 84 + + + 403 + 88 + + + + + smallerThan + toggled(bool) + maxSize + setEnabled(bool) + + + 96 + 119 + + + 241 + 123 + + + + + largerThan + toggled(bool) + minSize + setEnabled(bool) + + + 93 + 84 + + + 241 + 88 + + + + + smallerThan + toggled(bool) + maxSizeUnit + setEnabled(bool) + + + 96 + 119 + + + 403 + 123 + + + + + laterThan + toggled(bool) + minTime + setEnabled(bool) + + + 88 + 223 + + + 319 + 226 + + + + + earlierThan + toggled(bool) + maxTime + setEnabled(bool) + + + 93 + 190 + + + 319 + 193 + + + + + diff --git a/src/filesearchdialog.cpp b/src/filesearchdialog.cpp new file mode 100644 index 0000000..9dbec7c --- /dev/null +++ b/src/filesearchdialog.cpp @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "filesearchdialog.h" +#include +#include "fm-search.h" +#include "ui_filesearch.h" +#include +#include +#include + +namespace Fm { + +FileSearchDialog::FileSearchDialog(QStringList paths, QWidget* parent, Qt::WindowFlags f): + QDialog(parent, f), + ui(new Ui::SearchDialog()) { + ui->setupUi(this); + ui->minSize->setMaximum(std::numeric_limits().max()); + ui->maxSize->setMaximum(std::numeric_limits().max()); + Q_FOREACH(const QString& path, paths) { + ui->listView->addItem(path); + } + + ui->maxTime->setDate(QDate::currentDate()); + ui->minTime->setDate(QDate::currentDate()); + + connect(ui->addPath, &QPushButton::clicked, this, &FileSearchDialog::onAddPath); + connect(ui->removePath, &QPushButton::clicked, this, &FileSearchDialog::onRemovePath); + + ui->namePatterns->setFocus(); +} + +FileSearchDialog::~FileSearchDialog() { + delete ui; +} + +void FileSearchDialog::accept() { + // build the search:/// uri + int n = ui->listView->count(); + if(n > 0) { + FmSearch* search = fm_search_new(); + for(int i = 0; i < n; ++i) { // add directories + QListWidgetItem* item = ui->listView->item(i); + fm_search_add_dir(search, item->text().toLocal8Bit().constData()); + } + + fm_search_set_recursive(search, ui->recursiveSearch->isChecked()); + fm_search_set_show_hidden(search, ui->searchHidden->isChecked()); + fm_search_set_name_patterns(search, ui->namePatterns->text().toUtf8().constData()); + fm_search_set_name_ci(search, ui->nameCaseInsensitive->isChecked()); + fm_search_set_name_regex(search, ui->nameRegExp->isChecked()); + + fm_search_set_content_pattern(search, ui->contentPattern->text().toUtf8().constData()); + fm_search_set_content_ci(search, ui->contentCaseInsensitive->isChecked()); + fm_search_set_content_regex(search, ui->contentRegExp->isChecked()); + + // search for the files of specific mime-types + if(ui->searchTextFiles->isChecked()) { + fm_search_add_mime_type(search, "text/plain"); + } + if(ui->searchImages->isChecked()) { + fm_search_add_mime_type(search, "image/*"); + } + if(ui->searchAudio->isChecked()) { + fm_search_add_mime_type(search, "audio/*"); + } + if(ui->searchVideo->isChecked()) { + fm_search_add_mime_type(search, "video/*"); + } + if(ui->searchFolders->isChecked()) { + fm_search_add_mime_type(search, "inode/directory"); + } + if(ui->searchDocuments->isChecked()) { + const char* doc_types[] = { + "application/pdf", + /* "text/html;" */ + "application/vnd.oasis.opendocument.*", + "application/vnd.openxmlformats-officedocument.*", + "application/msword;application/vnd.ms-word", + "application/msexcel;application/vnd.ms-excel" + }; + for(unsigned int i = 0; i < sizeof(doc_types) / sizeof(char*); ++i) { + fm_search_add_mime_type(search, doc_types[i]); + } + } + + // search based on file size + const unsigned int unit_bytes[] = {1, (1024), (1024 * 1024), (1024 * 1024 * 1024)}; + if(ui->largerThan->isChecked()) { + guint64 size = ui->minSize->value() * unit_bytes[ui->minSizeUnit->currentIndex()]; + fm_search_set_min_size(search, size); + } + + if(ui->smallerThan->isChecked()) { + guint64 size = ui->maxSize->value() * unit_bytes[ui->maxSizeUnit->currentIndex()]; + fm_search_set_max_size(search, size); + } + + // search based on file mtime (we only support date in YYYY-MM-DD format) + if(ui->earlierThan->isChecked()) { + fm_search_set_max_mtime(search, ui->maxTime->date().toString(QStringLiteral("yyyy-MM-dd")).toUtf8().constData()); + } + if(ui->laterThan->isChecked()) { + fm_search_set_min_mtime(search, ui->minTime->date().toString(QStringLiteral("yyyy-MM-dd")).toUtf8().constData()); + } + + searchUri_ = Path::wrapPtr(fm_search_dup_path(search)); + + fm_search_free(search); + } + else { + QMessageBox::critical(this, tr("Error"), tr("You should add at least one directory to search.")); + return; + } + QDialog::accept(); +} + +void FileSearchDialog::onAddPath() { + QString dir = QFileDialog::getExistingDirectory(this, tr("Select a folder")); + if(dir.isEmpty()) { + return; + } + // avoid adding duplicated items + if(ui->listView->findItems(dir, Qt::MatchFixedString | Qt::MatchCaseSensitive).isEmpty()) { + ui->listView->addItem(dir); + } +} + +void FileSearchDialog::onRemovePath() { + // remove selected items + Q_FOREACH(QListWidgetItem* item, ui->listView->selectedItems()) { + delete item; + } +} + +void FileSearchDialog::setNameCaseInsensitive(bool caseInsensitive) { + ui->nameCaseInsensitive->setChecked(caseInsensitive); +} + +void FileSearchDialog::setContentCaseInsensitive(bool caseInsensitive) { + ui->contentCaseInsensitive->setChecked(caseInsensitive); +} + +void FileSearchDialog::setNameRegexp(bool reg) { + ui->nameRegExp->setChecked(reg); +} + +void FileSearchDialog::setContentRegexp(bool reg) { + ui->contentRegExp->setChecked(reg); +} + +void FileSearchDialog::setRecursive(bool rec) { + ui->recursiveSearch->setChecked(rec); +} + +void FileSearchDialog::setSearchhHidden(bool hidden) { + ui->searchHidden->setChecked(hidden); +} + +bool FileSearchDialog::nameCaseInsensitive() const { + return ui->nameCaseInsensitive->isChecked(); +} + +bool FileSearchDialog::contentCaseInsensitive() const { + return ui->contentCaseInsensitive->isChecked(); +} + +bool FileSearchDialog::nameRegexp() const { + return ui->nameRegExp->isChecked(); +} + +bool FileSearchDialog::contentRegexp() const { + return ui->contentRegExp->isChecked(); +} + +bool FileSearchDialog::recursive() const { + return ui->recursiveSearch->isChecked(); +} + +bool FileSearchDialog::searchhHidden() const { + return ui->searchHidden->isChecked(); +} + +} diff --git a/src/filesearchdialog.h b/src/filesearchdialog.h new file mode 100644 index 0000000..1ca6632 --- /dev/null +++ b/src/filesearchdialog.h @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_FILESEARCHDIALOG_H +#define FM_FILESEARCHDIALOG_H + +#include "libfmqtglobals.h" +#include +#include "path.h" + +namespace Ui { +class SearchDialog; +} + +namespace Fm { + +class LIBFM_QT_API FileSearchDialog : public QDialog { +public: + explicit FileSearchDialog(QStringList paths = QStringList(), QWidget* parent = 0, Qt::WindowFlags f = 0); + ~FileSearchDialog(); + + Path searchUri() const { + return searchUri_; + } + + virtual void accept(); + + bool nameCaseInsensitive() const; + void setNameCaseInsensitive(bool caseInsensitive); + + bool contentCaseInsensitive() const; + void setContentCaseInsensitive(bool caseInsensitive); + + bool nameRegexp() const; + void setNameRegexp(bool reg); + + bool contentRegexp() const; + void setContentRegexp(bool reg); + + bool recursive() const; + void setRecursive(bool rec); + + bool searchhHidden() const; + void setSearchhHidden(bool hidden); + +private Q_SLOTS: + void onAddPath(); + void onRemovePath(); + +private: + Ui::SearchDialog* ui; + Path searchUri_; +}; + +} + +#endif // FM_FILESEARCHDIALOG_H diff --git a/src/fm-search.c b/src/fm-search.c new file mode 100644 index 0000000..4298848 --- /dev/null +++ b/src/fm-search.c @@ -0,0 +1,317 @@ +/* + * fm-search-uri.c + * + * Copyright 2015 Hong Jen Yee (PCMan) + * Copyright 2012-2014 Andriy Grytsenko (LStranger) + * + * 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 "fm-search.h" +#include + +struct _FmSearch +{ + gboolean recursive; + gboolean show_hidden; + char* name_patterns; + gboolean name_ci; + gboolean name_regex; + char* content_pattern; + gboolean content_ci; + gboolean content_regex; + GList* mime_types; + GList* search_path_list; + guint64 max_size; + guint64 min_size; + char* max_mtime; + char* min_mtime; +}; + +FmSearch* fm_search_new (void) +{ + FmSearch* search = (FmSearch*)g_slice_new0(FmSearch); + return search; +} + +void fm_search_free(FmSearch* search) +{ + g_list_free_full(search->mime_types, (GDestroyNotify)g_free); + g_list_free_full(search->search_path_list, (GDestroyNotify)g_free); + g_free(search->name_patterns); + g_free(search->content_pattern); + g_free(search->max_mtime); + g_free(search->min_mtime); + g_slice_free(FmSearch, search); +} + +gboolean fm_search_get_recursive(FmSearch* search) +{ + return search->recursive; +} + +void fm_search_set_recursive(FmSearch* search, gboolean recursive) +{ + search->recursive = recursive; +} + +gboolean fm_search_get_show_hidden(FmSearch* search) +{ + return search->show_hidden; +} + +void fm_search_set_show_hidden(FmSearch* search, gboolean show_hidden) +{ + search->show_hidden = show_hidden; +} + +const char* fm_search_get_name_patterns(FmSearch* search) +{ + return search->name_patterns; +} + +void fm_search_set_name_patterns(FmSearch* search, const char* name_patterns) +{ + g_free(search->name_patterns); + search->name_patterns = g_strdup(name_patterns); +} + +gboolean fm_search_get_name_ci(FmSearch* search) +{ + return search->name_ci; +} + +void fm_search_set_name_ci(FmSearch* search, gboolean name_ci) +{ + search->name_ci = name_ci; +} + +gboolean fm_search_get_name_regex(FmSearch* search) +{ + return search->name_regex; +} + +void fm_search_set_name_regex(FmSearch* search, gboolean name_regex) +{ + search->name_regex = name_regex; +} + +const char* fm_search_get_content_pattern(FmSearch* search) +{ + return search->content_pattern; +} + +void fm_search_set_content_pattern(FmSearch* search, const char* content_pattern) +{ + g_free(search->content_pattern); + search->content_pattern = g_strdup(content_pattern); +} + +gboolean fm_search_get_content_ci(FmSearch* search) +{ + return search->content_ci; +} + +void fm_search_set_content_ci(FmSearch* search, gboolean content_ci) +{ + search->content_ci = content_ci; +} + +gboolean fm_search_get_content_regex(FmSearch* search) +{ + return search->content_regex; +} + +void fm_search_set_content_regex(FmSearch* search, gboolean content_regex) +{ + search->content_regex = content_regex; +} + +void fm_search_add_dir(FmSearch* search, const char* dir) +{ + GList* l = g_list_find_custom(search->search_path_list, dir, (GCompareFunc)strcmp); + if(!l) + search->search_path_list = g_list_prepend(search->search_path_list, g_strdup(dir)); +} + +void fm_search_remove_dir(FmSearch* search, const char* dir) +{ + GList* l = g_list_find_custom(search->search_path_list, dir, (GCompareFunc)strcmp); + if(G_LIKELY(l)) + { + g_free(l->data); + search->search_path_list = g_list_delete_link(search->search_path_list, l); + } +} + +GList* fm_search_get_dirs(FmSearch* search) +{ + return search->search_path_list; +} + +void fm_search_add_mime_type(FmSearch* search, const char* mime_type) +{ + GList* l = g_list_find_custom(search->mime_types, mime_type, (GCompareFunc)strcmp); + if(!l) + search->mime_types = g_list_prepend(search->mime_types, g_strdup(mime_type)); +} + +void fm_search_remove_mime_type(FmSearch* search, const char* mime_type) +{ + GList* l = g_list_find_custom(search->mime_types, mime_type, (GCompareFunc)strcmp); + if(G_LIKELY(l)) + { + g_free(l->data); + search->mime_types = g_list_delete_link(search->mime_types, l); + } +} + +GList* fm_search_get_mime_types(FmSearch* search) +{ + return search->mime_types; +} + +guint64 fm_search_get_max_size(FmSearch* search) +{ + return search->max_size; +} + +void fm_search_set_max_size(FmSearch* search, guint64 size) +{ + search->max_size = size; +} + +guint64 fm_search_get_min_size(FmSearch* search) +{ + return search->min_size; +} + +void fm_search_set_min_size(FmSearch* search, guint64 size) +{ + search->min_size = size; +} + +/* format of mtime: YYYY-MM-DD */ +const char* fm_search_get_max_mtime(FmSearch* search) +{ + return search->max_mtime; +} + +void fm_search_set_max_mtime(FmSearch* search, const char* mtime) +{ + g_free(search->max_mtime); + search->max_mtime = g_strdup(mtime); +} + +/* format of mtime: YYYY-MM-DD */ +const char* fm_search_get_min_mtime(FmSearch* search) +{ + return search->min_mtime; +} + +void fm_search_set_min_mtime(FmSearch* search, const char* mtime) +{ + g_free(search->min_mtime); + search->min_mtime = g_strdup(mtime); +} + +/* really build the path */ +FmPath* fm_search_dup_path(FmSearch* search) +{ + FmPath* search_path = NULL; + GString* search_str = g_string_sized_new(1024); + /* build the search:// URI to perform the search */ + g_string_append(search_str, "search://"); + + if(search->search_path_list) /* we need to have at least one dir path */ + { + char *escaped; + /* add paths */ + GList* l; + for(l = search->search_path_list; ; ) + { + char *path_str = (char*)l->data; + /* escape possible '?' and ',' */ + escaped = g_uri_escape_string(path_str, "!$&'()*+:;=/@", TRUE); + g_string_append(search_str, escaped); + g_free(escaped); + + l = l->next; + if(!l) /* no more items */ + break; + g_string_append_c(search_str, ','); /* separator for paths */ + } + + g_string_append_c(search_str, '?'); + g_string_append_printf(search_str, "recursive=%c", search->recursive ? '1' : '0'); + g_string_append_printf(search_str, "&show_hidden=%c", search->show_hidden ? '1' : '0'); + if(search->name_patterns && *search->name_patterns) + { + /* escape ampersands in pattern */ + escaped = g_uri_escape_string(search->name_patterns, ":/?#[]@!$'()*+,;", TRUE); + if(search->name_regex) + g_string_append_printf(search_str, "&name_regex=%s", escaped); + else + g_string_append_printf(search_str, "&name=%s", escaped); + if(search->name_ci) + g_string_append_printf(search_str, "&name_ci=%c", search->name_ci ? '1' : '0'); + g_free(escaped); + } + + if(search->content_pattern && *search->content_pattern) + { + /* escape ampersands in pattern */ + escaped = g_uri_escape_string(search->content_pattern, ":/?#[]@!$'()*+,;^<>{}", TRUE); + if(search->content_regex) + g_string_append_printf(search_str, "&content_regex=%s", escaped); + else + g_string_append_printf(search_str, "&content=%s", escaped); + g_free(escaped); + if(search->content_ci) + g_string_append_printf(search_str, "&content_ci=%c", search->content_ci ? '1' : '0'); + } + + /* search for the files of specific mime-types */ + if(search->mime_types) + { + GList* l; + g_string_append(search_str, "&mime_types="); + for(l = search->mime_types; l; l=l->next) + { + const char* mime_type = (const char*)l->data; + g_string_append(search_str, mime_type); + if(l->next) + g_string_append_c(search_str, ';'); + } + } + + if(search->min_size) + g_string_append_printf(search_str, "&min_size=%llu", (unsigned long long)search->min_size); + + if(search->max_size) + g_string_append_printf(search_str, "&max_size=%llu", (unsigned long long)search->max_size); + + if(search->min_mtime) + g_string_append_printf(search_str, "&min_mtime=%s", search->min_mtime); + + if(search->max_mtime) + g_string_append_printf(search_str, "&max_mtime=%s", search->max_mtime); + + search_path = fm_path_new_for_uri(search_str->str); + g_string_free(search_str, TRUE); + } + return search_path; +} diff --git a/src/fm-search.h b/src/fm-search.h new file mode 100644 index 0000000..c99f71d --- /dev/null +++ b/src/fm-search.h @@ -0,0 +1,89 @@ +/* + * fm-search-uri.h + * + * Copyright 2015 Hong Jen Yee (PCMan) + * Copyright 2012-2014 Andriy Grytsenko (LStranger) + * + * 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 + * + */ + +/* FmSearch implements a tool used to generate a search:// URI used by libfm to search for files. + * This API might become part of libfm in the future. + */ + +#ifndef _FM_SEARCH_H_ +#define _FM_SEARCH_H_ + +#include + +G_BEGIN_DECLS + +typedef struct _FmSearch FmSearch; + +FmSearch* fm_search_new(void); +void fm_search_free(FmSearch* search); + +FmPath* fm_search_dup_path(FmSearch* search); + +gboolean fm_search_get_recursive(FmSearch* search); +void fm_search_set_recursive(FmSearch* search, gboolean recursive); + +gboolean fm_search_get_show_hidden(FmSearch* search); +void fm_search_set_show_hidden(FmSearch* search, gboolean show_hidden); + +const char* fm_search_get_name_patterns(FmSearch* search); +void fm_search_set_name_patterns(FmSearch* search, const char* name_patterns); + +gboolean fm_search_get_name_ci(FmSearch* search); +void fm_search_set_name_ci(FmSearch* search, gboolean name_ci); + +gboolean fm_search_get_name_regex(FmSearch* search); +void fm_search_set_name_regex(FmSearch* search, gboolean name_regex); + +const char* fm_search_get_content_pattern(FmSearch* search); +void fm_search_set_content_pattern(FmSearch* search, const char* content_pattern); + +gboolean fm_search_get_content_ci(FmSearch* search); +void fm_search_set_content_ci(FmSearch* search, gboolean content_ci); + +gboolean fm_search_get_content_regex(FmSearch* search); +void fm_search_set_content_regex(FmSearch* search, gboolean content_regex); + +void fm_search_add_dir(FmSearch* search, const char* dir); +void fm_search_remove_dir(FmSearch* search, const char* dir); +GList* fm_search_get_dirs(FmSearch* search); + +void fm_search_add_mime_type(FmSearch* search, const char* mime_type); +void fm_search_remove_mime_type(FmSearch* search, const char* mime_type); +GList* fm_search_get_mime_types(FmSearch* search); + +guint64 fm_search_get_max_size(FmSearch* search); +void fm_search_set_max_size(FmSearch* search, guint64 size); + +guint64 fm_search_get_min_size(FmSearch* search); +void fm_search_set_min_size(FmSearch* search, guint64 size); + +/* format of mtime: YYYY-MM-DD */ +const char* fm_search_get_max_mtime(FmSearch* search); +void fm_search_set_max_mtime(FmSearch* search, const char* mtime); + +/* format of mtime: YYYY-MM-DD */ +const char* fm_search_get_min_mtime(FmSearch* search); +void fm_search_set_min_mtime(FmSearch* search, const char* mtime); + +G_END_DECLS + +#endif /* _FM_SEARCH_H_ */ diff --git a/src/folderconfig.h b/src/folderconfig.h new file mode 100644 index 0000000..3a0843c --- /dev/null +++ b/src/folderconfig.h @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2016 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef __LIBFM_QT_FM_FOLDER_CONFIG_H__ +#define __LIBFM_QT_FM_FOLDER_CONFIG_H__ + +#include +#include +#include +#include "libfmqtglobals.h" + +#include "core/filepath.h" + +namespace Fm { + +// FIXME: port to the new API and drop libfm dependency + +class LIBFM_QT_API FolderConfig { +public: + + FolderConfig(const Fm::FilePath& path) { + FmPath* fmpath = fm_path_new_for_gfile(path.gfile().get()); + dataPtr_ = reinterpret_cast(fm_folder_config_open(fmpath)); + fm_path_unref(fmpath); + } + + + // default constructor + FolderConfig() { + dataPtr_ = nullptr; + } + + + // move constructor + FolderConfig(FolderConfig&& other) noexcept { + dataPtr_ = reinterpret_cast(other.takeDataPtr()); + } + + + // destructor + ~FolderConfig() { + if(dataPtr_ != nullptr) { + fm_folder_config_close(dataPtr_, nullptr); + } + } + + + // create a wrapper for the data pointer without increasing the reference count + static FolderConfig wrapPtr(FmFolderConfig* dataPtr) { + FolderConfig obj; + obj.dataPtr_ = reinterpret_cast(dataPtr); + return obj; + } + + // disown the managed data pointer + FmFolderConfig* takeDataPtr() { + FmFolderConfig* data = reinterpret_cast(dataPtr_); + dataPtr_ = nullptr; + return data; + } + + // get the raw pointer wrapped + FmFolderConfig* dataPtr() { + return reinterpret_cast(dataPtr_); + } + + // automatic type casting + operator FmFolderConfig* () { + return dataPtr(); + } + + // automatic type casting + operator void* () { + return dataPtr(); + } + + + + // move assignment + FolderConfig& operator=(FolderConfig&& other) noexcept { + dataPtr_ = reinterpret_cast(other.takeDataPtr()); + return *this; + } + + bool isNull() { + return (dataPtr_ == nullptr); + } + + // methods + + static void saveCache(void) { + fm_folder_config_save_cache(); + } + + + void purge(void) { + fm_folder_config_purge(dataPtr()); + } + + + void removeKey(const char* key) { + fm_folder_config_remove_key(dataPtr(), key); + } + + + void setStringList(const char* key, const gchar* const list[], gsize length) { + fm_folder_config_set_string_list(dataPtr(), key, list, length); + } + + + void setString(const char* key, const char* string) { + fm_folder_config_set_string(dataPtr(), key, string); + } + + + void setBoolean(const char* key, gboolean val) { + fm_folder_config_set_boolean(dataPtr(), key, val); + } + + + void setDouble(const char* key, gdouble val) { + fm_folder_config_set_double(dataPtr(), key, val); + } + + + void setUint64(const char* key, guint64 val) { + fm_folder_config_set_uint64(dataPtr(), key, val); + } + + + void setInteger(const char* key, gint val) { + fm_folder_config_set_integer(dataPtr(), key, val); + } + + + char** getStringList(const char* key, gsize* length) { + return fm_folder_config_get_string_list(dataPtr(), key, length); + } + + + char* getString(const char* key) { + return fm_folder_config_get_string(dataPtr(), key); + } + + + bool getBoolean(const char* key, gboolean* val) { + return fm_folder_config_get_boolean(dataPtr(), key, val); + } + + + bool getDouble(const char* key, gdouble* val) { + return fm_folder_config_get_double(dataPtr(), key, val); + } + + + bool getUint64(const char* key, guint64* val) { + return fm_folder_config_get_uint64(dataPtr(), key, val); + } + + + bool getInteger(const char* key, gint* val) { + return fm_folder_config_get_integer(dataPtr(), key, val); + } + + + bool isEmpty(void) { + return fm_folder_config_is_empty(dataPtr()); + } + + +// the wrapped object cannot be copied. +private: + FolderConfig(const FolderConfig& other) = delete; + FolderConfig& operator=(const FolderConfig& other) = delete; + + +private: + FmFolderConfig* dataPtr_; // data pointer for the underlying C struct + +}; + + +} + +#endif // __LIBFM_QT_FM_FOLDER_CONFIG_H__ diff --git a/src/folderitemdelegate.cpp b/src/folderitemdelegate.cpp new file mode 100644 index 0000000..62e1dbc --- /dev/null +++ b/src/folderitemdelegate.cpp @@ -0,0 +1,431 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "folderitemdelegate.h" +#include "foldermodel.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Fm { + +FolderItemDelegate::FolderItemDelegate(QAbstractItemView* view, QObject* parent): + QStyledItemDelegate(parent ? parent : view), + symlinkIcon_(QIcon::fromTheme("emblem-symbolic-link")), + fileInfoRole_(Fm::FolderModel::FileInfoRole), + iconInfoRole_(-1), + margins_(QSize(3, 3)), + hasEditor_(false) { + connect(this, &QAbstractItemDelegate::closeEditor, [=]{hasEditor_ = false;}); +} + +FolderItemDelegate::~FolderItemDelegate() { + +} + +QSize FolderItemDelegate::iconViewTextSize(const QModelIndex& index) const { + QStyleOptionViewItem opt; + initStyleOption(&opt, index); + opt.decorationSize = iconSize_.isValid() ? iconSize_ : QSize(0, 0); + opt.decorationAlignment = Qt::AlignHCenter | Qt::AlignTop; + opt.displayAlignment = Qt::AlignTop | Qt::AlignHCenter; + QRectF textRect(0, 0, + itemSize_.width() - 2 * margins_.width(), + itemSize_.height() - 2 * margins_.height() - opt.decorationSize.height()); + drawText(nullptr, opt, textRect); // passing nullptr for painter will calculate the bounding rect only + return textRect.toRect().size(); +} + +QSize FolderItemDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const { + QVariant value = index.data(Qt::SizeHintRole); + if(value.isValid()) { + // no further processing if the size is specified by the data model + return qvariant_cast(value); + } + + if(option.decorationPosition == QStyleOptionViewItem::Top || + option.decorationPosition == QStyleOptionViewItem::Bottom) { + // we handle vertical layout just by returning our item size + return itemSize_; + } + + // The default size hint of the horizontal layout isn't reliable + // because Qt calculates the row size based on the real icon size, + // which may not be equal to the requested icon size on various occasions. + // So, we do as in QStyledItemDelegate::sizeHint() but use the requested size. + QStyleOptionViewItem opt = option; + initStyleOption(&opt, index); + opt.decorationSize = option.decorationSize; // requested by the view + const QWidget* widget = option.widget; + QStyle* style = widget ? widget->style() : QApplication::style(); + return style->sizeFromContents(QStyle::CT_ItemViewItem, &opt, QSize(), widget); +} + +QIcon::Mode FolderItemDelegate::iconModeFromState(const QStyle::State state) { + + if(state & QStyle::State_Enabled) { + return (state & QStyle::State_Selected) ? QIcon::Selected : QIcon::Normal; + } + + return QIcon::Disabled; +} + +void FolderItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { + if(!index.isValid()) + return; + + // get emblems for this icon + std::forward_list> icon_emblems; + auto fmicon = index.data(iconInfoRole_).value>(); + if(fmicon) { + icon_emblems = fmicon->emblems(); + } + // get file info for the item + auto file = index.data(fileInfoRole_).value>(); + const auto& emblems = file ? file->emblems() : icon_emblems; + + bool isSymlink = file && file->isSymlink(); + // vertical layout (icon mode, thumbnail mode) + if(option.decorationPosition == QStyleOptionViewItem::Top || + option.decorationPosition == QStyleOptionViewItem::Bottom) { + painter->save(); + painter->setClipRect(option.rect); + + QStyleOptionViewItem opt = option; + initStyleOption(&opt, index); + opt.decorationAlignment = Qt::AlignHCenter | Qt::AlignTop; + opt.displayAlignment = Qt::AlignTop | Qt::AlignHCenter; + + // draw the icon + QIcon::Mode iconMode = iconModeFromState(opt.state); + QPoint iconPos(opt.rect.x() + (opt.rect.width() - option.decorationSize.width()) / 2, opt.rect.y() + margins_.height()); + QPixmap pixmap = opt.icon.pixmap(option.decorationSize, iconMode); + // in case the pixmap is smaller than the requested size + QSize margin = ((option.decorationSize - pixmap.size()) / 2).expandedTo(QSize(0, 0)); + bool isCut = index.data(FolderModel::FileIsCutRole).toBool(); + if(isCut) { + painter->save(); + painter->setOpacity(0.45); + } + painter->drawPixmap(iconPos + QPoint(margin.width(), margin.height()), pixmap); + if(isCut) { + painter->restore(); + } + + // draw some emblems for the item if needed + if(isSymlink) { + // draw the emblem for symlinks + painter->drawPixmap(iconPos, symlinkIcon_.pixmap(option.decorationSize / 2, iconMode)); + } + + // draw other emblems if there's any + if(!emblems.empty()) { + // FIXME: we only support one emblem now + QPoint emblemPos(opt.rect.x() + opt.rect.width() / 2, opt.rect.y() + option.decorationSize.height() / 2); + QIcon emblem = emblems.front()->qicon(); + painter->drawPixmap(emblemPos, emblem.pixmap(option.decorationSize / 2, iconMode)); + } + + // draw the text + QSize drawAreaSize = itemSize_ - 2 * margins_; + // The text rect dimensions should be exactly as they were in sizeHint() + QRectF textRect(opt.rect.x() + (opt.rect.width() - drawAreaSize.width()) / 2, + opt.rect.y() + margins_.height() + option.decorationSize.height(), + drawAreaSize.width(), + drawAreaSize.height() - option.decorationSize.height()); + drawText(painter, opt, textRect); + painter->restore(); + } + else { // horizontal layout (list view) + + // let QStyledItemDelegate does its default painting + // FIXME: For better text alignment, here we should increase + // the icon width if it's smaller that the requested size + QStyledItemDelegate::paint(painter, option, index); + + // draw emblems if needed + if(isSymlink || !emblems.empty()) { + QStyleOptionViewItem opt = option; + initStyleOption(&opt, index); + QIcon::Mode iconMode = iconModeFromState(opt.state); + // draw some emblems for the item if needed + if(isSymlink) { + QPoint iconPos(opt.rect.x(), opt.rect.y() + (opt.rect.height() - option.decorationSize.height()) / 2); + painter->drawPixmap(iconPos, symlinkIcon_.pixmap(option.decorationSize / 2, iconMode)); + } + else { + // FIXME: we only support one emblem now + QPoint iconPos(opt.rect.x() + option.decorationSize.width() / 2, opt.rect.y() + opt.rect.height() / 2); + QIcon emblem = emblems.front()->qicon(); + painter->drawPixmap(iconPos, emblem.pixmap(option.decorationSize / 2, iconMode)); + } + } + } +} + +// if painter is nullptr, the method calculate the bounding rectangle of the text and save it to textRect +void FolderItemDelegate::drawText(QPainter* painter, QStyleOptionViewItem& opt, QRectF& textRect) const { + QTextLayout layout(opt.text, opt.font); + QTextOption textOption; + textOption.setAlignment(opt.displayAlignment); + textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); + // FIXME: textOption.setTextDirection(opt.direction); ? + if(opt.text.isRightToLeft()) { + textOption.setTextDirection(Qt::RightToLeft); + } + else { + textOption.setTextDirection(Qt::LeftToRight); + } + layout.setTextOption(textOption); + qreal height = 0; + qreal width = 0; + int visibleLines = 0; + layout.beginLayout(); + QString elidedText; + textRect.adjust(2, 2, -2, -2); // a 2-px margin is considered at FolderView::updateGridSize() + for(;;) { + QTextLine line = layout.createLine(); + if(!line.isValid()) { + break; + } + line.setLineWidth(textRect.width()); + height += opt.fontMetrics.leading(); + line.setPosition(QPointF(0, height)); + if((height + line.height() + textRect.y()) > textRect.bottom()) { + // if part of this line falls outside the textRect, ignore it and quit. + QTextLine lastLine = layout.lineAt(visibleLines - 1); + elidedText = opt.text.mid(lastLine.textStart()); + elidedText = opt.fontMetrics.elidedText(elidedText, opt.textElideMode, textRect.width()); + if(visibleLines == 1) { // this is the only visible line + width = textRect.width(); + } + break; + } + height += line.height(); + width = qMax(width, line.naturalTextWidth()); + ++ visibleLines; + } + layout.endLayout(); + width = qMax(width, (qreal)opt.fontMetrics.width(elidedText)); + + // draw background for selected item + QRectF boundRect = layout.boundingRect(); + //qDebug() << "bound rect: " << boundRect << "width: " << width; + boundRect.setWidth(width); + boundRect.setHeight(height); + boundRect.moveTo(textRect.x() + (textRect.width() - width) / 2, textRect.y()); + + QRectF selRect = boundRect.adjusted(-2, -2, 2, 2); + + if(!painter) { // no painter, calculate the bounding rect only + textRect = selRect; + return; + } + + // ????? + QPalette::ColorGroup cg = (opt.state & QStyle::State_Enabled) + ? (opt.state & QStyle::State_Active) + ? QPalette::Active + : QPalette::Inactive + : QPalette::Disabled; + if(opt.state & QStyle::State_Selected) { + if(!opt.widget) { + painter->fillRect(selRect, opt.palette.highlight()); + } + painter->setPen(opt.palette.color(cg, QPalette::HighlightedText)); + } + else { + painter->setPen(opt.palette.color(cg, QPalette::Text)); + } + + if(opt.state & QStyle::State_Selected || opt.state & QStyle::State_MouseOver) { + if(const QWidget* widget = opt.widget) { // let the style engine do it + QStyle* style = widget->style() ? widget->style() : qApp->style(); + QStyleOptionViewItem o(opt); + o.text = QString(); + o.rect = selRect.toAlignedRect().intersected(opt.rect); // due to clipping and rounding, we might lose 1px + o.showDecorationSelected = true; + style->drawPrimitive(QStyle::PE_PanelItemViewItem, &o, painter, widget); + } + } + + // draw shadow for text if the item is not selected and a shadow color is set + if(!(opt.state & QStyle::State_Selected) && shadowColor_.isValid()) { + QPen prevPen = painter->pen(); + painter->setPen(QPen(shadowColor_)); + for(int i = 0; i < visibleLines; ++i) { + QTextLine line = layout.lineAt(i); + if(i == (visibleLines - 1) && !elidedText.isEmpty()) { // the last line, draw elided text + QPointF pos(boundRect.x() + line.position().x() + 1, boundRect.y() + line.y() + line.ascent() + 1); + painter->drawText(pos, elidedText); + } + else { + line.draw(painter, textRect.topLeft() + QPointF(1, 1)); + } + } + painter->setPen(prevPen); + } + + // draw text + for(int i = 0; i < visibleLines; ++i) { + QTextLine line = layout.lineAt(i); + if(i == (visibleLines - 1) && !elidedText.isEmpty()) { // the last line, draw elided text + QPointF pos(boundRect.x() + line.position().x(), boundRect.y() + line.y() + line.ascent()); + painter->drawText(pos, elidedText); + } + else { + line.draw(painter, textRect.topLeft()); + } + } + + if(opt.state & QStyle::State_HasFocus) { + // draw focus rect + QStyleOptionFocusRect o; + o.QStyleOption::operator=(opt); + o.rect = selRect.toRect(); // subElementRect(SE_ItemViewItemFocusRect, vopt, widget); + o.state |= QStyle::State_KeyboardFocusChange; + o.state |= QStyle::State_Item; + QPalette::ColorGroup cg = (opt.state & QStyle::State_Enabled) + ? QPalette::Normal : QPalette::Disabled; + o.backgroundColor = opt.palette.color(cg, (opt.state & QStyle::State_Selected) + ? QPalette::Highlight : QPalette::Window); + if(const QWidget* widget = opt.widget) { + QStyle* style = widget->style() ? widget->style() : qApp->style(); + style->drawPrimitive(QStyle::PE_FrameFocusRect, &o, painter, widget); + } + } +} + +/* + * The following methods are for inline renaming. + */ + +QWidget* FolderItemDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const { + hasEditor_ = true; + if (option.decorationPosition == QStyleOptionViewItem::Top + || option.decorationPosition == QStyleOptionViewItem::Bottom) + { + // in icon view, we use QTextEdit as the editor (and not QPlainTextEdit + // because the latter always shows an empty space at the bottom) + QTextEdit *textEdit = new QTextEdit(parent); + textEdit->setAcceptRichText(false); + + // Since the text color on desktop is inherited from desktop foreground color, + // it may not be suitable. So, we reset it by using the app palette. + QPalette p = textEdit->palette(); + p.setColor(QPalette::Text, qApp->palette().text().color()); + textEdit->setPalette(p); + + textEdit->ensureCursorVisible(); + textEdit->setFocusPolicy(Qt::StrongFocus); + textEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + textEdit->setContentsMargins(0, 0, 0, 0); + return textEdit; + } + else { + // return the default line-edit in compact view + return QStyledItemDelegate::createEditor(parent, option, index); + } +} + +void FolderItemDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const { + if (!index.isValid()) { + return; + } + const QString currentName = index.data(Qt::EditRole).toString(); + + if (QTextEdit* textEdit = qobject_cast(editor)) { + textEdit->setPlainText(currentName); + textEdit->setUndoRedoEnabled(false); + textEdit->setAlignment(Qt::AlignCenter); + textEdit->setUndoRedoEnabled(true); + // select text appropriately + QTextCursor cur = textEdit->textCursor(); + int end; + if (index.data(Fm::FolderModel::FileIsDirRole).toBool() || !currentName.contains(".")) { + end = currentName.size(); + } + else { + end = currentName.lastIndexOf("."); + } + cur.setPosition(end, QTextCursor::KeepAnchor); + textEdit->setTextCursor(cur); + } + else if (QLineEdit* lineEdit = qobject_cast(editor)) { + lineEdit->setText(currentName); + if (!index.data(Fm::FolderModel::FileIsDirRole).toBool() && currentName.contains(".")) + { + /* Qt will call QLineEdit::selectAll() after calling setEditorData() in + qabstractitemview.cpp -> QAbstractItemViewPrivate::editor(). Therefore, + we cannot select a part of the text in the usual way here. */ + QTimer::singleShot(0, [lineEdit]() { + int length = lineEdit->text().lastIndexOf("."); + lineEdit->setSelection(0, length); + }); + } + } +} + +bool FolderItemDelegate::eventFilter(QObject* object, QEvent* event) { + QWidget *editor = qobject_cast(object); + if (editor && event->type() == QEvent::KeyPress) { + int k = static_cast(event)->key(); + if (k == Qt::Key_Return || k == Qt::Key_Enter) { + Q_EMIT QAbstractItemDelegate::commitData(editor); + Q_EMIT QAbstractItemDelegate::closeEditor(editor, QAbstractItemDelegate::NoHint); + return true; + } + } + return QStyledItemDelegate::eventFilter(object, event); +} + +void FolderItemDelegate::updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const { + if (option.decorationPosition == QStyleOptionViewItem::Top + || option.decorationPosition == QStyleOptionViewItem::Bottom) { + // give all of the available space to the editor + QStyleOptionViewItem opt = option; + initStyleOption(&opt, index); + opt.decorationAlignment = Qt::AlignHCenter|Qt::AlignTop; + opt.displayAlignment = Qt::AlignTop|Qt::AlignHCenter; + QRect textRect(opt.rect.x(), + opt.rect.y() + margins_.height() + option.decorationSize.height(), + itemSize_.width(), + itemSize_.height() - margins_.height() - option.decorationSize.height()); + int frame = editor->style()->pixelMetric(QStyle::PM_DefaultFrameWidth, &option, editor); + editor->setGeometry(textRect.adjusted(-frame, -frame, frame, frame)); + } + else { + // use the default editor geometry in compact view + QStyledItemDelegate::updateEditorGeometry(editor, option, index); + } +} + + +} // namespace Fm diff --git a/src/folderitemdelegate.h b/src/folderitemdelegate.h new file mode 100644 index 0000000..1886e39 --- /dev/null +++ b/src/folderitemdelegate.h @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_FOLDERITEMDELEGATE_H +#define FM_FOLDERITEMDELEGATE_H + +#include "libfmqtglobals.h" +#include +class QAbstractItemView; + +namespace Fm { + +class LIBFM_QT_API FolderItemDelegate : public QStyledItemDelegate { + Q_OBJECT +public: + explicit FolderItemDelegate(QAbstractItemView* view, QObject* parent = nullptr); + + virtual ~FolderItemDelegate(); + + inline void setItemSize(QSize size) { + itemSize_ = size; + } + + inline QSize itemSize() const { + return itemSize_; + } + + inline void setIconSize(QSize size) { + iconSize_ = size; + } + + inline QSize iconSize() const { + return iconSize_; + } + + int fileInfoRole() { + return fileInfoRole_; + } + + void setFileInfoRole(int role) { + fileInfoRole_ = role; + } + + int iconInfoRole() { + return iconInfoRole_; + } + + void setIconInfoRole(int role) { + iconInfoRole_ = role; + } + + // only support vertical layout (icon view mode: text below icon) + void setShadowColor(const QColor& shadowColor) { + shadowColor_ = shadowColor; + } + + // only support vertical layout (icon view mode: text below icon) + const QColor& shadowColor() const { + return shadowColor_; + } + + // only support vertical layout (icon view mode: text below icon) + void setMargins(QSize margins) { + margins_ = margins.expandedTo(QSize(0, 0)); + } + + QSize getMargins() const { + return margins_; + } + + bool hasEditor() const { + return hasEditor_; + } + + virtual QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const; + + virtual void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const; + + virtual QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const; + + virtual void setEditorData(QWidget* editor, const QModelIndex& index) const; + + virtual bool eventFilter(QObject* object, QEvent* event); + + virtual void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const; + + QSize iconViewTextSize(const QModelIndex& index) const; + +private: + void drawText(QPainter* painter, QStyleOptionViewItem& opt, QRectF& textRect) const; + + static QIcon::Mode iconModeFromState(QStyle::State state); + +private: + QIcon symlinkIcon_; + QSize iconSize_; + QSize itemSize_; + int fileInfoRole_; + int iconInfoRole_; + QColor shadowColor_; + QSize margins_; + mutable bool hasEditor_; +}; + +} + +#endif // FM_FOLDERITEMDELEGATE_H diff --git a/src/foldermenu.cpp b/src/foldermenu.cpp new file mode 100644 index 0000000..1a87dd0 --- /dev/null +++ b/src/foldermenu.cpp @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * Copyright (C) 2012 - 2014 Andriy Grytsenko (LStranger) + * + * 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 "foldermenu.h" +#include "createnewmenu.h" +#include "filepropsdialog.h" +#include "folderview.h" +#include "utilities.h" +#include // for memset +#include +#include "customaction_p.h" +#include "customactions/fileaction.h" +#include + +namespace Fm { + +FolderMenu::FolderMenu(FolderView* view, QWidget* parent): + QMenu(parent), + view_(view) { + + ProxyFolderModel* model = view_->model(); + + createAction_ = new QAction(tr("Create &New"), this); + addAction(createAction_); + + createAction_->setMenu(new CreateNewMenu(view_, view_->path(), this)); + + separator1_ = addSeparator(); + + pasteAction_ = new QAction(QIcon::fromTheme("edit-paste"), tr("&Paste"), this); + addAction(pasteAction_); + connect(pasteAction_, &QAction::triggered, this, &FolderMenu::onPasteActionTriggered); + + separator2_ = addSeparator(); + + selectAllAction_ = new QAction(tr("Select &All"), this); + addAction(selectAllAction_); + connect(selectAllAction_, &QAction::triggered, this, &FolderMenu::onSelectAllActionTriggered); + + invertSelectionAction_ = new QAction(tr("Invert Selection"), this); + addAction(invertSelectionAction_); + connect(invertSelectionAction_, &QAction::triggered, this, &FolderMenu::onInvertSelectionActionTriggered); + + separator3_ = addSeparator(); + + sortAction_ = new QAction(tr("Sorting"), this); + addAction(sortAction_); + createSortMenu(); + sortAction_->setMenu(sortMenu_); + + showHiddenAction_ = new QAction(tr("Show Hidden"), this); + addAction(showHiddenAction_); + showHiddenAction_->setCheckable(true); + showHiddenAction_->setChecked(model->showHidden()); + connect(showHiddenAction_, &QAction::triggered, this, &FolderMenu::onShowHiddenActionTriggered); + + auto folderInfo = view_->folderInfo(); + if(folderInfo) { // should never be null (see FolderView::onFileClicked) + // DES-EMA custom actions integration + FileInfoList files; + files.push_back(folderInfo); + auto custom_actions = FileActionItem::get_actions_for_files(files); + for(auto& item: custom_actions) { + if(item && !(item->get_target() & FILE_ACTION_TARGET_CONTEXT)) { + continue; // this item is not for context menu + } + if(item == custom_actions.front() && item && !item->is_action()) { + addSeparator(); // before all custom actions + } + addCustomActionItem(this, item); + } + + // disable paste acton if it can't be used + pasteAction_->setEnabled(folderInfo->isWritable()); + } + + separator4_ = addSeparator(); + + propertiesAction_ = new QAction(tr("Folder Pr&operties"), this); + addAction(propertiesAction_); + connect(propertiesAction_, &QAction::triggered, this, &FolderMenu::onPropertiesActionTriggered); +} + +FolderMenu::~FolderMenu() { +} + +void FolderMenu::addCustomActionItem(QMenu* menu, std::shared_ptr item) { + if(!item) { + return; + } + if(item->is_action() && !(item->get_target() & FILE_ACTION_TARGET_CONTEXT)) { + return; + } + + CustomAction* action = new CustomAction(item, menu); + menu->addAction(action); + if(item->is_menu()) { + auto& subitems = item->get_sub_items(); + if(!subitems.empty()) { + QMenu* submenu = new QMenu(menu); + for(auto& subitem: subitems) { + addCustomActionItem(submenu, subitem); + } + action->setMenu(submenu); + } + } + else if(item->is_action()) { + connect(action, &QAction::triggered, this, &FolderMenu::onCustomActionTrigerred); + } +} + +void FolderMenu::onCustomActionTrigerred() { + CustomAction* action = static_cast(sender()); + auto& item = action->item(); + auto folderInfo = view_->folderInfo(); + if(folderInfo) { + CStrPtr output; + FileInfoList file_list; + file_list.push_back(folderInfo); + item->launch(nullptr, file_list, output); + if(output) { + QMessageBox::information(this, tr("Output"), output.get()); + } + } +} + +void FolderMenu::addSortMenuItem(QString title, int id) { + QAction* action = new QAction(title, this); + sortMenu_->addAction(action); + action->setCheckable(true); + sortActionGroup_->addAction(action); + connect(action, &QAction::triggered, this, &FolderMenu::onSortActionTriggered); + sortActions_[id] = action; +} + +void FolderMenu::createSortMenu() { + ProxyFolderModel* model = view_->model(); + + sortMenu_ = new QMenu(this); + sortActionGroup_ = new QActionGroup(sortMenu_); + sortActionGroup_->setExclusive(true); + + std::memset(sortActions_, 0, sizeof(sortActions_)); + + addSortMenuItem(tr("By File Name"), FolderModel::ColumnFileName); + addSortMenuItem(tr("By Modification Time"), FolderModel::ColumnFileMTime); + addSortMenuItem(tr("By File Size"), FolderModel::ColumnFileSize); + addSortMenuItem(tr("By File Type"), FolderModel::ColumnFileType); + addSortMenuItem(tr("By File Owner"), FolderModel::ColumnFileOwner); + + int col = model->sortColumn(); + + if(col >= 0 && col < FolderModel::NumOfColumns) { + sortActions_[col]->setChecked(true);; + } + + sortMenu_->addSeparator(); + + QActionGroup* group = new QActionGroup(this); + group->setExclusive(true); + actionAscending_ = new QAction(tr("Ascending"), this); + actionAscending_->setCheckable(true); + sortMenu_->addAction(actionAscending_); + group->addAction(actionAscending_); + + actionDescending_ = new QAction(tr("Descending"), this); + actionDescending_->setCheckable(true); + sortMenu_->addAction(actionDescending_); + group->addAction(actionDescending_); + + if(model->sortOrder() == Qt::AscendingOrder) { + actionAscending_->setChecked(true); + } + else { + actionDescending_->setChecked(true); + } + + connect(actionAscending_, &QAction::triggered, this, &FolderMenu::onSortOrderActionTriggered); + connect(actionDescending_, &QAction::triggered, this, &FolderMenu::onSortOrderActionTriggered); + + sortMenu_->addSeparator(); + + QAction* actionFolderFirst = new QAction(tr("Folder First"), this); + sortMenu_->addAction(actionFolderFirst); + actionFolderFirst->setCheckable(true); + + if(model->folderFirst()) { + actionFolderFirst->setChecked(true); + } + + connect(actionFolderFirst, &QAction::triggered, this, &FolderMenu::onFolderFirstActionTriggered); + + QAction* actionCaseSensitive = new QAction(tr("Case Sensitive"), this); + sortMenu_->addAction(actionCaseSensitive); + actionCaseSensitive->setCheckable(true); + + if(model->sortCaseSensitivity() == Qt::CaseSensitive) { + actionCaseSensitive->setChecked(true); + } + + connect(actionCaseSensitive, &QAction::triggered, this, &FolderMenu::onCaseSensitiveActionTriggered); +} + +void FolderMenu::onPasteActionTriggered() { + auto folderPath = view_->path(); + if(folderPath) { + pasteFilesFromClipboard(folderPath); + } +} + +void FolderMenu::onSelectAllActionTriggered() { + view_->selectAll(); +} + +void FolderMenu::onInvertSelectionActionTriggered() { + view_->invertSelection(); +} + +void FolderMenu::onSortActionTriggered(bool /*checked*/) { + ProxyFolderModel* model = view_->model(); + + if(model) { + QAction* action = static_cast(sender()); + + for(int col = 0; col < FolderModel::NumOfColumns; ++col) { + if(action == sortActions_[col]) { + model->sort(col, model->sortOrder()); + break; + } + } + } +} + +void FolderMenu::onSortOrderActionTriggered(bool /*checked*/) { + ProxyFolderModel* model = view_->model(); + + if(model) { + QAction* action = static_cast(sender()); + Qt::SortOrder order; + + if(action == actionAscending_) { + order = Qt::AscendingOrder; + } + else { + order = Qt::DescendingOrder; + } + + model->sort(model->sortColumn(), order); + } +} + +void FolderMenu::onShowHiddenActionTriggered(bool checked) { + ProxyFolderModel* model = view_->model(); + + if(model) { + qDebug("show hidden: %d", checked); + model->setShowHidden(checked); + } +} + +void FolderMenu::onCaseSensitiveActionTriggered(bool checked) { + ProxyFolderModel* model = view_->model(); + + if(model) { + model->setSortCaseSensitivity(checked ? Qt::CaseSensitive : Qt::CaseInsensitive); + } +} + +void FolderMenu::onFolderFirstActionTriggered(bool checked) { + ProxyFolderModel* model = view_->model(); + + if(model) { + model->setFolderFirst(checked); + } +} + +void FolderMenu::onPropertiesActionTriggered() { + auto folderInfo = view_->folderInfo(); + if(folderInfo) { + FilePropsDialog::showForFile(folderInfo); + } +} + +} // namespace Fm diff --git a/src/foldermenu.h b/src/foldermenu.h new file mode 100644 index 0000000..70d1e25 --- /dev/null +++ b/src/foldermenu.h @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_FOLDERMENU_H +#define FM_FOLDERMENU_H + +#include "libfmqtglobals.h" +#include +#include +#include "foldermodel.h" + +class QAction; + +namespace Fm { + +class FolderView; +class FileActionItem; + +class LIBFM_QT_API FolderMenu : public QMenu { + Q_OBJECT + +public: + explicit FolderMenu(FolderView* view, QWidget* parent = 0); + virtual ~FolderMenu(); + + QAction* createAction() { + return createAction_; + } + + QAction* separator1() { + return separator1_; + } + + QAction* pasteAction() { + return pasteAction_; + } + + QAction* separator2() { + return separator2_; + } + + QAction* selectAllAction() { + return selectAllAction_; + } + + QAction* invertSelectionAction() { + return invertSelectionAction_; + } + + QAction* separator3() { + return separator3_; + } + + QAction* sortAction() { + return sortAction_; + } + + QAction* showHiddenAction() { + return showHiddenAction_; + } + + QAction* separator4() { + return separator4_; + } + + QAction* propertiesAction() { + return propertiesAction_; + } + + FolderView* view() { + return view_; + } + +protected: + void addCustomActionItem(QMenu* menu, std::shared_ptr item); + +protected Q_SLOTS: + void onPasteActionTriggered(); + void onSelectAllActionTriggered(); + void onInvertSelectionActionTriggered(); + void onSortActionTriggered(bool checked); + void onSortOrderActionTriggered(bool checked); + void onShowHiddenActionTriggered(bool checked); + void onCaseSensitiveActionTriggered(bool checked); + void onFolderFirstActionTriggered(bool checked); + void onPropertiesActionTriggered(); + void onCustomActionTrigerred(); + +private: + void createSortMenu(); + void addSortMenuItem(QString title, int id); + +private: + FolderView* view_; + QAction* createAction_; + QAction* separator1_; + QAction* pasteAction_; + QAction* separator2_; + QAction* selectAllAction_; + QAction* invertSelectionAction_; + QAction* separator3_; + QAction* sortAction_; + QActionGroup* sortActionGroup_; + QMenu* sortMenu_; + QAction* sortActions_[FolderModel::NumOfColumns]; + QAction* actionAscending_; + QAction* actionDescending_; + QAction* showHiddenAction_; + QAction* separator4_; + QAction* propertiesAction_; +}; + +} + +#endif // FM_FOLDERMENU_H diff --git a/src/foldermodel.cpp b/src/foldermodel.cpp new file mode 100644 index 0000000..3437ca4 --- /dev/null +++ b/src/foldermodel.cpp @@ -0,0 +1,546 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "foldermodel.h" +#include "icontheme.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "utilities.h" +#include "fileoperation.h" + +namespace Fm { + +FolderModel::FolderModel(): + hasPendingThumbnailHandler_{false} { +} + +FolderModel::~FolderModel() { + qDebug("delete FolderModel"); + // if the thumbnail requests list is not empty, cancel them + for(auto job: pendingThumbnailJobs_) { + job->cancel(); + } +} + +void FolderModel::setFolder(const std::shared_ptr& new_folder) { + if(folder_) { + removeAll(); // remove old items + } + if(new_folder) { + folder_ = new_folder; + connect(folder_.get(), &Fm::Folder::startLoading, this, &FolderModel::onStartLoading); + connect(folder_.get(), &Fm::Folder::finishLoading, this, &FolderModel::onFinishLoading); + connect(folder_.get(), &Fm::Folder::filesAdded, this, &FolderModel::onFilesAdded); + connect(folder_.get(), &Fm::Folder::filesChanged, this, &FolderModel::onFilesChanged); + connect(folder_.get(), &Fm::Folder::filesRemoved, this, &FolderModel::onFilesRemoved); + // handle the case if the folder is already loaded + if(folder_->isLoaded()) { + insertFiles(0, folder_->files()); + } + } +} + +void FolderModel::onStartLoading() { + // remove all items + removeAll(); +} + +void FolderModel::onFinishLoading() { +} + +void FolderModel::onFilesAdded(const Fm::FileInfoList& files) { + int n_files = files.size(); + beginInsertRows(QModelIndex(), items.count(), items.count() + n_files - 1); + for(auto& info : files) { + FolderModelItem item(info); + /* + if(fm_file_info_is_hidden(info)) { + model->hiddenItems.append(item); + continue; + } + */ + items.append(item); + } + endInsertRows(); +} + +void FolderModel::onFilesChanged(std::vector& files) { + for(auto& change : files) { + int row; + auto& oldInfo = change.first; + auto& newInfo = change.second; + QList::iterator it = findItemByFileInfo(oldInfo.get(), &row); + if(it != items.end()) { + FolderModelItem& item = *it; + // try to update the item + item.info = newInfo; + item.thumbnails.clear(); + QModelIndex index = createIndex(row, 0, &item); + Q_EMIT dataChanged(index, index); + if(oldInfo->size() != newInfo->size()) { + Q_EMIT fileSizeChanged(index); + } + } + } +} + +void FolderModel::onFilesRemoved(const Fm::FileInfoList& files) { + for(auto& info : files) { + int row; + QList::iterator it = findItemByName(info->name().c_str(), &row); + if(it != items.end()) { + beginRemoveRows(QModelIndex(), row, row); + items.erase(it); + endRemoveRows(); + } + } +} + +void FolderModel::loadPendingThumbnails() { + hasPendingThumbnailHandler_ = false; + for(auto& item: thumbnailData_) { + if(!item.pendingThumbnails_.empty()) { + auto job = new Fm::ThumbnailJob(std::move(item.pendingThumbnails_), item.size_); + pendingThumbnailJobs_.push_back(job); + job->setAutoDelete(true); + connect(job, &Fm::ThumbnailJob::thumbnailLoaded, this, &FolderModel::onThumbnailLoaded, Qt::BlockingQueuedConnection); + connect(job, &Fm::ThumbnailJob::finished, this, &FolderModel::onThumbnailJobFinished, Qt::BlockingQueuedConnection); + Fm::ThumbnailJob::threadPool()->start(job); + } + } +} + +void FolderModel::queueLoadThumbnail(const std::shared_ptr& file, int size) { + auto it = std::find_if(thumbnailData_.begin(), thumbnailData_.end(), [size](ThumbnailData& item){return item.size_ == size;}); + if(it != thumbnailData_.end()) { + it->pendingThumbnails_.push_back(file); + if(!hasPendingThumbnailHandler_) { + QTimer::singleShot(0, this, &FolderModel::loadPendingThumbnails); + hasPendingThumbnailHandler_ = true; + } + } +} + +void FolderModel::insertFiles(int row, const Fm::FileInfoList& files) { + int n_files = files.size(); + beginInsertRows(QModelIndex(), row, row + n_files - 1); + for(auto& info : files) { + FolderModelItem item(info); + items.append(item); + } + endInsertRows(); +} + +void FolderModel::setCutFiles(const QItemSelection& selection) { + if(folder_) { + if(!selection.isEmpty()) { + auto cutFilesHashSet = std::make_shared(); + folder_->setCutFiles(cutFilesHashSet); + for(const auto& index : selection.indexes()) { + auto item = itemFromIndex(index); + item->bindCutFiles(cutFilesHashSet); + cutFilesHashSet->insert(item->info->path().hash()); + } + } + Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0)); + } +} + +void FolderModel::removeAll() { + if(items.empty()) { + return; + } + beginRemoveRows(QModelIndex(), 0, items.size() - 1); + items.clear(); + endRemoveRows(); +} + +int FolderModel::rowCount(const QModelIndex& parent) const { + if(parent.isValid()) { + return 0; + } + return items.size(); +} + +int FolderModel::columnCount(const QModelIndex& parent = QModelIndex()) const { + if(parent.isValid()) { + return 0; + } + return NumOfColumns; +} + +FolderModelItem* FolderModel::itemFromIndex(const QModelIndex& index) const { + return reinterpret_cast(index.internalPointer()); +} + +std::shared_ptr FolderModel::fileInfoFromIndex(const QModelIndex& index) const { + FolderModelItem* item = itemFromIndex(index); + return item ? item->info : nullptr; +} + +QVariant FolderModel::data(const QModelIndex& index, int role/* = Qt::DisplayRole*/) const { + if(!index.isValid() || index.row() > items.size() || index.column() >= NumOfColumns) { + return QVariant(); + } + FolderModelItem* item = itemFromIndex(index); + auto info = item->info; + + bool isCut = false; + if(folder_ && Q_UNLIKELY(folder_->hasCutFiles())) { + isCut = item->isCut(); + } + + switch(role) { + case Qt::ToolTipRole: + return QVariant(item->displayName()); + case Qt::DisplayRole: { + switch(index.column()) { + case ColumnFileName: + return item->displayName(); + case ColumnFileType: + return QString(info->mimeType()->desc()); + case ColumnFileMTime: + return item->displayMtime(); + case ColumnFileSize: + return item->displaySize(); + case ColumnFileOwner: + return item->ownerName(); + } + break; + } + case Qt::DecorationRole: { + if(index.column() == 0) { + return QVariant(item->icon(isCut)); + } + break; + } + case Qt::EditRole: { + if(index.column() == 0) { + return QString::fromStdString(info->name()); + } + break; + } + case FileInfoRole: + return QVariant::fromValue(info); + case FileIsDirRole: + return QVariant(info->isDir()); + case FileIsCutRole: + return isCut; + } + return QVariant(); +} + +QVariant FolderModel::headerData(int section, Qt::Orientation orientation, int role/* = Qt::DisplayRole*/) const { + if(role == Qt::DisplayRole) { + if(orientation == Qt::Horizontal) { + QString title; + switch(section) { + case ColumnFileName: + title = tr("Name"); + break; + case ColumnFileType: + title = tr("Type"); + break; + case ColumnFileSize: + title = tr("Size"); + break; + case ColumnFileMTime: + title = tr("Modified"); + break; + case ColumnFileOwner: + title = tr("Owner"); + break; + } + return QVariant(title); + } + } + return QVariant(); +} + +QModelIndex FolderModel::index(int row, int column, const QModelIndex& /*parent*/) const { + if(row < 0 || row >= items.size() || column < 0 || column >= NumOfColumns) { + return QModelIndex(); + } + const FolderModelItem& item = items.at(row); + return createIndex(row, column, (void*)&item); +} + +QModelIndex FolderModel::parent(const QModelIndex& /*index*/) const { + return QModelIndex(); +} + +Qt::ItemFlags FolderModel::flags(const QModelIndex& index) const { + // FIXME: should not return same flags unconditionally for all columns + Qt::ItemFlags flags; + if(index.isValid()) { + flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable; + if(index.column() == ColumnFileName) { + flags |= (Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled + | Qt::ItemIsEditable); // inline renaming); + } + } + else { + flags = Qt::ItemIsDropEnabled; + } + return flags; +} + +// FIXME: this is very inefficient and should be replaced with a +// more reasonable implementation later. +QList::iterator FolderModel::findItemByPath(const Fm::FilePath& path, int* row) { + QList::iterator it = items.begin(); + int i = 0; + while(it != items.end()) { + FolderModelItem& item = *it; + auto item_path = item.info->path(); + if(item_path == path) { + *row = i; + return it; + } + ++it; + ++i; + } + return items.end(); +} + +// FIXME: this is very inefficient and should be replaced with a +// more reasonable implementation later. +QList::iterator FolderModel::findItemByName(const char* name, int* row) { + QList::iterator it = items.begin(); + int i = 0; + while(it != items.end()) { + FolderModelItem& item = *it; + if(item.info->name() == name) { + *row = i; + return it; + } + ++it; + ++i; + } + return items.end(); +} + +QList< FolderModelItem >::iterator FolderModel::findItemByFileInfo(const Fm::FileInfo* info, int* row) { + QList::iterator it = items.begin(); + int i = 0; + while(it != items.end()) { + FolderModelItem& item = *it; + if(item.info.get() == info) { + *row = i; + return it; + } + ++it; + ++i; + } + return items.end(); +} + +QStringList FolderModel::mimeTypes() const { + qDebug("FolderModel::mimeTypes"); + QStringList types = QAbstractItemModel::mimeTypes(); + // now types contains "application/x-qabstractitemmodeldatalist" + + // add support for freedesktop Xdnd direct save (XDS) protocol. + // http://www.freedesktop.org/wiki/Specifications/XDS/#index4h2 + // the real implementation is in FolderView::childDropEvent(). + types << "XdndDirectSave0"; + types << "text/uri-list"; + // types << "x-special/gnome-copied-files"; + return types; +} + +QMimeData* FolderModel::mimeData(const QModelIndexList& indexes) const { + QMimeData* data = QAbstractItemModel::mimeData(indexes); + qDebug("FolderModel::mimeData"); + // build a uri list + QByteArray urilist; + urilist.reserve(4096); + + for(const auto& index : indexes) { + FolderModelItem* item = itemFromIndex(index); + if(item && item->info) { + auto path = item->info->path(); + if(path.isValid()) { + auto uri = path.uri(); + urilist.append(uri.get()); + urilist.append('\n'); + } + } + } + data->setData("text/uri-list", urilist); + + return data; +} + +bool FolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) { + qDebug("FolderModel::dropMimeData"); + if(!folder_ || !data) { + return false; + } + Fm::FilePath destPath; + if(parent.isValid()) { // drop on an item + std::shared_ptr info; + if(row == -1 && column == -1) { + info = fileInfoFromIndex(parent); + } + else { + QModelIndex itemIndex = parent.child(row, column); + info = fileInfoFromIndex(itemIndex); + } + if(info) { + destPath = info->path(); + } + else { + return false; + } + } + else { // drop on blank area of the folder + destPath = path(); + } + + // FIXME: should we put this in dropEvent handler of FolderView instead? + if(data->hasUrls()) { + qDebug("drop action: %d", action); + auto srcPaths = pathListFromQUrls(data->urls()); + switch(action) { + case Qt::CopyAction: + FileOperation::copyFiles(srcPaths, destPath); + break; + case Qt::MoveAction: + FileOperation::moveFiles(srcPaths, destPath); + break; + case Qt::LinkAction: + FileOperation::symlinkFiles(srcPaths, destPath); + default: + return false; + } + return true; + } + else if(data->hasFormat("application/x-qabstractitemmodeldatalist")) { + return true; + } + return QAbstractListModel::dropMimeData(data, action, row, column, parent); +} + +Qt::DropActions FolderModel::supportedDropActions() const { + qDebug("FolderModel::supportedDropActions"); + return Qt::CopyAction | Qt::MoveAction | Qt::LinkAction; +} + +// ask the model to load thumbnails of the specified size +void FolderModel::cacheThumbnails(const int size) { + auto it = std::find_if(thumbnailData_.begin(), thumbnailData_.end(), [size](ThumbnailData& item){return item.size_ == size;}); + if(it != thumbnailData_.cend()) { + ++it->refCount_; + } + else { + thumbnailData_.push_front(ThumbnailData(size)); + } +} + +// ask the model to free cached thumbnails of the specified size +void FolderModel::releaseThumbnails(int size) { + auto prev = thumbnailData_.before_begin(); + for(auto it = thumbnailData_.begin(); it != thumbnailData_.end(); ++it) { + if(it->size_ == size) { + --it->refCount_; + if(it->refCount_ == 0) { + thumbnailData_.erase_after(prev); + } + + // remove all cached thumbnails of the specified size + QList::iterator itemIt; + for(itemIt = items.begin(); itemIt != items.end(); ++itemIt) { + FolderModelItem& item = *itemIt; + item.removeThumbnail(size); + } + break; + } + prev = it; + } +} + +void FolderModel::onThumbnailJobFinished() { + Fm::ThumbnailJob* job = static_cast(sender()); + auto it = std::find(pendingThumbnailJobs_.cbegin(), pendingThumbnailJobs_.cend(), job); + if(it != pendingThumbnailJobs_.end()) { + pendingThumbnailJobs_.erase(it); + } +} + +void FolderModel::onThumbnailLoaded(const std::shared_ptr& file, int size, const QImage& image) { + // find the model item this thumbnail belongs to + int row; + QList::iterator it = findItemByFileInfo(file.get(), &row); + if(it != items.end()) { + // the file is found in our model + FolderModelItem& item = *it; + QModelIndex index = createIndex(row, 0, (void*)&item); + // store the image in the folder model item. + FolderModelItem::Thumbnail* thumbnail = item.findThumbnail(size, false); + thumbnail->image = image; + thumbnail->transparent = false; + // qDebug("thumbnail loaded for: %s, size: %d", item.displayName.toUtf8().constData(), size); + if(image.isNull()) { + thumbnail->status = FolderModelItem::ThumbnailFailed; + } + else { + thumbnail->status = FolderModelItem::ThumbnailLoaded; + thumbnail->image = image; + + // tell the world that we have the thumbnail loaded + Q_EMIT thumbnailLoaded(index, size); + } + } +} + +// get a thumbnail of size at the index +// if a thumbnail is not yet loaded, this will initiate loading of the thumbnail. +QImage FolderModel::thumbnailFromIndex(const QModelIndex& index, int size) { + FolderModelItem* item = itemFromIndex(index); + if(item) { + FolderModelItem::Thumbnail* thumbnail = item->findThumbnail(size, item->isCut()); + // qDebug("FolderModel::thumbnailFromIndex: %d, %s", thumbnail->status, item->displayName.toUtf8().data()); + switch(thumbnail->status) { + case FolderModelItem::ThumbnailNotChecked: { + // load the thumbnail + queueLoadThumbnail(item->info, size); + thumbnail->status = FolderModelItem::ThumbnailLoading; + break; + } + case FolderModelItem::ThumbnailLoaded: + return thumbnail->image; + default: + ; + } + } + return QImage(); +} + + +} // namespace Fm diff --git a/src/foldermodel.h b/src/foldermodel.h new file mode 100644 index 0000000..3fd34c7 --- /dev/null +++ b/src/foldermodel.h @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_FOLDERMODEL_H +#define FM_FOLDERMODEL_H + +#include "libfmqtglobals.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "foldermodelitem.h" + +#include "core/folder.h" +#include "core/thumbnailjob.h" + +namespace Fm { + +class LIBFM_QT_API FolderModel : public QAbstractListModel { + Q_OBJECT +public: + + enum Role { + FileInfoRole = Qt::UserRole, + FileIsDirRole, + FileIsCutRole + }; + + enum ColumnId { + ColumnFileName, + ColumnFileType, + ColumnFileSize, + ColumnFileMTime, + ColumnFileOwner, + NumOfColumns + }; + +public: + explicit FolderModel(); + virtual ~FolderModel(); + + const std::shared_ptr& folder() const { + return folder_; + } + + void setFolder(const std::shared_ptr& new_folder); + + Fm::FilePath path() { + return folder_ ? folder_->path() : Fm::FilePath(); + } + + int rowCount(const QModelIndex& parent = QModelIndex()) const; + int columnCount(const QModelIndex& parent) const; + QVariant data(const QModelIndex& index, int role) const; + QVariant headerData(int section, Qt::Orientation orientation, int role) const; + QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const; + QModelIndex parent(const QModelIndex& index) const; + // void sort(int column, Qt::SortOrder order = Qt::AscendingOrder); + + Qt::ItemFlags flags(const QModelIndex& index) const; + + virtual QStringList mimeTypes() const; + virtual QMimeData* mimeData(const QModelIndexList& indexes) const; + virtual Qt::DropActions supportedDropActions() const; + virtual bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent); + + std::shared_ptr fileInfoFromIndex(const QModelIndex& index) const; + FolderModelItem* itemFromIndex(const QModelIndex& index) const; + QImage thumbnailFromIndex(const QModelIndex& index, int size); + + void cacheThumbnails(int size); + void releaseThumbnails(int size); + + void setCutFiles(const QItemSelection& selection); + +Q_SIGNALS: + void thumbnailLoaded(const QModelIndex& index, int size); + void fileSizeChanged(const QModelIndex& index); + +protected Q_SLOTS: + + void onStartLoading(); + void onFinishLoading(); + void onFilesAdded(const Fm::FileInfoList& files); + void onFilesChanged(std::vector& files); + void onFilesRemoved(const Fm::FileInfoList& files); + + void onThumbnailLoaded(const std::shared_ptr& file, int size, const QImage& image); + void onThumbnailJobFinished(); + void loadPendingThumbnails(); + +protected: + void queueLoadThumbnail(const std::shared_ptr& file, int size); + void insertFiles(int row, const Fm::FileInfoList& files); + void removeAll(); + QList::iterator findItemByPath(const Fm::FilePath& path, int* row); + QList::iterator findItemByName(const char* name, int* row); + QList::iterator findItemByFileInfo(const Fm::FileInfo* info, int* row); + +private: + + struct ThumbnailData { + ThumbnailData(int size): + size_{size}, + refCount_{1} { + } + + int size_; + int refCount_; + Fm::FileInfoList pendingThumbnails_; + }; + + std::shared_ptr folder_; + QList items; + + bool hasPendingThumbnailHandler_; + std::vector pendingThumbnailJobs_; + std::forward_list thumbnailData_; +}; + +} + +#endif // FM_FOLDERMODEL_H diff --git a/src/foldermodelitem.cpp b/src/foldermodelitem.cpp new file mode 100644 index 0000000..273fca1 --- /dev/null +++ b/src/foldermodelitem.cpp @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "foldermodelitem.h" +#include +#include +#include "utilities.h" +#include "core/userinfocache.h" + +namespace Fm { + +FolderModelItem::FolderModelItem(const std::shared_ptr& _info): + info{_info} { + thumbnails.reserve(2); +} + +FolderModelItem::FolderModelItem(const FolderModelItem& other): + info{other.info}, + thumbnails{other.thumbnails} { +} + +FolderModelItem::~FolderModelItem() { +} + +QString FolderModelItem::ownerName() const { + QString name; + auto user = Fm::UserInfoCache::globalInstance()->userFromId(info->uid()); + if(user) { + name = user->realName(); + if(name.isEmpty()) { + name = user->name(); + } + } + return name; +} + +QString FolderModelItem::ownerGroup() const { + auto group = Fm::UserInfoCache::globalInstance()->groupFromId(info->gid()); + return group ? group->name() : QString(); +} + +const QString &FolderModelItem::displayMtime() const { + if(dispMtime_.isEmpty()) { + auto mtime = QDateTime::fromMSecsSinceEpoch(info->mtime() * 1000); + dispMtime_ = mtime.toString(Qt::SystemLocaleShortDate); + } + return dispMtime_; +} + +const QString& FolderModelItem::displaySize() const { + if(!info->isDir()) { + // FIXME: choose IEC or SI units + dispSize_ = Fm::formatFileSize(info->size(), false); + } + return dispSize_; +} + +bool FolderModelItem::isCut() const { + return !cutFilesHashSet_.expired() || info->isCut(); +} + +void FolderModelItem::bindCutFiles(const std::shared_ptr& cutFilesHashSet) { + cutFilesHashSet_ = cutFilesHashSet; +} + +// find thumbnail of the specified size +// The returned thumbnail item is temporary and short-lived +// If you need to use the struct later, copy it to your own struct to keep it. +FolderModelItem::Thumbnail* FolderModelItem::findThumbnail(int size, bool transparent) { + QVector::iterator it; + Thumbnail* transThumb = nullptr; + for(it = thumbnails.begin(); it != thumbnails.end(); ++it) { + if(it->size == size) { + if(it->status != ThumbnailLoaded) { + return it; + } + else { // it->status == ThumbnailLoaded + if(it->transparent == false && transparent == true + && size < 48 /* (dirty) needed only for 'compact' and 'details list' view */ ) { + transThumb = it; // save thumb to add transparency later + } + else { + return it; // an image of the same size and transparency is found + } + } + } + } + if(transThumb) { + QImage image(transThumb->image); + + if(!image.hasAlphaChannel()) { + image = image.convertToFormat(QImage::Format_ARGB32); + } + + // add transparency to image + QPainter p; + p.begin(&image); + p.setCompositionMode(QPainter::CompositionMode_DestinationIn); + p.fillRect(image.rect(), QColor(0, 0, 0, 115 /* alpha 45% */)); + p.end(); + + // add image to thumbnails + Thumbnail thumbnail; + thumbnail.status = ThumbnailLoaded; + thumbnail.image = image; + thumbnail.size = size; + thumbnail.transparent = true; + thumbnails.append(thumbnail); + } + else if(it == thumbnails.end()) { + Thumbnail thumbnail; + thumbnail.status = ThumbnailNotChecked; + thumbnail.size = size; + thumbnail.transparent = false; + thumbnails.append(thumbnail); + } + return &thumbnails.back(); +} + +// remove cached thumbnail of the specified size +void FolderModelItem::removeThumbnail(int size) { + QVector::iterator it; + for(it = thumbnails.begin(); it != thumbnails.end(); ++it) { + if(it->size == size) { // an image of the same size is found + thumbnails.erase(it); + break; + } + } +} + + +} // namespace Fm diff --git a/src/foldermodelitem.h b/src/foldermodelitem.h new file mode 100644 index 0000000..d0bcee6 --- /dev/null +++ b/src/foldermodelitem.h @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_FOLDERMODELITEM_H +#define FM_FOLDERMODELITEM_H + +#include "libfmqtglobals.h" +#include +#include +#include +#include +#include +#include "icontheme.h" + +#include "core/folder.h" + +namespace Fm { + +class LIBFM_QT_API FolderModelItem { +public: + + enum ThumbnailStatus { + ThumbnailNotChecked, + ThumbnailLoading, + ThumbnailLoaded, + ThumbnailFailed + }; + + struct Thumbnail { + int size; + bool transparent; + ThumbnailStatus status; + QImage image; + }; + +public: + explicit FolderModelItem(const std::shared_ptr& _info); + FolderModelItem(const FolderModelItem& other); + virtual ~FolderModelItem(); + + const QString& displayName() const { + return info->displayName(); + } + + QIcon icon(bool transparent = false) const { + const auto i = info->icon(); + return i ? i->qicon(transparent) : QIcon{}; + } + + QString ownerName() const; + + QString ownerGroup() const; + + const QString& displayMtime() const; + + const QString &displaySize() const; + + bool isCut() const; + + void bindCutFiles(const std::shared_ptr& cutFilesHashSet); + + Thumbnail* findThumbnail(int size, bool transparent); + + void removeThumbnail(int size); + + std::shared_ptr info; + mutable QString dispMtime_; + mutable QString dispSize_; + std::weak_ptr cutFilesHashSet_; + QVector thumbnails; +}; + +} + +#endif // FM_FOLDERMODELITEM_H diff --git a/src/folderview.cpp b/src/folderview.cpp new file mode 100644 index 0000000..c2166f9 --- /dev/null +++ b/src/folderview.cpp @@ -0,0 +1,1242 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "folderview.h" +#include "foldermodel.h" +#include +#include +#include +#include "proxyfoldermodel.h" +#include "folderitemdelegate.h" +#include "dndactionmenu.h" +#include "filemenu.h" +#include "foldermenu.h" +#include "filelauncher.h" +#include "utilities.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // for XDS support +#include // for XDS support +#include "xdndworkaround.h" // for XDS support +#include "path.h" +#include "folderview_p.h" +#include "utilities.h" + +Q_DECLARE_OPAQUE_POINTER(FmFileInfo*) + +using namespace Fm; + +FolderViewListView::FolderViewListView(QWidget* parent): + QListView(parent), + activationAllowed_(true) { + connect(this, &QListView::activated, this, &FolderViewListView::activation); + // inline renaming + setEditTriggers(QAbstractItemView::NoEditTriggers); +} + +FolderViewListView::~FolderViewListView() { +} + +void FolderViewListView::startDrag(Qt::DropActions supportedActions) { + if(movement() != Static) { + QListView::startDrag(supportedActions); + } + else { + QAbstractItemView::startDrag(supportedActions); + } +} + +void FolderViewListView::mousePressEvent(QMouseEvent* event) { + QListView::mousePressEvent(event); + static_cast(parent())->childMousePressEvent(event); +} + +void FolderViewListView::mouseMoveEvent(QMouseEvent* event) { + // NOTE: Filter the BACK & FORWARD buttons to not Drag & Drop with them. + // (by default Qt views drag with any button) + if (event->buttons() == Qt::NoButton || event->buttons() & ~(Qt::BackButton | Qt::ForwardButton)) + QListView::mouseMoveEvent(event); +} + +QModelIndex FolderViewListView::indexAt(const QPoint& point) const { + QModelIndex index = QListView::indexAt(point); + // NOTE: QListView has a severe design flaw here. It does hit-testing based on the + // total bound rect of the item. The width of an item is determined by max(icon_width, text_width). + // So if the text label is much wider than the icon, when you click outside the icon but + // the point is still within the outer bound rect, the item is still selected. + // This results in very poor usability. Let's do precise hit-testing here. + // An item is hit only when the point is in the icon or text label. + // If the point is in the bound rectangle but outside the icon or text, it should not be selected. + if(viewMode() == QListView::IconMode && index.isValid()) { + QRect visRect = visualRect(index); // visible area on the screen + FolderItemDelegate* delegate = static_cast(itemDelegateForColumn(FolderModel::ColumnFileName)); + QSize margins = delegate->getMargins(); + QSize _iconSize = iconSize(); + if(point.y() < visRect.top() + margins.height()) { // above icon + return QModelIndex(); + } + else if(point.y() < visRect.top() + margins.height() + _iconSize.height()) { // on the icon area + int iconXMargin = (visRect.width() - _iconSize.width()) / 2; + if(point.x() < (visRect.left() + iconXMargin) || point.x() > (visRect.right() + 1 - iconXMargin)) { + // to the left or right of the icon + return QModelIndex(); + } + } + else { + QSize _textSize = delegate->iconViewTextSize(index); + int textHMargin = (visRect.width() - _textSize.width()) / 2; + if(point.y() > visRect.top() + margins.height() + _iconSize.height() + _textSize.height() // below text + // on the text area but to the left or right of the text + || point.x() < visRect.left() + textHMargin || point.x() > visRect.right() + 1 - textHMargin) { + return QModelIndex(); + } + } + // qDebug() << "visualRect: " << visRect << "point:" << point; + } + return index; +} + + +// NOTE: +// QListView has a problem which I consider a bug or a design flaw. +// When you set movement property to Static, theoratically the icons +// should not be movable. However, if you turned on icon mode, +// the icons becomes freely movable despite the value of movement is Static. +// To overcome this bug, we override all drag handling methods, and +// call QAbstractItemView directly, bypassing QListView. +// In this way, we can workaround the buggy behavior. +// The drag handlers of QListView basically does the same things +// as its parent QAbstractItemView, but it also stores the currently +// dragged item and paint them in the view as needed. +// TODO: I really should file a bug report to Qt developers. + +void FolderViewListView::dragEnterEvent(QDragEnterEvent* event) { + if(movement() != Static) { + QListView::dragEnterEvent(event); + } + else { + QAbstractItemView::dragEnterEvent(event); + } + qDebug("dragEnterEvent"); + //static_cast(parent())->childDragEnterEvent(event); +} + +void FolderViewListView::dragLeaveEvent(QDragLeaveEvent* e) { + if(movement() != Static) { + QListView::dragLeaveEvent(e); + } + else { + QAbstractItemView::dragLeaveEvent(e); + } + static_cast(parent())->childDragLeaveEvent(e); +} + +void FolderViewListView::dragMoveEvent(QDragMoveEvent* e) { + if(movement() != Static) { + QListView::dragMoveEvent(e); + } + else { + QAbstractItemView::dragMoveEvent(e); + } + static_cast(parent())->childDragMoveEvent(e); +} + +void FolderViewListView::dropEvent(QDropEvent* e) { + + static_cast(parent())->childDropEvent(e); + + if(movement() != Static) { + QListView::dropEvent(e); + } + else { + QAbstractItemView::dropEvent(e); + } +} + +void FolderViewListView::mouseReleaseEvent(QMouseEvent* event) { + bool activationWasAllowed = activationAllowed_; + if((!style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, this)) || (event->button() != Qt::LeftButton)) { + activationAllowed_ = false; + } + + QListView::mouseReleaseEvent(event); + + activationAllowed_ = activationWasAllowed; +} + +void FolderViewListView::mouseDoubleClickEvent(QMouseEvent* event) { + bool activationWasAllowed = activationAllowed_; + if((style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, this)) || (event->button() != Qt::LeftButton)) { + activationAllowed_ = false; + } + + QListView::mouseDoubleClickEvent(event); + + activationAllowed_ = activationWasAllowed; +} + +QModelIndex FolderViewListView::moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers) { + QAbstractItemModel* model_ = model(); + + if(model_ && currentIndex().isValid()) { + FolderView::ViewMode viewMode = static_cast(parent())->viewMode(); + if((viewMode == FolderView::IconMode) || (viewMode == FolderView::ThumbnailMode)) { + int next = (layoutDirection() == Qt::RightToLeft) ? - 1 : 1; + + if(cursorAction == QAbstractItemView::MoveRight) { + return model_->index(currentIndex().row() + next, 0); + } + else if(cursorAction == QAbstractItemView::MoveLeft) { + return model_->index(currentIndex().row() - next, 0); + } + } + } + + return QListView::moveCursor(cursorAction, modifiers); +} + +void FolderViewListView::activation(const QModelIndex& index) { + if(activationAllowed_) { + Q_EMIT activatedFiltered(index); + } +} + +//----------------------------------------------------------------------------- + +FolderViewTreeView::FolderViewTreeView(QWidget* parent): + QTreeView(parent), + doingLayout_(false), + layoutTimer_(nullptr), + activationAllowed_(true) { + + header()->setStretchLastSection(true); + setIndentation(0); + /* the default true value may cause a crash on entering a folder + by double clicking because of the viewport update done by + QTreeView::mouseDoubleClickEvent() (a Qt bug?) */ + setExpandsOnDoubleClick(false); + + connect(this, &QTreeView::activated, this, &FolderViewTreeView::activation); + // don't open editor on double clicking + setEditTriggers(QAbstractItemView::NoEditTriggers); +} + +FolderViewTreeView::~FolderViewTreeView() { + if(layoutTimer_) { + delete layoutTimer_; + } +} + +void FolderViewTreeView::setModel(QAbstractItemModel* model) { + QTreeView::setModel(model); + layoutColumns(); + if(ProxyFolderModel* proxyModel = qobject_cast(model)) { + connect(proxyModel, &ProxyFolderModel::sortFilterChanged, this, &FolderViewTreeView::onSortFilterChanged, + Qt::UniqueConnection); + onSortFilterChanged(); + } +} + +void FolderViewTreeView::mousePressEvent(QMouseEvent* event) { + QTreeView::mousePressEvent(event); + static_cast(parent())->childMousePressEvent(event); +} + +void FolderViewTreeView::mouseMoveEvent(QMouseEvent* event) { + // NOTE: Filter the BACK & FORWARD buttons to not Drag & Drop with them. + // (by default Qt views drag with any button) + if (event->buttons() == Qt::NoButton || event->buttons() & ~(Qt::BackButton | Qt::ForwardButton)) + QTreeView::mouseMoveEvent(event); +} + +void FolderViewTreeView::dragEnterEvent(QDragEnterEvent* event) { + QTreeView::dragEnterEvent(event); + //static_cast(parent())->childDragEnterEvent(event); +} + +void FolderViewTreeView::dragLeaveEvent(QDragLeaveEvent* e) { + QTreeView::dragLeaveEvent(e); + static_cast(parent())->childDragLeaveEvent(e); +} + +void FolderViewTreeView::dragMoveEvent(QDragMoveEvent* e) { + QTreeView::dragMoveEvent(e); + static_cast(parent())->childDragMoveEvent(e); +} + +void FolderViewTreeView::dropEvent(QDropEvent* e) { + static_cast(parent())->childDropEvent(e); + QTreeView::dropEvent(e); +} + +// the default list mode of QListView handles column widths +// very badly (worse than gtk+) and it's not very flexible. +// so, let's handle column widths outselves. +void FolderViewTreeView::layoutColumns() { + // qDebug("layoutColumns"); + if(!model()) { + return; + } + doingLayout_ = true; + QHeaderView* headerView = header(); + // the width that's available for showing the columns. + int availWidth = viewport()->contentsRect().width(); + + // get the width that every column want + int numCols = headerView->count(); + if(numCols > 0) { + int desiredWidth = 0; + int* widths = new int[numCols]; // array to store the widths every column needs + QStyleOptionHeader opt; + opt.initFrom(headerView); + opt.fontMetrics = QFontMetrics(font()); + if (headerView->isSortIndicatorShown()) { + opt.sortIndicator = QStyleOptionHeader::SortDown; + } + QAbstractItemModel* model_ = model(); + int column; + for(column = 0; column < numCols; ++column) { + int columnId = headerView->logicalIndex(column); + // get the size that the column needs + if(model_) { + QVariant data = model_->headerData(columnId, Qt::Horizontal, Qt::DisplayRole); + if(data.isValid()) { + opt.text = data.isValid() ? data.toString() : QString(); + } + } + opt.section = columnId; + widths[column] = qMax(sizeHintForColumn(columnId), + style()->sizeFromContents(QStyle::CT_HeaderSection, &opt, QSize(), headerView).width()); + // compute the total width needed + desiredWidth += widths[column]; + } + + int filenameColumn = headerView->visualIndex(FolderModel::ColumnFileName); + // if the total witdh we want exceeds the available space + if(desiredWidth > availWidth) { + // Compute the width available for the filename column + int filenameAvailWidth = availWidth - desiredWidth + widths[filenameColumn]; + + // Compute the minimum acceptable width for the filename column + int filenameMinWidth = qMin(200, sizeHintForColumn(filenameColumn)); + + if(filenameAvailWidth > filenameMinWidth) { + // Shrink the filename column to the available width + widths[filenameColumn] = filenameAvailWidth; + } + else { + // Set the filename column to its minimum width + widths[filenameColumn] = filenameMinWidth; + } + } + else { + // Fill the extra available space with the filename column + widths[filenameColumn] += availWidth - desiredWidth; + } + + // really do the resizing for every column + for(int column = 0; column < numCols; ++column) { + headerView->resizeSection(headerView->logicalIndex(column), widths[column]); + } + delete []widths; + } + doingLayout_ = false; + + if(layoutTimer_) { + delete layoutTimer_; + layoutTimer_ = nullptr; + } +} + +void FolderViewTreeView::resizeEvent(QResizeEvent* event) { + QAbstractItemView::resizeEvent(event); + // prevent endless recursion. + // When manually resizing columns, at the point where a horizontal scroll + // bar has to be inserted or removed, the vertical size changes, a resize + // event occurs and the column headers are flickering badly if the column + // layout is modified at this point. Therefore only layout the columns if + // the horizontal size changes. + if(!doingLayout_ && event->size().width() != event->oldSize().width()) { + layoutColumns(); // layoutColumns() also triggers resizeEvent + } +} + +void FolderViewTreeView::rowsInserted(const QModelIndex& parent, int start, int end) { + QTreeView::rowsInserted(parent, start, end); + queueLayoutColumns(); +} + +void FolderViewTreeView::rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { + QTreeView::rowsAboutToBeRemoved(parent, start, end); + queueLayoutColumns(); +} + +void FolderViewTreeView::dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector& roles /*= QVector{}*/) { + QTreeView::dataChanged(topLeft, bottomRight, roles); + // FIXME: this will be very inefficient + // queueLayoutColumns(); +} + +void FolderViewTreeView::reset() { + // Sometimes when the content of the model is radically changed, Qt does reset() + // on the model rather than doing large amount of insertion and deletion. + // This is for performance reason so in this case rowsInserted() and rowsAboutToBeRemoved() + // might not be called. Hence we also have to re-layout the columns when the model is reset. + // This fixes bug #190 + // https://github.com/lxde/pcmanfm-qt/issues/190 + QTreeView::reset(); + queueLayoutColumns(); +} + +void FolderViewTreeView::queueLayoutColumns() { + // qDebug("queueLayoutColumns"); + if(!layoutTimer_) { + layoutTimer_ = new QTimer(); + layoutTimer_->setSingleShot(true); + layoutTimer_->setInterval(0); + connect(layoutTimer_, &QTimer::timeout, this, &FolderViewTreeView::layoutColumns); + } + layoutTimer_->start(); +} + +void FolderViewTreeView::mouseReleaseEvent(QMouseEvent* event) { + bool activationWasAllowed = activationAllowed_; + if((!style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, this)) || (event->button() != Qt::LeftButton)) { + activationAllowed_ = false; + } + + QTreeView::mouseReleaseEvent(event); + + activationAllowed_ = activationWasAllowed; +} + +void FolderViewTreeView::mouseDoubleClickEvent(QMouseEvent* event) { + bool activationWasAllowed = activationAllowed_; + if((style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, this)) || (event->button() != Qt::LeftButton)) { + activationAllowed_ = false; + } + + QTreeView::mouseDoubleClickEvent(event); + + activationAllowed_ = activationWasAllowed; +} + +void FolderViewTreeView::activation(const QModelIndex& index) { + if(activationAllowed_) { + Q_EMIT activatedFiltered(index); + } +} + +void FolderViewTreeView::onSortFilterChanged() { + if(QSortFilterProxyModel* proxyModel = qobject_cast(model())) { + header()->setSortIndicatorShown(true); + header()->setSortIndicator(proxyModel->sortColumn(), proxyModel->sortOrder()); + if(!isSortingEnabled()) { + setSortingEnabled(true); + } + } +} + + +//----------------------------------------------------------------------------- + +FolderView::FolderView(FolderView::ViewMode _mode, QWidget *parent): + QWidget(parent), + view(nullptr), + model_(nullptr), + mode((ViewMode)0), + fileLauncher_(nullptr), + autoSelectionDelay_(600), + autoSelectionTimer_(nullptr), + selChangedTimer_(nullptr), + itemDelegateMargins_(QSize(3, 3)) { + + iconSize_[IconMode - FirstViewMode] = QSize(48, 48); + iconSize_[CompactMode - FirstViewMode] = QSize(24, 24); + iconSize_[ThumbnailMode - FirstViewMode] = QSize(128, 128); + iconSize_[DetailedListMode - FirstViewMode] = QSize(24, 24); + + QVBoxLayout* layout = new QVBoxLayout(); + layout->setMargin(0); + setLayout(layout); + + setViewMode(_mode); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + connect(this, &FolderView::clicked, this, &FolderView::onFileClicked); + connect(QApplication::clipboard(), &QClipboard::dataChanged, this, &FolderView::onClipboardDataChange); +} + +FolderView::~FolderView() { +} + +void FolderView::onItemActivated(QModelIndex index) { + if(index.isValid() && index.model()) { + QVariant data = index.model()->data(index, FolderModel::FileInfoRole); + auto info = data.value>(); + if(info) { + if(!(QApplication::keyboardModifiers() & (Qt::ShiftModifier | Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier))) { + Q_EMIT clicked(ActivatedClick, info); + } + } + } +} + +void FolderView::onSelChangedTimeout() { + selChangedTimer_->deleteLater(); + selChangedTimer_ = nullptr; + // qDebug()<<"selected:" << nSel; + Q_EMIT selChanged(); +} + +void FolderView::onSelectionChanged(const QItemSelection& /*selected*/, const QItemSelection& /*deselected*/) { + // It's possible that the selected items change too often and this slot gets called for thousands of times. + // For example, when you select thousands of files and delete them, we will get one selectionChanged() event + // for every deleted file. So, we use a timer to delay the handling to avoid too frequent updates of the UI. + if(!selChangedTimer_) { + selChangedTimer_ = new QTimer(this); + selChangedTimer_->setSingleShot(true); + connect(selChangedTimer_, &QTimer::timeout, this, &FolderView::onSelChangedTimeout); + selChangedTimer_->start(200); + } +} + +void FolderView::onClosingEditor(QWidget* editor, QAbstractItemDelegate::EndEditHint hint) { + if (hint != QAbstractItemDelegate::NoHint) { + // we set the hint to NoHint in FolderItemDelegate::eventFilter() + return; + } + QString newName; + if (qobject_cast(editor)) { // icon and thumbnail view + newName = qobject_cast(editor)->toPlainText(); + } + else if (qobject_cast(editor)) { // compact view + newName = qobject_cast(editor)->text(); + } + if (newName.isEmpty()) { + return; + } + // the editor will be deleted by QAbstractItemDelegate::destroyEditor() when no longer needed + + QModelIndex index = view->selectionModel()->currentIndex(); + if(index.isValid() && index.model()) { + QVariant data = index.model()->data(index, FolderModel::FileInfoRole); + auto info = data.value>(); + if (info) { + auto oldName = QString::fromStdString(info->name()); + if(newName == oldName) { + return; + } + QWidget* parent = window(); + if (window() == this) { // supposedly desktop, in case it uses this + parent = nullptr; + } + changeFileName(info->path(), newName, parent); + } + } +} + +void FolderView::setViewMode(ViewMode _mode) { + if(_mode == mode) { // if it's the same more, ignore + return; + } + // FIXME: retain old selection + + // since only detailed list mode uses QTreeView, and others + // all use QListView, it's wise to preserve QListView when possible. + bool recreateView = false; + if(view && (mode == DetailedListMode || _mode == DetailedListMode)) { + delete view; // FIXME: no virtual dtor? + view = nullptr; + recreateView = true; + } + mode = _mode; + QSize iconSize = iconSize_[mode - FirstViewMode]; + + FolderItemDelegate* delegate = nullptr; + if(mode == DetailedListMode) { + FolderViewTreeView* treeView = new FolderViewTreeView(this); + connect(treeView, &FolderViewTreeView::activatedFiltered, this, &FolderView::onItemActivated); + setFocusProxy(treeView); + + view = treeView; + treeView->setItemsExpandable(false); + treeView->setRootIsDecorated(false); + treeView->setAllColumnsShowFocus(false); + + // set our own custom delegate + delegate = new FolderItemDelegate(treeView); + treeView->setItemDelegateForColumn(FolderModel::ColumnFileName, delegate); + } + else { + FolderViewListView* listView; + if(view) { + listView = static_cast(view); + } + else { + listView = new FolderViewListView(this); + connect(listView, &FolderViewListView::activatedFiltered, this, &FolderView::onItemActivated); + view = listView; + } + setFocusProxy(listView); + + // set our own custom delegate + delegate = new FolderItemDelegate(listView); + listView->setItemDelegateForColumn(FolderModel::ColumnFileName, delegate); + // FIXME: should we expose the delegate? + listView->setMovement(QListView::Static); + /* If listView is already visible, setMovement() will lay out items again with delay + (see Qt, QListView::setMovement(), d->doDelayedItemsLayout()) and thus drop events + will remain disabled for the viewport. So, we should re-enable drop events here. */ + if(listView->viewport()->isVisible()) { + listView->viewport()->setAcceptDrops(true); + } + listView->setResizeMode(QListView::Adjust); + listView->setWrapping(true); + switch(mode) { + case IconMode: { + listView->setViewMode(QListView::IconMode); + listView->setWordWrap(true); + listView->setFlow(QListView::LeftToRight); + break; + } + case CompactMode: { + listView->setViewMode(QListView::ListMode); + listView->setWordWrap(false); + listView->setFlow(QListView::QListView::TopToBottom); + break; + } + case ThumbnailMode: { + listView->setViewMode(QListView::IconMode); + listView->setWordWrap(true); + listView->setFlow(QListView::LeftToRight); + break; + } + default: + ; + } + updateGridSize(); + } + if(view) { + // we have to install the event filter on the viewport instead of the view itself. + view->viewport()->installEventFilter(this); + // we want the QEvent::HoverMove event for single click + auto-selection support + view->viewport()->setAttribute(Qt::WA_Hover, true); + view->setContextMenuPolicy(Qt::NoContextMenu); // defer the context menu handling to parent widgets + view->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + view->setIconSize(iconSize); + + view->setSelectionMode(QAbstractItemView::ExtendedSelection); + layout()->addWidget(view); + + // enable dnd + view->setDragEnabled(true); + view->setAcceptDrops(true); + view->setDragDropMode(QAbstractItemView::DragDrop); + view->setDropIndicatorShown(true); + + // inline renaming + if(delegate) { + connect(delegate, &QAbstractItemDelegate::closeEditor, this, &FolderView::onClosingEditor); + } + + if(model_) { + // FIXME: preserve selections + model_->setThumbnailSize(iconSize.width()); + view->setModel(model_); + if(recreateView) { + connect(view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &FolderView::onSelectionChanged); + } + } + } +} + +// set proper grid size for the QListView based on current view mode, icon size, and font size. +void FolderView::updateGridSize() { + if(mode == DetailedListMode || !view) { + return; + } + FolderViewListView* listView = static_cast(view); + QSize icon = iconSize(mode); // size of the icon + QFontMetrics fm = fontMetrics(); // size of current font + QSize grid; // the final grid size + switch(mode) { + case IconMode: + case ThumbnailMode: { + // NOTE by PCMan about finding the optimal text label size: + // The average filename length on my root filesystem is roughly 18-20 chars. + // So, a reasonable size for the text label is about 10 chars each line since string of this length + // can be shown in two lines. If you consider word wrap, then the result is around 10 chars per word. + // In average, 10 char per line should be enough to display a "word" in the filename without breaking. + // The values can be estimated with this command: + // > find / | xargs basename -a | sed -e s'/[_-]/ /g' | wc -mcw + // However, this average only applies to English. For some Asian characters, such as Chinese chars, + // each char actually takes doubled space. To be safe, we use 13 chars per line x average char width + // to get a nearly optimal width for the text label. As most of the filenames have less than 40 chars + // 13 chars x 3 lines should be enough to show the full filenames for most files. + int textWidth = fm.averageCharWidth() * 13; + int textHeight = fm.lineSpacing() * 3; + grid.setWidth(qMax(icon.width(), textWidth) + 4); // a margin of 2 px for selection rects + grid.setHeight(icon.height() + textHeight + 4); // a margin of 2 px for selection rects + // grow to include margins + grid += 2*itemDelegateMargins_; + // let horizontal and vertical spacings be set only by itemDelegateMargins_ + listView->setSpacing(0); + + break; + } + default: + // FIXME: set proper item size + listView->setSpacing(2); + ; // do not use grid size + } + + FolderItemDelegate* delegate = static_cast(listView->itemDelegateForColumn(FolderModel::ColumnFileName)); + delegate->setItemSize(grid); + delegate->setIconSize(icon); + delegate->setMargins(itemDelegateMargins_); +} + +void FolderView::setIconSize(ViewMode mode, QSize size) { + Q_ASSERT(mode >= FirstViewMode && mode <= LastViewMode); + iconSize_[mode - FirstViewMode] = size; + if(viewMode() == mode) { + view->setIconSize(size); + if(model_) { + model_->setThumbnailSize(size.width()); + } + updateGridSize(); + } +} + +QSize FolderView::iconSize(ViewMode mode) const { + Q_ASSERT(mode >= FirstViewMode && mode <= LastViewMode); + return iconSize_[mode - FirstViewMode]; +} + +void FolderView::setMargins(QSize size) { + if(itemDelegateMargins_ != size.expandedTo(QSize(0, 0))) { + itemDelegateMargins_ = size.expandedTo(QSize(0, 0)); + updateGridSize(); + } +} + +FolderView::ViewMode FolderView::viewMode() const { + return mode; +} + +void FolderView::setAutoSelectionDelay(int delay) { + autoSelectionDelay_ = delay; +} + +QAbstractItemView* FolderView::childView() const { + return view; +} + +ProxyFolderModel* FolderView::model() const { + return model_; +} + +void FolderView::setModel(ProxyFolderModel* model) { + if(view) { + view->setModel(model); + QSize iconSize = iconSize_[mode - FirstViewMode]; + model->setThumbnailSize(iconSize.width()); + if(view->selectionModel()) { + connect(view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &FolderView::onSelectionChanged); + } + } + if(model_) { + delete model_; + } + model_ = model; +} + +bool FolderView::event(QEvent* event) { + switch(event->type()) { + case QEvent::StyleChange: + break; + case QEvent::FontChange: + updateGridSize(); + break; + default: + break; + } + return QWidget::event(event); +} + +void FolderView::contextMenuEvent(QContextMenuEvent* event) { + QWidget::contextMenuEvent(event); + QPoint pos = event->pos(); + QPoint view_pos = view->mapFromParent(pos); + QPoint viewport_pos = view->viewport()->mapFromParent(view_pos); + emitClickedAt(ContextMenuClick, viewport_pos); +} + +void FolderView::childMousePressEvent(QMouseEvent* event) { + // called from mousePressEvent() of child view + Qt::MouseButton button = event->button(); + if(button == Qt::MiddleButton) { + emitClickedAt(MiddleClick, event->pos()); + } + else if(button == Qt::BackButton) { + Q_EMIT clickedBack(); + } + else if(button == Qt::ForwardButton) { + Q_EMIT clickedForward(); + } +} + +void FolderView::emitClickedAt(ClickType type, const QPoint& pos) { + // indexAt() needs a point in "viewport" coordinates. + QModelIndex index = view->indexAt(pos); + if(index.isValid()) { + QVariant data = index.data(FolderModel::FileInfoRole); + auto info = data.value>(); + Q_EMIT clicked(type, info); + } + else { + // FIXME: should we show popup menu for the selected files instead + // if there are selected files? + if(type == ContextMenuClick) { + // clear current selection if clicked outside selected files + view->clearSelection(); + Q_EMIT clicked(type, nullptr); + } + } +} + +QModelIndexList FolderView::selectedRows(int column) const { + QItemSelectionModel* selModel = selectionModel(); + if(selModel) { + return selModel->selectedRows(column); + } + return QModelIndexList(); +} + +// This returns all selected "cells", which means all cells of the same row are returned. +QModelIndexList FolderView::selectedIndexes() const { + QItemSelectionModel* selModel = selectionModel(); + if(selModel) { + return selModel->selectedIndexes(); + } + return QModelIndexList(); +} + +QItemSelectionModel* FolderView::selectionModel() const { + return view ? view->selectionModel() : nullptr; +} + +Fm::FilePathList FolderView::selectedFilePaths() const { + if(model_) { + QModelIndexList selIndexes = mode == DetailedListMode ? selectedRows() : selectedIndexes(); + if(!selIndexes.isEmpty()) { + Fm::FilePathList paths; + QModelIndexList::const_iterator it; + for(it = selIndexes.constBegin(); it != selIndexes.constEnd(); ++it) { + auto file = model_->fileInfoFromIndex(*it); + paths.push_back(file->path()); + } + return paths; + } + } + return Fm::FilePathList(); +} + +bool FolderView::hasSelection() const { + QItemSelectionModel* selModel = selectionModel(); + return selModel ? selModel->hasSelection() : false; +} + +QModelIndex FolderView::indexFromFolderPath(const Fm::FilePath& folderPath) const { + if(!model_ || !folderPath.isValid()) { + return QModelIndex(); + } + QModelIndex index; + int count = model_->rowCount(); + for(int row = 0; row < count; ++row) { + index = model_->index(row, 0); + auto info = model_->fileInfoFromIndex(index); + if(info && info->isDir() && folderPath == info->path()) { + return index; + } + } + return QModelIndex(); +} + +Fm::FileInfoList FolderView::selectedFiles() const { + if(model_) { + QModelIndexList selIndexes = mode == DetailedListMode ? selectedRows() : selectedIndexes(); + if(!selIndexes.isEmpty()) { + Fm::FileInfoList files; + QModelIndexList::const_iterator it; + for(it = selIndexes.constBegin(); it != selIndexes.constEnd(); ++it) { + auto file = model_->fileInfoFromIndex(*it); + files.push_back(file); + } + return files; + } + } + return Fm::FileInfoList(); +} + +void FolderView::selectAll() { + if(mode == DetailedListMode) { + view->selectAll(); + } + else { + // NOTE: By default QListView::selectAll() selects all columns in the model. + // However, QListView only show the first column. Normal selection by mouse + // can only select the first column of every row. I consider this discripancy yet + // another design flaw of Qt. To make them consistent, we do it ourselves by only + // selecting the first column of every row and do not select all columns as Qt does. + // I'll report a Qt bug for this later. + if(model_) { + const QItemSelection sel{model_->index(0, 0), model_->index(model_->rowCount() - 1, 0)}; + selectionModel()->select(sel, QItemSelectionModel::Select); + } + } +} + +void FolderView::invertSelection() { + if(model_) { + QItemSelectionModel* selModel = view->selectionModel(); + int rows = model_->rowCount(); + QItemSelectionModel::SelectionFlags flags = QItemSelectionModel::Toggle; + if(mode == DetailedListMode) { + flags |= QItemSelectionModel::Rows; + } + for(int row = 0; row < rows; ++row) { + QModelIndex index = model_->index(row, 0); + selModel->select(index, flags); + } + } +} + +void FolderView::childDragEnterEvent(QDragEnterEvent* event) { + qDebug("drag enter"); + if(event->mimeData()->hasFormat("text/uri-list")) { + event->accept(); + } + else { + event->ignore(); + } +} + +void FolderView::childDragLeaveEvent(QDragLeaveEvent* e) { + qDebug("drag leave"); + e->accept(); +} + +void FolderView::childDragMoveEvent(QDragMoveEvent* /*e*/) { + qDebug("drag move"); +} + +void FolderView::childDropEvent(QDropEvent* e) { + // qDebug("drop"); + // Try to support XDS + // NOTE: in theory, it's not possible to implement XDS with pure Qt. + // We achieved this with some dirty XCB/XDND workarounds. + // Please refer to XdndWorkaround::clientMessage() in xdndworkaround.cpp for details. + if(QX11Info::isPlatformX11() && e->mimeData()->hasFormat("XdndDirectSave0")) { + e->setDropAction(Qt::CopyAction); + const QWidget* targetWidget = childView()->viewport(); + // these are dynamic QObject property set by our XDND workarounds in xdndworkaround.cpp. + xcb_window_t dndSource = xcb_window_t(targetWidget->property("xdnd::lastDragSource").toUInt()); + //xcb_timestamp_t dropTimestamp = (xcb_timestamp_t)targetWidget->property("xdnd::lastDropTime").toUInt(); + // qDebug() << "XDS: source window" << dndSource << dropTimestamp; + if(dndSource != 0) { + xcb_atom_t XdndDirectSaveAtom = XdndWorkaround::internAtom("XdndDirectSave0", 15); + xcb_atom_t textAtom = XdndWorkaround::internAtom("text/plain", 10); + + // 1. get the filename from XdndDirectSave property of the source window + QByteArray basename = XdndWorkaround::windowProperty(dndSource, XdndDirectSaveAtom, textAtom, 1024); + + // 2. construct the fill URI for the file, and update the source window property. + Fm::FilePath filePath; + if(model_) { + QModelIndex index = view->indexAt(e->pos()); + auto info = model_->fileInfoFromIndex(index); + if(info && info->isDir()) { + filePath = info->path().child(basename); + } + } + if(!filePath.isValid()) { + filePath = path().child(basename); + } + QByteArray fileUri = filePath.uri().get(); + XdndWorkaround::setWindowProperty(dndSource, XdndDirectSaveAtom, textAtom, (void*)fileUri.constData(), fileUri.length()); + + // 3. send to XDS selection data request with type "XdndDirectSave" to the source window and + // receive result from the source window. (S: success, E: error, or F: failure) + QByteArray result = e->mimeData()->data("XdndDirectSave0"); + // NOTE: there seems to be some bugs in file-roller so it always replies with "E" even if the + // file extraction is finished successfully. Anyways, we ignore any error at the moment. + } + e->accept(); // yeah! we've done with XDS so stop Qt from further event propagation. + return; + } + + if(e->keyboardModifiers() == Qt::NoModifier) { + // if no key modifiers are used, popup a menu + // to ask the user for the action he/she wants to perform. + Qt::DropAction action = DndActionMenu::askUser(e->possibleActions(), QCursor::pos()); + e->setDropAction(action); + } +} + +bool FolderView::eventFilter(QObject* watched, QEvent* event) { + // NOTE: Instead of simply filtering the drag and drop events of the child view in + // the event filter, we overrided each event handler virtual methods in + // both QListView and QTreeView and added some childXXXEvent() callbacks. + // We did this because of a design flaw of Qt. + // All QAbstractScrollArea derived widgets, including QAbstractItemView + // contains an internal child widget, which is called a viewport. + // The events actually comes from the child viewport, not the parent view itself. + // Qt redirects the events of viewport to the viewportEvent() method of + // QAbstractScrollArea and let the parent widget handle the events. + // Qt implemented this using a event filter installed on the child viewport widget. + // That means, when we try to install an event filter on the viewport, + // there is already a filter installed by Qt which will be called before ours. + // So we can never intercept the event handling of QAbstractItemView by using a filter. + // That's why we override respective virtual methods for different events. + if(view && watched == view->viewport()) { + switch(event->type()) { + case QEvent::HoverMove: + // activate items on single click + if(style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick)) { + QHoverEvent* hoverEvent = static_cast(event); + QModelIndex index = view->indexAt(hoverEvent->pos()); // find out the hovered item + if(index.isValid()) { // change the cursor to a hand when hovering on an item + setCursor(Qt::PointingHandCursor); + } + else { + setCursor(Qt::ArrowCursor); + } + // turn on auto-selection for hovered item when single click mode is used. + if(autoSelectionDelay_ > 0 && model_) { + if(!autoSelectionTimer_) { + autoSelectionTimer_ = new QTimer(this); + connect(autoSelectionTimer_, &QTimer::timeout, this, &FolderView::onAutoSelectionTimeout); + lastAutoSelectionIndex_ = QModelIndex(); + } + autoSelectionTimer_->start(autoSelectionDelay_); + } + break; + } + case QEvent::HoverLeave: + if(style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick)) { + setCursor(Qt::ArrowCursor); + } + break; + case QEvent::Wheel: + // don't let the view scroll during an inline renaming + if (view) { + FolderItemDelegate* delegate = nullptr; + if(mode == DetailedListMode) { + FolderViewTreeView* treeView = static_cast(view); + delegate = static_cast(treeView->itemDelegateForColumn(FolderModel::ColumnFileName)); + } + else { + FolderViewListView* listView = static_cast(view); + delegate = static_cast(listView->itemDelegateForColumn(FolderModel::ColumnFileName)); + } + if (delegate && delegate->hasEditor()) { + return true; + } + } + // This is to fix #85: Scrolling doesn't work in compact view + // Actually, I think it's the bug of Qt, not ours. + // When in compact mode, only the horizontal scroll bar is used and the vertical one is hidden. + // So, when a user scroll his mouse wheel, it's reasonable to scroll the horizontal scollbar. + // Qt does not implement such a simple feature, unfortunately. + // We do it by forwarding the scroll event in the viewport to the horizontal scrollbar. + // FIXME: if someday Qt supports this, we have to disable the workaround. + if(mode == CompactMode) { + QScrollBar* scroll = view->horizontalScrollBar(); + if(scroll) { + QApplication::sendEvent(scroll, event); + return true; + } + } + break; + default: + break; + } + } + return QObject::eventFilter(watched, event); +} + +// this slot handles auto-selection of items. +void FolderView::onAutoSelectionTimeout() { + if(QApplication::mouseButtons() != Qt::NoButton) { + return; + } + + Qt::KeyboardModifiers mods = QApplication::keyboardModifiers(); + QPoint pos = view->viewport()->mapFromGlobal(QCursor::pos()); // convert to viewport coordinates + QModelIndex index = view->indexAt(pos); // find out the hovered item + QItemSelectionModel::SelectionFlags flags = (mode == DetailedListMode ? QItemSelectionModel::Rows : QItemSelectionModel::NoUpdate); + QItemSelectionModel* selModel = view->selectionModel(); + + if(mods & Qt::ControlModifier) { // Ctrl key is pressed + if(selModel->isSelected(index) && index != lastAutoSelectionIndex_) { + // unselect a previously selected item + selModel->select(index, flags | QItemSelectionModel::Deselect); + lastAutoSelectionIndex_ = QModelIndex(); + } + else { + // select an unselected item + selModel->select(index, flags | QItemSelectionModel::Select); + lastAutoSelectionIndex_ = index; + } + selModel->setCurrentIndex(index, QItemSelectionModel::NoUpdate); // move the cursor + } + else if(mods & Qt::ShiftModifier) { // Shift key is pressed + // select all items between current index and the hovered index. + QModelIndex current = selModel->currentIndex(); + if(selModel->hasSelection() && current.isValid()) { + selModel->clear(); // clear old selection + selModel->setCurrentIndex(current, QItemSelectionModel::NoUpdate); + int begin = current.row(); + int end = index.row(); + if(begin > end) { + qSwap(begin, end); + } + for(int row = begin; row <= end; ++row) { + QModelIndex sel = model_->index(row, 0); + selModel->select(sel, flags | QItemSelectionModel::Select); + } + } + else { // no items are selected, select the hovered item. + if(index.isValid()) { + selModel->select(index, flags | QItemSelectionModel::SelectCurrent); + selModel->setCurrentIndex(index, QItemSelectionModel::NoUpdate); + } + } + lastAutoSelectionIndex_ = index; + } + else if(mods == Qt::NoModifier) { // no modifier keys are pressed. + if(index.isValid()) { + // select the hovered item + view->clearSelection(); + selModel->select(index, flags | QItemSelectionModel::SelectCurrent); + selModel->setCurrentIndex(index, QItemSelectionModel::NoUpdate); + } + lastAutoSelectionIndex_ = index; + } + + autoSelectionTimer_->deleteLater(); + autoSelectionTimer_ = nullptr; +} + +void FolderView::onFileClicked(int type, const std::shared_ptr &fileInfo) { + if(type == ActivatedClick) { + if(fileLauncher_) { + Fm::FileInfoList files; + files.push_back(fileInfo); + fileLauncher_->launchFiles(nullptr, std::move(files)); + } + } + else if(type == ContextMenuClick) { + Fm::FilePath folderPath; + bool isWritableDir(true); + auto files = selectedFiles(); + if(!files.empty()) { + auto& first = files.front(); + if(files.size() == 1 && first->isDir()) { + folderPath = first->path(); + isWritableDir = first->isWritable(); + } + } + if(!folderPath.isValid()) { + folderPath = path(); + if(auto info = folderInfo()) { + isWritableDir = info->isWritable(); + } + } + QMenu* menu = nullptr; + if(fileInfo) { + // show context menu + auto files = selectedFiles(); + if(!files.empty()) { + QModelIndexList selIndexes = mode == DetailedListMode ? selectedRows() : selectedIndexes(); + Fm::FileMenu* fileMenu = (view && selIndexes.size() == 1) + ? new Fm::FileMenu(files, fileInfo, folderPath, isWritableDir, QString(), view) + : new Fm::FileMenu(files, fileInfo, folderPath, isWritableDir); + fileMenu->setFileLauncher(fileLauncher_); + prepareFileMenu(fileMenu); + menu = fileMenu; + } + } + else if (folderInfo()) { + Fm::FolderMenu* folderMenu = new Fm::FolderMenu(this); + prepareFolderMenu(folderMenu); + menu = folderMenu; + } + if(menu) { + menu->exec(QCursor::pos()); + delete menu; + } + } +} + +void FolderView::onClipboardDataChange() { + if(model_) { + const QClipboard* clipboard = QApplication::clipboard(); + const QMimeData* data = clipboard->mimeData(); + Fm::FilePathList paths; + bool isCutSelection; + std::tie(paths, isCutSelection) = Fm::parseClipboardData(*data); + if(!folder()->path().hasUriScheme("search") // skip for search results + && isCutSelection + && Fm::isCurrentPidClipboardData(*data)) { // set cut files only with this app + auto cutDirPath = paths.size() > 0 ? paths[0].parent(): FilePath(); + if(folder()->path() == cutDirPath) { + model_->setCutFiles(selectionModel()->selection()); + } + else if(folder()->hadCutFilesUnset() || folder()->hasCutFiles()) { + model_->setCutFiles(QItemSelection()); + } + return; + } + + folder()->setCutFiles(std::make_shared()); // clean Folder::cutFilesHashSet_ + if(folder()->hadCutFilesUnset()) { + model_->setCutFiles(QItemSelection()); // update indexes if there were cut files here + } + } +} + +void FolderView::prepareFileMenu(FileMenu* /*menu*/) { +} + +void FolderView::prepareFolderMenu(FolderMenu* /*menu*/) { +} diff --git a/src/folderview.h b/src/folderview.h new file mode 100644 index 0000000..5e36433 --- /dev/null +++ b/src/folderview.h @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_FOLDERVIEW_H +#define FM_FOLDERVIEW_H + +#include "libfmqtglobals.h" +#include +#include +#include +#include +#include +#include "foldermodel.h" +#include "proxyfoldermodel.h" +#include "path.h" + +#include "core/folder.h" + +class QTimer; + +namespace Fm { + +class FileMenu; +class FolderMenu; +class FileLauncher; +class FolderViewStyle; + +class LIBFM_QT_API FolderView : public QWidget { + Q_OBJECT + +public: + + enum ViewMode { + FirstViewMode = 1, + IconMode = FirstViewMode, + CompactMode, + DetailedListMode, + ThumbnailMode, + LastViewMode = ThumbnailMode, + NumViewModes = (LastViewMode - FirstViewMode + 1) + }; + + enum ClickType { + ActivatedClick, + MiddleClick, + ContextMenuClick + }; + + friend class FolderViewTreeView; + friend class FolderViewListView; + + explicit FolderView(ViewMode _mode = IconMode, QWidget* parent = 0); + + explicit FolderView(QWidget* parent): FolderView{IconMode, parent} {} + + virtual ~FolderView(); + + void setViewMode(ViewMode _mode); + ViewMode viewMode() const; + + void setIconSize(ViewMode mode, QSize size); + QSize iconSize(ViewMode mode) const; + + QAbstractItemView* childView() const; + + ProxyFolderModel* model() const; + void setModel(ProxyFolderModel* _model); + + std::shared_ptr folder() const { + return model_ ? static_cast(model_->sourceModel())->folder() : nullptr; + } + + std::shared_ptr folderInfo() const { + auto _folder = folder(); + return _folder ? _folder->info() : nullptr; + } + + Fm::FilePath path() { + auto _folder = folder(); + return _folder ? _folder->path() : Fm::FilePath(); + } + + QItemSelectionModel* selectionModel() const; + Fm::FileInfoList selectedFiles() const; + Fm::FilePathList selectedFilePaths() const; + bool hasSelection() const; + QModelIndex indexFromFolderPath(const Fm::FilePath& folderPath) const; + + void selectAll(); + + void invertSelection(); + + void setFileLauncher(FileLauncher* launcher) { + fileLauncher_ = launcher; + } + + FileLauncher* fileLauncher() { + return fileLauncher_; + } + + int autoSelectionDelay() const { + return autoSelectionDelay_; + } + + void setAutoSelectionDelay(int delay); + +protected: + virtual bool event(QEvent* event); + virtual void contextMenuEvent(QContextMenuEvent* event); + virtual void childMousePressEvent(QMouseEvent* event); + virtual void childDragEnterEvent(QDragEnterEvent* event); + virtual void childDragMoveEvent(QDragMoveEvent* e); + virtual void childDragLeaveEvent(QDragLeaveEvent* e); + virtual void childDropEvent(QDropEvent* e); + + void emitClickedAt(ClickType type, const QPoint& pos); + + QModelIndexList selectedRows(int column = 0) const; + QModelIndexList selectedIndexes() const; + + virtual void prepareFileMenu(Fm::FileMenu* menu); + virtual void prepareFolderMenu(Fm::FolderMenu* menu); + + virtual bool eventFilter(QObject* watched, QEvent* event); + + void updateGridSize(); // called when view mode, icon size, font size or cell margin is changed + + QSize getMargins() const { + return itemDelegateMargins_; + } + + // sets the cell margins in the icon and thumbnail modes + // and calls updateGridSize() when needed + void setMargins(QSize size); + +public Q_SLOTS: + void onItemActivated(QModelIndex index); + void onSelectionChanged(const QItemSelection& selected, const QItemSelection& deselected); + virtual void onFileClicked(int type, const std::shared_ptr& fileInfo); + void onClipboardDataChange(); + +private Q_SLOTS: + void onAutoSelectionTimeout(); + void onSelChangedTimeout(); + void onClosingEditor(QWidget* editor, QAbstractItemDelegate::EndEditHint hint); + +Q_SIGNALS: + void clicked(int type, const std::shared_ptr& file); + void clickedBack(); + void clickedForward(); + void selChanged(); + void sortChanged(); + +private: + + QAbstractItemView* view; + ProxyFolderModel* model_; + ViewMode mode; + QSize iconSize_[NumViewModes]; + FileLauncher* fileLauncher_; + int autoSelectionDelay_; + QTimer* autoSelectionTimer_; + QModelIndex lastAutoSelectionIndex_; + QTimer* selChangedTimer_; + // the cell margins in the icon and thumbnail modes + QSize itemDelegateMargins_; +}; + +} + +#endif // FM_FOLDERVIEW_H diff --git a/src/folderview_p.h b/src/folderview_p.h new file mode 100644 index 0000000..769bb38 --- /dev/null +++ b/src/folderview_p.h @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_FOLDERVIEW_P_H +#define FM_FOLDERVIEW_P_H + +#include +#include +#include +#include "folderview.h" + +class QTimer; + +namespace Fm { + +// override these classes for implementing FolderView +class FolderViewListView : public QListView { + Q_OBJECT +public: + friend class FolderView; + FolderViewListView(QWidget* parent = 0); + virtual ~FolderViewListView(); + virtual void startDrag(Qt::DropActions supportedActions); + virtual void mousePressEvent(QMouseEvent* event); + virtual void mouseMoveEvent(QMouseEvent* event); + virtual void mouseReleaseEvent(QMouseEvent* event); + virtual void mouseDoubleClickEvent(QMouseEvent* event); + virtual void dragEnterEvent(QDragEnterEvent* event); + virtual void dragMoveEvent(QDragMoveEvent* e); + virtual void dragLeaveEvent(QDragLeaveEvent* e); + virtual void dropEvent(QDropEvent* e); + + virtual QModelIndex indexAt(const QPoint & point) const; + + inline void setPositionForIndex(const QPoint & position, const QModelIndex & index) { + QListView::setPositionForIndex(position, index); + } + + inline QRect rectForIndex(const QModelIndex & index) const { + return QListView::rectForIndex(index); + } + + inline QStyleOptionViewItem getViewOptions() { + return viewOptions(); + } + +Q_SIGNALS: + void activatedFiltered(const QModelIndex &index); + +protected: + virtual QModelIndex moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers); + +private Q_SLOTS: + void activation(const QModelIndex &index); + +private: + bool activationAllowed_; +}; + +class FolderViewTreeView : public QTreeView { + Q_OBJECT +public: + friend class FolderView; + FolderViewTreeView(QWidget* parent = 0); + virtual ~FolderViewTreeView(); + virtual void setModel(QAbstractItemModel* model); + virtual void mousePressEvent(QMouseEvent* event); + virtual void mouseMoveEvent(QMouseEvent* event); + virtual void mouseReleaseEvent(QMouseEvent* event); + virtual void mouseDoubleClickEvent(QMouseEvent* event); + virtual void dragEnterEvent(QDragEnterEvent* event); + virtual void dragMoveEvent(QDragMoveEvent* e); + virtual void dragLeaveEvent(QDragLeaveEvent* e); + virtual void dropEvent(QDropEvent* e); + + virtual void rowsInserted(const QModelIndex& parent,int start, int end); + virtual void rowsAboutToBeRemoved(const QModelIndex& parent,int start, int end); + virtual void dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector& roles = QVector{}); + virtual void reset(); + + virtual void resizeEvent(QResizeEvent* event); + void queueLayoutColumns(); + +Q_SIGNALS: + void activatedFiltered(const QModelIndex &index); + +private Q_SLOTS: + void layoutColumns(); + void activation(const QModelIndex &index); + void onSortFilterChanged(); + +private: + bool doingLayout_; + QTimer* layoutTimer_; + bool activationAllowed_; +}; + + +} // namespace Fm + +#endif // FM_FOLDERVIEW_P_H diff --git a/src/fontbutton.cpp b/src/fontbutton.cpp new file mode 100644 index 0000000..f3b21a6 --- /dev/null +++ b/src/fontbutton.cpp @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "fontbutton.h" +#include +#include + +namespace Fm { + +FontButton::FontButton(QWidget* parent): QPushButton(parent) { + connect(this, &QPushButton::clicked, this, &FontButton::onClicked); +} + +FontButton::~FontButton() { +} + +void FontButton::onClicked() { + QFontDialog dlg(font_); + if(dlg.exec() == QDialog::Accepted) { + setFont(dlg.selectedFont()); + } +} + +void FontButton::setFont(QFont font) { + font_ = font; + QString text = font.family(); + if(font.bold()) { + text += " "; + text += tr("Bold"); + } + if(font.italic()) { + text += " "; + text += tr("Italic"); + } + text += QString(" %1").arg(font.pointSize()); + setText(text); + Q_EMIT changed(); +} + + +} // namespace Fm diff --git a/src/fontbutton.h b/src/fontbutton.h new file mode 100644 index 0000000..4dee7b2 --- /dev/null +++ b/src/fontbutton.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_FONTBUTTON_H +#define FM_FONTBUTTON_H + +#include "libfmqtglobals.h" +#include + + +namespace Fm { + +class LIBFM_QT_API FontButton : public QPushButton { + Q_OBJECT +public: + explicit FontButton(QWidget* parent = 0); + virtual ~FontButton(); + + QFont font() { + return font_; + } + + void setFont(QFont font); + +Q_SIGNALS: + void changed(); + +private Q_SLOTS: + void onClicked(); + +private: + QFont font_; +}; + +} + +#endif // FM_FONTBUTTON_H diff --git a/src/icontheme.cpp b/src/icontheme.cpp new file mode 100644 index 0000000..55d1f28 --- /dev/null +++ b/src/icontheme.cpp @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "icontheme.h" +#include +#include +#include +#include +#include +#include + +#include "core/iconinfo.h" + +namespace Fm { + +static IconTheme* theIconTheme = nullptr; // the global single instance of IconTheme. + +IconTheme::IconTheme(): + currentThemeName_(QIcon::themeName()) { + // NOTE: only one instance is allowed + Q_ASSERT(theIconTheme == nullptr); + Q_ASSERT(qApp != nullptr); // QApplication should exists before contructing IconTheme. + + theIconTheme = this; + + // We need to get notified when there is a QEvent::StyleChange event so + // we can check if the current icon theme name is changed. + // To do this, we can filter QApplication object itself to intercept + // signals of all widgets, but this may be too inefficient. + // So, we only filter the events on QDesktopWidget instead. + qApp->desktop()->installEventFilter(this); +} + +IconTheme::~IconTheme() { +} + +IconTheme* IconTheme::instance() { + return theIconTheme; +} + +// check if the icon theme name is changed and emit "changed()" signal if any change is detected. +void IconTheme::checkChanged() { + if(QIcon::themeName() != theIconTheme->currentThemeName_) { + // if the icon theme is changed + theIconTheme->currentThemeName_ = QIcon::themeName(); + // invalidate the cached data + Fm::IconInfo::updateQIcons(); + Q_EMIT theIconTheme->changed(); + } +} + +// this method is called whenever there is an event on the QDesktopWidget object. +bool IconTheme::eventFilter(QObject* obj, QEvent* event) { + // we're only interested in the StyleChange event. + // FIXME: QEvent::ThemeChange seems to be interal to Qt 5 and is not documented + if(event->type() == QEvent::StyleChange || event->type() == QEvent::ThemeChange) { + checkChanged(); // check if the icon theme is changed + } + return QObject::eventFilter(obj, event); +} + + +} // namespace Fm diff --git a/src/icontheme.h b/src/icontheme.h new file mode 100644 index 0000000..8337e8b --- /dev/null +++ b/src/icontheme.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_ICONTHEME_H +#define FM_ICONTHEME_H + +#include "libfmqtglobals.h" +#include +#include +#include "libfm/fm.h" + +namespace Fm { + +class LIBFM_QT_API IconTheme: public QObject { + Q_OBJECT +public: + IconTheme(); + ~IconTheme(); + + static IconTheme* instance(); + + static void checkChanged(); // check if current icon theme name is changed + +Q_SIGNALS: + void changed(); // emitted when the name of current icon theme is changed + +protected: + bool eventFilter(QObject* obj, QEvent* event); + +private: + QString currentThemeName_; +}; + +} + +#endif // FM_ICONTHEME_H diff --git a/src/libfm-qt.pc.in b/src/libfm-qt.pc.in new file mode 100644 index 0000000..11e35a3 --- /dev/null +++ b/src/libfm-qt.pc.in @@ -0,0 +1,12 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +exec_prefix=${prefix} +libdir=${exec_prefix}/lib +includedir=${prefix}/include + +Name: libfm-qt +Description: A Qt/glib/gio-based lib used to develop file managers providing some file management utilities. (This is a Qt port of the original libfm library) +URL: http://pcmanfm.sourceforge.net/ +Requires: @REQUIRED_QT@ libfm >= 1.2.0 +Version: @LIBFM_QT_VERSION@ +Libs: -L${libdir} -lfm -l@LIBFM_QT_LIBRARY_NAME@ +Cflags: -I${includedir} diff --git a/src/libfmqt.cpp b/src/libfmqt.cpp new file mode 100644 index 0000000..756f76b --- /dev/null +++ b/src/libfmqt.cpp @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include +#include "libfmqt.h" +#include +#include +#include "icontheme.h" +#include "core/thumbnailer.h" +#include "xdndworkaround.h" + +namespace Fm { + +struct LibFmQtData { + LibFmQtData(); + ~LibFmQtData(); + + IconTheme* iconTheme; + QTranslator translator; + XdndWorkaround workaround; + int refCount; + Q_DISABLE_COPY(LibFmQtData) +}; + +static LibFmQtData* theLibFmData = nullptr; + +static GFile* lookupCustomUri(GVfs * /*vfs*/, const char *identifier, gpointer /*user_data*/) { + GFile* gf = fm_file_new_for_uri(identifier); + return gf; +} + +LibFmQtData::LibFmQtData(): refCount(1) { +#if !GLIB_CHECK_VERSION(2, 36, 0) + g_type_init(); +#endif + fm_init(nullptr); + // turn on glib debug message + // g_setenv("G_MESSAGES_DEBUG", "all", true); + iconTheme = new IconTheme(); + Fm::Thumbnailer::loadAll(); + translator.load("libfm-qt_" + QLocale::system().name(), LIBFM_QT_DATA_DIR "/translations"); + + // register some URI schemes implemented by libfm + // FIXME: move these implementations into libfm-qt to avoid linking with libfm. + GVfs* vfs = g_vfs_get_default(); + g_vfs_register_uri_scheme(vfs, "menu", lookupCustomUri, nullptr, nullptr, lookupCustomUri, nullptr, nullptr); + g_vfs_register_uri_scheme(vfs, "search", lookupCustomUri, nullptr, nullptr, lookupCustomUri, nullptr, nullptr); +} + +LibFmQtData::~LibFmQtData() { + GVfs* vfs = g_vfs_get_default(); + g_vfs_unregister_uri_scheme(vfs, "menu"); + g_vfs_unregister_uri_scheme(vfs, "search"); + delete iconTheme; + fm_finalize(); +} + +LibFmQt::LibFmQt() { + if(!theLibFmData) { + theLibFmData = new LibFmQtData(); + } + else { + ++theLibFmData->refCount; + } + d = theLibFmData; +} + +LibFmQt::~LibFmQt() { + if(--d->refCount == 0) { + delete d; + theLibFmData = nullptr; + } +} + +QTranslator* LibFmQt::translator() { + return &d->translator; +} + +} // namespace Fm diff --git a/src/libfmqt.h b/src/libfmqt.h new file mode 100644 index 0000000..e63e2db --- /dev/null +++ b/src/libfmqt.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_APPLICATION_H +#define FM_APPLICATION_H + +#include "libfmqtglobals.h" +#include +#include +#include + +namespace Fm { + +struct LibFmQtData; + +class LIBFM_QT_API LibFmQt { +public: + explicit LibFmQt(); + ~LibFmQt(); + + QTranslator* translator(); + +private: + LibFmQt(LibFmQt& other); // disable copy + LibFmQtData* d; +}; + +} + +#endif // FM_APPLICATION_H diff --git a/src/libfmqtglobals.h b/src/libfmqtglobals.h new file mode 100644 index 0000000..1a6d4fe --- /dev/null +++ b/src/libfmqtglobals.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef _LIBFM_QT_GLOBALS_ +#define _LIBFM_QT_GLOBALS_ + +#include "fm-qt_export.h" + +#endif diff --git a/src/mount-operation-password.ui b/src/mount-operation-password.ui new file mode 100644 index 0000000..72f1b30 --- /dev/null +++ b/src/mount-operation-password.ui @@ -0,0 +1,215 @@ + + + MountOperationPasswordDialog + + + + 0 + 0 + 244 + 302 + + + + + 0 + 0 + + + + Mount + + + + + + false + + + false + + + + + + + 0 + 0 + + + + + + + + + + + Connect &anonymously + + + usernameGroup + + + + + + + Connect as u&ser: + + + usernameGroup + + + + + + + + + + + + + 0 + 0 + + + + &Username: + + + username + + + + + + + QLineEdit::Password + + + + + + + + 0 + 0 + + + + &Password: + + + password + + + + + + + &Domain: + + + domain + + + + + + + + + + + + Forget password &immediately + + + passwordGroup + + + + + + + Remember password until you &logout + + + passwordGroup + + + + + + + Remember &forever + + + passwordGroup + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + Anonymous + asUser + username + domain + password + forgetPassword + sessionPassword + storePassword + + + + + buttonBox + accepted() + MountOperationPasswordDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + MountOperationPasswordDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + + + + + diff --git a/src/mountoperation.cpp b/src/mountoperation.cpp new file mode 100644 index 0000000..f9a1a5b --- /dev/null +++ b/src/mountoperation.cpp @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "mountoperation.h" +#include // for _() +#include +#include +#include +#include "mountoperationpassworddialog_p.h" +#include "mountoperationquestiondialog_p.h" +#include "ui_mount-operation-password.h" + +namespace Fm { + +MountOperation::MountOperation(bool interactive, QWidget* parent): + QObject(parent), + op(g_mount_operation_new()), + cancellable_(g_cancellable_new()), + running(false), + interactive_(interactive), + eventLoop(nullptr), + autoDestroy_(true) { + + g_signal_connect(op, "ask-password", G_CALLBACK(onAskPassword), this); + g_signal_connect(op, "ask-question", G_CALLBACK(onAskQuestion), this); + // g_signal_connect(op, "reply", G_CALLBACK(onReply), this); + +#if GLIB_CHECK_VERSION(2, 20, 0) + g_signal_connect(op, "aborted", G_CALLBACK(onAbort), this); +#endif +#if GLIB_CHECK_VERSION(2, 22, 0) + g_signal_connect(op, "show-processes", G_CALLBACK(onShowProcesses), this); +#endif +#if GLIB_CHECK_VERSION(2, 34, 0) + g_signal_connect(op, "show-unmount-progress", G_CALLBACK(onShowUnmountProgress), this); +#endif + +} + +MountOperation::~MountOperation() { + qDebug("delete MountOperation"); + if(cancellable_) { + cancel(); + g_object_unref(cancellable_); + } + + if(eventLoop) { // if wait() is called to block the main loop, but the event loop is still running + // NOTE: is this possible? + eventLoop->exit(1); + } + + if(op) { + g_signal_handlers_disconnect_by_func(op, (gpointer)G_CALLBACK(onAskPassword), this); + g_signal_handlers_disconnect_by_func(op, (gpointer)G_CALLBACK(onAskQuestion), this); +#if GLIB_CHECK_VERSION(2, 20, 0) + g_signal_handlers_disconnect_by_func(op, (gpointer)G_CALLBACK(onAbort), this); +#endif +#if GLIB_CHECK_VERSION(2, 22, 0) + g_signal_handlers_disconnect_by_func(op, (gpointer)G_CALLBACK(onShowProcesses), this); +#endif +#if GLIB_CHECK_VERSION(2, 34, 0) + g_signal_handlers_disconnect_by_func(op, (gpointer)G_CALLBACK(onShowUnmountProgress), this); +#endif + g_object_unref(op); + } + // qDebug("MountOperation deleted"); +} + +void MountOperation::onAbort(GMountOperation* /*_op*/, MountOperation* /*pThis*/) { + +} + +void MountOperation::onAskPassword(GMountOperation* /*_op*/, gchar* message, gchar* default_user, gchar* default_domain, GAskPasswordFlags flags, MountOperation* pThis) { + qDebug("ask password"); + MountOperationPasswordDialog dlg(pThis, flags); + dlg.setMessage(QString::fromUtf8(message)); + dlg.setDefaultUser(QString::fromUtf8(default_user)); + dlg.setDefaultDomain(QString::fromUtf8(default_domain)); + dlg.exec(); +} + +void MountOperation::onAskQuestion(GMountOperation* /*_op*/, gchar* message, GStrv choices, MountOperation* pThis) { + qDebug("ask question"); + MountOperationQuestionDialog dialog(pThis, message, choices); + dialog.exec(); +} + +/* +void MountOperation::onReply(GMountOperation* _op, GMountOperationResult result, MountOperation* pThis) { + qDebug("reply"); +} +*/ + +void MountOperation::onShowProcesses(GMountOperation* /*_op*/, gchar* /*message*/, GArray* /*processes*/, GStrv /*choices*/, MountOperation* /*pThis*/) { + qDebug("show processes"); +} + +void MountOperation::onShowUnmountProgress(GMountOperation* /*_op*/, gchar* /*message*/, gint64 /*time_left*/, gint64 /*bytes_left*/, MountOperation* /*pThis*/) { + qDebug("show unmount progress"); +} + +void MountOperation::onEjectMountFinished(GMount* mount, GAsyncResult* res, QPointer< MountOperation >* pThis) { + if(*pThis) { + GError* error = nullptr; + g_mount_eject_with_operation_finish(mount, res, &error); + (*pThis)->handleFinish(error); + } + delete pThis; +} + +void MountOperation::onEjectVolumeFinished(GVolume* volume, GAsyncResult* res, QPointer< MountOperation >* pThis) { + if(*pThis) { + GError* error = nullptr; + g_volume_eject_with_operation_finish(volume, res, &error); + (*pThis)->handleFinish(error); + } + delete pThis; +} + +void MountOperation::onMountFileFinished(GFile* file, GAsyncResult* res, QPointer< MountOperation >* pThis) { + if(*pThis) { + GError* error = nullptr; + g_file_mount_enclosing_volume_finish(file, res, &error); + (*pThis)->handleFinish(error); + } + delete pThis; +} + +void MountOperation::onMountVolumeFinished(GVolume* volume, GAsyncResult* res, QPointer< MountOperation >* pThis) { + if(*pThis) { + GError* error = nullptr; + g_volume_mount_finish(volume, res, &error); + (*pThis)->handleFinish(error); + } + delete pThis; +} + +void MountOperation::onUnmountMountFinished(GMount* mount, GAsyncResult* res, QPointer< MountOperation >* pThis) { + if(*pThis) { + GError* error = nullptr; + g_mount_unmount_with_operation_finish(mount, res, &error); + (*pThis)->handleFinish(error); + } + delete pThis; +} + +void MountOperation::handleFinish(GError* error) { + qDebug("operation finished: %p", static_cast(error)); + if(error) { + bool showError = interactive_; + if(error->domain == G_IO_ERROR) { + if(error->code == G_IO_ERROR_FAILED) { + // Generate a more human-readable error message instead of using a gvfs one. + // The original error message is something like: + // Error unmounting: umount exited with exit code 1: + // helper failed with: umount: only root can unmount + // UUID=18cbf00c-e65f-445a-bccc-11964bdea05d from /media/sda4 */ + // Why they pass this back to us? This is not human-readable for the users at all. + if(strstr(error->message, "only root can ")) { + g_free(error->message); + error->message = g_strdup(_("Only system administrators have the permission to do this.")); + } + } + else if(error->code == G_IO_ERROR_FAILED_HANDLED) { + showError = false; + } + } + if(showError) { + QMessageBox::critical(nullptr, QObject::tr("Error"), QString::fromUtf8(error->message)); + } + } + + Q_EMIT finished(error); + + if(eventLoop) { // if wait() is called to block the main loop + eventLoop->exit(error != nullptr ? 1 : 0); + eventLoop = nullptr; + } + + if(error) { + g_error_free(error); + } + + // free ourself here!! + if(autoDestroy_) { + deleteLater(); + } +} + +void MountOperation::prepareUnmount(GMount* mount) { + /* ensure that CWD is not on the mounted filesystem. */ + char* cwd_str = g_get_current_dir(); + GFile* cwd = g_file_new_for_path(cwd_str); + GFile* root = g_mount_get_root(mount); + g_free(cwd_str); + /* FIXME: This cannot cover 100% cases since symlinks are not checked. + * There may be other cases that cwd is actually under mount root + * but checking prefix is not enough. We already did our best, though. */ + if(g_file_has_prefix(cwd, root)) { + g_chdir("/"); + } + g_object_unref(cwd); + g_object_unref(root); +} + +// block the operation used an internal QEventLoop and returns +// only after the whole operation is finished. +bool MountOperation::wait() { + QEventLoop loop; + eventLoop = &loop; + int exitCode = loop.exec(); + return exitCode == 0 ? true : false; +} + +} // namespace Fm diff --git a/src/mountoperation.h b/src/mountoperation.h new file mode 100644 index 0000000..97e558f --- /dev/null +++ b/src/mountoperation.h @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_MOUNTOPERATION_H +#define FM_MOUNTOPERATION_H + +#include "libfmqtglobals.h" +#include +#include +#include +#include +#include + +#include "core/filepath.h" + +class QEventLoop; + +namespace Fm { + +// FIXME: the original APIs in gtk+ version of libfm for mounting devices is poor. +// Need to find a better API design which make things fully async and cancellable. + +// FIXME: parent_ does not work. All dialogs shown by the mount operation has no parent window assigned. +// FIXME: Need to reconsider the propery way of API design. Blocking sync calls are handy, but +// indeed causes some problems. :-( + +class LIBFM_QT_API MountOperation: public QObject { + Q_OBJECT + +public: + explicit MountOperation(bool interactive = true, QWidget* parent = 0); + ~MountOperation(); + + void mount(const Fm::FilePath& path) { + g_file_mount_enclosing_volume(path.gfile().get(), G_MOUNT_MOUNT_NONE, op, cancellable_, + (GAsyncReadyCallback)onMountFileFinished, new QPointer(this)); + } + + void mount(GVolume* volume) { + g_volume_mount(volume, G_MOUNT_MOUNT_NONE, op, cancellable_, (GAsyncReadyCallback)onMountVolumeFinished, new QPointer(this)); + } + + void unmount(GMount* mount) { + prepareUnmount(mount); + g_mount_unmount_with_operation(mount, G_MOUNT_UNMOUNT_NONE, op, cancellable_, (GAsyncReadyCallback)onUnmountMountFinished, new QPointer(this)); + } + + void unmount(GVolume* volume) { + GMount* mount = g_volume_get_mount(volume); + if(!mount) { + return; + } + unmount(mount); + g_object_unref(mount); + } + + void eject(GMount* mount) { + prepareUnmount(mount); + g_mount_eject_with_operation(mount, G_MOUNT_UNMOUNT_NONE, op, cancellable_, (GAsyncReadyCallback)onEjectMountFinished, new QPointer(this)); + } + + void eject(GVolume* volume) { + GMount* mnt = g_volume_get_mount(volume); + prepareUnmount(mnt); + g_object_unref(mnt); + g_volume_eject_with_operation(volume, G_MOUNT_UNMOUNT_NONE, op, cancellable_, (GAsyncReadyCallback)onEjectVolumeFinished, new QPointer(this)); + } + + QWidget* parent() const { + return parent_; + } + + void setParent(QWidget* parent) { + parent_ = parent; + } + + GCancellable* cancellable() const { + return cancellable_; + } + + GMountOperation* mountOperation() { + return op; + } + + void cancel() { + g_cancellable_cancel(cancellable_); + } + + bool isRunning() const { + return running; + } + + // block the operation used an internal QEventLoop and returns + // only after the whole operation is finished. + bool wait(); + + bool autoDestroy() { + return autoDestroy_; + } + + void setAutoDestroy(bool destroy = true) { + autoDestroy_ = destroy; + } + +Q_SIGNALS: + void finished(GError* error = nullptr); + +private: + void prepareUnmount(GMount* mount); + + static void onAskPassword(GMountOperation* _op, gchar* message, gchar* default_user, gchar* default_domain, GAskPasswordFlags flags, MountOperation* pThis); + static void onAskQuestion(GMountOperation* _op, gchar* message, GStrv choices, MountOperation* pThis); + // static void onReply(GMountOperation *_op, GMountOperationResult result, MountOperation* pThis); + + static void onAbort(GMountOperation* _op, MountOperation* pThis); + static void onShowProcesses(GMountOperation* _op, gchar* message, GArray* processes, GStrv choices, MountOperation* pThis); + static void onShowUnmountProgress(GMountOperation* _op, gchar* message, gint64 time_left, gint64 bytes_left, MountOperation* pThis); + + // it's possible that this object is freed when the callback is called by gio, so guarding with QPointer is needed here. + static void onMountFileFinished(GFile* file, GAsyncResult* res, QPointer* pThis); + static void onMountVolumeFinished(GVolume* volume, GAsyncResult* res, QPointer* pThis); + static void onUnmountMountFinished(GMount* mount, GAsyncResult* res, QPointer* pThis); + static void onEjectMountFinished(GMount* mount, GAsyncResult* res, QPointer* pThis); + static void onEjectVolumeFinished(GVolume* volume, GAsyncResult* res, QPointer* pThis); + + void handleFinish(GError* error); + +private: + GMountOperation* op; + GCancellable* cancellable_; + QWidget* parent_; + bool running; + bool interactive_; + QEventLoop* eventLoop; + bool autoDestroy_; +}; + +} + +#endif // FM_MOUNTOPERATION_H diff --git a/src/mountoperationpassworddialog.cpp b/src/mountoperationpassworddialog.cpp new file mode 100644 index 0000000..4e7d850 --- /dev/null +++ b/src/mountoperationpassworddialog.cpp @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "mountoperationpassworddialog_p.h" +#include "ui_mount-operation-password.h" +#include "mountoperation.h" + +namespace Fm { + +MountOperationPasswordDialog::MountOperationPasswordDialog(MountOperation* op, GAskPasswordFlags flags): + QDialog(), + mountOperation(op), + needPassword(flags & G_ASK_PASSWORD_NEED_PASSWORD ? true : false), + needUserName(flags & G_ASK_PASSWORD_NEED_USERNAME ? true : false), + needDomain(flags & G_ASK_PASSWORD_NEED_DOMAIN ? true : false), + canSavePassword(flags & G_ASK_PASSWORD_SAVING_SUPPORTED ? true : false), + canAnonymous(flags & G_ASK_PASSWORD_ANONYMOUS_SUPPORTED ? true : false) { + + ui = new Ui::MountOperationPasswordDialog(); + ui->setupUi(this); + + // change the text of Ok button to Connect + ui->buttonBox->buttons().constFirst()->setText(tr("&Connect")); + connect(ui->Anonymous, &QAbstractButton::toggled, this, &MountOperationPasswordDialog::onAnonymousToggled); + + if(canAnonymous) { + // select ananymous by default if applicable. + ui->Anonymous->setChecked(true); + } + else { + ui->Anonymous->setEnabled(false); + ui->asUser->setChecked(true); + } + if(!needUserName) { + ui->username->setEnabled(false); + } + if(needPassword) { + if(!needUserName) { + ui->password->setFocus(); + } + } + else { + ui->password->setEnabled(false); + } + if(!needDomain) { + ui->domain->hide(); + ui->domainLabel->hide(); + } + if(canSavePassword) { + ui->sessionPassword->setChecked(true); + } + else { + ui->storePassword->setEnabled(false); + ui->sessionPassword->setEnabled(false); + ui->forgetPassword->setChecked(true); + } +} + +MountOperationPasswordDialog::~MountOperationPasswordDialog() { + delete ui; +} + +void MountOperationPasswordDialog::onAnonymousToggled(bool checked) { + // disable username/password entries if anonymous mode is used + bool useUserPassword = !checked; + if(needUserName) { + ui->username->setEnabled(useUserPassword); + } + if(needPassword) { + ui->password->setEnabled(useUserPassword); + } + if(needDomain) { + ui->domain->setEnabled(useUserPassword); + } + + if(canSavePassword) { + ui->forgetPassword->setEnabled(useUserPassword); + ui->sessionPassword->setEnabled(useUserPassword); + ui->storePassword->setEnabled(useUserPassword); + } +} + +void MountOperationPasswordDialog::setMessage(QString message) { + ui->message->setText(message); +} + +void MountOperationPasswordDialog::setDefaultDomain(QString domain) { + ui->domain->setText(domain); +} + +void MountOperationPasswordDialog::setDefaultUser(QString user) { + ui->username->setText(user); +} + +void MountOperationPasswordDialog::done(int r) { + GMountOperation* gmop = mountOperation->mountOperation(); + + if(r == QDialog::Accepted) { + + if(needUserName) { + g_mount_operation_set_username(gmop, ui->username->text().toUtf8()); + } + if(needDomain) { + g_mount_operation_set_domain(gmop, ui->domain->text().toUtf8()); + } + if(needPassword) { + g_mount_operation_set_password(gmop, ui->password->text().toUtf8()); + } + if(canAnonymous) { + g_mount_operation_set_anonymous(gmop, ui->Anonymous->isChecked()); + } + + g_mount_operation_reply(gmop, G_MOUNT_OPERATION_HANDLED); + } + else { + g_mount_operation_reply(gmop, G_MOUNT_OPERATION_ABORTED); + } + QDialog::done(r); +} + +} // namespace Fm diff --git a/src/mountoperationpassworddialog_p.h b/src/mountoperationpassworddialog_p.h new file mode 100644 index 0000000..1fd6cd1 --- /dev/null +++ b/src/mountoperationpassworddialog_p.h @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_MOUNTOPERATIONPASSWORDDIALOG_H +#define FM_MOUNTOPERATIONPASSWORDDIALOG_H + +#include "libfmqtglobals.h" +#include +#include + +namespace Ui { +class MountOperationPasswordDialog; +} + +namespace Fm { + +class MountOperation; + +class MountOperationPasswordDialog : public QDialog { + Q_OBJECT + +public: + explicit MountOperationPasswordDialog(MountOperation* op, GAskPasswordFlags flags); + virtual ~MountOperationPasswordDialog(); + + void setMessage(QString message); + void setDefaultUser(QString user); + void setDefaultDomain(QString domain); + + virtual void done(int r); + +private Q_SLOTS: + void onAnonymousToggled(bool checked); + +private: + Ui::MountOperationPasswordDialog* ui; + MountOperation* mountOperation; + bool needPassword; + bool needUserName; + bool needDomain; + bool canSavePassword; + bool canAnonymous; +}; + +} + +#endif // FM_MOUNTOPERATIONPASSWORDDIALOG_H diff --git a/src/mountoperationquestiondialog.cpp b/src/mountoperationquestiondialog.cpp new file mode 100644 index 0000000..3679740 --- /dev/null +++ b/src/mountoperationquestiondialog.cpp @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "mountoperationquestiondialog_p.h" +#include "mountoperation.h" +#include + +namespace Fm { + +MountOperationQuestionDialog::MountOperationQuestionDialog(MountOperation* op, gchar* message, GStrv choices): + QMessageBox(), + mountOperation(op) { + + setIcon(QMessageBox::Question); + setText(QString::fromUtf8(message)); + + choiceCount = g_strv_length(choices); + choiceButtons = new QAbstractButton*[choiceCount]; + for(int i = 0; i < choiceCount; ++i) { + // It's not allowed to add custom buttons without standard roles + // to QMessageBox. So we set role of all buttons to AcceptRole. + // When any of the set buttons is clicked, exec() always returns "accept". + QPushButton* button = new QPushButton(QString::fromUtf8(choices[i])); + addButton(button, QMessageBox::AcceptRole); + choiceButtons[i] = button; + } +} + +MountOperationQuestionDialog::~MountOperationQuestionDialog() { + delete []choiceButtons; +} + +void MountOperationQuestionDialog::done(int r) { + GMountOperation* op = mountOperation->mountOperation(); + + g_mount_operation_set_choice(op, r); + g_mount_operation_reply(op, G_MOUNT_OPERATION_HANDLED); + + QDialog::done(r); +} + +void MountOperationQuestionDialog::closeEvent(QCloseEvent *event) +{ + GMountOperation* op = mountOperation->mountOperation(); + + g_mount_operation_reply(op, G_MOUNT_OPERATION_ABORTED); + + event->accept(); +} + +} // namespace Fm diff --git a/src/mountoperationquestiondialog_p.h b/src/mountoperationquestiondialog_p.h new file mode 100644 index 0000000..8c6a810 --- /dev/null +++ b/src/mountoperationquestiondialog_p.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_MOUNTOPERATIONQUESTIONDIALOG_H +#define FM_MOUNTOPERATIONQUESTIONDIALOG_H + +#include "libfmqtglobals.h" +#include +#include +#include + +namespace Fm { + +class MountOperation; + +class MountOperationQuestionDialog : public QMessageBox { + Q_OBJECT +public: + MountOperationQuestionDialog(MountOperation* op, gchar* message, GStrv choices); + virtual ~MountOperationQuestionDialog(); + + virtual void done(int r); + virtual void closeEvent(QCloseEvent *event); + +private: + MountOperation* mountOperation; + QAbstractButton** choiceButtons; + int choiceCount; +}; + +} + +#endif // FM_MOUNTOPERATIONQUESTIONDIALOG_H diff --git a/src/path.h b/src/path.h new file mode 100644 index 0000000..dccf91e --- /dev/null +++ b/src/path.h @@ -0,0 +1,441 @@ +/* + * Copyright (C) 2016 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef __LIBFM_QT_FM_PATH_H__ +#define __LIBFM_QT_FM_PATH_H__ + +#include +#include +#include +#include +#include "libfmqtglobals.h" + + +namespace Fm { + + +class LIBFM_QT_API PathList { +public: + + + PathList(void ) { + dataPtr_ = reinterpret_cast(fm_path_list_new()); + } + + + PathList(FmPathList* dataPtr){ + dataPtr_ = dataPtr != nullptr ? reinterpret_cast(fm_list_ref(FM_LIST(dataPtr))) : nullptr; + } + + + // copy constructor + PathList(const PathList& other) { + dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast(fm_list_ref(FM_LIST(other.dataPtr_))) : nullptr; + } + + + // move constructor + PathList(PathList&& other) { + dataPtr_ = reinterpret_cast(other.takeDataPtr()); + } + + + // destructor + ~PathList() { + if(dataPtr_ != nullptr) { + fm_list_unref(FM_LIST(dataPtr_)); + } + } + + + // create a wrapper for the data pointer without increasing the reference count + static PathList wrapPtr(FmPathList* dataPtr) { + PathList obj; + obj.dataPtr_ = reinterpret_cast(dataPtr); + return obj; + } + + // disown the managed data pointer + FmPathList* takeDataPtr() { + FmPathList* data = reinterpret_cast(dataPtr_); + dataPtr_ = nullptr; + return data; + } + + // get the raw pointer wrapped + FmPathList* dataPtr() { + return reinterpret_cast(dataPtr_); + } + + // automatic type casting + operator FmPathList*() { + return dataPtr(); + } + + // copy assignment + PathList& operator=(const PathList& other) { + if(dataPtr_ != nullptr) { + fm_list_unref(FM_LIST(dataPtr_)); + } + dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast(fm_list_ref(FM_LIST(other.dataPtr_))) : nullptr; + return *this; + } + + + // move assignment + PathList& operator=(PathList&& other) { + dataPtr_ = reinterpret_cast(other.takeDataPtr()); + return *this; + } + + bool isNull() { + return (dataPtr_ == nullptr); + } + + // methods + + void writeUriList(GString* buf) { + fm_path_list_write_uri_list(dataPtr(), buf); + } + + char* toUriList(void) { + return fm_path_list_to_uri_list(dataPtr()); + } + + unsigned int getLength() { + return fm_path_list_get_length(dataPtr()); + } + + bool isEmpty() { + return fm_path_list_is_empty(dataPtr()); + } + + FmPath* peekHead() { + return fm_path_list_peek_head(dataPtr()); + } + + GList* peekHeadLink() { + return fm_path_list_peek_head_link(dataPtr()); + } + + void pushTail(FmPath* path) { + fm_path_list_push_tail(dataPtr(), path); + } + + static PathList newFromFileInfoGslist(GSList* fis) { + return PathList::wrapPtr(fm_path_list_new_from_file_info_gslist(fis)); + } + + + static PathList newFromFileInfoGlist(GList* fis) { + return PathList::wrapPtr(fm_path_list_new_from_file_info_glist(fis)); + } + + + static PathList newFromFileInfoList(FmFileInfoList* fis) { + return PathList::wrapPtr(fm_path_list_new_from_file_info_list(fis)); + } + + + static PathList newFromUris(char* const* uris) { + return PathList::wrapPtr(fm_path_list_new_from_uris(uris)); + } + + + static PathList newFromUriList(const char* uri_list) { + return PathList::wrapPtr(fm_path_list_new_from_uri_list(uri_list)); + } + + + +private: + FmPathList* dataPtr_; // data pointer for the underlying C struct + +}; + + + +class LIBFM_QT_API Path { +public: + + + // default constructor + Path() { + dataPtr_ = nullptr; + } + + + Path(FmPath* dataPtr){ + dataPtr_ = dataPtr != nullptr ? reinterpret_cast(fm_path_ref(dataPtr)) : nullptr; + } + + + // copy constructor + Path(const Path& other) { + dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast(fm_path_ref(other.dataPtr_)) : nullptr; + } + + + // move constructor + Path(Path&& other) { + dataPtr_ = reinterpret_cast(other.takeDataPtr()); + } + + + // destructor + ~Path() { + if(dataPtr_ != nullptr) { + fm_path_unref(dataPtr_); + } + } + + + // create a wrapper for the data pointer without increasing the reference count + static Path wrapPtr(FmPath* dataPtr) { + Path obj; + obj.dataPtr_ = reinterpret_cast(dataPtr); + return obj; + } + + // disown the managed data pointer + FmPath* takeDataPtr() { + FmPath* data = reinterpret_cast(dataPtr_); + dataPtr_ = nullptr; + return data; + } + + // get the raw pointer wrapped + FmPath* dataPtr() { + return reinterpret_cast(dataPtr_); + } + + // automatic type casting + operator FmPath*() { + return dataPtr(); + } + + // copy assignment + Path& operator=(const Path& other) { + if(dataPtr_ != nullptr) { + fm_path_unref(dataPtr_); + } + dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast(fm_path_ref(other.dataPtr_)) : nullptr; + return *this; + } + + + // move assignment + Path& operator=(Path&& other) { + dataPtr_ = reinterpret_cast(other.takeDataPtr()); + return *this; + } + + bool isNull() { + return (dataPtr_ == nullptr); + } + + // methods + bool isNative() { + return fm_path_is_native(dataPtr()); + } + + bool isTrash() { + return fm_path_is_trash(dataPtr()); + } + + bool isTrashRoot() { + return fm_path_is_trash_root(dataPtr()); + } + + bool isNativeOrTrash() { + return fm_path_is_native_or_trash(dataPtr()); + } + + int depth(void) { + return fm_path_depth(dataPtr()); + } + + + bool equalStr(const gchar* str, int n) { + return fm_path_equal_str(dataPtr(), str, n); + } + + + int compare(FmPath* p2) { + return fm_path_compare(dataPtr(), p2); + } + + int compare(Path& p2) { + return fm_path_compare(dataPtr(), p2.dataPtr()); + } + + bool equal(FmPath* p2) { + return fm_path_equal(dataPtr(), p2); + } + + bool equal(Path& p2) { + return fm_path_equal(dataPtr(), p2.dataPtr()); + } + + bool operator == (Path& other) { + return fm_path_equal(dataPtr(), other.dataPtr()); + } + + bool operator != (Path& other) { + return !fm_path_equal(dataPtr(), other.dataPtr()); + } + + bool operator < (Path& other) { + return compare(other); + } + + bool operator > (Path& other) { + return (other < *this); + } + + unsigned int hash(void) { + return fm_path_hash(dataPtr()); + } + + + char* displayBasename(void) { + return fm_path_display_basename(dataPtr()); + } + + char* displayName(gboolean human_readable) { + return fm_path_display_name(dataPtr(), human_readable); + } + + + GFile* toGfile(void) { + return fm_path_to_gfile(dataPtr()); + } + + + char* toUri(void) { + return fm_path_to_uri(dataPtr()); + } + + + char* toStr(void) { + return fm_path_to_str(dataPtr()); + } + + + Path getSchemePath(void) { + return Path(fm_path_get_scheme_path(dataPtr())); + } + + + bool hasPrefix(FmPath* prefix) { + return fm_path_has_prefix(dataPtr(), prefix); + } + + + FmPathFlags getFlags(void) { + return fm_path_get_flags(dataPtr()); + } + + + Path getParent(void) { + return Path(fm_path_get_parent(dataPtr())); + } + + + static Path getAppsMenu(void ) { + return Path(fm_path_get_apps_menu()); + } + + + static Path getTrash(void ) { + return Path(fm_path_get_trash()); + } + + + static Path getDesktop(void ) { + return Path(fm_path_get_desktop()); + } + + + static Path getHome(void ) { + return Path(fm_path_get_home()); + } + + + static Path getRoot(void ) { + return Path(fm_path_get_root()); + } + + + static Path newForGfile(GFile* gf) { + return Path::wrapPtr(fm_path_new_for_gfile(gf)); + } + + + Path newRelative(const char* rel) { + return Path::wrapPtr(fm_path_new_relative(dataPtr(), rel)); + } + + + Path newChildLen(const char* basename, int name_len) { + return Path::wrapPtr(fm_path_new_child_len(dataPtr(), basename, name_len)); + } + + + Path newChild(const char* basename) { + return Path::wrapPtr(fm_path_new_child(dataPtr(), basename)); + } + + + static Path newForCommandlineArg(const char* arg) { + return Path::wrapPtr(fm_path_new_for_commandline_arg(arg)); + } + + + static Path newForStr(const char* path_str) { + return Path::wrapPtr(fm_path_new_for_str(path_str)); + } + + + static Path newForDisplayName(const char* path_name) { + return Path::wrapPtr(fm_path_new_for_display_name(path_name)); + } + + + static Path newForUri(const char* uri) { + return Path::wrapPtr(fm_path_new_for_uri(uri)); + } + + + static Path newForPath(const char* path_name) { + return Path::wrapPtr(fm_path_new_for_path(path_name)); + } + + + +private: + FmPath* dataPtr_; // data pointer for the underlying C struct + +}; + +} + +Q_DECLARE_OPAQUE_POINTER(FmPath*) + +#endif // __LIBFM_QT_FM_PATH_H__ diff --git a/src/pathbar.cpp b/src/pathbar.cpp new file mode 100644 index 0000000..3ddb250 --- /dev/null +++ b/src/pathbar.cpp @@ -0,0 +1,350 @@ +/* + * Copyright (C) 2016 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "pathbar.h" +#include "pathbar_p.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "pathedit.h" + + +namespace Fm { + +PathBar::PathBar(QWidget* parent): + QWidget(parent), + tempPathEdit_(nullptr) { + + QHBoxLayout* topLayout = new QHBoxLayout(this); + topLayout->setContentsMargins(0, 0, 0, 0); + topLayout->setSpacing(0); + bool rtl(layoutDirection() == Qt::RightToLeft); + + // the arrow button used to scroll to start of the path + scrollToStart_ = new QToolButton(this); + scrollToStart_->setArrowType(rtl ? Qt::RightArrow : Qt::LeftArrow); + scrollToStart_->setAutoRepeat(true); + scrollToStart_->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::MinimumExpanding); + connect(scrollToStart_, &QToolButton::clicked, this, &PathBar::onScrollButtonClicked); + topLayout->addWidget(scrollToStart_); + + // there might be too many buttons when the path is long, so make it scrollable. + scrollArea_ = new QScrollArea(this); + scrollArea_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + scrollArea_->setFrameShape(QFrame::NoFrame); + scrollArea_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scrollArea_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); + scrollArea_->verticalScrollBar()->setDisabled(true); + connect(scrollArea_->horizontalScrollBar(), &QAbstractSlider::valueChanged, this, &PathBar::setArrowEnabledState); + topLayout->addWidget(scrollArea_, 1); // stretch factor=1, make it expandable + + // the arrow button used to scroll to end of the path + scrollToEnd_ = new QToolButton(this); + scrollToEnd_->setArrowType(rtl ? Qt::LeftArrow : Qt::RightArrow); + scrollToEnd_->setAutoRepeat(true); + scrollToEnd_->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::MinimumExpanding); + connect(scrollToEnd_, &QToolButton::clicked, this, &PathBar::onScrollButtonClicked); + topLayout->addWidget(scrollToEnd_); + + // container widget of the path buttons + buttonsWidget_ = new QWidget(this); + buttonsWidget_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + + buttonsLayout_ = new QHBoxLayout(buttonsWidget_); + buttonsLayout_->setContentsMargins(0, 0, 0, 0); + buttonsLayout_->setSpacing(0); + buttonsLayout_->setSizeConstraint(QLayout::SetFixedSize); // required when added to scroll area according to QScrollArea doc. + scrollArea_->setWidget(buttonsWidget_); // make the buttons widget scrollable if the path is too long +} + +void PathBar::resizeEvent(QResizeEvent* event) { + QWidget::resizeEvent(event); + updateScrollButtonVisibility(); +} + +void PathBar::wheelEvent(QWheelEvent* event) { + QWidget::wheelEvent(event); + QAbstractSlider::SliderAction action = QAbstractSlider::SliderNoAction; + int vDelta = event->angleDelta().y(); + if(vDelta > 0) { + if(scrollToStart_->isEnabled()) { + action = QAbstractSlider::SliderSingleStepSub; + } + } + else if(vDelta < 0) { + if(scrollToEnd_->isEnabled()) { + action = QAbstractSlider::SliderSingleStepAdd; + } + } + scrollArea_->horizontalScrollBar()->triggerAction(action); +} + +void PathBar::mousePressEvent(QMouseEvent* event) { + QWidget::mousePressEvent(event); + if(event->button() == Qt::LeftButton) { + openEditor(); + } + else if(event->button() == Qt::MiddleButton) { + PathButton* btn = qobject_cast(childAt(event->x(), event->y())); + if(btn != nullptr) { + scrollArea_->ensureWidgetVisible(btn, + 1); // a harmless compensation for a miscalculation in Qt + Q_EMIT middleClickChdir(pathForButton(btn)); + } + } +} + +void PathBar::contextMenuEvent(QContextMenuEvent* event) { + QMenu* menu = new QMenu(this); + connect(menu, &QMenu::aboutToHide, menu, &QMenu::deleteLater); + + QAction* action = menu->addAction(tr("&Edit Path")); + connect(action, &QAction::triggered, this, &PathBar::openEditor); + + action = menu->addAction(tr("&Copy Path")); + connect(action, &QAction::triggered, this, &PathBar::copyPath); + + menu->popup(mapToGlobal(event->pos())); +} + +void PathBar::updateScrollButtonVisibility() { + // Wait for the horizontal scrollbar to be completely shaped. + // Without this, the enabled state of arrow buttons might be + // wrong when the pathbar is created for the first time. + QTimer::singleShot(0, this, SLOT(setScrollButtonVisibility())); +} + +void PathBar::setScrollButtonVisibility() { + bool showScrollers; + if(tempPathEdit_ != nullptr) { + showScrollers = false; + } + else { + showScrollers = (buttonsLayout_->sizeHint().width() > width()); + } + scrollToStart_->setVisible(showScrollers); + scrollToEnd_->setVisible(showScrollers); + if(showScrollers) { + QScrollBar* sb = scrollArea_->horizontalScrollBar(); + int value = sb->value(); + scrollToStart_->setEnabled(value != sb->minimum()); + scrollToEnd_->setEnabled(value != sb->maximum()); + } +} + +Fm::FilePath PathBar::pathForButton(PathButton* btn) { + std::string fullPath; + int buttonCount = buttonsLayout_->count() - 1; // the last item is a spacer + for(int i = 0; i < buttonCount; ++i) { + if(!fullPath.empty() && fullPath.back() != '/') { + fullPath += '/'; + } + PathButton* elem = static_cast(buttonsLayout_->itemAt(i)->widget()); + fullPath += elem->name(); + if(elem == btn) + break; + } + return Fm::FilePath::fromPathStr(fullPath.c_str()); +} + +void PathBar::onButtonToggled(bool checked) { + if(checked) { + PathButton* btn = static_cast(sender()); + currentPath_ = pathForButton(btn); + Q_EMIT chdir(currentPath_); + + // since scrolling to the toggled buton will happen correctly only when the + // layout is updated and because the update is disabled on creating buttons + // in setPath(), the update status can be used as a sign to know when to wait + if(updatesEnabled()) { + scrollArea_->ensureWidgetVisible(btn, 1); + } + else { + QTimer::singleShot(0, this, SLOT(ensureToggledVisible())); + } + } +} + +void PathBar::ensureToggledVisible() { + int buttonCount = buttonsLayout_->count() - 1; // the last item is a spacer + for(int i = buttonCount - 1; i >= 0; --i) { + if(auto btn = static_cast(buttonsLayout_->itemAt(i)->widget())) { + if(btn->isChecked()) { + scrollArea_->ensureWidgetVisible(btn, 1); + return; + } + } + } +} + +void PathBar::onScrollButtonClicked() { + QToolButton* btn = static_cast(sender()); + QAbstractSlider::SliderAction action = QAbstractSlider::SliderNoAction; + if(btn == scrollToEnd_) { + action = QAbstractSlider::SliderSingleStepAdd; + } + else if(btn == scrollToStart_) { + action = QAbstractSlider::SliderSingleStepSub; + } + scrollArea_->horizontalScrollBar()->triggerAction(action); +} + +void PathBar::setPath(Fm::FilePath path) { + if(currentPath_ == path) { // same path, do nothing + return; + } + + auto oldPath = std::move(currentPath_); + currentPath_ = std::move(path); + // check if we already have a button for this path + int buttonCount = buttonsLayout_->count() - 1; // the last item is a spacer + if(oldPath && currentPath_.isPrefixOf(oldPath)) { + for(int i = buttonCount - 1; i >= 0; --i) { + auto btn = static_cast(buttonsLayout_->itemAt(i)->widget()); + if(pathForButton(btn) == currentPath_) { + btn->setChecked(true); // toggle the button + /* we don't need to emit chdir signal here since later + * toggled signal will be triggered on the button, which + * in turns emit chdir. */ + return; + } + } + } + + /* FIXME: if the new path is the subdir of our full path, actually + * we can append several new buttons rather than re-create + * all of the buttons. This can reduce flickers. */ + + setUpdatesEnabled(false); + // we do not have the path in the buttons list + // destroy existing path element buttons and the spacer + QLayoutItem* item; + while((item = buttonsLayout_->takeAt(0)) != nullptr) { + delete item->widget(); + delete item; + } + + // create new buttons for the new path + auto btnPath = currentPath_; + while(btnPath) { + Fm::CStrPtr name; + Fm::CStrPtr displayName; + auto parent = btnPath.parent(); + // FIXME: some buggy uri types, such as menu://, fail to return NULL when there is no parent path. + // Instead, the path itself is returned. So we check if the parent path is the same as current path. + auto isRoot = !parent.isValid() || parent == btnPath; + if(isRoot) { + displayName = btnPath.displayName(); + name = btnPath.toString(); + } + else { + name = btnPath.baseName(); + } + auto btn = new PathButton(name.get(), displayName ? displayName.get() : name.get(), isRoot, buttonsWidget_); + btn->show(); + connect(btn, &QAbstractButton::toggled, this, &PathBar::onButtonToggled); + buttonsLayout_->insertWidget(0, btn); + if(isRoot) { // this is the root element of the path + break; + } + btnPath = parent; + } + buttonsLayout_->addStretch(1); // add a spacer at the tail of the buttons + + // we don't want to scroll vertically. make the scroll area fit the height of the buttons + // FIXME: this is a little bit hackish :-( + scrollArea_->setFixedHeight(buttonsLayout_->sizeHint().height()); + updateScrollButtonVisibility(); + + // to guarantee that the button will be scrolled to correctly, + // it should be toggled only after the layout update starts above + buttonCount = buttonsLayout_->count() - 1; + if(buttonCount > 0) { + PathButton* lastBtn = static_cast(buttonsLayout_->itemAt(buttonCount - 1)->widget()); + // we don't have to emit the chdir signal since the "onButtonToggled()" slot will be triggered by this. + lastBtn->setChecked(true); + } + + setUpdatesEnabled(true); +} + +void PathBar::openEditor() { + if(tempPathEdit_ == nullptr) { + tempPathEdit_ = new PathEdit(this); + delete layout()->replaceWidget(scrollArea_, tempPathEdit_, Qt::FindDirectChildrenOnly); + scrollArea_->hide(); + scrollToStart_->setVisible(false); + scrollToEnd_->setVisible(false); + tempPathEdit_->setText(currentPath_.toString().get()); + + connect(tempPathEdit_, &PathEdit::returnPressed, this, &PathBar::onReturnPressed); + connect(tempPathEdit_, &PathEdit::editingFinished, this, &PathBar::closeEditor); + } + tempPathEdit_->setFocus(); + tempPathEdit_->selectAll(); +} + +void PathBar::closeEditor() { + if(tempPathEdit_ == nullptr) { + return; + } + // If a menu has popped up synchronously (with QMenu::exec), the path buttons may be drawn + // but the path-edit may not disappear until the menu is closed. So, we hide it here. + tempPathEdit_->setVisible(false); + delete layout()->replaceWidget(tempPathEdit_, scrollArea_, Qt::FindDirectChildrenOnly); + scrollArea_->show(); + if(buttonsLayout_->sizeHint().width() > width()) { + scrollToStart_->setVisible(true); + scrollToEnd_->setVisible(true); + } + + tempPathEdit_->deleteLater(); + tempPathEdit_ = nullptr; + updateScrollButtonVisibility(); + + Q_EMIT editingFinished(); +} + +void PathBar::copyPath() { + QApplication::clipboard()->setText(currentPath_.toString().get()); +} + +void PathBar::onReturnPressed() { + QByteArray pathStr = tempPathEdit_->text().toLocal8Bit(); + setPath(Fm::FilePath::fromPathStr(pathStr.constData())); +} + +void PathBar::setArrowEnabledState(int value) { + if(buttonsLayout_->sizeHint().width() > width()) { + QScrollBar* sb = scrollArea_->horizontalScrollBar(); + scrollToStart_->setEnabled(value != sb->minimum()); + scrollToEnd_->setEnabled(value != sb->maximum()); + } +} + +} // namespace Fm diff --git a/src/pathbar.h b/src/pathbar.h new file mode 100644 index 0000000..34609fd --- /dev/null +++ b/src/pathbar.h @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2016 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_PATHBAR_H +#define FM_PATHBAR_H + +#include "libfmqtglobals.h" +#include +#include "core/filepath.h" + +class QToolButton; +class QScrollArea; +class QPushButton; +class QHBoxLayout; + +namespace Fm { + +class PathEdit; +class PathButton; + +class LIBFM_QT_API PathBar: public QWidget { + Q_OBJECT +public: + explicit PathBar(QWidget* parent = 0); + + const Fm::FilePath& path() { + return currentPath_; + } + + void setPath(Fm::FilePath path); + +Q_SIGNALS: + void chdir(const Fm::FilePath& path); + void middleClickChdir(const Fm::FilePath& path); + void editingFinished(); + +public Q_SLOTS: + void openEditor(); + void closeEditor(); + void copyPath(); + +private Q_SLOTS: + void onButtonToggled(bool checked); + void onScrollButtonClicked(); + void onReturnPressed(); + void setArrowEnabledState(int value); + void setScrollButtonVisibility(); + void ensureToggledVisible(); + +protected: + void resizeEvent(QResizeEvent* event); + void wheelEvent(QWheelEvent* event); + void mousePressEvent(QMouseEvent* event); + void contextMenuEvent(QContextMenuEvent* event); + +private: + void updateScrollButtonVisibility(); + Fm::FilePath pathForButton(PathButton* btn); + +private: + QToolButton* scrollToStart_; + QToolButton* scrollToEnd_; + QScrollArea* scrollArea_; + QWidget* buttonsWidget_; + QHBoxLayout* buttonsLayout_; + PathEdit* tempPathEdit_; + + Fm::FilePath currentPath_; // currently active path +}; + +} // namespace Fm + +#endif // FM_PATHBAR_H diff --git a/src/pathbar_p.h b/src/pathbar_p.h new file mode 100644 index 0000000..6a64b98 --- /dev/null +++ b/src/pathbar_p.h @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2016 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_PATHBAR_P_H +#define FM_PATHBAR_P_H + +#include +#include +#include +#include +#include +#include + +namespace Fm { + +class PathButton: public QToolButton { + Q_OBJECT +public: + PathButton(std::string name, QString displayName, bool isRoot = false, QWidget* parent = nullptr): + QToolButton(parent), + name_{name} { + + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::MinimumExpanding); + setCheckable(true); + setAutoExclusive(true); + setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + /* respect the toolbar icon size (can be set with some styles) */ + int icnSize = style()->pixelMetric(QStyle::PM_ToolBarIconSize); + setIconSize(QSize(icnSize, icnSize)); + + setText(displayName); + + if(isRoot) { /* this element is root */ + QIcon icon = QIcon::fromTheme("drive-harddisk"); + setIcon(icon); + } + } + + void changeEvent(QEvent* event) override { + QToolButton::changeEvent(event); + if(event->type() == QEvent::StyleChange) { + int icnSize = style()->pixelMetric(QStyle::PM_ToolBarIconSize); + setIconSize(QSize(icnSize, icnSize)); + } + } + + std::string name() const { + return name_; + } + + void setName(const std::string& name) { + name_ = name; + } + +private: + QString displayName_; + std::string name_; +}; + +} // namespace Fm + +#endif // FM_PATHBAR_P_H diff --git a/src/pathedit.cpp b/src/pathedit.cpp new file mode 100644 index 0000000..27b3a2a --- /dev/null +++ b/src/pathedit.cpp @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "pathedit.h" +#include "pathedit_p.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Fm { + +void PathEditJob::runJob() { + GError* err = nullptr; + GFileEnumerator* enu = g_file_enumerate_children(dirName, + // G_FILE_ATTRIBUTE_STANDARD_NAME"," + G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME"," + G_FILE_ATTRIBUTE_STANDARD_TYPE, + G_FILE_QUERY_INFO_NONE, cancellable, + &err); + if(enu) { + while(!g_cancellable_is_cancelled(cancellable)) { + GFileInfo* inf = g_file_enumerator_next_file(enu, cancellable, &err); + if(inf) { + GFileType type = g_file_info_get_file_type(inf); + if(type == G_FILE_TYPE_DIRECTORY) { + const char* name = g_file_info_get_display_name(inf); + // FIXME: encoding conversion here? + subDirs.append(QString::fromUtf8(name)); + } + g_object_unref(inf); + } + else { + if(err) { + g_error_free(err); + err = nullptr; + } + else { /* EOF */ + break; + } + } + } + g_file_enumerator_close(enu, cancellable, nullptr); + g_object_unref(enu); + } + // finished! let's update the UI in the main thread + Q_EMIT finished(); + QThread::currentThread()->quit(); +} + + +PathEdit::PathEdit(QWidget* parent): + QLineEdit(parent), + completer_(new QCompleter()), + model_(new QStringListModel()), + cancellable_(nullptr) { + setCompleter(completer_); + completer_->setModel(model_); + connect(this, &PathEdit::textChanged, this, &PathEdit::onTextChanged); + connect(this, &PathEdit::textEdited, this, &PathEdit::onTextEdited); +} + +PathEdit::~PathEdit() { + delete completer_; + if(model_) { + delete model_; + } + if(cancellable_) { + g_cancellable_cancel(cancellable_); + g_object_unref(cancellable_); + } +} + +void PathEdit::focusInEvent(QFocusEvent* e) { + QLineEdit::focusInEvent(e); + // build the completion list only when we have the keyboard focus + reloadCompleter(true); +} + +void PathEdit::focusOutEvent(QFocusEvent* e) { + QLineEdit::focusOutEvent(e); + // free the completion list since we don't need it anymore + freeCompleter(); +} + +bool PathEdit::event(QEvent* e) { + // Stop Qt from moving the keyboard focus to the next widget when "Tab" is pressed. + // Instead, we need to do auto-completion in this case. + if(e->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast(e); + if(keyEvent->key() == Qt::Key_Tab && keyEvent->modifiers() == Qt::NoModifier) { // Tab key is pressed + e->accept(); + // do auto-completion when the user press the Tab key. + // This fixes #201: https://github.com/lxde/pcmanfm-qt/issues/201 + autoComplete(); + return true; + } + } + return QLineEdit::event(e); +} + +void PathEdit::onTextEdited(const QString& text) { + // just replace start tilde with home path if text is changed by user + if(text == QLatin1String("~") || text.startsWith(QLatin1String("~/"))) { + QString txt(text); + txt.replace(0, 1, QDir::homePath()); + setText(txt); // emits textChanged() + return; + } +} + +void PathEdit::onTextChanged(const QString& text) { + if(text == QLatin1String("~") || text.startsWith(QLatin1String("~/"))) { + // do nothing with a start tilde because neither Fm::FilePath nor autocompletion + // understands it; instead, wait until textChanged() is emitted again without it + // WARNING: replacing tilde may not be safe here + return; + } + int pos = text.lastIndexOf('/'); + if(pos >= 0) { + ++pos; + } + else { + pos = text.length(); + } + QString newPrefix = text.left(pos); + if(currentPrefix_ != newPrefix) { + currentPrefix_ = newPrefix; + // only build the completion list if we have the keyboard focus + // if we don't have the focus now, then we'll rebuild the completion list + // when focusInEvent happens. this avoid unnecessary dir loading. + if(hasFocus()) { + reloadCompleter(false); + } + } +} + +void PathEdit::autoComplete() { + // find longest common prefix of the strings currently shown in the candidate list + QAbstractItemModel* model = completer_->completionModel(); + if(model->rowCount() > 0) { + int minLen = text().length(); + QString commonPrefix = model->data(model->index(0, 0)).toString(); + for(int row = 1; row < model->rowCount() && commonPrefix.length() > minLen; ++row) { + QModelIndex index = model->index(row, 0); + QString rowText = model->data(index).toString(); + int prefixLen = 0; + while(prefixLen < rowText.length() && prefixLen < commonPrefix.length() && rowText[prefixLen] == commonPrefix[prefixLen]) { + ++prefixLen; + } + commonPrefix.truncate(prefixLen); + } + if(commonPrefix.length() > minLen) { + setText(commonPrefix); + } + } +} + +void PathEdit::reloadCompleter(bool triggeredByFocusInEvent) { + // parent dir has been changed, reload dir list + // if(currentPrefix_[0] == "~") { // special case for home dir + // cancel running dir-listing jobs, if there's any + if(cancellable_) { + g_cancellable_cancel(cancellable_); + g_object_unref(cancellable_); + } + + // create a new job to do dir listing + PathEditJob* job = new PathEditJob(); + job->edit = this; + job->triggeredByFocusInEvent = triggeredByFocusInEvent; + // need to use fm_file_new_for_commandline_arg() rather than g_file_new_for_commandline_arg(). + // otherwise, our own vfs, such as menu://, won't be loaded. + job->dirName = fm_file_new_for_commandline_arg(currentPrefix_.toLocal8Bit().constData()); + // qDebug("load: %s", g_file_get_uri(data->dirName)); + cancellable_ = g_cancellable_new(); + job->cancellable = (GCancellable*)g_object_ref(cancellable_); + + // launch a new worker thread to handle the job + QThread* thread = new QThread(); + job->moveToThread(thread); + connect(job, &PathEditJob::finished, this, &PathEdit::onJobFinished, Qt::BlockingQueuedConnection); + // connect(job, &PathEditJob::finished, thread, &QThread::quit); + connect(thread, &QThread::started, job, &PathEditJob::runJob); + connect(thread, &QThread::finished, thread, &QObject::deleteLater); + connect(thread, &QThread::finished, job, &QObject::deleteLater); + thread->start(QThread::LowPriority); +} + +void PathEdit::freeCompleter() { + if(cancellable_) { + g_cancellable_cancel(cancellable_); + g_object_unref(cancellable_); + cancellable_ = nullptr; + } + model_->setStringList(QStringList()); +} + +// This slot is called from main thread so it's safe to access the GUI +void PathEdit::onJobFinished() { + PathEditJob* data = static_cast(sender()); + if(!g_cancellable_is_cancelled(data->cancellable)) { + // update the completer only if the job is not cancelled + QStringList::iterator it; + for(it = data->subDirs.begin(); it != data->subDirs.end(); ++it) { + // qDebug("%s", it->toUtf8().constData()); + *it = (currentPrefix_ % *it); + } + model_->setStringList(data->subDirs); + // trigger completion manually + if(hasFocus() && !data->triggeredByFocusInEvent) { + completer_->complete(); + } + } + else { + model_->setStringList(QStringList()); + } + if(cancellable_) { + g_object_unref(cancellable_); + cancellable_ = nullptr; + } +} + +} // namespace Fm diff --git a/src/pathedit.h b/src/pathedit.h new file mode 100644 index 0000000..6385ddf --- /dev/null +++ b/src/pathedit.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_PATHEDIT_H +#define FM_PATHEDIT_H + +#include "libfmqtglobals.h" +#include +#include + +class QCompleter; +class QStringListModel; + +namespace Fm { + +class PathEditJob; + +class LIBFM_QT_API PathEdit : public QLineEdit { + Q_OBJECT +public: + explicit PathEdit(QWidget* parent = 0); + virtual ~PathEdit(); + +protected: + virtual void focusInEvent(QFocusEvent* e); + virtual void focusOutEvent(QFocusEvent* e); + virtual bool event(QEvent* e); + +private Q_SLOTS: + void onTextChanged(const QString& text); + void onTextEdited(const QString& text); + +private: + void autoComplete(); + void reloadCompleter(bool triggeredByFocusInEvent = false); + void freeCompleter(); + void onJobFinished(); + +private: + QCompleter* completer_; + QStringListModel* model_; + QString currentPrefix_; + GCancellable* cancellable_; +}; + +} + +#endif // FM_PATHEDIT_H diff --git a/src/pathedit_p.h b/src/pathedit_p.h new file mode 100644 index 0000000..ce0e8f7 --- /dev/null +++ b/src/pathedit_p.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_PATHEDIT_P_H +#define FM_PATHEDIT_P_H + +#include +#include + +namespace Fm { + +class PathEdit; + +class PathEditJob : public QObject { + Q_OBJECT +public: + GCancellable* cancellable; + GFile* dirName; + QStringList subDirs; + PathEdit* edit; + bool triggeredByFocusInEvent; + + ~PathEditJob() { + g_object_unref(dirName); + g_object_unref(cancellable); + } + +Q_SIGNALS: + void finished(); + +public Q_SLOTS: + void runJob(); + +}; + +} + +#endif // FM_PATHEDIT_P_H diff --git a/src/placesmodel.cpp b/src/placesmodel.cpp new file mode 100644 index 0000000..d6d3776 --- /dev/null +++ b/src/placesmodel.cpp @@ -0,0 +1,612 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "placesmodel.h" +#include "icontheme.h" +#include +#include +#include +#include +#include +#include +#include "utilities.h" +#include "placesmodelitem.h" + +namespace Fm { + +std::weak_ptr PlacesModel::globalInstance_; + +PlacesModel::PlacesModel(QObject* parent): + QStandardItemModel(parent), + showApplications_(true), + showDesktop_(true), + // FIXME: this seems to be broken when porting to new API. + ejectIcon_(QIcon::fromTheme("media-eject")) { + setColumnCount(2); + + placesRoot = new QStandardItem(tr("Places")); + placesRoot->setSelectable(false); + placesRoot->setColumnCount(2); + appendRow(placesRoot); + + homeItem = new PlacesModelItem("user-home", g_get_user_name(), Fm::FilePath::homeDir()); + placesRoot->appendRow(homeItem); + + desktopItem = new PlacesModelItem("user-desktop", tr("Desktop"), + Fm::FilePath::fromLocalPath(QStandardPaths::writableLocation(QStandardPaths::DesktopLocation).toLocal8Bit().constData())); + placesRoot->appendRow(desktopItem); + + createTrashItem(); + + // FIXME: add an option to hide network:/// + if(true) { + computerItem = new PlacesModelItem("computer", tr("Computer"), Fm::FilePath::fromUri("computer:///")); + placesRoot->appendRow(computerItem); + } + else { + computerItem = nullptr; + } + + // FIXME: add an option to hide applications:/// + const char* applicaion_icon_names[] = {"system-software-install", "applications-accessories", "application-x-executable"}; + // NOTE: g_themed_icon_new_from_names() accepts char**, but actually const char** is OK. + Fm::GIconPtr gicon{g_themed_icon_new_from_names((char**)applicaion_icon_names, G_N_ELEMENTS(applicaion_icon_names)), false}; + auto fmicon = Fm::IconInfo::fromGIcon(std::move(gicon)); + applicationsItem = new PlacesModelItem(fmicon, tr("Applications"), Fm::FilePath::fromUri("menu:///applications/")); + placesRoot->appendRow(applicationsItem); + + // FIXME: add an option to hide network:/// + if(true) { + const char* network_icon_names[] = {"network", "folder-network", "folder"}; + // NOTE: g_themed_icon_new_from_names() accepts char**, but actually const char** is OK. + Fm::GIconPtr gicon{g_themed_icon_new_from_names((char**)network_icon_names, G_N_ELEMENTS(network_icon_names)), false}; + auto fmicon = Fm::IconInfo::fromGIcon(std::move(gicon)); + networkItem = new PlacesModelItem(fmicon, tr("Network"), Fm::FilePath::fromUri("network:///")); + placesRoot->appendRow(networkItem); + } + else { + networkItem = nullptr; + } + + devicesRoot = new QStandardItem(tr("Devices")); + devicesRoot->setSelectable(false); + devicesRoot->setColumnCount(2); + appendRow(devicesRoot); + + // volumes + volumeMonitor = g_volume_monitor_get(); + if(volumeMonitor) { + g_signal_connect(volumeMonitor, "volume-added", G_CALLBACK(onVolumeAdded), this); + g_signal_connect(volumeMonitor, "volume-removed", G_CALLBACK(onVolumeRemoved), this); + g_signal_connect(volumeMonitor, "volume-changed", G_CALLBACK(onVolumeChanged), this); + g_signal_connect(volumeMonitor, "mount-added", G_CALLBACK(onMountAdded), this); + g_signal_connect(volumeMonitor, "mount-changed", G_CALLBACK(onMountChanged), this); + g_signal_connect(volumeMonitor, "mount-removed", G_CALLBACK(onMountRemoved), this); + + // add volumes to side-pane + GList* vols = g_volume_monitor_get_volumes(volumeMonitor); + GList* l; + for(l = vols; l; l = l->next) { + GVolume* volume = G_VOLUME(l->data); + onVolumeAdded(volumeMonitor, volume, this); + g_object_unref(volume); + } + g_list_free(vols); + + /* add mounts to side-pane */ + vols = g_volume_monitor_get_mounts(volumeMonitor); + for(l = vols; l; l = l->next) { + GMount* mount = G_MOUNT(l->data); + GVolume* volume = g_mount_get_volume(mount); + if(volume) { + g_object_unref(volume); + } + else { /* network mounts or others */ + gboolean shadowed = FALSE; +#if GLIB_CHECK_VERSION(2, 20, 0) + shadowed = g_mount_is_shadowed(mount); +#endif + // according to gio API doc, a shadowed mount should not be visible to the user + if(shadowed) { + shadowedMounts_.push_back(mount); + continue; + } + else { + PlacesModelItem* item = new PlacesModelMountItem(mount); + devicesRoot->appendRow(item); + } + } + g_object_unref(mount); + } + g_list_free(vols); + } + + // bookmarks + bookmarksRoot = new QStandardItem(tr("Bookmarks")); + bookmarksRoot->setSelectable(false); + bookmarksRoot->setColumnCount(2); + appendRow(bookmarksRoot); + + bookmarks = Fm::Bookmarks::globalInstance(); + loadBookmarks(); + connect(bookmarks.get(), &Fm::Bookmarks::changed, this, &PlacesModel::onBookmarksChanged); +} + +void PlacesModel::loadBookmarks() { + for(auto& bm_item: bookmarks->items()) { + PlacesModelBookmarkItem* item = new PlacesModelBookmarkItem(bm_item); + bookmarksRoot->appendRow(item); + } +} + +PlacesModel::~PlacesModel() { + if(volumeMonitor) { + g_signal_handlers_disconnect_by_func(volumeMonitor, (gpointer)G_CALLBACK(onVolumeAdded), this); + g_signal_handlers_disconnect_by_func(volumeMonitor, (gpointer)G_CALLBACK(onVolumeRemoved), this); + g_signal_handlers_disconnect_by_func(volumeMonitor, (gpointer)G_CALLBACK(onVolumeChanged), this); + g_signal_handlers_disconnect_by_func(volumeMonitor, (gpointer)G_CALLBACK(onMountAdded), this); + g_signal_handlers_disconnect_by_func(volumeMonitor, (gpointer)G_CALLBACK(onMountChanged), this); + g_signal_handlers_disconnect_by_func(volumeMonitor, (gpointer)G_CALLBACK(onMountRemoved), this); + g_object_unref(volumeMonitor); + } + if(trashMonitor_) { + g_signal_handlers_disconnect_by_func(trashMonitor_, (gpointer)G_CALLBACK(onTrashChanged), this); + g_object_unref(trashMonitor_); + } + + Q_FOREACH(GMount* mount, shadowedMounts_) { + g_object_unref(mount); + } +} + +// static +void PlacesModel::onTrashChanged(GFileMonitor* /*monitor*/, GFile* /*gf*/, GFile* /*other*/, GFileMonitorEvent /*evt*/, PlacesModel* pThis) { + QTimer::singleShot(0, pThis, SLOT(updateTrash())); +} + +void PlacesModel::updateTrash() { + + struct UpdateTrashData { + QPointer model; + GFile* gf; + UpdateTrashData(PlacesModel* _model) : model(_model) { + gf = fm_file_new_for_uri("trash:///"); + } + ~UpdateTrashData() { + g_object_unref(gf); + } + }; + + if(trashItem_) { + UpdateTrashData* data = new UpdateTrashData(this); + g_file_query_info_async(data->gf, G_FILE_ATTRIBUTE_TRASH_ITEM_COUNT, G_FILE_QUERY_INFO_NONE, G_PRIORITY_LOW, nullptr, + [](GObject * /*source_object*/, GAsyncResult * res, gpointer user_data) { + // the callback lambda function is called when the asyn query operation is finished + UpdateTrashData* data = reinterpret_cast(user_data); + PlacesModel* _this = data->model.data(); + if(_this != nullptr) { // ensure that our model object is not deleted yet + Fm::GFileInfoPtr inf{g_file_query_info_finish(data->gf, res, nullptr), false}; + if(inf) { + if(_this->trashItem_ != nullptr) { // it's possible that when we finish, the trash item is removed + guint32 n = g_file_info_get_attribute_uint32(inf.get(), G_FILE_ATTRIBUTE_TRASH_ITEM_COUNT); + const char* icon_name = n > 0 ? "user-trash-full" : "user-trash"; + auto icon = Fm::IconInfo::fromName(icon_name); + _this->trashItem_->setIcon(std::move(icon)); + } + } + } + delete data; // free the data used for this async operation. + }, data); + } +} + +void PlacesModel::createTrashItem() { + GFile* gf; + gf = fm_file_new_for_uri("trash:///"); + // check if trash is supported by the current vfs + // if gvfs is not installed, this can be unavailable. + if(!g_file_query_exists(gf, nullptr)) { + g_object_unref(gf); + trashItem_ = nullptr; + trashMonitor_ = nullptr; + return; + } + trashItem_ = new PlacesModelItem("user-trash", tr("Trash"), Fm::FilePath::fromUri("trash:///")); + + trashMonitor_ = fm_monitor_directory(gf, nullptr); + if(trashMonitor_) { + g_signal_connect(trashMonitor_, "changed", G_CALLBACK(onTrashChanged), this); + } + g_object_unref(gf); + + placesRoot->insertRow(desktopItem->row() + 1, trashItem_); + QTimer::singleShot(0, this, SLOT(updateTrash())); +} + +void PlacesModel::setShowApplications(bool show) { + if(showApplications_ != show) { + showApplications_ = show; + } +} + +void PlacesModel::setShowDesktop(bool show) { + if(showDesktop_ != show) { + showDesktop_ = show; + } +} + +void PlacesModel::setShowTrash(bool show) { + if(show) { + if(!trashItem_) { + createTrashItem(); + } + } + else { + if(trashItem_) { + if(trashMonitor_) { + g_signal_handlers_disconnect_by_func(trashMonitor_, (gpointer)G_CALLBACK(onTrashChanged), this); + g_object_unref(trashMonitor_); + trashMonitor_ = nullptr; + } + placesRoot->removeRow(trashItem_->row()); // delete trashItem_; + trashItem_ = nullptr; + } + } +} + +PlacesModelItem* PlacesModel::itemFromPath(const Fm::FilePath &path) { + PlacesModelItem* item = itemFromPath(placesRoot, path); + if(!item) { + item = itemFromPath(devicesRoot, path); + } + if(!item) { + item = itemFromPath(bookmarksRoot, path); + } + return item; +} + +PlacesModelItem* PlacesModel::itemFromPath(QStandardItem* rootItem, const Fm::FilePath &path) { + int rowCount = rootItem->rowCount(); + for(int i = 0; i < rowCount; ++i) { + PlacesModelItem* item = static_cast(rootItem->child(i, 0)); + if(item->path() == path) { + return item; + } + } + return nullptr; +} + +PlacesModelVolumeItem* PlacesModel::itemFromVolume(GVolume* volume) { + int rowCount = devicesRoot->rowCount(); + for(int i = 0; i < rowCount; ++i) { + PlacesModelItem* item = static_cast(devicesRoot->child(i, 0)); + if(item->type() == PlacesModelItem::Volume) { + PlacesModelVolumeItem* volumeItem = static_cast(item); + if(volumeItem->volume() == volume) { + return volumeItem; + } + } + } + return nullptr; +} + +PlacesModelMountItem* PlacesModel::itemFromMount(GMount* mount) { + int rowCount = devicesRoot->rowCount(); + for(int i = 0; i < rowCount; ++i) { + PlacesModelItem* item = static_cast(devicesRoot->child(i, 0)); + if(item->type() == PlacesModelItem::Mount) { + PlacesModelMountItem* mountItem = static_cast(item); + if(mountItem->mount() == mount) { + return mountItem; + } + } + } + return nullptr; +} + +PlacesModelBookmarkItem* PlacesModel::itemFromBookmark(std::shared_ptr bkitem) { + int rowCount = bookmarksRoot->rowCount(); + for(int i = 0; i < rowCount; ++i) { + PlacesModelBookmarkItem* item = static_cast(bookmarksRoot->child(i, 0)); + if(item->bookmark() == bkitem) { + return item; + } + } + return nullptr; +} + +void PlacesModel::onMountAdded(GVolumeMonitor* /*monitor*/, GMount* mount, PlacesModel* pThis) { + // according to gio API doc, a shadowed mount should not be visible to the user +#if GLIB_CHECK_VERSION(2, 20, 0) + if(g_mount_is_shadowed(mount)) { + if(pThis->shadowedMounts_.indexOf(mount) == -1) { + pThis->shadowedMounts_.push_back(G_MOUNT(g_object_ref(mount))); + } + return; + } +#endif + GVolume* vol = g_mount_get_volume(mount); + if(vol) { // mount-added is also emitted when a volume is newly mounted. + PlacesModelVolumeItem* item = pThis->itemFromVolume(vol); + if(item && !item->path()) { + // update the mounted volume and show a button for eject. + Fm::FilePath path{g_mount_get_root(mount), false}; + item->setPath(path); + // update the mount indicator (eject button) + QStandardItem* ejectBtn = item->parent()->child(item->row(), 1); + Q_ASSERT(ejectBtn); + ejectBtn->setIcon(pThis->ejectIcon_); + } + g_object_unref(vol); + } + else { // network mounts and others + PlacesModelMountItem* item = pThis->itemFromMount(mount); + /* for some unknown reasons, sometimes we get repeated mount-added + * signals and added a device more than one. So, make a sanity check here. */ + if(!item) { + item = new PlacesModelMountItem(mount); + QStandardItem* eject_btn = new QStandardItem(pThis->ejectIcon_, QString()); + pThis->devicesRoot->appendRow(QList() << item << eject_btn); + } + } +} + +void PlacesModel::onMountChanged(GVolumeMonitor* monitor, GMount* mount, PlacesModel* pThis) { + gboolean shadowed = FALSE; + // according to gio API doc, a shadowed mount should not be visible to the user +#if GLIB_CHECK_VERSION(2, 20, 0) + shadowed = g_mount_is_shadowed(mount); + // qDebug() << "changed:" << mount << shadowed; +#endif + PlacesModelMountItem* item = pThis->itemFromMount(mount); + if(item) { + if(shadowed) { // if a visible item becomes shadowed, remove it from the model + pThis->shadowedMounts_.push_back(G_MOUNT(g_object_ref(mount))); // remember the shadowed mount + pThis->devicesRoot->removeRow(item->row()); + } + else { // otherwise, update its status + item->update(); + } + } + else { +#if GLIB_CHECK_VERSION(2, 20, 0) + if(!shadowed) { // if a mount is unshadowed + int i = pThis->shadowedMounts_.indexOf(mount); + if(i != -1) { // a previously shadowed mount is unshadowed + pThis->shadowedMounts_.removeAt(i); + onMountAdded(monitor, mount, pThis); // add it to our model again + } + } +#endif + } +} + +void PlacesModel::onMountRemoved(GVolumeMonitor* monitor, GMount* mount, PlacesModel* pThis) { + GVolume* vol = g_mount_get_volume(mount); + // qDebug() << "mount removed" << mount << "volume umounted: " << vol; + if(vol) { + // a volume is being unmounted + // NOTE: Due to some problems of gvfs, sometimes the volume does not receive + // "change" signal so it does not update the eject button. Let's workaround + // this by calling onVolumeChanged() handler manually. (This is needed for mtp://) + onVolumeChanged(monitor, vol, pThis); + g_object_unref(vol); + } + else { // network mounts and others + PlacesModelMountItem* item = pThis->itemFromMount(mount); + if(item) { + pThis->devicesRoot->removeRow(item->row()); + } + } + +#if GLIB_CHECK_VERSION(2, 20, 0) + // NOTE: g_mount_is_shadowed() sometimes returns FALSE here even if the mount is shadowed. + // I don't know whether this is a bug in gvfs or not. + // So let's check if its in our list instead. + if(pThis->shadowedMounts_.removeOne(mount)) { + // if this is a shadowed mount + // qDebug() << "remove shadow mount"; + g_object_unref(mount); + } +#endif +} + +void PlacesModel::onVolumeAdded(GVolumeMonitor* /*monitor*/, GVolume* volume, PlacesModel* pThis) { + // for some unknown reasons, sometimes we get repeated volume-added + // signals and added a device more than one. So, make a sanity check here. + PlacesModelVolumeItem* volumeItem = pThis->itemFromVolume(volume); + if(!volumeItem) { + volumeItem = new PlacesModelVolumeItem(volume); + QStandardItem* ejectBtn = new QStandardItem(); + if(volumeItem->isMounted()) { + ejectBtn->setIcon(pThis->ejectIcon_); + } + pThis->devicesRoot->appendRow(QList() << volumeItem << ejectBtn); + } +} + +void PlacesModel::onVolumeChanged(GVolumeMonitor* /*monitor*/, GVolume* volume, PlacesModel* pThis) { + PlacesModelVolumeItem* item = pThis->itemFromVolume(volume); + if(item) { + item->update(); + if(!item->isMounted()) { // the volume is unmounted, remove the eject button if needed + // remove the eject button for the volume (at column 1 of the same row) + QStandardItem* ejectBtn = item->parent()->child(item->row(), 1); + Q_ASSERT(ejectBtn); + ejectBtn->setIcon(QIcon()); + } + } +} + +void PlacesModel::onVolumeRemoved(GVolumeMonitor* /*monitor*/, GVolume* volume, PlacesModel* pThis) { + PlacesModelVolumeItem* item = pThis->itemFromVolume(volume); + if(item) { + pThis->devicesRoot->removeRow(item->row()); + } +} + +void PlacesModel::onBookmarksChanged() { + // remove all items + bookmarksRoot->removeRows(0, bookmarksRoot->rowCount()); + loadBookmarks(); +} + +Qt::ItemFlags PlacesModel::flags(const QModelIndex& index) const { + if(index.column() == 1) { // make 2nd column of every row selectable. + return Qt::ItemIsSelectable | Qt::ItemIsEnabled; + } + if(!index.parent().isValid()) { // root items + if(index.row() == 2) { // bookmarks root + return Qt::ItemIsEnabled | Qt::ItemIsDropEnabled; + } + else { + return Qt::ItemIsEnabled; + } + } + return QStandardItemModel::flags(index); +} + + +QVariant PlacesModel::data(const QModelIndex& index, int role) const { + if(index.column() == 0 && index.parent().isValid()) { + PlacesModelItem* item = static_cast(QStandardItemModel::itemFromIndex(index)); + if(item != nullptr) { + switch(role) { + case FileInfoRole: + return QVariant::fromValue(item->fileInfo()); + case FmIconRole: + return QVariant::fromValue(item->icon()); + } + } + } + return QStandardItemModel::data(index, role); +} + +std::shared_ptr PlacesModel::globalInstance() { + auto model = globalInstance_.lock(); + if(!model) { + model = std::make_shared(); + globalInstance_ = model; + } + return model; +} + + +bool PlacesModel::dropMimeData(const QMimeData* data, Qt::DropAction /*action*/, int row, int column, const QModelIndex& parent) { + QStandardItem* item = itemFromIndex(parent); + if(data->hasFormat("application/x-bookmark-row")) { // the data being dopped is a bookmark row + // decode it and do bookmark reordering + QByteArray buf = data->data("application/x-bookmark-row"); + QDataStream stream(&buf, QIODevice::ReadOnly); + int oldPos = -1; + char* pathStr = nullptr; + stream >> oldPos >> pathStr; + // find the source bookmark item being dragged + auto allBookmarks = bookmarks->items(); + auto& draggedItem = allBookmarks[oldPos]; + // If we cannot find the dragged bookmark item at position , or we find an item, + // but the path of the item is not the same as what we expected, than it's the wrong item. + // This means that the bookmarks are changed during our dnd processing, which is an extremely rare case. + auto draggedPath = Fm::FilePath::fromPathStr(pathStr); + if(!draggedItem || draggedItem->path() != draggedPath) { + delete []pathStr; + return false; + } + delete []pathStr; + + int newPos = -1; + if(row == -1 && column == -1) { // drop on an item + // we only allow dropping on an bookmark item + if(item && item->parent() == bookmarksRoot) { + newPos = parent.row(); + } + } + else { // drop on a position between items + if(item == bookmarksRoot) { // we only allow dropping on a bookmark item + newPos = row; + } + } + if(newPos != -1 && newPos != oldPos) { // reorder the bookmark item + bookmarks->reorder(draggedItem, newPos); + } + } + else if(data->hasUrls()) { // files uris are dropped + if(row == -1 && column == -1) { // drop uris on an item + if(item && item->parent()) { // need to be a child item + PlacesModelItem* placesItem = static_cast(item); + if(placesItem->path()) { + qDebug() << "dropped dest:" << placesItem->text(); + // TODO: copy or move the dragged files to the dir pointed by the item. + qDebug() << "drop on" << item->text(); + } + } + } + else { // drop uris on a position between items + if(item == bookmarksRoot) { // we only allow dropping on blank row of bookmarks section + auto paths = pathListFromQUrls(data->urls()); + for(auto& path: paths) { + // FIXME: this is a blocking call + if(g_file_query_file_type(path.gfile().get(), G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + nullptr) == G_FILE_TYPE_DIRECTORY) { + auto disp_name = path.baseName(); + bookmarks->insert(path, disp_name.get(), row); + } + return true; + } + } + } + } + return false; +} + +// we only support dragging bookmark items and use our own +// custom pseudo-mime-type: application/x-bookmark-row +QMimeData* PlacesModel::mimeData(const QModelIndexList& indexes) const { + if(!indexes.isEmpty()) { + // we only allow dragging one bookmark item at a time, so handle the first index only. + QModelIndex index = indexes.first(); + QStandardItem* item = itemFromIndex(index); + // ensure that it's really a bookmark item + if(item && item->parent() == bookmarksRoot) { + PlacesModelBookmarkItem* bookmarkItem = static_cast(item); + QMimeData* mime = new QMimeData(); + QByteArray data; + QDataStream stream(&data, QIODevice::WriteOnly); + // There is no safe and cross-process way to store a reference of a row. + // Let's store the pos, name, and path of the bookmark item instead. + auto pathStr = bookmarkItem->path().toString(); + stream << index.row() << pathStr.get(); + mime->setData("application/x-bookmark-row", data); + return mime; + } + } + return nullptr; +} + +QStringList PlacesModel::mimeTypes() const { + return QStringList() << "application/x-bookmark-row" << "text/uri-list"; +} + +Qt::DropActions PlacesModel::supportedDropActions() const { + return QStandardItemModel::supportedDropActions(); +} + + +} // namespace Fm diff --git a/src/placesmodel.h b/src/placesmodel.h new file mode 100644 index 0000000..08d0a5a --- /dev/null +++ b/src/placesmodel.h @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_PLACESMODEL_H +#define FM_PLACESMODEL_H + +#include "libfmqtglobals.h" +#include +#include +#include +#include +#include + +#include + +#include "core/filepath.h" +#include "core/bookmarks.h" + +namespace Fm { + +class PlacesModelItem; +class PlacesModelVolumeItem; +class PlacesModelMountItem; +class PlacesModelBookmarkItem; + +class LIBFM_QT_API PlacesModel : public QStandardItemModel { + Q_OBJECT + friend class PlacesView; +public: + + enum { + FileInfoRole = Qt::UserRole, + FmIconRole + }; + + // QAction used for popup menus + class ItemAction : public QAction { + public: + explicit ItemAction(const QModelIndex& index, QString text, QObject* parent = 0): + QAction(text, parent), + index_(index) { + } + + QPersistentModelIndex& index() { + return index_; + } + private: + QPersistentModelIndex index_; + }; + +public: + explicit PlacesModel(QObject* parent = 0); + virtual ~PlacesModel(); + + bool showTrash() { + return trashItem_ != nullptr; + } + void setShowTrash(bool show); + + bool showApplications() { + return showApplications_; + } + void setShowApplications(bool show); + + bool showDesktop() { + return showDesktop_; + } + void setShowDesktop(bool show); + + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + + static std::shared_ptr globalInstance(); + +public Q_SLOTS: + void updateTrash(); + void onBookmarksChanged(); + +protected: + + PlacesModelItem* itemFromPath(const Fm::FilePath& path); + PlacesModelItem* itemFromPath(QStandardItem* rootItem, const Fm::FilePath & path); + PlacesModelVolumeItem* itemFromVolume(GVolume* volume); + PlacesModelMountItem* itemFromMount(GMount* mount); + PlacesModelBookmarkItem* itemFromBookmark(std::shared_ptr bkitem); + + virtual Qt::ItemFlags flags(const QModelIndex& index) const; + virtual QStringList mimeTypes() const; + virtual QMimeData* mimeData(const QModelIndexList& indexes) const; + virtual bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent); + Qt::DropActions supportedDropActions() const; + + void createTrashItem(); + +private: + void loadBookmarks(); + + static void onVolumeAdded(GVolumeMonitor* monitor, GVolume* volume, PlacesModel* pThis); + static void onVolumeRemoved(GVolumeMonitor* monitor, GVolume* volume, PlacesModel* pThis); + static void onVolumeChanged(GVolumeMonitor* monitor, GVolume* volume, PlacesModel* pThis); + static void onMountAdded(GVolumeMonitor* monitor, GMount* mount, PlacesModel* pThis); + static void onMountRemoved(GVolumeMonitor* monitor, GMount* mount, PlacesModel* pThis); + static void onMountChanged(GVolumeMonitor* monitor, GMount* mount, PlacesModel* pThis); + + static void onTrashChanged(GFileMonitor* monitor, GFile* gf, GFile* other, GFileMonitorEvent evt, PlacesModel* pThis); + +private: + std::shared_ptr bookmarks; + GVolumeMonitor* volumeMonitor; + QList jobs; + bool showApplications_; + bool showDesktop_; + QStandardItem* placesRoot; + QStandardItem* devicesRoot; + QStandardItem* bookmarksRoot; + PlacesModelItem* trashItem_; + GFileMonitor* trashMonitor_; + PlacesModelItem* desktopItem; + PlacesModelItem* homeItem; + PlacesModelItem* computerItem; + PlacesModelItem* networkItem; + PlacesModelItem* applicationsItem; + QIcon ejectIcon_; + QList shadowedMounts_; + + static std::weak_ptr globalInstance_; +}; + +} + +#endif // FM_PLACESMODEL_H diff --git a/src/placesmodelitem.cpp b/src/placesmodelitem.cpp new file mode 100644 index 0000000..cc1b2e3 --- /dev/null +++ b/src/placesmodelitem.cpp @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "placesmodelitem.h" +#include "icontheme.h" +#include +#include + +namespace Fm { + +PlacesModelItem::PlacesModelItem(): + QStandardItem(), + fileInfo_(nullptr), + icon_(nullptr) { +} + +PlacesModelItem::PlacesModelItem(const char* iconName, QString title, Fm::FilePath path): + QStandardItem(title), + path_{std::move(path)}, + icon_(Fm::IconInfo::fromName(iconName)) { + if(icon_) { + QStandardItem::setIcon(icon_->qicon()); + } + setEditable(false); +} + +PlacesModelItem::PlacesModelItem(std::shared_ptr icon, QString title, Fm::FilePath path): + QStandardItem(title), + path_{std::move(path)}, + icon_{std::move(icon)} { + if(icon_) { + QStandardItem::setIcon(icon_->qicon()); + } + setEditable(false); +} + +PlacesModelItem::PlacesModelItem(QIcon icon, QString title, Fm::FilePath path): + QStandardItem(icon, title), + path_{std::move(path)} { + setEditable(false); +} + +PlacesModelItem::~PlacesModelItem() { +} + + +void PlacesModelItem::setIcon(std::shared_ptr icon) { + icon_= std::move(icon); + if(icon_) { + QStandardItem::setIcon(icon_->qicon()); + } + else { + QStandardItem::setIcon(QIcon()); + } +} + +void PlacesModelItem::setIcon(GIcon* gicon) { + setIcon(Fm::IconInfo::fromGIcon(Fm::GIconPtr{gicon, true})); +} + +void PlacesModelItem::updateIcon() { + if(icon_) { + QStandardItem::setIcon(icon_->qicon()); + } +} + +QVariant PlacesModelItem::data(int role) const { + // we use a QPixmap from FmIcon cache rather than QIcon object for decoration role. + return QStandardItem::data(role); +} + +PlacesModelBookmarkItem::PlacesModelBookmarkItem(std::shared_ptr bm_item): + PlacesModelItem{Fm::IconInfo::fromName("folder"), bm_item->name(), bm_item->path()}, + bookmarkItem_{std::move(bm_item)} { + setEditable(true); +} + +PlacesModelVolumeItem::PlacesModelVolumeItem(GVolume* volume): + PlacesModelItem(), + volume_(reinterpret_cast(g_object_ref(volume))) { + update(); + setEditable(false); +} + +void PlacesModelVolumeItem::update() { + // set title + char* volumeName = g_volume_get_name(volume_); + setText(QString::fromUtf8(volumeName)); + g_free(volumeName); + + // set icon + Fm::GIconPtr gicon{g_volume_get_icon(volume_), false}; + setIcon(gicon.get()); + + // set dir path + Fm::GMountPtr mount{g_volume_get_mount(volume_), false}; + if(mount) { + Fm::FilePath mount_root{g_mount_get_root(mount.get()), false}; + setPath(mount_root); + } + else { + setPath(Fm::FilePath{}); + } +} + + +bool PlacesModelVolumeItem::isMounted() { + GMount* mount = g_volume_get_mount(volume_); + if(mount) { + g_object_unref(mount); + } + return mount != nullptr ? true : false; +} + + +PlacesModelMountItem::PlacesModelMountItem(GMount* mount): + PlacesModelItem(), + mount_(reinterpret_cast(mount)) { + update(); + setEditable(false); +} + +void PlacesModelMountItem::update() { + // set title + setText(QString::fromUtf8(g_mount_get_name(mount_))); + + // set path + Fm::FilePath mount_root{g_mount_get_root(mount_), false}; + setPath(mount_root); + + // set icon + Fm::GIconPtr gicon{g_mount_get_icon(mount_), false}; + setIcon(gicon.get()); +} + +} diff --git a/src/placesmodelitem.h b/src/placesmodelitem.h new file mode 100644 index 0000000..cc5bd57 --- /dev/null +++ b/src/placesmodelitem.h @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_PLACESMODELITEM_H +#define FM_PLACESMODELITEM_H + +#include "libfmqtglobals.h" +#include +#include +#include +#include +#include + +#include "core/fileinfo.h" +#include "core/filepath.h" +#include "core/bookmarks.h" + +namespace Fm { + +// model item +class LIBFM_QT_API PlacesModelItem : public QStandardItem { +public: + enum Type { + Places = QStandardItem::UserType + 1, + Volume, + Mount, + Bookmark + }; + +public: + explicit PlacesModelItem(); + explicit PlacesModelItem(QIcon icon, QString title, Fm::FilePath path = Fm::FilePath{}); + explicit PlacesModelItem(const char* iconName, QString title, Fm::FilePath path = Fm::FilePath{}); + explicit PlacesModelItem(std::shared_ptr icon, QString title, Fm::FilePath path = Fm::FilePath{}); + ~PlacesModelItem(); + + const std::shared_ptr& fileInfo() const { + return fileInfo_; + } + void setFileInfo(std::shared_ptr fileInfo) { + fileInfo_ = std::move(fileInfo); + } + + const Fm::FilePath& path() const { + return path_; + } + void setPath(Fm::FilePath path) { + path_ = std::move(path); + } + + const std::shared_ptr& icon() const { + return icon_; + } + void setIcon(std::shared_ptr icon); + void setIcon(GIcon* gicon); + void updateIcon(); + + QVariant data(int role = Qt::UserRole + 1) const; + + virtual int type() const { + return Places; + } + +private: + Fm::FilePath path_; + std::shared_ptr fileInfo_; + std::shared_ptr icon_; +}; + +class LIBFM_QT_API PlacesModelVolumeItem : public PlacesModelItem { +public: + PlacesModelVolumeItem(GVolume* volume); + bool isMounted(); + bool canEject() { + return g_volume_can_eject(volume_); + } + virtual int type() const { + return Volume; + } + GVolume* volume() { + return volume_; + } + void update(); +private: + GVolume* volume_; +}; + +class LIBFM_QT_API PlacesModelMountItem : public PlacesModelItem { +public: + PlacesModelMountItem(GMount* mount); + virtual int type() const { + return Mount; + } + GMount* mount() const { + return mount_; + } + void update(); +private: + GMount* mount_; +}; + +class LIBFM_QT_API PlacesModelBookmarkItem : public PlacesModelItem { +public: + virtual int type() const { + return Bookmark; + } + PlacesModelBookmarkItem(std::shared_ptr bm_item); + const std::shared_ptr& bookmark() const { + return bookmarkItem_; + } +private: + std::shared_ptr bookmarkItem_; +}; + +} + +#endif // FM_PLACESMODELITEM_H diff --git a/src/placesview.cpp b/src/placesview.cpp new file mode 100644 index 0000000..8fa3609 --- /dev/null +++ b/src/placesview.cpp @@ -0,0 +1,434 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "placesview.h" +#include "placesmodel.h" +#include "placesmodelitem.h" +#include "mountoperation.h" +#include "fileoperation.h" +#include +#include +#include +#include +#include +#include "folderitemdelegate.h" + +namespace Fm { + +PlacesView::PlacesView(QWidget* parent): + QTreeView(parent) { + setRootIsDecorated(false); + setHeaderHidden(true); + setIndentation(12); + + connect(this, &QTreeView::clicked, this, &PlacesView::onClicked); + connect(this, &QTreeView::pressed, this, &PlacesView::onPressed); + + setIconSize(QSize(24, 24)); + + FolderItemDelegate* delegate = new FolderItemDelegate(this, this); + delegate->setFileInfoRole(PlacesModel::FileInfoRole); + delegate->setIconInfoRole(PlacesModel::FmIconRole); + setItemDelegateForColumn(0, delegate); + + model_ = PlacesModel::globalInstance(); + setModel(model_.get()); + + QHeaderView* headerView = header(); + headerView->setSectionResizeMode(0, QHeaderView::Stretch); + headerView->setSectionResizeMode(1, QHeaderView::Fixed); + headerView->setStretchLastSection(false); + expandAll(); + + // FIXME: is there any better way to make the first column span the whole row? + setFirstColumnSpanned(0, QModelIndex(), true); // places root + setFirstColumnSpanned(1, QModelIndex(), true); // devices root + setFirstColumnSpanned(2, QModelIndex(), true); // bookmarks root + + // the 2nd column is for the eject buttons + setSelectionBehavior(QAbstractItemView::SelectRows); // FIXME: why this does not work? + setAllColumnsShowFocus(false); + + setAcceptDrops(true); + setDragEnabled(true); + + // update the umount button's column width based on icon size + onIconSizeChanged(iconSize()); +#if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0) // this signal requires Qt >= 5.5 + connect(this, &QAbstractItemView::iconSizeChanged, this, &PlacesView::onIconSizeChanged); +#endif +} + +PlacesView::~PlacesView() { + // qDebug("delete PlacesView"); +} + +void PlacesView::activateRow(int type, const QModelIndex& index) { + if(!index.parent().isValid()) { // ignore root items + return; + } + PlacesModelItem* item = static_cast(model_->itemFromIndex(index)); + if(item) { + auto path = item->path(); + if(!path) { + // check if mounting volumes is needed + if(item->type() == PlacesModelItem::Volume) { + PlacesModelVolumeItem* volumeItem = static_cast(item); + if(!volumeItem->isMounted()) { + // Mount the volume + GVolume* volume = volumeItem->volume(); + MountOperation* op = new MountOperation(true, this); + op->mount(volume); + // connect(op, SIGNAL(finished(GError*)), SLOT(onMountOperationFinished(GError*))); + // blocking here until the mount operation is finished? + + // FIXME: update status of the volume after mount is finished!! + if(!op->wait()) { + return; + } + path = item->path(); + } + } + } + if(path) { + Q_EMIT chdirRequested(type, path); + } + } +} + +// mouse button pressed +void PlacesView::onPressed(const QModelIndex& index) { + // if middle button is pressed + if(QGuiApplication::mouseButtons() & Qt::MiddleButton) { + // the real item is at column 0 + activateRow(1, 0 == index.column() ? index : index.sibling(index.row(), 0)); + } +} + +void PlacesView::onIconSizeChanged(const QSize& size) { + setColumnWidth(1, size.width() + 5); +} + +void PlacesView::onEjectButtonClicked(PlacesModelItem* item) { + // The eject button is clicked for a device item (volume or mount) + if(item->type() == PlacesModelItem::Volume) { + PlacesModelVolumeItem* volumeItem = static_cast(item); + MountOperation* op = new MountOperation(true, this); + if(volumeItem->canEject()) { // do eject if applicable + op->eject(volumeItem->volume()); + } + else { // otherwise, do unmount instead + op->unmount(volumeItem->volume()); + } + } + else if(item->type() == PlacesModelItem::Mount) { + PlacesModelMountItem* mountItem = static_cast(item); + MountOperation* op = new MountOperation(true, this); + op->unmount(mountItem->mount()); + } + qDebug("PlacesView::onEjectButtonClicked"); +} + +void PlacesView::onClicked(const QModelIndex& index) { + if(!index.parent().isValid()) { // ignore root items + return; + } + + if(index.column() == 0) { + activateRow(0, index); + } + else if(index.column() == 1) { // column 1 contains eject buttons of the mounted devices + if(index.parent() == model_->devicesRoot->index()) { // this is a mounted device + // the eject button is clicked + QModelIndex itemIndex = index.sibling(index.row(), 0); // the real item is at column 0 + PlacesModelItem* item = static_cast(model_->itemFromIndex(itemIndex)); + if(item) { + // eject the volume or the mount + onEjectButtonClicked(item); + } + } + else { + activateRow(0, index.sibling(index.row(), 0)); + } + } +} + +void PlacesView::setCurrentPath(Fm::FilePath path) { + currentPath_ = std::move(path); + if(currentPath_) { + // TODO: search for item with the path in model_ and select it. + PlacesModelItem* item = model_->itemFromPath(currentPath_); + if(item) { + selectionModel()->select(item->index(), QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); + } + else { + clearSelection(); + } + } + else { + clearSelection(); + } +} + + +void PlacesView::dragMoveEvent(QDragMoveEvent* event) { + QTreeView::dragMoveEvent(event); + /* + QModelIndex index = indexAt(event->pos()); + if(event->isAccepted() && index.isValid() && index.parent() == model_->bookmarksRoot->index()) { + if(dropIndicatorPosition() != OnItem) { + event->setDropAction(Qt::LinkAction); + event->accept(); + } + } + */ +} + +void PlacesView::dropEvent(QDropEvent* event) { + QTreeView::dropEvent(event); +} + +void PlacesView::onEmptyTrash() { + Fm::FilePathList files; + files.push_back(Fm::FilePath::fromUri("trash:///")); + Fm::FileOperation::deleteFiles(std::move(files)); +} + +void PlacesView::onMoveBookmarkUp() { + PlacesModel::ItemAction* action = static_cast(sender()); + if(!action->index().isValid()) { + return; + } + PlacesModelBookmarkItem* item = static_cast(model_->itemFromIndex(action->index())); + + int row = item->row(); + if(row > 0) { + auto bookmarkItem = item->bookmark(); + Fm::Bookmarks::globalInstance()->reorder(bookmarkItem, row - 1); + } +} + +void PlacesView::onMoveBookmarkDown() { + PlacesModel::ItemAction* action = static_cast(sender()); + if(!action->index().isValid()) { + return; + } + PlacesModelBookmarkItem* item = static_cast(model_->itemFromIndex(action->index())); + + int row = item->row(); + if(row < model_->rowCount()) { + auto bookmarkItem = item->bookmark(); + Fm::Bookmarks::globalInstance()->reorder(bookmarkItem, row + 1); + } +} + +void PlacesView::onDeleteBookmark() { + PlacesModel::ItemAction* action = static_cast(sender()); + if(!action->index().isValid()) { + return; + } + PlacesModelBookmarkItem* item = static_cast(model_->itemFromIndex(action->index())); + auto bookmarkItem = item->bookmark(); + Fm::Bookmarks::globalInstance()->remove(bookmarkItem); +} + +// virtual +void PlacesView::commitData(QWidget* editor) { + QTreeView::commitData(editor); + PlacesModelBookmarkItem* item = static_cast(model_->itemFromIndex(currentIndex())); + auto bookmarkItem = item->bookmark(); + // rename bookmark + Fm::Bookmarks::globalInstance()->rename(bookmarkItem, item->text()); +} + +void PlacesView::onOpenNewTab() { + PlacesModel::ItemAction* action = static_cast(sender()); + if(!action->index().isValid()) { + return; + } + PlacesModelItem* item = static_cast(model_->itemFromIndex(action->index())); + if(item) { + Q_EMIT chdirRequested(1, item->path()); + } +} + +void PlacesView::onOpenNewWindow() { + PlacesModel::ItemAction* action = static_cast(sender()); + if(!action->index().isValid()) { + return; + } + PlacesModelItem* item = static_cast(model_->itemFromIndex(action->index())); + if(item) { + Q_EMIT chdirRequested(2, item->path()); + } +} + +void PlacesView::onRenameBookmark() { + PlacesModel::ItemAction* action = static_cast(sender()); + if(!action->index().isValid()) { + return; + } + PlacesModelBookmarkItem* item = static_cast(model_->itemFromIndex(action->index())); + setFocus(); + setCurrentIndex(item->index()); + edit(item->index()); +} + +void PlacesView::onMountVolume() { + PlacesModel::ItemAction* action = static_cast(sender()); + if(!action->index().isValid()) { + return; + } + PlacesModelVolumeItem* item = static_cast(model_->itemFromIndex(action->index())); + MountOperation* op = new MountOperation(true, this); + op->mount(item->volume()); + op->wait(); +} + +void PlacesView::onUnmountVolume() { + PlacesModel::ItemAction* action = static_cast(sender()); + if(!action->index().isValid()) { + return; + } + PlacesModelVolumeItem* item = static_cast(model_->itemFromIndex(action->index())); + MountOperation* op = new MountOperation(true, this); + op->unmount(item->volume()); + op->wait(); +} + +void PlacesView::onUnmountMount() { + PlacesModel::ItemAction* action = static_cast(sender()); + if(!action->index().isValid()) { + return; + } + PlacesModelMountItem* item = static_cast(model_->itemFromIndex(action->index())); + GMount* mount = item->mount(); + MountOperation* op = new MountOperation(true, this); + op->unmount(mount); + op->wait(); +} + +void PlacesView::onEjectVolume() { + PlacesModel::ItemAction* action = static_cast(sender()); + if(!action->index().isValid()) { + return; + } + PlacesModelVolumeItem* item = static_cast(model_->itemFromIndex(action->index())); + MountOperation* op = new MountOperation(true, this); + op->eject(item->volume()); + op->wait(); +} + +void PlacesView::contextMenuEvent(QContextMenuEvent* event) { + QModelIndex index = indexAt(event->pos()); + if(index.isValid() && index.parent().isValid()) { + if(index.column() != 0) { // the real item is at column 0 + index = index.sibling(index.row(), 0); + } + + // Do not take the ownership of the menu since + // it will be deleted with deleteLater() upon hidden. + // This is possibly related to #145 - https://github.com/lxde/pcmanfm-qt/issues/145 + QMenu* menu = new QMenu(); + QAction* action; + PlacesModelItem* item = static_cast(model_->itemFromIndex(index)); + + if(item->type() != PlacesModelItem::Mount + && (item->type() != PlacesModelItem::Volume + || static_cast(item)->isMounted())) { + action = new PlacesModel::ItemAction(item->index(), tr("Open in New Tab"), menu); + connect(action, &QAction::triggered, this, &PlacesView::onOpenNewTab); + menu->addAction(action); + action = new PlacesModel::ItemAction(item->index(), tr("Open in New Window"), menu); + connect(action, &QAction::triggered, this, &PlacesView::onOpenNewWindow); + menu->addAction(action); + } + + switch(item->type()) { + case PlacesModelItem::Places: { + auto path = item->path(); + auto path_str = path.toString(); + // FIXME: inefficient + if(path && strcmp(path_str.get(), "trash:///") == 0) { + action = new PlacesModel::ItemAction(item->index(), tr("Empty Trash"), menu); + connect(action, &QAction::triggered, this, &PlacesView::onEmptyTrash); + menu->addAction(action); + } + break; + } + case PlacesModelItem::Bookmark: { + // create context menu for bookmark item + if(item->index().row() > 0) { + action = new PlacesModel::ItemAction(item->index(), tr("Move Bookmark Up"), menu); + connect(action, &QAction::triggered, this, &PlacesView::onMoveBookmarkUp); + menu->addAction(action); + } + if(item->index().row() < model_->rowCount()) { + action = new PlacesModel::ItemAction(item->index(), tr("Move Bookmark Down"), menu); + connect(action, &QAction::triggered, this, &PlacesView::onMoveBookmarkDown); + menu->addAction(action); + } + action = new PlacesModel::ItemAction(item->index(), tr("Rename Bookmark"), menu); + connect(action, &QAction::triggered, this, &PlacesView::onRenameBookmark); + menu->addAction(action); + action = new PlacesModel::ItemAction(item->index(), tr("Remove Bookmark"), menu); + connect(action, &QAction::triggered, this, &PlacesView::onDeleteBookmark); + menu->addAction(action); + break; + } + case PlacesModelItem::Volume: { + PlacesModelVolumeItem* volumeItem = static_cast(item); + + if(volumeItem->isMounted()) { + action = new PlacesModel::ItemAction(item->index(), tr("Unmount"), menu); + connect(action, &QAction::triggered, this, &PlacesView::onUnmountVolume); + } + else { + action = new PlacesModel::ItemAction(item->index(), tr("Mount"), menu); + connect(action, &QAction::triggered, this, &PlacesView::onMountVolume); + } + menu->addAction(action); + + if(volumeItem->canEject()) { + action = new PlacesModel::ItemAction(item->index(), tr("Eject"), menu); + connect(action, &QAction::triggered, this, &PlacesView::onEjectVolume); + menu->addAction(action); + } + break; + } + case PlacesModelItem::Mount: { + action = new PlacesModel::ItemAction(item->index(), tr("Unmount"), menu); + connect(action, &QAction::triggered, this, &PlacesView::onUnmountMount); + menu->addAction(action); + break; + } + } + if(menu->actions().size()) { + menu->popup(mapToGlobal(event->pos())); + connect(menu, &QMenu::aboutToHide, menu, &QMenu::deleteLater); + } + else { + menu->deleteLater(); + } + } +} + + +} // namespace Fm diff --git a/src/placesview.h b/src/placesview.h new file mode 100644 index 0000000..ccb7fa9 --- /dev/null +++ b/src/placesview.h @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_PLACESVIEW_H +#define FM_PLACESVIEW_H + +#include "libfmqtglobals.h" +#include +#include + +#include +#include "core/filepath.h" + +namespace Fm { + +class PlacesModel; +class PlacesModelItem; + +class LIBFM_QT_API PlacesView : public QTreeView { + Q_OBJECT + +public: + explicit PlacesView(QWidget* parent = 0); + virtual ~PlacesView(); + + void setCurrentPath(Fm::FilePath path); + + const Fm::FilePath& currentPath() const { + return currentPath_; + } + + void chdir(Fm::FilePath path) { + setCurrentPath(std::move(path)); + } + +#if QT_VERSION < QT_VERSION_CHECK(5, 5, 0) + void setIconSize(const QSize& size) { + // The signal QAbstractItemView::iconSizeChanged is only available after Qt 5.5. + // To simulate the effect for older Qt versions, we override setIconSize(). + QAbstractItemView::setIconSize(size); + onIconSizeChanged(size); + } +#endif + +Q_SIGNALS: + void chdirRequested(int type, const Fm::FilePath& path); + +protected Q_SLOTS: + void onClicked(const QModelIndex& index); + void onPressed(const QModelIndex& index); + void onIconSizeChanged(const QSize& size); + // void onMountOperationFinished(GError* error); + + void onOpenNewTab(); + void onOpenNewWindow(); + + void onEmptyTrash(); + + void onMountVolume(); + void onUnmountVolume(); + void onEjectVolume(); + void onUnmountMount(); + + void onMoveBookmarkUp(); + void onMoveBookmarkDown(); + void onDeleteBookmark(); + void onRenameBookmark(); + +protected: + void drawBranches(QPainter* /*painter*/, const QRect& /*rect*/, const QModelIndex& /*index*/) const { + // override this method to inhibit drawing of the branch grid lines by Qt. + } + + virtual void dragMoveEvent(QDragMoveEvent* event); + virtual void dropEvent(QDropEvent* event); + virtual void contextMenuEvent(QContextMenuEvent* event); + + virtual void commitData(QWidget* editor); + +private: + void onEjectButtonClicked(PlacesModelItem* item); + void activateRow(int type, const QModelIndex& index); + +private: + std::shared_ptr model_; + Fm::FilePath currentPath_; +}; + +} + +#endif // FM_PLACESVIEW_H diff --git a/src/proxyfoldermodel.cpp b/src/proxyfoldermodel.cpp new file mode 100644 index 0000000..106e846 --- /dev/null +++ b/src/proxyfoldermodel.cpp @@ -0,0 +1,303 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "proxyfoldermodel.h" +#include "foldermodel.h" +#include + +namespace Fm { + +ProxyFolderModel::ProxyFolderModel(QObject* parent): + QSortFilterProxyModel(parent), + showHidden_(false), + folderFirst_(true), + showThumbnails_(false), + thumbnailSize_(0) { + + setDynamicSortFilter(true); + setSortCaseSensitivity(Qt::CaseInsensitive); + + collator_.setNumericMode(true); +} + +ProxyFolderModel::~ProxyFolderModel() { + qDebug("delete ProxyFolderModel"); + + if(showThumbnails_ && thumbnailSize_ != 0) { + FolderModel* srcModel = static_cast(sourceModel()); + // tell the source model that we don't need the thumnails anymore + if(srcModel) { + srcModel->releaseThumbnails(thumbnailSize_); + disconnect(srcModel, SIGNAL(thumbnailLoaded(QModelIndex, int))); + } + } +} + +void ProxyFolderModel::setSourceModel(QAbstractItemModel* model) { + if(model == sourceModel()) // avoid setting the same model twice + return; + if(model) { + // we only support Fm::FolderModel + Q_ASSERT(model->inherits("Fm::FolderModel")); + + if(showThumbnails_ && thumbnailSize_ != 0) { // if we're showing thumbnails + FolderModel* oldSrcModel = static_cast(sourceModel()); + FolderModel* newSrcModel = static_cast(model); + if(oldSrcModel) { // we need to release cached thumbnails for the old source model + oldSrcModel->releaseThumbnails(thumbnailSize_); + disconnect(oldSrcModel, SIGNAL(thumbnailLoaded(QModelIndex, int))); + } + if(newSrcModel) { // tell the new source model that we want thumbnails of this size + newSrcModel->cacheThumbnails(thumbnailSize_); + connect(newSrcModel, &FolderModel::thumbnailLoaded, this, &ProxyFolderModel::onThumbnailLoaded); + } + } + } + QSortFilterProxyModel::setSourceModel(model); +} + +void ProxyFolderModel::sort(int column, Qt::SortOrder order) { + int oldColumn = sortColumn(); + Qt::SortOrder oldOrder = sortOrder(); + QSortFilterProxyModel::sort(column, order); + if(column != oldColumn || order != oldOrder) { + Q_EMIT sortFilterChanged(); + } +} + +void ProxyFolderModel::setShowHidden(bool show) { + if(show != showHidden_) { + showHidden_ = show; + invalidateFilter(); + Q_EMIT sortFilterChanged(); + } +} + +// need to call invalidateFilter() manually. +void ProxyFolderModel::setFolderFirst(bool folderFirst) { + if(folderFirst != folderFirst_) { + folderFirst_ = folderFirst; + invalidate(); + Q_EMIT sortFilterChanged(); + } +} + +void ProxyFolderModel::setSortCaseSensitivity(Qt::CaseSensitivity cs) { + collator_.setCaseSensitivity(cs); + QSortFilterProxyModel::setSortCaseSensitivity(cs); + invalidate(); + Q_EMIT sortFilterChanged(); +} + +bool ProxyFolderModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { + if(!showHidden_) { + QAbstractItemModel* srcModel = sourceModel(); + QString name = srcModel->data(srcModel->index(source_row, 0, source_parent)).toString(); + if(name.startsWith(".") || name.endsWith("~")) { + return false; + } + } + // apply additional filters if there're any + Q_FOREACH(ProxyFolderModelFilter* filter, filters_) { + FolderModel* srcModel = static_cast(sourceModel()); + auto fileInfo = srcModel->fileInfoFromIndex(srcModel->index(source_row, 0, source_parent)); + if(!filter->filterAcceptsRow(this, fileInfo)) { + return false; + } + } + return true; +} + +bool ProxyFolderModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { + FolderModel* srcModel = static_cast(sourceModel()); + // left and right are indexes of source model, not the proxy model. + if(srcModel) { + auto leftInfo = srcModel->fileInfoFromIndex(left); + auto rightInfo = srcModel->fileInfoFromIndex(right); + + if(folderFirst_) { + bool leftIsFolder = leftInfo->isDir(); + bool rightIsFolder = rightInfo->isDir(); + if(leftIsFolder != rightIsFolder) { + return sortOrder() == Qt::AscendingOrder ? leftIsFolder : rightIsFolder; + } + } + + switch(sortColumn()) { + case FolderModel::ColumnFileMTime: + return leftInfo->mtime() < rightInfo->mtime(); + case FolderModel::ColumnFileSize: + return leftInfo->size() < rightInfo->size(); + default: { + QString leftText = left.data(Qt::DisplayRole).toString(); + QString rightText = right.data(Qt::DisplayRole).toString(); + return collator_.compare(leftText, rightText) < 0; + } + } + } + return QSortFilterProxyModel::lessThan(left, right); +} + +std::shared_ptr ProxyFolderModel::fileInfoFromIndex(const QModelIndex& index) const { + if(index.isValid()) { + FolderModel* srcModel = static_cast(sourceModel()); + if(srcModel) { + QModelIndex srcIndex = mapToSource(index); + return srcModel->fileInfoFromIndex(srcIndex); + } + } + return nullptr; +} + +QModelIndex ProxyFolderModel::indexFromPath(const FilePath &path) const { + QModelIndex ret; + int n_rows = rowCount(); + for(int row = 0; row < n_rows; ++row) { + auto idx = index(row, FolderModel::ColumnFileName, QModelIndex()); + auto fi = fileInfoFromIndex(idx); + if(fi && fi->path() == path) { // found the item + ret = idx; + break; + } + } + return ret; +} + +std::shared_ptr ProxyFolderModel::fileInfoFromPath(const FilePath &path) const { + return fileInfoFromIndex(indexFromPath(path)); +} + +void ProxyFolderModel::setCutFiles(const QItemSelection& selection) { + FolderModel* srcModel = static_cast(sourceModel()); + if(srcModel) { + srcModel->setCutFiles(mapSelectionToSource(selection)); + } +} + +void ProxyFolderModel::setShowThumbnails(bool show) { + if(show != showThumbnails_) { + showThumbnails_ = show; + FolderModel* srcModel = static_cast(sourceModel()); + if(srcModel && thumbnailSize_ != 0) { + if(show) { + // ask for cache of thumbnails of the new size in source model + srcModel->cacheThumbnails(thumbnailSize_); + // connect to the srcModel so we can be notified when a thumbnail is loaded. + connect(srcModel, &FolderModel::thumbnailLoaded, this, &ProxyFolderModel::onThumbnailLoaded); + } + else { // turn off thumbnails + // free cached old thumbnails in souce model + srcModel->releaseThumbnails(thumbnailSize_); + disconnect(srcModel, SIGNAL(thumbnailLoaded(QModelIndex, int))); + } + // reload all items, FIXME: can we only update items previously having thumbnails + Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0)); + } + } +} + +void ProxyFolderModel::setThumbnailSize(int size) { + if(size != thumbnailSize_) { + FolderModel* srcModel = static_cast(sourceModel()); + if(showThumbnails_ && srcModel) { + // free cached thumbnails of the old size + if(thumbnailSize_ != 0) { + srcModel->releaseThumbnails(thumbnailSize_); + } + else { + // if the old thumbnail size is 0, we did not turn on thumbnail initially + connect(srcModel, &FolderModel::thumbnailLoaded, this, &ProxyFolderModel::onThumbnailLoaded); + } + // ask for cache of thumbnails of the new size in source model + srcModel->cacheThumbnails(size); + // reload all items, FIXME: can we only update items previously having thumbnails + Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0)); + } + + thumbnailSize_ = size; + } +} + +QVariant ProxyFolderModel::data(const QModelIndex& index, int role) const { + if(index.column() == 0) { // only show the decoration role for the first column + if(role == Qt::DecorationRole && showThumbnails_ && thumbnailSize_) { + // we need to show thumbnails instead of icons + FolderModel* srcModel = static_cast(sourceModel()); + QModelIndex srcIndex = mapToSource(index); + QImage image = srcModel->thumbnailFromIndex(srcIndex, thumbnailSize_); + if(!image.isNull()) { // if we got a thumbnail of the desired size, use it + return QVariant(image); + } + } + } + // fallback to icons if thumbnails are not available + return QSortFilterProxyModel::data(index, role); +} + +void ProxyFolderModel::onThumbnailLoaded(const QModelIndex& srcIndex, int size) { + // FolderModel* srcModel = static_cast(sourceModel()); + // FolderModelItem* item = srcModel->itemFromIndex(srcIndex); + // qDebug("ProxyFolderModel::onThumbnailLoaded: %d, %s", size, item->displayName.toUtf8().data()); + + if(size == thumbnailSize_ // if a thumbnail of the size we want is loaded + && srcIndex.model() == sourceModel()) { // check if the sourse model contains the index item + QModelIndex index = mapFromSource(srcIndex); + Q_EMIT dataChanged(index, index); + } +} + +void ProxyFolderModel::addFilter(ProxyFolderModelFilter* filter) { + filters_.append(filter); + invalidateFilter(); + Q_EMIT sortFilterChanged(); +} + +void ProxyFolderModel::removeFilter(ProxyFolderModelFilter* filter) { + filters_.removeOne(filter); + invalidateFilter(); + Q_EMIT sortFilterChanged(); +} + +void ProxyFolderModel::updateFilters() { + invalidate(); + Q_EMIT sortFilterChanged(); +} + +#if 0 +void ProxyFolderModel::reloadAllThumbnails() { + // reload all thumbnails and update UI + FolderModel* srcModel = static_cast(sourceModel()); + if(srcModel) { + int rows = rowCount(); + for(int row = 0; row < rows; ++row) { + QModelIndex index = this->index(row, 0); + QModelIndex srcIndex = mapToSource(index); + QImage image = srcModel->thumbnailFromIndex(srcIndex, size); + // tell the world that the item is changed to trigger a UI update + if(!image.isNull()) { + Q_EMIT dataChanged(index, index); + } + } + } +} +#endif + + +} // namespace Fm diff --git a/src/proxyfoldermodel.h b/src/proxyfoldermodel.h new file mode 100644 index 0000000..9e29239 --- /dev/null +++ b/src/proxyfoldermodel.h @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_PROXYFOLDERMODEL_H +#define FM_PROXYFOLDERMODEL_H + +#include "libfmqtglobals.h" +#include +#include +#include +#include + +#include "core/fileinfo.h" + +namespace Fm { + +// a proxy model used to sort and filter FolderModel + +class FolderModelItem; +class ProxyFolderModel; + +class LIBFM_QT_API ProxyFolderModelFilter { +public: + virtual bool filterAcceptsRow(const ProxyFolderModel* model, const std::shared_ptr& info) const = 0; + virtual ~ProxyFolderModelFilter() {} +}; + + +class LIBFM_QT_API ProxyFolderModel : public QSortFilterProxyModel { + Q_OBJECT +public: + explicit ProxyFolderModel(QObject* parent = 0); + virtual ~ProxyFolderModel(); + + // only Fm::FolderModel is allowed for being sourceModel + virtual void setSourceModel(QAbstractItemModel* model); + + void setShowHidden(bool show); + bool showHidden() const { + return showHidden_; + } + + void setFolderFirst(bool folderFirst); + bool folderFirst() { + return folderFirst_; + } + + void setSortCaseSensitivity(Qt::CaseSensitivity cs); + + void setCutFiles(const QItemSelection& selection); + + bool showThumbnails() { + return showThumbnails_; + } + void setShowThumbnails(bool show); + + int thumbnailSize() { + return thumbnailSize_; + } + void setThumbnailSize(int size); + + std::shared_ptr fileInfoFromIndex(const QModelIndex& index) const; + + std::shared_ptr fileInfoFromPath(const FilePath& path) const; + + QModelIndex indexFromPath(const FilePath& path) const; + + virtual void sort(int column, Qt::SortOrder order = Qt::AscendingOrder); + virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + + void addFilter(ProxyFolderModelFilter* filter); + void removeFilter(ProxyFolderModelFilter* filter); + void updateFilters(); + +Q_SIGNALS: + void sortFilterChanged(); + +protected Q_SLOTS: + void onThumbnailLoaded(const QModelIndex& srcIndex, int size); + +protected: + bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const; + bool lessThan(const QModelIndex& left, const QModelIndex& right) const; + // void reloadAllThumbnails(); + +private: + QCollator collator_; + bool showHidden_; + bool folderFirst_; + bool showThumbnails_; + int thumbnailSize_; + QList filters_; +}; + +} + +#endif // FM_PROXYFOLDERMODEL_H diff --git a/src/rename-dialog.ui b/src/rename-dialog.ui new file mode 100644 index 0000000..2b0c123 --- /dev/null +++ b/src/rename-dialog.ui @@ -0,0 +1,204 @@ + + + RenameDialog + + + + 0 + 0 + 398 + 220 + + + + Confirm to replace files + + + false + + + + 6 + + + 10 + + + + + + 0 + 0 + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + + + + + + + 12 + + + 6 + + + + + + 0 + 0 + + + + dest + + + + + + + with the following file? + + + + + + + + 0 + 0 + + + + src file info + + + + + + + + 0 + 0 + + + + dest file info + + + + + + + + 0 + 0 + + + + src + + + + + + + + + 12 + + + + + + 0 + 0 + + + + &File name: + + + fileName + + + + + + + + + + + + Apply this option to all existing files + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 0 + 0 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ignore|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + RenameDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + RenameDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/renamedialog.cpp b/src/renamedialog.cpp new file mode 100644 index 0000000..4dc17b8 --- /dev/null +++ b/src/renamedialog.cpp @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "renamedialog.h" +#include "ui_rename-dialog.h" +#include +#include +#include "core/iconinfo.h" + +namespace Fm { + +RenameDialog::RenameDialog(FmFileInfo* src, FmFileInfo* dest, QWidget* parent, Qt::WindowFlags f): + QDialog(parent, f), + action_(ActionIgnore), + applyToAll_(false) { + + ui = new Ui::RenameDialog(); + ui->setupUi(this); + + FmPath* path = fm_file_info_get_path(dest); + FmIcon* srcIcon = fm_file_info_get_icon(src); + FmIcon* destIcon = fm_file_info_get_icon(dest); + + // show info for the source file + QIcon icon = Fm::IconInfo::fromGIcon(G_ICON(srcIcon))->qicon(); + QSize iconSize(fm_config->big_icon_size, fm_config->big_icon_size); + QPixmap pixmap = icon.pixmap(iconSize); + ui->srcIcon->setPixmap(pixmap); + + QString infoStr; + const char* disp_size = fm_file_info_get_disp_size(src); + if(disp_size) { + infoStr = QString(tr("Type: %1\nSize: %2\nModified: %3")) + .arg(QString::fromUtf8(fm_file_info_get_desc(src))) + .arg(QString::fromUtf8(disp_size)) + .arg(QString::fromUtf8(fm_file_info_get_disp_mtime(src))); + } + else { + infoStr = QString(tr("Type: %1\nModified: %2")) + .arg(QString::fromUtf8(fm_file_info_get_desc(src))) + .arg(QString::fromUtf8(fm_file_info_get_disp_mtime(src))); + } + ui->srcInfo->setText(infoStr); + + // show info for the dest file + icon = Fm::IconInfo::fromGIcon(G_ICON(destIcon))->qicon(); + pixmap = icon.pixmap(iconSize); + ui->destIcon->setPixmap(pixmap); + + disp_size = fm_file_info_get_disp_size(dest); + if(disp_size) { + infoStr = QString(tr("Type: %1\nSize: %2\nModified: %3")) + .arg(QString::fromUtf8(fm_file_info_get_desc(dest))) + .arg(QString::fromUtf8(disp_size)) + .arg(QString::fromUtf8(fm_file_info_get_disp_mtime(dest))); + } + else { + infoStr = QString(tr("Type: %1\nModified: %2")) + .arg(QString::fromUtf8(fm_file_info_get_desc(dest))) + .arg(QString::fromUtf8(fm_file_info_get_disp_mtime(dest))); + } + ui->destInfo->setText(infoStr); + + char* basename = fm_path_display_basename(path); + ui->fileName->setText(QString::fromUtf8(basename)); + oldName_ = basename; + g_free(basename); + connect(ui->fileName, &QLineEdit::textChanged, this, &RenameDialog::onFileNameChanged); + + // add "Rename" button + QAbstractButton* button = ui->buttonBox->button(QDialogButtonBox::Ok); + button->setText(tr("&Overwrite")); + // FIXME: there seems to be no way to place the Rename button next to the overwrite one. + renameButton_ = ui->buttonBox->addButton(tr("&Rename"), QDialogButtonBox::ActionRole); + connect(renameButton_, &QPushButton::clicked, this, &RenameDialog::onRenameClicked); + renameButton_->setEnabled(false); // disabled by default + + button = ui->buttonBox->button(QDialogButtonBox::Ignore); + connect(button, &QPushButton::clicked, this, &RenameDialog::onIgnoreClicked); +} + +RenameDialog::~RenameDialog() { + delete ui; +} + +void RenameDialog::onRenameClicked() { + action_ = ActionRename; + QDialog::done(QDialog::Accepted); +} + +void RenameDialog::onIgnoreClicked() { + action_ = ActionIgnore; +} + +// the overwrite button +void RenameDialog::accept() { + action_ = ActionOverwrite; + applyToAll_ = ui->applyToAll->isChecked(); + QDialog::accept(); +} + +// cancel, or close the dialog +void RenameDialog::reject() { + action_ = ActionCancel; + QDialog::reject(); +} + +void RenameDialog::onFileNameChanged(QString newName) { + newName_ = newName; + // FIXME: check if the name already exists in the current dir + bool hasNewName = (newName_ != oldName_); + renameButton_->setEnabled(hasNewName); + renameButton_->setDefault(hasNewName); + + // change default button to rename rather than overwrire + // if the user typed a new filename + QPushButton* overwriteButton = static_cast(ui->buttonBox->button(QDialogButtonBox::Ok)); + overwriteButton->setEnabled(!hasNewName); + overwriteButton->setDefault(!hasNewName); +} + + +} // namespace Fm diff --git a/src/renamedialog.h b/src/renamedialog.h new file mode 100644 index 0000000..994c21a --- /dev/null +++ b/src/renamedialog.h @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_RENAMEDIALOG_H +#define FM_RENAMEDIALOG_H + +#include "libfmqtglobals.h" +#include +#include + +namespace Ui { +class RenameDialog; +} + +class QPushButton; + +namespace Fm { + +class LIBFM_QT_API RenameDialog : public QDialog { + Q_OBJECT + +public: + enum Action { + ActionCancel, + ActionRename, + ActionOverwrite, + ActionIgnore + }; + +public: + explicit RenameDialog(FmFileInfo* src, FmFileInfo* dest, QWidget* parent = 0, Qt::WindowFlags f = 0); + virtual ~RenameDialog(); + + Action action() { + return action_; + } + + bool applyToAll() { + return applyToAll_; + } + + QString newName() { + return newName_; + } + +protected Q_SLOTS: + void onRenameClicked(); + void onIgnoreClicked(); + void onFileNameChanged(QString newName); + +protected: + void accept(); + void reject(); + +private: + Ui::RenameDialog* ui; + QPushButton* renameButton_; + Action action_; + bool applyToAll_; + QString oldName_; + QString newName_; +}; + +} + +#endif // FM_RENAMEDIALOG_H diff --git a/src/sidepane.cpp b/src/sidepane.cpp new file mode 100644 index 0000000..717594f --- /dev/null +++ b/src/sidepane.cpp @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "sidepane.h" +#include +#include +#include +#include "placesview.h" +#include "dirtreeview.h" +#include "dirtreemodel.h" +#include "path.h" +#include "filemenu.h" + +namespace Fm { + +SidePane::SidePane(QWidget* parent): + QWidget(parent), + view_(nullptr), + combo_(nullptr), + iconSize_(24, 24), + mode_(ModeNone), + showHidden_(false) { + + verticalLayout = new QVBoxLayout(this); + verticalLayout->setContentsMargins(0, 0, 0, 0); + + combo_ = new QComboBox(this); + combo_->setFrame(false); + combo_->addItem(tr("Places")); + combo_->addItem(tr("Directory Tree")); + connect(combo_, static_cast(&QComboBox::currentIndexChanged), this, &SidePane::onComboCurrentIndexChanged); + verticalLayout->addWidget(combo_); +} + +SidePane::~SidePane() { + // qDebug("delete SidePane"); +} + +void SidePane::onComboCurrentIndexChanged(int current) { + if(current != mode_) { + setMode(Mode(current)); + } +} + +void SidePane::setIconSize(QSize size) { + iconSize_ = size; + switch(mode_) { + case ModePlaces: + static_cast(view_)->setIconSize(size); + case ModeDirTree: + static_cast(view_)->setIconSize(size); + break; + default: + ; + } +} + +void SidePane::setCurrentPath(Fm::FilePath path) { + Q_ASSERT(path); + currentPath_ = std::move(path); + switch(mode_) { + case ModePlaces: + static_cast(view_)->setCurrentPath(currentPath_); + break; + case ModeDirTree: + static_cast(view_)->setCurrentPath(currentPath_); + break; + default: + ; + } +} + +SidePane::Mode SidePane::modeByName(const char* str) { + if(str == nullptr) { + return ModeNone; + } + if(strcmp(str, "places") == 0) { + return ModePlaces; + } + if(strcmp(str, "dirtree") == 0) { + return ModeDirTree; + } + return ModeNone; +} + +const char* SidePane::modeName(SidePane::Mode mode) { + switch(mode) { + case ModePlaces: + return "places"; + case ModeDirTree: + return "dirtree"; + default: + return nullptr; + } +} + +bool SidePane::setHomeDir(const char* /*home_dir*/) { + if(view_ == nullptr) { + return false; + } + // TODO: SidePane::setHomeDir + + switch(mode_) { + case ModePlaces: + // static_cast(view_); + return true; + case ModeDirTree: + // static_cast(view_); + return true; + default: + ; + } + return true; +} + +void SidePane::initDirTree() { + DirTreeModel* model = new DirTreeModel(view_); + model->setShowHidden(showHidden_); + + Fm::FilePathList rootPaths; + rootPaths.emplace_back(Fm::FilePath::homeDir()); + rootPaths.emplace_back(Fm::FilePath::fromLocalPath("/")); + model->addRoots(std::move(rootPaths)); + static_cast(view_)->setModel(model); +} + +void SidePane::setMode(Mode mode) { + if(mode == mode_) { + return; + } + + if(view_) { + delete view_; + view_ = nullptr; + //if(sp->update_popup) + // g_signal_handlers_disconnect_by_func(sp->view, on_item_popup, sp); + } + mode_ = mode; + + combo_->setCurrentIndex(mode); + switch(mode) { + case ModePlaces: { + PlacesView* placesView = new Fm::PlacesView(this); + view_ = placesView; + placesView->setIconSize(iconSize_); + placesView->setCurrentPath(currentPath_); + connect(placesView, &PlacesView::chdirRequested, this, &SidePane::chdirRequested); + break; + } + case ModeDirTree: { + DirTreeView* dirTreeView = new Fm::DirTreeView(this); + view_ = dirTreeView; + initDirTree(); + dirTreeView->setIconSize(iconSize_); + dirTreeView->setCurrentPath(currentPath_); + connect(dirTreeView, &DirTreeView::chdirRequested, this, &SidePane::chdirRequested); + connect(dirTreeView, &DirTreeView::openFolderInNewWindowRequested, + this, &SidePane::openFolderInNewWindowRequested); + connect(dirTreeView, &DirTreeView::openFolderInNewTabRequested, + this, &SidePane::openFolderInNewTabRequested); + connect(dirTreeView, &DirTreeView::openFolderInTerminalRequested, + this, &SidePane::openFolderInTerminalRequested); + connect(dirTreeView, &DirTreeView::createNewFolderRequested, + this, &SidePane::createNewFolderRequested); + connect(dirTreeView, &DirTreeView::prepareFileMenu, + this, &SidePane::prepareFileMenu); + break; + } + default: + ; + } + if(view_) { + // if(sp->update_popup) + // g_signal_connect(sp->view, "item-popup", G_CALLBACK(on_item_popup), sp); + verticalLayout->addWidget(view_); + } + Q_EMIT modeChanged(mode); +} + +void SidePane::setShowHidden(bool show_hidden) { + if(view_ == nullptr || show_hidden == showHidden_) { + return; + } + showHidden_ = show_hidden; + if(mode_ == ModeDirTree) { + DirTreeView* dirTreeView = static_cast(view_); + DirTreeModel* model = static_cast(dirTreeView->model()); + if(model) { + model->setShowHidden(showHidden_); + } + } +} + +} // namespace Fm diff --git a/src/sidepane.h b/src/sidepane.h new file mode 100644 index 0000000..04215ca --- /dev/null +++ b/src/sidepane.h @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_SIDEPANE_H +#define FM_SIDEPANE_H + +#include "libfmqtglobals.h" +#include +#include + +#include "core/filepath.h" + +class QComboBox; +class QVBoxLayout; +class QWidget; + +namespace Fm { + +class FileMenu; + +class LIBFM_QT_API SidePane : public QWidget { + Q_OBJECT + +public: + enum Mode { + ModeNone = -1, + ModePlaces = 0, + ModeDirTree, + NumModes + }; + +public: + explicit SidePane(QWidget* parent = 0); + virtual ~SidePane(); + + QSize iconSize() const { + return iconSize_; + } + + void setIconSize(QSize size); + + const Fm::FilePath& currentPath() const { + return currentPath_; + } + + void setCurrentPath(Fm::FilePath path); + + void setMode(Mode mode); + + Mode mode() const { + return mode_; + } + + QWidget* view() const { + return view_; + } + + static const char* modeName(Mode mode); + + static Mode modeByName(const char* str); + +#if 0 // FIXME: are these APIs from libfm-qt needed? + int modeCount(void) { + return NumModes; + } + + QString modeLabel(Mode mode); + + QString modeTooltip(Mode mode); +#endif + + void setShowHidden(bool show_hidden); + + bool showHidden() const { + return showHidden_; + } + + bool setHomeDir(const char* home_dir); + + void chdir(Fm::FilePath path) { + setCurrentPath(std::move(path)); + } + +Q_SIGNALS: + void chdirRequested(int type, const Fm::FilePath& path); + void openFolderInNewWindowRequested(const Fm::FilePath& path); + void openFolderInNewTabRequested(const Fm::FilePath& path); + void openFolderInTerminalRequested(const Fm::FilePath& path); + void createNewFolderRequested(const Fm::FilePath& path); + void modeChanged(Fm::SidePane::Mode mode); + + void prepareFileMenu(Fm::FileMenu* menu); // emit before showing a Fm::FileMenu + +protected Q_SLOTS: + void onComboCurrentIndexChanged(int current); + +private: + void initDirTree(); + +private: + Fm::FilePath currentPath_; + QWidget* view_; + QComboBox* combo_; + QVBoxLayout* verticalLayout; + QSize iconSize_; + Mode mode_; + bool showHidden_; +}; + +} + +#endif // FM_SIDEPANE_H diff --git a/src/templates.h b/src/templates.h new file mode 100644 index 0000000..bb7fbed --- /dev/null +++ b/src/templates.h @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2016 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef __LIBFM_QT_FM_TEMPLATES_H__ +#define __LIBFM_QT_FM_TEMPLATES_H__ + +#include +#include +#include +#include "libfmqtglobals.h" + + +namespace Fm { + + +class LIBFM_QT_API Template { +public: + + + // default constructor + Template() { + dataPtr_ = nullptr; + } + + + Template(FmTemplate* dataPtr){ + dataPtr_ = dataPtr != nullptr ? reinterpret_cast(g_object_ref(dataPtr)) : nullptr; + } + + + // copy constructor + Template(const Template& other) { + dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast(g_object_ref(other.dataPtr_)) : nullptr; + } + + + // move constructor + Template(Template&& other) noexcept { + dataPtr_ = reinterpret_cast(other.takeDataPtr()); + } + + + // destructor + virtual ~Template() { + if(dataPtr_ != nullptr) { + g_object_unref(dataPtr_); + } + } + + + // create a wrapper for the data pointer without increasing the reference count + static Template wrapPtr(FmTemplate* dataPtr) { + Template obj; + obj.dataPtr_ = reinterpret_cast(dataPtr); + return obj; + } + + // disown the managed data pointer + FmTemplate* takeDataPtr() { + FmTemplate* data = reinterpret_cast(dataPtr_); + dataPtr_ = nullptr; + return data; + } + + // get the raw pointer wrapped + FmTemplate* dataPtr() { + return reinterpret_cast(dataPtr_); + } + + // automatic type casting + operator FmTemplate*() { + return dataPtr(); + } + + // automatic type casting + operator void*() { + return dataPtr(); + } + + + // copy assignment + Template& operator=(const Template& other) { + if(dataPtr_ != nullptr) { + g_object_unref(dataPtr_); + } + dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast(g_object_ref(other.dataPtr_)) : nullptr; + return *this; + } + + + // move assignment + Template& operator=(Template&& other) noexcept { + dataPtr_ = reinterpret_cast(other.takeDataPtr()); + return *this; + } + + bool isNull() { + return (dataPtr_ == nullptr); + } + + // methods + + bool createFile(GFile* path, GError** error, gboolean run_default) { + return fm_template_create_file(dataPtr(), path, error, run_default); + } + + + bool isDirectory(void) { + return fm_template_is_directory(dataPtr()); + } + + + FmIcon* getIcon(void) { + return fm_template_get_icon(dataPtr()); + } + + + FmMimeType* getMimeType(void) { + return fm_template_get_mime_type(dataPtr()); + } + + + static GList* listAll(gboolean user_only) { + return fm_template_list_all(user_only); + } + + + // automatic type casting for GObject + operator GObject*() { + return reinterpret_cast(dataPtr_); + } + + +protected: + GObject* dataPtr_; // data pointer for the underlying C struct + +}; + + +} + +#endif // __LIBFM_QT_FM_TEMPLATES_H__ diff --git a/src/tests/test-filedialog.cpp b/src/tests/test-filedialog.cpp new file mode 100644 index 0000000..ccd1809 --- /dev/null +++ b/src/tests/test-filedialog.cpp @@ -0,0 +1,48 @@ +#include +#include +#include +#include +#include "../core/folder.h" +#include "../foldermodel.h" +#include "../folderview.h" +#include "../cachedfoldermodel.h" +#include "../proxyfoldermodel.h" +#include "../pathedit.h" +#include "../filedialog.h" +#include "libfmqt.h" + + +int main(int argc, char** argv) { + QApplication app(argc, argv); + + Fm::LibFmQt contex; + + /* + QFileDialog dlg0; + dlg0.setFileMode(QFileDialog::ExistingFiles); + + dlg0.setNameFilters(QStringList() << "Txt (*.txt)"); + QObject::connect(&dlg0, &QFileDialog::currentChanged, [](const QString& path) { + qDebug() << "currentChanged:" << path; + }); + QObject::connect(&dlg0, &QFileDialog::fileSelected, [](const QString& path) { + qDebug() << "fileSelected:" << path; + }); + QObject::connect(&dlg0, &QFileDialog::filesSelected, [](const QStringList& paths) { + qDebug() << "filesSelected:" << paths; + }); + + dlg0.exec(); + */ + + Fm::FileDialog dlg; + // dlg.setFileMode(QFileDialog::ExistingFile); + dlg.setFileMode(QFileDialog::ExistingFiles); + // dlg.setFileMode(QFileDialog::Directory); + dlg.setNameFilters(QStringList() << "All (*)" << "Text (*.txt)" << "Images (*.gif *.jpeg *.jpg)"); + + dlg.exec(); + qDebug() << "selected files:" << dlg.selectedFiles(); + + return 0; +} diff --git a/src/tests/test-folder.cpp b/src/tests/test-folder.cpp new file mode 100644 index 0000000..45af775 --- /dev/null +++ b/src/tests/test-folder.cpp @@ -0,0 +1,44 @@ +#include +#include +#include "../core/folder.h" + +int main(int argc, char** argv) { + QApplication app(argc, argv); + + auto home = Fm::FilePath::homeDir(); + auto folder = Fm::Folder::fromPath(home); + + QObject::connect(folder.get(), &Fm::Folder::startLoading, [=]() { + qDebug("start loading"); + }); + QObject::connect(folder.get(), &Fm::Folder::finishLoading, [=]() { + qDebug("finish loading"); + }); + + QObject::connect(folder.get(), &Fm::Folder::filesAdded, [=](Fm::FileInfoList& files) { + qDebug("files added"); + for(auto& item: files) { + qDebug() << item->displayName(); + } + }); + QObject::connect(folder.get(), &Fm::Folder::filesRemoved, [=](Fm::FileInfoList& files) { + qDebug("files removed"); + for(auto& item: files) { + qDebug() << item->displayName(); + } + }); + QObject::connect(folder.get(), &Fm::Folder::filesChanged, [=](std::vector& file_pairs) { + qDebug("files changed"); + for(auto& pair: file_pairs) { + auto& item = pair.second; + qDebug() << item->displayName(); + } + }); + + for(auto& item: folder->files()) { + qDebug() << item->displayName(); + } + qDebug() << "here"; + + return app.exec(); +} diff --git a/src/tests/test-folderview.cpp b/src/tests/test-folderview.cpp new file mode 100644 index 0000000..427f1e5 --- /dev/null +++ b/src/tests/test-folderview.cpp @@ -0,0 +1,48 @@ +#include +#include +#include +#include +#include "../core/folder.h" +#include "../foldermodel.h" +#include "../folderview.h" +#include "../cachedfoldermodel.h" +#include "../proxyfoldermodel.h" +#include "../pathedit.h" +#include "libfmqt.h" + +int main(int argc, char** argv) { + QApplication app(argc, argv); + + Fm::LibFmQt contex; + QMainWindow win; + + Fm::FolderView folder_view; + win.setCentralWidget(&folder_view); + + auto home = Fm::FilePath::homeDir(); + Fm::CachedFolderModel* model = Fm::CachedFolderModel::modelFromPath(home); + auto proxy_model = new Fm::ProxyFolderModel(); + proxy_model->sort(Fm::FolderModel::ColumnFileName, Qt::AscendingOrder); + proxy_model->setSourceModel(model); + + proxy_model->setThumbnailSize(64); + proxy_model->setShowThumbnails(true); + + folder_view.setModel(proxy_model); + + QToolBar toolbar; + win.addToolBar(Qt::TopToolBarArea, &toolbar); + Fm::PathEdit edit; + edit.setText(home.toString().get()); + toolbar.addWidget(&edit); + auto action = new QAction("Go", nullptr); + toolbar.addAction(action); + QObject::connect(action, &QAction::triggered, [&]() { + auto path = Fm::FilePath::fromPathStr(edit.text().toLocal8Bit().constData()); + auto new_model = Fm::CachedFolderModel::modelFromPath(path); + proxy_model->setSourceModel(new_model); + }); + + win.show(); + return app.exec(); +} diff --git a/src/tests/test-placesview.cpp b/src/tests/test-placesview.cpp new file mode 100644 index 0000000..ddfe213 --- /dev/null +++ b/src/tests/test-placesview.cpp @@ -0,0 +1,20 @@ +#include +#include +#include +#include +#include +#include "../placesview.h" +#include "libfmqt.h" + +int main(int argc, char** argv) { + QApplication app(argc, argv); + + Fm::LibFmQt contex; + QMainWindow win; + + Fm::PlacesView view; + win.setCentralWidget(&view); + + win.show(); + return app.exec(); +} diff --git a/src/tests/test-volumemanager.cpp b/src/tests/test-volumemanager.cpp new file mode 100644 index 0000000..7b6438a --- /dev/null +++ b/src/tests/test-volumemanager.cpp @@ -0,0 +1,24 @@ +#include +#include +#include +#include "../core/volumemanager.h" + +int main(int argc, char** argv) { + QApplication app(argc, argv); + + auto vm = Fm::VolumeManager::globalInstance(); + + QObject::connect(vm.get(), &Fm::VolumeManager::volumeAdded, [=](const Fm::Volume& vol) { + qDebug() << "volume added:" << vol.name().get(); + }); + QObject::connect(vm.get(), &Fm::VolumeManager::volumeRemoved, [=](const Fm::Volume& vol) { + qDebug() << "volume removed:" << vol.name().get(); + }); + + for(auto& item: vm->volumes()) { + auto name = item.name(); + qDebug() << "list volume:" << name.get(); + } + + return app.exec(); +} diff --git a/src/utilities.cpp b/src/utilities.cpp new file mode 100644 index 0000000..c4351b2 --- /dev/null +++ b/src/utilities.cpp @@ -0,0 +1,367 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "utilities.h" +#include "utilities_p.h" +#include +#include +#include +#include +#include +#include +#include +#include "fileoperation.h" +#include + +#include +#include +#include +#include + +namespace Fm { + +Fm::FilePathList pathListFromUriList(const char* uriList) { + Fm::FilePathList pathList; + char** uris = g_strsplit_set(uriList, "\r\n", -1); + for(char** uri = uris; *uri; ++uri) { + if(**uri != '\0') { + pathList.push_back(Fm::FilePath::fromUri(*uri)); + } + } + g_strfreev(uris); + return pathList; +} + +QByteArray pathListToUriList(const Fm::FilePathList& paths) { + QByteArray uriList; + for(auto& path: paths) { + uriList += path.uri().get(); + uriList += "\r\n"; + } + return uriList; +} + +Fm::FilePathList pathListFromQUrls(QList urls) { + Fm::FilePathList pathList; + for(auto it = urls.cbegin(); it != urls.cend(); ++it) { + auto path = Fm::FilePath::fromUri(it->toString().toUtf8().constData()); + pathList.push_back(std::move(path)); + } + return pathList; +} + +std::pair parseClipboardData(const QMimeData& data) { + bool isCut = false; + Fm::FilePathList paths; + + if(data.hasFormat("x-special/gnome-copied-files")) { + // Gnome, LXDE, and XFCE + QByteArray gnomeData = data.data("x-special/gnome-copied-files"); + char* pdata = gnomeData.data(); + char* eol = strchr(pdata, '\n'); + + if(eol) { + *eol = '\0'; + isCut = (strcmp(pdata, "cut") == 0 ? true : false); + paths = pathListFromUriList(eol + 1); + } + } + + if(paths.empty() && data.hasUrls()) { + // The KDE way + paths = Fm::pathListFromQUrls(data.urls()); + QByteArray cut = data.data(QStringLiteral("application/x-kde-cutselection")); + if(!cut.isEmpty() && QChar::fromLatin1(cut.at(0)) == QLatin1Char('1')) { + isCut = true; + } + } + + return std::make_pair(paths, isCut); +} + +void pasteFilesFromClipboard(const Fm::FilePath& destPath, QWidget* parent) { + QClipboard* clipboard = QApplication::clipboard(); + const QMimeData* data = clipboard->mimeData(); + Fm::FilePathList paths; + bool isCut = false; + + std::tie(paths, isCut) = parseClipboardData(*data); + + if(!paths.empty()) { + if(isCut) { + FileOperation::moveFiles(paths, destPath, parent); + clipboard->clear(QClipboard::Clipboard); + } + else { + FileOperation::copyFiles(paths, destPath, parent); + } + } +} + +void copyFilesToClipboard(const Fm::FilePathList& files) { + QClipboard* clipboard = QApplication::clipboard(); + QMimeData* data = new QMimeData(); + QByteArray ba; + auto urilist = pathListToUriList(files); + + // Add current pid to trace cut/copy operations to current app + data->setData(QStringLiteral("text/x-libfmqt-pid"), ba.setNum(QCoreApplication::applicationPid())); + // Gnome, LXDE, and XFCE + // Note: the standard text/urilist format uses CRLF for line breaks, but gnome format uses LF only + data->setData("x-special/gnome-copied-files", QByteArray("copy\n") + urilist.replace("\r\n", "\n")); + // The KDE way + data->setData("text/uri-list", urilist); + // data->setData(QStringLiteral("application/x-kde-cutselection"), QByteArrayLiteral("0")); + clipboard->setMimeData(data); +} + +void cutFilesToClipboard(const Fm::FilePathList& files) { + QClipboard* clipboard = QApplication::clipboard(); + QMimeData* data = new QMimeData(); + QByteArray ba; + auto urilist = pathListToUriList(files); + + // Add current pid to trace cut/copy operations to current app + data->setData(QStringLiteral("text/x-libfmqt-pid"), ba.setNum(QCoreApplication::applicationPid())); + // Gnome, LXDE, and XFCE + // Note: the standard text/urilist format uses CRLF for line breaks, but gnome format uses LF only + data->setData("x-special/gnome-copied-files", QByteArray("cut\n") + urilist.replace("\r\n", "\n")); + // The KDE way + data->setData("text/uri-list", urilist); + data->setData(QStringLiteral("application/x-kde-cutselection"), QByteArrayLiteral("1")); + clipboard->setMimeData(data); +} + +bool isCurrentPidClipboardData(const QMimeData& data) { + QByteArray clip_pid = data.data(QStringLiteral("text/x-libfmqt-pid")); + QByteArray curr_pid; + curr_pid.setNum(QCoreApplication::applicationPid()); + + return !clip_pid.isEmpty() && clip_pid == curr_pid; +} + +void changeFileName(const Fm::FilePath& filePath, const QString& newName, QWidget* parent) { + auto dest = filePath.parent().child(newName.toLocal8Bit().constData()); + Fm::GErrorPtr err; + if(!g_file_move(filePath.gfile().get(), dest.gfile().get(), + GFileCopyFlags(G_FILE_COPY_ALL_METADATA | + G_FILE_COPY_NO_FALLBACK_FOR_MOVE | + G_FILE_COPY_NOFOLLOW_SYMLINKS), + nullptr, /* make this cancellable later. */ + nullptr, nullptr, &err)) { + QMessageBox::critical(parent, QObject::tr("Error"), err.message()); + } +} + +void renameFile(std::shared_ptr file, QWidget* parent) { + FilenameDialog dlg(parent); + dlg.setWindowTitle(QObject::tr("Rename File")); + dlg.setLabelText(QObject::tr("Please enter a new name:")); + // FIXME: what's the best way to handle non-UTF8 filename encoding here? + auto old_name = QString::fromStdString(file->name()); + dlg.setTextValue(old_name); + + if(file->isDir()) { // select filename extension for directories + dlg.setSelectExtension(true); + } + + if(dlg.exec() != QDialog::Accepted) { + return; + } + + QString new_name = dlg.textValue(); + if(new_name == old_name) { + return; + } + changeFileName(file->path(), new_name, parent); +} + +// templateFile is a file path used as a template of the new file. +void createFileOrFolder(CreateFileType type, Fm::FilePath parentDir, FmTemplate* templ, QWidget* parent) { + QString defaultNewName; + QString prompt; + QString dialogTitle = type == CreateNewFolder ? QObject::tr("Create Folder") + : QObject::tr("Create File"); + + switch(type) { + case CreateNewTextFile: + prompt = QObject::tr("Please enter a new file name:"); + defaultNewName = QObject::tr("New text file"); + break; + + case CreateNewFolder: + prompt = QObject::tr("Please enter a new folder name:"); + defaultNewName = QObject::tr("New folder"); + break; + + case CreateWithTemplate: { + FmMimeType* mime = fm_template_get_mime_type(templ); + prompt = QObject::tr("Enter a name for the new %1:").arg(QString::fromUtf8(fm_mime_type_get_desc(mime))); + defaultNewName = QString::fromUtf8(fm_template_get_name(templ, nullptr)); + } + break; + } + +_retry: + // ask the user to input a file name + bool ok; + QString new_name = QInputDialog::getText(parent, dialogTitle, + prompt, + QLineEdit::Normal, + defaultNewName, + &ok); + + if(!ok) { + return; + } + + auto dest = parentDir.child(new_name.toLocal8Bit().data()); + Fm::GErrorPtr err; + switch(type) { + case CreateNewTextFile: { + Fm::GFileOutputStreamPtr f{g_file_create(dest.gfile().get(), G_FILE_CREATE_NONE, nullptr, &err), false}; + if(f) { + g_output_stream_close(G_OUTPUT_STREAM(f.get()), nullptr, nullptr); + } + break; + } + case CreateNewFolder: + g_file_make_directory(dest.gfile().get(), nullptr, &err); + break; + case CreateWithTemplate: + fm_template_create_file(templ, dest.gfile().get(), &err, false); + break; + } + if(err) { + if(err.domain() == G_IO_ERROR && err.code() == G_IO_ERROR_EXISTS) { + err.reset(); + goto _retry; + } + + QMessageBox::critical(parent, QObject::tr("Error"), err.message()); + } +} + +uid_t uidFromName(QString name) { + uid_t ret; + if(name.isEmpty()) { + return -1; + } + if(name.at(0).digitValue() != -1) { + ret = uid_t(name.toUInt()); + } + else { + struct passwd* pw = getpwnam(name.toLatin1()); + // FIXME: use getpwnam_r instead later to make it reentrant + ret = pw ? pw->pw_uid : -1; + } + + return ret; +} + +QString uidToName(uid_t uid) { + QString ret; + struct passwd* pw = getpwuid(uid); + + if(pw) { + ret = pw->pw_name; + } + else { + ret = QString::number(uid); + } + + return ret; +} + +gid_t gidFromName(QString name) { + gid_t ret; + if(name.isEmpty()) { + return -1; + } + if(name.at(0).digitValue() != -1) { + ret = gid_t(name.toUInt()); + } + else { + // FIXME: use getgrnam_r instead later to make it reentrant + struct group* grp = getgrnam(name.toLatin1()); + ret = grp ? grp->gr_gid : -1; + } + + return ret; +} + +QString gidToName(gid_t gid) { + QString ret; + struct group* grp = getgrgid(gid); + + if(grp) { + ret = grp->gr_name; + } + else { + ret = QString::number(gid); + } + + return ret; +} + +int execModelessDialog(QDialog* dlg) { + // FIXME: this does much less than QDialog::exec(). Will this work flawlessly? + QEventLoop loop; + QObject::connect(dlg, &QDialog::finished, &loop, &QEventLoop::quit); + // DialogExec does not seem to be documented in the Qt API doc? + // However, in the source code of QDialog::exec(), it's used so let's use it too. + dlg->show(); + (void)loop.exec(QEventLoop::DialogExec); + return dlg->result(); +} + +// check if GVFS can support this uri scheme (lower case) +// NOTE: this does not work reliably due to some problems in gio/gvfs and causes bug lxde/lxqt#512 +// https://github.com/lxde/lxqt/issues/512 +// Use uriExists() whenever possible. +bool isUriSchemeSupported(const char* uriScheme) { + const gchar* const* schemes = g_vfs_get_supported_uri_schemes(g_vfs_get_default()); + if(Q_UNLIKELY(schemes == nullptr)) { + return false; + } + for(const gchar * const* scheme = schemes; *scheme; ++scheme) + if(strcmp(uriScheme, *scheme) == 0) { + return true; + } + return false; +} + +// check if the URI exists. +// NOTE: this is a blocking call possibly involving I/O. +// So it's better to use it in limited cases, like checking trash:// or computer://. +// Avoid calling this on a slow filesystem. +// Checking "network:///" is very slow, for example. +bool uriExists(const char* uri) { + GFile* gf = g_file_new_for_uri(uri); + bool ret = (bool)g_file_query_exists(gf, nullptr); + g_object_unref(gf); + return ret; +} + +QString formatFileSize(uint64_t size, bool useSI) { + Fm::CStrPtr str{g_format_size_full(size, useSI ? G_FORMAT_SIZE_DEFAULT : G_FORMAT_SIZE_IEC_UNITS)}; + return QString(str.get()); +} + +} // namespace Fm diff --git a/src/utilities.h b/src/utilities.h new file mode 100644 index 0000000..e6bf715 --- /dev/null +++ b/src/utilities.h @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_UTILITIES_H +#define FM_UTILITIES_H + +#include "libfmqtglobals.h" +#include +#include +#include +#include +#include + +#include +#include + +#include "core/filepath.h" +#include "core/fileinfo.h" + +class QDialog; + +namespace Fm { + +LIBFM_QT_API Fm::FilePathList pathListFromUriList(const char* uriList); + +LIBFM_QT_API QByteArray pathListToUriList(const Fm::FilePathList& paths); + +LIBFM_QT_API Fm::FilePathList pathListFromQUrls(QList urls); + +LIBFM_QT_API std::pair parseClipboardData(const QMimeData& data); + +LIBFM_QT_API void pasteFilesFromClipboard(const Fm::FilePath& destPath, QWidget* parent = 0); + +LIBFM_QT_API void copyFilesToClipboard(const Fm::FilePathList& files); + +LIBFM_QT_API void cutFilesToClipboard(const Fm::FilePathList& files); + +LIBFM_QT_API bool isCurrentPidClipboardData(const QMimeData& data); + +LIBFM_QT_API void changeFileName(const Fm::FilePath& path, const QString& newName, QWidget* parent); + +LIBFM_QT_API void renameFile(std::shared_ptr file, QWidget* parent = 0); + +enum CreateFileType { + CreateNewFolder, + CreateNewTextFile, + CreateWithTemplate +}; + +LIBFM_QT_API void createFileOrFolder(CreateFileType type, Fm::FilePath parentDir, FmTemplate* templ = nullptr, QWidget* parent = 0); + +LIBFM_QT_API uid_t uidFromName(QString name); + +LIBFM_QT_API QString uidToName(uid_t uid); + +LIBFM_QT_API gid_t gidFromName(QString name); + +LIBFM_QT_API QString gidToName(gid_t gid); + +LIBFM_QT_API int execModelessDialog(QDialog* dlg); + +// NOTE: this does not work reliably due to some problems in gio/gvfs +// Use uriExists() whenever possible. +LIBFM_QT_API bool isUriSchemeSupported(const char* uriScheme); + +LIBFM_QT_API bool uriExists(const char* uri); + +LIBFM_QT_API QString formatFileSize(std::uint64_t size, bool useSI = false); + +} + +#endif // FM_UTILITIES_H diff --git a/src/utilities_p.h b/src/utilities_p.h new file mode 100644 index 0000000..7852f99 --- /dev/null +++ b/src/utilities_p.h @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef __UTILITS_P_H__ +#define __UTILITS_P_H__ + +#include +#include +#include + +namespace Fm { + +// private class used in internal implementation +class FilenameDialog : public QInputDialog { + Q_OBJECT +public: + FilenameDialog(QWidget* parent = 0, Qt::WindowFlags flags = 0): + QInputDialog(parent, flags), + selectExtension_(false) { + } + + virtual void showEvent(QShowEvent * event) { + QWidget::showEvent(event); + if(!selectExtension_) // dot not select filename extension + QTimer::singleShot(0, this, SLOT(initSelection())); + } + + bool selectExtension() const { + return selectExtension_; + } + + void setSelectExtension(bool value) { + selectExtension_ = value; + } + +private Q_SLOTS: + // do not select filename extensions + void initSelection() { + // find the QLineEdit child widget + QLineEdit* lineEdit = findChild(); + if(lineEdit) { + QString filename = lineEdit->text(); + if(!filename.isEmpty()) { + // only select filename part without extension name. + int ext = filename.lastIndexOf('.'); + if(ext != -1) { + // add special cases for tar.gz, tar.bz2, and other tar.* files + if(filename.leftRef(ext).endsWith(".tar")) + ext -= 4; + // FIXME: should we also handle other special cases? + lineEdit->setSelection(0, ext); + } + } + } + } + +private: + bool selectExtension_; +}; + +} // namespace Fm + +#endif diff --git a/src/utils.h b/src/utils.h new file mode 100644 index 0000000..109146d --- /dev/null +++ b/src/utils.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2016 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef __LIBFM_QT_FM_UTILS_H__ +#define __LIBFM_QT_FM_UTILS_H__ + +#include +#include +#include +#include "libfmqtglobals.h" + + +namespace Fm { + + + +} + +#endif // __LIBFM_QT_FM_UTILS_H__ diff --git a/src/xdndworkaround.cpp b/src/xdndworkaround.cpp new file mode 100644 index 0000000..e60feac --- /dev/null +++ b/src/xdndworkaround.cpp @@ -0,0 +1,292 @@ +/* + * Copyright (C) 2016 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include +#include "xdndworkaround.h" +#include +#include +#include +#include +#include +#include + +// This part is for Qt >= 5.4 only +#if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)) +#include +#include +#include + +// these are private Qt headers which are not part of Qt APIs +#include // Too bad that we need to use private headers of Qt :-( + +// For some unknown reasons, the event type constants defined in +// xcb/input.h are different from that in X11/extension/XI2.h +// To be safe, we define it ourselves. +#undef XI_ButtonRelease +#define XI_ButtonRelease 5 + +#endif // (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)) + +XdndWorkaround::XdndWorkaround() { + if(!QX11Info::isPlatformX11()) { + return; + } + + // we need to filter all X11 events + qApp->installNativeEventFilter(this); + +// This part is for Qt >= 5.4 only +#if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)) + lastDrag_ = nullptr; + + // initialize xinput2 since newer versions of Qt5 uses it. + static char xi_name[] = "XInputExtension"; + xcb_connection_t* conn = QX11Info::connection(); + xcb_query_extension_cookie_t cookie = xcb_query_extension(conn, strlen(xi_name), xi_name); + xcb_generic_error_t* err = nullptr; + xcb_query_extension_reply_t* reply = xcb_query_extension_reply(conn, cookie, &err); + if(err == nullptr) { + xinput2Enabled_ = true; + xinputOpCode_ = reply->major_opcode; + xinputEventBase_ = reply->first_event; + xinputErrorBase_ = reply->first_error; + // qDebug() << "xinput: " << m_xi2Enabled << m_xiOpCode << m_xiEventBase; + } + else { + xinput2Enabled_ = false; + free(err); + } + free(reply); +#endif // (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)) +} + +XdndWorkaround::~XdndWorkaround() { + if(!QX11Info::isPlatformX11()) { + return; + } + qApp->removeNativeEventFilter(this); +} + +bool XdndWorkaround::nativeEventFilter(const QByteArray& eventType, void* message, long* /*result*/) { + if(Q_LIKELY(eventType == "xcb_generic_event_t")) { + xcb_generic_event_t* event = static_cast(message); + switch(event->response_type & ~0x80) { + case XCB_CLIENT_MESSAGE: + return clientMessage(reinterpret_cast(event)); + case XCB_SELECTION_NOTIFY: + return selectionNotify(reinterpret_cast(event)); +// This part is for Qt >= 5.4 only +#if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)) + case XCB_SELECTION_REQUEST: + return selectionRequest(reinterpret_cast(event)); + case XCB_GE_GENERIC: + // newer versions of Qt5 supports xinput2, which sends its mouse events via XGE. + return genericEvent(reinterpret_cast(event)); + case XCB_BUTTON_RELEASE: + // older versions of Qt5 receive mouse events via old XCB events. + buttonRelease(); + break; +#endif // Qt >= 5.4 + default: + break; + } + } + return false; +} + +// static +QByteArray XdndWorkaround::atomName(xcb_atom_t atom) { + QByteArray name; + xcb_connection_t* conn = QX11Info::connection(); + xcb_get_atom_name_cookie_t cookie = xcb_get_atom_name(conn, atom); + xcb_get_atom_name_reply_t* reply = xcb_get_atom_name_reply(conn, cookie, nullptr); + int len = xcb_get_atom_name_name_length(reply); + if(len > 0) { + name.append(xcb_get_atom_name_name(reply), len); + } + free(reply); + return name; +} + +// static +xcb_atom_t XdndWorkaround::internAtom(const char* name, int len) { + xcb_atom_t atom = 0; + if(len == -1) { + len = strlen(name); + } + xcb_connection_t* conn = QX11Info::connection(); + xcb_intern_atom_cookie_t cookie = xcb_intern_atom(conn, false, len, name); + xcb_generic_error_t* err = nullptr; + xcb_intern_atom_reply_t* reply = xcb_intern_atom_reply(conn, cookie, &err); + if(reply != nullptr) { + atom = reply->atom; + free(reply); + } + if(err != nullptr) { + free(err); + } + return atom; +} + +// static +QByteArray XdndWorkaround::windowProperty(xcb_window_t window, xcb_atom_t propAtom, xcb_atom_t typeAtom, int len) { + QByteArray data; + xcb_connection_t* conn = QX11Info::connection(); + xcb_get_property_cookie_t cookie = xcb_get_property(conn, false, window, propAtom, typeAtom, 0, len); + xcb_generic_error_t* err = nullptr; + xcb_get_property_reply_t* reply = xcb_get_property_reply(conn, cookie, &err); + if(reply != nullptr) { + len = xcb_get_property_value_length(reply); + const char* buf = (const char*)xcb_get_property_value(reply); + data.append(buf, len); + free(reply); + } + if(err != nullptr) { + free(err); + } + return data; +} + +// static +void XdndWorkaround::setWindowProperty(xcb_window_t window, xcb_atom_t propAtom, xcb_atom_t typeAtom, void* data, int len, int format) { + xcb_connection_t* conn = QX11Info::connection(); + xcb_change_property(conn, XCB_PROP_MODE_REPLACE, window, propAtom, typeAtom, format, len, data); +} + + +bool XdndWorkaround::clientMessage(xcb_client_message_event_t* event) { + QByteArray event_type = atomName(event->type); + // qDebug() << "client message:" << event_type; + + // NOTE: Because of the limitation of Qt, this hack is required to provide + // Xdnd direct save (XDS) protocol support. + // http://www.freedesktop.org/wiki/Specifications/XDS/#index4h2 + // + // XDS requires that the drop target should get and set the window property of the + // drag source to pass the file path, but in Qt there is NO way to know the + // window ID of the drag source so it's not possible to implement XDS with Qt alone. + // Here is a simple hack. We get the drag source window ID with raw XCB code. + // Then, save it on the drop target widget using QObject dynamic property. + // So in the drop event handler of the target widget, it can obtain the + // window ID of the drag source with QObject::property(). + // This hack works 99.99% of the time, but it's not bullet-proof. + // In theory, there is one corner case for which this will not work. + // That is, when you drag multiple XDS sources at the same time and drop + // all of them on the same widget. (Does XDND support doing this?) + // I do not think that any app at the moment support this. + // Even if somebody is using it, X11 will die and we should solve this in Wayland instead. + // + if(event_type == "XdndDrop") { + // data.l[0] contains the XID of the source window. + // data.l[1] is reserved for future use (flags). + // data.l[2] contains the time stamp for retrieving the data. (new in version 1) + QWidget* target = QWidget::find(event->window); + if(target != nullptr) { // drop on our widget + target = qApp->widgetAt(QCursor::pos()); // get the exact child widget that receives the drop + if(target != nullptr) { + target->setProperty("xdnd::lastDragSource", event->data.data32[0]); + target->setProperty("xdnd::lastDropTime", event->data.data32[2]); + } + } + } + // This part is for Qt >= 5.4 only +#if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)) + else if(event_type == "XdndFinished") { + lastDrag_ = nullptr; + } +#endif // Qt >= 5.4 + return false; +} + +bool XdndWorkaround::selectionNotify(xcb_selection_notify_event_t* event) { + qDebug() << "selection notify" << atomName(event->selection); + return false; +} + +// This part is for Qt >= 5.4 only +#if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)) + +bool XdndWorkaround::selectionRequest(xcb_selection_request_event_t* event) { + xcb_connection_t* conn = QX11Info::connection(); + if(event->property == XCB_ATOM_PRIMARY || event->property == XCB_ATOM_SECONDARY) { + return false; // we only touch selection requests related to XDnd + } + QByteArray prop_name = atomName(event->property); + if(prop_name == "CLIPBOARD") { + return false; // we do not touch clipboard, either + } + + xcb_atom_t atomFormat = event->target; + QByteArray type_name = atomName(atomFormat); + // qDebug() << "selection request" << prop_name << type_name; + // We only want to handle text/x-moz-url and text/uri-list + if(type_name == "text/x-moz-url" || type_name.startsWith("text/uri-list")) { + QDragManager* mgr = QDragManager::self(); + QDrag* drag = mgr->object(); + if(drag == nullptr) { + drag = lastDrag_; + } + QMimeData* mime = drag ? drag->mimeData() : nullptr; + if(mime != nullptr && mime->hasUrls()) { + QByteArray data; + QList uris = mime->urls(); + if(type_name == "text/x-moz-url") { + QString mozurl = uris.at(0).toString(QUrl::FullyEncoded); + data.append((const char*)mozurl.utf16(), mozurl.length() * 2); + } + else { // text/uri-list + for(const QUrl& uri : uris) { + data.append(uri.toString(QUrl::FullyEncoded)); + data.append("\r\n"); + } + } + xcb_change_property(conn, XCB_PROP_MODE_REPLACE, event->requestor, event->property, + atomFormat, 8, data.size(), (const void*)data.constData()); + xcb_selection_notify_event_t notify; + notify.response_type = XCB_SELECTION_NOTIFY; + notify.requestor = event->requestor; + notify.selection = event->selection; + notify.time = event->time; + notify.property = event->property; + notify.target = atomFormat; + xcb_window_t proxy_target = event->requestor; + xcb_send_event(conn, false, proxy_target, XCB_EVENT_MASK_NO_EVENT, (const char*)¬ify); + return true; // stop Qt 5 from touching the event + } + } + return false; // let Qt handle this +} + +bool XdndWorkaround::genericEvent(xcb_ge_generic_event_t* event) { + // check this is an xinput event + if(xinput2Enabled_ && event->extension == xinputOpCode_) { + if(event->event_type == XI_ButtonRelease) { + buttonRelease(); + } + } + return false; +} + +void XdndWorkaround::buttonRelease() { + QDragManager* mgr = QDragManager::self(); + lastDrag_ = mgr->object(); + // qDebug() << "BUTTON RELEASE!!!!" << xcbDrag()->canDrop() << lastDrag_; +} + +#endif // QT_VERSION >= QT_VERSION_CHECK(5, 4, 0) diff --git a/src/xdndworkaround.h b/src/xdndworkaround.h new file mode 100644 index 0000000..e5a3f51 --- /dev/null +++ b/src/xdndworkaround.h @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2016 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +/* + * Note: + * This is a workaround for the following Qt5 bugs. + * + * #49947: Drop events have broken mimeData()->urls() and text/uri-list. + * #47981: Qt5.4 regression: Dropping text/urilist over browser windows stop working. + * + * Related LXQt bug: https://github.com/lxde/lxqt/issues/688 + * + * This workaround is not 100% reliable, but it should work most of the time. + * In theory, when there are multiple drag and drops at nearly the same time and + * you are using a remote X11 instance via a slow network connection, this workaround + * might break. However, that should be a really rare corner case. + * + * How this fix works: + * 1. Hook QApplication to filter raw X11 events + * 2. Intercept SelectionRequest events sent from XDnd target window. + * 3. Check if the data requested have the type "text/uri-list" or "x-moz-url" + * 4. Bypass the broken Qt5 code and send the mime data to the target with our own code. + * + * The mime data is obtained during the most recent mouse button release event. + * This can be incorrect in some corner cases, but it is still a simple and + * good enough approximation that returns the correct data most of the time. + * Anyway, a workarond is just a workaround. Ask Qt developers to fix their bugs. + */ + +#ifndef XDNDWORKAROUND_H +#define XDNDWORKAROUND_H + +#include + +#include +#include +#include +#include + +class QDrag; + +class XdndWorkaround : public QAbstractNativeEventFilter { +public: + explicit XdndWorkaround(); + ~XdndWorkaround(); + bool nativeEventFilter(const QByteArray& eventType, void* message, long* result) override; + static QByteArray atomName(xcb_atom_t atom); + static xcb_atom_t internAtom(const char* name, int len = -1); + static QByteArray windowProperty(xcb_window_t window, xcb_atom_t propAtom, xcb_atom_t typeAtom, int len); + static void setWindowProperty(xcb_window_t window, xcb_atom_t propAtom, xcb_atom_t typeAtom, void* data, int len, int format = 8); + +private: + bool clientMessage(xcb_client_message_event_t* event); + bool selectionNotify(xcb_selection_notify_event_t* event); + +// This part is for Qt >= 5.4 only +#if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)) +private: + bool selectionRequest(xcb_selection_request_event_t* event); + bool genericEvent(xcb_ge_generic_event_t* event); + // _QBasicDrag* xcbDrag() const; + void buttonRelease(); + + QDrag* lastDrag_; + // xinput related + bool xinput2Enabled_; + int xinputOpCode_; + int xinputEventBase_; + int xinputErrorBase_; +#endif // Qt >= 5.4 +}; + +#endif // XDNDWORKAROUND_H -- 2.30.2