--- /dev/null
+Upstream Authors:
+ LXQt team: http://lxqt.org
+ Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+
+Copyright:
+ Copyright (c) 2013-2017 LXQt team
--- /dev/null
+
+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 <pbalicek@seznam.cz>)
+ * 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 <Open with...>/<Other Applications> 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.
--- /dev/null
+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()
--- /dev/null
+# 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 <command> <input-file>, where <command> is the value of
+# the FILE_VERSION_FILTER tag, and <input-file> 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 <filter> <input-file>, where <filter>
+# is the value of the INPUT_FILTER tag, and <input-file> 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
+# <a href = "http://doc.trolltech.com/qthelpproject.html#custom-filters">
+# Qt Help Project / Custom Filters</a>.
+
+QHP_CUST_FILTER_ATTRS =
+
+# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this
+# project's
+# filter section matches.
+# <a href = "http://doc.trolltech.com/qthelpproject.html#filter-attributes">
+# Qt Help Project / Filter Attributes</a>.
+
+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
--- /dev/null
+ 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
--- /dev/null
+# 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.
--- /dev/null
+#=============================================================================
+# Copyright 2015 Luís Pereira <luis.artur.pereira@gmail.com>
+#
+# 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()
--- /dev/null
+install(FILES
+ "archivers.list"
+ "terminals.list"
+ DESTINATION "${CMAKE_INSTALL_DATADIR}/libfm-qt"
+)
+
+install(FILES
+ "libfm-qt-mimetypes.xml"
+ DESTINATION "${CMAKE_INSTALL_DATADIR}/mime/packages"
+)
--- /dev/null
+[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
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Additional mime-types provided by libfm, adding some
+ missing but frequently seen globs for some common mime-types.
+-->
+<mime-info xmlns='http://www.freedesktop.org/standards/shared-mime-info'>
+
+ <mime-type type="text/plain">
+ <glob pattern="*.ini"/>
+ <glob pattern="*.inf"/>
+ </mime-type>
+
+ <mime-type type="application/x-ms-dos-executable">
+ <glob pattern="*.com" />
+ </mime-type>
+
+ <mime-type type="application/x-ms-win-installer">
+ <comment>Windows installer</comment>
+ <comment xml:lang="zh_TW">Windows 安裝程式</comment>
+ <glob pattern="*.msi" />
+ </mime-type>
+
+ <mime-type type="application/x-vbscript">
+ <comment>MS VBScript</comment>
+ <glob pattern="*.vbs" />
+ </mime-type>
+
+ <mime-type type="text/x-csharp">
+ <comment xml:lang="en">C# source</comment>
+ <comment xml:lang="zh_TW">C# 程式碼</comment>
+ <glob pattern="*.cs"/>
+ </mime-type>
+
+ <mime-type type="application/x-desktop">
+ <comment xml:lang="zh_TW">應用程式捷徑</comment>
+ </mime-type>
+
+ <mime-type type="application/x-sharedlib">
+ <!--
+ This pattern matching is not very accurate ,but the probability that
+ a file is named like this and it's not a shared lib, is very low.
+ -->
+ <glob pattern="*.dll"/> <!-- Windows dll are shared libs, too -->
+ <glob pattern="*.so.[0-9]" />
+ <glob pattern="*.so.[0-9].*" />
+ </mime-type>
+
+ <mime-type type="application/x-desktop">
+ <glob pattern="*.directory"/>
+ </mime-type>
+
+</mime-info>
--- /dev/null
+[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
--- /dev/null
+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
+ "$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
+ "$<BUILD_INTERFACE:${LIBFM_QT_INTREE_INCLUDE_DIR}>"
+)
+
+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})
+
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>AppChooserDialog</class>
+ <widget class="QDialog" name="AppChooserDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>432</width>
+ <height>387</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Choose an Application</string>
+ </property>
+ <layout class="QFormLayout" name="formLayout">
+ <property name="fieldGrowthPolicy">
+ <enum>QFormLayout::AllNonFixedFieldsGrow</enum>
+ </property>
+ <item row="0" column="1">
+ <widget class="QLabel" name="fileTypeHeader"/>
+ </item>
+ <item row="1" column="0" colspan="2">
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>1</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="tab">
+ <attribute name="title">
+ <string>Installed Applications</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="Fm::AppMenuView" name="appMenuView"/>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="tab_2">
+ <attribute name="title">
+ <string>Custom Command</string>
+ </attribute>
+ <layout class="QFormLayout" name="formLayout_2">
+ <item row="0" column="0" colspan="2">
+ <widget class="QLabel" name="label_3">
+ <property name="text">
+ <string>Command line to execute:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0" colspan="2">
+ <widget class="QLineEdit" name="cmdLine"/>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="label_4">
+ <property name="text">
+ <string>Application name:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QLineEdit" name="appName"/>
+ </item>
+ <item row="2" column="0" colspan="2">
+ <widget class="QLabel" name="label_5">
+ <property name="text">
+ <string><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></string>
+ </property>
+ <property name="textFormat">
+ <enum>Qt::RichText</enum>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="0" colspan="2">
+ <widget class="QCheckBox" name="keepTermOpen">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Keep terminal window open after command execution</string>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="0" colspan="2">
+ <widget class="QCheckBox" name="useTerminal">
+ <property name="text">
+ <string>Execute in terminal emulator</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ <item row="2" column="0" colspan="2">
+ <widget class="QCheckBox" name="setDefault">
+ <property name="text">
+ <string>Set selected application as default action of this file type</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0" colspan="2">
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>Fm::AppMenuView</class>
+ <extends>QTreeView</extends>
+ <header>appmenuview.h</header>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>AppChooserDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>227</x>
+ <y>359</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>AppChooserDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>295</x>
+ <y>365</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>useTerminal</sender>
+ <signal>toggled(bool)</signal>
+ <receiver>keepTermOpen</receiver>
+ <slot>setEnabled(bool)</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>72</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>79</x>
+ <y>282</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
--- /dev/null
+/*
+ * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#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<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &AppChooserComboBox::onCurrentIndexChanged);
+}
+
+AppChooserComboBox::~AppChooserComboBox() {
+}
+
+void AppChooserComboBox::setMimeType(std::shared_ptr<const Fm::MimeType> mimeType) {
+ clear();
+ defaultApp_.reset();
+ appInfos_.clear();
+
+ mimeType_ = std::move(mimeType);
+ if(mimeType_) {
+ const char* typeName = mimeType_->name();
+ defaultApp_ = Fm::GAppInfoPtr{g_app_info_get_default_for_type(typeName, FALSE), false};
+ GList* appInfos_glist = g_app_info_get_all_for_type(typeName);
+ int i = 0;
+ for(GList* l = appInfos_glist; l; l = l->next, ++i) {
+ Fm::GAppInfoPtr app{G_APP_INFO(l->data), false};
+ GIcon* gicon = g_app_info_get_icon(app.get());
+ addItem(gicon ? Fm::IconInfo::fromGIcon(gicon)->qicon(): QIcon(), g_app_info_get_name(app.get()));
+ if(g_app_info_equal(app.get(), defaultApp_.get())) {
+ defaultAppIndex_ = i;
+ }
+ appInfos_.push_back(std::move(app));
+ }
+ g_list_free(appInfos_glist);
+ }
+ // add "Other applications" item
+ insertSeparator(count());
+ addItem(tr("Customize"));
+ if(defaultAppIndex_ != -1) {
+ setCurrentIndex(defaultAppIndex_);
+ }
+}
+
+// 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
--- /dev/null
+/*
+ * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef FM_APPCHOOSERCOMBOBOX_H
+#define FM_APPCHOOSERCOMBOBOX_H
+
+#include "libfmqtglobals.h"
+#include <QComboBox>
+#include <libfm/fm.h>
+
+#include <vector>
+
+#include "core/mimetype.h"
+#include "core/gioptrs.h"
+
+namespace Fm {
+
+class LIBFM_QT_API AppChooserComboBox : public QComboBox {
+ Q_OBJECT
+public:
+ ~AppChooserComboBox();
+ explicit AppChooserComboBox(QWidget* parent);
+
+ void setMimeType(std::shared_ptr<const Fm::MimeType> mimeType);
+
+ const std::shared_ptr<const Fm::MimeType>& 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<const Fm::MimeType> mimeType_;
+ std::vector<Fm::GAppInfoPtr> appInfos_; // applications used to open the file type
+ Fm::GAppInfoPtr defaultApp_; // default application used to open the file type
+ int defaultAppIndex_;
+ int prevIndex_;
+ bool blockOnCurrentIndexChanged_;
+};
+
+}
+
+#endif // FM_APPCHOOSERCOMBOBOX_H
--- /dev/null
+/*
+ * Copyright 2010-2014 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ * Copyright 2012-2013 Andriy Grytsenko (LStranger) <andrej@rep.kiev.ua>
+ *
+ * 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 <QPushButton>
+#include <gio/gdesktopappinfo.h>
+
+namespace Fm {
+
+AppChooserDialog::AppChooserDialog(std::shared_ptr<const Fm::MimeType> mimeType, QWidget* parent, Qt::WindowFlags f):
+ QDialog(parent, f),
+ ui(new Ui::AppChooserDialog()),
+ mimeType_{std::move(mimeType)},
+ canSetDefault_(true) {
+ ui->setupUi(this);
+
+ connect(ui->appMenuView, &AppMenuView::selectionChanged, this, &AppChooserDialog::onSelectionChanged);
+ connect(ui->tabWidget, &QTabWidget::currentChanged, this, &AppChooserDialog::onTabChanged);
+
+ 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<const Fm::MimeType> mimeType) {
+ mimeType_ = std::move(mimeType);
+ if(mimeType_) {
+ QString text = tr("Select an application to open \"%1\" files")
+ .arg(QString::fromUtf8(mimeType_->desc()));
+ ui->fileTypeHeader->setText(text);
+ }
+ else {
+ ui->fileTypeHeader->hide();
+ ui->setDefault->hide();
+ }
+}
+
+void AppChooserDialog::setCanSetDefault(bool value) {
+ canSetDefault_ = value;
+ ui->setDefault->setVisible(value);
+}
+
+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
--- /dev/null
+/*
+ * Copyright 2010-2014 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ * Copyright 2012-2013 Andriy Grytsenko (LStranger) <andrej@rep.kiev.ua>
+ *
+ * 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 <QDialog>
+#include "libfmqtglobals.h"
+#include <libfm/fm.h>
+
+#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<const Fm::MimeType> mimeType, QWidget* parent = nullptr, Qt::WindowFlags f = 0);
+ ~AppChooserDialog();
+
+ virtual void accept();
+
+ void setMimeType(std::shared_ptr<const Fm::MimeType> mimeType);
+
+ const std::shared_ptr<const Fm::MimeType>& 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<const Fm::MimeType> mimeType_;
+ bool canSetDefault_;
+ Fm::GAppInfoPtr selectedApp_;
+};
+
+}
+
+#endif // FM_APPCHOOSERDIALOG_H
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#include "applaunchcontext.h"
+#include <QX11Info>
+#include <X11/Xlib.h>
+
+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;
+}
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_APP_LAUNCHCONTEXT_H
+#define FM_APP_LAUNCHCONTEXT_H
+
+#include "libfmqtglobals.h"
+#include <gio/gio.h>
+#include <QWidget>
+
+#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
--- /dev/null
+/*
+ * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#include "appmenuview.h"
+#include <QStandardItemModel>
+#include "icontheme.h"
+#include "appmenuview_p.h"
+#include <gio/gdesktopappinfo.h>
+
+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<AppMenuViewItem*>(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
--- /dev/null
+/*
+ * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef FM_APPMENUVIEW_H
+#define FM_APPMENUVIEW_H
+
+#include <QTreeView>
+#include "libfmqtglobals.h"
+#include <libfm/fm.h>
+#include <menu-cache/menu-cache.h>
+
+#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<AppMenuView*>(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
--- /dev/null
+/*
+ * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef FM_APPMENUVIEW_P_H
+#define FM_APPMENUVIEW_P_H
+
+#include <QStandardItem>
+#include <menu-cache/menu-cache.h>
+#include "icontheme.h"
+#include "core/iconinfo.h"
+
+namespace Fm {
+
+class AppMenuViewItem : public QStandardItem {
+public:
+ explicit AppMenuViewItem(MenuCacheItem* item):
+ item_(menu_cache_item_ref(item)) {
+ std::shared_ptr<const Fm::IconInfo> icon;
+ if(menu_cache_item_get_icon(item)) {
+ icon = Fm::IconInfo::fromName(menu_cache_item_get_icon(item));
+ }
+ setText(menu_cache_item_get_name(item));
+ setEditable(false);
+ setDragEnabled(false);
+ if(icon) {
+ setIcon(icon->qicon());
+ }
+ }
+
+ ~AppMenuViewItem() {
+ menu_cache_item_unref(item_);
+ }
+
+ 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
--- /dev/null
+/*
+ * Copyright (C) 2016 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef __LIBFM_QT_FM_ARCHIVER_H__
+#define __LIBFM_QT_FM_ARCHIVER_H__
+
+#include <libfm/fm.h>
+#include <QObject>
+#include <QtGlobal>
+#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<FmArchiver*>(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<FmArchiver*>(dataPtr);
+ return obj;
+ }
+
+ // disown the managed data pointer
+ FmArchiver* takeDataPtr() {
+ FmArchiver* data = reinterpret_cast<FmArchiver*>(dataPtr_);
+ dataPtr_ = nullptr;
+ return data;
+ }
+
+ // get the raw pointer wrapped
+ FmArchiver* dataPtr() {
+ return reinterpret_cast<FmArchiver*>(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<FmArchiver*>(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__
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#include "bookmarkaction.h"
+
+namespace Fm {
+
+BookmarkAction::BookmarkAction(std::shared_ptr<const Fm::BookmarkItem> item, QObject* parent):
+ QAction(parent),
+ item_(std::move(item)) {
+
+ setText(item_->name());
+}
+
+} // namespace Fm
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef BOOKMARKACTION_H
+#define BOOKMARKACTION_H
+
+#include "libfmqtglobals.h"
+#include <QAction>
+#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<const Fm::BookmarkItem> item, QObject* parent = 0);
+
+ const std::shared_ptr<const Fm::BookmarkItem>& bookmark() const {
+ return item_;
+ }
+
+ const Fm::FilePath& path() const {
+ return item_->path();
+ }
+
+private:
+ std::shared_ptr<const Fm::BookmarkItem> item_;
+};
+
+}
+
+#endif // BOOKMARKACTION_H
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#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<size_t>(maxCount_)) {
+ // if there are too many items, remove the oldest one.
+ // FIXME: what if currentIndex_ == 0? remove the last item instead?
+ if(currentIndex_ == 0) {
+ items_.erase(items_.cbegin() + lastIndex);
+ }
+ else {
+ items_.erase(items_.cbegin());
+ --currentIndex_;
+ }
+ }
+ // add a path and current scroll position to browse history
+ items_.push_back(BrowseHistoryItem(path, scrollPos));
+ currentIndex_ = items_.size() - 1;
+}
+
+void BrowseHistory::setCurrentIndex(int index) {
+ if(index >= 0 && static_cast<size_t>(index) < items_.size()) {
+ currentIndex_ = index;
+ // FIXME: should we emit a signal for the change?
+ }
+}
+
+bool BrowseHistory::canBackward() const {
+ return (currentIndex_ > 0);
+}
+
+int BrowseHistory::backward() {
+ if(canBackward()) {
+ --currentIndex_;
+ }
+ return currentIndex_;
+}
+
+bool BrowseHistory::canForward() const {
+ return (static_cast<size_t>(currentIndex_) + 1 < items_.size());
+}
+
+int BrowseHistory::forward() {
+ if(canForward()) {
+ ++currentIndex_;
+ }
+ return currentIndex_;
+}
+
+void BrowseHistory::setMaxCount(int maxCount) {
+ maxCount_ = maxCount;
+ if(items_.size() > static_cast<size_t>(maxCount)) {
+ // TODO: remove some items
+ }
+}
+
+
+} // namespace Fm
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_BROWSEHISTORY_H
+#define FM_BROWSEHISTORY_H
+
+#include "libfmqtglobals.h"
+#include <vector>
+#include <libfm/fm.h>
+
+#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<BrowseHistoryItem> items_;
+ int currentIndex_;
+ int maxCount_;
+};
+
+}
+
+#endif // FM_BROWSEHISTORY_H
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#include "cachedfoldermodel.h"
+
+namespace Fm {
+
+CachedFolderModel::CachedFolderModel(const std::shared_ptr<Fm::Folder>& folder):
+ FolderModel(),
+ refCount(1) {
+ FolderModel::setFolder(folder);
+}
+
+CachedFolderModel::~CachedFolderModel() {
+ // qDebug("delete CachedFolderModel");
+}
+
+CachedFolderModel* CachedFolderModel::modelFromFolder(const std::shared_ptr<Fm::Folder>& folder) {
+ QVariant cache = folder->property(cacheKey);
+ CachedFolderModel* model = cache.value<CachedFolderModel*>();
+ if(model) {
+ model->ref();
+ }
+ else {
+ model = new CachedFolderModel(folder);
+ cache = QVariant::fromValue(model);
+ folder->setProperty(cacheKey, cache);
+ }
+ return model;
+}
+
+CachedFolderModel* CachedFolderModel::modelFromPath(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
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_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<Fm::Folder>& folder);
+ void ref() {
+ ++refCount;
+ }
+ void unref();
+
+ static CachedFolderModel* modelFromFolder(const std::shared_ptr<Fm::Folder>& 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
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#include "colorbutton.h"
+#include <QColorDialog>
+
+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
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_COLORBUTTON_H
+#define FM_COLORBUTTON_H
+
+#include "libfmqtglobals.h"
+#include <QPushButton>
+#include <QColor>
+
+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
--- /dev/null
+#include "bookmarks.h"
+#include "cstrptr.h"
+#include <algorithm>
+#include <QTimer>
+
+namespace Fm {
+
+std::weak_ptr<Bookmarks> Bookmarks::globalInstance_;
+
+static inline CStrPtr get_legacy_bookmarks_file(void) {
+ return CStrPtr{g_build_filename(g_get_home_dir(), ".gtk-bookmarks", nullptr)};
+}
+
+static inline CStrPtr get_new_bookmarks_file(void) {
+ return CStrPtr{g_build_filename(g_get_user_config_dir(), "gtk-3.0", "bookmarks", nullptr)};
+}
+
+Bookmarks::Bookmarks(QObject* parent):
+ QObject(parent),
+ idle_handler{false} {
+
+ /* trying the gtk-3.0 first and use it if it exists */
+ auto fpath = get_new_bookmarks_file();
+ file = FilePath::fromLocalPath(fpath.get());
+ load();
+ if(items_.empty()) { /* not found, use legacy file */
+ fpath = get_legacy_bookmarks_file();
+ file = FilePath::fromLocalPath(fpath.get());
+ load();
+ }
+ mon = GObjectPtr<GFileMonitor>{g_file_monitor_file(file.gfile().get(), G_FILE_MONITOR_NONE, nullptr, nullptr), false};
+ if(mon) {
+ g_signal_connect(mon.get(), "changed", G_CALLBACK(_onFileChanged), this);
+ }
+}
+
+Bookmarks::~Bookmarks() {
+ if(mon) {
+ g_signal_handlers_disconnect_by_data(mon.get(), this);
+ }
+}
+
+const std::shared_ptr<const BookmarkItem>& Bookmarks::insert(const FilePath& path, const QString& name, int pos) {
+ const auto insert_pos = (pos < 0 || static_cast<size_t>(pos) > items_.size()) ? items_.cend() : items_.cbegin() + pos;
+ auto it = items_.insert(insert_pos, std::make_shared<const BookmarkItem>(path, name));
+ queueSave();
+ return *it;
+}
+
+void Bookmarks::remove(const std::shared_ptr<const BookmarkItem>& item) {
+ items_.erase(std::remove(items_.begin(), items_.end(), item), items_.end());
+ queueSave();
+}
+
+void Bookmarks::reorder(const std::shared_ptr<const BookmarkItem>& item, int pos) {
+ auto old_it = std::find(items_.cbegin(), items_.cend(), item);
+ if(old_it == items_.cend())
+ return;
+ std::shared_ptr<const BookmarkItem> newItem = item;
+ auto old_pos = old_it - items_.cbegin();
+ items_.erase(old_it);
+ if(old_pos < pos)
+ --pos;
+ auto new_it = items_.cbegin() + pos;
+ if(new_it > items_.cend())
+ new_it = items_.cend();
+ items_.insert(new_it, std::move(newItem));
+ queueSave();
+}
+
+void Bookmarks::rename(const std::shared_ptr<const BookmarkItem>& item, QString new_name) {
+ auto it = std::find_if(items_.cbegin(), items_.cend(), [item](const std::shared_ptr<const BookmarkItem>& elem) {
+ return elem->path() == item->path();
+ });
+ if(it != items_.cend()) {
+ // create a new item to replace the old one
+ // we do not modify the old item directly since this data structure is shared with others
+ it = items_.insert(it, std::make_shared<const BookmarkItem>(item->path(), new_name));
+ items_.erase(it + 1); // remove the old item
+ queueSave();
+ }
+}
+
+std::shared_ptr<Bookmarks> Bookmarks::globalInstance() {
+ auto bookmarks = globalInstance_.lock();
+ if(!bookmarks) {
+ bookmarks = std::make_shared<Bookmarks>();
+ globalInstance_ = bookmarks;
+ }
+ return bookmarks;
+}
+
+void Bookmarks::save() {
+ std::string buf;
+ // G_LOCK(bookmarks);
+ for(auto& item: items_) {
+ auto uri = item->path().uri();
+ buf += uri.get();
+ buf += ' ';
+ buf += item->name().toUtf8().constData();
+ buf += '\n';
+ }
+ idle_handler = false;
+ // G_UNLOCK(bookmarks);
+ GError* err = nullptr;
+ if(!g_file_replace_contents(file.gfile().get(), buf.c_str(), buf.length(), nullptr,
+ FALSE, G_FILE_CREATE_NONE, nullptr, nullptr, &err)) {
+ g_critical("%s", err->message);
+ g_error_free(err);
+ }
+ /* we changed bookmarks list, let inform who interested in that */
+ Q_EMIT changed();
+}
+
+void Bookmarks::load() {
+ auto fpath = file.localPath();
+ FILE* f;
+ char buf[1024];
+ /* load the file */
+ f = fopen(fpath.get(), "r");
+ if(f) {
+ while(fgets(buf, 1024, f)) {
+ // format of each line in the bookmark file:
+ // <URI> <name>\n
+ char* sep;
+ sep = strchr(buf, '\n');
+ if(sep) {
+ *sep = '\0';
+ }
+
+ QString name;
+ sep = strchr(buf, ' '); // find the separator between URI and name
+ if(sep) {
+ *sep = '\0';
+ name = sep + 1;
+ }
+ auto uri = buf;
+ if(uri[0] != '\0') {
+ items_.push_back(std::make_shared<BookmarkItem>(FilePath::fromUri(uri), name));
+ }
+ }
+ fclose(f);
+ }
+}
+
+void Bookmarks::onFileChanged(GFileMonitor* /*mon*/, GFile* /*gf*/, GFile* /*other*/, GFileMonitorEvent /*evt*/) {
+ // reload the bookmarks
+ items_.clear();
+ load();
+ Q_EMIT changed();
+}
+
+
+void Bookmarks::queueSave() {
+ if(!idle_handler) {
+ QTimer::singleShot(0, this, &Bookmarks::save);
+ idle_handler = true;
+ }
+}
+
+
+} // namespace Fm
--- /dev/null
+#ifndef FM2_BOOKMARKS_H
+#define FM2_BOOKMARKS_H
+
+#include "../libfmqtglobals.h"
+#include <QObject>
+#include "gobjectptr.h"
+#include "fileinfo.h"
+
+
+namespace Fm {
+
+class LIBFM_QT_API BookmarkItem {
+public:
+ friend class Bookmarks;
+
+ explicit BookmarkItem(const FilePath& path, const QString name): path_{path}, name_{name} {
+ if(name_.isEmpty()) { // if the name is not specified, use basename of the path
+ name_ = path_.baseName().get();
+ }
+ }
+
+ const QString& name() const {
+ return name_;
+ }
+
+ const FilePath& path() const {
+ return path_;
+ }
+
+ const std::shared_ptr<const FmFileInfo>& info() const {
+ return info_;
+ }
+
+private:
+ void setInfo(const std::shared_ptr<const FmFileInfo>& info) {
+ info_ = info;
+ }
+
+ void setName(const QString& name) {
+ name_ = name;
+ }
+
+private:
+ FilePath path_;
+ QString name_;
+ std::shared_ptr<const FmFileInfo> info_;
+};
+
+
+class LIBFM_QT_API Bookmarks : public QObject {
+ Q_OBJECT
+public:
+ explicit Bookmarks(QObject* parent = 0);
+
+ ~Bookmarks();
+
+ const std::shared_ptr<const BookmarkItem> &insert(const FilePath& path, const QString& name, int pos);
+
+ void remove(const std::shared_ptr<const BookmarkItem>& item);
+
+ void reorder(const std::shared_ptr<const BookmarkItem> &item, int pos);
+
+ void rename(const std::shared_ptr<const BookmarkItem>& item, QString new_name);
+
+ const std::vector<std::shared_ptr<const BookmarkItem>>& items() const {
+ return items_;
+ }
+
+ static std::shared_ptr<Bookmarks> globalInstance();
+
+Q_SIGNALS:
+ void changed();
+
+private Q_SLOTS:
+ void save();
+
+private:
+ void load();
+ void queueSave();
+
+ static void _onFileChanged(GFileMonitor* mon, GFile* gf, GFile* other, GFileMonitorEvent evt, Bookmarks* _this) {
+ _this->onFileChanged(mon, gf, other, evt);
+ }
+ void onFileChanged(GFileMonitor* mon, GFile* gf, GFile* other, GFileMonitorEvent evt);
+
+private:
+ FilePath file;
+ GObjectPtr<GFileMonitor> mon;
+ std::vector<std::shared_ptr<const BookmarkItem>> items_;
+ static std::weak_ptr<Bookmarks> globalInstance_;
+ bool idle_handler;
+};
+
+} // namespace Fm
+
+#endif // FM2_BOOKMARKS_H
--- /dev/null
+#ifndef LIBFM_QT_COMPAT_P_H
+#define LIBFM_QT_COMPAT_P_H
+
+#include "../libfmqtglobals.h"
+#include "core/filepath.h"
+#include "core/fileinfo.h"
+#include "core/gioptrs.h"
+
+// deprecated
+#include <libfm/fm.h>
+#include "path.h"
+
+// compatibility functions bridging the old libfm C APIs and new C++ APIs.
+
+namespace Fm {
+
+inline FM_QT_DEPRECATED Fm::Path _convertPath(const Fm::FilePath& path) {
+ return Fm::Path::newForGfile(path.gfile().get());
+}
+
+inline FM_QT_DEPRECATED Fm::PathList _convertPathList(const Fm::FilePathList& srcFiles) {
+ Fm::PathList ret;
+ for(auto& file: srcFiles) {
+ ret.pushTail(_convertPath(file));
+ }
+ return ret;
+}
+
+inline FM_QT_DEPRECATED FmFileInfo* _convertFileInfo(const std::shared_ptr<const Fm::FileInfo>& info) {
+ // conver to GFileInfo first
+ GFileInfoPtr ginfo{g_file_info_new(), false};
+ g_file_info_set_name(ginfo.get(), info->name().c_str());
+ g_file_info_set_display_name(ginfo.get(), info->displayName().toUtf8().constData());
+ g_file_info_set_content_type(ginfo.get(), info->mimeType()->name());
+
+ auto mode = info->mode();
+ g_file_info_set_attribute_uint32(ginfo.get(), G_FILE_ATTRIBUTE_UNIX_MODE, mode);
+ GFileType ftype = info->isDir() ? G_FILE_TYPE_DIRECTORY : G_FILE_TYPE_REGULAR; // FIXME: generate more accurate type
+ g_file_info_set_file_type(ginfo.get(), ftype);
+ g_file_info_set_size(ginfo.get(), info->size());
+ g_file_info_set_icon(ginfo.get(), info->icon()->gicon().get());
+
+ g_file_info_set_attribute_uint64(ginfo.get(), G_FILE_ATTRIBUTE_TIME_MODIFIED, info->mtime());
+ g_file_info_set_attribute_uint64(ginfo.get(), G_FILE_ATTRIBUTE_TIME_ACCESS, info->atime());
+ g_file_info_set_attribute_uint64(ginfo.get(), G_FILE_ATTRIBUTE_TIME_CHANGED, info->ctime());
+
+ auto gf = info->path().gfile();
+ return fm_file_info_new_from_g_file_data(gf.get(), ginfo.get(), nullptr);
+}
+
+}
+
+#endif // LIBFM_QT_COMPAT_P_H
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#ifndef FM2_CSTRPTR_H
+#define FM2_CSTRPTR_H
+
+#include <memory>
+#include <glib.h>
+
+namespace Fm {
+
+struct CStrDeleter {
+ void operator()(char* ptr) {
+ g_free(ptr);
+ }
+};
+
+// smart pointer for C string (char*) which should be freed by free()
+typedef std::unique_ptr<char[], CStrDeleter> CStrPtr;
+
+struct CStrHash {
+ std::size_t operator()(const char* str) const {
+ return g_str_hash(str);
+ }
+};
+
+struct CStrEqual {
+ bool operator()(const char* str1, const char* str2) const {
+ return g_str_equal(str1, str2);
+ }
+};
+
+struct CStrVDeleter {
+ void operator()(char** ptr) {
+ g_strfreev(ptr);
+ }
+};
+
+// smart pointer for C string array (char**) which should be freed by g_strfreev() of glib
+typedef std::unique_ptr<char*[], CStrVDeleter> CStrArrayPtr;
+
+
+} // namespace Fm
+
+#endif // FM2_CSTRPTR_H
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#include "dirlistjob.h"
+#include <gio/gio.h>
+#include "fileinfo_p.h"
+#include "gioptrs.h"
+#include <QDebug>
+
+namespace Fm {
+
+DirListJob::DirListJob(const FilePath& path, Flags _flags, const std::shared_ptr<const HashSet>& cutFilesHashSet):
+ dir_path{path}, flags{_flags}, cutFilesHashSet_{cutFilesHashSet} {
+}
+
+void DirListJob::exec() {
+ GErrorPtr err;
+ GFileInfoPtr dir_inf;
+ GFilePtr dir_gfile = dir_path.gfile();
+ // FIXME: these are hacks for search:/// URI implemented by libfm which contains some bugs
+ bool isFileSearch = dir_path.hasUriScheme("search");
+ if(isFileSearch) {
+ // NOTE: The GFile instance changes its URI during file enumeration (bad design).
+ // So we create a copy here to avoid channging the gfile stored in dir_path.
+ // FIXME: later we should refactor file search and remove this dirty hack.
+ dir_gfile = GFilePtr{g_file_dup(dir_gfile.get())};
+ }
+_retry:
+ err.reset();
+ dir_inf = GFileInfoPtr{
+ g_file_query_info(dir_gfile.get(), gfile_info_query_attribs,
+ G_FILE_QUERY_INFO_NONE, cancellable().get(), &err),
+ false
+ };
+ if(!dir_inf) {
+ ErrorAction act = emitError(err, ErrorSeverity::MODERATE);
+ if(act == ErrorAction::RETRY) {
+ err.reset();
+ goto _retry;
+ }
+ return;
+ }
+
+ if(g_file_info_get_file_type(dir_inf.get()) != G_FILE_TYPE_DIRECTORY) {
+ auto path_str = dir_path.toString();
+ err = GErrorPtr{
+ G_IO_ERROR,
+ G_IO_ERROR_NOT_DIRECTORY,
+ tr("The specified directory '%1' is not valid").arg(path_str.get())
+ };
+ emitError(err, ErrorSeverity::CRITICAL);
+ return;
+ }
+ else {
+ std::lock_guard<std::mutex> lock{mutex_};
+ dir_fi = std::make_shared<FileInfo>(dir_inf, dir_path.parent());
+ }
+
+ FileInfoList foundFiles;
+ /* check if FS is R/O and set attr. into inf */
+ // FIXME: _fm_file_info_job_update_fs_readonly(gf, inf, nullptr, nullptr);
+ err.reset();
+ GFileEnumeratorPtr enu = GFileEnumeratorPtr{
+ g_file_enumerate_children(dir_gfile.get(), gfile_info_query_attribs,
+ G_FILE_QUERY_INFO_NONE, cancellable().get(), &err),
+ false
+ };
+ if(enu) {
+ // qDebug() << "START LISTING:" << dir_path.toString().get();
+ while(!isCancelled()) {
+ err.reset();
+ GFileInfoPtr inf{g_file_enumerator_next_file(enu.get(), cancellable().get(), &err), false};
+ if(inf) {
+#if 0
+ FmPath* dir, *sub;
+ GFile* child;
+ if(G_UNLIKELY(job->flags & FM_DIR_LIST_JOB_DIR_ONLY)) {
+ /* FIXME: handle symlinks */
+ if(g_file_info_get_file_type(inf) != G_FILE_TYPE_DIRECTORY) {
+ g_object_unref(inf);
+ continue;
+ }
+ }
+#endif
+ // virtual folders may return children not within them
+ // For example: the search:/// URI implemented by libfm might return files from different folders during enumeration.
+ // So here we call g_file_enumerator_get_container() to get the real parent path rather than simply using dir_path.
+ // This is not the behaviour of gio, but the extensions by libfm might do this.
+ // FIXME: after we port these vfs implementation from libfm, we can redesign this.
+ FilePath realParentPath = FilePath{g_file_enumerator_get_container(enu.get()), true};
+ if(isFileSearch) { // this is a file sarch job (search:/// URI)
+ // FIXME: redesign file search and remove this dirty hack
+ // the libfm implementation of search:/// URI returns a customized GFile implementation that does not behave normally.
+ // let's get its actual URI and re-create a normal gio GFile instance from it.
+ realParentPath = FilePath::fromUri(realParentPath.uri().get());
+ }
+#if 0
+ if(g_file_info_get_file_type(inf) == G_FILE_TYPE_DIRECTORY)
+ /* for dir: check if its FS is R/O and set attr. into inf */
+ {
+ _fm_file_info_job_update_fs_readonly(child, inf, nullptr, nullptr);
+ }
+ fi = fm_file_info_new_from_g_file_data(child, inf, sub);
+#endif
+ auto fileInfo = std::make_shared<FileInfo>(inf, realParentPath);
+ if(emit_files_found) {
+ // Q_EMIT filesFound();
+ }
+
+ if(cutFilesHashSet_
+ && cutFilesHashSet_->count(fileInfo->path().hash()) > 0) {
+ fileInfo->bindCutFiles(cutFilesHashSet_);
+ }
+
+ foundFiles.push_back(std::move(fileInfo));
+ }
+ else {
+ if(err) {
+ ErrorAction act = emitError(err, ErrorSeverity::MILD);
+ /* ErrorAction::RETRY is not supported. */
+ if(act == ErrorAction::ABORT) {
+ cancel();
+ }
+ }
+ /* otherwise it's EOL */
+ break;
+ }
+ }
+ err.reset();
+ g_file_enumerator_close(enu.get(), cancellable().get(), &err);
+ }
+ else {
+ emitError(err, ErrorSeverity::CRITICAL);
+ }
+
+ // qDebug() << "END LISTING:" << dir_path.toString().get();
+ if(!foundFiles.empty()) {
+ std::lock_guard<std::mutex> lock{mutex_};
+ files_.swap(foundFiles);
+ }
+}
+
+#if 0
+//FIXME: incremental..
+
+static gboolean emit_found_files(gpointer user_data) {
+ /* this callback is called from the main thread */
+ FmDirListJob* job = FM_DIR_LIST_JOB(user_data);
+ /* g_print("emit_found_files: %d\n", g_slist_length(job->files_to_add)); */
+
+ if(g_source_is_destroyed(g_main_current_source())) {
+ return FALSE;
+ }
+ g_signal_emit(job, signals[FILES_FOUND], 0, job->files_to_add);
+ g_slist_free_full(job->files_to_add, (GDestroyNotify)fm_file_info_unref);
+ job->files_to_add = nullptr;
+ job->delay_add_files_handler = 0;
+ return FALSE;
+}
+
+static gpointer queue_add_file(FmJob* fmjob, gpointer user_data) {
+ FmDirListJob* job = FM_DIR_LIST_JOB(fmjob);
+ FmFileInfo* file = FM_FILE_INFO(user_data);
+ /* this callback is called from the main thread */
+ /* g_print("queue_add_file: %s\n", fm_file_info_get_disp_name(file)); */
+ job->files_to_add = g_slist_prepend(job->files_to_add, fm_file_info_ref(file));
+ if(job->delay_add_files_handler == 0)
+ job->delay_add_files_handler = g_timeout_add_seconds_full(G_PRIORITY_LOW,
+ 1, emit_found_files, g_object_ref(job), g_object_unref);
+ return nullptr;
+}
+
+void fm_dir_list_job_add_found_file(FmDirListJob* job, FmFileInfo* file) {
+ fm_file_info_list_push_tail(job->files, file);
+ if(G_UNLIKELY(job->emit_files_found)) {
+ fm_job_call_main_thread(FM_JOB(job), queue_add_file, file);
+ }
+}
+#endif
+
+} // namespace Fm
--- /dev/null
+#ifndef FM2_DIRLISTJOB_H
+#define FM2_DIRLISTJOB_H
+
+#include "../libfmqtglobals.h"
+#include <mutex>
+#include "job.h"
+#include "filepath.h"
+#include "gobjectptr.h"
+#include "fileinfo.h"
+
+namespace Fm {
+
+class LIBFM_QT_API DirListJob : public Job {
+ Q_OBJECT
+public:
+ enum Flags {
+ FAST = 0,
+ DIR_ONLY = 1 << 0,
+ DETAILED = 1 << 1
+ };
+
+ explicit DirListJob(const FilePath& path, Flags flags, const std::shared_ptr<const HashSet>& cutFilesHashSet = nullptr);
+
+ FileInfoList& files() {
+ return files_;
+ }
+
+ void setIncremental(bool set);
+
+ bool incremental() const {
+ return emit_files_found;
+ }
+
+ FilePath dirPath() const {
+ std::lock_guard<std::mutex> lock{mutex_};
+ return dir_path;
+ }
+
+ std::shared_ptr<const FileInfo> dirInfo() const {
+ std::lock_guard<std::mutex> lock{mutex_};
+ return dir_fi;
+ }
+
+Q_SIGNALS:
+ void filesFound(FileInfoList& foundFiles);
+
+protected:
+
+ void exec() override;
+
+private:
+ mutable std::mutex mutex_;
+ FilePath dir_path;
+ Flags flags;
+ std::shared_ptr<const FileInfo> dir_fi;
+ FileInfoList files_;
+ const std::shared_ptr<const HashSet> cutFilesHashSet_;
+ bool emit_files_found;
+ // guint delay_add_files_handler;
+ // GSList* files_to_add;
+};
+
+} // namespace Fm
+
+#endif // FM2_DIRLISTJOB_H
--- /dev/null
+#include "filechangeattrjob.h"
+
+namespace Fm {
+
+FileChangeAttrJob::FileChangeAttrJob() {
+
+}
+
+} // namespace Fm
--- /dev/null
+#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
--- /dev/null
+#include "fileinfo.h"
+#include "fileinfo_p.h"
+#include <gio/gio.h>
+
+namespace Fm {
+
+const char gfile_info_query_attribs[] = "standard::*,"
+ "unix::*,"
+ "time::*,"
+ "access::*,"
+ "id::filesystem,"
+ "metadata::emblems";
+
+FileInfo::FileInfo() {
+ // FIXME: initialize numeric data members
+}
+
+FileInfo::FileInfo(const GFileInfoPtr& inf, const FilePath& parentDirPath) {
+ setFromGFileInfo(inf, parentDirPath);
+}
+
+FileInfo::~FileInfo() {
+}
+
+void FileInfo::setFromGFileInfo(const GObjectPtr<GFileInfo>& inf, const FilePath& parentDirPath) {
+ dirPath_ = parentDirPath;
+ const char* tmp, *uri;
+ GIcon* gicon;
+ GFileType type;
+
+ name_ = g_file_info_get_name(inf.get());
+
+ dispName_ = g_file_info_get_display_name(inf.get());
+
+ size_ = g_file_info_get_size(inf.get());
+
+ tmp = g_file_info_get_content_type(inf.get());
+ if(!tmp) {
+ tmp = "application/octet-stream";
+ }
+ mimeType_ = MimeType::fromName(tmp);
+
+ mode_ = g_file_info_get_attribute_uint32(inf.get(), G_FILE_ATTRIBUTE_UNIX_MODE);
+
+ uid_ = gid_ = -1;
+ if(g_file_info_has_attribute(inf.get(), G_FILE_ATTRIBUTE_UNIX_UID)) {
+ uid_ = g_file_info_get_attribute_uint32(inf.get(), G_FILE_ATTRIBUTE_UNIX_UID);
+ }
+ if(g_file_info_has_attribute(inf.get(), G_FILE_ATTRIBUTE_UNIX_GID)) {
+ gid_ = g_file_info_get_attribute_uint32(inf.get(), G_FILE_ATTRIBUTE_UNIX_GID);
+ }
+
+ type = g_file_info_get_file_type(inf.get());
+ if(0 == mode_) { /* if UNIX file mode is not available, compose a fake one. */
+ switch(type) {
+ case G_FILE_TYPE_REGULAR:
+ mode_ |= S_IFREG;
+ break;
+ case G_FILE_TYPE_DIRECTORY:
+ mode_ |= S_IFDIR;
+ break;
+ case G_FILE_TYPE_SYMBOLIC_LINK:
+ mode_ |= S_IFLNK;
+ break;
+ case G_FILE_TYPE_SHORTCUT:
+ break;
+ case G_FILE_TYPE_MOUNTABLE:
+ break;
+ case G_FILE_TYPE_SPECIAL:
+ if(mode_) {
+ break;
+ }
+ /* if it's a special file but it doesn't have UNIX mode, compose a fake one. */
+ if(strcmp(tmp, "inode/chardevice") == 0) {
+ mode_ |= S_IFCHR;
+ }
+ else if(strcmp(tmp, "inode/blockdevice") == 0) {
+ mode_ |= S_IFBLK;
+ }
+ else if(strcmp(tmp, "inode/fifo") == 0) {
+ mode_ |= S_IFIFO;
+ }
+#ifdef S_IFSOCK
+ else if(strcmp(tmp, "inode/socket") == 0) {
+ mode_ |= S_IFSOCK;
+ }
+#endif
+ break;
+ case G_FILE_TYPE_UNKNOWN:
+ ;
+ }
+ }
+
+ if(g_file_info_has_attribute(inf.get(), G_FILE_ATTRIBUTE_ACCESS_CAN_READ)) {
+ isAccessible_ = g_file_info_get_attribute_boolean(inf.get(), G_FILE_ATTRIBUTE_ACCESS_CAN_READ);
+ }
+ else
+ /* assume it's accessible */
+ {
+ isAccessible_ = true;
+ }
+
+ if(g_file_info_has_attribute(inf.get(), G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE)) {
+ isWritable_ = g_file_info_get_attribute_boolean(inf.get(), G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE);
+ }
+ else
+ /* assume it's writable */
+ {
+ isWritable_ = true;
+ }
+
+ if(g_file_info_has_attribute(inf.get(), G_FILE_ATTRIBUTE_ACCESS_CAN_DELETE)) {
+ isDeletable_ = g_file_info_get_attribute_boolean(inf.get(), G_FILE_ATTRIBUTE_ACCESS_CAN_DELETE);
+ }
+ else
+ /* assume it's deletable */
+ {
+ isDeletable_ = true;
+ }
+
+ /* special handling for symlinks */
+ if(g_file_info_get_is_symlink(inf.get())) {
+ mode_ &= ~S_IFMT; /* reset type */
+ mode_ |= S_IFLNK; /* set type to symlink */
+ goto _file_is_symlink;
+ }
+
+ isShortcut_ = false;
+
+ switch(type) {
+ case G_FILE_TYPE_SHORTCUT:
+ isShortcut_ = true;
+ case G_FILE_TYPE_MOUNTABLE:
+ uri = g_file_info_get_attribute_string(inf.get(), G_FILE_ATTRIBUTE_STANDARD_TARGET_URI);
+ if(uri) {
+ if(g_str_has_prefix(uri, "file:///")) {
+ auto filename = CStrPtr{g_filename_from_uri(uri, nullptr, nullptr)};
+ target_ = filename.get();
+ }
+ else {
+ target_ = uri;
+ }
+ if(!mimeType_) {
+ mimeType_ = MimeType::guessFromFileName(target_.c_str());
+ }
+ }
+
+ /* if the mime-type is not determined or is unknown */
+ if(G_UNLIKELY(!mimeType_ || mimeType_->isUnknownType())) {
+ /* FIXME: is this appropriate? */
+ if(type == G_FILE_TYPE_SHORTCUT) {
+ mimeType_ = MimeType::inodeShortcut();
+ }
+ else {
+ mimeType_ = MimeType::inodeMountPoint();
+ }
+ }
+ break;
+ case G_FILE_TYPE_DIRECTORY:
+ if(!mimeType_) {
+ mimeType_ = MimeType::inodeDirectory();
+ }
+ isReadOnly_ = false; /* default is R/W */
+ if(g_file_info_has_attribute(inf.get(), G_FILE_ATTRIBUTE_FILESYSTEM_READONLY)) {
+ isReadOnly_ = g_file_info_get_attribute_boolean(inf.get(), G_FILE_ATTRIBUTE_FILESYSTEM_READONLY);
+ }
+ /* directories should be writable to be deleted by user */
+ if(isReadOnly_ || !isWritable_) {
+ isDeletable_ = false;
+ }
+ break;
+ case G_FILE_TYPE_SYMBOLIC_LINK:
+_file_is_symlink:
+ uri = g_file_info_get_symlink_target(inf.get());
+ if(uri) {
+ if(g_str_has_prefix(uri, "file:///")) {
+ auto filename = CStrPtr{g_filename_from_uri(uri, nullptr, nullptr)};
+ target_ = filename.get();
+ }
+ else {
+ target_ = uri;
+ }
+ if(!mimeType_) {
+ mimeType_ = MimeType::guessFromFileName(target_.c_str());
+ }
+ }
+ /* continue with absent mime type */
+ default: /* G_FILE_TYPE_UNKNOWN G_FILE_TYPE_REGULAR G_FILE_TYPE_SPECIAL */
+ if(G_UNLIKELY(!mimeType_)) {
+ if(!mimeType_) {
+ mimeType_ = MimeType::guessFromFileName(name_.c_str());
+ }
+ }
+ }
+
+ /* if there is a custom folder icon, use it */
+ if(isNative() && type == G_FILE_TYPE_DIRECTORY) {
+ auto local_path = path().localPath();
+ auto dot_dir = CStrPtr{g_build_filename(local_path.get(), ".directory", nullptr)};
+ if(g_file_test(dot_dir.get(), G_FILE_TEST_IS_REGULAR)) {
+ GKeyFile* kf = g_key_file_new();
+ if(g_key_file_load_from_file(kf, dot_dir.get(), G_KEY_FILE_NONE, nullptr)) {
+ CStrPtr icon_name{g_key_file_get_string(kf, "Desktop Entry", "Icon", nullptr)};
+ if(icon_name) {
+ auto dot_icon = IconInfo::fromName(icon_name.get());
+ if(dot_icon && dot_icon->isValid()) {
+ icon_ = dot_icon;
+ }
+ }
+ }
+ g_key_file_free(kf);
+ }
+ }
+
+ if(!icon_) {
+ /* try file-specific icon first */
+ gicon = g_file_info_get_icon(inf.get());
+ if(gicon) {
+ icon_ = IconInfo::fromGIcon(gicon);
+ }
+ }
+
+#if 0
+ /* set "locked" icon on unaccesible folder */
+ else if(!accessible && type == G_FILE_TYPE_DIRECTORY) {
+ icon = g_object_ref(icon_locked_folder);
+ }
+ else {
+ icon = g_object_ref(fm_mime_type_get_icon(mime_type));
+ }
+#endif
+
+ /* if the file has emblems, add them to the icon */
+ auto emblem_names = g_file_info_get_attribute_stringv(inf.get(), "metadata::emblems");
+ if(emblem_names) {
+ auto n_emblems = g_strv_length(emblem_names);
+ for(int i = n_emblems - 1; i >= 0; --i) {
+ emblems_.emplace_front(Fm::IconInfo::fromName(emblem_names[i]));
+ }
+ }
+
+ tmp = g_file_info_get_attribute_string(inf.get(), G_FILE_ATTRIBUTE_ID_FILESYSTEM);
+ filesystemId_ = g_intern_string(tmp);
+
+ mtime_ = g_file_info_get_attribute_uint64(inf.get(), G_FILE_ATTRIBUTE_TIME_MODIFIED);
+ atime_ = g_file_info_get_attribute_uint64(inf.get(), G_FILE_ATTRIBUTE_TIME_ACCESS);
+ ctime_ = g_file_info_get_attribute_uint64(inf.get(), G_FILE_ATTRIBUTE_TIME_CHANGED);
+ isHidden_ = g_file_info_get_is_hidden(inf.get());
+ isBackup_ = g_file_info_get_is_backup(inf.get());
+ isNameChangeable_ = true; /* GVFS tends to ignore this attribute */
+ isIconChangeable_ = isHiddenChangeable_ = false;
+ if(g_file_info_has_attribute(inf.get(), G_FILE_ATTRIBUTE_ACCESS_CAN_RENAME)) {
+ isNameChangeable_ = g_file_info_get_attribute_boolean(inf.get(), G_FILE_ATTRIBUTE_ACCESS_CAN_RENAME);
+ }
+
+ // special handling for desktop entry files (show the name and icon defined in the desktop entry instead)
+ if(isNative() && G_UNLIKELY(isDesktopEntry())) {
+ auto local_path = path().localPath();
+ GKeyFile* kf = g_key_file_new();
+ if(g_key_file_load_from_file(kf, local_path.get(), G_KEY_FILE_NONE, nullptr)) {
+ /* check if type is correct and supported */
+ CStrPtr type{g_key_file_get_string(kf, "Desktop Entry", "Type", nullptr)};
+ if(type) {
+ // Type == "Link"
+ if(strcmp(type.get(), G_KEY_FILE_DESKTOP_TYPE_LINK) == 0) {
+ CStrPtr uri{g_key_file_get_string(kf, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_URL, nullptr)};
+ if(uri) {
+ isShortcut_ = true;
+ target_ = uri.get();
+ }
+ }
+ }
+ CStrPtr icon_name{g_key_file_get_string(kf, "Desktop Entry", "Icon", nullptr)};
+ if(icon_name) {
+ icon_ = IconInfo::fromName(icon_name.get());
+ }
+ /* Use title of the desktop entry for display */
+ CStrPtr displayName{g_key_file_get_locale_string(kf, "Desktop Entry", "Name", nullptr, nullptr)};
+ if(displayName) {
+ dispName_ = displayName.get();
+ }
+ /* handle 'Hidden' key to set hidden attribute */
+ if(!isHidden_) {
+ isHidden_ = g_key_file_get_boolean(kf, "Desktop Entry", "Hidden", nullptr);
+ }
+ }
+ g_key_file_free(kf);
+ }
+
+ if(!icon_ && mimeType_)
+ icon_ = mimeType_->icon();
+
+#if 0
+ GFile* _gf = nullptr;
+ GFileAttributeInfoList* list;
+ auto list = g_file_query_settable_attributes(gf, nullptr, nullptr);
+ if(G_LIKELY(list)) {
+ if(g_file_attribute_info_list_lookup(list, G_FILE_ATTRIBUTE_STANDARD_ICON)) {
+ icon_is_changeable = true;
+ }
+ if(g_file_attribute_info_list_lookup(list, G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN)) {
+ hidden_is_changeable = true;
+ }
+ g_file_attribute_info_list_unref(list);
+ }
+ if(G_UNLIKELY(_gf)) {
+ g_object_unref(_gf);
+ }
+#endif
+}
+
+void FileInfo::bindCutFiles(const std::shared_ptr<const HashSet>& cutFilesHashSet) {
+ cutFilesHashSet_ = cutFilesHashSet;
+}
+
+bool FileInfo::canThumbnail() const {
+ /* We cannot use S_ISREG here as this exclude all symlinks */
+ if(size_ == 0 || /* don't generate thumbnails for empty files */
+ !(mode_ & S_IFREG) ||
+ isDesktopEntry() ||
+ isUnknownType()) {
+ return false;
+ }
+ return true;
+}
+
+/* full path of the file is required by this function */
+bool FileInfo::isExecutableType() const {
+ if(isText()) { /* g_content_type_can_be_executable reports text files as executables too */
+ /* We don't execute remote files nor files in trash */
+ if(isNative() && (mode_ & (S_IXOTH | S_IXGRP | S_IXUSR))) {
+ /* it has executable bits so lets check shell-bang */
+ auto pathStr = path().toString();
+ int fd = open(pathStr.get(), O_RDONLY);
+ if(fd >= 0) {
+ char buf[2];
+ ssize_t rdlen = read(fd, &buf, 2);
+ close(fd);
+ if(rdlen == 2 && buf[0] == '#' && buf[1] == '!') {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ return mimeType_->canBeExecutable();
+}
+
+
+bool FileInfoList::isSameType() const {
+ if(!empty()) {
+ auto& item = front();
+ for(auto it = cbegin() + 1; it != cend(); ++it) {
+ auto& item2 = *it;
+ if(item->mimeType() != item2->mimeType()) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+bool FileInfoList::isSameFilesystem() const {
+ if(!empty()) {
+ auto& item = front();
+ for(auto it = cbegin() + 1; it != cend(); ++it) {
+ auto& item2 = *it;
+ if(item->filesystemId() != item2->filesystemId()) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+
+
+} // namespace Fm
--- /dev/null
+/*
+ * Copyright (C) 2016 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef __LIBFM_QT_FM2_FILE_INFO_H__
+#define __LIBFM_QT_FM2_FILE_INFO_H__
+
+#include <libfm/fm.h>
+#include <QObject>
+#include <QtGlobal>
+#include "../libfmqtglobals.h"
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+#include <vector>
+#include <set>
+#include <utility>
+#include <string>
+#include <forward_list>
+
+#include "gioptrs.h"
+#include "filepath.h"
+#include "iconinfo.h"
+#include "mimetype.h"
+
+
+namespace Fm {
+
+class FileInfoList;
+typedef std::set<unsigned int> HashSet;
+
+class LIBFM_QT_API FileInfo {
+public:
+
+ explicit FileInfo();
+
+ explicit FileInfo(const GFileInfoPtr& inf, const FilePath& parentDirPath);
+
+ virtual ~FileInfo();
+
+ bool canSetHidden() const {
+ return isHiddenChangeable_;
+ }
+
+ bool canSetIcon() const {
+ return isIconChangeable_;
+ }
+
+ bool canSetName() const {
+ return isNameChangeable_;
+ }
+
+ bool canThumbnail() const;
+
+ gid_t gid() const {
+ return gid_;
+ }
+
+ uid_t uid() const {
+ return uid_;
+ }
+
+ const char* filesystemId() const {
+ return filesystemId_;
+ }
+
+ const std::shared_ptr<const IconInfo>& icon() const {
+ return icon_;
+ }
+
+ const std::shared_ptr<const MimeType>& mimeType() const {
+ return mimeType_;
+ }
+
+ time_t ctime() const {
+ return ctime_;
+ }
+
+
+ time_t atime() const {
+ return atime_;
+ }
+
+ time_t mtime() const {
+ return mtime_;
+ }
+
+ const std::string& target() const {
+ return target_;
+ }
+
+ bool isWritableDirectory() const {
+ return (!isReadOnly_ && isDir());
+ }
+
+ bool isAccessible() const {
+ return isAccessible_;
+ }
+
+ bool isWritable() const {
+ return isWritable_;
+ }
+
+ bool isDeletable() const {
+ return isDeletable_;
+ }
+
+ bool isExecutableType() const;
+
+ bool isBackup() const {
+ return isBackup_;
+ }
+
+ bool isHidden() const {
+ // FIXME: we might treat backup files as hidden
+ return isHidden_;
+ }
+
+ bool isUnknownType() const {
+ return mimeType_->isUnknownType();
+ }
+
+ bool isDesktopEntry() const {
+ return mimeType_->isDesktopEntry();
+ }
+
+ bool isText() const {
+ return mimeType_->isText();
+ }
+
+ bool isImage() const {
+ return mimeType_->isImage();
+ }
+
+ bool isMountable() const {
+ return mimeType_->isMountable();
+ }
+
+ bool isShortcut() const {
+ return isShortcut_;
+ }
+
+ bool isSymlink() const {
+ return S_ISLNK(mode_) ? true : false;
+ }
+
+ bool isDir() const {
+ return mimeType_->isDir();
+ }
+
+ bool isNative() const {
+ return dirPath_ ? dirPath_.isNative() : path().isNative();
+ }
+
+ bool isCut() const {
+ return !cutFilesHashSet_.expired();
+ }
+
+ mode_t mode() const {
+ return mode_;
+ }
+
+ uint64_t realSize() const {
+ return blksize_ *blocks_;
+ }
+
+ uint64_t size() const {
+ return size_;
+ }
+
+ const std::string& name() const {
+ return name_;
+ }
+
+ const QString& displayName() const {
+ return dispName_;
+ }
+
+ FilePath path() const {
+ return dirPath_ ? dirPath_.child(name_.c_str()) : FilePath::fromPathStr(name_.c_str());
+ }
+
+ const FilePath& dirPath() const {
+ return dirPath_;
+ }
+
+ void setFromGFileInfo(const GFileInfoPtr& inf, const FilePath& parentDirPath);
+
+ void bindCutFiles(const std::shared_ptr<const HashSet>& cutFilesHashSet);
+
+ const std::forward_list<std::shared_ptr<const IconInfo>>& emblems() const {
+ return emblems_;
+ }
+
+private:
+ std::string name_;
+ QString dispName_;
+
+ FilePath dirPath_;
+
+ mode_t mode_;
+ const char* filesystemId_;
+ uid_t uid_;
+ gid_t gid_;
+ uint64_t size_;
+ time_t mtime_;
+ time_t atime_;
+ time_t ctime_;
+
+ uint64_t blksize_;
+ uint64_t blocks_;
+
+ std::shared_ptr<const MimeType> mimeType_;
+ std::shared_ptr<const IconInfo> icon_;
+ std::forward_list<std::shared_ptr<const IconInfo>> emblems_;
+
+ std::string target_; /* target of shortcut or mountable. */
+
+ bool isShortcut_ : 1; /* TRUE if file is shortcut type */
+ bool isAccessible_ : 1; /* TRUE if can be read by user */
+ bool isWritable_ : 1; /* TRUE if can be written to by user */
+ bool isDeletable_ : 1; /* TRUE if can be deleted by user */
+ bool isHidden_ : 1; /* TRUE if file is hidden */
+ bool isBackup_ : 1; /* TRUE if file is backup */
+ bool isNameChangeable_ : 1; /* TRUE if name can be changed */
+ bool isIconChangeable_ : 1; /* TRUE if icon can be changed */
+ bool isHiddenChangeable_ : 1; /* TRUE if hidden can be changed */
+ bool isReadOnly_ : 1; /* TRUE if host FS is R/O */
+
+ std::weak_ptr<const HashSet> cutFilesHashSet_;
+ // std::vector<std::tuple<int, void*, void(void*)>> extraData_;
+};
+
+
+class LIBFM_QT_API FileInfoList: public std::vector<std::shared_ptr<const FileInfo>> {
+public:
+
+ bool isSameType() const;
+
+ bool isSameFilesystem() const;
+
+ FilePathList paths() const {
+ FilePathList ret;
+ for(auto& file: *this) {
+ ret.push_back(file->path());
+ }
+ return ret;
+ }
+};
+
+
+typedef std::pair<std::shared_ptr<const FileInfo>, std::shared_ptr<const FileInfo>> FileInfoPair;
+
+
+}
+
+Q_DECLARE_METATYPE(std::shared_ptr<const Fm::FileInfo>)
+
+
+#endif // __LIBFM_QT_FM2_FILE_INFO_H__
--- /dev/null
+#ifndef FILEINFO_P_H
+#define FILEINFO_P_H
+
+namespace Fm {
+
+ extern const char gfile_info_query_attribs[];
+
+} // namespace Fm
+
+#endif // FILEINFO_P_H
--- /dev/null
+#include "fileinfojob.h"
+#include "fileinfo_p.h"
+
+namespace Fm {
+
+FileInfoJob::FileInfoJob(FilePathList paths, FilePath commonDirPath, const std::shared_ptr<const HashSet>& cutFilesHashSet):
+ Job(),
+ paths_{std::move(paths)},
+ commonDirPath_{std::move(commonDirPath)},
+ cutFilesHashSet_{cutFilesHashSet} {
+}
+
+void FileInfoJob::exec() {
+ for(const auto& path: paths_) {
+ if(!isCancelled()) {
+ GErrorPtr err;
+ GFileInfoPtr inf{
+ g_file_query_info(path.gfile().get(), gfile_info_query_attribs,
+ G_FILE_QUERY_INFO_NONE, cancellable().get(), &err),
+ false
+ };
+ if(!inf)
+ return;
+
+ // Reuse the same dirPath object when the path remains the same (optimize for files in the same dir)
+ auto dirPath = commonDirPath_.isValid() ? commonDirPath_ : path.parent();
+ FileInfo fileInfo(inf, dirPath);
+
+ if(cutFilesHashSet_
+ && cutFilesHashSet_->count(fileInfo.path().hash())) {
+ fileInfo.bindCutFiles(cutFilesHashSet_);
+ }
+
+ auto fileInfoPtr = std::make_shared<const FileInfo>(fileInfo);
+
+ results_.push_back(fileInfoPtr);
+ Q_EMIT gotInfo(path, fileInfoPtr);
+ }
+ }
+}
+
+} // namespace Fm
--- /dev/null
+#ifndef FM2_FILEINFOJOB_H
+#define FM2_FILEINFOJOB_H
+
+#include "../libfmqtglobals.h"
+#include "job.h"
+#include "filepath.h"
+#include "fileinfo.h"
+
+namespace Fm {
+
+
+class LIBFM_QT_API FileInfoJob : public Job {
+ Q_OBJECT
+public:
+
+ explicit FileInfoJob(FilePathList paths, FilePath commonDirPath = FilePath(), const std::shared_ptr<const HashSet>& cutFilesHashSet = nullptr);
+
+ const FilePathList& paths() const {
+ return paths_;
+ }
+
+ const FileInfoList& files() const {
+ return results_;
+ }
+
+Q_SIGNALS:
+ void gotInfo(const FilePath& path, std::shared_ptr<const FileInfo>& info);
+
+protected:
+ void exec() override;
+
+private:
+ FilePathList paths_;
+ FileInfoList results_;
+ FilePath commonDirPath_;
+ const std::shared_ptr<const HashSet> cutFilesHashSet_;
+};
+
+} // namespace Fm
+
+#endif // FM2_FILEINFOJOB_H
--- /dev/null
+#include "filelinkjob.h"
+
+namespace Fm {
+
+FileLinkJob::FileLinkJob() {
+
+}
+
+} // namespace Fm
--- /dev/null
+#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
--- /dev/null
+#include "filemonitor.h"
+
+namespace Fm {
+
+FileMonitor::FileMonitor() {
+
+}
+
+} // namespace Fm
--- /dev/null
+#ifndef FM2_FILEMONITOR_H
+#define FM2_FILEMONITOR_H
+
+#include "../libfmqtglobals.h"
+#include <QObject>
+#include "gioptrs.h"
+#include "filepath.h"
+
+namespace Fm {
+
+class LIBFM_QT_API FileMonitor: public QObject {
+ Q_OBJECT
+public:
+
+ explicit FileMonitor();
+
+Q_SIGNALS:
+
+
+private:
+ GFileMonitorPtr monitor_;
+};
+
+} // namespace Fm
+
+#endif // FM2_FILEMONITOR_H
--- /dev/null
+#include "fileoperationjob.h"
+
+namespace Fm {
+
+FileOperationJob::FileOperationJob():
+ hasTotalAmount_{false},
+ totalSize_{0},
+ totalCount_{0},
+ finishedSize_{0},
+ finishedCount_{0},
+ currentFileSize_{0},
+ currentFileFinished_{0} {
+}
+
+bool FileOperationJob::totalAmount(uint64_t& fileSize, uint64_t& fileCount) const {
+ std::lock_guard<std::mutex> lock{mutex_};
+ if(hasTotalAmount_) {
+ fileSize = totalSize_;
+ fileCount = totalCount_;
+ }
+ return hasTotalAmount_;
+}
+
+bool FileOperationJob::currentFileProgress(FilePath& path, uint64_t& totalSize, uint64_t& finishedSize) const {
+ std::lock_guard<std::mutex> lock{mutex_};
+ if(currentFile_.isValid()) {
+ path = currentFile_;
+ totalSize = currentFileSize_;
+ finishedSize = currentFileFinished_;
+ }
+ return currentFile_.isValid();
+}
+
+FileOperationJob::FileExistsAction FileOperationJob::askRename(const FileInfo &src, const FileInfo &dest, FilePath &newDest) {
+ FileExistsAction action = SKIP;
+ Q_EMIT fileExists(src, dest, action, newDest);
+ return action;
+}
+
+bool FileOperationJob::finishedAmount(uint64_t& finishedSize, uint64_t& finishedCount) const {
+ std::lock_guard<std::mutex> lock{mutex_};
+ if(hasTotalAmount_) {
+ finishedSize = finishedSize_;
+ finishedCount = finishedCount_;
+ }
+ return hasTotalAmount_;
+}
+
+void FileOperationJob::setTotalAmount(uint64_t fileSize, uint64_t fileCount) {
+ std::lock_guard<std::mutex> locl{mutex_};
+ hasTotalAmount_ = true;
+ totalSize_ = fileSize;
+ totalCount_ = fileCount;
+}
+
+void FileOperationJob::setFinishedAmount(uint64_t finishedSize, uint64_t finishedCount) {
+ std::lock_guard<std::mutex> locl{mutex_};
+ finishedSize_ = finishedSize;
+ finishedCount_ = finishedCount;
+}
+
+void FileOperationJob::addFinishedAmount(uint64_t finishedSize, uint64_t finishedCount) {
+ std::lock_guard<std::mutex> locl{mutex_};
+ finishedSize_ += finishedSize;
+ finishedCount_ += finishedCount;
+}
+
+void FileOperationJob::setCurrentFile(const FilePath& path) {
+ std::lock_guard<std::mutex> locl{mutex_};
+ currentFile_ = path;
+}
+
+void FileOperationJob::setCurrentFileProgress(uint64_t totalSize, uint64_t finishedSize) {
+ std::lock_guard<std::mutex> locl{mutex_};
+ currentFileSize_ = totalSize;
+ currentFileFinished_ = finishedSize;
+}
+
+} // namespace Fm
--- /dev/null
+#ifndef FM2_FILEOPERATIONJOB_H
+#define FM2_FILEOPERATIONJOB_H
+
+#include "../libfmqtglobals.h"
+#include "job.h"
+#include <string>
+#include <mutex>
+#include <cstdint>
+#include "fileinfo.h"
+#include "filepath.h"
+
+namespace Fm {
+
+class LIBFM_QT_API FileOperationJob : public Fm::Job {
+ Q_OBJECT
+public:
+ enum FileExistsAction {
+ CANCEL = 0,
+ OVERWRITE = 1<<0,
+ RENAME = 1<<1,
+ SKIP = 1<<2,
+ SKIP_ERROR = 1<<3
+ };
+
+ explicit FileOperationJob();
+
+ bool totalAmount(std::uint64_t& fileSize, std::uint64_t& fileCount) const;
+
+ bool finishedAmount(std::uint64_t& finishedSize, std::uint64_t& finishedCount) const;
+
+ bool currentFileProgress(FilePath& path, std::uint64_t& totalSize, std::uint64_t& finishedSize) const;
+
+Q_SIGNALS:
+
+ void preparedToRun();
+
+ // void currentFile(const char* file);
+
+ // void progress(uint32_t percent);
+
+ // to correctly handle the signal, connect with Qt::BlockingQueuedConnection so it's a sync call.
+ void fileExists(const FileInfo& src, const FileInfo& dest, FileExistsAction& response, FilePath& newDest);
+
+protected:
+
+ FileExistsAction askRename(const FileInfo& src, const FileInfo& dest, FilePath& newDest);
+
+ void setTotalAmount(std::uint64_t fileSize, std::uint64_t fileCount);
+
+ void setFinishedAmount(std::uint64_t finishedSize, std::uint64_t finishedCount);
+
+ void addFinishedAmount(std::uint64_t finishedSize, std::uint64_t finishedCount);
+
+ void setCurrentFile(const FilePath &path);
+
+ void setCurrentFileProgress(uint64_t totalSize, uint64_t finishedSize);
+
+private:
+ bool hasTotalAmount_;
+ std::uint64_t totalSize_;
+ std::uint64_t totalCount_;
+ std::uint64_t finishedSize_;
+ std::uint64_t finishedCount_;
+
+ FilePath currentFile_;
+ std::uint64_t currentFileSize_;
+ std::uint64_t currentFileFinished_;
+ mutable std::mutex mutex_;
+};
+
+} // namespace Fm
+
+#endif // FM2_FILEOPERATIONJOB_H
--- /dev/null
+#include "filepath.h"
+#include <cstdlib>
+#include <utility>
+#include <glib.h>
+
+namespace Fm {
+
+FilePath FilePath::homeDir_;
+
+const FilePath &FilePath::homeDir() {
+ if(!homeDir_) {
+ const char* home = getenv("HOME");
+ if(!home) {
+ home = g_get_home_dir();
+ }
+ homeDir_ = FilePath::fromLocalPath(home);
+ }
+ return homeDir_;
+}
+
+} // namespace Fm
--- /dev/null
+#ifndef FM2_FILEPATH_H
+#define FM2_FILEPATH_H
+
+#include "../libfmqtglobals.h"
+#include "gobjectptr.h"
+#include "cstrptr.h"
+#include <gio/gio.h>
+#include <vector>
+#include <QMetaType>
+
+namespace Fm {
+
+class LIBFM_QT_API FilePath {
+public:
+
+ explicit FilePath() {
+ }
+
+ explicit FilePath(GFile* gfile, bool add_ref): gfile_{gfile, add_ref} {
+ }
+
+ FilePath(const FilePath& other): FilePath{} {
+ *this = other;
+ }
+
+ FilePath(FilePath&& other) noexcept: FilePath{} {
+ *this = other;
+ }
+
+ static FilePath fromUri(const char* uri) {
+ return FilePath{g_file_new_for_uri(uri), false};
+ }
+
+ static FilePath fromLocalPath(const char* path) {
+ return FilePath{g_file_new_for_path(path), false};
+ }
+
+ static FilePath fromDisplayName(const char* path) {
+ return FilePath{g_file_parse_name(path), false};
+ }
+
+ static FilePath fromPathStr(const char* path_str) {
+ return FilePath{g_file_new_for_commandline_arg(path_str), false};
+ }
+
+ bool isValid() const {
+ return gfile_ != nullptr;
+ }
+
+ unsigned int hash() const {
+ return g_file_hash(gfile_.get());
+ }
+
+ CStrPtr baseName() const {
+ return CStrPtr{g_file_get_basename(gfile_.get())};
+ }
+
+ CStrPtr localPath() const {
+ return CStrPtr{g_file_get_path(gfile_.get())};
+ }
+
+ CStrPtr uri() const {
+ return CStrPtr{g_file_get_uri(gfile_.get())};
+ }
+
+ CStrPtr toString() const {
+ if(isNative()) {
+ return localPath();
+ }
+ return uri();
+ }
+
+ // a human readable UTF-8 display name for the path
+ CStrPtr displayName() const {
+ return CStrPtr{g_file_get_parse_name(gfile_.get())};
+ }
+
+ FilePath parent() const {
+ return FilePath{g_file_get_parent(gfile_.get()), false};
+ }
+
+ bool hasParent() const {
+ return g_file_has_parent(gfile_.get(), nullptr);
+ }
+
+ bool isParentOf(const FilePath& other) {
+ return g_file_has_parent(other.gfile_.get(), gfile_.get());
+ }
+
+ bool isPrefixOf(const FilePath& other) {
+ return g_file_has_prefix(other.gfile_.get(), gfile_.get());
+ }
+
+ FilePath child(const char* name) const {
+ return FilePath{g_file_get_child(gfile_.get(), name), false};
+ }
+
+ CStrPtr relativePathStr(const FilePath& descendant) const {
+ return CStrPtr{g_file_get_relative_path(gfile_.get(), descendant.gfile_.get())};
+ }
+
+ FilePath relativePath(const char* relPath) const {
+ return FilePath{g_file_resolve_relative_path(gfile_.get(), relPath), false};
+ }
+
+ bool isNative() const {
+ return g_file_is_native(gfile_.get());
+ }
+
+ bool hasUriScheme(const char* scheme) const {
+ return g_file_has_uri_scheme(gfile_.get(), scheme);
+ }
+
+ CStrPtr uriScheme() const {
+ return CStrPtr{g_file_get_uri_scheme(gfile_.get())};
+ }
+
+ const GObjectPtr<GFile>& gfile() const {
+ return gfile_;
+ }
+
+ FilePath& operator = (const FilePath& other) {
+ gfile_ = other.gfile_;
+ return *this;
+ }
+
+ FilePath& operator = (const FilePath&& other) noexcept {
+ gfile_ = std::move(other.gfile_);
+ return *this;
+ }
+
+ bool operator == (const FilePath& other) const {
+ return operator==(other.gfile_.get());
+ }
+
+ bool operator == (GFile* other_gfile) const {
+ if(gfile_ == other_gfile) {
+ return true;
+ }
+ if(gfile_ && other_gfile) {
+ return g_file_equal(gfile_.get(), other_gfile);
+ }
+ return false;
+ }
+
+ bool operator != (const FilePath& other) const {
+ return !operator==(other);
+ }
+
+ bool operator != (std::nullptr_t) const {
+ return gfile_ != nullptr;
+ }
+
+ operator bool() const {
+ return gfile_ != nullptr;
+ }
+
+ static const FilePath& homeDir();
+
+private:
+ GObjectPtr<GFile> gfile_;
+ static FilePath homeDir_;
+};
+
+struct FilePathHash {
+ std::size_t operator() (const FilePath& path) const {
+ return path.hash();
+ }
+};
+
+typedef std::vector<FilePath> FilePathList;
+
+} // namespace Fm
+
+Q_DECLARE_METATYPE(Fm::FilePath)
+
+#endif // FM2_FILEPATH_H
--- /dev/null
+#include "filesysteminfojob.h"
+#include "gobjectptr.h"
+
+namespace Fm {
+
+void FileSystemInfoJob::exec() {
+ GObjectPtr<GFileInfo> inf = GObjectPtr<GFileInfo>{
+ g_file_query_filesystem_info(
+ path_.gfile().get(),
+ G_FILE_ATTRIBUTE_FILESYSTEM_SIZE","
+ G_FILE_ATTRIBUTE_FILESYSTEM_FREE,
+ cancellable().get(), nullptr),
+ false
+ };
+ if(!inf)
+ return;
+ if(g_file_info_has_attribute(inf.get(), G_FILE_ATTRIBUTE_FILESYSTEM_SIZE)) {
+ size_ = g_file_info_get_attribute_uint64(inf.get(), G_FILE_ATTRIBUTE_FILESYSTEM_SIZE);
+ freeSize_ = g_file_info_get_attribute_uint64(inf.get(), G_FILE_ATTRIBUTE_FILESYSTEM_FREE);
+ isAvailable_ = true;
+ }
+}
+
+} // namespace Fm
--- /dev/null
+#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
--- /dev/null
+/*
+ * fm-folder.c
+ *
+ * Copyright 2009 - 2012 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ * Copyright 2012-2016 Andriy Grytsenko (LStranger) <andrej@rep.kiev.ua>
+ *
+ * This file is a part of the Libfm library.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "folder.h"
+#include <string.h>
+#include <cassert>
+#include <QTimer>
+#include <QDebug>
+
+#include "dirlistjob.h"
+#include "filesysteminfojob.h"
+#include "fileinfojob.h"
+
+namespace Fm {
+
+std::unordered_map<FilePath, std::weak_ptr<Folder>, FilePathHash> Folder::cache_;
+FilePath Folder::cutFilesDirPath_;
+FilePath Folder::lastCutFilesDirPath_;
+std::shared_ptr<const HashSet> Folder::cutFilesHashSet_;
+std::mutex Folder::mutex_;
+
+Folder::Folder():
+ dirlist_job{nullptr},
+ fsInfoJob_{nullptr},
+ volumeManager_{VolumeManager::globalInstance()},
+ /* for file monitor */
+ has_idle_reload_handler{0},
+ has_idle_update_handler{false},
+ pending_change_notify{false},
+ filesystem_info_pending{false},
+ wants_incremental{false},
+ stop_emission{false}, /* don't set it 1 bit to not lock other bits */
+ /* filesystem info - set in query thread, read in main */
+ fs_total_size{0},
+ fs_free_size{0},
+ has_fs_info{false},
+ defer_content_test{false} {
+
+ connect(volumeManager_.get(), &VolumeManager::mountAdded, this, &Folder::onMountAdded);
+ connect(volumeManager_.get(), &VolumeManager::mountRemoved, this, &Folder::onMountRemoved);
+}
+
+Folder::Folder(const FilePath& path): Folder() {
+ dirPath_ = path;
+}
+
+Folder::~Folder() {
+ if(dirMonitor_) {
+ g_signal_handlers_disconnect_by_data(dirMonitor_.get(), this);
+ dirMonitor_.reset();
+ }
+
+ if(dirlist_job) {
+ dirlist_job->cancel();
+ }
+
+ // cancel any file info job in progress.
+ for(auto job: fileinfoJobs_) {
+ job->cancel();
+ }
+
+ if(fsInfoJob_) {
+ fsInfoJob_->cancel();
+ }
+
+ // We store a weak_ptr instead of shared_ptr in the hash table, so the hash table
+ // does not own a reference to the folder. When the last reference to Folder is
+ // freed, we need to remove its hash table entry.
+ std::lock_guard<std::mutex> lock{mutex_};
+ auto it = cache_.find(dirPath_);
+ if(it != cache_.end()) {
+ cache_.erase(it);
+ }
+}
+
+// static
+std::shared_ptr<Folder> Folder::fromPath(const FilePath& path) {
+ std::lock_guard<std::mutex> lock{mutex_};
+ auto it = cache_.find(path);
+ if(it != cache_.end()) {
+ auto folder = it->second.lock();
+ if(folder) {
+ return folder;
+ }
+ else { // FIXME: is this possible?
+ cache_.erase(it);
+ }
+ }
+ auto folder = std::make_shared<Folder>(path);
+ folder->reload();
+ cache_.emplace(path, folder);
+ return folder;
+}
+
+bool Folder::makeDirectory(const char* /*name*/, GError** /*error*/) {
+ // TODO:
+ // FIXME: what the API is used for in the original libfm C API?
+ return false;
+}
+
+bool Folder::isIncremental() const {
+ return wants_incremental;
+}
+
+bool Folder::isValid() const {
+ return dirInfo_ != nullptr;
+}
+
+bool Folder::isLoaded() const {
+ return (dirlist_job == nullptr);
+}
+
+std::shared_ptr<const FileInfo> Folder::fileByName(const char* name) const {
+ auto it = files_.find(name);
+ if(it != files_.end()) {
+ return it->second;
+ }
+ return nullptr;
+}
+
+bool Folder::isEmpty() const {
+ return files_.empty();
+}
+
+FileInfoList Folder::files() const {
+ FileInfoList ret;
+ ret.reserve(files_.size());
+ for(const auto& item : files_) {
+ ret.push_back(item.second);
+ }
+ return ret;
+}
+
+
+const FilePath& Folder::path() const {
+ auto pathStr = dirPath_.toString();
+ // qDebug() << this << "FOLDER_PATH:" << pathStr.get() << dirPath_.gfile().get();
+ //assert(!g_str_has_prefix(pathStr.get(), "file:"));
+ return dirPath_;
+}
+
+const std::shared_ptr<const FileInfo>& Folder::info() const {
+ return dirInfo_;
+}
+
+#if 0
+void Folder::init(FmFolder* folder) {
+ files = fm_file_info_list_new();
+ G_LOCK(hash);
+ if(G_UNLIKELY(hash_uses == 0)) {
+ hash = g_hash_table_new((GHashFunc)fm_path_hash, (GEqualFunc)fm_path_equal);
+ volume_monitor = g_volume_monitor_get();
+ if(G_LIKELY(volume_monitor)) {
+ g_signal_connect(volume_monitor, "mount-added", G_CALLBACK(on_mount_added), nullptr);
+ g_signal_connect(volume_monitor, "mount-removed", G_CALLBACK(on_mount_removed), nullptr);
+ }
+ }
+ hash_uses++;
+ G_UNLOCK(hash);
+}
+#endif
+
+void Folder::onIdleReload() {
+ /* check if folder still exists */
+ reload();
+ // G_LOCK(query);
+ has_idle_reload_handler = false;
+ // G_UNLOCK(query);
+}
+
+void Folder::queueReload() {
+ // G_LOCK(query);
+ if(!has_idle_reload_handler) {
+ has_idle_reload_handler = true;
+ QTimer::singleShot(0, this, &Folder::onIdleReload);
+ }
+ // G_UNLOCK(query);
+}
+
+void Folder::onFileInfoFinished() {
+ FileInfoJob* job = static_cast<FileInfoJob*>(sender());
+ fileinfoJobs_.erase(std::find(fileinfoJobs_.cbegin(), fileinfoJobs_.cend(), job));
+
+ if(job->isCancelled())
+ return;
+
+ FileInfoList files_to_add;
+ std::vector<FileInfoPair> files_to_update;
+
+ const auto& paths = job->paths();
+ const auto& infos = job->files();
+ auto path_it = paths.cbegin();
+ auto info_it = infos.cbegin();
+ for(; path_it != paths.cend() && info_it != infos.cend(); ++path_it, ++info_it) {
+ const auto& path = *path_it;
+ const auto& info = *info_it;
+
+ if(path == dirPath_) { // got the info for the folder itself.
+ dirInfo_ = info;
+ }
+ else {
+ auto it = files_.find(info->name());
+ if(it != files_.end()) { // the file already exists, update
+ files_to_update.push_back(std::make_pair(it->second, info));
+ }
+ else { // newly added
+ files_to_add.push_back(info);
+ }
+ files_[info->name()] = info;
+ }
+ }
+ if(!files_to_add.empty()) {
+ Q_EMIT filesAdded(files_to_add);
+ }
+ if(!files_to_update.empty()) {
+ Q_EMIT filesChanged(files_to_update);
+ }
+ Q_EMIT contentChanged();
+}
+
+void Folder::processPendingChanges() {
+ has_idle_update_handler = false;
+ // FmFileInfoJob* job = nullptr;
+ std::lock_guard<std::mutex> lock{mutex_};
+
+ // idle_handler = 0;
+ /* if we were asked to block updates let delay it for now */
+ if(stop_emission) {
+ return;
+ }
+
+ FileInfoJob* info_job = nullptr;
+ if(!paths_to_update.empty() || !paths_to_add.empty()) {
+ FilePathList paths;
+ paths.insert(paths.end(), paths_to_add.cbegin(), paths_to_add.cend());
+ paths.insert(paths.end(), paths_to_update.cbegin(), paths_to_update.cend());
+ info_job = new FileInfoJob{paths, dirPath_,
+ hasCutFiles() ? cutFilesHashSet_ : nullptr};
+ paths_to_update.clear();
+ paths_to_add.clear();
+ }
+
+ if(info_job) {
+ fileinfoJobs_.push_back(info_job);
+ info_job->setAutoDelete(true);
+ connect(info_job, &FileInfoJob::finished, this, &Folder::onFileInfoFinished, Qt::BlockingQueuedConnection);
+ info_job->runAsync();
+#if 0
+ pending_jobs = g_slist_prepend(pending_jobs, job);
+ if(!fm_job_run_async(FM_JOB(job))) {
+ pending_jobs = g_slist_remove(pending_jobs, job);
+ g_object_unref(job);
+ g_critical("failed to start folder update job");
+ }
+#endif
+ }
+
+ if(!paths_to_del.empty()) {
+ FileInfoList deleted_files;
+ for(const auto &path: paths_to_del) {
+ auto name = path.baseName();
+ auto it = files_.find(name.get());
+ if(it != files_.end()) {
+ deleted_files.push_back(it->second);
+ files_.erase(it);
+ }
+ }
+ Q_EMIT filesRemoved(deleted_files);
+ Q_EMIT contentChanged();
+ paths_to_del.clear();
+ }
+
+ if(pending_change_notify) {
+ Q_EMIT changed();
+ /* update volume info */
+ queryFilesystemInfo();
+ pending_change_notify = false;
+ }
+
+ if(filesystem_info_pending) {
+ Q_EMIT fileSystemChanged();
+ filesystem_info_pending = false;
+ }
+}
+
+/* should be called only with G_LOCK(lists) on! */
+void Folder::queueUpdate() {
+ // qDebug() << "queue_update:" << !has_idle_handler << paths_to_add.size() << paths_to_update.size() << paths_to_del.size();
+ if(!has_idle_update_handler) {
+ QTimer::singleShot(0, this, &Folder::processPendingChanges);
+ has_idle_update_handler = true;
+ }
+}
+
+
+/* returns true if reference was taken from path */
+bool Folder::eventFileAdded(const FilePath &path) {
+ bool added = true;
+ // G_LOCK(lists);
+ /* make sure that the file is not already queued for addition. */
+ if(std::find(paths_to_add.cbegin(), paths_to_add.cend(), path) == paths_to_add.cend()) {
+ if(files_.find(path.baseName().get()) != files_.end()) { // the file already exists, update instead
+ if(std::find(paths_to_update.cbegin(), paths_to_update.cend(), path) == paths_to_update.cend()) {
+ paths_to_update.push_back(path);
+ }
+ }
+ else { // newly added file
+ paths_to_add.push_back(path);
+ }
+ /* bug #3591771: 'ln -fns . test' leave no file visible in folder.
+ If it is queued for deletion then cancel that operation */
+ paths_to_del.erase(std::remove(paths_to_del.begin(), paths_to_del.end(), path), paths_to_del.cend());
+ }
+ else
+ /* file already queued for adding, don't duplicate */
+ {
+ added = false;
+ }
+ if(added) {
+ queueUpdate();
+ }
+ // G_UNLOCK(lists);
+ return added;
+}
+
+bool Folder::eventFileChanged(const FilePath &path) {
+ bool added;
+ // G_LOCK(lists);
+ /* make sure that the file is not already queued for changes or
+ * it's already queued for addition. */
+ if(std::find(paths_to_update.cbegin(), paths_to_update.cend(), path) == paths_to_update.cend()
+ && std::find(paths_to_add.cbegin(), paths_to_add.cend(), path) == paths_to_add.cend()) {
+ /* Since this function is called only when a file already exists, even if that file
+ isn't included in "files_" yet, it will be soon due to a previous call to queueUpdate().
+ So, here, we should queue it for changes regardless of what "files_" may contain. */
+ paths_to_update.push_back(path);
+ added = true;
+ queueUpdate();
+ }
+ else {
+ added = false;
+ }
+ // G_UNLOCK(lists);
+ return added;
+}
+
+void Folder::eventFileDeleted(const FilePath& path) {
+ // qDebug() << "delete " << path.baseName().get();
+ // G_LOCK(lists);
+ if(files_.find(path.baseName().get()) != files_.cend()) {
+ if(std::find(paths_to_del.cbegin(), paths_to_del.cend(), path) == paths_to_del.cend()) {
+ paths_to_del.push_back(path);
+ }
+ }
+ /* if the file is already queued for addition or update, that operation
+ will be just a waste, therefore cancel it right now */
+ paths_to_add.erase(std::remove(paths_to_add.begin(), paths_to_add.end(), path), paths_to_add.cend());
+ paths_to_update.erase(std::remove(paths_to_update.begin(), paths_to_update.end(), path), paths_to_update.cend());
+ queueUpdate();
+ // G_UNLOCK(lists);
+}
+
+
+void Folder::onDirChanged(GFileMonitorEvent evt) {
+ switch(evt) {
+ case G_FILE_MONITOR_EVENT_PRE_UNMOUNT:
+ /* g_debug("folder is going to be unmounted"); */
+ break;
+ case G_FILE_MONITOR_EVENT_UNMOUNTED:
+ Q_EMIT unmount();
+ /* g_debug("folder is unmounted"); */
+ queueReload();
+ break;
+ case G_FILE_MONITOR_EVENT_DELETED:
+ Q_EMIT removed();
+ /* g_debug("folder is deleted"); */
+ break;
+ case G_FILE_MONITOR_EVENT_CREATED:
+ queueReload();
+ break;
+ case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED:
+ case G_FILE_MONITOR_EVENT_CHANGED: {
+ std::lock_guard<std::mutex> lock{mutex_};
+ pending_change_notify = true;
+ if(std::find(paths_to_update.cbegin(), paths_to_update.cend(), dirPath_) != paths_to_update.cend()) {
+ paths_to_update.push_back(dirPath_);
+ queueUpdate();
+ }
+ /* g_debug("folder is changed"); */
+ break;
+ }
+#if GLIB_CHECK_VERSION(2,24,0)
+ case G_FILE_MONITOR_EVENT_MOVED:
+#endif
+ case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT:
+ ;
+ default:
+ break;
+ }
+}
+
+void Folder::onFileChangeEvents(GFileMonitor* /*monitor*/, GFile* gf, GFile* /*other_file*/, GFileMonitorEvent evt) {
+ /* const char* names[]={
+ "G_FILE_MONITOR_EVENT_CHANGED",
+ "G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT",
+ "G_FILE_MONITOR_EVENT_DELETED",
+ "G_FILE_MONITOR_EVENT_CREATED",
+ "G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED",
+ "G_FILE_MONITOR_EVENT_PRE_UNMOUNT",
+ "G_FILE_MONITOR_EVENT_UNMOUNTED"
+ }; */
+ if(dirPath_ == gf) {
+ onDirChanged(evt);
+ return;
+ }
+ else {
+ std::lock_guard<std::mutex> lock{mutex_};
+ auto path = FilePath{gf, true};
+ /* NOTE: sometimes, for unknown reasons, GFileMonitor gives us the
+ * same event of the same file for multiple times. So we need to
+ * check for duplications ourselves here. */
+ switch(evt) {
+ case G_FILE_MONITOR_EVENT_CREATED:
+ eventFileAdded(path);
+ break;
+ case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED:
+ case G_FILE_MONITOR_EVENT_CHANGED:
+ eventFileChanged(path);
+ break;
+ case G_FILE_MONITOR_EVENT_DELETED:
+ eventFileDeleted(path);
+ break;
+ default:
+ return;
+ }
+ queueUpdate();
+ }
+}
+
+// checks whether there were cut files here
+// and if there were, invalidates this last cut path
+bool Folder::hadCutFilesUnset() {
+ if(lastCutFilesDirPath_ == dirPath_) {
+ lastCutFilesDirPath_ = FilePath();
+ return true;
+ }
+ return false;
+}
+
+bool Folder::hasCutFiles() {
+ return cutFilesHashSet_
+ && !cutFilesHashSet_->empty()
+ && cutFilesDirPath_ == dirPath_;
+}
+
+void Folder::setCutFiles(const std::shared_ptr<const HashSet>& cutFilesHashSet) {
+ if(cutFilesHashSet_ && !cutFilesHashSet_->empty()) {
+ lastCutFilesDirPath_ = cutFilesDirPath_;
+ }
+ cutFilesDirPath_ = dirPath_;
+ cutFilesHashSet_ = cutFilesHashSet;
+}
+
+void Folder::onDirListFinished() {
+ DirListJob* job = static_cast<DirListJob*>(sender());
+ if(job->isCancelled()) { // this is a cancelled job, ignore!
+ if(job == dirlist_job) {
+ dirlist_job = nullptr;
+ }
+ Q_EMIT finishLoading();
+ return;
+ }
+ dirInfo_ = job->dirInfo();
+
+ FileInfoList files_to_add;
+ std::vector<FileInfoPair> files_to_update;
+ const auto& infos = job->files();
+
+ // with "search://", there is no update for infos and all of them should be added
+ if(strcmp(dirPath_.uriScheme().get(), "search") == 0) {
+ files_to_add = infos;
+ for(auto& file: files_to_add) {
+ files_[file->name()] = file;
+ }
+ }
+ else {
+ auto info_it = infos.cbegin();
+ for(; info_it != infos.cend(); ++info_it) {
+ const auto& info = *info_it;
+ auto it = files_.find(info->name());
+ if(it != files_.end()) {
+ files_to_update.push_back(std::make_pair(it->second, info));
+ }
+ else {
+ files_to_add.push_back(info);
+ }
+ files_[info->name()] = info;
+ }
+ }
+
+ if(!files_to_add.empty()) {
+ Q_EMIT filesAdded(files_to_add);
+ }
+ if(!files_to_update.empty()) {
+ Q_EMIT filesChanged(files_to_update);
+ }
+
+#if 0
+ if(dirlist_job->isCancelled() && !wants_incremental) {
+ GList* l;
+ for(l = fm_file_info_list_peek_head_link(job->files); l; l = l->next) {
+ FmFileInfo* inf = (FmFileInfo*)l->data;
+ files = g_slist_prepend(files, inf);
+ fm_file_info_list_push_tail(files, inf);
+ }
+ if(G_LIKELY(files)) {
+ GSList* l;
+
+ G_LOCK(lists);
+ if(defer_content_test && fm_path_is_native(dir_path))
+ /* we got only basic info on content, schedule update it now */
+ for(l = files; l; l = l->next)
+ files_to_update = g_slist_prepend(files_to_update,
+ fm_path_ref(fm_file_info_get_path(l->data)));
+ G_UNLOCK(lists);
+ g_signal_emit(folder, signals[FILES_ADDED], 0, files);
+ g_slist_free(files);
+ }
+
+ if(job->dir_fi) {
+ dir_fi = fm_file_info_ref(job->dir_fi);
+ }
+
+ /* Some new files are created while FmDirListJob is loading the folder. */
+ G_LOCK(lists);
+ if(G_UNLIKELY(files_to_add)) {
+ /* This should be a very rare case. Could this happen? */
+ GSList* l;
+ for(l = files_to_add; l;) {
+ FmPath* path = l->data;
+ GSList* next = l->next;
+ if(_Folder::get_file_by_path(folder, path)) {
+ /* we already have the file. remove it from files_to_add,
+ * and put it in files_to_update instead.
+ * No strdup for name is needed here. We steal
+ * the string from files_to_add.*/
+ files_to_update = g_slist_prepend(files_to_update, path);
+ files_to_add = g_slist_delete_link(files_to_add, l);
+ }
+ l = next;
+ }
+ }
+ G_UNLOCK(lists);
+ }
+ else if(!dir_fi && job->dir_fi)
+ /* we may need dir_fi for incremental folders too */
+ {
+ dir_fi = fm_file_info_ref(job->dir_fi);
+ }
+ g_object_unref(dirlist_job);
+#endif
+
+ dirlist_job = nullptr;
+ Q_EMIT finishLoading();
+}
+
+#if 0
+
+
+void on_dirlist_job_files_found(FmDirListJob* job, GSList* files, gpointer user_data) {
+ FmFolder* folder = FM_FOLDER(user_data);
+ GSList* l;
+ for(l = files; l; l = l->next) {
+ FmFileInfo* file = FM_FILE_INFO(l->data);
+ fm_file_info_list_push_tail(files, file);
+ }
+ if(G_UNLIKELY(!dir_fi && job->dir_fi))
+ /* we may want info while folder is still loading */
+ {
+ dir_fi = fm_file_info_ref(job->dir_fi);
+ }
+ g_signal_emit(folder, signals[FILES_ADDED], 0, files);
+}
+
+ErrorAction on_dirlist_job_error(FmDirListJob* job, GError* err, FmJobErrorSeverity severity, FmFolder* folder) {
+ guint ret;
+ /* it's possible that some signal handlers tries to free the folder
+ * when errors occurs, so let's g_object_ref here. */
+ g_object_ref(folder);
+ g_signal_emit(folder, signals[ERROR], 0, err, (guint)severity, &ret);
+ g_object_unref(folder);
+ return ret;
+}
+
+void free_dirlist_job(FmFolder* folder) {
+ if(wants_incremental) {
+ g_signal_handlers_disconnect_by_func(dirlist_job, on_dirlist_job_files_found, folder);
+ }
+ g_signal_handlers_disconnect_by_func(dirlist_job, on_dirlist_job_finished, folder);
+ g_signal_handlers_disconnect_by_func(dirlist_job, on_dirlist_job_error, folder);
+ fm_job_cancel(FM_JOB(dirlist_job));
+ g_object_unref(dirlist_job);
+ dirlist_job = nullptr;
+}
+
+#endif
+
+
+void Folder::reload() {
+ // cancel in-progress jobs if there are any
+ GError* err = nullptr;
+ // cancel directory monitoring
+ if(dirMonitor_) {
+ g_signal_handlers_disconnect_by_data(dirMonitor_.get(), this);
+ dirMonitor_.reset();
+ }
+
+ /* clear all update-lists now, see SF bug #919 - if update comes before
+ listing job is finished, a duplicate may be created in the folder */
+ if(has_idle_update_handler) {
+ // FIXME: cancel the idle handler
+ paths_to_add.clear();
+ paths_to_update.clear();
+ paths_to_del.clear();
+
+ // cancel any file info job in progress.
+ for(auto job: fileinfoJobs_) {
+ job->cancel();
+ disconnect(job, &FileInfoJob::finished, this, &Folder::onFileInfoFinished);
+ }
+ fileinfoJobs_.clear();
+ }
+
+ /* remove all existing files */
+ if(!files_.empty()) {
+ // FIXME: this is not very efficient :(
+ auto tmp = files();
+ files_.clear();
+ Q_EMIT filesRemoved(tmp);
+ }
+
+ /* Tell the world that we're about to reload the folder.
+ * It might be a good idea for users of the folder to disconnect
+ * from the folder temporarily and reconnect to it again after
+ * the folder complete the loading. This might reduce some
+ * unnecessary signal handling and UI updates. */
+ Q_EMIT startLoading();
+
+ dirInfo_.reset(); // clear dir info
+
+ /* also re-create a new file monitor */
+ // mon = GFileMonitorPtr{fm_monitor_directory(dir_path.gfile().get(), &err), false};
+ // FIXME: should we make this cancellable?
+ dirMonitor_ = GFileMonitorPtr{
+ g_file_monitor_directory(dirPath_.gfile().get(), G_FILE_MONITOR_WATCH_MOUNTS, nullptr, &err),
+ false
+ };
+
+ if(dirMonitor_) {
+ g_signal_connect(dirMonitor_.get(), "changed", G_CALLBACK(_onFileChangeEvents), this);
+ }
+ else {
+ qDebug("file monitor cannot be created: %s", err->message);
+ g_error_free(err);
+ }
+
+ Q_EMIT contentChanged();
+
+ /* run a new dir listing job */
+ // FIXME:
+ // defer_content_test = fm_config->defer_content_test;
+ dirlist_job = new DirListJob(dirPath_, defer_content_test ? DirListJob::FAST : DirListJob::DETAILED,
+ hasCutFiles() ? cutFilesHashSet_ : nullptr);
+ dirlist_job->setAutoDelete(true);
+ connect(dirlist_job, &DirListJob::error, this, &Folder::error, Qt::BlockingQueuedConnection);
+ connect(dirlist_job, &DirListJob::finished, this, &Folder::onDirListFinished, Qt::BlockingQueuedConnection);
+
+#if 0
+ if(wants_incremental) {
+ g_signal_connect(dirlist_job, "files-found", G_CALLBACK(on_dirlist_job_files_found), folder);
+ }
+ fm_dir_list_job_set_incremental(dirlist_job, wants_incremental);
+#endif
+
+ dirlist_job->runAsync();
+
+ /* also reload filesystem info.
+ * FIXME: is this needed? */
+ queryFilesystemInfo();
+}
+
+#if 0
+
+/**
+ * Folder::is_incremental
+ * @folder: folder to test
+ *
+ * Checks if a folder is incrementally loaded.
+ * After an FmFolder object is obtained from calling Folder::from_path(),
+ * if it's not yet loaded, it begins loading the content of the folder
+ * and emits "start-loading" signal. Most of the time, the info of the
+ * files in the folder becomes available only after the folder is fully
+ * loaded. That means, after the "finish-loading" signal is emitted.
+ * Before the loading is finished, Folder::get_files() returns nothing.
+ * You can tell if a folder is still being loaded with Folder::is_loaded().
+ *
+ * However, for some special FmFolder types, such as the ones handling
+ * search:// URIs, we want to access the file infos while the folder is
+ * still being loaded (the search is still ongoing).
+ * The content of the folder grows incrementally and Folder::get_files()
+ * returns files currently being loaded even when the folder is not
+ * fully loaded. This is what we called incremental.
+ * Folder::is_incremental() tells you if the FmFolder has this feature.
+ *
+ * Returns: %true if @folder is incrementally loaded
+ *
+ * Since: 1.0.2
+ */
+bool Folder::is_incremental(FmFolder* folder) {
+ return wants_incremental;
+}
+
+#endif
+
+bool Folder::getFilesystemInfo(uint64_t* total_size, uint64_t* free_size) const {
+ if(has_fs_info) {
+ *total_size = fs_total_size;
+ *free_size = fs_free_size;
+ return true;
+ }
+ return false;
+}
+
+
+void Folder::onFileSystemInfoFinished() {
+ FileSystemInfoJob* job = static_cast<FileSystemInfoJob*>(sender());
+ if(job->isCancelled() || job != fsInfoJob_) { // this is a cancelled job, ignore!
+ fsInfoJob_ = nullptr;
+ has_fs_info = false;
+ return;
+ }
+ has_fs_info = job->isAvailable();
+ fs_total_size = job->size();
+ fs_free_size = job->freeSize();
+ filesystem_info_pending = true;
+ fsInfoJob_ = nullptr;
+ queueUpdate();
+}
+
+
+void Folder::queryFilesystemInfo() {
+ // G_LOCK(query);
+ if(fsInfoJob_)
+ return;
+ fsInfoJob_ = new FileSystemInfoJob{dirPath_};
+ fsInfoJob_->setAutoDelete(true);
+ connect(fsInfoJob_, &FileSystemInfoJob::finished, this, &Folder::onFileSystemInfoFinished, Qt::BlockingQueuedConnection);
+
+ fsInfoJob_->runAsync();
+ // G_UNLOCK(query);
+}
+
+
+#if 0
+/**
+ * Folder::block_updates
+ * @folder: folder to apply
+ *
+ * Blocks emitting signals for changes in folder, i.e. if some file was
+ * added, changed, or removed in folder after this API, no signal will be
+ * sent until next call to Folder::unblock_updates().
+ *
+ * Since: 1.2.0
+ */
+void Folder::block_updates(FmFolder* folder) {
+ /* g_debug("Folder::block_updates %p", folder); */
+ G_LOCK(lists);
+ /* just set the flag */
+ stop_emission = true;
+ G_UNLOCK(lists);
+}
+
+/**
+ * Folder::unblock_updates
+ * @folder: folder to apply
+ *
+ * Unblocks emitting signals for changes in folder. If some changes were
+ * in folder after previous call to Folder::block_updates() then these
+ * changes will be sent after this call.
+ *
+ * Since: 1.2.0
+ */
+void Folder::unblock_updates(FmFolder* folder) {
+ /* g_debug("Folder::unblock_updates %p", folder); */
+ G_LOCK(lists);
+ stop_emission = false;
+ /* query update now */
+ queue_update(folder);
+ G_UNLOCK(lists);
+ /* g_debug("Folder::unblock_updates OK"); */
+}
+
+/**
+ * Folder::make_directory
+ * @folder: folder to apply
+ * @name: display name for new directory
+ * @error: (allow-none) (out): location to save error
+ *
+ * Creates new directory in given @folder.
+ *
+ * Returns: %true in case of success.
+ *
+ * Since: 1.2.0
+ */
+bool Folder::make_directory(FmFolder* folder, const char* name, GError** error) {
+ GFile* dir, *gf;
+ FmPath* path;
+ bool ok;
+
+ dir = fm_path_to_gfile(dir_path);
+ gf = g_file_get_child_for_display_name(dir, name, error);
+ g_object_unref(dir);
+ if(gf == nullptr) {
+ return false;
+ }
+ ok = g_file_make_directory(gf, nullptr, error);
+ if(ok) {
+ path = fm_path_new_for_gfile(gf);
+ if(!_Folder::event_file_added(folder, path)) {
+ fm_path_unref(path);
+ }
+ }
+ g_object_unref(gf);
+ return ok;
+}
+
+void Folder::content_changed(FmFolder* folder) {
+ if(has_fs_info && !fs_info_not_avail) {
+ Folder::query_filesystem_info(folder);
+ }
+}
+
+#endif
+
+/* NOTE:
+ * GFileMonitor has some significant limitations:
+ * 1. Currently it can correctly emit unmounted event for a directory.
+ * 2. After a directory is unmounted, its content changes.
+ * Inotify does not fire events for this so a forced reload is needed.
+ * 3. If a folder is empty, and later a filesystem is mounted to the
+ * folder, its content should reflect the content of the newly mounted
+ * filesystem. However, GFileMonitor and inotify do not emit events
+ * for this case. A forced reload might be needed for this case as well.
+ * 4. Some limitations come from Linux/inotify. If FAM/gamin is used,
+ * the condition may be different. More testing is needed.
+ */
+void Folder::onMountAdded(const Mount& mnt) {
+ /* If a filesystem is mounted over an existing folder,
+ * we need to refresh the content of the folder to reflect
+ * the changes. Besides, we need to create a new GFileMonitor
+ * for the newly-mounted filesystem as the inode already changed.
+ * GFileMonitor cannot detect this kind of changes caused by mounting.
+ * So let's do it ourselves. */
+ auto mountRoot = mnt.root();
+ if(mountRoot.isPrefixOf(dirPath_)) {
+ queueReload();
+ }
+ /* g_debug("FmFolder::mount_added"); */
+}
+
+void Folder::onMountRemoved(const Mount& mnt) {
+ /* g_debug("FmFolder::mount_removed"); */
+
+ /* NOTE: gvfs does not emit unmount signals for remote folders since
+ * GFileMonitor does not support remote filesystems at all.
+ * So here is the side effect, no unmount notifications.
+ * We need to generate the signal ourselves. */
+ if(!dirMonitor_) {
+ // this is only needed when we don't have a GFileMonitor
+ auto mountRoot = mnt.root();
+ if(mountRoot.isPrefixOf(dirPath_)) {
+ // if the current folder is under the unmounted path, generate the event ourselves
+ onDirChanged(G_FILE_MONITOR_EVENT_UNMOUNTED);
+ }
+ }
+}
+
+} // namespace Fm
--- /dev/null
+/*
+ * Copyright (C) 2016 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef __LIBFM2_QT_FM_FOLDER_H__
+#define __LIBFM2_QT_FM_FOLDER_H__
+
+#include <gio/gio.h>
+#include <cstdint>
+#include <memory>
+#include <vector>
+#include <unordered_map>
+#include <mutex>
+#include <functional>
+
+#include <QObject>
+#include <QtGlobal>
+#include "../libfmqtglobals.h"
+
+#include "gioptrs.h"
+#include "fileinfo.h"
+#include "job.h"
+#include "volumemanager.h"
+
+namespace Fm {
+
+class DirListJob;
+class FileSystemInfoJob;
+class FileInfoJob;
+
+
+class LIBFM_QT_API Folder: public QObject {
+ Q_OBJECT
+public:
+
+ explicit Folder();
+
+ explicit Folder(const FilePath& path);
+
+ virtual ~Folder();
+
+ static std::shared_ptr<Folder> fromPath(const FilePath& path);
+
+ bool makeDirectory(const char* name, GError** error);
+
+ void queryFilesystemInfo();
+
+ bool getFilesystemInfo(uint64_t* total_size, uint64_t* free_size) const;
+
+ void reload();
+
+ bool isIncremental() const;
+
+ bool isValid() const;
+
+ bool isLoaded() const;
+
+ std::shared_ptr<const FileInfo> fileByName(const char* name) const;
+
+ bool isEmpty() const;
+
+ FileInfoList files() const;
+
+ const FilePath& path() const;
+
+ const std::shared_ptr<const FileInfo> &info() const;
+
+ bool hadCutFilesUnset();
+
+ bool hasCutFiles();
+
+ void setCutFiles(const std::shared_ptr<const HashSet>& cutFilesHashSet);
+
+ void forEachFile(std::function<void (const std::shared_ptr<const FileInfo>&)> func) const {
+ std::lock_guard<std::mutex> lock{mutex_};
+ for(auto it = files_.begin(); it != files_.end(); ++it) {
+ func(it->second);
+ }
+ }
+
+Q_SIGNALS:
+ void startLoading();
+
+ void finishLoading();
+
+ void filesAdded(FileInfoList& addedFiles);
+
+ void filesChanged(std::vector<FileInfoPair>& changePairs);
+
+ void filesRemoved(FileInfoList& removedFiles);
+
+ void removed();
+
+ void changed();
+
+ void unmount();
+
+ void contentChanged();
+
+ void fileSystemChanged();
+
+ // FIXME: this API design is bad. We leave this here to be compatible with the old libfm C API.
+ // It might be better to remember the error state while loading the folder, and let the user of the
+ // API handle the error on finish.
+ void error(const GErrorPtr& err, Job::ErrorSeverity severity, Job::ErrorAction& response);
+
+private:
+
+ static void _onFileChangeEvents(GFileMonitor* monitor, GFile* file, GFile* other_file, GFileMonitorEvent event_type, Folder* _this) {
+ _this->onFileChangeEvents(monitor, file, other_file, event_type);
+ }
+ void onFileChangeEvents(GFileMonitor* monitor, GFile* file, GFile* other_file, GFileMonitorEvent event_type);
+ void onDirChanged(GFileMonitorEvent event_type);
+
+ void queueUpdate();
+ void queueReload();
+
+ bool eventFileAdded(const FilePath &path);
+ bool eventFileChanged(const FilePath &path);
+ void eventFileDeleted(const FilePath &path);
+
+private Q_SLOTS:
+
+ void processPendingChanges();
+
+ void onDirListFinished();
+
+ void onFileSystemInfoFinished();
+
+ void onFileInfoFinished();
+
+ void onIdleReload();
+
+ void onMountAdded(const Mount& mnt);
+
+ void onMountRemoved(const Mount& mnt);
+
+private:
+ FilePath dirPath_;
+ GFileMonitorPtr dirMonitor_;
+
+ std::shared_ptr<const FileInfo> dirInfo_;
+ DirListJob* dirlist_job;
+ std::vector<FileInfoJob*> fileinfoJobs_;
+ FileSystemInfoJob* fsInfoJob_;
+
+ std::shared_ptr<VolumeManager> volumeManager_;
+
+ /* for file monitor */
+ bool has_idle_reload_handler;
+ bool has_idle_update_handler;
+ std::vector<FilePath> paths_to_add;
+ std::vector<FilePath> paths_to_update;
+ std::vector<FilePath> paths_to_del;
+ // GSList* pending_jobs;
+ bool pending_change_notify;
+ bool filesystem_info_pending;
+
+ bool wants_incremental;
+ bool stop_emission; /* don't set it 1 bit to not lock other bits */
+
+ std::unordered_map<const std::string, std::shared_ptr<const FileInfo>, std::hash<std::string>> files_;
+
+ /* filesystem info - set in query thread, read in main */
+ uint64_t fs_total_size;
+ uint64_t fs_free_size;
+ GCancellablePtr fs_size_cancellable;
+
+ bool has_fs_info : 1;
+ bool defer_content_test : 1;
+
+ static std::unordered_map<FilePath, std::weak_ptr<Folder>, FilePathHash> cache_;
+ static FilePath cutFilesDirPath_;
+ static FilePath lastCutFilesDirPath_;
+ static std::shared_ptr<const HashSet> cutFilesHashSet_;
+ static std::mutex mutex_;
+};
+
+}
+
+#endif // __LIBFM_QT_FM2_FOLDER_H__
--- /dev/null
+#ifndef GIOPTRS_H
+#define GIOPTRS_H
+// define smart pointers for GIO data types
+
+#include <glib.h>
+#include <gio/gio.h>
+#include "gobjectptr.h"
+#include "cstrptr.h"
+
+namespace Fm {
+
+typedef GObjectPtr<GFile> GFilePtr;
+typedef GObjectPtr<GFileInfo> GFileInfoPtr;
+typedef GObjectPtr<GFileMonitor> GFileMonitorPtr;
+typedef GObjectPtr<GCancellable> GCancellablePtr;
+typedef GObjectPtr<GFileEnumerator> GFileEnumeratorPtr;
+
+typedef GObjectPtr<GInputStream> GInputStreamPtr;
+typedef GObjectPtr<GFileInputStream> GFileInputStreamPtr;
+typedef GObjectPtr<GOutputStream> GOutputStreamPtr;
+typedef GObjectPtr<GFileOutputStream> GFileOutputStreamPtr;
+
+typedef GObjectPtr<GIcon> GIconPtr;
+
+typedef GObjectPtr<GVolumeMonitor> GVolumeMonitorPtr;
+typedef GObjectPtr<GVolume> GVolumePtr;
+typedef GObjectPtr<GMount> GMountPtr;
+
+typedef GObjectPtr<GAppInfo> GAppInfoPtr;
+
+
+class GErrorPtr {
+public:
+ GErrorPtr(): err_{nullptr} {
+ }
+
+ GErrorPtr(GError*&& err) noexcept: err_{err} {
+ err = nullptr;
+ }
+
+ GErrorPtr(const GErrorPtr& other) = delete;
+
+ GErrorPtr(GErrorPtr&& other) noexcept: err_{other.err_} {
+ other.err_ = nullptr;
+ }
+
+ GErrorPtr(std::uint32_t domain, unsigned int code, const char* msg):
+ GErrorPtr{g_error_new_literal(domain, code, msg)} {
+ }
+
+ GErrorPtr(std::uint32_t domain, unsigned int code, const QString& msg):
+ GErrorPtr{domain, code, msg.toUtf8().constData()} {
+ }
+
+ ~GErrorPtr() {
+ reset();
+ }
+
+ std::uint32_t domain() const {
+ if(err_ != nullptr) {
+ return err_->domain;
+ }
+ return 0;
+ }
+
+ unsigned int code() const {
+ if(err_ != nullptr) {
+ return err_->code;
+ }
+ return 0;
+ }
+
+ QString message() const {
+ if(err_ != nullptr) {
+ return err_->message;
+ }
+ return QString();
+ }
+
+ void reset() {
+ if(err_) {
+ g_error_free(err_);
+ }
+ err_ = nullptr;
+ }
+
+ GError* get() const {
+ return err_;
+ }
+
+ GErrorPtr& operator = (const GErrorPtr& other) = delete;
+
+ GErrorPtr& operator = (GErrorPtr&& other) noexcept {
+ reset();
+ err_ = other.err_;
+ other.err_ = nullptr;
+ return *this;
+ }
+
+ GErrorPtr& operator = (GError*&& err) {
+ reset();
+ err_ = err;
+ err_ = nullptr;
+ return *this;
+ }
+
+ GError** operator&() {
+ return &err_;
+ }
+
+ GError* operator->() {
+ return err_;
+ }
+
+ bool operator == (const GErrorPtr& other) const {
+ return err_ == other.err_;
+ }
+
+ bool operator == (GError* err) const {
+ return err_ == err;
+ }
+
+ bool operator != (std::nullptr_t) const {
+ return err_ != nullptr;
+ }
+
+ operator bool() const {
+ return err_ != nullptr;
+ }
+
+private:
+ GError* err_;
+};
+
+} //namespace Fm
+
+#endif // GIOPTRS_H
--- /dev/null
+#ifndef FM2_GOBJECTPTR_H
+#define FM2_GOBJECTPTR_H
+
+#include "../libfmqtglobals.h"
+#include <glib.h>
+#include <glib-object.h>
+#include <cstddef>
+#include <QDebug>
+
+namespace Fm {
+
+template <typename T>
+class LIBFM_QT_API GObjectPtr {
+public:
+
+ explicit GObjectPtr(): gobj_{nullptr} {
+ }
+
+ explicit GObjectPtr(T* gobj, bool add_ref = true): gobj_{gobj} {
+ if(gobj_ != nullptr && add_ref)
+ g_object_ref(gobj_);
+ }
+
+ GObjectPtr(const GObjectPtr& other): gobj_{other.gobj_ ? reinterpret_cast<T*>(g_object_ref(other.gobj_)) : nullptr} {
+ }
+
+ GObjectPtr(GObjectPtr&& other) noexcept: gobj_{other.release()} {
+ }
+
+ ~GObjectPtr() {
+ if(gobj_ != nullptr)
+ g_object_unref(gobj_);
+ }
+
+ T* get() const {
+ return gobj_;
+ }
+
+ T* release() {
+ T* tmp = gobj_;
+ gobj_ = nullptr;
+ return tmp;
+ }
+
+ void reset() {
+ if(gobj_ != nullptr)
+ g_object_unref(gobj_);
+ gobj_ = nullptr;
+ }
+
+ GObjectPtr& operator = (const GObjectPtr& other) {
+ if (*this == other)
+ return *this;
+
+ if(gobj_ != nullptr)
+ g_object_unref(gobj_);
+ gobj_ = other.gobj_ ? reinterpret_cast<T*>(g_object_ref(other.gobj_)) : nullptr;
+ return *this;
+ }
+
+ GObjectPtr& operator = (GObjectPtr&& other) noexcept {
+ if (this == &other)
+ return *this;
+
+ if(gobj_ != nullptr)
+ g_object_unref(gobj_);
+ gobj_ = other.release();
+ return *this;
+ }
+
+ GObjectPtr& operator = (T* gobj) {
+ if (*this == gobj)
+ return *this;
+
+ if(gobj_ != nullptr)
+ g_object_unref(gobj_);
+ gobj_ = gobj ? reinterpret_cast<T*>(g_object_ref(gobj_)) : nullptr;
+ return *this;
+ }
+
+ bool operator == (const GObjectPtr& other) const {
+ return gobj_ == other.gobj_;
+ }
+
+ bool operator == (T* gobj) const {
+ return gobj_ == gobj;
+ }
+
+ bool operator != (std::nullptr_t) const {
+ return gobj_ != nullptr;
+ }
+
+ operator bool() const {
+ return gobj_ != nullptr;
+ }
+
+private:
+ mutable T* gobj_;
+};
+
+
+} // namespace Fm
+
+#endif // FM2_GOBJECTPTR_H
--- /dev/null
+#include "iconinfo.h"
+#include "iconinfo_p.h"
+
+namespace Fm {
+
+std::unordered_map<GIcon*, std::shared_ptr<IconInfo>, IconInfo::GIconHash, IconInfo::GIconEqual> IconInfo::cache_;
+std::mutex IconInfo::mutex_;
+QIcon IconInfo::fallbackQicon_;
+
+static const char* fallbackIconNames[] = {
+ "unknown",
+ "application-octet-stream",
+ "application-x-generic",
+ "text-x-generic",
+ nullptr
+};
+
+IconInfo::IconInfo(const char* name):
+ gicon_{g_themed_icon_new(name), false} {
+}
+
+IconInfo::IconInfo(const GIconPtr gicon):
+ gicon_{std::move(gicon)} {
+}
+
+IconInfo::~IconInfo() {
+}
+
+// static
+std::shared_ptr<const IconInfo> IconInfo::fromName(const char* name) {
+ GObjectPtr<GIcon> gicon{g_themed_icon_new(name), false};
+ return fromGIcon(gicon);
+}
+
+// static
+std::shared_ptr<const IconInfo> IconInfo::fromGIcon(GIconPtr gicon) {
+ if(Q_LIKELY(gicon)) {
+ std::lock_guard<std::mutex> lock{mutex_};
+ auto it = cache_.find(gicon.get());
+ if(it != cache_.end()) {
+ return it->second;
+ }
+ // not found in the cache, create a new entry for it.
+ auto icon = std::make_shared<IconInfo>(std::move(gicon));
+ cache_.insert(std::make_pair(icon->gicon_.get(), icon));
+ return icon;
+ }
+ return std::shared_ptr<const IconInfo>{};
+}
+
+void IconInfo::updateQIcons() {
+ std::lock_guard<std::mutex> lock{mutex_};
+ fallbackQicon_ = QIcon();
+ for(auto& elem: cache_) {
+ auto& info = elem.second;
+ info->internalQicon_ = QIcon();
+ }
+}
+
+QIcon IconInfo::qicon(const bool& transparent) const {
+ if(Q_LIKELY(!transparent)) {
+ if(Q_UNLIKELY(qicon_.isNull() && gicon_)) {
+ if(!G_IS_FILE_ICON(gicon_.get())) {
+ qicon_ = QIcon(new IconEngine{shared_from_this()});
+ }
+ else {
+ qicon_ = internalQicon_;
+ }
+ }
+ }
+ else { // transparent == true
+ if(Q_UNLIKELY(qiconTransparent_.isNull() && gicon_)) {
+ if(!G_IS_FILE_ICON(gicon_.get())) {
+ qiconTransparent_ = QIcon(new IconEngine{shared_from_this(), transparent});
+ }
+ else {
+ qiconTransparent_ = internalQicon_;
+ }
+ }
+ }
+ return !transparent ? qicon_ : qiconTransparent_;
+}
+
+QIcon IconInfo::qiconFromNames(const char* const* names) {
+ const gchar* const* name;
+ // qDebug("names: %p", names);
+ for(name = names; *name; ++name) {
+ // qDebug("icon name=%s", *name);
+ QIcon qicon = QIcon::fromTheme(*name);
+ if(!qicon.isNull()) {
+ return qicon;
+ }
+ }
+ return QIcon();
+}
+
+std::forward_list<std::shared_ptr<const IconInfo>> IconInfo::emblems() const {
+ std::forward_list<std::shared_ptr<const IconInfo>> result;
+ if(hasEmblems()) {
+ const GList* emblems_glist = g_emblemed_icon_get_emblems(G_EMBLEMED_ICON(gicon_.get()));
+ for(auto l = emblems_glist; l; l = l->next) {
+ auto gemblem = G_EMBLEM(l->data);
+ GIconPtr gemblem_icon{g_emblem_get_icon(gemblem), true};
+ result.emplace_front(fromGIcon(gemblem_icon));
+ }
+ result.reverse();
+ }
+ return result;
+}
+
+QIcon IconInfo::internalQicon() const {
+ if(Q_UNLIKELY(internalQicon_.isNull())) {
+ GIcon* gicon = gicon_.get();
+ if(G_IS_EMBLEMED_ICON(gicon_.get())) {
+ gicon = g_emblemed_icon_get_icon(G_EMBLEMED_ICON(gicon));
+ }
+ if(G_IS_THEMED_ICON(gicon)) {
+ const gchar* const* names = g_themed_icon_get_names(G_THEMED_ICON(gicon));
+ internalQicon_ = qiconFromNames(names);
+ }
+ else if(G_IS_FILE_ICON(gicon)) {
+ GFile* file = g_file_icon_get_file(G_FILE_ICON(gicon));
+ CStrPtr fpath{g_file_get_path(file)};
+ internalQicon_ = QIcon(fpath.get());
+ }
+
+ // fallback to default icon
+ if(Q_UNLIKELY(internalQicon_.isNull())) {
+ if(Q_UNLIKELY(fallbackQicon_.isNull())) {
+ fallbackQicon_ = qiconFromNames(fallbackIconNames);
+ }
+ internalQicon_ = fallbackQicon_;
+ }
+ }
+ return internalQicon_;
+}
+
+} // namespace Fm
--- /dev/null
+/*
+ * fm-icon.h
+ *
+ * Copyright 2009 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ * Copyright 2013 Andriy Grytsenko (LStranger) <andrej@rep.kiev.ua>
+ *
+ * This file is a part of the Libfm library.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+
+#ifndef __FM2_ICON_INFO_H__
+#define __FM2_ICON_INFO_H__
+
+#include "../libfmqtglobals.h"
+#include <gio/gio.h>
+#include "gioptrs.h"
+#include <memory>
+#include <mutex>
+#include <unordered_map>
+#include <forward_list>
+#include <QIcon>
+
+
+namespace Fm {
+
+class LIBFM_QT_API IconInfo: public std::enable_shared_from_this<IconInfo> {
+public:
+ friend class IconEngine;
+
+ explicit IconInfo() {}
+
+ explicit IconInfo(const char* name);
+
+ explicit IconInfo(const GIconPtr gicon);
+
+ ~IconInfo();
+
+ static std::shared_ptr<const IconInfo> fromName(const char* name);
+
+ static std::shared_ptr<const IconInfo> fromGIcon(GIconPtr gicon);
+
+ static std::shared_ptr<const IconInfo> fromGIcon(GIcon* gicon) {
+ return fromGIcon(GIconPtr{gicon, true});
+ }
+
+ static void updateQIcons();
+
+ GIconPtr gicon() const {
+ return gicon_;
+ }
+
+ QIcon qicon(const bool& transparent = false) const;
+
+ bool hasEmblems() const {
+ return G_IS_EMBLEMED_ICON(gicon_.get());
+ }
+
+ std::forward_list<std::shared_ptr<const IconInfo>> emblems() const;
+
+ bool isValid() const {
+ return gicon_ != nullptr;
+ }
+
+private:
+
+ static QIcon qiconFromNames(const char* const* names);
+
+ // actual QIcon loaded by QIcon::fromTheme
+ QIcon internalQicon() const;
+
+ struct GIconHash {
+ std::size_t operator()(GIcon* gicon) const {
+ return g_icon_hash(gicon);
+ }
+ };
+
+ struct GIconEqual {
+ bool operator()(GIcon* gicon1, GIcon* gicon2) const {
+ return g_icon_equal(gicon1, gicon2);
+ }
+ };
+
+private:
+ GIconPtr gicon_;
+ mutable QIcon qicon_;
+ mutable QIcon qiconTransparent_;
+ mutable QIcon internalQicon_;
+
+ static std::unordered_map<GIcon*, std::shared_ptr<IconInfo>, GIconHash, GIconEqual> cache_;
+ static std::mutex mutex_;
+ static QIcon fallbackQicon_;
+};
+
+} // namespace Fm
+
+Q_DECLARE_METATYPE(std::shared_ptr<const Fm::IconInfo>)
+
+#endif /* __FM2_ICON_INFO_H__ */
--- /dev/null
+/*
+ * Copyright (C) 2016 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef FM_ICONENGINE_H
+#define FM_ICONENGINE_H
+
+#include <QIconEngine>
+#include <QPainter>
+#include "../libfmqtglobals.h"
+#include "iconinfo.h"
+#include <gio/gio.h>
+
+namespace Fm {
+
+class IconEngine: public QIconEngine {
+public:
+
+ IconEngine(std::shared_ptr<const Fm::IconInfo> info, const bool& transparent = false);
+
+ ~IconEngine();
+
+ virtual QSize actualSize(const QSize& size, QIcon::Mode mode, QIcon::State state) override;
+
+ // not supported
+ virtual void addFile(const QString& /*fileName*/, const QSize& /*size*/, QIcon::Mode /*mode*/, QIcon::State /*state*/) override {}
+
+ // not supported
+ virtual void addPixmap(const QPixmap& /*pixmap*/, QIcon::Mode /*mode*/, QIcon::State /*state*/) override {}
+
+ virtual QIconEngine* clone() const override;
+
+ virtual QString key() const override;
+
+ virtual void paint(QPainter* painter, const QRect& rect, QIcon::Mode mode, QIcon::State state) override;
+
+ virtual QPixmap pixmap(const QSize& size, QIcon::Mode mode, QIcon::State state) override;
+
+ virtual void virtual_hook(int id, void* data) override;
+
+private:
+ std::weak_ptr<const Fm::IconInfo> info_;
+ bool transparent_;
+};
+
+IconEngine::IconEngine(std::shared_ptr<const IconInfo> info, const bool& transparent):
+ info_{info}, transparent_{transparent} {
+}
+
+IconEngine::~IconEngine() {
+}
+
+QSize IconEngine::actualSize(const QSize& size, QIcon::Mode mode, QIcon::State state) {
+ auto info = info_.lock();
+ return info ? info->internalQicon().actualSize(size, mode, state) : QSize{};
+}
+
+QIconEngine* IconEngine::clone() const {
+ IconEngine* engine = new IconEngine(info_.lock());
+ return engine;
+}
+
+QString IconEngine::key() const {
+ return QStringLiteral("Fm::IconEngine");
+}
+
+void IconEngine::paint(QPainter* painter, const QRect& rect, QIcon::Mode mode, QIcon::State state) {
+ auto info = info_.lock();
+ if(info) {
+ if(transparent_) {
+ painter->save();
+ painter->setOpacity(0.45);
+ }
+ info->internalQicon().paint(painter, rect, Qt::AlignCenter, mode, state);
+ if(transparent_) {
+ painter->restore();
+ }
+ }
+}
+
+QPixmap IconEngine::pixmap(const QSize& size, QIcon::Mode mode, QIcon::State state) {
+ auto info = info_.lock();
+ return info ? info->internalQicon().pixmap(size, mode, state) : QPixmap{};
+}
+
+void IconEngine::virtual_hook(int id, void* data) {
+ auto info = info_.lock();
+ switch(id) {
+ case QIconEngine::AvailableSizesHook: {
+ auto* args = reinterpret_cast<QIconEngine::AvailableSizesArgument*>(data);
+ args->sizes = info ? info->internalQicon().availableSizes(args->mode, args->state) : QList<QSize>{};
+ break;
+ }
+ case QIconEngine::IconNameHook: {
+ QString* result = reinterpret_cast<QString*>(data);
+ *result = info ? info->internalQicon().name() : QString{};
+ break;
+ }
+#if QT_VERSION >= QT_VERSION_CHECK(5, 7, 0)
+ case QIconEngine::IsNullHook: {
+ bool* result = reinterpret_cast<bool*>(data);
+ *result = info ? info->internalQicon().isNull() : true;
+ break;
+ }
+#endif
+ }
+}
+
+} // namespace Fm
+
+#endif // FM_ICONENGINE_H
--- /dev/null
+#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
--- /dev/null
+/*
+ * Copyright (C) 2016 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef __LIBFM_QT_FM_JOB_H__
+#define __LIBFM_QT_FM_JOB_H__
+
+#include <libfm/fm.h>
+#include <QObject>
+#include <QtGlobal>
+#include <QThread>
+#include <QRunnable>
+#include <memory>
+#include <gio/gio.h>
+#include "gobjectptr.h"
+#include "gioptrs.h"
+#include "../libfmqtglobals.h"
+
+
+namespace Fm {
+
+/*
+ * Fm::Job can be used in several different modes.
+ * 1. run with QThreadPool::start()
+ * 2. call runAsync(), which will create a new QThread and move the object to the thread.
+ * 3. create a new QThread, and connect the started() signal to the slot Job::run()
+ * 4. Directly call Job::run(), which executes synchrounously as a normal blocking call
+*/
+
+class LIBFM_QT_API Job: public QObject, public QRunnable {
+ Q_OBJECT
+public:
+
+ enum class ErrorAction{
+ CONTINUE,
+ RETRY,
+ ABORT
+ };
+
+ enum class ErrorSeverity {
+ UNKNOWN,
+ WARNING,
+ MILD,
+ MODERATE,
+ SEVERE,
+ CRITICAL
+ };
+
+ explicit Job();
+
+ virtual ~Job();
+
+ bool isCancelled() const {
+ return g_cancellable_is_cancelled(cancellable_.get());
+ }
+
+ void runAsync(QThread::Priority priority = QThread::InheritPriority);
+
+ bool pause();
+
+ void resume();
+
+ const GCancellablePtr& cancellable() const {
+ return cancellable_;
+ }
+
+Q_SIGNALS:
+ void cancelled();
+
+ void finished();
+
+ // this signal should be connected with Qt::BlockingQueuedConnection
+ void error(const GErrorPtr& err, ErrorSeverity severity, ErrorAction& response);
+
+public Q_SLOTS:
+
+ void cancel();
+
+ void run() override;
+
+protected:
+ ErrorAction emitError(const GErrorPtr& err, ErrorSeverity severity = ErrorSeverity::MODERATE);
+
+ // all derived job subclasses should do their work in this method.
+ virtual void exec() = 0;
+
+private:
+ static void _onCancellableCancelled(GCancellable* cancellable, Job* _this) {
+ _this->onCancellableCancelled(cancellable);
+ }
+
+ void onCancellableCancelled(GCancellable* /*cancellable*/) {
+ Q_EMIT cancelled();
+ }
+
+private:
+ bool paused_;
+ GCancellablePtr cancellable_;
+ gulong cancellableHandler_;
+};
+
+
+}
+
+#endif // __LIBFM_QT_FM_JOB_H__
--- /dev/null
+#ifndef JOB_P_H
+#define JOB_P_H
+
+#include <QThread>
+#include "job.h"
+
+namespace Fm {
+
+class JobThread: public QThread {
+ Q_OBJECT
+public:
+ JobThread(Job* job): job_{job} {
+ }
+
+protected:
+
+ void run() override {
+ job_->run();
+ }
+
+ Job* job_;
+};
+
+} // namespace Fm
+
+#endif // JOB_P_H
--- /dev/null
+#include "mimetype.h"
+#include <cstring>
+
+#include <glib.h>
+#include <gio/gio.h>
+
+using namespace std;
+
+namespace Fm {
+
+std::unordered_map<const char*, std::shared_ptr<const MimeType>, CStrHash, CStrEqual> MimeType::cache_;
+std::mutex MimeType::mutex_;
+
+std::shared_ptr<const MimeType> MimeType::inodeDirectory_; // inode/directory
+std::shared_ptr<const MimeType> MimeType::inodeShortcut_; // inode/x-shortcut
+std::shared_ptr<const MimeType> MimeType::inodeMountPoint_; // inode/mount-point
+std::shared_ptr<const MimeType> MimeType::desktopEntry_; // application/x-desktop
+
+
+MimeType::MimeType(const char* typeName):
+ name_{g_strdup(typeName)},
+ desc_{nullptr} {
+
+ GObjectPtr<GIcon> gicon{g_content_type_get_icon(typeName), false};
+ if(strcmp(typeName, "inode/directory") == 0)
+ g_themed_icon_prepend_name(G_THEMED_ICON(gicon.get()), "folder");
+ else if(g_content_type_can_be_executable(typeName))
+ g_themed_icon_append_name(G_THEMED_ICON(gicon.get()), "application-x-executable");
+
+ icon_ = IconInfo::fromGIcon(gicon);
+}
+
+MimeType::~MimeType () {
+}
+
+//static
+std::shared_ptr<const MimeType> MimeType::fromName(const char* typeName) {
+ std::shared_ptr<const MimeType> ret;
+ std::lock_guard<std::mutex> lock(mutex_);
+ auto it = cache_.find(typeName);
+ if(it == cache_.end()) {
+ ret = std::make_shared<MimeType>(typeName);
+ cache_.insert(std::make_pair(ret->name_.get(), ret));
+ }
+ else {
+ ret = it->second;
+ }
+ return ret;
+}
+
+// static
+std::shared_ptr<const MimeType> MimeType::guessFromFileName(const char* fileName) {
+ gboolean uncertain;
+ /* let skip scheme and host from non-native names */
+ auto uri_scheme = g_strstr_len(fileName, -1, "://");
+ if(uri_scheme)
+ fileName = strchr(uri_scheme + 3, '/');
+ if(fileName == nullptr)
+ fileName = "unknown";
+ auto type = CStrPtr{g_content_type_guess(fileName, nullptr, 0, &uncertain)};
+ return fromName(type.get());
+}
+
+} // namespace Fm
--- /dev/null
+/*
+ * fm-mime-type.h
+ *
+ * Copyright 2009 - 2012 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This file is a part of the Libfm library.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef _FM2_MIME_TYPE_H_
+#define _FM2_MIME_TYPE_H_
+
+#include "../libfmqtglobals.h"
+#include <glib.h>
+#include <gio/gio.h>
+
+#include <memory>
+#include <vector>
+#include <string>
+#include <mutex>
+#include <cstring>
+#include <forward_list>
+#include <functional>
+
+#include "cstrptr.h"
+#include "gobjectptr.h"
+#include "iconinfo.h"
+#include "thumbnailer.h"
+
+namespace Fm {
+
+class LIBFM_QT_API MimeType {
+public:
+ friend class Thumbnailer;
+
+ explicit MimeType(const char* typeName);
+
+ MimeType() = delete;
+
+ ~MimeType();
+
+ std::shared_ptr<const Thumbnailer> firstThumbnailer() const {
+ std::lock_guard<std::mutex> lock{mutex_};
+ return thumbnailers_.empty() ? nullptr : thumbnailers_.front();
+ }
+
+ void forEachThumbnailer(std::function<bool(const std::shared_ptr<const Thumbnailer>&)> func) const {
+ std::lock_guard<std::mutex> lock{mutex_};
+ for(auto& thumbnailer: thumbnailers_) {
+ if(func(thumbnailer)) {
+ break;
+ }
+ }
+ }
+
+ const std::shared_ptr<const IconInfo>& icon() const {
+ return icon_;
+ }
+
+ const char* name() const {
+ return name_.get();
+ }
+
+ const char* desc() const {
+ if(!desc_) {
+ desc_ = CStrPtr{g_content_type_get_description(name_.get())};
+ }
+ return desc_.get();
+ }
+
+ static std::shared_ptr<const MimeType> fromName(const char* typeName);
+
+ static std::shared_ptr<const MimeType> guessFromFileName(const char* fileName);
+
+ bool isUnknownType() const {
+ return g_content_type_is_unknown(name_.get());
+ }
+
+ bool isDesktopEntry() const {
+ return this == desktopEntry().get();
+ }
+
+ bool isText() const {
+ return g_content_type_is_a(name_.get(), "text/plain");
+ }
+
+ bool isImage() const {
+ return !std::strncmp("image/", name_.get(), 6);
+ }
+
+ bool isMountable() const {
+ return this == inodeMountPoint().get();
+ }
+
+ bool isShortcut() const {
+ return this == inodeShortcut().get();
+ }
+
+ bool isDir() const {
+ return this == inodeDirectory().get();
+ }
+
+ bool canBeExecutable() const {
+ return g_content_type_can_be_executable(name_.get());
+ }
+
+ static std::shared_ptr<const MimeType> inodeDirectory() { // inode/directory
+ if(!inodeDirectory_)
+ inodeDirectory_ = fromName("inode/directory");
+ return inodeDirectory_;
+ }
+
+ static std::shared_ptr<const MimeType> inodeShortcut() { // inode/x-shortcut
+ if(!inodeShortcut_)
+ inodeShortcut_ = fromName("inode/x-shortcut");
+ return inodeShortcut_;
+ }
+
+ static std::shared_ptr<const MimeType> inodeMountPoint() { // inode/mount-point
+ if(!inodeMountPoint_)
+ inodeMountPoint_ = fromName("inode/mount-point");
+ return inodeMountPoint_;
+ }
+
+ static std::shared_ptr<const MimeType> desktopEntry() { // application/x-desktop
+ if(!desktopEntry_)
+ desktopEntry_ = fromName("application/x-desktop");
+ return desktopEntry_;
+ }
+
+private:
+ void removeThumbnailer(std::shared_ptr<const Thumbnailer>& thumbnailer) {
+ std::lock_guard<std::mutex> lock{mutex_};
+ thumbnailers_.remove(thumbnailer);
+ }
+
+ void addThumbnailer(std::shared_ptr<const Thumbnailer> thumbnailer) {
+ std::lock_guard<std::mutex> lock{mutex_};
+ thumbnailers_.push_front(std::move(thumbnailer));
+ }
+
+private:
+ std::shared_ptr<const IconInfo> icon_;
+ CStrPtr name_;
+ mutable CStrPtr desc_;
+ std::forward_list<std::shared_ptr<const Thumbnailer>> thumbnailers_;
+ static std::unordered_map<const char*, std::shared_ptr<const MimeType>, CStrHash, CStrEqual> cache_;
+ static std::mutex mutex_;
+
+ static std::shared_ptr<const MimeType> inodeDirectory_; // inode/directory
+ static std::shared_ptr<const MimeType> inodeShortcut_; // inode/x-shortcut
+ static std::shared_ptr<const MimeType> inodeMountPoint_; // inode/mount-point
+ static std::shared_ptr<const MimeType> desktopEntry_; // application/x-desktop
+};
+
+
+} // namespace Fm
+
+#endif
--- /dev/null
+#include "terminal.h"
+
+namespace Fm {
+
+#include <glib.h>
+#include <gio/gdesktopappinfo.h>
+#include <string.h>
+#include <unistd.h>
+
+#if !GLIB_CHECK_VERSION(2, 28, 0) && !HAVE_DECL_ENVIRON
+extern char** environ;
+#endif
+
+static void child_setup(gpointer user_data) {
+ /* Move child to grandparent group so it will not die with parent */
+ setpgid(0, (pid_t)(gsize)user_data);
+}
+
+bool launchTerminal(const char* programName, const FilePath& workingDir, Fm::GErrorPtr& error) {
+ /* read system terminals file */
+ GKeyFile* kf = g_key_file_new();
+ if(!g_key_file_load_from_file(kf, LIBFM_QT_DATA_DIR "/terminals.list", G_KEY_FILE_NONE, &error)) {
+ g_key_file_free(kf);
+ return false;
+ }
+ auto launch = g_key_file_get_string(kf, programName, "launch", nullptr);
+ auto desktop_id = g_key_file_get_string(kf, programName, "desktop_id", nullptr);
+
+ GDesktopAppInfo* appinfo = nullptr;
+ if(desktop_id) {
+ appinfo = g_desktop_app_info_new(desktop_id);
+ }
+
+ const gchar* cmd;
+ gchar* _cmd = nullptr;
+ if(appinfo) {
+ cmd = g_app_info_get_commandline(G_APP_INFO(appinfo));
+ }
+ else if(launch) {
+ cmd = _cmd = g_strdup_printf("%s %s", programName, launch);
+ }
+ else {
+ cmd = programName;
+ }
+
+#if 0 // FIXME: what's this?
+ if(custom_args) {
+ cmd = g_strdup_printf("%s %s", cmd, custom_args);
+ g_free(_cmd);
+ _cmd = (char*)cmd;
+ }
+#endif
+
+ char** argv;
+ int argc;
+ if(!g_shell_parse_argv(cmd, &argc, &argv, nullptr)) {
+ argv = nullptr;
+ }
+ g_free(_cmd);
+
+ if(appinfo) {
+ g_object_unref(appinfo);
+ }
+ if(!argv) { /* parsing failed */
+ return false;
+ }
+ char** envp;
+#if GLIB_CHECK_VERSION(2, 28, 0)
+ envp = g_get_environ();
+#else
+ envp = g_strdupv(environ);
+#endif
+
+ auto dir = workingDir ? workingDir.localPath() : nullptr;
+ if(dir) {
+#if GLIB_CHECK_VERSION(2, 32, 0)
+ envp = g_environ_setenv(envp, "PWD", dir.get(), TRUE);
+#else
+ char** env = envp;
+
+ if(env) while(*env != nullptr) {
+ if(strncmp(*env, "PWD=", 4) == 0) {
+ break;
+ }
+ env++;
+ }
+ if(env == nullptr || *env == nullptr) {
+ gint length;
+
+ length = envp ? g_strv_length(envp) : 0;
+ envp = g_renew(gchar*, envp, length + 2);
+ env = &envp[length];
+ env[1] = nullptr;
+ }
+ else {
+ g_free(*env);
+ }
+ *env = g_strdup_printf("PWD=%s", dir);
+#endif
+ }
+
+ bool ret = g_spawn_async(dir.get(), argv, envp, G_SPAWN_SEARCH_PATH,
+ child_setup, (gpointer)(gsize)getpgid(getppid()),
+ nullptr, &error);
+ g_strfreev(argv);
+ g_strfreev(envp);
+ g_key_file_free(kf);
+ return ret;
+}
+
+std::vector<CStrPtr> allKnownTerminals() {
+ std::vector<CStrPtr> terminals;
+ GKeyFile* kf = g_key_file_new();
+ if(g_key_file_load_from_file(kf, LIBFM_QT_DATA_DIR "/terminals.list", G_KEY_FILE_NONE, nullptr)) {
+ gsize n;
+ auto programs = g_key_file_get_groups(kf, &n);
+ terminals.reserve(n);
+ for(auto name = programs; *name; ++name) {
+ terminals.emplace_back(*name);
+ }
+ g_free(programs);
+ }
+ g_key_file_free(kf);
+ return terminals;
+}
+
+} // namespace Fm
--- /dev/null
+#ifndef TERMINAL_H
+#define TERMINAL_H
+
+#include "../libfmqtglobals.h"
+#include "gioptrs.h"
+#include "filepath.h"
+#include <vector>
+
+namespace Fm {
+
+LIBFM_QT_API bool launchTerminal(const char* programName, const FilePath& workingDir, GErrorPtr& error);
+
+LIBFM_QT_API std::vector<CStrPtr> allKnownTerminals();
+
+} // namespace Fm
+
+#endif // TERMINAL_H
--- /dev/null
+#include "thumbnailer.h"
+#include "mimetype.h"
+#include <string>
+#include <QDebug>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+namespace Fm {
+
+std::mutex Thumbnailer::mutex_;
+std::vector<std::shared_ptr<Thumbnailer>> Thumbnailer::allThumbnailers_;
+
+Thumbnailer::Thumbnailer(const char* id, GKeyFile* kf):
+ id_{g_strdup(id)},
+ try_exec_{g_key_file_get_string(kf, "Thumbnailer Entry", "TryExec", nullptr)},
+ exec_{g_key_file_get_string(kf, "Thumbnailer Entry", "Exec", nullptr)} {
+}
+
+CStrPtr Thumbnailer::commandForUri(const char* uri, const char* output_file, guint size) const {
+ if(exec_) {
+ /* FIXME: how to handle TryExec? */
+
+ /* parse the command line and do required substitutions according to:
+ * http://developer.gnome.org/integration-guide/stable/thumbnailer.html.en
+ */
+ GString* cmd_line = g_string_sized_new(1024);
+ const char* p;
+ for(p = exec_.get(); *p; ++p) {
+ if(G_LIKELY(*p != '%')) {
+ g_string_append_c(cmd_line, *p);
+ }
+ else {
+ char* quoted;
+ ++p;
+ switch(*p) {
+ case '\0':
+ break;
+ case 's':
+ g_string_append_printf(cmd_line, "%d", size);
+ break;
+ case 'i': {
+ char* src_path = g_filename_from_uri(uri, nullptr, nullptr);
+ if(src_path) {
+ quoted = g_shell_quote(src_path);
+ g_string_append(cmd_line, quoted);
+ g_free(quoted);
+ g_free(src_path);
+ }
+ break;
+ }
+ case 'u':
+ quoted = g_shell_quote(uri);
+ g_string_append(cmd_line, quoted);
+ g_free(quoted);
+ break;
+ case 'o':
+ g_string_append(cmd_line, output_file);
+ break;
+ default:
+ g_string_append_c(cmd_line, '%');
+ if(*p != '%') {
+ g_string_append_c(cmd_line, *p);
+ }
+ }
+ }
+ }
+ return CStrPtr{g_string_free(cmd_line, FALSE)};
+ }
+ return nullptr;
+}
+
+bool Thumbnailer::run(const char* uri, const char* output_file, int size) const {
+ auto cmd = commandForUri(uri, output_file, size);
+ qDebug() << cmd.get();
+ int status;
+ bool ret = g_spawn_command_line_sync(cmd.get(), nullptr, nullptr, &status, nullptr);
+ return ret && status == 0;
+}
+
+static void find_thumbnailers_in_data_dir(std::unordered_map<std::string, const char*>& hash, const char* data_dir) {
+ CStrPtr dir_path{g_build_filename(data_dir, "thumbnailers", nullptr)};
+ GDir* dir = g_dir_open(dir_path.get(), 0, nullptr);
+ if(dir) {
+ const char* basename;
+ while((basename = g_dir_read_name(dir)) != nullptr) {
+ /* we only want filenames with .thumbnailer extension */
+ if(G_LIKELY(g_str_has_suffix(basename, ".thumbnailer"))) {
+ hash.insert(std::make_pair(basename, data_dir));
+ }
+ }
+ g_dir_close(dir);
+ }
+}
+
+void Thumbnailer::loadAll() {
+ const gchar* const* data_dirs = g_get_system_data_dirs();
+ const gchar* const* data_dir;
+
+ /* use a temporary hash table to collect thumbnailer basenames
+ * key: basename of thumbnailer entry file
+ * value: data dir the thumbnailer entry file is in */
+ std::unordered_map<std::string, const char*> hash;
+
+ /* load user-specific thumbnailers */
+ find_thumbnailers_in_data_dir(hash, g_get_user_data_dir());
+
+ /* load system-wide thumbnailers */
+ for(data_dir = data_dirs; *data_dir; ++data_dir) {
+ find_thumbnailers_in_data_dir(hash, *data_dir);
+ }
+
+ /* load all found thumbnailers */
+ if(!hash.empty()) {
+ std::lock_guard<std::mutex> lock{mutex_};
+ GKeyFile* kf = g_key_file_new();
+ for(auto& item: hash) {
+ auto& base_name = item.first;
+ auto& dir_path = item.second;
+ CStrPtr file_path{g_build_filename(dir_path, "thumbnailers", base_name.c_str(), nullptr)};
+ if(g_key_file_load_from_file(kf, file_path.get(), G_KEY_FILE_NONE, nullptr)) {
+ auto thumbnailer = std::make_shared<Thumbnailer>(base_name.c_str(), kf);
+ char** mime_types = g_key_file_get_string_list(kf, "Thumbnailer Entry", "MimeType", nullptr, nullptr);
+ if(mime_types && thumbnailer->exec_) {
+ for(char** name = mime_types; *name; ++name) {
+ auto mime_type = MimeType::fromName(*name);
+ if(mime_type) {
+ thumbnailer->mimeTypes_.push_back(mime_type);
+ std::const_pointer_cast<MimeType>(mime_type)->addThumbnailer(thumbnailer);
+ }
+ }
+ g_strfreev(mime_types);
+ }
+ allThumbnailers_.push_back(std::move(thumbnailer));
+ }
+ }
+ g_key_file_free(kf);
+ }
+}
+
+} // namespace Fm
--- /dev/null
+#ifndef FM2_THUMBNAILER_H
+#define FM2_THUMBNAILER_H
+
+#include "../libfmqtglobals.h"
+#include "cstrptr.h"
+#include <unordered_map>
+#include <vector>
+#include <memory>
+#include <mutex>
+
+namespace Fm {
+
+class MimeType;
+
+class LIBFM_QT_API Thumbnailer {
+public:
+ explicit Thumbnailer(const char *id, GKeyFile *kf);
+
+ CStrPtr commandForUri(const char* uri, const char* output_file, guint size) const;
+
+ bool run(const char* uri, const char* output_file, int size) const;
+
+ static void loadAll();
+
+private:
+ CStrPtr id_;
+ CStrPtr try_exec_; /* FIXME: is this useful? */
+ CStrPtr exec_;
+ std::vector<std::shared_ptr<const MimeType>> mimeTypes_;
+
+ static std::mutex mutex_;
+ static std::vector<std::shared_ptr<Thumbnailer>> allThumbnailers_;
+};
+
+} // namespace Fm
+
+#endif // FM2_THUMBNAILER_H
--- /dev/null
+#include "thumbnailjob.h"
+#include <string>
+#include <memory>
+#include <algorithm>
+#include <libexif/exif-loader.h>
+#include <QImageReader>
+#include <QDir>
+#include "thumbnailer.h"
+
+namespace Fm {
+
+QThreadPool* ThumbnailJob::threadPool_ = nullptr;
+
+bool ThumbnailJob::localFilesOnly_ = true;
+int ThumbnailJob::maxThumbnailFileSize_ = 0;
+
+ThumbnailJob::ThumbnailJob(FileInfoList files, int size):
+ files_{std::move(files)},
+ size_{size},
+ md5Calc_{g_checksum_new(G_CHECKSUM_MD5)} {
+}
+
+ThumbnailJob::~ThumbnailJob() {
+ g_checksum_free(md5Calc_);
+ // qDebug("delete ThumbnailJob");
+}
+
+void ThumbnailJob::exec() {
+ for(auto& file: files_) {
+ if(isCancelled()) {
+ break;
+ }
+ auto image = loadForFile(file);
+ Q_EMIT thumbnailLoaded(file, size_, image);
+ results_.emplace_back(std::move(image));
+ }
+}
+
+QImage ThumbnailJob::readImageFromStream(GInputStream* stream, size_t len) {
+ // FIXME: should we set a limit here? Otherwise if len is too large, we can run out of memory.
+ std::unique_ptr<unsigned char[]> buffer{new unsigned char[len]}; // allocate enough buffer
+ unsigned char* pbuffer = buffer.get();
+ size_t totalReadSize = 0;
+ while(!isCancelled() && totalReadSize < len) {
+ size_t bytesToRead = totalReadSize + 4096 > len ? len - totalReadSize : 4096;
+ gssize readSize = g_input_stream_read(stream, pbuffer, bytesToRead, cancellable_.get(), nullptr);
+ if(readSize == 0) { // end of file
+ break;
+ }
+ else if(readSize == -1) { // error
+ return QImage();
+ }
+ totalReadSize += readSize;
+ pbuffer += readSize;
+ }
+ QImage image;
+ image.loadFromData(buffer.get(), totalReadSize);
+ return image;
+}
+
+QImage ThumbnailJob::loadForFile(const std::shared_ptr<const FileInfo> &file) {
+ if(!file->canThumbnail()) {
+ return QImage();
+ }
+
+ // thumbnails are stored in $XDG_CACHE_HOME/thumbnails/large|normal|failed
+ QString thumbnailDir{g_get_user_cache_dir()};
+ thumbnailDir += "/thumbnails/";
+
+ // don't make thumbnails for files inside the thumbnail directory
+ if(FilePath::fromLocalPath(thumbnailDir.toLocal8Bit().constData()).isParentOf(file->dirPath())) {
+ return QImage();
+ }
+
+ const char* subdir = size_ > 128 ? "large" : "normal";
+ thumbnailDir += subdir;
+
+ // generate base name of the thumbnail => {md5 of uri}.png
+ auto origPath = file->path();
+ auto uri = origPath.uri();
+
+ char thumbnailName[32 + 5];
+ // calculate md5 hash for the uri of the original file
+ g_checksum_update(md5Calc_, reinterpret_cast<const unsigned char*>(uri.get()), -1);
+ memcpy(thumbnailName, g_checksum_get_string(md5Calc_), 32);
+ mempcpy(thumbnailName + 32, ".png", 5);
+ g_checksum_reset(md5Calc_); // reset the checksum calculator for next use
+
+ QString thumbnailFilename = thumbnailDir;
+ thumbnailFilename += '/';
+ thumbnailFilename += thumbnailName;
+ // qDebug() << "thumbnail:" << file->getName().c_str() << thumbnailFilename;
+
+ // try to load the thumbnail file if it exists
+ QImage thumbnail{thumbnailFilename};
+ if(thumbnail.isNull() || isThumbnailOutdated(file, thumbnail)) {
+ // the existing thumbnail cannot be loaded, generate a new one
+
+ // create the thumbnail dir as needd (FIXME: Qt file I/O is slow)
+ QDir().mkpath(thumbnailDir);
+
+ thumbnail = generateThumbnail(file, origPath, uri.get(), thumbnailFilename);
+ }
+ // resize to the size we need
+ if(thumbnail.width() > size_ || thumbnail.height() > size_) {
+ thumbnail = thumbnail.scaled(size_, size_, Qt::KeepAspectRatio, Qt::SmoothTransformation);
+ }
+ return thumbnail;
+}
+
+bool ThumbnailJob::isSupportedImageType(const std::shared_ptr<const MimeType>& mimeType) const {
+ if(mimeType->isImage()) {
+ auto supportedTypes = QImageReader::supportedMimeTypes();
+ auto found = std::find(supportedTypes.cbegin(), supportedTypes.cend(), mimeType->name());
+ if(found != supportedTypes.cend())
+ return true;
+ }
+ return false;
+}
+
+bool ThumbnailJob::isThumbnailOutdated(const std::shared_ptr<const FileInfo>& file, const QImage &thumbnail) const {
+ QString thumb_mtime = thumbnail.text("Thumb::MTime");
+ return (thumb_mtime.isEmpty() || thumb_mtime.toInt() != file->mtime());
+}
+
+bool ThumbnailJob::readJpegExif(GInputStream *stream, QImage& thumbnail, int& rotate_degrees) {
+ /* try to extract thumbnails embedded in jpeg files */
+ ExifLoader* exif_loader = exif_loader_new();
+ while(!isCancelled()) {
+ unsigned char buf[4096];
+ gssize read_size = g_input_stream_read(stream, buf, 4096, cancellable_.get(), nullptr);
+ if(read_size <= 0) { // EOF or error
+ break;
+ }
+ if(exif_loader_write(exif_loader, buf, read_size) == 0) {
+ break; // no more EXIF data
+ }
+ }
+ ExifData* exif_data = exif_loader_get_data(exif_loader);
+ exif_loader_unref(exif_loader);
+ if(exif_data) {
+ /* reference for EXIF orientation tag:
+ * http://www.impulseadventure.com/photo/exif-orientation.html */
+ ExifEntry* orient_ent = exif_data_get_entry(exif_data, EXIF_TAG_ORIENTATION);
+ if(orient_ent) { /* orientation flag found in EXIF */
+ gushort orient;
+ ExifByteOrder bo = exif_data_get_byte_order(exif_data);
+ /* bo == EXIF_BYTE_ORDER_INTEL ; */
+ orient = exif_get_short(orient_ent->data, bo);
+ switch(orient) {
+ case 1: /* no rotation */
+ rotate_degrees = 0;
+ break;
+ case 8:
+ rotate_degrees = 90;
+ break;
+ case 3:
+ rotate_degrees = 180;
+ break;
+ case 6:
+ rotate_degrees = 270;
+ break;
+ }
+ }
+ if(exif_data->data) { // if an embedded thumbnail is available, load it
+ thumbnail.loadFromData(exif_data->data, exif_data->size);
+ }
+ exif_data_unref(exif_data);
+ }
+ return !thumbnail.isNull();
+}
+
+QImage ThumbnailJob::generateThumbnail(const std::shared_ptr<const FileInfo>& file, const FilePath& origPath, const char* uri, const QString& thumbnailFilename) {
+ QImage result;
+ auto mime_type = file->mimeType();
+ if(isSupportedImageType(mime_type)) {
+ GFileInputStreamPtr ins{g_file_read(origPath.gfile().get(), cancellable_.get(), nullptr), false};
+ if(!ins)
+ return QImage();
+ bool fromExif = false;
+ int rotate_degrees = 0;
+ if(strcmp(mime_type->name(), "image/jpeg") == 0) { // if this is a jpeg file
+ // try to get the thumbnail embedded in EXIF data
+ if(readJpegExif(G_INPUT_STREAM(ins.get()), result, rotate_degrees)) {
+ fromExif = true;
+ }
+ }
+ if(!fromExif) { // not able to generate a thumbnail from the EXIF data
+ // load the original file and do the scaling ourselves
+ g_seekable_seek(G_SEEKABLE(ins.get()), 0, G_SEEK_SET, cancellable_.get(), nullptr);
+ result = readImageFromStream(G_INPUT_STREAM(ins.get()), file->size());
+ }
+ g_input_stream_close(G_INPUT_STREAM(ins.get()), nullptr, nullptr);
+
+ if(!result.isNull()) { // the image is successfully loaded
+ // scale the image as needed
+ int target_size = size_ > 128 ? 256 : 128;
+
+ // only scale the original image if it's too large
+ if(result.width() > target_size || result.height() > target_size) {
+ result = result.scaled(target_size, target_size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
+ }
+
+ if(rotate_degrees != 0) {
+ // degree values are 0, 90, 180, and 270 counterclockwise.
+ // In Qt, QMatrix does rotation counterclockwise as well.
+ // However, because the y axis of widget coordinate system is downward,
+ // the real effect of the coordinate transformation becomes clockwise rotation.
+ // So we need to use (360 - degree) here.
+ // Quote from QMatrix API doc:
+ // Note that if you apply a QMatrix to a point defined in widget
+ // coordinates, the direction of the rotation will be clockwise because
+ // the y-axis points downwards.
+ result = result.transformed(QMatrix().rotate(360 - rotate_degrees));
+ }
+
+ // save the generated thumbnail to disk (don't save png thumbnails for JPEG EXIF thumbnails since loading them is cheap)
+ if(!fromExif) {
+ result.setText("Thumb::MTime", QString::number(file->mtime()));
+ result.setText("Thumb::URI", uri);
+ result.save(thumbnailFilename, "PNG");
+ }
+ // qDebug() << "save thumbnail:" << thumbnailFilename;
+ }
+ }
+ else { // the image format is not supported, try to find an external thumbnailer
+ // try all available external thumbnailers for it until sucess
+ int target_size = size_ > 128 ? 256 : 128;
+ file->mimeType()->forEachThumbnailer([&](const std::shared_ptr<const Thumbnailer>& thumbnailer) {
+ if(thumbnailer->run(uri, thumbnailFilename.toLocal8Bit().constData(), target_size)) {
+ result = QImage(thumbnailFilename);
+ }
+ return !result.isNull(); // return true on success, and forEachThumbnailer() will stop.
+ });
+
+ if(!result.isNull()) {
+ // Some thumbnailers did not write the proper metadata required by the xdg spec to the output (such as evince-thumbnailer)
+ // Here we waste some time to fix them so next time we don't need to re-generate these thumbnails. :-(
+ bool changed = false;
+ if(Q_UNLIKELY(result.text("Thumb::MTime").isEmpty())) {
+ result.setText("Thumb::MTime", QString::number(file->mtime()));
+ changed = true;
+ }
+ if(Q_UNLIKELY(result.text("Thumb::URI").isEmpty())) {
+ result.setText("Thumb::URI", uri);
+ changed = true;
+ }
+ if(Q_UNLIKELY(changed)) {
+ // save the modified PNG file containing metadata to a file.
+ result.save(thumbnailFilename, "PNG");
+ }
+ }
+ }
+ return result;
+}
+
+QThreadPool* ThumbnailJob::threadPool() {
+ if(Q_UNLIKELY(threadPool_ == nullptr)) {
+ threadPool_ = new QThreadPool();
+ threadPool_->setMaxThreadCount(1);
+ }
+ return threadPool_;
+}
+
+
+} // namespace Fm
--- /dev/null
+#ifndef FM2_THUMBNAILJOB_H
+#define FM2_THUMBNAILJOB_H
+
+#include "../libfmqtglobals.h"
+#include "fileinfo.h"
+#include "gioptrs.h"
+#include "job.h"
+#include <QThreadPool>
+
+namespace Fm {
+
+class LIBFM_QT_API ThumbnailJob: public Job {
+ Q_OBJECT
+public:
+
+ explicit ThumbnailJob(FileInfoList files, int size);
+
+ ~ThumbnailJob();
+
+ int size() const {
+ return size_;
+ }
+
+ static QThreadPool* threadPool();
+
+
+ static void setLocalFilesOnly(bool value) {
+ localFilesOnly_ = value;
+ if(fm_config) {
+ fm_config->thumbnail_local = localFilesOnly_;
+ }
+ }
+
+ static bool localFilesOnly() {
+ return localFilesOnly_;
+ }
+
+ static int maxThumbnailFileSize() {
+ return maxThumbnailFileSize_;
+ }
+
+ static void setMaxThumbnailFileSize(int size) {
+ maxThumbnailFileSize_ = size;
+ if(fm_config) {
+ fm_config->thumbnail_max = maxThumbnailFileSize_;
+ }
+ }
+
+ const std::vector<QImage>& results() const {
+ return results_;
+ }
+
+Q_SIGNALS:
+ void thumbnailLoaded(const std::shared_ptr<const FileInfo>& file, int size, QImage thumbnail);
+
+protected:
+
+ void exec() override;
+
+private:
+
+ bool isSupportedImageType(const std::shared_ptr<const MimeType>& mimeType) const;
+
+ bool isThumbnailOutdated(const std::shared_ptr<const FileInfo>& file, const QImage& thumbnail) const;
+
+ QImage generateThumbnail(const std::shared_ptr<const FileInfo>& file, const FilePath& origPath, const char* uri, const QString& thumbnailFilename);
+
+ QImage readImageFromStream(GInputStream* stream, size_t len);
+
+ QImage loadForFile(const std::shared_ptr<const FileInfo>& file);
+
+ bool readJpegExif(GInputStream* stream, QImage& thumbnail, int& rotate_degrees);
+
+private:
+ FileInfoList files_;
+ int size_;
+ std::vector<QImage> results_;
+ GCancellablePtr cancellable_;
+ GChecksum* md5Calc_;
+
+ static QThreadPool* threadPool_;
+
+ static bool localFilesOnly_;
+ static int maxThumbnailFileSize_;
+};
+
+} // namespace Fm
+
+#endif // FM2_THUMBNAILJOB_H
--- /dev/null
+#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
--- /dev/null
+#ifndef FM2_TOTALSIZEJOB_H
+#define FM2_TOTALSIZEJOB_H
+
+#include "../libfmqtglobals.h"
+#include "fileoperationjob.h"
+#include "filepath.h"
+#include <cstdint>
+#include "gioptrs.h"
+
+namespace Fm {
+
+class LIBFM_QT_API TotalSizeJob : public Fm::FileOperationJob {
+ Q_OBJECT
+public:
+ enum Flags {
+ DEFAULT = 0,
+ FOLLOW_LINKS = 1 << 0,
+ SAME_FS = 1 << 1,
+ PREPARE_MOVE = 1 << 2,
+ PREPARE_DELETE = 1 << 3
+ };
+
+ explicit TotalSizeJob(FilePathList paths = FilePathList{}, Flags flags = DEFAULT);
+
+ std::uint64_t totalSize() const {
+ return totalSize_;
+ }
+
+ std::uint64_t totalOnDiskSize() const {
+ return totalOndiskSize_;
+ }
+
+ unsigned int fileCount() const {
+ return fileCount_;
+ }
+
+protected:
+
+ void exec() override;
+
+private:
+ void exec(FilePath path, GFileInfoPtr inf);
+
+private:
+ FilePathList paths_;
+
+ int flags_;
+ std::uint64_t totalSize_;
+ std::uint64_t totalOndiskSize_;
+ unsigned int fileCount_;
+ const char* dest_fs_id;
+};
+
+} // namespace Fm
+
+#endif // FM2_TOTALSIZEJOB_H
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#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
--- /dev/null
+#include "userinfocache.h"
+#include <pwd.h>
+#include <grp.h>
+
+namespace Fm {
+
+UserInfoCache* UserInfoCache::globalInstance_ = nullptr;
+std::mutex UserInfoCache::mutex_;
+
+UserInfoCache::UserInfoCache() : QObject() {
+}
+
+const std::shared_ptr<const UserInfo>& UserInfoCache::userFromId(uid_t uid) {
+ std::lock_guard<std::mutex> lock{mutex_};
+ auto it = users_.find(uid);
+ if(it != users_.end())
+ return it->second;
+ std::shared_ptr<const UserInfo> user;
+ auto pw = getpwuid(uid);
+ if(pw) {
+ user = std::make_shared<UserInfo>(uid, pw->pw_name, pw->pw_gecos);
+ }
+ return (users_[uid] = user);
+}
+
+const std::shared_ptr<const GroupInfo>& UserInfoCache::groupFromId(gid_t gid) {
+ std::lock_guard<std::mutex> lock{mutex_};
+ auto it = groups_.find(gid);
+ if(it != groups_.end())
+ return it->second;
+ std::shared_ptr<const GroupInfo> group;
+ auto gr = getgrgid(gid);
+ if(gr) {
+ group = std::make_shared<GroupInfo>(gid, gr->gr_name);
+ }
+ return (groups_[gid] = group);
+}
+
+// static
+UserInfoCache* UserInfoCache::globalInstance() {
+ std::lock_guard<std::mutex> lock{mutex_};
+ if(!globalInstance_)
+ globalInstance_ = new UserInfoCache();
+ return globalInstance_;
+}
+
+} // namespace Fm
--- /dev/null
+#ifndef FM2_USERINFOCACHE_H
+#define FM2_USERINFOCACHE_H
+
+#include "../libfmqtglobals.h"
+#include <QObject>
+#include <string>
+#include <unordered_map>
+#include <sys/types.h>
+#include <memory>
+#include <mutex>
+
+namespace Fm {
+
+class LIBFM_QT_API UserInfo {
+public:
+ explicit UserInfo(uid_t uid, const char* name, const char* realName):
+ uid_{uid}, name_{name}, realName_{realName} {
+ }
+
+ uid_t uid() const {
+ return uid_;
+ }
+
+ const QString& name() const {
+ return name_;
+ }
+
+ const QString& realName() const {
+ return realName_;
+ }
+
+private:
+ uid_t uid_;
+ QString name_;
+ QString realName_;
+
+};
+
+class LIBFM_QT_API GroupInfo {
+public:
+ explicit GroupInfo(gid_t gid, const char* name): gid_{gid}, name_{name} {
+ }
+
+ gid_t gid() const {
+ return gid_;
+ }
+
+ const QString& name() const {
+ return name_;
+ }
+
+private:
+ gid_t gid_;
+ QString name_;
+};
+
+// FIXME: handle file changes
+
+class LIBFM_QT_API UserInfoCache : public QObject {
+ Q_OBJECT
+public:
+ explicit UserInfoCache();
+
+ const std::shared_ptr<const UserInfo>& userFromId(uid_t uid);
+
+ const std::shared_ptr<const GroupInfo>& groupFromId(gid_t gid);
+
+ static UserInfoCache* globalInstance();
+
+Q_SIGNALS:
+ void changed();
+
+private:
+ std::unordered_map<uid_t, std::shared_ptr<const UserInfo>> users_;
+ std::unordered_map<gid_t, std::shared_ptr<const GroupInfo>> groups_;
+ static UserInfoCache* globalInstance_;
+ static std::mutex mutex_;
+};
+
+} // namespace Fm
+
+#endif // FM2_USERINFOCACHE_H
--- /dev/null
+#include "volumemanager.h"
+
+namespace Fm {
+
+std::mutex VolumeManager::mutex_;
+std::weak_ptr<VolumeManager> VolumeManager::globalInstance_;
+
+VolumeManager::VolumeManager():
+ QObject(),
+ monitor_{g_volume_monitor_get(), false} {
+
+ // connect gobject signal handlers
+ g_signal_connect(monitor_.get(), "volume-added", G_CALLBACK(_onGVolumeAdded), this);
+ g_signal_connect(monitor_.get(), "volume-removed", G_CALLBACK(_onGVolumeRemoved), this);
+ g_signal_connect(monitor_.get(), "volume-changed", G_CALLBACK(_onGVolumeChanged), this);
+
+ g_signal_connect(monitor_.get(), "mount-added", G_CALLBACK(_onGMountAdded), this);
+ g_signal_connect(monitor_.get(), "mount-removed", G_CALLBACK(_onGMountRemoved), this);
+ g_signal_connect(monitor_.get(), "mount-changed", G_CALLBACK(_onGMountChanged), this);
+
+ // g_get_volume_monitor() is a slow blocking call, so call it in a low priority thread
+ auto job = new GetGVolumeMonitorJob();
+ job->setAutoDelete(true);
+ connect(job, &GetGVolumeMonitorJob::finished, this, &VolumeManager::onGetGVolumeMonitorFinished, Qt::BlockingQueuedConnection);
+ job->runAsync(QThread::LowPriority);
+}
+
+VolumeManager::~VolumeManager() {
+ if(monitor_) {
+ g_signal_handlers_disconnect_by_data(monitor_.get(), this);
+ }
+}
+
+std::shared_ptr<VolumeManager> VolumeManager::globalInstance() {
+ std::lock_guard<std::mutex> lock{mutex_};
+ auto mon = globalInstance_.lock();
+ if(mon == nullptr) {
+ mon = std::make_shared<VolumeManager>();
+ globalInstance_ = mon;
+ }
+ return mon;
+}
+
+void VolumeManager::onGetGVolumeMonitorFinished() {
+ auto job = static_cast<GetGVolumeMonitorJob*>(sender());
+ monitor_ = std::move(job->monitor_);
+ GList* vols = g_volume_monitor_get_volumes(monitor_.get());
+ for(GList* l = vols; l != nullptr; l = l->next) {
+ volumes_.push_back(Volume{G_VOLUME(l->data), false});
+ Q_EMIT volumeAdded(volumes_.back());
+ }
+ g_list_free(vols);
+
+ GList* mnts = g_volume_monitor_get_mounts(monitor_.get());
+ for(GList* l = mnts; l != nullptr; l = l->next) {
+ mounts_.push_back(Mount{G_MOUNT(l->data), false});
+ Q_EMIT mountAdded(mounts_.back());
+ }
+ g_list_free(mnts);
+}
+
+void VolumeManager::onGVolumeAdded(GVolume* vol) {
+ if(std::find(volumes_.cbegin(), volumes_.cend(), vol) != volumes_.cend())
+ return;
+ volumes_.push_back(Volume{vol, true});
+ Q_EMIT volumeAdded(volumes_.back());
+}
+
+void VolumeManager::onGVolumeRemoved(GVolume* vol) {
+ auto it = std::find(volumes_.begin(), volumes_.end(), vol);
+ if(it == volumes_.end())
+ return;
+ Q_EMIT volumeRemoved(*it);
+ volumes_.erase(it);
+}
+
+void VolumeManager::onGVolumeChanged(GVolume* vol) {
+ auto it = std::find(volumes_.begin(), volumes_.end(), vol);
+ if(it == volumes_.end())
+ return;
+ Q_EMIT volumeChanged(*it);
+}
+
+void VolumeManager::onGMountAdded(GMount* mnt) {
+ if(std::find(mounts_.cbegin(), mounts_.cend(), mnt) != mounts_.cend())
+ return;
+ mounts_.push_back(Mount{mnt, true});
+ Q_EMIT mountAdded(mounts_.back());
+}
+
+void VolumeManager::onGMountRemoved(GMount* mnt) {
+ auto it = std::find(mounts_.begin(), mounts_.end(), mnt);
+ if(it == mounts_.end())
+ return;
+ Q_EMIT mountRemoved(*it);
+ mounts_.erase(it);
+}
+
+void VolumeManager::onGMountChanged(GMount* mnt) {
+ auto it = std::find(mounts_.begin(), mounts_.end(), mnt);
+ if(it == mounts_.end())
+ return;
+ Q_EMIT mountChanged(*it);
+}
+
+void VolumeManager::GetGVolumeMonitorJob::exec() {
+ monitor_ = GVolumeMonitorPtr{g_volume_monitor_get(), false};
+}
+
+
+} // namespace Fm
--- /dev/null
+#ifndef FM2_VOLUMEMANAGER_H
+#define FM2_VOLUMEMANAGER_H
+
+#include "../libfmqtglobals.h"
+#include <QObject>
+#include <gio/gio.h>
+#include "gioptrs.h"
+#include "filepath.h"
+#include "iconinfo.h"
+#include "job.h"
+#include <vector>
+#include <mutex>
+
+namespace Fm {
+
+class LIBFM_QT_API Volume: public GVolumePtr {
+public:
+
+ explicit Volume(GVolume* gvol, bool addRef): GVolumePtr{gvol, addRef} {
+ }
+
+ explicit Volume(GVolumePtr gvol): GVolumePtr{std::move(gvol)} {
+ }
+
+ CStrPtr name() const {
+ return CStrPtr{g_volume_get_name(get())};
+ }
+
+ CStrPtr uuid() const {
+ return CStrPtr{g_volume_get_uuid(get())};
+ }
+
+ std::shared_ptr<const IconInfo> icon() const {
+ return IconInfo::fromGIcon(GIconPtr{g_volume_get_icon(get()), false});
+ }
+
+ // GDrive * g_volume_get_drive(get());
+ GMountPtr mount() const {
+ return GMountPtr{g_volume_get_mount(get()), false};
+ }
+
+ bool canMount() const {
+ return g_volume_can_mount(get());
+ }
+
+ bool shouldAutoMount() const {
+ return g_volume_should_automount(get());
+ }
+
+ FilePath activationRoot() const {
+ return FilePath{g_volume_get_activation_root(get()), false};
+ }
+
+ /*
+ void g_volume_mount(get());
+ gboolean g_volume_mount_finish(get());
+ */
+ bool canEject() const {
+ return g_volume_can_eject(get());
+ }
+
+ /*
+ void g_volume_eject(get());
+ gboolean g_volume_eject_finish(get());
+ void g_volume_eject_with_operation(get());
+ gboolean g_volume_eject_with_operation_finish(get());
+ char ** g_volume_enumerate_identifiers(get());
+ char * g_volume_get_identifier(get());
+ const gchar * g_volume_get_sort_key(get());
+ */
+
+ CStrPtr device() const {
+ return CStrPtr{g_volume_get_identifier(get(), G_VOLUME_IDENTIFIER_KIND_UNIX_DEVICE)};
+ }
+
+ CStrPtr label() const {
+ return CStrPtr{g_volume_get_identifier(get(), G_VOLUME_IDENTIFIER_KIND_LABEL)};
+ }
+
+};
+
+
+class LIBFM_QT_API Mount: public GMountPtr {
+public:
+
+ explicit Mount(GMount* mnt, bool addRef): GMountPtr{mnt, addRef} {
+ }
+
+ explicit Mount(GMountPtr gmnt): GMountPtr{std::move(gmnt)} {
+ }
+
+ CStrPtr name() const {
+ return CStrPtr{g_mount_get_name(get())};
+ }
+
+ CStrPtr uuid() const {
+ return CStrPtr{g_mount_get_uuid(get())};
+ }
+
+ std::shared_ptr<const IconInfo> icon() const {
+ return IconInfo::fromGIcon(GIconPtr{g_mount_get_icon(get()), false});
+ }
+
+ // GIcon * g_mount_get_symbolic_icon(get());
+ // GDrive * g_mount_get_drive(get());
+ FilePath root() const {
+ return FilePath{g_mount_get_root(get()), false};
+ }
+
+ GVolumePtr volume() const {
+ return GVolumePtr{g_mount_get_volume(get()), false};
+ }
+
+ FilePath defaultLocation() const {
+ return FilePath{g_mount_get_default_location(get()), false};
+ }
+
+ bool canUnmount() const {
+ return g_mount_can_unmount(get());
+ }
+
+/*
+ void g_mount_unmount(get());
+ gboolean g_mount_unmount_finish(get());
+ void g_mount_unmount_with_operation(get());
+ gboolean g_mount_unmount_with_operation_finish(get());
+ void g_mount_remount(get());
+ gboolean g_mount_remount_finish(get());
+*/
+ bool canEject() const {
+ return g_mount_can_eject(get());
+ }
+
+/*
+ void g_mount_eject(get());
+ gboolean g_mount_eject_finish(get());
+ void g_mount_eject_with_operation(get());
+ gboolean g_mount_eject_with_operation_finish(get());
+*/
+ // void g_mount_guess_content_type(get());
+ // gchar ** g_mount_guess_content_type_finish(get());
+ // gchar ** g_mount_guess_content_type_sync(get());
+
+ bool isShadowed() const {
+ return g_mount_is_shadowed(get());
+ }
+
+ // void g_mount_shadow(get());
+ // void g_mount_unshadow(get());
+ // const gchar * g_mount_get_sort_key(get());
+};
+
+
+
+class LIBFM_QT_API VolumeManager : public QObject {
+ Q_OBJECT
+public:
+ explicit VolumeManager();
+
+ ~VolumeManager();
+
+ const std::vector<Volume>& volumes() const {
+ return volumes_;
+ }
+
+ const std::vector<Mount>& mounts() const {
+ return mounts_;
+ }
+
+ static std::shared_ptr<VolumeManager> globalInstance();
+
+Q_SIGNALS:
+ void volumeAdded(const Volume& vol);
+ void volumeRemoved(const Volume& vol);
+ void volumeChanged(const Volume& vol);
+
+ void mountAdded(const Mount& mnt);
+ void mountRemoved(const Mount& mnt);
+ void mountChanged(const Mount& mnt);
+
+public Q_SLOTS:
+
+ void onGetGVolumeMonitorFinished();
+
+private:
+
+ class GetGVolumeMonitorJob: public Job {
+ public:
+ GetGVolumeMonitorJob() {}
+ GVolumeMonitorPtr monitor_;
+ protected:
+ void exec() override;
+ };
+
+ static void _onGVolumeAdded(GVolumeMonitor* /*mon*/, GVolume* vol, VolumeManager* _this) {
+ _this->onGVolumeAdded(vol);
+ }
+ void onGVolumeAdded(GVolume* vol);
+
+ static void _onGVolumeRemoved(GVolumeMonitor* /*mon*/, GVolume* vol, VolumeManager* _this) {
+ _this->onGVolumeRemoved(vol);
+ }
+ void onGVolumeRemoved(GVolume* vol);
+
+ static void _onGVolumeChanged(GVolumeMonitor* /*mon*/, GVolume* vol, VolumeManager* _this) {
+ _this->onGVolumeChanged(vol);
+ }
+ void onGVolumeChanged(GVolume* vol);
+
+ static void _onGMountAdded(GVolumeMonitor* /*mon*/, GMount* mnt, VolumeManager* _this) {
+ _this->onGMountAdded(mnt);
+ }
+ void onGMountAdded(GMount* mnt);
+
+ static void _onGMountRemoved(GVolumeMonitor* /*mon*/, GMount* mnt, VolumeManager* _this) {
+ _this->onGMountRemoved(mnt);
+ }
+ void onGMountRemoved(GMount* mnt);
+
+ static void _onGMountChanged(GVolumeMonitor* /*mon*/, GMount* mnt, VolumeManager* _this) {
+ _this->onGMountChanged(mnt);
+ }
+ void onGMountChanged(GMount* mnt);
+
+private:
+ GVolumeMonitorPtr monitor_;
+
+ std::vector<Volume> volumes_;
+ std::vector<Mount> mounts_;
+
+ static std::mutex mutex_;
+ static std::weak_ptr<VolumeManager> globalInstance_;
+};
+
+} // namespace Fm
+
+#endif // FM2_VOLUMEMANAGER_H
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#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<QAction*>(sender());
+ QByteArray name = action->objectName().toUtf8();
+ GList* templates = fm_template_list_all(fm_config->only_user_templates);
+ FmTemplate* templ = nullptr;
+ for(GList* l = templates; l; l = l->next) {
+ FmTemplate* t = (FmTemplate*)l->data;
+ if(name == fm_template_get_name(t, nullptr)) {
+ templ = t;
+ break;
+ }
+ }
+ if(templ) { // template found
+ if(dirPath_) {
+ createFileOrFolder(CreateWithTemplate, dirPath_, templ, dialogParent_);
+ }
+ }
+}
+
+} // namespace Fm
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef FM_CREATENEWMENU_H
+#define FM_CREATENEWMENU_H
+
+#include "libfmqtglobals.h"
+#include <QMenu>
+#include <libfm/fm.h>
+
+#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
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef FM_CUSTOMACTION_P_H
+#define FM_CUSTOMACTION_P_H
+
+#include <QAction>
+#include "customactions/fileaction.h"
+
+namespace Fm {
+
+class CustomAction : public QAction {
+public:
+ explicit CustomAction(std::shared_ptr<const FileActionItem> item, QObject* parent = nullptr):
+ QAction{QString::fromStdString(item->get_name()), parent},
+ item_{item} {
+ auto& icon_name = item->get_icon();
+ if(!icon_name.empty()) {
+ setIcon(QIcon::fromTheme(icon_name.c_str()));
+ }
+ }
+
+ virtual ~CustomAction() {
+ }
+
+ const std::shared_ptr<const FileActionItem>& item() const {
+ return item_;
+ }
+
+private:
+ std::shared_ptr<const FileActionItem> item_;
+};
+
+} // namespace Fm
+
+#endif
--- /dev/null
+#include "fileaction.h"
+#include <unordered_map>
+#include <QDebug>
+
+using namespace std;
+
+namespace Fm {
+
+static const char* desktop_env = nullptr; // current desktop environment
+static bool actions_loaded = false; // all actions are loaded?
+static unordered_map<const char*, shared_ptr<FileActionObject>, CStrHash, CStrEqual> all_actions; // cache all loaded actions
+
+FileActionObject::FileActionObject() {
+}
+
+FileActionObject::FileActionObject(GKeyFile* kf) {
+ name = CStrPtr{g_key_file_get_locale_string(kf, "Desktop Entry", "Name", nullptr, nullptr)};
+ tooltip = CStrPtr{g_key_file_get_locale_string(kf, "Desktop Entry", "Tooltip", nullptr, nullptr)};
+ icon = CStrPtr{g_key_file_get_locale_string(kf, "Desktop Entry", "Icon", nullptr, nullptr)};
+ desc = CStrPtr{g_key_file_get_locale_string(kf, "Desktop Entry", "Description", nullptr, nullptr)};
+ GErrorPtr err;
+ enabled = g_key_file_get_boolean(kf, "Desktop Entry", "Enabled", &err);
+ if(err) { // key not found, default to true
+ err.reset();
+ enabled = true;
+ }
+ hidden = g_key_file_get_boolean(kf, "Desktop Entry", "Hidden", nullptr);
+ suggested_shortcut = CStrPtr{g_key_file_get_string(kf, "Desktop Entry", "SuggestedShortcut", nullptr)};
+
+ condition = unique_ptr<FileActionCondition> {new FileActionCondition(kf, "Desktop Entry")};
+
+ has_parent = false;
+}
+
+FileActionObject::~FileActionObject() {
+}
+
+//static
+bool FileActionObject::is_plural_exec(const char* exec) {
+ if(!exec) {
+ return false;
+ }
+ // the first relevent code encountered in Exec parameter
+ // determines whether the command accepts singular or plural forms
+ for(int i = 0; exec[i]; ++i) {
+ char ch = exec[i];
+ if(ch == '%') {
+ ++i;
+ ch = exec[i];
+ switch(ch) {
+ case 'B':
+ case 'D':
+ case 'F':
+ case 'M':
+ case 'O':
+ case 'U':
+ case 'W':
+ case 'X':
+ return true; // plural
+ case 'b':
+ case 'd':
+ case 'f':
+ case 'm':
+ case 'o':
+ case 'u':
+ case 'w':
+ case 'x':
+ return false; // singular
+ default:
+ // irrelevent code, skip
+ break;
+ }
+ }
+ }
+ return false; // singular form by default
+}
+
+std::string FileActionObject::expand_str(const char* templ, const FileInfoList& files, bool for_display, std::shared_ptr<const FileInfo> first_file) {
+ if(!templ) {
+ return string{};
+ }
+ string result;
+
+ if(!first_file) {
+ first_file = files.front();
+ }
+
+ for(int i = 0; templ[i]; ++i) {
+ char ch = templ[i];
+ if(ch == '%') {
+ ++i;
+ ch = templ[i];
+ switch(ch) {
+ case 'b': // (first) basename
+ if(for_display) {
+ result += first_file->name();
+ }
+ else {
+ CStrPtr quoted{g_shell_quote(first_file->name().c_str())};
+ result += quoted.get();
+ }
+ break;
+ case 'B': // space-separated list of basenames
+ for(auto& fi : files) {
+ if(for_display) {
+ result += fi->name();
+ }
+ else {
+ CStrPtr quoted{g_shell_quote(fi->name().c_str())};
+ result += quoted.get();
+ }
+ result += ' ';
+ }
+ if(result[result.length() - 1] == ' ') { // remove trailing space
+ result.erase(result.length() - 1);
+ }
+ break;
+ case 'c': // count of selected items
+ result += to_string(files.size());
+ break;
+ case 'd': { // (first) base directory
+ // FIXME: should the base dir be a URI?
+ auto base_dir = first_file->dirPath();
+ auto str = base_dir.toString();
+ if(for_display) {
+ // FIXME: str = Filename.display_name(str);
+ }
+ CStrPtr quoted{g_shell_quote(str.get())};
+ result += quoted.get();
+ break;
+ }
+ case 'D': // space-separated list of base directory of each selected items
+ for(auto& fi : files) {
+ auto base_dir = fi->dirPath();
+ auto str = base_dir.toString();
+ if(for_display) {
+ // str = Filename.display_name(str);
+ }
+ CStrPtr quoted{g_shell_quote(str.get())};
+ result += quoted.get();
+ result += ' ';
+ }
+ if(result[result.length() - 1] == ' ') { // remove trailing space
+ result.erase(result.length() - 1);
+ }
+ break;
+ case 'f': { // (first) file name
+ auto filename = first_file->path().toString();
+ if(for_display) {
+ // filename = Filename.display_name(filename);
+ }
+ CStrPtr quoted{g_shell_quote(filename.get())};
+ result += quoted.get();
+ break;
+ }
+ case 'F': // space-separated list of selected file names
+ for(auto& fi : files) {
+ auto filename = fi->path().toString();
+ if(for_display) {
+ // filename = Filename.display_name(filename);
+ }
+ CStrPtr quoted{g_shell_quote(filename.get())};
+ result += quoted.get();
+ result += ' ';
+ }
+ if(result[result.length() - 1] == ' ') { // remove trailing space
+ result.erase(result.length() - 1);
+ }
+ break;
+ case 'h': // hostname of the (first) URI
+ // FIXME: how to support this correctly?
+ // FIXME: currently we pass g_get_host_name()
+ result += g_get_host_name();
+ break;
+ case 'm': // mimetype of the (first) selected item
+ result += first_file->mimeType()->name();
+ break;
+ case 'M': // space-separated list of the mimetypes of the selected items
+ for(auto& fi : files) {
+ result += fi->mimeType()->name();
+ result += ' ';
+ }
+ break;
+ case 'n': // username of the (first) URI
+ // FIXME: how to support this correctly?
+ result += g_get_user_name();
+ break;
+ case 'o': // no-op operator which forces a singular form of execution when specified as first parameter,
+ case 'O': // no-op operator which forces a plural form of execution when specified as first parameter,
+ break;
+ case 'p': // port number of the (first) URI
+ // FIXME: how to support this correctly?
+ // result.append("0");
+ break;
+ case 's': // scheme of the (first) URI
+ result += first_file->path().uriScheme().get();
+ break;
+ case 'u': // (first) URI
+ result += first_file->path().uri().get();
+ break;
+ case 'U': // space-separated list of selected URIs
+ for(auto& fi : files) {
+ result += fi->path().uri().get();
+ result += ' ';
+ }
+ if(result[result.length() - 1] == ' ') { // remove trailing space
+ result.erase(result.length() - 1);
+ }
+ break;
+ case 'w': { // (first) basename without the extension
+ auto basename = first_file->name();
+ int pos = basename.rfind('.');
+ // FIXME: handle non-UTF8 filenames
+ if(pos != -1) {
+ basename.erase(pos, string::npos);
+ }
+ CStrPtr quoted{g_shell_quote(basename.c_str())};
+ result += quoted.get();
+ break;
+ }
+ case 'W': // space-separated list of basenames without their extension
+ for(auto& fi : files) {
+ auto basename = fi->name();
+ int pos = basename.rfind('.');
+ // FIXME: for_display ? Shell.quote(str) : str);
+ if(pos != -1) {
+ basename.erase(pos, string::npos);
+ }
+ CStrPtr quoted{g_shell_quote(basename.c_str())};
+ result += quoted.get();
+ result += ' ';
+ }
+ if(result[result.length() - 1] == ' ') { // remove trailing space
+ result.erase(result.length() - 1);
+ }
+ break;
+ case 'x': { // (first) extension
+ auto basename = first_file->name();
+ int pos = basename.rfind('.');
+ const char* ext = "";
+ if(pos >= 0) {
+ ext = basename.c_str() + pos + 1;
+ }
+ CStrPtr quoted{g_shell_quote(ext)};
+ result += quoted.get();
+ break;
+ }
+ case 'X': // space-separated list of extensions
+ for(auto& fi : files) {
+ auto basename = fi->name();
+ int pos = basename.rfind('.');
+ const char* ext = "";
+ if(pos >= 0) {
+ ext = basename.c_str() + pos + 1;
+ }
+ CStrPtr quoted{g_shell_quote(ext)};
+ result += quoted.get();
+ result += ' ';
+ }
+ if(result[result.length() - 1] == ' ') { // remove trailing space
+ result.erase(result.length() - 1);
+ }
+ break;
+ case '%': // the % character
+ result += '%';
+ break;
+ case '\0':
+ break;
+ }
+ }
+ else {
+ result += ch;
+ }
+ }
+ return result;
+}
+
+FileAction::FileAction(GKeyFile* kf): FileActionObject{kf}, target{FILE_ACTION_TARGET_CONTEXT} {
+ type = FileActionType::ACTION;
+
+ GErrorPtr err;
+ if(g_key_file_get_boolean(kf, "Desktop Entry", "TargetContext", &err)) { // default to true
+ target |= FILE_ACTION_TARGET_CONTEXT;
+ }
+ else if(!err) { // error means the key is abscent
+ target &= ~FILE_ACTION_TARGET_CONTEXT;
+ }
+ if(g_key_file_get_boolean(kf, "Desktop Entry", "TargetLocation", nullptr)) {
+ target |= FILE_ACTION_TARGET_LOCATION;
+ }
+ if(g_key_file_get_boolean(kf, "Desktop Entry", "TargetToolbar", nullptr)) {
+ target |= FILE_ACTION_TARGET_TOOLBAR;
+ }
+ toolbar_label = CStrPtr{g_key_file_get_locale_string(kf, "Desktop Entry", "ToolbarLabel", nullptr, nullptr)};
+
+ auto profile_names = CStrArrayPtr{g_key_file_get_string_list(kf, "Desktop Entry", "Profiles", nullptr, nullptr)};
+ if(profile_names != nullptr) {
+ for(auto profile_name = profile_names.get(); *profile_name; ++profile_name) {
+ // stdout.printf("%s", profile);
+ profiles.push_back(make_shared<FileActionProfile>(kf, *profile_name));
+ }
+ }
+}
+
+std::shared_ptr<FileActionProfile> FileAction::match(const FileInfoList& files) const {
+ //qDebug() << "FileAction.match: " << id.get();
+ if(hidden || !enabled) {
+ return nullptr;
+ }
+
+ if(!condition->match(files)) {
+ return nullptr;
+ }
+ for(const auto& profile : profiles) {
+ if(profile->match(files)) {
+ //qDebug() << " profile matched!\n\n";
+ return profile;
+ }
+ }
+ // stdout.printf("\n");
+ return nullptr;
+}
+
+FileActionMenu::FileActionMenu(GKeyFile* kf): FileActionObject{kf} {
+ type = FileActionType::MENU;
+ items_list = CStrArrayPtr{g_key_file_get_string_list(kf, "Desktop Entry", "ItemsList", nullptr, nullptr)};
+}
+
+bool FileActionMenu::match(const FileInfoList& files) const {
+ // stdout.printf("FileActionMenu.match: %s\n", id);
+ if(hidden || !enabled) {
+ return false;
+ }
+ if(!condition->match(files)) {
+ return false;
+ }
+ // stdout.printf("menu matched!: %s\n\n", id);
+ return true;
+}
+
+void FileActionMenu::cache_children(const FileInfoList& files, const char** items_list) {
+ for(; *items_list; ++items_list) {
+ const char* item_id_prefix = *items_list;
+ size_t len = strlen(item_id_prefix);
+ if(item_id_prefix[0] == '[' && item_id_prefix[len - 1] == ']') {
+ // runtime dynamic item list
+ char* output;
+ int exit_status;
+ string prefix{item_id_prefix + 1, len - 2}; // skip [ and ]
+ auto command = expand_str(prefix.c_str(), files);
+ if(g_spawn_command_line_sync(command.c_str(), &output, nullptr, &exit_status, nullptr) && exit_status == 0) {
+ CStrArrayPtr item_ids{g_strsplit(output, ";", -1)};
+ g_free(output);
+ cache_children(files, (const char**)item_ids.get());
+ }
+ }
+ else if(strcmp(item_id_prefix, "SEPARATOR") == 0) {
+ // separator item
+ cached_children.push_back(nullptr);
+ }
+ else {
+ CStrPtr item_id{g_strconcat(item_id_prefix, ".desktop", nullptr)};
+ auto it = all_actions.find(item_id.get());
+ if(it != all_actions.end()) {
+ auto child_action = it->second;
+ child_action->has_parent = true;
+ cached_children.push_back(child_action);
+ // stdout.printf("add child: %s to menu: %s\n", item_id, id);
+ }
+ }
+ }
+}
+
+std::shared_ptr<FileActionItem> FileActionItem::fromActionObject(std::shared_ptr<FileActionObject> action_obj, const FileInfoList& files) {
+ std::shared_ptr<FileActionItem> item;
+ if(action_obj->type == FileActionType::MENU) {
+ auto menu = static_pointer_cast<FileActionMenu>(action_obj);
+ if(menu->match(files)) {
+ item = make_shared<FileActionItem>(menu, files);
+ // eliminate empty menus
+ if(item->children.empty()) {
+ item = nullptr;
+ }
+ }
+ }
+ else {
+ // handle profiles here
+ auto action = static_pointer_cast<FileAction>(action_obj);
+ auto profile = action->match(files);
+ if(profile) {
+ item = make_shared<FileActionItem>(action, profile, files);
+ }
+ }
+ return item;
+}
+
+FileActionItem::FileActionItem(std::shared_ptr<FileAction> _action, std::shared_ptr<FileActionProfile> _profile, const FileInfoList& files):
+ FileActionItem{static_pointer_cast<FileActionObject>(_action), files} {
+ profile = _profile;
+}
+
+FileActionItem::FileActionItem(std::shared_ptr<FileActionMenu> menu, const FileInfoList& files):
+ FileActionItem{static_pointer_cast<FileActionObject>(menu), files} {
+ for(auto& action_obj : menu->cached_children) {
+ if(action_obj == nullptr) { // separator
+ children.push_back(nullptr);
+ }
+ else { // action item or menu
+ auto subitem = fromActionObject(action_obj, files);
+ if(subitem != nullptr) {
+ children.push_back(subitem);
+ }
+ }
+ }
+}
+
+FileActionItem::FileActionItem(std::shared_ptr<FileActionObject> _action, const FileInfoList& files) {
+ action = std::move(_action);
+ name = FileActionObject::expand_str(action->name.get(), files, true);
+ desc = FileActionObject::expand_str(action->desc.get(), files, true);
+ icon = FileActionObject::expand_str(action->icon.get(), files, false);
+}
+
+bool FileActionItem::launch(GAppLaunchContext* ctx, const FileInfoList& files, CStrPtr& output) const {
+ if(action->type == FileActionType::ACTION) {
+ if(profile != nullptr) {
+ profile->launch(ctx, files, output);
+ }
+ return true;
+ }
+ return false;
+}
+
+static void load_actions_from_dir(const char* dirname, const char* id_prefix) {
+ //qDebug() << "loading from: " << dirname << endl;
+ auto dir = g_dir_open(dirname, 0, nullptr);
+ if(dir != nullptr) {
+ for(;;) {
+ const char* name = g_dir_read_name(dir);
+ if(name == nullptr) {
+ break;
+ }
+ // found a file in file-manager/actions dir, get its full path
+ CStrPtr full_path{g_build_filename(dirname, name, nullptr)};
+ // stdout.printf("\nfound %s\n", full_path);
+
+ // see if it's a sub dir
+ if(g_file_test(full_path.get(), G_FILE_TEST_IS_DIR)) {
+ // load sub dirs recursively
+ CStrPtr new_id_prefix;
+ if(id_prefix) {
+ new_id_prefix = CStrPtr{g_strconcat(id_prefix, "-", name, nullptr)};
+ }
+ load_actions_from_dir(full_path.get(), id_prefix ? new_id_prefix.get() : name);
+ }
+ else if(g_str_has_suffix(name, ".desktop")) {
+ CStrPtr new_id_prefix;
+ if(id_prefix) {
+ new_id_prefix = CStrPtr{g_strconcat(id_prefix, "-", name, nullptr)};
+ }
+ const char* id = id_prefix ? new_id_prefix.get() : name;
+ // ensure that it's not already in the cache
+ if(all_actions.find(id) == all_actions.cend()) {
+ auto kf = g_key_file_new();
+ if(g_key_file_load_from_file(kf, full_path.get(), G_KEY_FILE_NONE, nullptr)) {
+ auto type = CStrPtr{g_key_file_get_string(kf, "Desktop Entry", "Type", nullptr)};
+ if(!type) {
+ continue;
+ }
+ std::shared_ptr<FileActionObject> action;
+ if(strcmp(type.get(), "Action") == 0) {
+ action = static_pointer_cast<FileActionObject>(make_shared<FileAction>(kf));
+ // stdout.printf("load action: %s\n", id);
+ }
+ else if(strcmp(type.get(), "Menu") == 0) {
+ action = static_pointer_cast<FileActionObject>(make_shared<FileActionMenu>(kf));
+ // stdout.printf("load menu: %s\n", id);
+ }
+ else {
+ continue;
+ }
+ action->setId(id);
+ all_actions.insert(make_pair(action->id.get(), action)); // add the id/action pair to hash table
+ // stdout.printf("add to cache %s\n", id);
+ }
+ g_key_file_free(kf);
+ }
+ else {
+ // stdout.printf("cache found for action: %s\n", id);
+ }
+ }
+ }
+ g_dir_close(dir);
+ }
+}
+
+void file_actions_set_desktop_env(const char* env) {
+ desktop_env = env;
+}
+
+static void load_all_actions() {
+ all_actions.clear();
+ auto dirs = g_get_system_data_dirs();
+ for(auto dir = dirs; *dir; ++dir) {
+ CStrPtr dir_path{g_build_filename(*dir, "file-manager/actions", nullptr)};
+ load_actions_from_dir(dir_path.get(), nullptr);
+ }
+ CStrPtr dir_path{g_build_filename(g_get_user_data_dir(), "file-manager/actions", nullptr)};
+ load_actions_from_dir(dir_path.get(), nullptr);
+ actions_loaded = true;
+}
+
+bool FileActionItem::compare_items(std::shared_ptr<const FileActionItem> a, std::shared_ptr<const FileActionItem> b)
+{
+ // first get the list of level-zero item names (http://www.nautilus-actions.org/?q=node/377)
+ static QStringList itemNamesList;
+ static bool level_zero_checked = false;
+ if(!level_zero_checked) {
+ level_zero_checked = true;
+ auto level_zero = CStrPtr{g_build_filename(g_get_user_data_dir(),
+ "file-manager/actions/level-zero.directory", nullptr)};
+ if(g_file_test(level_zero.get(), G_FILE_TEST_IS_REGULAR)) {
+ GKeyFile* kf = g_key_file_new();
+ if(g_key_file_load_from_file(kf, level_zero.get(), G_KEY_FILE_NONE, nullptr)) {
+ auto itemsList = CStrArrayPtr{g_key_file_get_string_list(kf,
+ "Desktop Entry",
+ "ItemsList", nullptr, nullptr)};
+ if(itemsList) {
+ for(uint i = 0; i < g_strv_length(itemsList.get()); ++i) {
+ CStrPtr desktop_file_name{g_strconcat(itemsList.get()[i], ".desktop", nullptr)};
+ auto desktop_file = CStrPtr{g_build_filename(g_get_user_data_dir(),
+ "file-manager/actions",
+ desktop_file_name.get(), nullptr)};
+ GKeyFile* desktop_file_key = g_key_file_new();
+ if(g_key_file_load_from_file(desktop_file_key, desktop_file.get(), G_KEY_FILE_NONE, nullptr)) {
+ auto actionName = CStrPtr{g_key_file_get_string(desktop_file_key,
+ "Desktop Entry",
+ "Name", NULL)};
+ if(actionName) {
+ itemNamesList << QString::fromUtf8(actionName.get());
+ }
+ }
+ g_key_file_free(desktop_file_key);
+ }
+ }
+ }
+ g_key_file_free(kf);
+ }
+ }
+ if(!itemNamesList.isEmpty()) {
+ int first = itemNamesList.indexOf(QString::fromStdString(a->get_name()));
+ int second = itemNamesList.indexOf(QString::fromStdString(b->get_name()));
+ if(first > -1) {
+ if(second > -1) {
+ return (first < second);
+ }
+ else {
+ return true; // list items have priority
+ }
+ }
+ else if(second > -1) {
+ return false;
+ }
+ }
+ return (a->get_name().compare(b->get_name()) < 0);
+}
+
+FileActionItemList FileActionItem::get_actions_for_files(const FileInfoList& files) {
+ if(!actions_loaded) {
+ load_all_actions();
+ }
+
+ // Iterate over all actions to establish association between parent menu
+ // and children actions, and to find out toplevel ones which are not
+ // attached to any parent menu
+ for(auto& item : all_actions) {
+ auto& action_obj = item.second;
+ // stdout.printf("id = %s\n", action_obj.id);
+ if(action_obj->type == FileActionType::MENU) { // this is a menu
+ auto menu = static_pointer_cast<FileActionMenu>(action_obj);
+ // stdout.printf("menu: %s\n", menu.name);
+ // associate child items with menus
+ menu->cache_children(files, (const char**)menu->items_list.get());
+ }
+ }
+
+ // Output the menus
+ FileActionItemList items;
+
+ for(auto& item : all_actions) {
+ auto& action_obj = item.second;
+ // only output toplevel items here
+ if(action_obj->has_parent == false) { // this is a toplevel item
+ auto item = FileActionItem::fromActionObject(action_obj, files);
+ if(item != nullptr) {
+ items.push_back(item);
+ }
+ }
+ }
+
+ // cleanup temporary data cached during menu generation
+ for(auto& item : all_actions) {
+ auto& action_obj = item.second;
+ action_obj->has_parent = false;
+ if(action_obj->type == FileActionType::MENU) {
+ auto menu = static_pointer_cast<FileActionMenu>(action_obj);
+ menu->cached_children.clear();
+ }
+ }
+
+ std::sort(items.begin(), items.end(), compare_items);
+ return items;
+}
+
+} // namespace Fm
--- /dev/null
+#ifndef FILEACTION_H
+#define FILEACTION_H
+
+#include <glib.h>
+#include <string>
+
+#include "../core/fileinfo.h"
+#include "fileactioncondition.h"
+#include "fileactionprofile.h"
+
+namespace Fm {
+
+enum class FileActionType {
+ NONE,
+ ACTION,
+ MENU
+};
+
+
+enum FileActionTarget {
+ FILE_ACTION_TARGET_NONE,
+ FILE_ACTION_TARGET_CONTEXT = 1,
+ FILE_ACTION_TARGET_LOCATION = 1 << 1,
+ FILE_ACTION_TARGET_TOOLBAR = 1 << 2
+};
+
+
+class FileActionObject {
+public:
+ explicit FileActionObject();
+
+ explicit FileActionObject(GKeyFile* kf);
+
+ virtual ~FileActionObject();
+
+ void setId(const char* _id) {
+ id = CStrPtr{g_strdup(_id)};
+ }
+
+ static bool is_plural_exec(const char* exec);
+
+ static std::string expand_str(const char* templ, const FileInfoList& files, bool for_display = false, std::shared_ptr<const FileInfo> first_file = nullptr);
+
+ FileActionType type;
+ CStrPtr id;
+ CStrPtr name;
+ CStrPtr tooltip;
+ CStrPtr icon;
+ CStrPtr desc;
+ bool enabled;
+ bool hidden;
+ CStrPtr suggested_shortcut;
+ std::unique_ptr<FileActionCondition> condition;
+
+ // values cached during menu generation
+ bool has_parent;
+};
+
+
+class FileAction: public FileActionObject {
+public:
+
+ FileAction(GKeyFile* kf);
+
+ std::shared_ptr<FileActionProfile> match(const FileInfoList& files) const;
+
+ int target; // bitwise or of FileActionTarget
+ CStrPtr toolbar_label;
+
+ // FIXME: currently we don't support dynamic profiles
+ std::vector<std::shared_ptr<FileActionProfile>> profiles;
+};
+
+
+class FileActionMenu : public FileActionObject {
+public:
+
+ FileActionMenu(GKeyFile* kf);
+
+ bool match(const FileInfoList &files) const;
+
+ // called during menu generation
+ void cache_children(const FileInfoList &files, const char** items_list);
+
+ CStrArrayPtr items_list;
+
+ // values cached during menu generation
+ std::vector<std::shared_ptr<FileActionObject>> cached_children;
+};
+
+
+class FileActionItem {
+public:
+
+ static std::shared_ptr<FileActionItem> fromActionObject(std::shared_ptr<FileActionObject> action_obj, const FileInfoList &files);
+
+ FileActionItem(std::shared_ptr<FileAction> _action, std::shared_ptr<FileActionProfile> _profile, const FileInfoList& files);
+
+ FileActionItem(std::shared_ptr<FileActionMenu> menu, const FileInfoList& files);
+
+ FileActionItem(std::shared_ptr<FileActionObject> _action, const FileInfoList& files);
+
+ const std::string& get_name() const {
+ return name;
+ }
+
+ const std::string& get_desc() const {
+ return desc;
+ }
+
+ const std::string& get_icon() const {
+ return icon;
+ }
+
+ const char* get_id() const {
+ return action->id.get();
+ }
+
+ FileActionTarget get_target() const {
+ if(action->type == FileActionType::ACTION) {
+ return FileActionTarget(static_cast<FileAction*>(action.get())->target);
+ }
+ return FILE_ACTION_TARGET_CONTEXT;
+ }
+
+ bool is_menu() const {
+ return (action->type == FileActionType::MENU);
+ }
+
+ bool is_action() const {
+ return (action->type == FileActionType::ACTION);
+ }
+
+ bool launch(GAppLaunchContext *ctx, const FileInfoList &files, CStrPtr &output) const;
+
+ const std::vector<std::shared_ptr<const FileActionItem>>& get_sub_items() const {
+ return children;
+ }
+
+ static bool compare_items(std::shared_ptr<const FileActionItem> a, std::shared_ptr<const FileActionItem> b);
+ static std::vector<std::shared_ptr<const FileActionItem>> get_actions_for_files(const FileInfoList& files);
+
+ std::string name;
+ std::string desc;
+ std::string icon;
+ std::shared_ptr<FileActionObject> action;
+ std::shared_ptr<FileActionProfile> profile; // only used by action item
+ std::vector<std::shared_ptr<const FileActionItem>> children; // only used by menu
+};
+
+typedef std::vector<std::shared_ptr<const FileActionItem>> FileActionItemList;
+
+} // namespace Fm
+
+
+#endif // FILEACTION_H
--- /dev/null
+#include "fileactioncondition.h"
+#include "fileaction.h"
+#include <string>
+
+
+using namespace std;
+
+namespace Fm {
+
+FileActionCondition::FileActionCondition(GKeyFile *kf, const char* group) {
+ only_show_in = CStrArrayPtr{g_key_file_get_string_list(kf, group, "OnlyShowIn", nullptr, nullptr)};
+ not_show_in = CStrArrayPtr{g_key_file_get_string_list(kf, group, "NotShowIn", nullptr, nullptr)};
+ try_exec = CStrPtr{g_key_file_get_string(kf, group, "TryExec", nullptr)};
+ show_if_registered = CStrPtr{g_key_file_get_string(kf, group, "ShowIfRegistered", nullptr)};
+ show_if_true = CStrPtr{g_key_file_get_string(kf, group, "ShowIfTrue", nullptr)};
+ show_if_running = CStrPtr{g_key_file_get_string(kf, group, "ShowIfRunning", nullptr)};
+ mime_types = CStrArrayPtr{g_key_file_get_string_list(kf, group, "MimeTypes", nullptr, nullptr)};
+ base_names = CStrArrayPtr{g_key_file_get_string_list(kf, group, "Basenames", nullptr, nullptr)};
+ match_case = g_key_file_get_boolean(kf, group, "Matchcase", nullptr);
+
+ CStrPtr selection_count_str{g_key_file_get_string(kf, group, "SelectionCount", nullptr)};
+ if(selection_count_str != nullptr) {
+ switch(selection_count_str[0]) {
+ case '<':
+ case '>':
+ case '=':
+ selection_count_cmp = selection_count_str[0];
+ selection_count = atoi(selection_count_str.get() + 1);
+ break;
+ default:
+ selection_count_cmp = '>';
+ selection_count = 0;
+ break;
+ }
+ }
+ else {
+ selection_count_cmp = '>';
+ selection_count = 0;
+ }
+
+ schemes = CStrArrayPtr{g_key_file_get_string_list(kf, group, "Schemes", nullptr, nullptr)};
+ folders = CStrArrayPtr{g_key_file_get_string_list(kf, group, "Folders", nullptr, nullptr)};
+ auto caps = CStrArrayPtr{g_key_file_get_string_list(kf, group, "Capabilities", nullptr, nullptr)};
+
+ // FIXME: implement Capabilities support
+
+}
+
+bool FileActionCondition::match_try_exec(const FileInfoList& files) {
+ if(try_exec != nullptr) {
+ // stdout.printf(" TryExec: %s\n", try_exec);
+ CStrPtr exec_path{g_find_program_in_path(FileActionObject::expand_str(try_exec.get(), files).c_str())};
+ if(!g_file_test(exec_path.get(), G_FILE_TEST_IS_EXECUTABLE)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool FileActionCondition::match_show_if_registered(const FileInfoList& files) {
+ if(show_if_registered != nullptr) {
+ // stdout.printf(" ShowIfRegistered: %s\n", show_if_registered);
+ auto service = FileActionObject::expand_str(show_if_registered.get(), files);
+ // References:
+ // http://people.freedesktop.org/~david/eggdbus-20091014/eggdbus-interface-org.freedesktop.DBus.html#eggdbus-method-org.freedesktop.DBus.NameHasOwner
+ // glib source code: gio/tests/gdbus-names.c
+ auto con = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr);
+ auto result = g_dbus_connection_call_sync(con,
+ "org.freedesktop.DBus",
+ "/org/freedesktop/DBus",
+ "org.freedesktop.DBus",
+ "NameHasOwner",
+ g_variant_new("(s)", service.c_str()),
+ g_variant_type_new("(b)"),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1, nullptr, nullptr);
+ bool name_has_owner;
+ g_variant_get(result, "(b)", &name_has_owner);
+ g_variant_unref(result);
+ // stdout.printf("check if service: %s is in use: %d\n", service, (int)name_has_owner);
+ if(!name_has_owner) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool FileActionCondition::match_show_if_true(const FileInfoList& files) {
+ if(show_if_true != nullptr) {
+ auto cmd = FileActionObject::expand_str(show_if_true.get(), files);
+ int exit_status;
+ // FIXME: Process.spawn cannot handle shell commands. Use Posix.system() instead.
+ //if(!Process.spawn_command_line_sync(cmd, nullptr, nullptr, out exit_status)
+ // || exit_status != 0)
+ // return false;
+ exit_status = system(cmd.c_str());
+ if(exit_status != 0) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool FileActionCondition::match_show_if_running(const FileInfoList& files) {
+ if(show_if_running != nullptr) {
+ auto process_name = FileActionObject::expand_str(show_if_running.get(), files);
+ CStrPtr pgrep{g_find_program_in_path("pgrep")};
+ bool running = false;
+ // pgrep is not fully portable, but we don't have better options here
+ if(pgrep != nullptr) {
+ int exit_status;
+ // cmd = "$pgrep -x '$process_name'"
+ string cmd = pgrep.get();
+ cmd += " -x \'";
+ cmd += process_name;
+ cmd += "\'";
+ if(g_spawn_command_line_sync(cmd.c_str(), nullptr, nullptr, &exit_status, nullptr)) {
+ if(exit_status == 0) {
+ running = true;
+ }
+ }
+ }
+ if(!running) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool FileActionCondition::match_mime_type(const FileInfoList& files, const char* type, bool negated) {
+ // stdout.printf("match_mime_type: %s, neg: %d\n", type, (int)negated);
+
+ if(strcmp(type, "all/all") == 0 || strcmp(type, "*") == 0) {
+ return negated ? false : true;
+ }
+ else if(strcmp(type, "all/allfiles") == 0) {
+ // see if all fileinfos are files
+ if(negated) { // all fileinfos should not be files
+ for(auto& fi: files) {
+ if(!fi->isDir()) { // at least 1 of the fileinfos is a file.
+ return false;
+ }
+ }
+ }
+ else { // all fileinfos should be files
+ for(auto& fi: files) {
+ if(fi->isDir()) { // at least 1 of the fileinfos is a file.
+ return false;
+ }
+ }
+ }
+ }
+ else if(g_str_has_suffix(type, "/*")) {
+ // check if all are subtypes of allowed_type
+ string prefix{type};
+ prefix.erase(prefix.length() - 1); // remove the last char
+ if(negated) { // all files should not have the prefix
+ for(auto& fi: files) {
+ if(g_str_has_prefix(fi->mimeType()->name(), prefix.c_str())) {
+ return false;
+ }
+ }
+ }
+ else { // all files should have the prefix
+ for(auto& fi: files) {
+ if(!g_str_has_prefix(fi->mimeType()->name(), prefix.c_str())) {
+ return false;
+ }
+ }
+ }
+ }
+ else {
+ if(negated) { // all files should not be of the type
+ for(auto& fi: files) {
+ if(strcmp(fi->mimeType()->name(),type) == 0) {
+ // if(ContentType.is_a(type, fi.get_mime_type().get_type())) {
+ return false;
+ }
+ }
+ }
+ else { // all files should be of the type
+ for(auto& fi: files) {
+ // stdout.printf("get_type: %s, type: %s\n", fi.get_mime_type().get_type(), type);
+ if(strcmp(fi->mimeType()->name(),type) != 0) {
+ // if(!ContentType.is_a(type, fi.get_mime_type().get_type())) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+}
+
+bool FileActionCondition::match_mime_types(const FileInfoList& files) {
+ if(mime_types != nullptr) {
+ bool allowed = false;
+ // FIXME: this is inefficient, but easier to implement
+ // check if all of the mime_types are allowed
+ for(auto mime_type = mime_types.get(); *mime_type; ++mime_type) {
+ const char* allowed_type = *mime_type;
+ const char* type;
+ bool negated;
+ if(allowed_type[0] == '!') {
+ type = allowed_type + 1;
+ negated = true;
+ }
+ else {
+ type = allowed_type;
+ negated = false;
+ }
+
+ if(negated) { // negated mime_type rules are ANDed
+ bool type_is_allowed = match_mime_type(files, type, negated);
+ if(!type_is_allowed) { // so any mismatch is not allowed
+ return false;
+ }
+ }
+ else { // other mime_type rules are ORed
+ // matching any one of the mime_type is enough
+ if(!allowed) { // if no rule is matched yet
+ allowed = match_mime_type(files, type, false);
+ }
+ }
+ }
+ return allowed;
+ }
+ return true;
+}
+
+bool FileActionCondition::match_base_name(const FileInfoList& files, const char* base_name, bool negated) {
+ // see if all files has the base_name
+ // FIXME: this is inefficient, some optimization is needed later
+ GPatternSpec* pattern;
+ if(match_case) {
+ pattern = g_pattern_spec_new(base_name);
+ }
+ else {
+ CStrPtr case_fold{g_utf8_casefold(base_name, -1)};
+ pattern = g_pattern_spec_new(case_fold.get()); // FIXME: is this correct?
+ }
+ for(auto& fi: files) {
+ const char* name = fi->name().c_str();
+ if(match_case) {
+ if(g_pattern_match_string(pattern, name)) {
+ // at least 1 file has the base_name
+ if(negated) {
+ return false;
+ }
+ }
+ else {
+ // at least 1 file does not has the scheme
+ if(!negated) {
+ return false;
+ }
+ }
+ }
+ else {
+ CStrPtr case_fold{g_utf8_casefold(name, -1)};
+ if(g_pattern_match_string(pattern, case_fold.get())) {
+ // at least 1 file has the base_name
+ if(negated) {
+ return false;
+ }
+ }
+ else {
+ // at least 1 file does not has the scheme
+ if(!negated) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+}
+
+bool FileActionCondition::match_base_names(const FileInfoList& files) {
+ if(base_names != nullptr) {
+ bool allowed = false;
+ // FIXME: this is inefficient, but easier to implement
+ // check if all of the base_names are allowed
+ for(auto it = base_names.get(); *it; ++it) {
+ auto allowed_name = *it;
+ const char* name;
+ bool negated;
+ if(allowed_name[0] == '!') {
+ name = allowed_name + 1;
+ negated = true;
+ }
+ else {
+ name = allowed_name;
+ negated = false;
+ }
+
+ if(negated) { // negated base_name rules are ANDed
+ bool name_is_allowed = match_base_name(files, name, negated);
+ if(!name_is_allowed) { // so any mismatch is not allowed
+ return false;
+ }
+ }
+ else { // other base_name rules are ORed
+ // matching any one of the base_name is enough
+ if(!allowed) { // if no rule is matched yet
+ allowed = match_base_name(files, name, false);
+ }
+ }
+ }
+ return allowed;
+ }
+ return true;
+}
+
+bool FileActionCondition::match_scheme(const FileInfoList& files, const char* scheme, bool negated) {
+ // FIXME: this is inefficient, some optimization is needed later
+ // see if all files has the scheme
+ for(auto& fi: files) {
+ if(fi->path().hasUriScheme(scheme)) {
+ // at least 1 file has the scheme
+ if(negated) {
+ return false;
+ }
+ }
+ else {
+ // at least 1 file does not has the scheme
+ if(!negated) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+bool FileActionCondition::match_schemes(const FileInfoList& files) {
+ if(schemes != nullptr) {
+ bool allowed = false;
+ // FIXME: this is inefficient, but easier to implement
+ // check if all of the schemes are allowed
+ for(auto it = schemes.get(); *it; ++it) {
+ auto allowed_scheme = *it;
+ const char* scheme;
+ bool negated;
+ if(allowed_scheme[0] == '!') {
+ scheme = allowed_scheme + 1;
+ negated = true;
+ }
+ else {
+ scheme = allowed_scheme;
+ negated = false;
+ }
+
+ if(negated) { // negated scheme rules are ANDed
+ bool scheme_is_allowed = match_scheme(files, scheme, negated);
+ if(!scheme_is_allowed) { // so any mismatch is not allowed
+ return false;
+ }
+ }
+ else { // other scheme rules are ORed
+ // matching any one of the scheme is enough
+ if(!allowed) { // if no rule is matched yet
+ allowed = match_scheme(files, scheme, false);
+ }
+ }
+ }
+ return allowed;
+ }
+ return true;
+}
+
+bool FileActionCondition::match_folder(const FileInfoList& files, const char* folder, bool negated) {
+ // trailing /* should always be implied.
+ // FIXME: this is inefficient, some optimization is needed later
+ GPatternSpec* pattern;
+ if(g_str_has_suffix(folder, "/*")) {
+ pattern = g_pattern_spec_new(folder);
+ }
+ else {
+ auto pat_str = string(folder) + "/*";
+ pattern = g_pattern_spec_new(pat_str.c_str());
+ }
+ for(auto& fi: files) {
+ auto dirname = fi->dirPath().toString();
+ if(g_pattern_match_string(pattern, dirname.get())) { // at least 1 file is in the folder
+ if(negated) {
+ return false;
+ }
+ }
+ else {
+ if(!negated) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+bool FileActionCondition::match_folders(const FileInfoList& files) {
+ if(folders != nullptr) {
+ bool allowed = false;
+ // FIXME: this is inefficient, but easier to implement
+ // check if all of the schemes are allowed
+ for(auto it = folders.get(); *it; ++it) {
+ auto allowed_folder = *it;
+ const char* folder;
+ bool negated;
+ if(allowed_folder[0] == '!') {
+ folder = allowed_folder + 1;
+ negated = true;
+ }
+ else {
+ folder = allowed_folder;
+ negated = false;
+ }
+
+ if(negated) { // negated folder rules are ANDed
+ bool folder_is_allowed = match_folder(files, folder, negated);
+ if(!folder_is_allowed) { // so any mismatch is not allowed
+ return false;
+ }
+ }
+ else { // other folder rules are ORed
+ // matching any one of the folder is enough
+ if(!allowed) { // if no rule is matched yet
+ allowed = match_folder(files, folder, false);
+ }
+ }
+ }
+ return allowed;
+ }
+ return true;
+}
+
+bool FileActionCondition::match_selection_count(const FileInfoList& files) {
+ const int n_files = files.size();
+ switch(selection_count_cmp) {
+ case '<':
+ if(n_files >= selection_count) {
+ return false;
+ }
+ break;
+ case '=':
+ if(n_files != selection_count) {
+ return false;
+ }
+ break;
+ case '>':
+ if(n_files <= selection_count) {
+ return false;
+ }
+ break;
+ }
+ return true;
+}
+
+bool FileActionCondition::match_capabilities(const FileInfoList& /*files*/) {
+ // TODO
+ return true;
+}
+
+bool FileActionCondition::match(const FileInfoList& files) {
+ // all of the condition are combined with AND
+ // So, if any one of the conditions is not matched, we quit.
+
+ // TODO: OnlyShowIn, NotShowIn
+ if(!match_try_exec(files)) {
+ return false;
+ }
+
+ if(!match_mime_types(files)) {
+ return false;
+ }
+ if(!match_base_names(files)) {
+ return false;
+ }
+ if(!match_selection_count(files)) {
+ return false;
+ }
+ if(!match_schemes(files)) {
+ return false;
+ }
+ if(!match_folders(files)) {
+ return false;
+ }
+ // TODO: Capabilities
+ // currently, due to limitations of Fm.FileInfo, this cannot
+ // be implemanted correctly.
+ if(!match_capabilities(files)) {
+ return false;
+ }
+
+ if(!match_show_if_registered(files)) {
+ return false;
+ }
+ if(!match_show_if_true(files)) {
+ return false;
+ }
+ if(!match_show_if_running(files)) {
+ return false;
+ }
+
+ return true;
+}
+
+
+} // namespace Fm
--- /dev/null
+#ifndef FILEACTIONCONDITION_H
+#define FILEACTIONCONDITION_H
+
+#include <glib.h>
+#include "../core/gioptrs.h"
+#include "../core/fileinfo.h"
+
+namespace Fm {
+
+// FIXME: we can use getgroups() to get groups of current process
+// then, call stat() and stat.st_gid to handle capabilities
+// in this way, we don't have to call euidaccess
+
+enum class FileActionCapability {
+ OWNER = 0,
+ READABLE = 1 << 1,
+ WRITABLE = 1 << 2,
+ EXECUTABLE = 1 << 3,
+ LOCAL = 1 << 4
+};
+
+
+class FileActionCondition {
+public:
+ explicit FileActionCondition(GKeyFile* kf, const char* group);
+
+#if 0
+ bool match_base_name_(const FileInfoList& files, const char* allowed_base_name) {
+ // all files should match the base_name pattern.
+ bool allowed = true;
+ if(allowed_base_name.index_of_char('*') >= 0) {
+ string allowed_base_name_ci;
+ if(!match_case) {
+ allowed_base_name_ci = allowed_base_name.casefold(); // FIXME: is this ok?
+ allowed_base_name = allowed_base_name_ci;
+ }
+ var pattern = new PatternSpec(allowed_base_name);
+ foreach(unowned FileInfo fi in files) {
+ unowned string name = fi.get_name();
+ if(match_case) {
+ if(!pattern.match_string(name)) {
+ allowed = false;
+ break;
+ }
+ }
+ else {
+ if(!pattern.match_string(name.casefold())) {
+ allowed = false;
+ break;
+ }
+ }
+ }
+ }
+ else {
+ foreach(unowned FileInfo fi in files) {
+ unowned string name = fi.get_name();
+ if(match_case) {
+ if(allowed_base_name != name) {
+ allowed = false;
+ break;
+ }
+ }
+ else {
+ if(allowed_base_name.collate(name) != 0) {
+ allowed = false;
+ break;
+ }
+ }
+ }
+ }
+ return allowed;
+ }
+#endif
+
+ bool match_try_exec(const FileInfoList& files);
+
+ bool match_show_if_registered(const FileInfoList& files);
+
+ bool match_show_if_true(const FileInfoList& files);
+
+ bool match_show_if_running(const FileInfoList& files);
+
+ bool match_mime_type(const FileInfoList& files, const char* type, bool negated);
+
+ bool match_mime_types(const FileInfoList& files);
+
+ bool match_base_name(const FileInfoList& files, const char* base_name, bool negated);
+
+ bool match_base_names(const FileInfoList& files);
+
+ static bool match_scheme(const FileInfoList& files, const char* scheme, bool negated);
+
+ bool match_schemes(const FileInfoList& files);
+
+ static bool match_folder(const FileInfoList& files, const char* folder, bool negated);
+
+ bool match_folders(const FileInfoList& files);
+
+ bool match_selection_count(const FileInfoList &files);
+
+ bool match_capabilities(const FileInfoList& files);
+
+ bool match(const FileInfoList& files);
+
+ CStrArrayPtr only_show_in;
+ CStrArrayPtr not_show_in;
+ CStrPtr try_exec;
+ CStrPtr show_if_registered;
+ CStrPtr show_if_true;
+ CStrPtr show_if_running;
+ CStrArrayPtr mime_types;
+ CStrArrayPtr base_names;
+ bool match_case;
+ char selection_count_cmp;
+ int selection_count;
+ CStrArrayPtr schemes;
+ CStrArrayPtr folders;
+ FileActionCapability capabilities;
+};
+
+}
+
+#endif // FILEACTIONCONDITION_H
--- /dev/null
+#include "fileactionprofile.h"
+#include "fileaction.h"
+#include <QDebug>
+
+using namespace std;
+
+namespace Fm {
+
+FileActionProfile::FileActionProfile(GKeyFile *kf, const char* profile_name) {
+ id = profile_name;
+ std::string group_name = "X-Action-Profile " + id;
+ name = CStrPtr{g_key_file_get_string(kf, group_name.c_str(), "Name", nullptr)};
+ exec = CStrPtr{g_key_file_get_string(kf, group_name.c_str(), "Exec", nullptr)};
+ // stdout.printf("id: %s, Exec: %s\n", id, exec);
+
+ path = CStrPtr{g_key_file_get_string(kf, group_name.c_str(), "Path", nullptr)};
+ auto s = CStrPtr{g_key_file_get_string(kf, group_name.c_str(), "ExecutionMode", nullptr)};
+ if(s) {
+ if(strcmp(s.get(), "Normal") == 0) {
+ exec_mode = FileActionExecMode::NORMAL;
+ }
+ else if(strcmp(s.get(), "Terminal") == 0) {
+ exec_mode = FileActionExecMode::TERMINAL;
+ }
+ else if(strcmp(s.get(), "Embedded") == 0) {
+ exec_mode = FileActionExecMode::EMBEDDED;
+ }
+ else if(strcmp(s.get(), "DisplayOutput") == 0) {
+ exec_mode = FileActionExecMode::DISPLAY_OUTPUT;
+ }
+ else {
+ exec_mode = FileActionExecMode::NORMAL;
+ }
+ }
+
+ startup_notify = g_key_file_get_boolean(kf, group_name.c_str(), "StartupNotify", nullptr);
+ startup_wm_class = CStrPtr{g_key_file_get_string(kf, group_name.c_str(), "StartupWMClass", nullptr)};
+ exec_as = CStrPtr{g_key_file_get_string(kf, group_name.c_str(), "ExecuteAs", nullptr)};
+
+ condition = make_shared<FileActionCondition>(kf, group_name.c_str());
+}
+
+
+bool FileActionProfile::launch_once(GAppLaunchContext* /*ctx*/, std::shared_ptr<const FileInfo> first_file, const FileInfoList& files, CStrPtr& output) {
+ if(exec == nullptr) {
+ return false;
+ }
+ auto exec_cmd = FileActionObject::expand_str(exec.get(), files, false, first_file);
+ bool ret = false;
+ if(exec_mode == FileActionExecMode::DISPLAY_OUTPUT) {
+ int exit_status;
+ char* output_buf = nullptr;
+ ret = g_spawn_command_line_sync(exec_cmd.c_str(), &output_buf, nullptr, &exit_status, nullptr);
+ if(ret) {
+ ret = (exit_status == 0);
+ }
+ output = CStrPtr{output_buf};
+ }
+ else {
+ /*
+ AppInfoCreateFlags flags = AppInfoCreateFlags.NONE;
+ if(startup_notify)
+ flags |= AppInfoCreateFlags.SUPPORTS_STARTUP_NOTIFICATION;
+ if(exec_mode == FileActionExecMode::TERMINAL ||
+ exec_mode == FileActionExecMode::EMBEDDED)
+ flags |= AppInfoCreateFlags.NEEDS_TERMINAL;
+ GLib.AppInfo app = Fm.AppInfo.create_from_commandline(exec, nullptr, flags);
+ stdout.printf("Execute command line: %s\n\n", exec);
+ ret = app.launch(nullptr, ctx);
+ */
+
+ // NOTE: we cannot use GAppInfo here since GAppInfo does
+ // command line parsing which involving %u, %f, and other
+ // code defined in desktop entry spec.
+ // This may conflict with DES EMA parameters.
+ // FIXME: so how to handle this cleaner?
+ // Maybe we should leave all %% alone and don't translate
+ // them to %. Then GAppInfo will translate them to %, not
+ // codes specified in DES.
+ ret = g_spawn_command_line_async(exec_cmd.c_str(), nullptr);
+ }
+ return ret;
+}
+
+
+bool FileActionProfile::launch(GAppLaunchContext* ctx, const FileInfoList& files, CStrPtr& output) {
+ bool plural_form = FileActionObject::is_plural_exec(exec.get());
+ bool ret;
+ if(plural_form) { // plural form command, handle all files at a time
+ ret = launch_once(ctx, files.front(), files, output);
+ }
+ else { // singular form command, run once for each file
+ GString* all_output = g_string_sized_new(1024);
+ bool show_output = false;
+ for(auto& fi: files) {
+ CStrPtr one_output;
+ launch_once(ctx, fi, files, one_output);
+ if(one_output) {
+ show_output = true;
+ // FIXME: how to handle multiple output std::strings properly?
+ g_string_append(all_output, one_output.get());
+ g_string_append(all_output, "\n");
+ }
+ }
+ if(show_output) {
+ output = CStrPtr{g_string_free(all_output, false)};
+ }
+ else {
+ g_string_free(all_output, true);
+ }
+ ret = true;
+ }
+ return ret;
+}
+
+bool FileActionProfile::match(FileInfoList files) {
+ // stdout.printf(" match profile: %s\n", id);
+ return condition->match(files);
+}
+
+}
--- /dev/null
+#ifndef FILEACTIONPROFILE_H
+#define FILEACTIONPROFILE_H
+
+
+#include <string>
+#include <glib.h>
+#include <gio/gio.h>
+
+#include "../core/fileinfo.h"
+#include "fileactioncondition.h"
+
+namespace Fm {
+
+enum class FileActionExecMode {
+ NORMAL,
+ TERMINAL,
+ EMBEDDED,
+ DISPLAY_OUTPUT
+};
+
+class FileActionProfile {
+public:
+ explicit FileActionProfile(GKeyFile* kf, const char* profile_name);
+
+ bool launch_once(GAppLaunchContext* ctx, std::shared_ptr<const FileInfo> first_file, const FileInfoList& files, CStrPtr& output);
+
+ bool launch(GAppLaunchContext* ctx, const FileInfoList& files, CStrPtr& output);
+
+ bool match(FileInfoList files);
+
+ std::string id;
+ CStrPtr name;
+ CStrPtr exec;
+ CStrPtr path;
+ FileActionExecMode exec_mode;
+ bool startup_notify;
+ CStrPtr startup_wm_class;
+ CStrPtr exec_as;
+
+ std::shared_ptr<FileActionCondition> condition;
+};
+
+} // namespace Fm
+
+#endif // FILEACTIONPROFILE_H
--- /dev/null
+/*
+ * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#include "dirtreemodel.h"
+#include "dirtreemodelitem.h"
+#include <QDebug>
+#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<Fm::FileInfoJob*>(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<size_t>(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<size_t>(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<const Fm::FileInfo> 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<DirTreeModelItem*>(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<const Fm::FileInfo> 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
--- /dev/null
+/*
+ * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef FM_DIRTREEMODEL_H
+#define FM_DIRTREEMODEL_H
+
+#include "libfmqtglobals.h"
+#include <QModelIndex>
+#include <QAbstractItemModel>
+#include <QIcon>
+#include <QList>
+#include <QSharedPointer>
+#include <libfm/fm.h>
+#include <vector>
+
+#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<const Fm::FileInfo> 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<const Fm::FileInfo> root);
+
+ DirTreeModelItem* itemFromPath(const Fm::FilePath& path) const;
+ DirTreeModelItem* itemFromIndex(const QModelIndex& index) const;
+ QModelIndex indexFromItem(DirTreeModelItem* item) const;
+
+private:
+ bool showHidden_;
+ std::vector<DirTreeModelItem*> rootItems_;
+};
+
+}
+
+#endif // FM_DIRTREEMODEL_H
--- /dev/null
+/*
+ * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#include "dirtreemodelitem.h"
+#include "dirtreemodel.h"
+#include "icontheme.h"
+#include <QDebug>
+
+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<const Fm::FileInfo> 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<Fm::FileInfoPair>& 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<const Fm::FileInfo> fi) {
+ // qDebug() << "insertFileInfo: " << fm_file_info_get_disp_name(fi);
+ DirTreeModelItem* item = new DirTreeModelItem(std::move(fi), model_);
+ insertItem(item);
+ return item;
+}
+
+/* Add file info to parent node to proper position. */
+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<const Fm::FileInfo>& a, const std::shared_ptr<const Fm::FileInfo>& b) {
+ return QString::localeAwareCompare(a->displayName(), b->displayName()) < 0;
+ });
+ // insert the files into the visible children list at once
+ model_->beginInsertRows(index(), 1, files.size() + 1); // the first item is the placeholder item, so we start from row 1
+ for(auto& file: files) {
+ if(file->isDir()) {
+ DirTreeModelItem* newItem = new DirTreeModelItem(std::move(file), model_);
+ newItem->parent_ = this;
+ children_.push_back(newItem);
+ }
+ }
+ model_->endInsertRows();
+
+ // remove the place holder if a folder is added
+ if(children_.size() > 1) {
+ auto it = std::find(children_.cbegin(), children_.cend(), placeHolderChild_);
+ if(it != children_.cend()) {
+ auto pos = it - children_.cbegin();
+ model_->beginRemoveRows(index(), pos, pos);
+ children_.erase(it);
+ delete placeHolderChild_;
+ model_->endRemoveRows();
+ placeHolderChild_ = nullptr;
+ }
+ }
+ }
+ else {
+ // the list already contain some items, insert new items one by one so they can be sorted.
+ for(auto& file: files) {
+ if(file->isDir()) {
+ insertFile(std::move(file));
+ }
+ }
+ }
+}
+
+// find a good position to insert the new item
+// 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("<No sub folders>");
+ QModelIndex placeHolderIndex = placeHolderChild_->index();
+ // qDebug() << "placeHolderIndex: "<<placeHolderIndex;
+ Q_EMIT model->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("<No sub folders>");
+ }
+}
+
+void DirTreeModelItem::onFolderFilesChanged(std::vector<Fm::FileInfoPair> &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("<No sub folders>");
+ }
+ }
+}
+
+
+
+} // namespace Fm
--- /dev/null
+/*
+ * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef FM_DIRTREEMODELITEM_H
+#define FM_DIRTREEMODELITEM_H
+
+#include "libfmqtglobals.h"
+#include <libfm/fm.h>
+#include <vector>
+#include <QIcon>
+#include <QModelIndex>
+
+#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<const Fm::FileInfo> 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<const Fm::FileInfo> 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<Fm::FileInfoPair>& changes);
+
+private:
+ std::shared_ptr<const Fm::FileInfo> fileInfo_;
+ std::shared_ptr<Fm::Folder> folder_;
+ QString displayName_ ;
+ QIcon icon_;
+ bool expanded_;
+ bool loaded_;
+ DirTreeModelItem* parent_;
+ DirTreeModelItem* placeHolderChild_;
+ std::vector<DirTreeModelItem*> children_;
+ std::vector<DirTreeModelItem*> hiddenChildren_;
+ DirTreeModel* model_;
+ bool queuedForDeletion_;
+ // signal connections
+ QMetaObject::Connection onFolderFinishLoadingConn_;
+ QMetaObject::Connection onFolderFilesAddedConn_;
+ QMetaObject::Connection onFolderFilesRemovedConn_;
+ QMetaObject::Connection onFolderFilesChangedConn_;
+};
+
+}
+
+#endif // FM_DIRTREEMODELITEM_H
--- /dev/null
+/*
+ * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#include "dirtreeview.h"
+#include <QHeaderView>
+#include <QDebug>
+#include <QItemSelection>
+#include <QGuiApplication>
+#include <QMouseEvent>
+#include <QTimer>
+#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<DirTreeModel*>(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<DirTreeModel*>(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<DirTreeModel*>(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<DirTreeModel*>(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<std::shared_ptr<const Fm::FileInfo>>();
+ 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<Fm::FilePath>(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<QAction*>(sender())) {
+ setCurrentIndex(action->data().toModelIndex());
+ }
+}
+
+void DirTreeView::onNewWindow() {
+ if(QAction* action = qobject_cast<QAction*>(sender())) {
+ auto path = action->data().value<Fm::FilePath>();
+ Q_EMIT openFolderInNewWindowRequested(path);
+ }
+}
+
+void DirTreeView::onNewTab() {
+ if(QAction* action = qobject_cast<QAction*>(sender())) {
+ auto path = action->data().value<Fm::FilePath>();
+ Q_EMIT openFolderInNewTabRequested(path);
+ }
+}
+
+void DirTreeView::onOpenInTerminal() {
+ if(QAction* action = qobject_cast<QAction*>(sender())) {
+ auto path = action->data().value<Fm::FilePath>();
+ Q_EMIT openFolderInTerminalRequested(path);
+ }
+}
+
+void DirTreeView::onNewFolder() {
+ if(QAction* action = qobject_cast<QAction*>(sender())) {
+ auto path = action->data().value<Fm::FilePath>();
+ Q_EMIT createNewFolderRequested(path);
+ }
+}
+
+void DirTreeView::onCollapsed(const QModelIndex& index) {
+ DirTreeModel* treeModel = static_cast<DirTreeModel*>(model());
+ if(treeModel) {
+ treeModel->unloadRow(index);
+ }
+}
+
+void DirTreeView::onExpanded(const QModelIndex& index) {
+ DirTreeModel* treeModel = static_cast<DirTreeModel*>(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<DirTreeModelItem*>(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<DirTreeModel*>(model());
+ auto path = _model->filePath(index);
+ if(path && currentPath_ && path == currentPath_) {
+ return;
+ }
+ cancelPendingChdir();
+ if(!path) {
+ return;
+ }
+ currentPath_ = std::move(path);
+
+ // FIXME: use enums for type rather than hard-coded values 0 or 1
+ int type = 0;
+ if(QGuiApplication::mouseButtons() & Qt::MiddleButton) {
+ type = 1;
+ }
+ Q_EMIT chdirRequested(type, currentPath_);
+ }
+}
+
+
+} // namespace Fm
--- /dev/null
+/*
+ * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef FM_DIRTREEVIEW_H
+#define FM_DIRTREEVIEW_H
+
+#include "libfmqtglobals.h"
+#include <QTreeView>
+#include <libfm/fm.h>
+
+#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<DirTreeModelItem*> queuedForDeletion_;
+};
+
+}
+
+#endif // FM_DIRTREEVIEW_H
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#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
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_DNDACTIONMENU_H
+#define FM_DNDACTIONMENU_H
+
+#include "libfmqtglobals.h"
+#include <QMenu>
+#include <QAction>
+
+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
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#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
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef FM_DNDDEST_H
+#define FM_DNDDEST_H
+
+#include <QMimeData>
+#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
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>EditBookmarksDialog</class>
+ <widget class="QDialog" name="EditBookmarksDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>480</width>
+ <height>320</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Edit Bookmarks</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="1" column="0">
+ <widget class="QTreeWidget" name="treeWidget">
+ <property name="acceptDrops">
+ <bool>true</bool>
+ </property>
+ <property name="dragEnabled">
+ <bool>true</bool>
+ </property>
+ <property name="dragDropMode">
+ <enum>QAbstractItemView::InternalMove</enum>
+ </property>
+ <property name="defaultDropAction">
+ <enum>Qt::MoveAction</enum>
+ </property>
+ <property name="rootIsDecorated">
+ <bool>false</bool>
+ </property>
+ <property name="itemsExpandable">
+ <bool>false</bool>
+ </property>
+ <attribute name="headerDefaultSectionSize">
+ <number>100</number>
+ </attribute>
+ <column>
+ <property name="text">
+ <string>Name</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string>Location</string>
+ </property>
+ </column>
+ </widget>
+ </item>
+ <item row="2" column="0" colspan="2">
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QPushButton" name="addItem">
+ <property name="text">
+ <string>&Add Item</string>
+ </property>
+ <property name="icon">
+ <iconset theme="list-add"/>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="removeItem">
+ <property name="text">
+ <string>&Remove Item</string>
+ </property>
+ <property name="icon">
+ <iconset theme="list-remove"/>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Use drag and drop to reorder the items</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>EditBookmarksDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>EditBookmarksDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#include "editbookmarksdialog.h"
+#include "ui_edit-bookmarks.h"
+#include <QByteArray>
+#include <QUrl>
+#include <QSaveFile>
+#include <QStandardPaths>
+#include <QDir>
+
+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<FmBookmarkItem*>(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<QTreeWidgetItem*> sels = ui->treeWidget->selectedItems();
+ Q_FOREACH(QTreeWidgetItem* item, sels) {
+ delete item;
+ }
+}
+
+
+} // namespace Fm
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_EDITBOOKMARKSDIALOG_H
+#define FM_EDITBOOKMARKSDIALOG_H
+
+#include "libfmqtglobals.h"
+#include <QDialog>
+#include <libfm/fm.h>
+
+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
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ExecFileDialog</class>
+ <widget class="QDialog" name="ExecFileDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>487</width>
+ <height>58</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Execute file</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,1">
+ <item>
+ <widget class="QLabel" name="icon"/>
+ </item>
+ <item>
+ <widget class="QLabel" name="msg">
+ <property name="text">
+ <string/>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QPushButton" name="open">
+ <property name="text">
+ <string>&Open</string>
+ </property>
+ <property name="icon">
+ <iconset theme="document-open"/>
+ </property>
+ <property name="default">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="exec">
+ <property name="text">
+ <string>E&xecute</string>
+ </property>
+ <property name="icon">
+ <iconset theme="system-run"/>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="execTerm">
+ <property name="text">
+ <string>Execute in &Terminal</string>
+ </property>
+ <property name="icon">
+ <iconset theme="utilities-terminal"/>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="cancel">
+ <property name="text">
+ <string>Cancel</string>
+ </property>
+ <property name="icon">
+ <iconset theme="dialog-cancel"/>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>cancel</sender>
+ <signal>clicked()</signal>
+ <receiver>ExecFileDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>341</x>
+ <y>39</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>196</x>
+ <y>28</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>exec</sender>
+ <signal>clicked()</signal>
+ <receiver>ExecFileDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>56</x>
+ <y>39</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>196</x>
+ <y>28</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>execTerm</sender>
+ <signal>clicked()</signal>
+ <receiver>ExecFileDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>201</x>
+ <y>39</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>196</x>
+ <y>28</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>open</sender>
+ <signal>clicked()</signal>
+ <receiver>ExecFileDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>346</x>
+ <y>39</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>250</x>
+ <y>28</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
--- /dev/null
+/*
+ * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#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
--- /dev/null
+/*
+ * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef FM_EXECFILEDIALOG_H
+#define FM_EXECFILEDIALOG_H
+
+#include <QDialog>
+#include <libfm/fm.h>
+
+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
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>FileOperationDialog</class>
+ <widget class="QDialog" name="FileOperationDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>450</width>
+ <height>246</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string/>
+ </property>
+ <layout class="QFormLayout" name="formLayout_2">
+ <item row="0" column="0" colspan="2">
+ <widget class="QLabel" name="message">
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <layout class="QFormLayout" name="formLayout">
+ <property name="fieldGrowthPolicy">
+ <enum>QFormLayout::AllNonFixedFieldsGrow</enum>
+ </property>
+ <item row="1" column="0">
+ <widget class="QLabel" name="destLabel">
+ <property name="text">
+ <string>Destination:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="dest">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_3">
+ <property name="text">
+ <string>Processing:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLabel" name="curFile">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Preparing...</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="label_4">
+ <property name="text">
+ <string>Progress</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QProgressBar" name="progressBar">
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="0">
+ <widget class="QLabel" name="label_5">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Time remaining:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="1">
+ <widget class="QLabel" name="timeRemaining">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0" colspan="2">
+ <widget class="QListWidget" name="sourceFiles">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="0">
+ <widget class="QLabel" name="label_6">
+ <property name="text">
+ <string>Data transferred:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="1">
+ <widget class="QLabel" name="dataTransferred">
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="2" column="0" colspan="2">
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>FileOperationDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>FileOperationDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>FilePropsDialog</class>
+ <widget class="QDialog" name="FilePropsDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>424</width>
+ <height>456</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>File Properties</string>
+ </property>
+ <property name="windowIcon">
+ <iconset theme="document-properties">
+ <normaloff/>
+ </iconset>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>10</number>
+ </property>
+ <property name="topMargin">
+ <number>10</number>
+ </property>
+ <property name="rightMargin">
+ <number>10</number>
+ </property>
+ <property name="bottomMargin">
+ <number>10</number>
+ </property>
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="generalPage">
+ <attribute name="title">
+ <string>General</string>
+ </attribute>
+ <layout class="QFormLayout" name="formLayout_2">
+ <property name="horizontalSpacing">
+ <number>12</number>
+ </property>
+ <property name="verticalSpacing">
+ <number>6</number>
+ </property>
+ <item row="0" column="0">
+ <widget class="QPushButton" name="iconButton">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset theme="unknown">
+ <normaloff/>
+ </iconset>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="fileName"/>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>Location:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="location">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="label_4">
+ <property name="text">
+ <string>File type:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QLabel" name="fileType">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="0">
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>MIME type:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="1">
+ <widget class="QLabel" name="mimeType">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item row="6" column="0">
+ <widget class="QLabel" name="label_7">
+ <property name="text">
+ <string>File size:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="6" column="1">
+ <widget class="QLabel" name="fileSize">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item row="7" column="0">
+ <widget class="QLabel" name="label_6">
+ <property name="text">
+ <string>On-disk size:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="7" column="1">
+ <widget class="QLabel" name="onDiskSize">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item row="8" column="0">
+ <widget class="QLabel" name="label_3">
+ <property name="text">
+ <string>Last modified:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="8" column="1">
+ <widget class="QLabel" name="lastModified">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="targetLabel">
+ <property name="text">
+ <string>Link target:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLabel" name="target">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="0">
+ <widget class="QLabel" name="openWithLabel">
+ <property name="text">
+ <string>Open With:</string>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="1">
+ <widget class="Fm::AppChooserComboBox" name="openWith">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ <item row="9" column="0">
+ <widget class="QLabel" name="label_12">
+ <property name="text">
+ <string>Last accessed:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="9" column="1">
+ <widget class="QLabel" name="lastAccessed">
+ <property name="text">
+ <string/>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="permissionsPage">
+ <attribute name="title">
+ <string>Permissions</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <property name="spacing">
+ <number>6</number>
+ </property>
+ <item>
+ <widget class="QGroupBox" name="groupBox_2">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="title">
+ <string>Ownership</string>
+ </property>
+ <layout class="QFormLayout" name="formLayout">
+ <property name="horizontalSpacing">
+ <number>12</number>
+ </property>
+ <property name="verticalSpacing">
+ <number>6</number>
+ </property>
+ <item row="1" column="1">
+ <widget class="QLineEdit" name="owner"/>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLineEdit" name="ownerGroup"/>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_9">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Group:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_8">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Owner:</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="title">
+ <string>Access Control</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <widget class="QStackedWidget" name="stackedWidget">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="page">
+ <layout class="QFormLayout" name="formLayout_3">
+ <item row="0" column="0">
+ <widget class="QLabel" name="ownerLabel">
+ <property name="text">
+ <string>Owner:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QComboBox" name="ownerPerm">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="groupLabel">
+ <property name="text">
+ <string>Group:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QComboBox" name="groupPerm">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="otherLabel">
+ <property name="text">
+ <string>Other:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QComboBox" name="otherPerm">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0" colspan="2">
+ <widget class="QCheckBox" name="executable">
+ <property name="text">
+ <string>Make the file executable</string>
+ </property>
+ <property name="tristate">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="page_2">
+ <layout class="QGridLayout" name="gridLayout_2" columnstretch="1,0,0">
+ <item row="0" column="0" rowspan="3">
+ <layout class="QGridLayout" name="gridLayout">
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="spacing">
+ <number>6</number>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label_5">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Owner:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QCheckBox" name="checkBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Read</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="2">
+ <widget class="QCheckBox" name="checkBox_5">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Write</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="3">
+ <widget class="QCheckBox" name="checkBox_8">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Execute</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_10">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Group:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QCheckBox" name="checkBox_4">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Read</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="2">
+ <widget class="QCheckBox" name="checkBox_9">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Write</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="3">
+ <widget class="QCheckBox" name="checkBox_3">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Execute</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_11">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Other:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QCheckBox" name="checkBox_7">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Read</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="2">
+ <widget class="QCheckBox" name="checkBox_2">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Write</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="3">
+ <widget class="QCheckBox" name="checkBox_6">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Execute</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="0" column="2">
+ <widget class="QCheckBox" name="checkBox_10">
+ <property name="text">
+ <string>Sticky</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="2">
+ <widget class="QCheckBox" name="checkBox_11">
+ <property name="text">
+ <string>SetUID</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="2">
+ <widget class="QCheckBox" name="checkBox_12">
+ <property name="text">
+ <string>SetGID</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1" rowspan="3">
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Advanced Mode</string>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>Fm::AppChooserComboBox</class>
+ <extends>QComboBox</extends>
+ <header>appchoosercombobox.h</header>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>FilePropsDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>FilePropsDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
--- /dev/null
+#include "filedialog.h"
+#include "cachedfoldermodel.h"
+#include "proxyfoldermodel.h"
+#include "utilities.h"
+#include "core/fileinfojob.h"
+#include "ui_filedialog.h"
+
+#include <QDialogButtonBox>
+#include <QPushButton>
+#include <QMimeType>
+#include <QMimeDatabase>
+#include <QMessageBox>
+#include <QToolBar>
+#include <QCompleter>
+#include <QShortcut>
+#include <QTimer>
+#include <QDebug>
+
+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<void(QCompleter::*)(const QString &)>(&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<int> 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<const Fm::FileInfo> FileDialog::firstSelectedDir() const {
+ std::shared_ptr<const Fm::FileInfo> 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<const Fm::FileInfo> 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<const FileInfo> &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<QAction*>(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<FileInfoJob*>(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<QUrl> 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<const FileInfo> &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
--- /dev/null
+#ifndef FM_FILEDIALOG_H
+#define FM_FILEDIALOG_H
+
+#include "libfmqtglobals.h"
+#include "core/filepath.h"
+
+#include <QFileDialog>
+#include <QRegExp>
+#include <vector>
+#include <memory>
+#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<QUrl> 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<const Fm::FileInfo>& 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<QUrl> &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<const Fm::FileInfo>& info) const override;
+ void update();
+
+ FileDialog* dlg_;
+ std::vector<QRegExp> 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<const Fm::FileInfo> 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::FileDialog> ui;
+ CachedFolderModel* folderModel_;
+ ProxyFolderModel* proxyModel_;
+ FilePath directoryPath_;
+ std::shared_ptr<Fm::Folder> 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<QUrl> 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
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>FileDialog</class>
+ <widget class="QDialog" name="FileDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>700</width>
+ <height>500</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string/>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout" stretch="0,1,0">
+ <item>
+ <layout class="QHBoxLayout" name="toolbarLayout" stretch="0,1">
+ <item>
+ <widget class="QLabel" name="lookInLabel">
+ <property name="text">
+ <string>Location:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="Fm::PathBar" name="location" native="true"/>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QSplitter" name="splitter">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <widget class="Fm::SidePane" name="sidePane" native="true">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>120</width>
+ <height>0</height>
+ </size>
+ </property>
+ </widget>
+ <widget class="Fm::FolderView" name="folderView" native="true">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
+ <horstretch>1</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0">
+ <widget class="QLabel" name="fileNameLabel">
+ <property name="text">
+ <string>File name:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="fileName"/>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="fileTypeLabel">
+ <property name="text">
+ <string>File type:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QComboBox" name="fileTypeCombo"/>
+ </item>
+ <item row="0" column="2" rowspan="2">
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>Fm::SidePane</class>
+ <extends>QWidget</extends>
+ <header location="global">sidepane.h</header>
+ <container>1</container>
+ </customwidget>
+ <customwidget>
+ <class>Fm::FolderView</class>
+ <extends>QWidget</extends>
+ <header>folderview.h</header>
+ <container>1</container>
+ </customwidget>
+ <customwidget>
+ <class>Fm::PathBar</class>
+ <extends>QWidget</extends>
+ <header>pathbar.h</header>
+ <container>1</container>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>FileDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>FileDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#include "filelauncher.h"
+#include "applaunchcontext.h"
+#include <QMessageBox>
+#include <QDebug>
+#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
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_FILELAUNCHER_H
+#define FM_FILELAUNCHER_H
+
+#include "libfmqtglobals.h"
+#include <QWidget>
+#include <libfm/fm.h>
+#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<FileLauncher*>(user_data)->getApp(file_infos, mime_type, err);
+ }
+ static gboolean _openFolder(GAppLaunchContext* ctx, GList* folder_infos, gpointer user_data, GError** err) {
+ return reinterpret_cast<FileLauncher*>(user_data)->openFolder(ctx, folder_infos, err);
+ }
+ static FmFileLauncherExecAction _execFile(FmFileInfo* file, gpointer user_data) {
+ return reinterpret_cast<FileLauncher*>(user_data)->execFile(file);
+ }
+ static gboolean _error(GAppLaunchContext* ctx, GError* err, FmPath* file, gpointer user_data) {
+ return reinterpret_cast<FileLauncher*>(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<FileLauncher*>(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
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#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 <QMessageBox>
+#include <QAbstractItemView>
+#include <QDebug>
+#include "filemenu_p.h"
+
+#include "core/compat_p.h"
+
+namespace Fm {
+
+FileMenu::FileMenu(Fm::FileInfoList files, std::shared_ptr<const Fm::FileInfo> 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<const FileActionItem> 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<AppInfoAction*>(sender());
+ openFilesWithApp(action->appInfo().get());
+}
+
+void FileMenu::onCustomActionTrigerred() {
+ CustomAction* action = static_cast<CustomAction*>(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<QAbstractItemView*>(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
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_FILEMENU_H
+#define FM_FILEMENU_H
+
+#include "libfmqtglobals.h"
+#include <QMenu>
+#include <qabstractitemmodel.h>
+#include <libfm/fm.h>
+#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<const Fm::FileInfo> 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<const Fm::FileInfo>& 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<const FileActionItem> 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<const Fm::FileInfo> 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
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef FM_FILEMENU_P_H
+#define FM_FILEMENU_P_H
+
+#include "icontheme.h"
+#include <QDebug>
+#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
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#include "fileoperation.h"
+#include "fileoperationdialog.h"
+#include <QTimer>
+#include <QElapsedTimer>
+#include <QMessageBox>
+#include <QDebug>
+#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<FmPathList*>(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
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_FILEOPERATION_H
+#define FM_FILEOPERATION_H
+
+#include "libfmqtglobals.h"
+#include <QObject>
+#include <QElapsedTimer>
+#include <libfm/fm.h>
+#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
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#include "fileoperationdialog.h"
+#include "fileoperation.h"
+#include "renamedialog.h"
+#include <QLabel>
+#include <QMessageBox>
+#include <libfm/fm-config.h>
+#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
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_FILEOPERATIONDIALOG_H
+#define FM_FILEOPERATIONDIALOG_H
+
+#include "libfmqtglobals.h"
+#include <cstdint>
+#include <QDialog>
+#include <libfm/fm.h>
+#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
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#include "filepropsdialog.h"
+#include "ui_file-props.h"
+#include "icontheme.h"
+#include "utilities.h"
+#include "fileoperation.h"
+#include <QStringBuilder>
+#include <QStringListModel>
+#include <QMessageBox>
+#include <QDateTime>
+#include <QStandardPaths>
+#include <QFileDialog>
+#include <sys/types.h>
+#include <time.h>
+#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_t>(uid) != fi->uid()) {
+ uid = DIFFERENT_UIDS; // not all files have the same owner
+ }
+ if(gid != DIFFERENT_GIDS && static_cast<gid_t>(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<const Fm::IconInfo> 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<const Fm::IconInfo> 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
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_FILEPROPSDIALOG_H
+#define FM_FILEPROPSDIALOG_H
+
+#include "libfmqtglobals.h"
+#include <QDialog>
+#include <QTimer>
+#include <libfm/fm.h>
+
+#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<const Fm::FileInfo> 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<const Fm::FileInfo> 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<const Fm::MimeType> 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
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SearchDialog</class>
+ <widget class="QDialog" name="SearchDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>512</width>
+ <height>420</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Search Files</string>
+ </property>
+ <property name="windowIcon">
+ <iconset theme="system-search">
+ <normaloff/>
+ </iconset>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="tab">
+ <attribute name="title">
+ <string>Name/Location</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,1">
+ <item>
+ <widget class="QGroupBox" name="groupBox">
+ <property name="title">
+ <string>File Name Patterns:</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <widget class="QLineEdit" name="namePatterns">
+ <property name="text">
+ <string>*</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="nameCaseInsensitive">
+ <property name="text">
+ <string>Case insensitive</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="nameRegExp">
+ <property name="text">
+ <string>Use regular expression</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_2">
+ <property name="title">
+ <string>Places to Search:</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QListWidget" name="listView"/>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <widget class="QPushButton" name="addPath">
+ <property name="text">
+ <string>&Add</string>
+ </property>
+ <property name="icon">
+ <iconset theme="list-add">
+ <normaloff/>
+ </iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="removePath">
+ <property name="text">
+ <string>&Remove</string>
+ </property>
+ <property name="icon">
+ <iconset theme="list-remove">
+ <normaloff/>
+ </iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="recursiveSearch">
+ <property name="text">
+ <string>Search in sub directories</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="searchHidden">
+ <property name="text">
+ <string>Search for hidden files</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="tab_2">
+ <attribute name="title">
+ <string>File Type</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_7">
+ <item>
+ <widget class="QGroupBox" name="groupBox_3">
+ <property name="title">
+ <string>Only search for files of following types:</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_6">
+ <item>
+ <widget class="QCheckBox" name="searchTextFiles">
+ <property name="text">
+ <string>Text files</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="searchImages">
+ <property name="text">
+ <string>Image files</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="searchAudio">
+ <property name="text">
+ <string>Audio files</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="searchVideo">
+ <property name="text">
+ <string>Video files</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="searchDocuments">
+ <property name="text">
+ <string>Documents</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="searchFolders">
+ <property name="text">
+ <string>Folders</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="tab_3">
+ <attribute name="title">
+ <string>Content</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_9">
+ <item>
+ <widget class="QGroupBox" name="groupBox_4">
+ <property name="title">
+ <string>File contains:</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_8">
+ <item>
+ <widget class="QLineEdit" name="contentPattern"/>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="contentCaseInsensitive">
+ <property name="text">
+ <string>Case insensiti&ve</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="contentRegExp">
+ <property name="text">
+ <string>&Use regular expression</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>186</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="tab_4">
+ <attribute name="title">
+ <string>Properties</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_10">
+ <item>
+ <widget class="QGroupBox" name="groupBox_5">
+ <property name="title">
+ <string>File Size:</string>
+ </property>
+ <layout class="QFormLayout" name="formLayout">
+ <item row="0" column="0">
+ <layout class="QGridLayout" name="gridLayout_2">
+ <item row="1" column="0">
+ <widget class="QCheckBox" name="smallerThan">
+ <property name="text">
+ <string>Smaller than:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="2">
+ <widget class="QSpinBox" name="maxSize">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="2">
+ <widget class="QSpinBox" name="minSize">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="0">
+ <widget class="QCheckBox" name="largerThan">
+ <property name="text">
+ <string>Larger than:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="3">
+ <widget class="QComboBox" name="minSizeUnit">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="currentIndex">
+ <number>2</number>
+ </property>
+ <item>
+ <property name="text">
+ <string>Bytes</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>KiB</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>MiB</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>GiB</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ <item row="1" column="3">
+ <widget class="QComboBox" name="maxSizeUnit">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="currentIndex">
+ <number>2</number>
+ </property>
+ <item>
+ <property name="text">
+ <string>Bytes</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>KiB</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>MiB</string>
+ </property>
+ </item>
+ <item>
+ <property name="text">
+ <string>GiB</string>
+ </property>
+ </item>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_6">
+ <property name="layoutDirection">
+ <enum>Qt::LeftToRight</enum>
+ </property>
+ <property name="title">
+ <string>Last Modified Time:</string>
+ </property>
+ <layout class="QFormLayout" name="formLayout_2">
+ <item row="1" column="0">
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="2" column="0">
+ <widget class="QCheckBox" name="earlierThan">
+ <property name="text">
+ <string>Earlier than:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QCheckBox" name="laterThan">
+ <property name="text">
+ <string>Later than:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QDateEdit" name="minTime">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="calendarPopup">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QDateEdit" name="maxTime">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="calendarPopup">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_4">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>SearchDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>SearchDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>largerThan</sender>
+ <signal>toggled(bool)</signal>
+ <receiver>minSizeUnit</receiver>
+ <slot>setEnabled(bool)</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>93</x>
+ <y>84</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>403</x>
+ <y>88</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>smallerThan</sender>
+ <signal>toggled(bool)</signal>
+ <receiver>maxSize</receiver>
+ <slot>setEnabled(bool)</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>96</x>
+ <y>119</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>241</x>
+ <y>123</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>largerThan</sender>
+ <signal>toggled(bool)</signal>
+ <receiver>minSize</receiver>
+ <slot>setEnabled(bool)</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>93</x>
+ <y>84</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>241</x>
+ <y>88</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>smallerThan</sender>
+ <signal>toggled(bool)</signal>
+ <receiver>maxSizeUnit</receiver>
+ <slot>setEnabled(bool)</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>96</x>
+ <y>119</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>403</x>
+ <y>123</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>laterThan</sender>
+ <signal>toggled(bool)</signal>
+ <receiver>minTime</receiver>
+ <slot>setEnabled(bool)</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>88</x>
+ <y>223</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>319</x>
+ <y>226</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>earlierThan</sender>
+ <signal>toggled(bool)</signal>
+ <receiver>maxTime</receiver>
+ <slot>setEnabled(bool)</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>93</x>
+ <y>190</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>319</x>
+ <y>193</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
--- /dev/null
+/*
+ * Copyright (C) 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#include "filesearchdialog.h"
+#include <QMessageBox>
+#include "fm-search.h"
+#include "ui_filesearch.h"
+#include <limits>
+#include <QFileDialog>
+#include <utility>
+
+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<int>().max());
+ ui->maxSize->setMaximum(std::numeric_limits<int>().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();
+}
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef FM_FILESEARCHDIALOG_H
+#define FM_FILESEARCHDIALOG_H
+
+#include "libfmqtglobals.h"
+#include <QDialog>
+#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
--- /dev/null
+/*
+ * fm-search-uri.c
+ *
+ * Copyright 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ * Copyright 2012-2014 Andriy Grytsenko (LStranger) <andrej@rep.kiev.ua>
+ *
+ * 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 <string.h>
+
+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;
+}
--- /dev/null
+/*
+ * fm-search-uri.h
+ *
+ * Copyright 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ * Copyright 2012-2014 Andriy Grytsenko (LStranger) <andrej@rep.kiev.ua>
+ *
+ * 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 <libfm/fm.h>
+
+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_ */
--- /dev/null
+/*
+ * Copyright (C) 2016 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef __LIBFM_QT_FM_FOLDER_CONFIG_H__
+#define __LIBFM_QT_FM_FOLDER_CONFIG_H__
+
+#include <libfm/fm.h>
+#include <QObject>
+#include <QtGlobal>
+#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<FmFolderConfig*>(fm_folder_config_open(fmpath));
+ fm_path_unref(fmpath);
+ }
+
+
+ // default constructor
+ FolderConfig() {
+ dataPtr_ = nullptr;
+ }
+
+
+ // move constructor
+ FolderConfig(FolderConfig&& other) noexcept {
+ dataPtr_ = reinterpret_cast<FmFolderConfig*>(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<FmFolderConfig*>(dataPtr);
+ return obj;
+ }
+
+ // disown the managed data pointer
+ FmFolderConfig* takeDataPtr() {
+ FmFolderConfig* data = reinterpret_cast<FmFolderConfig*>(dataPtr_);
+ dataPtr_ = nullptr;
+ return data;
+ }
+
+ // get the raw pointer wrapped
+ FmFolderConfig* dataPtr() {
+ return reinterpret_cast<FmFolderConfig*>(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<FmFolderConfig*>(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__
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#include "folderitemdelegate.h"
+#include "foldermodel.h"
+#include <QPainter>
+#include <QModelIndex>
+#include <QAbstractItemView>
+#include <QStyleOptionViewItem>
+#include <QApplication>
+#include <QIcon>
+#include <QTextLayout>
+#include <QTextOption>
+#include <QTextLine>
+#include <QLineEdit>
+#include <QTextEdit>
+#include <QTimer>
+#include <QDebug>
+
+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<QSize>(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<std::shared_ptr<const Fm::IconInfo>> icon_emblems;
+ auto fmicon = index.data(iconInfoRole_).value<std::shared_ptr<const Fm::IconInfo>>();
+ if(fmicon) {
+ icon_emblems = fmicon->emblems();
+ }
+ // get file info for the item
+ auto file = index.data(fileInfoRole_).value<std::shared_ptr<const Fm::FileInfo>>();
+ 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<QTextEdit*>(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<QLineEdit*>(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<QWidget*>(object);
+ if (editor && event->type() == QEvent::KeyPress) {
+ int k = static_cast<QKeyEvent *>(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
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_FOLDERITEMDELEGATE_H
+#define FM_FOLDERITEMDELEGATE_H
+
+#include "libfmqtglobals.h"
+#include <QStyledItemDelegate>
+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
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ * Copyright (C) 2012 - 2014 Andriy Grytsenko (LStranger) <andrej@rep.kiev.ua>
+ *
+ * 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 <cstring> // for memset
+#include <QDebug>
+#include "customaction_p.h"
+#include "customactions/fileaction.h"
+#include <QMessageBox>
+
+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<const FileActionItem> 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<CustomAction*>(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<QAction*>(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<QAction*>(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
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_FOLDERMENU_H
+#define FM_FOLDERMENU_H
+
+#include "libfmqtglobals.h"
+#include <QMenu>
+#include <libfm/fm.h>
+#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<const FileActionItem> 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
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#include "foldermodel.h"
+#include "icontheme.h"
+#include <iostream>
+#include <algorithm>
+#include <QtAlgorithms>
+#include <QVector>
+#include <qmimedata.h>
+#include <QMimeData>
+#include <QByteArray>
+#include <QPixmap>
+#include <QPainter>
+#include <QTimer>
+#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<Fm::Folder>& 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<Fm::FileInfoPair>& files) {
+ for(auto& change : files) {
+ int row;
+ auto& oldInfo = change.first;
+ auto& newInfo = change.second;
+ QList<FolderModelItem>::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<FolderModelItem>::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<const Fm::FileInfo>& 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<HashSet>();
+ 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<FolderModelItem*>(index.internalPointer());
+}
+
+std::shared_ptr<const Fm::FileInfo> 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<FolderModelItem>::iterator FolderModel::findItemByPath(const Fm::FilePath& path, int* row) {
+ QList<FolderModelItem>::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<FolderModelItem>::iterator FolderModel::findItemByName(const char* name, int* row) {
+ QList<FolderModelItem>::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<FolderModelItem>::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<const Fm::FileInfo> 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<FolderModelItem>::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<Fm::ThumbnailJob*>(sender());
+ auto it = std::find(pendingThumbnailJobs_.cbegin(), pendingThumbnailJobs_.cend(), job);
+ if(it != pendingThumbnailJobs_.end()) {
+ pendingThumbnailJobs_.erase(it);
+ }
+}
+
+void FolderModel::onThumbnailLoaded(const std::shared_ptr<const Fm::FileInfo>& file, int size, const QImage& image) {
+ // find the model item this thumbnail belongs to
+ int row;
+ QList<FolderModelItem>::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
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_FOLDERMODEL_H
+#define FM_FOLDERMODEL_H
+
+#include "libfmqtglobals.h"
+#include <QAbstractListModel>
+#include <QItemSelection>
+#include <QIcon>
+#include <QImage>
+#include <libfm/fm.h>
+#include <QList>
+#include <vector>
+#include <utility>
+#include <forward_list>
+#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<Fm::Folder>& folder() const {
+ return folder_;
+ }
+
+ void setFolder(const std::shared_ptr<Fm::Folder>& 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<const Fm::FileInfo> 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<Fm::FileInfoPair>& files);
+ void onFilesRemoved(const Fm::FileInfoList& files);
+
+ void onThumbnailLoaded(const std::shared_ptr<const Fm::FileInfo>& file, int size, const QImage& image);
+ void onThumbnailJobFinished();
+ void loadPendingThumbnails();
+
+protected:
+ void queueLoadThumbnail(const std::shared_ptr<const Fm::FileInfo>& file, int size);
+ void insertFiles(int row, const Fm::FileInfoList& files);
+ void removeAll();
+ QList<FolderModelItem>::iterator findItemByPath(const Fm::FilePath& path, int* row);
+ QList<FolderModelItem>::iterator findItemByName(const char* name, int* row);
+ QList<FolderModelItem>::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<Fm::Folder> folder_;
+ QList<FolderModelItem> items;
+
+ bool hasPendingThumbnailHandler_;
+ std::vector<Fm::ThumbnailJob*> pendingThumbnailJobs_;
+ std::forward_list<ThumbnailData> thumbnailData_;
+};
+
+}
+
+#endif // FM_FOLDERMODEL_H
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#include "foldermodelitem.h"
+#include <QDateTime>
+#include <QPainter>
+#include "utilities.h"
+#include "core/userinfocache.h"
+
+namespace Fm {
+
+FolderModelItem::FolderModelItem(const std::shared_ptr<const Fm::FileInfo>& _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<const HashSet>& 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<Thumbnail>::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<Thumbnail>::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
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_FOLDERMODELITEM_H
+#define FM_FOLDERMODELITEM_H
+
+#include "libfmqtglobals.h"
+#include <libfm/fm.h>
+#include <QImage>
+#include <QString>
+#include <QIcon>
+#include <QVector>
+#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<const Fm::FileInfo>& _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<const HashSet>& cutFilesHashSet);
+
+ Thumbnail* findThumbnail(int size, bool transparent);
+
+ void removeThumbnail(int size);
+
+ std::shared_ptr<const Fm::FileInfo> info;
+ mutable QString dispMtime_;
+ mutable QString dispSize_;
+ std::weak_ptr<const HashSet> cutFilesHashSet_;
+ QVector<Thumbnail> thumbnails;
+};
+
+}
+
+#endif // FM_FOLDERMODELITEM_H
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#include "folderview.h"
+#include "foldermodel.h"
+#include <QHeaderView>
+#include <QVBoxLayout>
+#include <QContextMenuEvent>
+#include "proxyfoldermodel.h"
+#include "folderitemdelegate.h"
+#include "dndactionmenu.h"
+#include "filemenu.h"
+#include "foldermenu.h"
+#include "filelauncher.h"
+#include "utilities.h"
+#include <QTimer>
+#include <QDate>
+#include <QDebug>
+#include <QClipboard>
+#include <QMimeData>
+#include <QHoverEvent>
+#include <QApplication>
+#include <QScrollBar>
+#include <QMetaType>
+#include <QMessageBox>
+#include <QLineEdit>
+#include <QTextEdit>
+#include <QX11Info> // for XDS support
+#include <xcb/xcb.h> // 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<FolderView*>(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<FolderItemDelegate*>(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<FolderView*>(parent())->childDragEnterEvent(event);
+}
+
+void FolderViewListView::dragLeaveEvent(QDragLeaveEvent* e) {
+ if(movement() != Static) {
+ QListView::dragLeaveEvent(e);
+ }
+ else {
+ QAbstractItemView::dragLeaveEvent(e);
+ }
+ static_cast<FolderView*>(parent())->childDragLeaveEvent(e);
+}
+
+void FolderViewListView::dragMoveEvent(QDragMoveEvent* e) {
+ if(movement() != Static) {
+ QListView::dragMoveEvent(e);
+ }
+ else {
+ QAbstractItemView::dragMoveEvent(e);
+ }
+ static_cast<FolderView*>(parent())->childDragMoveEvent(e);
+}
+
+void FolderViewListView::dropEvent(QDropEvent* e) {
+
+ static_cast<FolderView*>(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<FolderView*>(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<ProxyFolderModel*>(model)) {
+ connect(proxyModel, &ProxyFolderModel::sortFilterChanged, this, &FolderViewTreeView::onSortFilterChanged,
+ Qt::UniqueConnection);
+ onSortFilterChanged();
+ }
+}
+
+void FolderViewTreeView::mousePressEvent(QMouseEvent* event) {
+ QTreeView::mousePressEvent(event);
+ static_cast<FolderView*>(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<FolderView*>(parent())->childDragEnterEvent(event);
+}
+
+void FolderViewTreeView::dragLeaveEvent(QDragLeaveEvent* e) {
+ QTreeView::dragLeaveEvent(e);
+ static_cast<FolderView*>(parent())->childDragLeaveEvent(e);
+}
+
+void FolderViewTreeView::dragMoveEvent(QDragMoveEvent* e) {
+ QTreeView::dragMoveEvent(e);
+ static_cast<FolderView*>(parent())->childDragMoveEvent(e);
+}
+
+void FolderViewTreeView::dropEvent(QDropEvent* e) {
+ static_cast<FolderView*>(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<int>& roles /*= QVector<int>{}*/) {
+ 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<QSortFilterProxyModel*>(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<std::shared_ptr<const Fm::FileInfo>>();
+ 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<QTextEdit*>(editor)) { // icon and thumbnail view
+ newName = qobject_cast<QTextEdit*>(editor)->toPlainText();
+ }
+ else if (qobject_cast<QLineEdit*>(editor)) { // compact view
+ newName = qobject_cast<QLineEdit*>(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<std::shared_ptr<const Fm::FileInfo>>();
+ 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<FolderViewListView*>(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<FolderViewListView*>(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<FolderItemDelegate*>(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<std::shared_ptr<const Fm::FileInfo>>();
+ 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<QHoverEvent*>(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<FolderViewTreeView*>(view);
+ delegate = static_cast<FolderItemDelegate*>(treeView->itemDelegateForColumn(FolderModel::ColumnFileName));
+ }
+ else {
+ FolderViewListView* listView = static_cast<FolderViewListView*>(view);
+ delegate = static_cast<FolderItemDelegate*>(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<const Fm::FileInfo> &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<HashSet>()); // 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*/) {
+}
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_FOLDERVIEW_H
+#define FM_FOLDERVIEW_H
+
+#include "libfmqtglobals.h"
+#include <QWidget>
+#include <QListView>
+#include <QTreeView>
+#include <QMouseEvent>
+#include <libfm/fm.h>
+#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<Fm::Folder> folder() const {
+ return model_ ? static_cast<FolderModel*>(model_->sourceModel())->folder() : nullptr;
+ }
+
+ std::shared_ptr<const Fm::FileInfo> 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<const Fm::FileInfo>& 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<const Fm::FileInfo>& 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
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_FOLDERVIEW_P_H
+#define FM_FOLDERVIEW_P_H
+
+#include <QListView>
+#include <QTreeView>
+#include <QMouseEvent>
+#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<int>& roles = QVector<int>{});
+ 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
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#include "fontbutton.h"
+#include <QFontDialog>
+#include <X11/X.h>
+
+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
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_FONTBUTTON_H
+#define FM_FONTBUTTON_H
+
+#include "libfmqtglobals.h"
+#include <QPushButton>
+
+
+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
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#include "icontheme.h"
+#include <libfm/fm.h>
+#include <QList>
+#include <QIcon>
+#include <QtGlobal>
+#include <QApplication>
+#include <QDesktopWidget>
+
+#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
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_ICONTHEME_H
+#define FM_ICONTHEME_H
+
+#include "libfmqtglobals.h"
+#include <QIcon>
+#include <QString>
+#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
--- /dev/null
+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}
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#include <libfm/fm.h>
+#include "libfmqt.h"
+#include <QLocale>
+#include <QPixmapCache>
+#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
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_APPLICATION_H
+#define FM_APPLICATION_H
+
+#include "libfmqtglobals.h"
+#include <QtGlobal>
+#include <QTranslator>
+#include <libfm/fm.h>
+
+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
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef _LIBFM_QT_GLOBALS_
+#define _LIBFM_QT_GLOBALS_
+
+#include "fm-qt_export.h"
+
+#endif
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MountOperationPasswordDialog</class>
+ <widget class="QDialog" name="MountOperationPasswordDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>244</width>
+ <height>302</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="windowTitle">
+ <string>Mount</string>
+ </property>
+ <property name="windowIcon">
+ <iconset theme="dialog-password"/>
+ </property>
+ <property name="sizeGripEnabled">
+ <bool>false</bool>
+ </property>
+ <property name="modal">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="message">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="Anonymous">
+ <property name="text">
+ <string>Connect &anonymously</string>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">usernameGroup</string>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="asUser">
+ <property name="text">
+ <string>Connect as u&ser:</string>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">usernameGroup</string>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="1">
+ <widget class="QLineEdit" name="username"/>
+ </item>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>&Username:</string>
+ </property>
+ <property name="buddy">
+ <cstring>username</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QLineEdit" name="password">
+ <property name="echoMode">
+ <enum>QLineEdit::Password</enum>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="label_2">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>&Password:</string>
+ </property>
+ <property name="buddy">
+ <cstring>password</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="domainLabel">
+ <property name="text">
+ <string>&Domain:</string>
+ </property>
+ <property name="buddy">
+ <cstring>domain</cstring>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLineEdit" name="domain"/>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="forgetPassword">
+ <property name="text">
+ <string>Forget password &immediately</string>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">passwordGroup</string>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="sessionPassword">
+ <property name="text">
+ <string>Remember password until you &logout</string>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">passwordGroup</string>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="storePassword">
+ <property name="text">
+ <string>Remember &forever</string>
+ </property>
+ <attribute name="buttonGroup">
+ <string notr="true">passwordGroup</string>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <tabstops>
+ <tabstop>Anonymous</tabstop>
+ <tabstop>asUser</tabstop>
+ <tabstop>username</tabstop>
+ <tabstop>domain</tabstop>
+ <tabstop>password</tabstop>
+ <tabstop>forgetPassword</tabstop>
+ <tabstop>sessionPassword</tabstop>
+ <tabstop>storePassword</tabstop>
+ </tabstops>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>MountOperationPasswordDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>MountOperationPasswordDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+ <buttongroups>
+ <buttongroup name="usernameGroup"/>
+ <buttongroup name="passwordGroup"/>
+ </buttongroups>
+</ui>
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#include "mountoperation.h"
+#include <glib/gi18n.h> // for _()
+#include <QMessageBox>
+#include <QPushButton>
+#include <QEventLoop>
+#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<void *>(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
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_MOUNTOPERATION_H
+#define FM_MOUNTOPERATION_H
+
+#include "libfmqtglobals.h"
+#include <QWidget>
+#include <QDialog>
+#include <libfm/fm.h>
+#include <gio/gio.h>
+#include <QPointer>
+
+#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<MountOperation>(this));
+ }
+
+ void mount(GVolume* volume) {
+ g_volume_mount(volume, G_MOUNT_MOUNT_NONE, op, cancellable_, (GAsyncReadyCallback)onMountVolumeFinished, new QPointer<MountOperation>(this));
+ }
+
+ void unmount(GMount* mount) {
+ prepareUnmount(mount);
+ g_mount_unmount_with_operation(mount, G_MOUNT_UNMOUNT_NONE, op, cancellable_, (GAsyncReadyCallback)onUnmountMountFinished, new QPointer<MountOperation>(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<MountOperation>(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<MountOperation>(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<MountOperation>* pThis);
+ static void onMountVolumeFinished(GVolume* volume, GAsyncResult* res, QPointer<MountOperation>* pThis);
+ static void onUnmountMountFinished(GMount* mount, GAsyncResult* res, QPointer<MountOperation>* pThis);
+ static void onEjectMountFinished(GMount* mount, GAsyncResult* res, QPointer<MountOperation>* pThis);
+ static void onEjectVolumeFinished(GVolume* volume, GAsyncResult* res, QPointer<MountOperation>* pThis);
+
+ void handleFinish(GError* error);
+
+private:
+ GMountOperation* op;
+ GCancellable* cancellable_;
+ QWidget* parent_;
+ bool running;
+ bool interactive_;
+ QEventLoop* eventLoop;
+ bool autoDestroy_;
+};
+
+}
+
+#endif // FM_MOUNTOPERATION_H
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#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
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_MOUNTOPERATIONPASSWORDDIALOG_H
+#define FM_MOUNTOPERATIONPASSWORDDIALOG_H
+
+#include "libfmqtglobals.h"
+#include <QDialog>
+#include <gio/gio.h>
+
+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
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#include "mountoperationquestiondialog_p.h"
+#include "mountoperation.h"
+#include <QPushButton>
+
+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
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_MOUNTOPERATIONQUESTIONDIALOG_H
+#define FM_MOUNTOPERATIONQUESTIONDIALOG_H
+
+#include "libfmqtglobals.h"
+#include <QCloseEvent>
+#include <QMessageBox>
+#include <gio/gio.h>
+
+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
--- /dev/null
+/*
+ * Copyright (C) 2016 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef __LIBFM_QT_FM_PATH_H__
+#define __LIBFM_QT_FM_PATH_H__
+
+#include <libfm/fm.h>
+#include <QObject>
+#include <QtGlobal>
+#include <QMetaType>
+#include "libfmqtglobals.h"
+
+
+namespace Fm {
+
+
+class LIBFM_QT_API PathList {
+public:
+
+
+ PathList(void ) {
+ dataPtr_ = reinterpret_cast<FmPathList*>(fm_path_list_new());
+ }
+
+
+ PathList(FmPathList* dataPtr){
+ dataPtr_ = dataPtr != nullptr ? reinterpret_cast<FmPathList*>(fm_list_ref(FM_LIST(dataPtr))) : nullptr;
+ }
+
+
+ // copy constructor
+ PathList(const PathList& other) {
+ dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<FmPathList*>(fm_list_ref(FM_LIST(other.dataPtr_))) : nullptr;
+ }
+
+
+ // move constructor
+ PathList(PathList&& other) {
+ dataPtr_ = reinterpret_cast<FmPathList*>(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<FmPathList*>(dataPtr);
+ return obj;
+ }
+
+ // disown the managed data pointer
+ FmPathList* takeDataPtr() {
+ FmPathList* data = reinterpret_cast<FmPathList*>(dataPtr_);
+ dataPtr_ = nullptr;
+ return data;
+ }
+
+ // get the raw pointer wrapped
+ FmPathList* dataPtr() {
+ return reinterpret_cast<FmPathList*>(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<FmPathList*>(fm_list_ref(FM_LIST(other.dataPtr_))) : nullptr;
+ return *this;
+ }
+
+
+ // move assignment
+ PathList& operator=(PathList&& other) {
+ dataPtr_ = reinterpret_cast<FmPathList*>(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<FmPath*>(fm_path_ref(dataPtr)) : nullptr;
+ }
+
+
+ // copy constructor
+ Path(const Path& other) {
+ dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<FmPath*>(fm_path_ref(other.dataPtr_)) : nullptr;
+ }
+
+
+ // move constructor
+ Path(Path&& other) {
+ dataPtr_ = reinterpret_cast<FmPath*>(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<FmPath*>(dataPtr);
+ return obj;
+ }
+
+ // disown the managed data pointer
+ FmPath* takeDataPtr() {
+ FmPath* data = reinterpret_cast<FmPath*>(dataPtr_);
+ dataPtr_ = nullptr;
+ return data;
+ }
+
+ // get the raw pointer wrapped
+ FmPath* dataPtr() {
+ return reinterpret_cast<FmPath*>(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<FmPath*>(fm_path_ref(other.dataPtr_)) : nullptr;
+ return *this;
+ }
+
+
+ // move assignment
+ Path& operator=(Path&& other) {
+ dataPtr_ = reinterpret_cast<FmPath*>(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__
--- /dev/null
+/*
+ * Copyright (C) 2016 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#include "pathbar.h"
+#include "pathbar_p.h"
+#include <QToolButton>
+#include <QScrollArea>
+#include <QScrollBar>
+#include <QHBoxLayout>
+#include <QResizeEvent>
+#include <QContextMenuEvent>
+#include <QMenu>
+#include <QClipboard>
+#include <QApplication>
+#include <QTimer>
+#include <QDebug>
+#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<PathButton*>(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<PathButton*>(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<PathButton*>(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<PathButton*>(buttonsLayout_->itemAt(i)->widget())) {
+ if(btn->isChecked()) {
+ scrollArea_->ensureWidgetVisible(btn, 1);
+ return;
+ }
+ }
+ }
+}
+
+void PathBar::onScrollButtonClicked() {
+ QToolButton* btn = static_cast<QToolButton*>(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<PathButton*>(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<PathButton*>(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
--- /dev/null
+/*
+ * Copyright (C) 2016 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef FM_PATHBAR_H
+#define FM_PATHBAR_H
+
+#include "libfmqtglobals.h"
+#include <QWidget>
+#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
--- /dev/null
+/*
+ * Copyright (C) 2016 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef FM_PATHBAR_P_H
+#define FM_PATHBAR_P_H
+
+#include <QToolButton>
+#include <QStyle>
+#include <QEvent>
+#include <QMouseEvent>
+#include <QString>
+#include <string>
+
+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
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#include "pathedit.h"
+#include "pathedit_p.h"
+#include <QCompleter>
+#include <QStringListModel>
+#include <QStringBuilder>
+#include <QThread>
+#include <QDebug>
+#include <QKeyEvent>
+#include <QDir>
+#include <libfm/fm.h>
+
+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<QKeyEvent*>(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<PathEditJob*>(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
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_PATHEDIT_H
+#define FM_PATHEDIT_H
+
+#include "libfmqtglobals.h"
+#include <QLineEdit>
+#include <gio/gio.h>
+
+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
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_PATHEDIT_P_H
+#define FM_PATHEDIT_P_H
+
+#include <QObject>
+#include <libfm/fm.h>
+
+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
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#include "placesmodel.h"
+#include "icontheme.h"
+#include <gio/gio.h>
+#include <QDebug>
+#include <QMimeData>
+#include <QTimer>
+#include <QPointer>
+#include <QStandardPaths>
+#include "utilities.h"
+#include "placesmodelitem.h"
+
+namespace Fm {
+
+std::weak_ptr<PlacesModel> 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<PlacesModel> 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<UpdateTrashData*>(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<PlacesModelItem*>(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<PlacesModelItem*>(devicesRoot->child(i, 0));
+ if(item->type() == PlacesModelItem::Volume) {
+ PlacesModelVolumeItem* volumeItem = static_cast<PlacesModelVolumeItem*>(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<PlacesModelItem*>(devicesRoot->child(i, 0));
+ if(item->type() == PlacesModelItem::Mount) {
+ PlacesModelMountItem* mountItem = static_cast<PlacesModelMountItem*>(item);
+ if(mountItem->mount() == mount) {
+ return mountItem;
+ }
+ }
+ }
+ return nullptr;
+}
+
+PlacesModelBookmarkItem* PlacesModel::itemFromBookmark(std::shared_ptr<const Fm::BookmarkItem> bkitem) {
+ int rowCount = bookmarksRoot->rowCount();
+ for(int i = 0; i < rowCount; ++i) {
+ PlacesModelBookmarkItem* item = static_cast<PlacesModelBookmarkItem*>(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<QStandardItem*>() << 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<QStandardItem*>() << 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<PlacesModelItem*>(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> PlacesModel::globalInstance() {
+ auto model = globalInstance_.lock();
+ if(!model) {
+ model = std::make_shared<PlacesModel>();
+ 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 <oldRow>, 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<PlacesModelItem*>(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<PlacesModelBookmarkItem*>(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
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_PLACESMODEL_H
+#define FM_PLACESMODEL_H
+
+#include "libfmqtglobals.h"
+#include <QStandardItemModel>
+#include <QStandardItem>
+#include <QList>
+#include <QAction>
+#include <libfm/fm.h>
+
+#include <memory>
+
+#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<PlacesModel> 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<const Fm::BookmarkItem> 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<Fm::Bookmarks> bookmarks;
+ GVolumeMonitor* volumeMonitor;
+ QList<FmJob*> 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<GMount*> shadowedMounts_;
+
+ static std::weak_ptr<PlacesModel> globalInstance_;
+};
+
+}
+
+#endif // FM_PLACESMODEL_H
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#include "placesmodelitem.h"
+#include "icontheme.h"
+#include <gio/gio.h>
+#include <QPainter>
+
+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<const Fm::IconInfo> 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<const Fm::IconInfo> 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<const Fm::BookmarkItem> 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<GVolume*>(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<GMount*>(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());
+}
+
+}
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_PLACESMODELITEM_H
+#define FM_PLACESMODELITEM_H
+
+#include "libfmqtglobals.h"
+#include <QStandardItemModel>
+#include <QStandardItem>
+#include <QList>
+#include <QAction>
+#include <libfm/fm.h>
+
+#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<const Fm::IconInfo> icon, QString title, Fm::FilePath path = Fm::FilePath{});
+ ~PlacesModelItem();
+
+ const std::shared_ptr<const Fm::FileInfo>& fileInfo() const {
+ return fileInfo_;
+ }
+ void setFileInfo(std::shared_ptr<const Fm::FileInfo> 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<const Fm::IconInfo>& icon() const {
+ return icon_;
+ }
+ void setIcon(std::shared_ptr<const Fm::IconInfo> 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<const Fm::FileInfo> fileInfo_;
+ std::shared_ptr<const Fm::IconInfo> 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<const Fm::BookmarkItem> bm_item);
+ const std::shared_ptr<const Fm::BookmarkItem>& bookmark() const {
+ return bookmarkItem_;
+ }
+private:
+ std::shared_ptr<const Fm::BookmarkItem> bookmarkItem_;
+};
+
+}
+
+#endif // FM_PLACESMODELITEM_H
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#include "placesview.h"
+#include "placesmodel.h"
+#include "placesmodelitem.h"
+#include "mountoperation.h"
+#include "fileoperation.h"
+#include <QMenu>
+#include <QContextMenuEvent>
+#include <QHeaderView>
+#include <QDebug>
+#include <QGuiApplication>
+#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<PlacesModelItem*>(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<PlacesModelVolumeItem*>(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<PlacesModelVolumeItem*>(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<PlacesModelMountItem*>(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<PlacesModelItem*>(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<PlacesModel::ItemAction*>(sender());
+ if(!action->index().isValid()) {
+ return;
+ }
+ PlacesModelBookmarkItem* item = static_cast<PlacesModelBookmarkItem*>(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<PlacesModel::ItemAction*>(sender());
+ if(!action->index().isValid()) {
+ return;
+ }
+ PlacesModelBookmarkItem* item = static_cast<PlacesModelBookmarkItem*>(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<PlacesModel::ItemAction*>(sender());
+ if(!action->index().isValid()) {
+ return;
+ }
+ PlacesModelBookmarkItem* item = static_cast<PlacesModelBookmarkItem*>(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<PlacesModelBookmarkItem*>(model_->itemFromIndex(currentIndex()));
+ auto bookmarkItem = item->bookmark();
+ // rename bookmark
+ Fm::Bookmarks::globalInstance()->rename(bookmarkItem, item->text());
+}
+
+void PlacesView::onOpenNewTab() {
+ PlacesModel::ItemAction* action = static_cast<PlacesModel::ItemAction*>(sender());
+ if(!action->index().isValid()) {
+ return;
+ }
+ PlacesModelItem* item = static_cast<PlacesModelItem*>(model_->itemFromIndex(action->index()));
+ if(item) {
+ Q_EMIT chdirRequested(1, item->path());
+ }
+}
+
+void PlacesView::onOpenNewWindow() {
+ PlacesModel::ItemAction* action = static_cast<PlacesModel::ItemAction*>(sender());
+ if(!action->index().isValid()) {
+ return;
+ }
+ PlacesModelItem* item = static_cast<PlacesModelItem*>(model_->itemFromIndex(action->index()));
+ if(item) {
+ Q_EMIT chdirRequested(2, item->path());
+ }
+}
+
+void PlacesView::onRenameBookmark() {
+ PlacesModel::ItemAction* action = static_cast<PlacesModel::ItemAction*>(sender());
+ if(!action->index().isValid()) {
+ return;
+ }
+ PlacesModelBookmarkItem* item = static_cast<PlacesModelBookmarkItem*>(model_->itemFromIndex(action->index()));
+ setFocus();
+ setCurrentIndex(item->index());
+ edit(item->index());
+}
+
+void PlacesView::onMountVolume() {
+ PlacesModel::ItemAction* action = static_cast<PlacesModel::ItemAction*>(sender());
+ if(!action->index().isValid()) {
+ return;
+ }
+ PlacesModelVolumeItem* item = static_cast<PlacesModelVolumeItem*>(model_->itemFromIndex(action->index()));
+ MountOperation* op = new MountOperation(true, this);
+ op->mount(item->volume());
+ op->wait();
+}
+
+void PlacesView::onUnmountVolume() {
+ PlacesModel::ItemAction* action = static_cast<PlacesModel::ItemAction*>(sender());
+ if(!action->index().isValid()) {
+ return;
+ }
+ PlacesModelVolumeItem* item = static_cast<PlacesModelVolumeItem*>(model_->itemFromIndex(action->index()));
+ MountOperation* op = new MountOperation(true, this);
+ op->unmount(item->volume());
+ op->wait();
+}
+
+void PlacesView::onUnmountMount() {
+ PlacesModel::ItemAction* action = static_cast<PlacesModel::ItemAction*>(sender());
+ if(!action->index().isValid()) {
+ return;
+ }
+ PlacesModelMountItem* item = static_cast<PlacesModelMountItem*>(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<PlacesModel::ItemAction*>(sender());
+ if(!action->index().isValid()) {
+ return;
+ }
+ PlacesModelVolumeItem* item = static_cast<PlacesModelVolumeItem*>(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<PlacesModelItem*>(model_->itemFromIndex(index));
+
+ if(item->type() != PlacesModelItem::Mount
+ && (item->type() != PlacesModelItem::Volume
+ || static_cast<PlacesModelVolumeItem*>(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<PlacesModelVolumeItem*>(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
--- /dev/null
+/*
+ * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_PLACESVIEW_H
+#define FM_PLACESVIEW_H
+
+#include "libfmqtglobals.h"
+#include <QTreeView>
+#include <libfm/fm.h>
+
+#include <memory>
+#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<PlacesModel> model_;
+ Fm::FilePath currentPath_;
+};
+
+}
+
+#endif // FM_PLACESVIEW_H
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#include "proxyfoldermodel.h"
+#include "foldermodel.h"
+#include <QCollator>
+
+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<FolderModel*>(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<FolderModel*>(sourceModel());
+ FolderModel* newSrcModel = static_cast<FolderModel*>(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<FolderModel*>(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<FolderModel*>(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<const Fm::FileInfo> ProxyFolderModel::fileInfoFromIndex(const QModelIndex& index) const {
+ if(index.isValid()) {
+ FolderModel* srcModel = static_cast<FolderModel*>(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<const FileInfo> ProxyFolderModel::fileInfoFromPath(const FilePath &path) const {
+ return fileInfoFromIndex(indexFromPath(path));
+}
+
+void ProxyFolderModel::setCutFiles(const QItemSelection& selection) {
+ FolderModel* srcModel = static_cast<FolderModel*>(sourceModel());
+ if(srcModel) {
+ srcModel->setCutFiles(mapSelectionToSource(selection));
+ }
+}
+
+void ProxyFolderModel::setShowThumbnails(bool show) {
+ if(show != showThumbnails_) {
+ showThumbnails_ = show;
+ FolderModel* srcModel = static_cast<FolderModel*>(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<FolderModel*>(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<FolderModel*>(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<FolderModel*>(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<FolderModel*>(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
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_PROXYFOLDERMODEL_H
+#define FM_PROXYFOLDERMODEL_H
+
+#include "libfmqtglobals.h"
+#include <QSortFilterProxyModel>
+#include <libfm/fm.h>
+#include <QList>
+#include <QCollator>
+
+#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<const Fm::FileInfo>& 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<const Fm::FileInfo> fileInfoFromIndex(const QModelIndex& index) const;
+
+ std::shared_ptr<const Fm::FileInfo> 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<ProxyFolderModelFilter*> filters_;
+};
+
+}
+
+#endif // FM_PROXYFOLDERMODEL_H
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>RenameDialog</class>
+ <widget class="QDialog" name="RenameDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>398</width>
+ <height>220</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Confirm to replace files</string>
+ </property>
+ <property name="sizeGripEnabled">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="spacing">
+ <number>6</number>
+ </property>
+ <property name="margin">
+ <number>10</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string><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></string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QGridLayout" name="gridLayout">
+ <property name="horizontalSpacing">
+ <number>12</number>
+ </property>
+ <property name="verticalSpacing">
+ <number>6</number>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="destIcon">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>dest</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0" colspan="2">
+ <widget class="QLabel" name="label_3">
+ <property name="text">
+ <string>with the following file?</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLabel" name="srcInfo">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>src file info</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="destInfo">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>dest file info</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="srcIcon">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>src</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="spacing">
+ <number>12</number>
+ </property>
+ <item>
+ <widget class="QLabel" name="label_6">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>&File name:</string>
+ </property>
+ <property name="buddy">
+ <cstring>fileName</cstring>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="fileName"/>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="applyToAll">
+ <property name="text">
+ <string>Apply this option to all existing files</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Expanding</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ignore|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>RenameDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>RenameDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#include "renamedialog.h"
+#include "ui_rename-dialog.h"
+#include <QStringBuilder>
+#include <QPushButton>
+#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<QPushButton*>(ui->buttonBox->button(QDialogButtonBox::Ok));
+ overwriteButton->setEnabled(!hasNewName);
+ overwriteButton->setDefault(!hasNewName);
+}
+
+
+} // namespace Fm
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_RENAMEDIALOG_H
+#define FM_RENAMEDIALOG_H
+
+#include "libfmqtglobals.h"
+#include <QDialog>
+#include <libfm/fm.h>
+
+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
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#include "sidepane.h"
+#include <QComboBox>
+#include <QVBoxLayout>
+#include <QHeaderView>
+#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<void (QComboBox::*)(int)>(&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<PlacesView*>(view_)->setIconSize(size);
+ case ModeDirTree:
+ static_cast<QTreeView*>(view_)->setIconSize(size);
+ break;
+ default:
+ ;
+ }
+}
+
+void SidePane::setCurrentPath(Fm::FilePath path) {
+ Q_ASSERT(path);
+ currentPath_ = std::move(path);
+ switch(mode_) {
+ case ModePlaces:
+ static_cast<PlacesView*>(view_)->setCurrentPath(currentPath_);
+ break;
+ case ModeDirTree:
+ static_cast<DirTreeView*>(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<PlacesView*>(view_);
+ return true;
+ case ModeDirTree:
+ // static_cast<PlacesView*>(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<DirTreeView*>(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<DirTreeView*>(view_);
+ DirTreeModel* model = static_cast<DirTreeModel*>(dirTreeView->model());
+ if(model) {
+ model->setShowHidden(showHidden_);
+ }
+ }
+}
+
+} // namespace Fm
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+
+#ifndef FM_SIDEPANE_H
+#define FM_SIDEPANE_H
+
+#include "libfmqtglobals.h"
+#include <libfm/fm.h>
+#include <QWidget>
+
+#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
--- /dev/null
+/*
+ * Copyright (C) 2016 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef __LIBFM_QT_FM_TEMPLATES_H__
+#define __LIBFM_QT_FM_TEMPLATES_H__
+
+#include <libfm/fm.h>
+#include <QObject>
+#include <QtGlobal>
+#include "libfmqtglobals.h"
+
+
+namespace Fm {
+
+
+class LIBFM_QT_API Template {
+public:
+
+
+ // default constructor
+ Template() {
+ dataPtr_ = nullptr;
+ }
+
+
+ Template(FmTemplate* dataPtr){
+ dataPtr_ = dataPtr != nullptr ? reinterpret_cast<GObject*>(g_object_ref(dataPtr)) : nullptr;
+ }
+
+
+ // copy constructor
+ Template(const Template& other) {
+ dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<GObject*>(g_object_ref(other.dataPtr_)) : nullptr;
+ }
+
+
+ // move constructor
+ Template(Template&& other) noexcept {
+ dataPtr_ = reinterpret_cast<GObject*>(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<GObject*>(dataPtr);
+ return obj;
+ }
+
+ // disown the managed data pointer
+ FmTemplate* takeDataPtr() {
+ FmTemplate* data = reinterpret_cast<FmTemplate*>(dataPtr_);
+ dataPtr_ = nullptr;
+ return data;
+ }
+
+ // get the raw pointer wrapped
+ FmTemplate* dataPtr() {
+ return reinterpret_cast<FmTemplate*>(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<GObject*>(g_object_ref(other.dataPtr_)) : nullptr;
+ return *this;
+ }
+
+
+ // move assignment
+ Template& operator=(Template&& other) noexcept {
+ dataPtr_ = reinterpret_cast<GObject*>(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<GObject*>(dataPtr_);
+ }
+
+
+protected:
+ GObject* dataPtr_; // data pointer for the underlying C struct
+
+};
+
+
+}
+
+#endif // __LIBFM_QT_FM_TEMPLATES_H__
--- /dev/null
+#include <QApplication>
+#include <QMainWindow>
+#include <QToolBar>
+#include <QDebug>
+#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;
+}
--- /dev/null
+#include <QApplication>
+#include <QDebug>
+#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<Fm::FileInfoPair>& 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();
+}
--- /dev/null
+#include <QApplication>
+#include <QMainWindow>
+#include <QToolBar>
+#include <QDebug>
+#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();
+}
--- /dev/null
+#include <QApplication>
+#include <QMainWindow>
+#include <QToolBar>
+#include <QDir>
+#include <QDebug>
+#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();
+}
--- /dev/null
+#include <QApplication>
+#include <QDir>
+#include <QDebug>
+#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();
+}
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#include "utilities.h"
+#include "utilities_p.h"
+#include <QApplication>
+#include <QClipboard>
+#include <QMimeData>
+#include <QUrl>
+#include <QList>
+#include <QStringBuilder>
+#include <QMessageBox>
+#include "fileoperation.h"
+#include <QEventLoop>
+
+#include <pwd.h>
+#include <grp.h>
+#include <stdlib.h>
+#include <glib.h>
+
+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<QUrl> 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<Fm::FilePathList, bool> 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<const Fm::FileInfo> 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
--- /dev/null
+/*
+ * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef FM_UTILITIES_H
+#define FM_UTILITIES_H
+
+#include "libfmqtglobals.h"
+#include <QUrl>
+#include <QList>
+#include <QMimeData>
+#include <libfm/fm.h>
+#include <sys/types.h>
+
+#include <cstdint>
+#include <utility>
+
+#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<QUrl> urls);
+
+LIBFM_QT_API std::pair<Fm::FilePathList, bool> 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<const Fm::FileInfo> 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
--- /dev/null
+/*
+ * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef __UTILITS_P_H__
+#define __UTILITS_P_H__
+
+#include <QInputDialog>
+#include <QTimer>
+#include <QLineEdit>
+
+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<QLineEdit*>();
+ 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
--- /dev/null
+/*
+ * Copyright (C) 2016 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#ifndef __LIBFM_QT_FM_UTILS_H__
+#define __LIBFM_QT_FM_UTILS_H__
+
+#include <libfm/fm.h>
+#include <QObject>
+#include <QtGlobal>
+#include "libfmqtglobals.h"
+
+
+namespace Fm {
+
+
+
+}
+
+#endif // __LIBFM_QT_FM_UTILS_H__
--- /dev/null
+/*
+ * Copyright (C) 2016 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#include <QtGlobal>
+#include "xdndworkaround.h"
+#include <QApplication>
+#include <QDebug>
+#include <QX11Info>
+#include <QMimeData>
+#include <QCursor>
+#include <QWidget>
+
+// This part is for Qt >= 5.4 only
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0))
+#include <QDrag>
+#include <QUrl>
+#include <string.h>
+
+// these are private Qt headers which are not part of Qt APIs
+#include <private/qdnd_p.h> // 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<xcb_generic_event_t*>(message);
+ switch(event->response_type & ~0x80) {
+ case XCB_CLIENT_MESSAGE:
+ return clientMessage(reinterpret_cast<xcb_client_message_event_t*>(event));
+ case XCB_SELECTION_NOTIFY:
+ return selectionNotify(reinterpret_cast<xcb_selection_notify_event_t*>(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<xcb_selection_request_event_t*>(event));
+ case XCB_GE_GENERIC:
+ // newer versions of Qt5 supports xinput2, which sends its mouse events via XGE.
+ return genericEvent(reinterpret_cast<xcb_ge_generic_event_t*>(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<QUrl> 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)
--- /dev/null
+/*
+ * Copyright (C) 2016 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+/*
+ * 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 <QtGlobal>
+
+#include <QObject>
+#include <QAbstractNativeEventFilter>
+#include <xcb/xcb.h>
+#include <QByteArray>
+
+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