Import libfm-qt_0.11.2.orig.tar.xz
authorAlf Gaida <agaida@siduction.org>
Thu, 22 Dec 2016 00:32:58 +0000 (00:32 +0000)
committerAlf Gaida <agaida@siduction.org>
Thu, 22 Dec 2016 00:32:58 +0000 (00:32 +0000)
[dgit import orig libfm-qt_0.11.2.orig.tar.xz]

137 files changed:
AUTHORS [new file with mode: 0644]
CHANGELOG [new file with mode: 0644]
CMakeLists.txt [new file with mode: 0644]
Doxyfile.in [new file with mode: 0644]
LICENSE [new file with mode: 0644]
README.md [new file with mode: 0644]
cmake/fm-qt-config.cmake.in [new file with mode: 0644]
src/CMakeLists.txt [new file with mode: 0644]
src/app-chooser-dialog.ui [new file with mode: 0644]
src/appchoosercombobox.cpp [new file with mode: 0644]
src/appchoosercombobox.h [new file with mode: 0644]
src/appchooserdialog.cpp [new file with mode: 0644]
src/appchooserdialog.h [new file with mode: 0644]
src/applaunchcontext.cpp [new file with mode: 0644]
src/applaunchcontext.h [new file with mode: 0644]
src/appmenuview.cpp [new file with mode: 0644]
src/appmenuview.h [new file with mode: 0644]
src/appmenuview_p.h [new file with mode: 0644]
src/archiver.h [new file with mode: 0644]
src/bookmarkaction.cpp [new file with mode: 0644]
src/bookmarkaction.h [new file with mode: 0644]
src/bookmarks.h [new file with mode: 0644]
src/browsehistory.cpp [new file with mode: 0644]
src/browsehistory.h [new file with mode: 0644]
src/cachedfoldermodel.cpp [new file with mode: 0644]
src/cachedfoldermodel.h [new file with mode: 0644]
src/colorbutton.cpp [new file with mode: 0644]
src/colorbutton.h [new file with mode: 0644]
src/config.h [new file with mode: 0644]
src/createnewmenu.cpp [new file with mode: 0644]
src/createnewmenu.h [new file with mode: 0644]
src/customaction_p.h [new file with mode: 0644]
src/deepcountjob.h [new file with mode: 0644]
src/dirlistjob.h [new file with mode: 0644]
src/dirtreemodel.cpp [new file with mode: 0644]
src/dirtreemodel.h [new file with mode: 0644]
src/dirtreemodelitem.cpp [new file with mode: 0644]
src/dirtreemodelitem.h [new file with mode: 0644]
src/dirtreeview.cpp [new file with mode: 0644]
src/dirtreeview.h [new file with mode: 0644]
src/dndactionmenu.cpp [new file with mode: 0644]
src/dndactionmenu.h [new file with mode: 0644]
src/dnddest.cpp [new file with mode: 0644]
src/dnddest.h [new file with mode: 0644]
src/dummymonitor.h [new file with mode: 0644]
src/edit-bookmarks.ui [new file with mode: 0644]
src/editbookmarksdialog.cpp [new file with mode: 0644]
src/editbookmarksdialog.h [new file with mode: 0644]
src/exec-file.ui [new file with mode: 0644]
src/execfiledialog.cpp [new file with mode: 0644]
src/execfiledialog_p.h [new file with mode: 0644]
src/file-operation-dialog.ui [new file with mode: 0644]
src/file-props.ui [new file with mode: 0644]
src/file.h [new file with mode: 0644]
src/fileinfo.h [new file with mode: 0644]
src/fileinfojob.h [new file with mode: 0644]
src/filelauncher.cpp [new file with mode: 0644]
src/filelauncher.h [new file with mode: 0644]
src/filemenu.cpp [new file with mode: 0644]
src/filemenu.h [new file with mode: 0644]
src/filemenu_p.h [new file with mode: 0644]
src/fileoperation.cpp [new file with mode: 0644]
src/fileoperation.h [new file with mode: 0644]
src/fileoperationdialog.cpp [new file with mode: 0644]
src/fileoperationdialog.h [new file with mode: 0644]
src/fileopsjob.h [new file with mode: 0644]
src/filepropsdialog.cpp [new file with mode: 0644]
src/filepropsdialog.h [new file with mode: 0644]
src/filesearch.ui [new file with mode: 0644]
src/filesearchdialog.cpp [new file with mode: 0644]
src/filesearchdialog.h [new file with mode: 0644]
src/fm-search.c [new file with mode: 0644]
src/fm-search.h [new file with mode: 0644]
src/folder.h [new file with mode: 0644]
src/folderconfig.h [new file with mode: 0644]
src/folderitemdelegate.cpp [new file with mode: 0644]
src/folderitemdelegate.h [new file with mode: 0644]
src/foldermenu.cpp [new file with mode: 0644]
src/foldermenu.h [new file with mode: 0644]
src/foldermodel.cpp [new file with mode: 0644]
src/foldermodel.h [new file with mode: 0644]
src/foldermodelitem.cpp [new file with mode: 0644]
src/foldermodelitem.h [new file with mode: 0644]
src/folderview.cpp [new file with mode: 0644]
src/folderview.h [new file with mode: 0644]
src/folderview_p.h [new file with mode: 0644]
src/fontbutton.cpp [new file with mode: 0644]
src/fontbutton.h [new file with mode: 0644]
src/icon.h [new file with mode: 0644]
src/icontheme.cpp [new file with mode: 0644]
src/icontheme.h [new file with mode: 0644]
src/job.h [new file with mode: 0644]
src/libfm-qt.pc.in [new file with mode: 0644]
src/libfmqt.cpp [new file with mode: 0644]
src/libfmqt.h [new file with mode: 0644]
src/libfmqtglobals.h [new file with mode: 0644]
src/list.h [new file with mode: 0644]
src/mimetype.h [new file with mode: 0644]
src/mount-operation-password.ui [new file with mode: 0644]
src/mountoperation.cpp [new file with mode: 0644]
src/mountoperation.h [new file with mode: 0644]
src/mountoperationpassworddialog.cpp [new file with mode: 0644]
src/mountoperationpassworddialog_p.h [new file with mode: 0644]
src/mountoperationquestiondialog.cpp [new file with mode: 0644]
src/mountoperationquestiondialog_p.h [new file with mode: 0644]
src/navhistory.h [new file with mode: 0644]
src/path.h [new file with mode: 0644]
src/pathbar.cpp [new file with mode: 0644]
src/pathbar.h [new file with mode: 0644]
src/pathbar_p.h [new file with mode: 0644]
src/pathedit.cpp [new file with mode: 0644]
src/pathedit.h [new file with mode: 0644]
src/pathedit_p.h [new file with mode: 0644]
src/placesmodel.cpp [new file with mode: 0644]
src/placesmodel.h [new file with mode: 0644]
src/placesmodelitem.cpp [new file with mode: 0644]
src/placesmodelitem.h [new file with mode: 0644]
src/placesview.cpp [new file with mode: 0644]
src/placesview.h [new file with mode: 0644]
src/proxyfoldermodel.cpp [new file with mode: 0644]
src/proxyfoldermodel.h [new file with mode: 0644]
src/rename-dialog.ui [new file with mode: 0644]
src/renamedialog.cpp [new file with mode: 0644]
src/renamedialog.h [new file with mode: 0644]
src/sidepane.cpp [new file with mode: 0644]
src/sidepane.h [new file with mode: 0644]
src/templates.h [new file with mode: 0644]
src/terminal.h [new file with mode: 0644]
src/thumbnailer.h [new file with mode: 0644]
src/thumbnailloader.cpp [new file with mode: 0644]
src/thumbnailloader.h [new file with mode: 0644]
src/utilities.cpp [new file with mode: 0644]
src/utilities.h [new file with mode: 0644]
src/utilities_p.h [new file with mode: 0644]
src/utils.h [new file with mode: 0644]
src/xdndworkaround.cpp [new file with mode: 0644]
src/xdndworkaround.h [new file with mode: 0644]

diff --git a/AUTHORS b/AUTHORS
new file mode 100644 (file)
index 0000000..83cd2cd
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,6 @@
+Upstream Authors:
+    LXQt team: http://lxqt.org
+    Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+
+Copyright:
+    Copyright (c) 2013-2016 LXQt team
diff --git a/CHANGELOG b/CHANGELOG
new file mode 100644 (file)
index 0000000..3277cb2
--- /dev/null
+++ b/CHANGELOG
@@ -0,0 +1,396 @@
+
+libfm-qt-0.11.2 / 2016-12-21
+============================
+
+  * 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.
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644 (file)
index 0000000..d8f068d
--- /dev/null
@@ -0,0 +1,87 @@
+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 11)
+set(LIBFM_QT_VERSION_PATCH 2)
+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.3.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(XCB REQUIRED)
+
+message(STATUS "Building ${PROJECT_NAME} with Qt ${Qt5Core_VERSION_STRING}")
+
+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 Doxygen support to generate API docs
+# References:
+# http://majewsky.wordpress.com/2010/08/14/tip-of-the-day-cmake-and-doxygen/
+# http://www.bluequartz.net/projects/EIM_Segmentation/SoftwareDocumentation/html/usewithcmakeproject.html
+option(BUILD_DOCUMENTATION "Use Doxygen to create the HTML based API documentation" OFF)
+if(BUILD_DOCUMENTATION)
+    find_package(Doxygen REQUIRED)
+    configure_file("${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in" "${CMAKE_CURRENT_BINARY_DIR}/Doxyfile" @ONLY)
+    add_custom_target(doc ALL
+        ${DOXYGEN_EXECUTABLE} "${CMAKE_CURRENT_BINARY_DIR}/Doxyfile"
+        WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
+        COMMENT "Generating API documentation with Doxygen" VERBATIM
+    )
+    install(DIRECTORY
+        "${CMAKE_CURRENT_BINARY_DIR}/docs"
+        DESTINATION "${CMAKE_INSTALL_DOCDIR}"
+        COMPONENT Devel
+    )
+endif()
diff --git a/Doxyfile.in b/Doxyfile.in
new file mode 100644 (file)
index 0000000..b7961be
--- /dev/null
@@ -0,0 +1,1890 @@
+# Doxyfile 1.8.4
+
+# This file describes the settings to be used by the documentation system
+# doxygen (www.doxygen.org) for a project.
+#
+# All text after a double hash (##) is considered a comment and is placed
+# in front of the TAG it is preceding .
+# All text after a hash (#) is considered a comment and will be ignored.
+# The format is:
+#       TAG = value [value, ...]
+# For lists items can also be appended using:
+#       TAG + = value [value, ...]
+# Values that contain spaces should be placed between quotes (" ").
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+
+# This tag specifies the encoding used for all characters in the config file
+# that follow. The default is UTF-8 which is also the encoding used for all
+# text before the first occurrence of this tag. Doxygen uses libiconv (or the
+# iconv built into libc) for the transcoding. See
+# http://www.gnu.org/software/libiconv for the list of possible encodings.
+
+DOXYFILE_ENCODING = UTF-8
+
+# The PROJECT_NAME tag is a single word (or sequence of words) that should
+# identify the project. Note that if you do not use Doxywizard you need
+# to put quotes around the project name if it contains spaces.
+
+PROJECT_NAME = "libfm-qt"
+
+# The PROJECT_NUMBER tag can be used to enter a project or revision number.
+# This could be handy for archiving the generated documentation or
+# if some version control system is used.
+
+PROJECT_NUMBER = 
+
+# Using the PROJECT_BRIEF tag one can provide an optional one line description
+# for a project that appears at the top of each page and should give viewer
+# a quick idea about the purpose of the project. Keep the description short.
+
+PROJECT_BRIEF = 
+
+# With the PROJECT_LOGO tag one can specify an logo or icon that is
+# included in the documentation. The maximum height of the logo should not
+# exceed 55 pixels and the maximum width should not exceed 200 pixels.
+# Doxygen will copy the logo to the output directory.
+
+PROJECT_LOGO = 
+
+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute)
+# base path where the generated documentation will be put.
+# If a relative path is entered, it will be relative to the location
+# where doxygen was started. If left blank the current directory will be used.
+
+OUTPUT_DIRECTORY = @PROJECT_BINARY_DIR@/docs
+
+# If the CREATE_SUBDIRS tag is set to YES, then doxygen will create
+# 4096 sub-directories (in 2 levels) under the output directory of each output
+# format and will distribute the generated files over these directories.
+# Enabling this option can be useful when feeding doxygen a huge amount of
+# source files, where putting all generated files in the same directory would
+# otherwise cause performance problems for the file system.
+
+CREATE_SUBDIRS = NO
+
+# The OUTPUT_LANGUAGE tag is used to specify the language in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all constant output in the proper language.
+# The default language is English, other supported languages are:
+# Afrikaans, Arabic, Brazilian, Catalan, Chinese, Chinese-Traditional,
+# Croatian, Czech, Danish, Dutch, Esperanto, Farsi, Finnish, French, German,
+# Greek, Hungarian, Italian, Japanese, Japanese-en (Japanese with English
+# messages), Korean, Korean-en, Latvian, Lithuanian, Norwegian, Macedonian,
+# Persian, Polish, Portuguese, Romanian, Russian, Serbian, Serbian-Cyrillic,
+# Slovak, Slovene, Spanish, Swedish, Ukrainian, and Vietnamese.
+
+OUTPUT_LANGUAGE = English
+
+# If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will
+# include brief member descriptions after the members that are listed in
+# the file and class documentation (similar to JavaDoc).
+# Set to NO to disable this.
+
+BRIEF_MEMBER_DESC = YES
+
+# If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend
+# the brief description of a member or function before the detailed description.
+# Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the
+# brief descriptions will be completely suppressed.
+
+REPEAT_BRIEF = YES
+
+# This tag implements a quasi-intelligent brief description abbreviator
+# that is used to form the text in various listings. Each string
+# in this list, if found as the leading text of the brief description, will be
+# stripped from the text and the result after processing the whole list, is
+# used as the annotated text. Otherwise, the brief description is used as-is.
+# If left blank, the following values are used ("$name" is automatically
+# replaced with the name of the entity): "The $name class" "The $name widget"
+# "The $name file" "is" "provides" "specifies" "contains"
+# "represents" "a" "an" "the"
+
+ABBREVIATE_BRIEF = 
+
+# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then
+# Doxygen will generate a detailed section even if there is only a brief
+# description.
+
+ALWAYS_DETAILED_SEC = NO
+
+# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
+# inherited members of a class in the documentation of that class as if those
+# members were ordinary class members. Constructors, destructors and assignment
+# operators of the base classes will not be shown.
+
+INLINE_INHERITED_MEMB = NO
+
+# If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full
+# path before files name in the file list and in the header files. If set
+# to NO the shortest path that makes the file name unique will be used.
+
+FULL_PATH_NAMES = NO
+
+# If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag
+# can be used to strip a user-defined part of the path. Stripping is
+# only done if one of the specified strings matches the left-hand part of
+# the path. The tag can be used to show relative paths in the file list.
+# If left blank the directory from which doxygen is run is used as the
+# path to strip. Note that you specify absolute paths here, but also
+# relative paths, which will be relative from the directory where doxygen is
+# started.
+
+STRIP_FROM_PATH = 
+
+# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of
+# the path mentioned in the documentation of a class, which tells
+# the reader which header file to include in order to use a class.
+# If left blank only the name of the header file containing the class
+# definition is used. Otherwise one should specify the include paths that
+# are normally passed to the compiler using the -I flag.
+
+STRIP_FROM_INC_PATH = 
+
+# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter
+# (but less readable) file names. This can be useful if your file system
+# doesn't support long names like on DOS, Mac, or CD-ROM.
+
+SHORT_NAMES = NO
+
+# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen
+# will interpret the first line (until the first dot) of a JavaDoc-style
+# comment as the brief description. If set to NO, the JavaDoc
+# comments will behave just like regular Qt-style comments
+# (thus requiring an explicit @brief command for a brief description.)
+
+JAVADOC_AUTOBRIEF = NO
+
+# If the QT_AUTOBRIEF tag is set to YES then Doxygen will
+# interpret the first line (until the first dot) of a Qt-style
+# comment as the brief description. If set to NO, the comments
+# will behave just like regular Qt-style comments (thus requiring
+# an explicit \brief command for a brief description.)
+
+QT_AUTOBRIEF = NO
+
+# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen
+# treat a multi-line C++ special comment block (i.e. a block of //! or ///
+# comments) as a brief description. This used to be the default behaviour.
+# The new default is to treat a multi-line C++ comment block as a detailed
+# description. Set this tag to YES if you prefer the old behaviour instead.
+
+MULTILINE_CPP_IS_BRIEF = NO
+
+# If the INHERIT_DOCS tag is set to YES (the default) then an undocumented
+# member inherits the documentation from any documented member that it
+# re-implements.
+
+INHERIT_DOCS = YES
+
+# If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce
+# a new page for each member. If set to NO, the documentation of a member will
+# be part of the file/class/namespace that contains it.
+
+SEPARATE_MEMBER_PAGES = NO
+
+# The TAB_SIZE tag can be used to set the number of spaces in a tab.
+# Doxygen uses this value to replace tabs by spaces in code fragments.
+
+TAB_SIZE = 4
+
+# This tag can be used to specify a number of aliases that acts
+# as commands in the documentation. An alias has the form "name = value".
+# For example adding "sideeffect = \par Side Effects:\n" will allow you to
+# put the command \sideeffect (or @sideeffect) in the documentation, which
+# will result in a user-defined paragraph with heading "Side Effects:".
+# You can put \n's in the value part of an alias to insert newlines.
+
+ALIASES = 
+
+# This tag can be used to specify a number of word-keyword mappings (TCL only).
+# A mapping has the form "name = value". For example adding
+# "class = itcl::class" will allow you to use the command class in the
+# itcl::class meaning.
+
+TCL_SUBST = 
+
+# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C
+# sources only. Doxygen will then generate output that is more tailored for C.
+# For instance, some of the names that are used will be different. The list
+# of all members will be omitted, etc.
+
+OPTIMIZE_OUTPUT_FOR_C = NO
+
+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java
+# sources only. Doxygen will then generate output that is more tailored for
+# Java. For instance, namespaces will be presented as packages, qualified
+# scopes will look different, etc.
+
+OPTIMIZE_OUTPUT_JAVA = NO
+
+# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran
+# sources only. Doxygen will then generate output that is more tailored for
+# Fortran.
+
+OPTIMIZE_FOR_FORTRAN = NO
+
+# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL
+# sources. Doxygen will then generate output that is tailored for
+# VHDL.
+
+OPTIMIZE_OUTPUT_VHDL = NO
+
+# Doxygen selects the parser to use depending on the extension of the files it
+# parses. With this tag you can assign which parser to use for a given
+# extension. Doxygen has a built-in mapping, but you can override or extend it
+# using this tag. The format is ext = language, where ext is a file extension,
+# and language is one of the parsers supported by doxygen: IDL, Java,
+# Javascript, CSharp, C, C++, D, PHP, Objective-C, Python, Fortran, VHDL, C,
+# C++. For instance to make doxygen treat .inc files as Fortran files (default
+# is PHP), and .f files as C (default is Fortran), use: inc = Fortran f = C. Note
+# that for custom extensions you also need to set FILE_PATTERNS otherwise the
+# files are not read by doxygen.
+
+EXTENSION_MAPPING = 
+
+# If MARKDOWN_SUPPORT is enabled (the default) then doxygen pre-processes all
+# comments according to the Markdown format, which allows for more readable
+# documentation. See http://daringfireball.net/projects/markdown/ for details.
+# The output of markdown processing is further processed by doxygen, so you
+# can mix doxygen, HTML, and XML commands with Markdown formatting.
+# Disable only in case of backward compatibilities issues.
+
+MARKDOWN_SUPPORT = YES
+
+# When enabled doxygen tries to link words that correspond to documented
+# classes, or namespaces to their corresponding documentation. Such a link can
+# be prevented in individual cases by by putting a % sign in front of the word
+# or globally by setting AUTOLINK_SUPPORT to NO.
+
+AUTOLINK_SUPPORT = YES
+
+# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want
+# to include (a tag file for) the STL sources as input, then you should
+# set this tag to YES in order to let doxygen match functions declarations and
+# definitions whose arguments contain STL classes (e.g. func(std::string); v.s.
+# func(std::string) {}). This also makes the inheritance and collaboration
+# diagrams that involve STL classes more complete and accurate.
+
+BUILTIN_STL_SUPPORT = NO
+
+# If you use Microsoft's C++/CLI language, you should set this option to YES to
+# enable parsing support.
+
+CPP_CLI_SUPPORT = NO
+
+# Set the SIP_SUPPORT tag to YES if your project consists of sip sources only.
+# Doxygen will parse them like normal C++ but will assume all classes use public
+# instead of private inheritance when no explicit protection keyword is present.
+
+SIP_SUPPORT = NO
+
+# For Microsoft's IDL there are propget and propput attributes to indicate
+# getter and setter methods for a property. Setting this option to YES (the
+# default) will make doxygen replace the get and set methods by a property in
+# the documentation. This will only work if the methods are indeed getting or
+# setting a simple type. If this is not the case, or you want to show the
+# methods anyway, you should set this option to NO.
+
+IDL_PROPERTY_SUPPORT = YES
+
+# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
+# tag is set to YES, then doxygen will reuse the documentation of the first
+# member in the group (if any) for the other members of the group. By default
+# all members of a group must be documented explicitly.
+
+DISTRIBUTE_GROUP_DOC = NO
+
+# Set the SUBGROUPING tag to YES (the default) to allow class member groups of
+# the same type (for instance a group of public functions) to be put as a
+# subgroup of that type (e.g. under the Public Functions section). Set it to
+# NO to prevent subgrouping. Alternatively, this can be done per class using
+# the \nosubgrouping command.
+
+SUBGROUPING = YES
+
+# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and
+# unions are shown inside the group in which they are included (e.g. using
+# @ingroup) instead of on a separate page (for HTML and Man pages) or
+# section (for LaTeX and RTF).
+
+INLINE_GROUPED_CLASSES = NO
+
+# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and
+# unions with only public data fields or simple typedef fields will be shown
+# inline in the documentation of the scope in which they are defined (i.e. file,
+# namespace, or group documentation), provided this scope is documented. If set
+# to NO (the default), structs, classes, and unions are shown on a separate
+# page (for HTML and Man pages) or section (for LaTeX and RTF).
+
+INLINE_SIMPLE_STRUCTS = NO
+
+# When TYPEDEF_HIDES_STRUCT is enabled, a typedef of a struct, union, or enum
+# is documented as struct, union, or enum with the name of the typedef. So
+# typedef struct TypeS {} TypeT, will appear in the documentation as a struct
+# with name TypeT. When disabled the typedef will appear as a member of a file,
+# namespace, or class. And the struct will be named TypeS. This can typically
+# be useful for C code in case the coding convention dictates that all compound
+# types are typedef'ed and only the typedef is referenced, never the tag name.
+
+TYPEDEF_HIDES_STRUCT = NO
+
+# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This
+# cache is used to resolve symbols given their name and scope. Since this can
+# be an expensive process and often the same symbol appear multiple times in
+# the code, doxygen keeps a cache of pre-resolved symbols. If the cache is too
+# small doxygen will become slower. If the cache is too large, memory is wasted.
+# The cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid
+# range is 0..9, the default is 0, corresponding to a cache size of 2^16 = 65536
+# symbols.
+
+LOOKUP_CACHE_SIZE = 0
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in
+# documentation are documented, even if no documentation was available.
+# Private class members and static file members will be hidden unless
+# the EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES
+
+EXTRACT_ALL = NO
+
+# If the EXTRACT_PRIVATE tag is set to YES all private members of a class
+# will be included in the documentation.
+
+EXTRACT_PRIVATE = NO
+
+# If the EXTRACT_PACKAGE tag is set to YES all members with package or internal
+# scope will be included in the documentation.
+
+EXTRACT_PACKAGE = NO
+
+# If the EXTRACT_STATIC tag is set to YES all static members of a file
+# will be included in the documentation.
+
+EXTRACT_STATIC = NO
+
+# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs)
+# defined locally in source files will be included in the documentation.
+# If set to NO only classes defined in header files are included.
+
+EXTRACT_LOCAL_CLASSES = YES
+
+# This flag is only useful for Objective-C code. When set to YES local
+# methods, which are defined in the implementation section but not in
+# the interface are included in the documentation.
+# If set to NO (the default) only methods in the interface are included.
+
+EXTRACT_LOCAL_METHODS = NO
+
+# If this flag is set to YES, the members of anonymous namespaces will be
+# extracted and appear in the documentation as a namespace called
+# 'anonymous_namespace{file}', where file will be replaced with the base
+# name of the file that contains the anonymous namespace. By default
+# anonymous namespaces are hidden.
+
+EXTRACT_ANON_NSPACES = NO
+
+# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all
+# undocumented members of documented classes, files or namespaces.
+# If set to NO (the default) these members will be included in the
+# various overviews, but no documentation section is generated.
+# This option has no effect if EXTRACT_ALL is enabled.
+
+HIDE_UNDOC_MEMBERS = NO
+
+# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all
+# undocumented classes that are normally visible in the class hierarchy.
+# If set to NO (the default) these classes will be included in the various
+# overviews. This option has no effect if EXTRACT_ALL is enabled.
+
+HIDE_UNDOC_CLASSES = NO
+
+# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all
+# friend (class|struct|union) declarations.
+# If set to NO (the default) these declarations will be included in the
+# documentation.
+
+HIDE_FRIEND_COMPOUNDS = NO
+
+# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any
+# documentation blocks found inside the body of a function.
+# If set to NO (the default) these blocks will be appended to the
+# function's detailed documentation block.
+
+HIDE_IN_BODY_DOCS = NO
+
+# The INTERNAL_DOCS tag determines if documentation
+# that is typed after a \internal command is included. If the tag is set
+# to NO (the default) then the documentation will be excluded.
+# Set it to YES to include the internal documentation.
+
+INTERNAL_DOCS = NO
+
+# If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate
+# file names in lower-case letters. If set to YES upper-case letters are also
+# allowed. This is useful if you have classes or files whose names only differ
+# in case and if your file system supports case sensitive file names. Windows
+# and Mac users are advised to set this option to NO.
+
+CASE_SENSE_NAMES = YES
+
+# If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen
+# will show members with their full class and namespace scopes in the
+# documentation. If set to YES the scope will be hidden.
+
+HIDE_SCOPE_NAMES = NO
+
+# If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen
+# will put a list of the files that are included by a file in the documentation
+# of that file.
+
+SHOW_INCLUDE_FILES = YES
+
+# If the FORCE_LOCAL_INCLUDES tag is set to YES then Doxygen
+# will list include files with double quotes in the documentation
+# rather than with sharp brackets.
+
+FORCE_LOCAL_INCLUDES = NO
+
+# If the INLINE_INFO tag is set to YES (the default) then a tag [inline]
+# is inserted in the documentation for inline members.
+
+INLINE_INFO = YES
+
+# If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen
+# will sort the (detailed) documentation of file and class members
+# alphabetically by member name. If set to NO the members will appear in
+# declaration order.
+
+SORT_MEMBER_DOCS = YES
+
+# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the
+# brief documentation of file, namespace and class members alphabetically
+# by member name. If set to NO (the default) the members will appear in
+# declaration order.
+
+SORT_BRIEF_DOCS = NO
+
+# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen
+# will sort the (brief and detailed) documentation of class members so that
+# constructors and destructors are listed first. If set to NO (the default)
+# the constructors will appear in the respective orders defined by
+# SORT_MEMBER_DOCS and SORT_BRIEF_DOCS.
+# This tag will be ignored for brief docs if SORT_BRIEF_DOCS is set to NO
+# and ignored for detailed docs if SORT_MEMBER_DOCS is set to NO.
+
+SORT_MEMBERS_CTORS_1ST = NO
+
+# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the
+# hierarchy of group names into alphabetical order. If set to NO (the default)
+# the group names will appear in their defined order.
+
+SORT_GROUP_NAMES = NO
+
+# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be
+# sorted by fully-qualified names, including namespaces. If set to
+# NO (the default), the class list will be sorted only by class name,
+# not including the namespace part.
+# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.
+# Note: This option applies only to the class list, not to the
+# alphabetical list.
+
+SORT_BY_SCOPE_NAME = NO
+
+# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to
+# do proper type resolution of all parameters of a function it will reject a
+# match between the prototype and the implementation of a member function even
+# if there is only one candidate or it is obvious which candidate to choose
+# by doing a simple string match. By disabling STRICT_PROTO_MATCHING doxygen
+# will still accept a match between prototype and implementation in such cases.
+
+STRICT_PROTO_MATCHING = NO
+
+# The GENERATE_TODOLIST tag can be used to enable (YES) or
+# disable (NO) the todo list. This list is created by putting \todo
+# commands in the documentation.
+
+GENERATE_TODOLIST = YES
+
+# The GENERATE_TESTLIST tag can be used to enable (YES) or
+# disable (NO) the test list. This list is created by putting \test
+# commands in the documentation.
+
+GENERATE_TESTLIST = YES
+
+# The GENERATE_BUGLIST tag can be used to enable (YES) or
+# disable (NO) the bug list. This list is created by putting \bug
+# commands in the documentation.
+
+GENERATE_BUGLIST = YES
+
+# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or
+# disable (NO) the deprecated list. This list is created by putting
+# \deprecated commands in the documentation.
+
+GENERATE_DEPRECATEDLIST = YES
+
+# The ENABLED_SECTIONS tag can be used to enable conditional
+# documentation sections, marked by \if section-label ... \endif
+# and \cond section-label ... \endcond blocks.
+
+ENABLED_SECTIONS = 
+
+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines
+# the initial value of a variable or macro consists of for it to appear in
+# the documentation. If the initializer consists of more lines than specified
+# here it will be hidden. Use a value of 0 to hide initializers completely.
+# The appearance of the initializer of individual variables and macros in the
+# documentation can be controlled using \showinitializer or \hideinitializer
+# command in the documentation regardless of this setting.
+
+MAX_INITIALIZER_LINES = 30
+
+# Set the SHOW_USED_FILES tag to NO to disable the list of files generated
+# at the bottom of the documentation of classes and structs. If set to YES the
+# list will mention the files that were used to generate the documentation.
+
+SHOW_USED_FILES = YES
+
+# Set the SHOW_FILES tag to NO to disable the generation of the Files page.
+# This will remove the Files entry from the Quick Index and from the
+# Folder Tree View (if specified). The default is YES.
+
+SHOW_FILES = YES
+
+# Set the SHOW_NAMESPACES tag to NO to disable the generation of the
+# Namespaces page.
+# This will remove the Namespaces entry from the Quick Index
+# and from the Folder Tree View (if specified). The default is YES.
+
+SHOW_NAMESPACES = YES
+
+# The FILE_VERSION_FILTER tag can be used to specify a program or script that
+# doxygen should invoke to get the current version for each file (typically from
+# the version control system). Doxygen will invoke the program by executing (via
+# popen()) the command <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
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..20fb9c7
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,458 @@
+                  GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL.  It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it.  You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+  When we speak of free software, we are referring to freedom of use,
+not price.  Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+  To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights.  These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  To protect each distributor, we want to make it very clear that
+there is no warranty for the free library.  Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+  Finally, software patents pose a constant threat to the existence of
+any free program.  We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder.  Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+  Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License.  This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License.  We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+  When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library.  The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom.  The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+  We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License.  It also provides other free software developers Less
+of an advantage over competing non-free programs.  These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries.  However, the Lesser license provides advantages in certain
+special circumstances.
+
+  For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard.  To achieve this, non-free programs must be
+allowed to use the library.  A more frequent case is that a free
+library does the same job as widely used non-free libraries.  In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+  In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software.  For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+  Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+                  GNU LESSER GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+  6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Use a suitable shared library mechanism for linking with the
+    Library.  A suitable mechanism is one that (1) uses at run time a
+    copy of the library already present on the user's computer system,
+    rather than copying library functions into the executable, and (2)
+    will operate properly with a modified version of the library, if
+    the user installs one, as long as the modified version is
+    interface-compatible with the version that the work was made with.
+
+    c) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    d) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    e) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+                            NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..29ec1b8
--- /dev/null
+++ b/README.md
@@ -0,0 +1,27 @@
+# libfm-qt
+
+## Overview
+
+libfm-qt is the Qt port of libfm, a library providing components to build desktop file managers which belongs to [LXDE](http://lxde.org).
+
+libfm-qt is licensed under the terms of the [LGPLv2.1](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html) or any later version. See file LICENSE for its full text.   
+
+## Installation
+
+### Compiling source code
+
+Runtime dependencies are Qt X11 Extras and libfm ≥ 1,2 (not all features are provided by libfm-qt yet).   
+Additional build dependencies are CMake, [lxqt-build-tools](https://github.com/lxde/lxqt-build-tools) and optionally Git to pull latest VCS checkouts. The localization files were outsourced to repository [lxqt-l10n](https://github.com/lxde/lxqt-l10n) so the corresponding dependencies are needed, too. Please refer to this repository's `README.md` for further information.   
+
+Code configuration is handled by CMake. CMake variable `CMAKE_INSTALL_PREFIX` has to be set to `/usr` on most operating systems, depending on the way library paths are dealt with on 64bit systems variables like `CMAKE_INSTALL_LIBDIR` may have to be set as well.   
+
+To build run `make`, to install `make install` which accepts variable `DESTDIR` as usual.   
+
+### Binary packages
+
+Official binary packages are available in Arch Linux, Debian (as of Debian stretch) and openSUSE (Leap 42.1 and Tumbleweed).   
+The library is still missing in Fedora which is providing version 0.10.0 of PCManFM-Qt only so far. This version was still including the code outsourced into libfm-qt later so libfm-qt will have to be provided by Fedora, too, as soon as the distribution upgrades to PCManFM-Qt ≥ 0.10.1.   
+
+## Development
+
+Issues should go to the tracker of PCManFM-Qt at https://github.com/lxde/pcmanfm-qt/issues.
diff --git a/cmake/fm-qt-config.cmake.in b/cmake/fm-qt-config.cmake.in
new file mode 100644 (file)
index 0000000..a44783f
--- /dev/null
@@ -0,0 +1,41 @@
+#=============================================================================
+# 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()
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
new file mode 100644 (file)
index 0000000..5ea17d3
--- /dev/null
@@ -0,0 +1,196 @@
+set(libfm_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
+    thumbnailloader.cpp
+    execfiledialog.cpp
+    appchoosercombobox.cpp
+    appmenuview.cpp
+    appchooserdialog.cpp
+    filesearchdialog.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
+)
+
+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}
+)
+
+# only turn on custom actions support if it is enabled in libfm.
+if(EXISTS "${FM_INCLUDE_DIR}/libfm/fm-actions.h")
+    target_compile_definitions(${LIBFM_QT_LIBRARY_NAME} PRIVATE CUSTOM_ACTIONS)
+endif()
+
+install(EXPORT
+    "${LIBFM_QT_LIBRARY_NAME}-targets"
+    DESTINATION "${CMAKE_INSTALL_DATADIR}/cmake/${LIBFM_QT_LIBRARY_NAME}"
+    COMPONENT Devel
+)
+
+target_link_libraries(${LIBFM_QT_LIBRARY_NAME}
+    Qt5::Widgets
+    Qt5::X11Extras
+    ${FM_LIBRARIES}
+    ${MENUCACHE_LIBRARIES}
+    ${XCB_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}"
+    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
+)
+
+file(GLOB libfm_HS "${CMAKE_CURRENT_SOURCE_DIR}/*.h")
+# install include header files (FIXME: can we make this cleaner? should dir name be versioned?)
+install(FILES ${libfm_HS}
+    DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/libfm-qt"
+    COMPONENT Devel
+)
+
+generate_export_header(${LIBFM_QT_LIBRARY_NAME}
+    EXPORT_MACRO_NAME LIBFM_QT_API
+)
+
+# InTree build
+file(COPY ${libfm_HS} ${CMAKE_CURRENT_BINARY_DIR}/${LIBFM_QT_LIBRARY_NAME}_export.h
+    DESTINATION "${LIBFM_QT_INTREE_INCLUDE_DIR}/libfm-qt"
+)
+
+
+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)
diff --git a/src/app-chooser-dialog.ui b/src/app-chooser-dialog.ui
new file mode 100644 (file)
index 0000000..ef1ebee
--- /dev/null
@@ -0,0 +1,183 @@
+<?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>&lt;b&gt;These special codes can be used in the command line:&lt;/b&gt;
+&lt;ul&gt;
+&lt;li&gt;&lt;b&gt;%f&lt;/b&gt;: Represents a single file name&lt;/li&gt;
+&lt;li&gt;&lt;b&gt;%F&lt;/b&gt;: Represents multiple file names&lt;/li&gt;
+&lt;li&gt;&lt;b&gt;%u&lt;/b&gt;: Represents a single URI of the file&lt;/li&gt;
+&lt;li&gt;&lt;b&gt;%U&lt;/b&gt;: Represents multiple URIs&lt;/li&gt;
+&lt;/ul&gt;</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>
diff --git a/src/appchoosercombobox.cpp b/src/appchoosercombobox.cpp
new file mode 100644 (file)
index 0000000..bdd6cfe
--- /dev/null
@@ -0,0 +1,153 @@
+/*
+ * 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"
+
+namespace Fm {
+
+AppChooserComboBox::AppChooserComboBox(QWidget* parent):
+  QComboBox(parent),
+  mimeType_(NULL),
+  appInfos_(NULL),
+  defaultApp_(NULL),
+  defaultAppIndex_(-1),
+  prevIndex_(0),
+  blockOnCurrentIndexChanged_(false) {
+
+  // the new Qt5 signal/slot syntax cannot handle overloaded methods by default
+  // hence a type-casting is needed here. really ugly!
+  // reference: http://qt-project.org/forums/viewthread/21513
+  connect((QComboBox*)this, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &AppChooserComboBox::onCurrentIndexChanged);
+}
+
+AppChooserComboBox::~AppChooserComboBox() {
+  if(mimeType_)
+    fm_mime_type_unref(mimeType_);
+  if(defaultApp_)
+    g_object_unref(defaultApp_);
+  // delete GAppInfo objects stored for Combobox
+  if(appInfos_) {
+    g_list_foreach(appInfos_, (GFunc)g_object_unref, NULL);
+    g_list_free(appInfos_);
+  }
+}
+
+void AppChooserComboBox::setMimeType(FmMimeType* mimeType) {
+  clear();
+  if(mimeType_)
+    fm_mime_type_unref(mimeType_);
+
+  mimeType_ = fm_mime_type_ref(mimeType);
+  if(mimeType_) {
+    const char* typeName = fm_mime_type_get_type(mimeType_);
+    defaultApp_ = g_app_info_get_default_for_type(typeName, FALSE);
+    appInfos_ = g_app_info_get_all_for_type(typeName);
+    int i = 0;
+    for(GList* l = appInfos_; l; l = l->next, ++i) {
+      GAppInfo* app = G_APP_INFO(l->data);
+      GIcon* gicon = g_app_info_get_icon(app);
+      QString name = QString::fromUtf8(g_app_info_get_name(app));
+      // QVariant data = qVariantFromValue<void*>(app);
+      // addItem(IconTheme::icon(gicon), name, data);
+      addItem(IconTheme::icon(gicon), name);
+      if(g_app_info_equal(app, defaultApp_))
+        defaultAppIndex_ = i;
+    }
+  }
+  // add "Other applications" item
+  insertSeparator(count());
+  addItem(tr("Customize"));
+  if(defaultAppIndex_ != -1)
+    setCurrentIndex(defaultAppIndex_);
+}
+
+// returns the currently selected app.
+GAppInfo* AppChooserComboBox::selectedApp() {
+  return G_APP_INFO(g_list_nth_data(appInfos_, currentIndex()));
+}
+
+bool AppChooserComboBox::isChanged() {
+  return (defaultAppIndex_ != currentIndex());
+}
+
+void AppChooserComboBox::onCurrentIndexChanged(int index) {
+  if(index == -1 || index == prevIndex_ || blockOnCurrentIndexChanged_)
+    return;
+
+  // the last item is "Customize"
+  if(index == (count() - 1)) {
+    /* TODO: let the user choose an app or add custom actions here. */
+    QWidget* toplevel = topLevelWidget();
+    AppChooserDialog dlg(mimeType_, toplevel);
+    dlg.setWindowModality(Qt::WindowModal);
+    dlg.setCanSetDefault(false);
+    if(dlg.exec() == QDialog::Accepted) {
+      GAppInfo* app = dlg.selectedApp();
+      if(app) {
+        /* see if it's already in the list to prevent duplication */
+        GList* found = NULL;
+        for(found = appInfos_; found; found = found->next) {
+          if(g_app_info_equal(app, G_APP_INFO(found->data)))
+            break;
+        }
+
+        // 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) {
+          setCurrentIndex(g_list_position(appInfos_, found));
+          g_object_unref(app);
+        }
+        else { /* if it's not found, add it to the list */
+          appInfos_ = g_list_prepend(appInfos_, app);
+          GIcon* gicon = g_app_info_get_icon(app);
+          QString name = QString::fromUtf8(g_app_info_get_name(app));
+          insertItem(0, IconTheme::icon(gicon), name);
+          setCurrentIndex(0);
+        }
+        blockOnCurrentIndexChanged_ = false;
+        return;
+      }
+    }
+
+    // block our handler to prevent recursive calls.
+    blockOnCurrentIndexChanged_ = true;
+    // restore to previously selected item
+    setCurrentIndex(prevIndex_);
+    blockOnCurrentIndexChanged_ = false;
+  }
+  else {
+    prevIndex_ = index;
+  }
+}
+
+
+#if 0
+/* get a list of custom apps added with app-chooser.
+* the returned GList is owned by the combo box and shouldn't be freed. */
+const GList* AppChooserComboBox::customApps() {
+
+}
+#endif
+
+} // namespace Fm
diff --git a/src/appchoosercombobox.h b/src/appchoosercombobox.h
new file mode 100644 (file)
index 0000000..53ffc3e
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * 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>
+
+namespace Fm {
+
+class LIBFM_QT_API AppChooserComboBox : public QComboBox {
+  Q_OBJECT
+public:
+  ~AppChooserComboBox();
+  AppChooserComboBox(QWidget* parent);
+
+  void setMimeType(FmMimeType* mimeType);
+
+  FmMimeType* mimeType() {
+    return mimeType_;
+  }
+
+  GAppInfo* selectedApp();
+  // const GList* customApps();
+
+  bool isChanged();
+
+private Q_SLOTS:
+  void onCurrentIndexChanged(int index);
+
+private:
+  FmMimeType* mimeType_;
+  GList* appInfos_; // applications used to open the file type
+  GAppInfo* defaultApp_; // default application used to open the file type
+  int defaultAppIndex_;
+  int prevIndex_;
+  bool blockOnCurrentIndexChanged_;
+};
+
+}
+
+#endif // FM_APPCHOOSERCOMBOBOX_H
diff --git a/src/appchooserdialog.cpp b/src/appchooserdialog.cpp
new file mode 100644 (file)
index 0000000..c331c7e
--- /dev/null
@@ -0,0 +1,286 @@
+/*
+ * 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(FmMimeType* mimeType, QWidget* parent, Qt::WindowFlags f):
+  QDialog(parent, f),
+  ui(new Ui::AppChooserDialog()),
+  mimeType_(NULL),
+  canSetDefault_(true),
+  selectedApp_(NULL) {
+  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
+
+  if(mimeType)
+    setMimeType(mimeType);
+}
+
+AppChooserDialog::~AppChooserDialog() {
+  delete ui;
+  if(mimeType_)
+    fm_mime_type_unref(mimeType_);
+  if(selectedApp_)
+    g_object_unref(selectedApp_);
+}
+
+bool AppChooserDialog::isSetDefault() {
+  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 = NULL;
+  char* dirname = g_build_filename(g_get_user_data_dir(), "applications", NULL);
+  const char* app_basename = strrchr(bin_name, '/');
+
+  if(app_basename)
+    app_basename++;
+  else
+    app_basename = bin_name;
+  if(g_mkdir_with_parents(dirname, 0700) == 0) {
+    char* filename = g_strdup_printf("%s/userapp-%s-XXXXXX.desktop", dirname, app_basename);
+    int fd = g_mkstemp(filename);
+    if(fd != -1) {
+      GString* content = g_string_sized_new(256);
+      g_string_printf(content,
+                      "[" G_KEY_FILE_DESKTOP_GROUP "]\n"
+                      G_KEY_FILE_DESKTOP_KEY_TYPE "=" G_KEY_FILE_DESKTOP_TYPE_APPLICATION "\n"
+                      G_KEY_FILE_DESKTOP_KEY_NAME "=%s\n"
+                      G_KEY_FILE_DESKTOP_KEY_EXEC "=%s\n"
+                      G_KEY_FILE_DESKTOP_KEY_CATEGORIES "=Other;\n"
+                      G_KEY_FILE_DESKTOP_KEY_NO_DISPLAY "=true\n",
+                      application_name,
+                      commandline
+                     );
+      if(mime_type)
+        g_string_append_printf(content,
+                               G_KEY_FILE_DESKTOP_KEY_MIME_TYPE "=%s\n",
+                               mime_type);
+      g_string_append_printf(content,
+                             G_KEY_FILE_DESKTOP_KEY_TERMINAL "=%s\n",
+                             terminal ? "true" : "false");
+      if(terminal)
+        g_string_append_printf(content, "X-KeepTerminal=%s\n",
+                               keep ? "true" : "false");
+      close(fd); /* g_file_set_contents() may fail creating duplicate */
+      if(g_file_set_contents(filename, content->str, content->len, NULL)) {
+        char* fbname = g_path_get_basename(filename);
+        app = G_APP_INFO(g_desktop_app_info_new(fbname));
+        g_free(fbname);
+        /* if there is mime_type set then created application will be
+           saved for the mime type (see fm_choose_app_for_mime_type()
+           below) but if not then we should remove this temp. file */
+        if(!mime_type || !application_name[0])
+          /* save the name so this file will be removed later */
+          g_object_weak_ref(G_OBJECT(app), on_temp_appinfo_destroy,
+                            g_strdup(filename));
+      }
+      else
+        g_unlink(filename);
+      g_string_free(content, TRUE);
+    }
+    g_free(filename);
+  }
+  g_free(dirname);
+  return app;
+}
+
+inline static char* get_binary(const char* cmdline, gboolean* arg_found) {
+  /* see if command line contains %f, %F, %u, or %U. */
+  const char* p = strstr(cmdline, " %");
+  if(p) {
+    if(!strchr("fFuU", *(p + 2)))
+      p = NULL;
+  }
+  if(arg_found)
+    *arg_found = (p != NULL);
+  if(p)
+    return g_strndup(cmdline, p - cmdline);
+  else
+    return g_strdup(cmdline);
+}
+
+GAppInfo* AppChooserDialog::customCommandToApp() {
+  GAppInfo* app = NULL;
+  QByteArray cmdline = ui->cmdLine->text().toLocal8Bit();
+  QByteArray app_name = ui->appName->text().toUtf8();
+  if(!cmdline.isEmpty()) {
+    gboolean arg_found = FALSE;
+    char* bin1 = get_binary(cmdline.constData(), &arg_found);
+    qDebug("bin1 = %s", bin1);
+    /* see if command line contains %f, %F, %u, or %U. */
+    if(!arg_found) {  /* append %f if no %f, %F, %u, or %U was found. */
+      cmdline += " %f";
+    }
+
+    /* FIXME: is there any better way to do this? */
+    /* We need to ensure that no duplicated items are added */
+    if(mimeType_) {
+      MenuCache* menu_cache;
+      /* see if the command is already in the list of known apps for this mime-type */
+      GList* apps = g_app_info_get_all_for_type(fm_mime_type_get_type(mimeType_));
+      GList* l;
+      for(l = apps; l; l = l->next) {
+        GAppInfo* app2 = G_APP_INFO(l->data);
+        const char* cmd = g_app_info_get_commandline(app2);
+        char* bin2 = get_binary(cmd, NULL);
+        if(g_strcmp0(bin1, bin2) == 0) {
+          app = G_APP_INFO(g_object_ref(app2));
+          qDebug("found in app list");
+          g_free(bin2);
+          break;
+        }
+        g_free(bin2);
+      }
+      g_list_foreach(apps, (GFunc)g_object_unref, NULL);
+      g_list_free(apps);
+      if(app)
+        goto _out;
+
+      /* see if this command can be found in menu cache */
+      menu_cache = menu_cache_lookup("applications.menu");
+      if(menu_cache) {
+        MenuCacheDir* root_dir = menu_cache_dup_root_dir(menu_cache);
+        if(root_dir) {
+          GSList* all_apps = menu_cache_list_all_apps(menu_cache);
+          GSList* l;
+          for(l = all_apps; l; l = l->next) {
+            MenuCacheApp* ma = MENU_CACHE_APP(l->data);
+            const char* exec = menu_cache_app_get_exec(ma);
+            char* bin2;
+            if(exec == NULL) {
+              g_warning("application %s has no Exec statement", menu_cache_item_get_id(MENU_CACHE_ITEM(ma)));
+              continue;
+            }
+            bin2 = get_binary(exec, NULL);
+            if(g_strcmp0(bin1, bin2) == 0) {
+              app = G_APP_INFO(g_desktop_app_info_new(menu_cache_item_get_id(MENU_CACHE_ITEM(ma))));
+              qDebug("found in menu cache");
+              menu_cache_item_unref(MENU_CACHE_ITEM(ma));
+              g_free(bin2);
+              break;
+            }
+            menu_cache_item_unref(MENU_CACHE_ITEM(ma));
+            g_free(bin2);
+          }
+          g_slist_free(all_apps);
+          menu_cache_item_unref(MENU_CACHE_ITEM(root_dir));
+        }
+        menu_cache_unref(menu_cache);
+      }
+      if(app)
+        goto _out;
+    }
+
+    /* FIXME: g_app_info_create_from_commandline force the use of %f or %u, so this is not we need */
+    app = app_info_create_from_commandline(cmdline.constData(), app_name.constData(), bin1,
+                                           mimeType_ ? fm_mime_type_get_type(mimeType_) : NULL,
+                                           ui->useTerminal->isChecked(), ui->keepTermOpen->isChecked());
+  _out:
+    g_free(bin1);
+  }
+  return app;
+}
+
+void AppChooserDialog::accept() {
+  QDialog::accept();
+
+  if(ui->tabWidget->currentIndex() == 0) {
+    selectedApp_ = ui->appMenuView->selectedApp();
+  }
+  else { // custom command line
+    selectedApp_ = customCommandToApp();
+  }
+
+  if(selectedApp_) {
+    if(mimeType_ && fm_mime_type_get_type(mimeType_) && g_app_info_get_name(selectedApp_)[0]) {
+      /* add this app to the mime-type */
+#if GLIB_CHECK_VERSION(2, 27, 6)
+      g_app_info_set_as_last_used_for_type(selectedApp_, fm_mime_type_get_type(mimeType_), NULL);
+#else
+      g_app_info_add_supports_type(selectedApp_, fm_mime_type_get_type(mimeType_), NULL);
+#endif
+      /* if need to set default */
+      if(ui->setDefault->isChecked())
+        g_app_info_set_as_default_for_type(selectedApp_, fm_mime_type_get_type(mimeType_), NULL);
+    }
+  }
+}
+
+void AppChooserDialog::onSelectionChanged() {
+  bool isAppSelected = ui->appMenuView->isAppSelected();
+  ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(isAppSelected);
+}
+
+void AppChooserDialog::setMimeType(FmMimeType* mimeType) {
+  if(mimeType_)
+    fm_mime_type_unref(mimeType_);
+
+  mimeType_ = mimeType ? fm_mime_type_ref(mimeType) : NULL;
+  if(mimeType_) {
+    QString text = tr("Select an application to open \"%1\" files")
+                   .arg(QString::fromUtf8(fm_mime_type_get_desc(mimeType_)));
+    ui->fileTypeHeader->setText(text);
+  }
+  else {
+    ui->fileTypeHeader->hide();
+    ui->setDefault->hide();
+  }
+}
+
+void AppChooserDialog::setCanSetDefault(bool value) {
+  canSetDefault_ = value;
+  ui->setDefault->setVisible(value);
+}
+
+void AppChooserDialog::onTabChanged(int index) {
+  if(index == 0) { // app menu view
+    onSelectionChanged();
+  }
+  else if(index == 1) { // custom command
+    ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
+  }
+}
+
+} // namespace Fm
diff --git a/src/appchooserdialog.h b/src/appchooserdialog.h
new file mode 100644 (file)
index 0000000..64a5464
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * 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>
+
+namespace Ui {
+  class AppChooserDialog;
+}
+
+namespace Fm {
+
+class LIBFM_QT_API AppChooserDialog : public QDialog {
+  Q_OBJECT
+public:
+  explicit AppChooserDialog(FmMimeType* mimeType, QWidget* parent = NULL, Qt::WindowFlags f = 0);
+  ~AppChooserDialog();
+
+  virtual void accept();
+
+  void setMimeType(FmMimeType* mimeType);
+  FmMimeType* mimeType() {
+    return mimeType_;
+  }
+
+  void setCanSetDefault(bool value);
+  bool canSetDefault() {
+    return canSetDefault_;
+  }
+
+  GAppInfo* selectedApp() {
+    return G_APP_INFO(g_object_ref(selectedApp_));
+  }
+
+  bool isSetDefault();
+
+private:
+  GAppInfo* customCommandToApp();
+
+private Q_SLOTS:
+  void onSelectionChanged();
+  void onTabChanged(int index);
+
+private:
+  Ui::AppChooserDialog* ui;
+  FmMimeType* mimeType_;
+  bool canSetDefault_;
+  GAppInfo* selectedApp_;
+};
+
+}
+
+#endif // FM_APPCHOOSERDIALOG_H
diff --git a/src/applaunchcontext.cpp b/src/applaunchcontext.cpp
new file mode 100644 (file)
index 0000000..727f462
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * 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 NULL;
+}
+
+static char* fm_app_launch_context_get_startup_notify_id(GAppLaunchContext *context, GAppInfo *info, GList *files) {
+  return NULL;
+}
+
+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, NULL);
+  return context;
+}
+
+FmAppLaunchContext* fm_app_launch_context_new() {
+  FmAppLaunchContext* context = (FmAppLaunchContext*)g_object_new(FM_TYPE_APP_LAUNCH_CONTEXT, NULL);
+  return context;
+}
diff --git a/src/applaunchcontext.h b/src/applaunchcontext.h
new file mode 100644 (file)
index 0000000..606c3b6
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * 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
diff --git a/src/appmenuview.cpp b/src/appmenuview.cpp
new file mode 100644 (file)
index 0000000..c66dc86
--- /dev/null
@@ -0,0 +1,158 @@
+/*
+ * 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(NULL),
+  menu_cache_reload_notify(NULL) {
+
+  setHeaderHidden(true);
+  setSelectionMode(SingleSelection);
+
+  // initialize model
+  // TODO: share one model among all app menu view widgets
+  // ensure that we're using lxmenu-data (FIXME: should we do this?)
+  QByteArray oldenv = qgetenv("XDG_MENU_PREFIX");
+  qputenv("XDG_MENU_PREFIX", "lxde-");
+  menu_cache = menu_cache_lookup("applications.menu");
+  // if(!oldenv.isEmpty())
+  qputenv("XDG_MENU_PREFIX", oldenv); // restore the original value if needed
+
+  if(menu_cache) {
+    MenuCacheDir* dir = menu_cache_dup_root_dir(menu_cache);
+    menu_cache_reload_notify = menu_cache_add_reload_notify(menu_cache, _onMenuCacheReload, this);
+    if(dir) { /* content of menu is already loaded */
+      addMenuItems(NULL, dir);
+      menu_cache_item_unref(MENU_CACHE_ITEM(dir));
+    }
+  }
+  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 != NULL; l = l->next) {
+    /* Get the menu item. */
+    MenuCacheItem* menuItem = MENU_CACHE_ITEM(l->data);
+    switch(menu_cache_item_get_type(menuItem)) {
+    case MENU_CACHE_TYPE_NONE:
+    case MENU_CACHE_TYPE_SEP:
+      break;
+    case MENU_CACHE_TYPE_APP:
+    case MENU_CACHE_TYPE_DIR: {
+      AppMenuViewItem* newItem = new AppMenuViewItem(menuItem);
+      if(parentItem)
+        parentItem->insertRow(parentItem->rowCount(), newItem);
+      else
+        model_->insertRow(model_->rowCount(), newItem);
+
+      if(menu_cache_item_get_type(menuItem) == MENU_CACHE_TYPE_DIR)
+        addMenuItems(newItem, MENU_CACHE_DIR(menuItem));
+      break;
+    }
+    }
+  }
+  g_slist_free_full(list, (GDestroyNotify)menu_cache_item_unref);
+}
+
+void AppMenuView::onMenuCacheReload(MenuCache* mc) {
+  MenuCacheDir* dir = menu_cache_dup_root_dir(mc);
+  model_->clear();
+  /* FIXME: preserve original selection */
+  if(dir) {
+    addMenuItems(NULL, dir);
+    menu_cache_item_unref(MENU_CACHE_ITEM(dir));
+    selectionModel()->select(model_->index(0, 0), QItemSelectionModel::SelectCurrent);
+  }
+}
+
+bool AppMenuView::isAppSelected() {
+  AppMenuViewItem* item = selectedItem();
+  return (item && item->isApp());
+}
+
+AppMenuViewItem* AppMenuView::selectedItem() {
+  QModelIndexList selected = selectedIndexes();
+  if(!selected.isEmpty()) {
+    AppMenuViewItem* item = static_cast<AppMenuViewItem*>(model_->itemFromIndex(selected.first()
+    ));
+    return item;
+  }
+  return NULL;
+}
+
+GAppInfo* AppMenuView::selectedApp() {
+  const char* id = selectedAppDesktopId();
+  return id ? G_APP_INFO(g_desktop_app_info_new(id)) : NULL;
+}
+
+QByteArray AppMenuView::selectedAppDesktopFilePath() {
+  AppMenuViewItem* item = selectedItem();
+  if(item && item->isApp()) {
+    char* path = menu_cache_item_get_file_path(item->item());
+    QByteArray ret(path);
+    g_free(path);
+    return ret;
+  }
+  return QByteArray();
+}
+
+const char* AppMenuView::selectedAppDesktopId() {
+  AppMenuViewItem* item = selectedItem();
+  if(item && item->isApp()) {
+    return menu_cache_item_get_id(item->item());
+  }
+  return NULL;
+}
+
+FmPath* AppMenuView::selectedAppDesktopPath() {
+  AppMenuViewItem* item = selectedItem();
+  if(item && item->isApp()) {
+    char* mpath = menu_cache_dir_make_path(MENU_CACHE_DIR(item));
+    FmPath* path = fm_path_new_relative(fm_path_get_apps_menu(),
+                                        mpath + 13 /* skip "/Applications" */);
+    g_free(mpath);
+    return path;
+  }
+  return NULL;
+}
+
+} // namespace Fm
diff --git a/src/appmenuview.h b/src/appmenuview.h
new file mode 100644 (file)
index 0000000..99c5607
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * 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>
+
+class QStandardItemModel;
+class QStandardItem;
+
+namespace Fm {
+
+class AppMenuViewItem;
+
+class LIBFM_QT_API AppMenuView : public QTreeView {
+  Q_OBJECT
+public:
+  explicit AppMenuView(QWidget* parent = NULL);
+  ~AppMenuView();
+
+  GAppInfo* selectedApp();
+
+  const char* selectedAppDesktopId();
+
+  QByteArray selectedAppDesktopFilePath();
+
+  FmPath * selectedAppDesktopPath();
+
+  bool isAppSelected();
+
+Q_SIGNALS:
+  void selectionChanged();
+
+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();
+
+private:
+  // gboolean fm_app_menu_view_is_item_app(, GtkTreeIter* it);
+  QStandardItemModel* model_;
+  MenuCache* menu_cache;
+  MenuCacheNotifyId menu_cache_reload_notify;
+};
+
+}
+
+#endif // FM_APPMENUVIEW_H
diff --git a/src/appmenuview_p.h b/src/appmenuview_p.h
new file mode 100644 (file)
index 0000000..03730d0
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * 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"
+
+namespace Fm {
+
+class AppMenuViewItem : public QStandardItem {
+public:
+  explicit AppMenuViewItem(MenuCacheItem* item):
+    item_(menu_cache_item_ref(item)) {
+    FmIcon* fmicon;
+    if(menu_cache_item_get_icon(item))
+      fmicon = fm_icon_from_name(menu_cache_item_get_icon(item));
+    else
+      fmicon = nullptr;
+    setText(QString::fromUtf8(menu_cache_item_get_name(item)));
+    setEditable(false);
+    setDragEnabled(false);
+    if(fmicon) {
+      setIcon(IconTheme::icon(fmicon));
+      fm_icon_unref(fmicon);
+    }
+  }
+
+  ~AppMenuViewItem() {
+    menu_cache_item_unref(item_);
+  }
+
+  MenuCacheItem* item() {
+    return item_;
+  }
+
+  MenuCacheType type() {
+       return menu_cache_item_get_type(item_);
+  }
+
+  bool isApp() {
+    return type() == MENU_CACHE_TYPE_APP;
+  }
+
+  bool isDir() {
+    return type() == MENU_CACHE_TYPE_DIR;
+  }
+
+private:
+  MenuCacheItem* item_;
+};
+
+}
+
+#endif // FM_APPMENUVIEW_P_H
diff --git a/src/archiver.h b/src/archiver.h
new file mode 100644 (file)
index 0000000..9688327
--- /dev/null
@@ -0,0 +1,143 @@
+/*
+ * 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) {
+    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) {
+    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__
diff --git a/src/bookmarkaction.cpp b/src/bookmarkaction.cpp
new file mode 100644 (file)
index 0000000..e67b6ac
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * 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(FmBookmarkItem* item, QObject* parent):
+  QAction(parent),
+  item_(fm_bookmark_item_ref(item)) {
+
+  setText(QString::fromUtf8(item->name));
+}
+
+} // namespace Fm
diff --git a/src/bookmarkaction.h b/src/bookmarkaction.h
new file mode 100644 (file)
index 0000000..65c5ebd
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * 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 <libfm/fm.h>
+
+namespace Fm {
+
+// action used to create bookmark menu items
+class LIBFM_QT_API BookmarkAction : public QAction {
+public:
+  explicit BookmarkAction(FmBookmarkItem* item, QObject* parent = 0);
+
+  virtual ~BookmarkAction() {
+    if(item_)
+      fm_bookmark_item_unref(item_);
+  }
+
+  FmBookmarkItem* bookmark() {
+    return item_;
+  }
+
+  FmPath* path() {
+    return item_->path;
+  }
+
+private:
+  FmBookmarkItem* item_;
+};
+
+}
+
+#endif // BOOKMARKACTION_H
diff --git a/src/bookmarks.h b/src/bookmarks.h
new file mode 100644 (file)
index 0000000..0545189
--- /dev/null
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2016 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#ifndef __LIBFM_QT_FM_BOOKMARKS_H__
+#define __LIBFM_QT_FM_BOOKMARKS_H__
+
+#include <libfm/fm.h>
+#include <QObject>
+#include <QtGlobal>
+#include "libfmqtglobals.h"
+
+
+namespace Fm {
+
+
+class LIBFM_QT_API Bookmarks {
+public:
+
+
+  // default constructor
+  Bookmarks() {
+    dataPtr_ = nullptr;
+  }
+
+
+  Bookmarks(FmBookmarks* dataPtr){
+    dataPtr_ = dataPtr != nullptr ? reinterpret_cast<GObject*>(g_object_ref(dataPtr)) : nullptr;
+  }
+
+
+  // copy constructor
+  Bookmarks(const Bookmarks& other) {
+    dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<GObject*>(g_object_ref(other.dataPtr_)) : nullptr;
+  }
+
+
+  // move constructor
+  Bookmarks(Bookmarks&& other) {
+    dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
+  }
+
+
+  // destructor
+  virtual ~Bookmarks() {
+    if(dataPtr_ != nullptr) {
+      g_object_unref(dataPtr_);
+    }
+  }
+
+
+  // create a wrapper for the data pointer without increasing the reference count
+  static Bookmarks wrapPtr(FmBookmarks* dataPtr) {
+    Bookmarks obj;
+    obj.dataPtr_ = reinterpret_cast<GObject*>(dataPtr);
+    return obj;
+  }
+
+  // disown the managed data pointer
+  FmBookmarks* takeDataPtr() {
+    FmBookmarks* data = reinterpret_cast<FmBookmarks*>(dataPtr_);
+    dataPtr_ = nullptr;
+    return data;
+  }
+
+  // get the raw pointer wrapped
+  FmBookmarks* dataPtr() {
+    return reinterpret_cast<FmBookmarks*>(dataPtr_);
+  }
+
+  // automatic type casting
+  operator FmBookmarks*() {
+    return dataPtr();
+  }
+
+  // automatic type casting
+  operator void*() {
+    return dataPtr();
+  }
+
+
+  // copy assignment
+  Bookmarks& operator=(const Bookmarks& other) {
+    if(dataPtr_ != nullptr) {
+      g_object_unref(dataPtr_);
+    }
+    dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<GObject*>(g_object_ref(other.dataPtr_)) : nullptr;
+    return *this;
+  }
+
+
+  // move assignment
+  Bookmarks& operator=(Bookmarks&& other) {
+    dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
+    return *this;
+  }
+
+  bool isNull() {
+    return (dataPtr_ == nullptr);
+  }
+
+  // methods
+
+  GList* getAll(void) {
+    return fm_bookmarks_get_all(dataPtr());
+  }
+
+
+  void rename(FmBookmarkItem* item, const char* new_name) {
+    fm_bookmarks_rename(dataPtr(), item, new_name);
+  }
+
+
+  void reorder(FmBookmarkItem* item, int pos) {
+    fm_bookmarks_reorder(dataPtr(), item, pos);
+  }
+
+
+  void remove(FmBookmarkItem* item) {
+    fm_bookmarks_remove(dataPtr(), item);
+  }
+
+
+  FmBookmarkItem* insert(FmPath* path, const char* name, int pos) {
+    return fm_bookmarks_insert(dataPtr(), path, name, pos);
+  }
+
+
+  static Bookmarks dup(void ) {
+    return Bookmarks::wrapPtr(fm_bookmarks_dup());
+  }
+
+
+  // automatic type casting for GObject
+  operator GObject*() {
+    return reinterpret_cast<GObject*>(dataPtr_);
+  }
+
+
+protected:
+  GObject* dataPtr_; // data pointer for the underlying C struct
+
+};
+
+
+}
+
+#endif // __LIBFM_QT_FM_BOOKMARKS_H__
diff --git a/src/browsehistory.cpp b/src/browsehistory.cpp
new file mode 100644 (file)
index 0000000..f33d185
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * 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(FmPath* path, int scrollPos) {
+  int lastIndex = size() - 1;
+  if(currentIndex_ < lastIndex) {
+    // if we're not at the last item, remove items after the current one.
+    erase(begin() + currentIndex_ + 1, end());
+  }
+
+  if(size() + 1 > maxCount_) {
+    // if there are too many items, remove the oldest one.
+    // FIXME: what if currentIndex_ == 0? remove the last item instead?
+    if(currentIndex_ == 0)
+      remove(lastIndex);
+    else {
+      remove(0);
+      --currentIndex_;
+    }
+  }
+  // add a path and current scroll position to browse history
+  append(BrowseHistoryItem(path, scrollPos));
+  currentIndex_ = size() - 1;
+}
+
+void BrowseHistory::setCurrentIndex(int index) {
+  if(index >= 0 && index < 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 (currentIndex_ + 1 < size());
+}
+
+int BrowseHistory::forward() {
+  if(canForward())
+    ++currentIndex_;
+  return currentIndex_;
+}
+
+void BrowseHistory::setMaxCount(int maxCount) {
+  maxCount_ = maxCount;
+  if(size() > maxCount) {
+    // TODO: remove some items
+  }
+}
+
+
+} // namespace Fm
diff --git a/src/browsehistory.h b/src/browsehistory.h
new file mode 100644 (file)
index 0000000..27864f3
--- /dev/null
@@ -0,0 +1,131 @@
+/*
+ * 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 <QVector>
+#include <libfm/fm.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:
+
+  BrowseHistoryItem():
+    path_(NULL),
+    scrollPos_(0) {
+  }
+
+  BrowseHistoryItem(FmPath* path, int scrollPos = 0):
+    path_(fm_path_ref(path)),
+    scrollPos_(scrollPos) {
+  }
+
+  BrowseHistoryItem(const BrowseHistoryItem& other):
+    path_(other.path_ ? fm_path_ref(other.path_) : NULL),
+    scrollPos_(other.scrollPos_) {
+  }
+
+  ~BrowseHistoryItem() {
+    if(path_)
+      fm_path_unref(path_);
+  }
+
+  BrowseHistoryItem& operator=(const BrowseHistoryItem& other) {
+    if(path_)
+      fm_path_unref(path_);
+    path_ = other.path_ ? fm_path_ref(other.path_) : NULL;
+    scrollPos_ = other.scrollPos_;
+    return *this;
+  }
+
+  FmPath* path() const {
+    return path_;
+  }
+
+  int scrollPos() const {
+    return scrollPos_;
+  }
+
+  void setScrollPos(int pos) {
+    scrollPos_ = pos;
+  }
+
+private:
+  FmPath* path_;
+  int scrollPos_;
+  // TODO: we may need to store current selection as well. reserve room for furutre expansion.
+  // void* reserved1;
+  // void* reserved2;
+};
+
+class LIBFM_QT_API BrowseHistory : public QVector<BrowseHistoryItem> {
+
+public:
+  BrowseHistory();
+  virtual ~BrowseHistory();
+
+  int currentIndex() const {
+    return currentIndex_;
+  }
+  void setCurrentIndex(int index);
+
+  FmPath* currentPath() const {
+    return at(currentIndex_).path();
+  }
+
+  int currentScrollPos() const {
+    return at(currentIndex_).scrollPos();
+  }
+
+  BrowseHistoryItem& currentItem() {
+    return operator[](currentIndex_);
+  }
+
+  void add(FmPath* path, int scrollPos = 0);
+
+  bool canForward() const;
+
+  bool canBackward() const;
+
+  int backward();
+
+  int forward();
+
+  int maxCount() const {
+    return maxCount_;
+  }
+
+  void setMaxCount(int maxCount);
+
+private:
+  int currentIndex_;
+  int maxCount_;
+};
+
+}
+
+#endif // FM_BROWSEHISTORY_H
diff --git a/src/cachedfoldermodel.cpp b/src/cachedfoldermodel.cpp
new file mode 100644 (file)
index 0000000..07b1e95
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * 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 {
+
+static GQuark data_id = 0;
+
+
+CachedFolderModel::CachedFolderModel(FmFolder* folder):
+  FolderModel(),
+  refCount(1) {
+
+  FolderModel::setFolder(folder);
+}
+
+CachedFolderModel::~CachedFolderModel() {
+}
+
+CachedFolderModel* CachedFolderModel::modelFromFolder(FmFolder* folder) {
+  CachedFolderModel* model = NULL;
+  if(!data_id)
+    data_id = g_quark_from_static_string("CachedFolderModel");
+  gpointer qdata = g_object_get_qdata(G_OBJECT(folder), data_id);
+  model = reinterpret_cast<CachedFolderModel*>(qdata);
+  if(model) {
+    // qDebug("cache found!!");
+    model->ref();
+  }
+  else {
+    model = new CachedFolderModel(folder);
+    g_object_set_qdata(G_OBJECT(folder), data_id, model);
+  }
+  return model;
+}
+
+CachedFolderModel* CachedFolderModel::modelFromPath(FmPath* path) {
+  FmFolder* folder = fm_folder_from_path(path);
+  if(folder) {
+    CachedFolderModel* model = modelFromFolder(folder);
+    g_object_unref(folder);
+    return model;
+  }
+  return NULL;
+}
+
+void CachedFolderModel::unref() {
+  // qDebug("unref cache");
+  --refCount;
+  if(refCount <= 0) {
+    g_object_set_qdata(G_OBJECT(folder()), data_id, NULL);
+    deleteLater();
+  }
+}
+
+
+} // namespace Fm
diff --git a/src/cachedfoldermodel.h b/src/cachedfoldermodel.h
new file mode 100644 (file)
index 0000000..273fba9
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * 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"
+
+namespace Fm {
+
+class LIBFM_QT_API CachedFolderModel : public FolderModel {
+  Q_OBJECT
+public:
+  CachedFolderModel(FmFolder* folder);
+  void ref() {
+    ++refCount;
+  }
+  void unref();
+
+  static CachedFolderModel* modelFromFolder(FmFolder* folder);
+  static CachedFolderModel* modelFromPath(FmPath* path);
+
+private:
+  virtual ~CachedFolderModel();
+  void setFolder(FmFolder* folder);
+private:
+  int refCount;
+};
+
+
+}
+
+#endif // FM_CACHEDFOLDERMODEL_H
diff --git a/src/colorbutton.cpp b/src/colorbutton.cpp
new file mode 100644 (file)
index 0000000..346abcd
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * 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
diff --git a/src/colorbutton.h b/src/colorbutton.h
new file mode 100644 (file)
index 0000000..e8e0072
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * 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
diff --git a/src/config.h b/src/config.h
new file mode 100644 (file)
index 0000000..d1cffd9
--- /dev/null
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2016 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#ifndef __LIBFM_QT_FM_CONFIG_H__
+#define __LIBFM_QT_FM_CONFIG_H__
+
+#include <libfm/fm.h>
+#include <QObject>
+#include <QtGlobal>
+#include "libfmqtglobals.h"
+
+
+namespace Fm {
+
+
+class LIBFM_QT_API Config {
+public:
+
+
+  Config(void ) {
+    dataPtr_ = reinterpret_cast<GObject*>(fm_config_new());
+  }
+
+
+  Config(FmConfig* dataPtr){
+    dataPtr_ = dataPtr != nullptr ? reinterpret_cast<GObject*>(g_object_ref(dataPtr)) : nullptr;
+  }
+
+
+  // copy constructor
+  Config(const Config& other) {
+    dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<GObject*>(g_object_ref(other.dataPtr_)) : nullptr;
+  }
+
+
+  // move constructor
+  Config(Config&& other) {
+    dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
+  }
+
+
+  // destructor
+  virtual ~Config() {
+    if(dataPtr_ != nullptr) {
+      g_object_unref(dataPtr_);
+    }
+  }
+
+
+  // create a wrapper for the data pointer without increasing the reference count
+  static Config wrapPtr(FmConfig* dataPtr) {
+    Config obj;
+    obj.dataPtr_ = reinterpret_cast<GObject*>(dataPtr);
+    return obj;
+  }
+
+  // disown the managed data pointer
+  FmConfig* takeDataPtr() {
+    FmConfig* data = reinterpret_cast<FmConfig*>(dataPtr_);
+    dataPtr_ = nullptr;
+    return data;
+  }
+
+  // get the raw pointer wrapped
+  FmConfig* dataPtr() {
+    return reinterpret_cast<FmConfig*>(dataPtr_);
+  }
+
+  // automatic type casting
+  operator FmConfig*() {
+    return dataPtr();
+  }
+
+  // automatic type casting
+  operator void*() {
+    return dataPtr();
+  }
+
+
+  // copy assignment
+  Config& operator=(const Config& other) {
+    if(dataPtr_ != nullptr) {
+      g_object_unref(dataPtr_);
+    }
+    dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<GObject*>(g_object_ref(other.dataPtr_)) : nullptr;
+    return *this;
+  }
+
+
+  // move assignment
+  Config& operator=(Config&& other) {
+    dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
+    return *this;
+  }
+
+  bool isNull() {
+    return (dataPtr_ == nullptr);
+  }
+
+  // methods
+
+  void emitChanged(const char* changed_key) {
+    fm_config_emit_changed(dataPtr(), changed_key);
+  }
+
+
+  void save(const char* name) {
+    fm_config_save(dataPtr(), name);
+  }
+
+
+  void loadFromKeyFile(GKeyFile* kf) {
+    fm_config_load_from_key_file(dataPtr(), kf);
+  }
+
+
+  void loadFromFile(const char* name) {
+    fm_config_load_from_file(dataPtr(), name);
+  }
+
+
+  // automatic type casting for GObject
+  operator GObject*() {
+    return reinterpret_cast<GObject*>(dataPtr_);
+  }
+
+
+protected:
+  GObject* dataPtr_; // data pointer for the underlying C struct
+
+};
+
+
+}
+
+#endif // __LIBFM_QT_FM_CONFIG_H__
diff --git a/src/createnewmenu.cpp b/src/createnewmenu.cpp
new file mode 100644 (file)
index 0000000..1597deb
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * 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"
+
+namespace Fm {
+
+CreateNewMenu::CreateNewMenu(QWidget* dialogParent, FmPath* dirPath, QWidget* parent):
+  QMenu(parent), dialogParent_(dialogParent), dirPath_(dirPath) {
+  QAction* action = new QAction(QIcon::fromTheme("folder-new"), tr("Folder"), this);
+  connect(action, &QAction::triggered, this, &CreateNewMenu::onCreateNewFolder);
+  addAction(action);
+
+  action = new QAction(QIcon::fromTheme("document-new"), tr("Blank File"), this);
+  connect(action, &QAction::triggered, this, &CreateNewMenu::onCreateNewFile);
+  addAction(action);
+
+  // add more items to "Create New" menu from templates
+  GList* templates = fm_template_list_all(fm_config->only_user_templates);
+  if(templates) {
+    addSeparator();
+    for(GList* l = templates; l; l = l->next) {
+      FmTemplate* templ = (FmTemplate*)l->data;
+      /* we support directories differently */
+      if(fm_template_is_directory(templ))
+        continue;
+      FmMimeType* mime_type = fm_template_get_mime_type(templ);
+      const char* label = fm_template_get_label(templ);
+      QString text = QString("%1 (%2)").arg(QString::fromUtf8(label)).arg(QString::fromUtf8(fm_mime_type_get_desc(mime_type)));
+      FmIcon* icon = fm_template_get_icon(templ);
+      if(!icon)
+        icon = fm_mime_type_get_icon(mime_type);
+      QAction* action = addAction(IconTheme::icon(icon), text);
+      action->setObjectName(QString::fromUtf8(fm_template_get_name(templ, NULL)));
+      connect(action, &QAction::triggered, this, &CreateNewMenu::onCreateNew);
+    }
+  }
+}
+
+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 = NULL;
+  for(GList* l = templates; l; l = l->next) {
+    FmTemplate* t = (FmTemplate*)l->data;
+    if(name == fm_template_get_name(t, NULL)) {
+      templ = t;
+      break;
+    }
+  }
+  if(templ) { // template found
+    if(dirPath_)
+      createFileOrFolder(CreateWithTemplate, dirPath_, templ, dialogParent_);
+  }
+}
+
+} // namespace Fm
diff --git a/src/createnewmenu.h b/src/createnewmenu.h
new file mode 100644 (file)
index 0000000..09ca9d9
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * 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>
+
+namespace Fm {
+
+class FolderView;
+
+class LIBFM_QT_API CreateNewMenu : public QMenu {
+Q_OBJECT
+
+public:
+  explicit CreateNewMenu(QWidget* dialogParent, FmPath* dirPath,
+                         QWidget* parent = 0);
+  virtual ~CreateNewMenu();
+
+protected Q_SLOTS:
+  void onCreateNewFolder();
+  void onCreateNewFile();
+  void onCreateNew();
+
+private:
+  QWidget* dialogParent_;
+  FmPath* dirPath_;
+};
+
+}
+
+#endif // FM_CREATENEWMENU_H
diff --git a/src/customaction_p.h b/src/customaction_p.h
new file mode 100644 (file)
index 0000000..e3751f1
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * 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
+
+namespace Fm {
+
+class CustomAction : public QAction {
+public:
+  explicit CustomAction(FmFileActionItem* item, QObject* parent = NULL):
+    QAction(QString::fromUtf8(fm_file_action_item_get_name(item)), parent),
+    item_(reinterpret_cast<FmFileActionItem*>(fm_file_action_item_ref(item))) {
+    const char* icon_name = fm_file_action_item_get_icon(item);
+    if(icon_name)
+      setIcon(QIcon::fromTheme(icon_name));
+  }
+
+  virtual ~CustomAction() {
+    fm_file_action_item_unref(item_);
+  }
+
+  FmFileActionItem* item() {
+    return item_;
+  }
+
+private:
+  FmFileActionItem* item_;
+};
+
+} // namespace Fm
+
+#endif
diff --git a/src/deepcountjob.h b/src/deepcountjob.h
new file mode 100644 (file)
index 0000000..1eda2fe
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2016 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#ifndef __LIBFM_QT_FM_DEEP_COUNT_JOB_H__
+#define __LIBFM_QT_FM_DEEP_COUNT_JOB_H__
+
+#include <libfm/fm.h>
+#include <QObject>
+#include <QtGlobal>
+#include "libfmqtglobals.h"
+#include "job.h"
+
+namespace Fm {
+
+
+class LIBFM_QT_API DeepCountJob: public Job {
+public:
+
+
+  DeepCountJob(FmPathList* paths, FmDeepCountJobFlags flags) {
+    dataPtr_ = reinterpret_cast<GObject*>(fm_deep_count_job_new(paths, flags));
+  }
+
+
+  // default constructor
+  DeepCountJob() {
+    dataPtr_ = nullptr;
+  }
+
+
+  DeepCountJob(FmDeepCountJob* dataPtr){
+    dataPtr_ = dataPtr != nullptr ? reinterpret_cast<GObject*>(g_object_ref(dataPtr)) : nullptr;
+  }
+
+
+  // copy constructor
+  DeepCountJob(const DeepCountJob& other) {
+    dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<GObject*>(g_object_ref(other.dataPtr_)) : nullptr;
+  }
+
+
+  // move constructor
+  DeepCountJob(DeepCountJob&& other) {
+    dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
+  }
+
+
+
+  // create a wrapper for the data pointer without increasing the reference count
+  static DeepCountJob wrapPtr(FmDeepCountJob* dataPtr) {
+    DeepCountJob obj;
+    obj.dataPtr_ = reinterpret_cast<GObject*>(dataPtr);
+    return obj;
+  }
+
+  // disown the managed data pointer
+  FmDeepCountJob* takeDataPtr() {
+    FmDeepCountJob* data = reinterpret_cast<FmDeepCountJob*>(dataPtr_);
+    dataPtr_ = nullptr;
+    return data;
+  }
+
+  // get the raw pointer wrapped
+  FmDeepCountJob* dataPtr() {
+    return reinterpret_cast<FmDeepCountJob*>(dataPtr_);
+  }
+
+  // automatic type casting
+  operator FmDeepCountJob*() {
+    return dataPtr();
+  }
+
+  // automatic type casting
+  operator void*() {
+    return dataPtr();
+  }
+
+
+  // copy assignment
+  DeepCountJob& operator=(const DeepCountJob& other) {
+    if(dataPtr_ != nullptr) {
+      g_object_unref(dataPtr_);
+    }
+    dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<GObject*>(g_object_ref(other.dataPtr_)) : nullptr;
+    return *this;
+  }
+
+
+  // move assignment
+  DeepCountJob& operator=(DeepCountJob&& other) {
+    dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
+    return *this;
+  }
+
+  bool isNull() {
+    return (dataPtr_ == nullptr);
+  }
+
+  // methods
+
+  void setDest(dev_t dev, const char* fs_id) {
+    fm_deep_count_job_set_dest(dataPtr(), dev, fs_id);
+  }
+
+
+
+};
+
+
+}
+
+#endif // __LIBFM_QT_FM_DEEP_COUNT_JOB_H__
diff --git a/src/dirlistjob.h b/src/dirlistjob.h
new file mode 100644 (file)
index 0000000..0c509a8
--- /dev/null
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2016 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+#ifndef __LIBFM_QT_FM_DIR_LIST_JOB_H__
+#define __LIBFM_QT_FM_DIR_LIST_JOB_H__
+
+#include <libfm/fm.h>
+#include <QObject>
+#include <QtGlobal>
+#include "libfmqtglobals.h"
+#include "job.h"
+
+namespace Fm {
+
+
+class LIBFM_QT_API DirListJob: public Job {
+public:
+
+
+  DirListJob(FmPath* path, gboolean dir_only) {
+    dataPtr_ = reinterpret_cast<GObject*>(fm_dir_list_job_new(path, dir_only));
+  }
+
+
+  // default constructor
+  DirListJob() {
+    dataPtr_ = nullptr;
+  }
+
+
+  DirListJob(FmDirListJob* dataPtr){
+    dataPtr_ = dataPtr != nullptr ? reinterpret_cast<GObject*>(g_object_ref(dataPtr)) : nullptr;
+  }
+
+
+  // copy constructor
+  DirListJob(const DirListJob& other) {
+    dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<GObject*>(g_object_ref(other.dataPtr_)) : nullptr;
+  }
+
+
+  // move constructor
+  DirListJob(DirListJob&& other) {
+    dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
+  }
+
+
+
+  // create a wrapper for the data pointer without increasing the reference count
+  static DirListJob wrapPtr(FmDirListJob* dataPtr) {
+    DirListJob obj;
+    obj.dataPtr_ = reinterpret_cast<GObject*>(dataPtr);
+    return obj;
+  }
+
+  // disown the managed data pointer
+  FmDirListJob* takeDataPtr() {
+    FmDirListJob* data = reinterpret_cast<FmDirListJob*>(dataPtr_);
+    dataPtr_ = nullptr;
+    return data;
+  }
+
+  // get the raw pointer wrapped
+  FmDirListJob* dataPtr() {
+    return reinterpret_cast<FmDirListJob*>(dataPtr_);
+  }
+
+  // automatic type casting
+  operator FmDirListJob*() {
+    return dataPtr();
+  }
+
+  // automatic type casting
+  operator void*() {
+    return dataPtr();
+  }
+
+
+  // copy assignment
+  DirListJob& operator=(const DirListJob& other) {
+    if(dataPtr_ != nullptr) {
+      g_object_unref(dataPtr_);
+    }
+    dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<GObject*>(g_object_ref(other.dataPtr_)) : nullptr;
+    return *this;
+  }
+
+
+  // move assignment
+  DirListJob& operator=(DirListJob&& other) {
+    dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
+    return *this;
+  }
+
+  bool isNull() {
+    return (dataPtr_ == nullptr);
+  }
+
+  // methods
+
+  void addFoundFile(FmFileInfo* file) {
+    fm_dir_list_job_add_found_file(dataPtr(), file);
+  }
+
+
+  void setIncremental(gboolean set) {
+    fm_dir_list_job_set_incremental(dataPtr(), set);
+  }
+
+
+  FmFileInfoList* getFiles(void) {
+    return fm_dir_list_job_get_files(dataPtr());
+  }
+
+
+  static DirListJob newForGfile(GFile* gf) {
+    return DirListJob::wrapPtr(fm_dir_list_job_new_for_gfile(gf));
+  }
+
+
+  static DirListJob new2(FmPath* path, FmDirListJobFlags flags) {
+    return DirListJob::wrapPtr(fm_dir_list_job_new2(path, flags));
+  }
+
+
+
+};
+
+
+}
+
+#endif // __LIBFM_QT_FM_DIR_LIST_JOB_H__
diff --git a/src/dirtreemodel.cpp b/src/dirtreemodel.cpp
new file mode 100644 (file)
index 0000000..6947327
--- /dev/null
@@ -0,0 +1,205 @@
+/*
+ * 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>
+
+namespace Fm {
+
+DirTreeModel::DirTreeModel(QObject* parent):
+  showHidden_(false) {
+}
+
+DirTreeModel::~DirTreeModel() {
+}
+
+// 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) {
+    FmFileInfo* 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:
+      return qVariantFromValue((void*)info);
+    }
+  }
+  return QVariant();
+}
+
+int DirTreeModel::columnCount(const QModelIndex& parent) const {
+  return 1;
+}
+
+int DirTreeModel::rowCount(const QModelIndex& parent) const {
+  if(!parent.isValid())
+    return rootItems_.count();
+  DirTreeModelItem* item = itemFromIndex(parent);
+  if(item)
+    return item->children_.count();
+  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 QList<DirTreeModelItem*>& items = item->parent_ ? item->parent_->children_ : rootItems_;
+      int row = items.indexOf(item); // this is Q(n) and may be slow :-(
+      if(row >= 0)
+       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(row < rootItems_.count()) {
+        const DirTreeModelItem* item = rootItems_.at(row);
+        return createIndex(row, column, (void*)item);
+      }
+    }
+    else { // child items
+      DirTreeModelItem* parentItem = itemFromIndex(parent);
+      if(row < parentItem->children_.count()) {
+        const DirTreeModelItem* item = parentItem->children_.at(row);
+        return createIndex(row, column, (void*)item);
+      }
+    }
+  }
+  return QModelIndex(); // invalid index
+}
+
+bool DirTreeModel::hasChildren(const QModelIndex& parent) const {
+  DirTreeModelItem* item = itemFromIndex(parent);
+  return item ? !item->isPlaceHolder() : true;
+}
+
+QModelIndex DirTreeModel::indexFromItem(DirTreeModelItem* item) const {
+  Q_ASSERT(item);
+  const QList<DirTreeModelItem*>& items = item->parent_ ? item->parent_->children_ : rootItems_;
+  int row = items.indexOf(item);
+  if(row >= 0)
+    return createIndex(row, 0, (void*)item);
+  return QModelIndex();
+}
+
+// public APIs
+QModelIndex DirTreeModel::addRoot(FmFileInfo* root) {
+  DirTreeModelItem* item = new DirTreeModelItem(root, this);
+  int row = rootItems_.count();
+  beginInsertRows(QModelIndex(), row, row);
+  item->fileInfo_ = fm_file_info_ref(root);
+  rootItems_.append(item);
+  // add_place_holder_child_item(model, item_l, NULL, FALSE);
+  endInsertRows();
+  return QModelIndex();
+}
+
+DirTreeModelItem* DirTreeModel::itemFromIndex(const QModelIndex& index) const {
+  return reinterpret_cast<DirTreeModelItem*>(index.internalPointer());
+}
+
+QModelIndex DirTreeModel::indexFromPath(FmPath* path) const {
+  DirTreeModelItem* item = itemFromPath(path);
+  return item ? item->index() : QModelIndex();
+}
+
+DirTreeModelItem* DirTreeModel::itemFromPath(FmPath* path) const {
+  Q_FOREACH(DirTreeModelItem* item, rootItems_) {
+    if(item->fileInfo_ && fm_path_equal(path, fm_file_info_get_path(item->fileInfo_))) {
+      return item;
+    }
+    else {
+      DirTreeModelItem* child = item->childFromPath(path, true);
+      if(child)
+       return child;
+    }
+  }
+  return NULL;
+}
+
+
+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();
+}
+
+FmFileInfo* DirTreeModel::fileInfo(const QModelIndex& index) {
+  DirTreeModelItem* item = itemFromIndex(index);
+  return item ? item->fileInfo_ : NULL;
+}
+
+FmPath* DirTreeModel::filePath(const QModelIndex& index) {
+  DirTreeModelItem* item = itemFromIndex(index);
+  return item && item->fileInfo_ ? fm_file_info_get_path(item->fileInfo_) : NULL;
+}
+
+QString DirTreeModel::dispName(const QModelIndex& index) {
+  DirTreeModelItem* item = itemFromIndex(index);
+  return item ? item->displayName_ : QString();
+}
+
+void DirTreeModel::setShowHidden(bool show_hidden) {
+  showHidden_ = show_hidden;
+  Q_FOREACH(DirTreeModelItem* item, rootItems_) {
+    item->setShowHidden(show_hidden);
+  }
+}
+
+
+} // namespace Fm
diff --git a/src/dirtreemodel.h b/src/dirtreemodel.h
new file mode 100644 (file)
index 0000000..c82ff3b
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * 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>
+
+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();
+
+  QModelIndex addRoot(FmFileInfo* root);
+  void loadRow(const QModelIndex& index);
+  void unloadRow(const QModelIndex& index);
+
+  bool isLoaded(const QModelIndex& index);
+  QIcon icon(const QModelIndex& index);
+  FmFileInfo* fileInfo(const QModelIndex& index);
+  FmPath* filePath(const QModelIndex& index);
+  QString dispName(const QModelIndex& index);
+
+  void setShowHidden(bool show_hidden);
+  bool showHidden() const {
+    return showHidden_;
+  }
+
+  QModelIndex indexFromPath(FmPath* 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;
+
+private:
+  DirTreeModelItem* itemFromPath(FmPath* path) const;
+  DirTreeModelItem* itemFromIndex(const QModelIndex& index) const;
+  QModelIndex indexFromItem(DirTreeModelItem* item) const;
+
+Q_SIGNALS:
+  void rowLoaded(const QModelIndex& index);
+
+private:
+  bool showHidden_;
+  QList<DirTreeModelItem*> rootItems_;
+};
+}
+
+#endif // FM_DIRTREEMODEL_H
diff --git a/src/dirtreemodelitem.cpp b/src/dirtreemodelitem.cpp
new file mode 100644 (file)
index 0000000..602ff3d
--- /dev/null
@@ -0,0 +1,337 @@
+/*
+ * 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) {
+}
+
+DirTreeModelItem::DirTreeModelItem(FmFileInfo* info, DirTreeModel* model, DirTreeModelItem* parent):
+  fileInfo_(fm_file_info_ref(info)),
+  folder_(nullptr),
+  displayName_(QString::fromUtf8(fm_file_info_get_disp_name(info))),
+  icon_(IconTheme::icon(fm_file_info_get_icon(info))),
+  expanded_(false),
+  loaded_(false),
+  parent_(parent),
+  placeHolderChild_(nullptr),
+  model_(model) {
+
+  if(info)
+    addPlaceHolderChild();
+}
+
+DirTreeModelItem::~DirTreeModelItem() {
+  if(fileInfo_)
+    fm_file_info_unref(fileInfo_);
+
+  if(folder_)
+    freeFolder();
+
+  // delete child items if needed
+  if(!children_.isEmpty()) {
+    Q_FOREACH(DirTreeModelItem* item, children_) {
+      delete item;
+    }
+  }
+  if(!hiddenChildren_.isEmpty()) {
+    Q_FOREACH(DirTreeModelItem* item, hiddenChildren_) {
+      delete item;
+    }
+  }
+}
+
+void DirTreeModelItem::addPlaceHolderChild() {
+  placeHolderChild_ = new DirTreeModelItem();
+  placeHolderChild_->parent_ = this;
+  placeHolderChild_->model_ = model_;
+  placeHolderChild_->displayName_ = DirTreeModel::tr("Loading...");
+  children_.append(placeHolderChild_);
+}
+
+void DirTreeModelItem::freeFolder() {
+  if(folder_) {
+    g_signal_handlers_disconnect_by_func(folder_, gpointer(onFolderFinishLoading), this);
+    g_signal_handlers_disconnect_by_func(folder_, gpointer(onFolderFilesAdded), this);
+    g_signal_handlers_disconnect_by_func(folder_, gpointer(onFolderFilesRemoved), this);
+    g_signal_handlers_disconnect_by_func(folder_, gpointer(onFolderFilesChanged), this);
+    g_object_unref(folder_);
+    folder_ = nullptr;
+  }
+}
+
+void DirTreeModelItem::loadFolder() {
+  if(!expanded_) {
+    /* dynamically load content of the folder. */
+    folder_ = fm_folder_from_path(fm_file_info_get_path(fileInfo_));
+    /* g_debug("fm_dir_tree_model_load_row()"); */
+    /* associate the data with loaded handler */
+    g_signal_connect(folder_, "finish-loading", G_CALLBACK(onFolderFinishLoading), this);
+    g_signal_connect(folder_, "files-added", G_CALLBACK(onFolderFilesAdded), this);
+    g_signal_connect(folder_, "files-removed", G_CALLBACK(onFolderFilesRemoved), this);
+    g_signal_connect(folder_, "files-changed", G_CALLBACK(onFolderFilesChanged), this);
+
+    /* set 'expanded' flag beforehand as callback may check it */
+    expanded_ = true;
+    /* if the folder is already loaded, call "loaded" handler ourselves */
+    if(fm_folder_is_loaded(folder_)) { // already loaded
+      GList* file_l;
+      FmFileInfoList* files = fm_folder_get_files(folder_);
+      for(file_l = fm_file_info_list_peek_head_link(files); file_l; file_l = file_l->next) {
+        FmFileInfo* fi = FM_FILE_INFO(file_l->data);
+        if(fm_file_info_is_dir(fi)) {
+          insertFileInfo(fi);
+        }
+      }
+      onFolderFinishLoading(folder_, this);
+    }
+  }
+}
+
+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_.count() - 1);
+    if(!children_.isEmpty()) {
+      Q_FOREACH(DirTreeModelItem* item, children_) {
+        delete item;
+      }
+      children_.clear();
+    }
+    model_->endRemoveRows();
+
+    // remove hidden children
+    if(!hiddenChildren_.isEmpty()) {
+      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.
+ * GtkTreePath tp is the tree path of parent node. */
+DirTreeModelItem* DirTreeModelItem::insertFileInfo(FmFileInfo* fi) {
+  // qDebug() << "insertFileInfo: " << fm_file_info_get_disp_name(fi);
+  DirTreeModelItem* item = new DirTreeModelItem(fi, model_);
+  insertItem(item);
+  return item;
+}
+
+// find a good position to insert the new item
+int DirTreeModelItem::insertItem(DirTreeModelItem* newItem) {
+  if(model_->showHidden() || !newItem->fileInfo_ || !fm_file_info_is_hidden(newItem->fileInfo_)) {
+    const char* new_key = fm_file_info_get_collate_key(newItem->fileInfo_);
+    int pos = 0;
+    QList<DirTreeModelItem*>::iterator it;
+    for(it = children_.begin(); it != children_.end(); ++it) {
+      DirTreeModelItem* child = *it;
+      if(G_UNLIKELY(!child->fileInfo_))
+       continue;
+      const char* key = fm_file_info_get_collate_key(child->fileInfo_);
+      if(strcmp(new_key, key) <= 0)
+       break;
+      ++pos;
+    }
+    // inform the world that we're about to insert the item
+    model_->beginInsertRows(index(), pos, pos);
+    newItem->parent_ = this;
+    children_.insert(it, newItem);
+    model_->endInsertRows();
+    return pos;
+  }
+  else { // hidden folder
+    hiddenChildren_.append(newItem);
+  }
+  return -1;
+}
+
+
+// FmFolder signal handlers
+
+// static
+void DirTreeModelItem::onFolderFinishLoading(FmFolder* folder, gpointer user_data) {
+  DirTreeModelItem* _this = (DirTreeModelItem*)user_data;
+  DirTreeModel* model = _this->model_;
+  /* set 'loaded' flag beforehand as callback may check it */
+  _this->loaded_ = true;
+  QModelIndex index = _this->index();
+qDebug() << "folder loaded";
+  // remove the placeholder child if needed
+  if(_this->children_.count() == 1) { // we have no other child other than the place holder item, leave it
+    _this->placeHolderChild_->displayName_ = DirTreeModel::tr("<No sub folders>");
+    QModelIndex placeHolderIndex = _this->placeHolderChild_->index();
+    // qDebug() << "placeHolderIndex: "<<placeHolderIndex;
+    Q_EMIT model->dataChanged(placeHolderIndex, placeHolderIndex);
+  }
+  else {
+    int pos = _this->children_.indexOf(_this->placeHolderChild_);
+    model->beginRemoveRows(index, pos, pos);
+    _this->children_.removeAt(pos);
+    delete _this->placeHolderChild_;
+    model->endRemoveRows();
+    _this->placeHolderChild_ = nullptr;
+  }
+
+  Q_EMIT model->rowLoaded(index);
+}
+
+// static
+void DirTreeModelItem::onFolderFilesAdded(FmFolder* folder, GSList* files, gpointer user_data) {
+  GSList* l;
+  DirTreeModelItem* _this = (DirTreeModelItem*)user_data;
+  for(l = files; l; l = l->next) {
+    FmFileInfo* fi = FM_FILE_INFO(l->data);
+    if(fm_file_info_is_dir(fi)) { /* FIXME: maybe adding files can be allowed later */
+      /* Ideally FmFolder should not emit files-added signals for files that
+       * already exists. So there is no need to check for duplication here. */
+      _this->insertFileInfo(fi);
+    }
+  }
+}
+
+// static
+void DirTreeModelItem::onFolderFilesRemoved(FmFolder* folder, GSList* files, gpointer user_data) {
+  DirTreeModelItem* _this = (DirTreeModelItem*)user_data;
+  DirTreeModel* model = _this->model_;
+
+  for(GSList* l = files; l; l = l->next) {
+    FmFileInfo* fi = FM_FILE_INFO(l->data);
+    int pos;
+    DirTreeModelItem* child  = _this->childFromName(fm_file_info_get_name(fi), &pos);
+    if(child) {
+      model->beginRemoveRows(_this->index(), pos, pos);
+      _this->children_.removeAt(pos);
+      delete child;
+      model->endRemoveRows();
+    }
+  }
+}
+
+// static
+void DirTreeModelItem::onFolderFilesChanged(FmFolder* folder, GSList* files, gpointer user_data) {
+  DirTreeModelItem* _this = (DirTreeModelItem*)user_data;
+  DirTreeModel* model = _this->model_;
+
+  for(GSList* l = files; l; l = l->next) {
+    FmFileInfo* changedFile = FM_FILE_INFO(l->data);
+    int pos;
+    DirTreeModelItem* child = _this->childFromName(fm_file_info_get_name(changedFile), &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_ && strcmp(fm_file_info_get_name(item->fileInfo_), utf8_name) == 0) {
+      if(pos)
+        *pos = i;
+      return item;
+    }
+    ++i;
+  }
+  return nullptr;
+}
+
+DirTreeModelItem* DirTreeModelItem::childFromPath(FmPath* 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_ && fm_path_equal(fm_file_info_get_path(item->fileInfo_), path)) {
+      return item;
+    } else if(recursive) {
+      DirTreeModelItem* child = item->childFromPath(path, true);
+      if(child)
+        return child;
+    }
+  }
+  return nullptr;
+}
+
+void DirTreeModelItem::setShowHidden(bool show) {
+  if(show) {
+    // move all hidden children to visible list
+    Q_FOREACH(DirTreeModelItem* item, hiddenChildren_) {
+      insertItem(item);
+    }
+    hiddenChildren_.clear();
+  }
+  else { // hide hidden folders
+    QModelIndex _index = index();
+    QList<DirTreeModelItem*>::iterator it, next;
+    int pos = 0;
+    for(it = children_.begin(); it != children_.end(); ++pos) {
+      DirTreeModelItem* item = *it;
+      next = it + 1;
+      if(item->fileInfo_) {
+        if(fm_file_info_is_hidden(item->fileInfo_)) { // hidden folder
+          // remove from the model and add to the hiddenChildren_ list
+          model_->beginRemoveRows(_index, pos, pos);
+          children_.erase(it);
+          hiddenChildren_.append(item);
+          model_->endRemoveRows();
+        }
+        else { // visible folder, recursively filter its children
+          item->setShowHidden(show);
+        }
+      }
+      it = next;
+    }
+  }
+}
+
+
+
+} // namespace Fm
+
diff --git a/src/dirtreemodelitem.h b/src/dirtreemodelitem.h
new file mode 100644 (file)
index 0000000..4d77ffb
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * 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 <QIcon>
+#include <QList>
+#include <QModelIndex>
+
+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(FmFileInfo* info, DirTreeModel* model, DirTreeModelItem* parent = nullptr);
+  ~DirTreeModelItem();
+
+  void loadFolder();
+  void unloadFolder();
+
+  inline bool isPlaceHolder() const {
+    return (fileInfo_ == nullptr);
+  }
+
+  void setShowHidden(bool show);
+
+private:
+  void freeFolder();
+  void addPlaceHolderChild();
+  DirTreeModelItem* childFromName(const char* utf8_name, int* pos);
+  DirTreeModelItem* childFromPath(FmPath* path, bool recursive) const;
+
+  DirTreeModelItem* insertFileInfo(FmFileInfo* fi);
+  int insertItem(Fm::DirTreeModelItem* newItem);
+  QModelIndex index();
+
+  static void onFolderFinishLoading(FmFolder* folder, gpointer user_data);
+  static void onFolderFilesAdded(FmFolder* folder, GSList* files, gpointer user_data);
+  static void onFolderFilesRemoved(FmFolder* folder, GSList* files, gpointer user_data);
+  static void onFolderFilesChanged(FmFolder* folder, GSList* files, gpointer user_data);
+
+private:
+  FmFileInfo* fileInfo_;
+  FmFolder* folder_;
+  QString displayName_ ;
+  QIcon icon_;
+  bool expanded_;
+  bool loaded_;
+  DirTreeModelItem* parent_;
+  DirTreeModelItem* placeHolderChild_;
+  QList<DirTreeModelItem*> children_;
+  QList<DirTreeModelItem*> hiddenChildren_;
+  DirTreeModel* model_;
+};
+
+}
+
+#endif // FM_DIRTREEMODELITEM_H
diff --git a/src/dirtreeview.cpp b/src/dirtreeview.cpp
new file mode 100644 (file)
index 0000000..8b58374
--- /dev/null
@@ -0,0 +1,298 @@
+/*
+ * 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 "dirtreemodel.h"
+#include "dirtreemodelitem.h"
+#include "filemenu.h"
+
+namespace Fm {
+
+DirTreeView::DirTreeView(QWidget* parent):
+  currentPath_(NULL),
+  currentExpandingItem_(NULL) {
+
+  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() {
+  if(currentPath_)
+    fm_path_unref(currentPath_);
+}
+
+void DirTreeView::cancelPendingChdir() {
+  if(!pathsToExpand_.isEmpty()) {
+    pathsToExpand_.clear();
+    if(!currentExpandingItem_)
+      return;
+    DirTreeModel* _model = static_cast<DirTreeModel*>(model());
+    disconnect(_model, &DirTreeModel::rowLoaded, this, &DirTreeView::onRowLoaded);
+    currentExpandingItem_ = NULL;
+  }
+}
+
+void DirTreeView::expandPendingPath() {
+  if(pathsToExpand_.isEmpty())
+    return;
+
+  FmPath* path = pathsToExpand_.first();
+  // 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 */
+    if(currentPath_)
+      fm_path_unref(currentPath_);
+    currentPath_ = fm_path_ref(path);
+
+    cancelPendingChdir(); // FIXME: is this correct? this is not done in the gtk+ version of libfm.
+  }
+}
+
+void DirTreeView::onRowLoaded(const QModelIndex& index) {
+  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_.removeFirst();
+  if(pathsToExpand_.isEmpty()) {  /* 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(FmPath* path) {
+  DirTreeModel* _model = static_cast<DirTreeModel*>(model());
+  if(!_model)
+    return;
+  int rowCount = _model->rowCount(QModelIndex());
+  if(rowCount <= 0 || fm_path_equal(currentPath_, path))
+    return;
+
+  if(currentPath_)
+    fm_path_unref(currentPath_);
+  currentPath_ = fm_path_ref(path);
+
+  // NOTE: The content of each node is loaded on demand dynamically.
+  // So, when we ask for a chdir operation, some nodes do not exists yet.
+  // We have to wait for the loading of child nodes and continue the
+  // pending chdir operation after the child nodes become available.
+
+  // cancel previous pending tree expansion
+  cancelPendingChdir();
+
+  /* find a root item containing this path */
+  FmPath* root;
+  for(int row = 0; row < rowCount; ++row) {
+    QModelIndex index = _model->index(row, 0, QModelIndex());
+    root = _model->filePath(index);
+    if(fm_path_has_prefix(path, root))
+      break;
+    root = NULL;
+  }
+
+  if(root) { /* root item is found */
+    do { /* add path elements one by one to a list */
+      pathsToExpand_.prepend(path);
+      // qDebug() << "prepend path: " << Path(path).displayBasename();
+      if(fm_path_equal(path, root))
+        break;
+      path = fm_path_get_parent(path);
+    }
+    while(path);
+
+    expandPendingPath();
+  }
+}
+
+void DirTreeView::setModel(QAbstractItemModel* model) {
+  Q_ASSERT(model->inherits("Fm::DirTreeModel"));
+
+  if(!pathsToExpand_.isEmpty()) // 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);
+    FmFileInfo* fileInfo = reinterpret_cast<FmFileInfo*>(data.value<void*>());
+    if(fileInfo) {
+      FmPath* path = fm_file_info_get_path(fileInfo);
+      FmFileInfoList* files = fm_file_info_list_new();
+      fm_file_info_list_push_tail(files, 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);
+      fm_file_info_list_unref(files);
+      QVariant pathData = qVariantFromValue(reinterpret_cast<void*>(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(fm_file_info_is_native(fileInfo)) {
+        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())) {
+    FmPath* path = reinterpret_cast<FmPath*>(action->data().value<void*>());
+    Q_EMIT openFolderInNewWindowRequested(path);
+  }
+}
+
+void DirTreeView::onNewTab() {
+  if(QAction* action = qobject_cast<QAction*>(sender())) {
+    FmPath* path = reinterpret_cast<FmPath*>(action->data().value<void*>());
+    Q_EMIT openFolderInNewTabRequested(path);
+  }
+}
+
+void DirTreeView::onOpenInTerminal() {
+  if(QAction* action = qobject_cast<QAction*>(sender())) {
+    FmPath* path = reinterpret_cast<FmPath*>(action->data().value<void*>());
+    Q_EMIT openFolderInTerminalRequested(path);
+  }
+}
+
+void DirTreeView::onNewFolder() {
+  if(QAction* action = qobject_cast<QAction*>(sender())) {
+    FmPath* path = reinterpret_cast<FmPath*>(action->data().value<void*>());
+    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::onSelectionChanged(const QItemSelection & selected, const QItemSelection & deselected) {
+  if(!selected.isEmpty()) {
+    QModelIndex index = selected.first().topLeft();
+    DirTreeModel* _model = static_cast<DirTreeModel*>(model());
+    FmPath* path = _model->filePath(index);
+    if(path && currentPath_ && fm_path_equal(path, currentPath_))
+      return;
+    cancelPendingChdir();
+    if(!path)
+      return;
+    if(currentPath_)
+      fm_path_unref(currentPath_);
+    currentPath_ = fm_path_ref(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, path);
+  }
+}
+
+
+} // namespace Fm
diff --git a/src/dirtreeview.h b/src/dirtreeview.h
new file mode 100644 (file)
index 0000000..44f4981
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * 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 "path.h"
+
+class QItemSelection;
+
+namespace Fm {
+
+class FileMenu;
+class DirTreeModelItem;
+
+class LIBFM_QT_API DirTreeView : public QTreeView {
+  Q_OBJECT
+
+public:
+  DirTreeView(QWidget* parent);
+  ~DirTreeView();
+
+  FmPath* currentPath() {
+    return currentPath_;
+  }
+
+  void setCurrentPath(FmPath* path);
+
+  // libfm-gtk compatible alias
+  FmPath* getCwd() {
+    return currentPath();
+  }
+
+  void chdir(FmPath* path) {
+    setCurrentPath(path);
+  }
+
+  virtual void setModel(QAbstractItemModel* model);
+
+protected:
+  virtual void mousePressEvent(QMouseEvent* event);
+
+private:
+  void cancelPendingChdir();
+  void expandPendingPath();
+
+Q_SIGNALS:
+  void chdirRequested(int type, FmPath* path);
+  void openFolderInNewWindowRequested(FmPath* path);
+  void openFolderInNewTabRequested(FmPath* path);
+  void openFolderInTerminalRequested(FmPath* path);
+  void createNewFolderRequested(FmPath* 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();
+
+private:
+  FmPath* currentPath_;
+  QList<Path> pathsToExpand_;
+  DirTreeModelItem* currentExpandingItem_;
+};
+
+}
+
+#endif // FM_DIRTREEVIEW_H
diff --git a/src/dndactionmenu.cpp b/src/dndactionmenu.cpp
new file mode 100644 (file)
index 0000000..73625ee
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * 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
diff --git a/src/dndactionmenu.h b/src/dndactionmenu.h
new file mode 100644 (file)
index 0000000..247aa1f
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * 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
diff --git a/src/dnddest.cpp b/src/dnddest.cpp
new file mode 100644 (file)
index 0000000..daf22f2
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * 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);
+    FmPathList* 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:
+        fm_path_list_unref(srcPaths);
+        return false;
+    }
+    fm_path_list_unref(srcPaths);
+    return true;
+  }
+  return false;
+}
+
+bool DndDest::isSupported(const QMimeData* data) {
+  return false;
+}
+
+bool DndDest::isSupported(QString mimeType) {
+  return false;
+}
+
+
+} // namespace Fm
diff --git a/src/dnddest.h b/src/dnddest.h
new file mode 100644 (file)
index 0000000..e292c3f
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * 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 "path.h"
+
+namespace Fm {
+
+class DndDest {
+public:
+  DndDest();
+  ~DndDest();
+
+  void setDestPath(FmPath* dest) {
+    destPath_ = dest;
+  }
+
+  const Path& destPath() {
+    return destPath_;
+  }
+
+  bool isSupported(const QMimeData* data);
+  bool isSupported(QString mimeType);
+
+  bool dropMimeData(const QMimeData* data, Qt::DropAction action);
+
+private:
+  Path destPath_;
+};
+
+}
+
+#endif // FM_DNDDEST_H
diff --git a/src/dummymonitor.h b/src/dummymonitor.h
new file mode 100644 (file)
index 0000000..1570250
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * 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_DUMMY_MONITOR_H__
+#define __LIBFM_QT_FM_DUMMY_MONITOR_H__
+
+#include <libfm/fm.h>
+#include <QObject>
+#include <QtGlobal>
+#include "libfmqtglobals.h"
+
+
+namespace Fm {
+
+
+
+}
+
+#endif // __LIBFM_QT_FM_DUMMY_MONITOR_H__
diff --git a/src/edit-bookmarks.ui b/src/edit-bookmarks.ui
new file mode 100644 (file)
index 0000000..8d989e6
--- /dev/null
@@ -0,0 +1,143 @@
+<?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>&amp;Add Item</string>
+       </property>
+       <property name="icon">
+        <iconset theme="list-add"/>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="removeItem">
+       <property name="text">
+        <string>&amp;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>
diff --git a/src/editbookmarksdialog.cpp b/src/editbookmarksdialog.cpp
new file mode 100644 (file)
index 0000000..3fae409
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * 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
diff --git a/src/editbookmarksdialog.h b/src/editbookmarksdialog.h
new file mode 100644 (file)
index 0000000..bf76f9b
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * 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
diff --git a/src/exec-file.ui b/src/exec-file.ui
new file mode 100644 (file)
index 0000000..c5a9ea3
--- /dev/null
@@ -0,0 +1,163 @@
+<?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>&amp;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&amp;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 &amp;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>
diff --git a/src/execfiledialog.cpp b/src/execfiledialog.cpp
new file mode 100644 (file)
index 0000000..3759d0b
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * 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"
+
+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
+  FmIcon* icon = fm_file_info_get_icon(fileInfo_);
+  ui->icon->setPixmap(IconTheme::icon(icon).pixmap(QSize(48, 48)));
+
+  QString msg;
+  if(fm_file_info_is_text(file)) {
+    msg = tr("This text file '%1' seems to be an executable script.\nWhat do you want to do with it?")
+            .arg(QString::fromUtf8(fm_file_info_get_disp_name(file)));
+    ui->execTerm->setDefault(true);
+  }
+  else {
+    msg= tr("This file '%1' is executable. Do you want to execute it?")
+           .arg(QString::fromUtf8(fm_file_info_get_disp_name(file)));
+    ui->exec->setDefault(true);
+    ui->open->hide();
+  }
+  ui->msg->setText(msg);
+}
+
+ExecFileDialog::~ExecFileDialog() {
+  delete ui;
+  if(fileInfo_)
+    fm_file_info_unref(fileInfo_);
+}
+
+void ExecFileDialog::accept() {
+  QObject* _sender = sender();
+  if(_sender == ui->exec)
+    result_ = FM_FILE_LAUNCHER_EXEC;
+  else if(_sender == ui->execTerm)
+    result_ = FM_FILE_LAUNCHER_EXEC_IN_TERMINAL;
+  else if(_sender == ui->open)
+    result_ = FM_FILE_LAUNCHER_EXEC_OPEN;
+  QDialog::accept();
+}
+
+} // namespace Fm
diff --git a/src/execfiledialog_p.h b/src/execfiledialog_p.h
new file mode 100644 (file)
index 0000000..10af394
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * 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
diff --git a/src/file-operation-dialog.ui b/src/file-operation-dialog.ui
new file mode 100644 (file)
index 0000000..f81018e
--- /dev/null
@@ -0,0 +1,171 @@
+<?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="4" 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="4" 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>
+    </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>
diff --git a/src/file-props.ui b/src/file-props.ui
new file mode 100644 (file)
index 0000000..23dd661
--- /dev/null
@@ -0,0 +1,736 @@
+<?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>
diff --git a/src/file.h b/src/file.h
new file mode 100644 (file)
index 0000000..8902cda
--- /dev/null
@@ -0,0 +1,153 @@
+/*
+ * 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_FILE_H__
+#define __LIBFM_QT_FM_FILE_H__
+
+#include <libfm/fm.h>
+#include <QObject>
+#include <QtGlobal>
+#include "libfmqtglobals.h"
+
+
+namespace Fm {
+
+
+class LIBFM_QT_API File {
+public:
+
+
+  // default constructor
+  File() {
+    dataPtr_ = nullptr;
+  }
+
+
+  File(FmFile* dataPtr){
+    dataPtr_ = dataPtr != nullptr ? reinterpret_cast<GObject*>(g_object_ref(dataPtr)) : nullptr;
+  }
+
+
+  // copy constructor
+  File(const File& other) {
+    dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<GObject*>(g_object_ref(other.dataPtr_)) : nullptr;
+  }
+
+
+  // move constructor
+  File(File&& other) {
+    dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
+  }
+
+
+  // destructor
+  virtual ~File() {
+    if(dataPtr_ != nullptr) {
+      g_object_unref(dataPtr_);
+    }
+  }
+
+
+  // create a wrapper for the data pointer without increasing the reference count
+  static File wrapPtr(FmFile* dataPtr) {
+    File obj;
+    obj.dataPtr_ = reinterpret_cast<GObject*>(dataPtr);
+    return obj;
+  }
+
+  // disown the managed data pointer
+  FmFile* takeDataPtr() {
+    FmFile* data = reinterpret_cast<FmFile*>(dataPtr_);
+    dataPtr_ = nullptr;
+    return data;
+  }
+
+  // get the raw pointer wrapped
+  FmFile* dataPtr() {
+    return reinterpret_cast<FmFile*>(dataPtr_);
+  }
+
+  // automatic type casting
+  operator FmFile*() {
+    return dataPtr();
+  }
+
+  // automatic type casting
+  operator void*() {
+    return dataPtr();
+  }
+
+
+  // copy assignment
+  File& operator=(const File& 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
+  File& operator=(File&& other) {
+    dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
+    return *this;
+  }
+
+  bool isNull() {
+    return (dataPtr_ == nullptr);
+  }
+
+  // methods
+
+  static GFile* newForCommandlineArg(const char* arg) {
+    return fm_file_new_for_commandline_arg(arg);
+  }
+
+
+  static GFile* newForUri(const char* uri) {
+    return fm_file_new_for_uri(uri);
+  }
+
+
+  static bool wantsIncremental(GFile* file) {
+    return fm_file_wants_incremental(file);
+  }
+
+
+  static void addVfs(const char* name, FmFileInitTable* init) {
+    fm_file_add_vfs(name, init);
+  }
+
+
+  // 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_FILE_H__
diff --git a/src/fileinfo.h b/src/fileinfo.h
new file mode 100644 (file)
index 0000000..3322cbd
--- /dev/null
@@ -0,0 +1,511 @@
+/*
+ * 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_FILE_INFO_H__
+#define __LIBFM_QT_FM_FILE_INFO_H__
+
+#include <libfm/fm.h>
+#include <QObject>
+#include <QtGlobal>
+#include "libfmqtglobals.h"
+
+
+namespace Fm {
+
+
+class LIBFM_QT_API FileInfoList {
+public:
+
+
+  FileInfoList( ) {
+    dataPtr_ = reinterpret_cast<FmFileInfoList*>(fm_file_info_list_new());
+  }
+
+
+  FileInfoList(FmFileInfoList* dataPtr){
+    dataPtr_ = dataPtr != nullptr ? reinterpret_cast<FmFileInfoList*>(fm_list_ref(FM_LIST(dataPtr))) : nullptr;
+  }
+
+
+  // copy constructor
+  FileInfoList(const FileInfoList& other) {
+    dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<FmFileInfoList*>(fm_list_ref(FM_LIST(other.dataPtr_))) : nullptr;
+  }
+
+
+  // move constructor
+  FileInfoList(FileInfoList&& other) {
+    dataPtr_ = reinterpret_cast<FmFileInfoList*>(other.takeDataPtr());
+  }
+
+
+  // destructor
+  ~FileInfoList() {
+    if(dataPtr_ != nullptr) {
+      fm_list_unref(FM_LIST(dataPtr_));
+    }
+  }
+
+
+  // create a wrapper for the data pointer without increasing the reference count
+  static FileInfoList wrapPtr(FmFileInfoList* dataPtr) {
+    FileInfoList obj;
+    obj.dataPtr_ = reinterpret_cast<FmFileInfoList*>(dataPtr);
+    return obj;
+  }
+
+  // disown the managed data pointer
+  FmFileInfoList* takeDataPtr() {
+    FmFileInfoList* data = reinterpret_cast<FmFileInfoList*>(dataPtr_);
+    dataPtr_ = nullptr;
+    return data;
+  }
+
+  // get the raw pointer wrapped
+  FmFileInfoList* dataPtr() {
+    return reinterpret_cast<FmFileInfoList*>(dataPtr_);
+  }
+  
+  // automatic type casting
+  operator FmFileInfoList*() {
+    return dataPtr();
+  }
+
+  // automatic type casting
+  operator void*() {
+    return dataPtr();
+  }
+
+
+  // copy assignment
+  FileInfoList& operator=(const FileInfoList& other) {
+    if(dataPtr_ != nullptr) {
+      fm_list_unref(FM_LIST(dataPtr_));
+    }
+    dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<FmFileInfoList*>(fm_list_ref(FM_LIST(other.dataPtr_))) : nullptr;
+    return *this;
+  }
+
+
+  // move assignment
+  FileInfoList& operator=(FileInfoList&& other) {
+    dataPtr_ = reinterpret_cast<FmFileInfoList*>(other.takeDataPtr());
+    return *this;
+  }
+
+  bool isNull() {
+    return (dataPtr_ == nullptr);
+  }
+
+  // methods
+
+  bool isSameFs(void) {
+    return fm_file_info_list_is_same_fs(dataPtr());
+  }
+
+
+  bool isSameType(void) {
+    return fm_file_info_list_is_same_type(dataPtr());
+  }
+
+
+  bool isEmpty() {
+    return fm_file_info_list_is_empty(dataPtr());
+  }
+
+  unsigned int getLength() {
+    return fm_file_info_list_get_length(dataPtr());
+  }
+
+  FmFileInfo* peekHead() {
+    return fm_file_info_list_peek_head(dataPtr());
+  }
+
+  GList* peekHeadLink() {
+    return fm_file_info_list_peek_head_link(dataPtr());
+  }
+
+  void pushTail(FmFileInfo* d) {
+    fm_file_info_list_push_tail(dataPtr(), d);
+  }
+
+  void pushTailLink(GList* d) {
+    fm_file_info_list_push_tail_link(dataPtr(), d);
+  }
+
+  FmFileInfo* popHead(){
+    return fm_file_info_list_pop_head(dataPtr());
+  }
+
+  void deleteLink(GList* _l) {
+    fm_file_info_list_delete_link(dataPtr(), _l);
+  }
+
+  void clear() {
+    fm_file_info_list_clear(dataPtr());
+  }
+
+
+private:
+  FmFileInfoList* dataPtr_; // data pointer for the underlying C struct
+
+};
+
+
+
+class LIBFM_QT_API FileInfo {
+public:
+
+
+  FileInfo( ) {
+    dataPtr_ = reinterpret_cast<FmFileInfo*>(fm_file_info_new());
+  }
+
+
+  FileInfo(FmFileInfo* dataPtr){
+    dataPtr_ = dataPtr != nullptr ? reinterpret_cast<FmFileInfo*>(fm_file_info_ref(dataPtr)) : nullptr;
+  }
+
+
+  // copy constructor
+  FileInfo(const FileInfo& other) {
+    dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<FmFileInfo*>(fm_file_info_ref(other.dataPtr_)) : nullptr;
+  }
+
+
+  // move constructor
+  FileInfo(FileInfo&& other) {
+    dataPtr_ = reinterpret_cast<FmFileInfo*>(other.takeDataPtr());
+  }
+
+
+  // destructor
+  ~FileInfo() {
+    if(dataPtr_ != nullptr) {
+      fm_file_info_unref(dataPtr_);
+    }
+  }
+
+
+  // create a wrapper for the data pointer without increasing the reference count
+  static FileInfo wrapPtr(FmFileInfo* dataPtr) {
+    FileInfo obj;
+    obj.dataPtr_ = reinterpret_cast<FmFileInfo*>(dataPtr);
+    return obj;
+  }
+
+  // disown the managed data pointer
+  FmFileInfo* takeDataPtr() {
+    FmFileInfo* data = reinterpret_cast<FmFileInfo*>(dataPtr_);
+    dataPtr_ = nullptr;
+    return data;
+  }
+
+  // get the raw pointer wrapped
+  FmFileInfo* dataPtr() {
+    return reinterpret_cast<FmFileInfo*>(dataPtr_);
+  }
+
+  // automatic type casting
+  operator FmFileInfo*() {
+    return dataPtr();
+  }
+
+
+  // copy assignment
+  FileInfo& operator=(const FileInfo& other) {
+    if(dataPtr_ != nullptr) {
+      fm_file_info_unref(dataPtr_);
+    }
+    dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<FmFileInfo*>(fm_file_info_ref(other.dataPtr_)) : nullptr;
+    return *this;
+  }
+
+
+  // move assignment
+  FileInfo& operator=(FileInfo&& other) {
+    dataPtr_ = reinterpret_cast<FmFileInfo*>(other.takeDataPtr());
+    return *this;
+  }
+
+  bool isNull() {
+    return (dataPtr_ == nullptr);
+  }
+
+  // methods
+
+  bool canSetHidden(void) {
+    return fm_file_info_can_set_hidden(dataPtr());
+  }
+
+
+  bool canSetIcon(void) {
+    return fm_file_info_can_set_icon(dataPtr());
+  }
+
+
+  bool canSetName(void) {
+    return fm_file_info_can_set_name(dataPtr());
+  }
+
+
+  bool canThumbnail(void) {
+    return fm_file_info_can_thumbnail(dataPtr());
+  }
+
+
+  dev_t getDev(void) {
+    return fm_file_info_get_dev(dataPtr());
+  }
+
+
+  gid_t getGid(void) {
+    return fm_file_info_get_gid(dataPtr());
+  }
+
+
+  uid_t getUid(void) {
+    return fm_file_info_get_uid(dataPtr());
+  }
+
+  const char* getDispGroup() {
+    return fm_file_info_get_disp_group(dataPtr());
+  }
+
+  const char* getFsId() {
+    return fm_file_info_get_fs_id(dataPtr());
+  }
+
+  FmIcon* getIcon(void) {
+    return fm_file_info_get_icon(dataPtr());
+  }
+
+
+  time_t getCtime(void) {
+    return fm_file_info_get_ctime(dataPtr());
+  }
+
+
+  time_t getAtime(void) {
+    return fm_file_info_get_atime(dataPtr());
+  }
+
+
+  time_t getMtime(void) {
+    return fm_file_info_get_mtime(dataPtr());
+  }
+
+
+  const char* getTarget() {
+    return fm_file_info_get_target(dataPtr());
+  }
+
+  const char* getCollateKey() {
+    return fm_file_info_get_collate_key(dataPtr());
+  }
+
+  const char* getCollateKeyNoCaseFold() {
+    return fm_file_info_get_collate_key_nocasefold(dataPtr());
+  }
+
+  const char* getDesc() {
+    return fm_file_info_get_desc(dataPtr());
+  }
+
+  const char* getDispMtime() {
+    return fm_file_info_get_disp_mtime(dataPtr());
+  }
+
+  bool isWritableDirectory(void) {
+    return fm_file_info_is_writable_directory(dataPtr());
+  }
+
+
+  bool isAccessible(void) {
+    return fm_file_info_is_accessible(dataPtr());
+  }
+
+
+  bool isExecutableType(void) {
+    return fm_file_info_is_executable_type(dataPtr());
+  }
+
+
+  bool isBackup(void) {
+    return fm_file_info_is_backup(dataPtr());
+  }
+
+
+  bool isHidden(void) {
+    return fm_file_info_is_hidden(dataPtr());
+  }
+
+
+  bool isUnknownType(void) {
+    return fm_file_info_is_unknown_type(dataPtr());
+  }
+
+
+  bool isDesktopEntry(void) {
+    return fm_file_info_is_desktop_entry(dataPtr());
+  }
+
+
+  bool isText(void) {
+    return fm_file_info_is_text(dataPtr());
+  }
+
+
+  bool isImage(void) {
+    return fm_file_info_is_image(dataPtr());
+  }
+
+
+  bool isMountable(void) {
+    return fm_file_info_is_mountable(dataPtr());
+  }
+
+
+  bool isShortcut(void) {
+    return fm_file_info_is_shortcut(dataPtr());
+  }
+
+
+  bool isSymlink(void) {
+    return fm_file_info_is_symlink(dataPtr());
+  }
+
+
+  bool isDir(void) {
+    return fm_file_info_is_dir(dataPtr());
+  }
+
+
+  FmMimeType* getMimeType(void) {
+    return fm_file_info_get_mime_type(dataPtr());
+  }
+
+
+  bool isNative(void) {
+    return fm_file_info_is_native(dataPtr());
+  }
+
+
+  mode_t getMode(void) {
+    return fm_file_info_get_mode(dataPtr());
+  }
+
+
+  goffset getBlocks(void) {
+    return fm_file_info_get_blocks(dataPtr());
+  }
+
+
+  goffset getSize(void) {
+    return fm_file_info_get_size(dataPtr());
+  }
+
+  const char* getDispSize() {
+    return fm_file_info_get_disp_size(dataPtr());
+  }
+
+  void setIcon(GIcon* icon) {
+    fm_file_info_set_icon(dataPtr(), icon);
+  }
+
+
+  void setDispName(const char* name) {
+    fm_file_info_set_disp_name(dataPtr(), name);
+  }
+
+
+  void setPath(FmPath* path) {
+    fm_file_info_set_path(dataPtr(), path);
+  }
+
+
+  const char* getName() {
+    return fm_file_info_get_name(dataPtr());
+  }
+
+
+  const char* getDispName() {
+    return fm_file_info_get_disp_name(dataPtr());
+  }
+
+
+  FmPath* getPath(void) {
+    return fm_file_info_get_path(dataPtr());
+  }
+
+
+  void update(FmFileInfo* src) {
+    fm_file_info_update(dataPtr(), src);
+  }
+
+
+  static FileInfo newFromNativeFile(FmPath* path, const char* path_str, GError** err) {
+    return FileInfo::wrapPtr(fm_file_info_new_from_native_file(path, path_str, err));
+  }
+
+
+  bool setFromNativeFile(const char* path, GError** err) {
+    return fm_file_info_set_from_native_file(dataPtr(), path, err);
+  }
+
+
+  void setFromMenuCacheItem(struct _MenuCacheItem* item) {
+    fm_file_info_set_from_menu_cache_item(dataPtr(), item);
+  }
+
+
+  static FileInfo newFromMenuCacheItem(FmPath* path, struct _MenuCacheItem* item) {
+    return FileInfo::wrapPtr(fm_file_info_new_from_menu_cache_item(path, item));
+  }
+
+
+  void setFromGFileData(GFile* gf, GFileInfo* inf) {
+    fm_file_info_set_from_g_file_data(dataPtr(), gf, inf);
+  }
+
+
+  static FileInfo newFromGFileData(GFile* gf, GFileInfo* inf, FmPath* path) {
+    return FileInfo::wrapPtr(fm_file_info_new_from_g_file_data(gf, inf, path));
+  }
+
+
+  void setFromGfileinfo(GFileInfo* inf) {
+    fm_file_info_set_from_gfileinfo(dataPtr(), inf);
+  }
+
+
+  static FileInfo newFromGfileinfo(FmPath* path, GFileInfo* inf) {
+    return FileInfo::wrapPtr(fm_file_info_new_from_gfileinfo(path, inf));
+  }
+
+
+private:
+  FmFileInfo* dataPtr_; // data pointer for the underlying C struct
+
+};
+
+
+}
+
+#endif // __LIBFM_QT_FM_FILE_INFO_H__
diff --git a/src/fileinfojob.h b/src/fileinfojob.h
new file mode 100644 (file)
index 0000000..396b254
--- /dev/null
@@ -0,0 +1,138 @@
+/*
+ * 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_FILE_INFO_JOB_H__
+#define __LIBFM_QT_FM_FILE_INFO_JOB_H__
+
+#include <libfm/fm.h>
+#include <QObject>
+#include <QtGlobal>
+#include "libfmqtglobals.h"
+#include "job.h"
+
+namespace Fm {
+
+
+class LIBFM_QT_API FileInfoJob: public Job {
+public:
+
+
+  FileInfoJob(FmPathList* files_to_query, FmFileInfoJobFlags flags) {
+    dataPtr_ = reinterpret_cast<GObject*>(fm_file_info_job_new(files_to_query, flags));
+  }
+
+
+  // default constructor
+  FileInfoJob() {
+    dataPtr_ = nullptr;
+  }
+
+
+  FileInfoJob(FmFileInfoJob* dataPtr){
+    dataPtr_ = dataPtr != nullptr ? reinterpret_cast<GObject*>(g_object_ref(dataPtr)) : nullptr;
+  }
+
+
+  // copy constructor
+  FileInfoJob(const FileInfoJob& other) {
+    dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<GObject*>(g_object_ref(other.dataPtr_)) : nullptr;
+  }
+
+
+  // move constructor
+  FileInfoJob(FileInfoJob&& other) {
+    dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
+  }
+
+
+
+  // create a wrapper for the data pointer without increasing the reference count
+  static FileInfoJob wrapPtr(FmFileInfoJob* dataPtr) {
+    FileInfoJob obj;
+    obj.dataPtr_ = reinterpret_cast<GObject*>(dataPtr);
+    return obj;
+  }
+
+  // disown the managed data pointer
+  FmFileInfoJob* takeDataPtr() {
+    FmFileInfoJob* data = reinterpret_cast<FmFileInfoJob*>(dataPtr_);
+    dataPtr_ = nullptr;
+    return data;
+  }
+
+  // get the raw pointer wrapped
+  FmFileInfoJob* dataPtr() {
+    return reinterpret_cast<FmFileInfoJob*>(dataPtr_);
+  }
+
+  // automatic type casting
+  operator FmFileInfoJob*() {
+    return dataPtr();
+  }
+
+  // automatic type casting
+  operator void*() {
+    return dataPtr();
+  }
+
+
+  // copy assignment
+  FileInfoJob& operator=(const FileInfoJob& 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
+  FileInfoJob& operator=(FileInfoJob&& other) {
+    dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
+    return *this;
+  }
+
+  bool isNull() {
+    return (dataPtr_ == nullptr);
+  }
+
+  // methods
+
+  FmPath* getCurrent(void) {
+    return fm_file_info_job_get_current(dataPtr());
+  }
+
+
+  void addGfile(GFile* gf) {
+    fm_file_info_job_add_gfile(dataPtr(), gf);
+  }
+
+
+  void add(FmPath* path) {
+    fm_file_info_job_add(dataPtr(), path);
+  }
+
+
+
+};
+
+
+}
+
+#endif // __LIBFM_QT_FM_FILE_INFO_JOB_H__
diff --git a/src/filelauncher.cpp b/src/filelauncher.cpp
new file mode 100644 (file)
index 0000000..1bee905
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+ * 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
+};
+
+FileLauncher::FileLauncher():
+  quickExec_(false) {
+}
+
+FileLauncher::~FileLauncher() {
+}
+
+//static
+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(NULL);
+  if(mime_type)
+    dlg.setMimeType(mime_type);
+  else
+    dlg.setCanSetDefault(false);
+  // FIXME: show error properly?
+  if(execModelessDialog(&dlg) == QDialog::Accepted) {
+    return dlg.selectedApp();
+  }
+  return NULL;
+}
+
+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, NULL, msg, btn_labels);
+  return -1;
+}
+
+bool FileLauncher::error(GAppLaunchContext* ctx, GError* err, FmPath* path) {
+  /* ask for mount if trying to launch unmounted path */
+  if(err->domain == G_IO_ERROR) {
+    if(path && err->code == G_IO_ERROR_NOT_MOUNTED) {
+      //if(fm_mount_path(data->parent, path, TRUE))
+      //  return FALSE; /* ask to retry */
+    }
+    else if(err->code == G_IO_ERROR_FAILED_HANDLED)
+      return true; /* don't show error message */
+  }
+  QMessageBox dlg(QMessageBox::Critical, QObject::tr("Error"), QString::fromUtf8(err->message), QMessageBox::Ok);
+  execModelessDialog(&dlg);
+  return true;
+}
+
+
+} // namespace Fm
diff --git a/src/filelauncher.h b/src/filelauncher.h
new file mode 100644 (file)
index 0000000..81c1dc6
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * 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>
+
+namespace Fm {
+
+class LIBFM_QT_API FileLauncher {
+public:
+  FileLauncher();
+  virtual ~FileLauncher();
+
+  bool launchFiles(QWidget* parent, FmFileInfoList* file_infos) {
+    GList* fileList = fm_file_info_list_peek_head_link(file_infos);
+    return launchFiles(parent, fileList);
+  }
+  bool launchPaths(QWidget* parent, FmPathList* paths) {
+    GList* pathList = fm_path_list_peek_head_link(paths);
+    return launchPaths(parent, pathList);
+  }
+
+  bool launchFiles(QWidget* parent, GList* file_infos);
+  bool launchPaths(QWidget* parent, GList* 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:
+  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
diff --git a/src/filemenu.cpp b/src/filemenu.cpp
new file mode 100644 (file)
index 0000000..49bc6e2
--- /dev/null
@@ -0,0 +1,408 @@
+/*
+ * 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"
+#ifdef CUSTOM_ACTIONS
+#include <libfm/fm-actions.h>
+#include "customaction_p.h"
+#endif
+#include <QMessageBox>
+#include <QDebug>
+#include "filemenu_p.h"
+
+namespace Fm {
+
+FileMenu::FileMenu(FmFileInfoList* files, FmFileInfo* info, FmPath* cwd, QWidget* parent):
+  QMenu(parent),
+  fileLauncher_(NULL) {
+  createMenu(files, info, cwd);
+}
+
+FileMenu::FileMenu(FmFileInfoList* files, FmFileInfo* info, FmPath* cwd, const QString& title, QWidget* parent):
+  QMenu(title, parent),
+  unTrashAction_(NULL),
+  fileLauncher_(NULL) {
+  createMenu(files, info, cwd);
+}
+
+FileMenu::~FileMenu() {
+  if(files_)
+    fm_file_info_list_unref(files_);
+  if(info_)
+    fm_file_info_unref(info_);
+  if(cwd_)
+    fm_path_unref(cwd_);
+}
+
+void FileMenu::createMenu(FmFileInfoList* files, FmFileInfo* info, FmPath* cwd) {
+  useTrash_ = true;
+  confirmDelete_ = true;
+  confirmTrash_ = false; // Confirm before moving files into "trash can"
+
+  openAction_ = NULL;
+  openWithMenuAction_ = NULL;
+  openWithAction_ = NULL;
+  separator1_ = NULL;
+  cutAction_ = NULL;
+  copyAction_ = NULL;
+  pasteAction_ = NULL;
+  deleteAction_ = NULL;
+  unTrashAction_ = NULL;
+  renameAction_ = NULL;
+  separator2_ = NULL;
+  propertiesAction_ = NULL;
+
+  files_ = fm_file_info_list_ref(files);
+  info_ = info ? fm_file_info_ref(info) : NULL;
+  cwd_ = cwd ? fm_path_ref(cwd) : NULL;
+
+  FmFileInfo* first = fm_file_info_list_peek_head(files);
+  FmMimeType* mime_type = fm_file_info_get_mime_type(first);
+  FmPath* path = fm_file_info_get_path(first);
+  // check if the files are of the same type
+  sameType_ = fm_file_info_list_is_same_type(files);
+  // check if the files are on the same filesystem
+  sameFilesystem_ = fm_file_info_list_is_same_fs(files);
+  // check if the files are all virtual
+  allVirtual_ = sameFilesystem_ && fm_path_is_virtual(path);
+  // check if the files are all in the trash can
+  allTrash_ =  sameFilesystem_ && fm_path_is_trash(path);
+
+  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(fm_mime_type_get_type(mime_type));
+      GList* l;
+      for(l=apps;l;l=l->next) {
+        GAppInfo* app = G_APP_INFO(l->data);
+
+        // check if the command really exists
+        gchar * program_path = g_find_program_in_path(g_app_info_get_executable(app));
+        if (!program_path)
+          continue;
+        g_free(program_path);
+
+        // create a QAction for the application.
+        AppInfoAction* action = new AppInfoAction(app, menu);
+        connect(action, &QAction::triggered, this, &FileMenu::onApplicationTriggered);
+        menu->addAction(action);
+      }
+      // unref GAppInfos here, they are still ref'ed in the AppInfoActions above
+      g_list_foreach(apps, (GFunc)g_object_unref, NULL);
+      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);
+  FmPath* dirPath = fm_file_info_list_get_length(files) == 1 && fm_file_info_is_dir(first)
+      ? path : cwd_;
+  createAction_->setMenu(new CreateNewMenu(NULL, 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. */
+    for(GList* l = fm_file_info_list_peek_head_link(files_); l; l=l->next) {
+        FmPath *trash_path = fm_file_info_get_path(FM_FILE_INFO(l->data));
+        if(!fm_path_get_parent(trash_path) ||
+           !fm_path_is_trash_root(fm_path_get_parent(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_);
+  }
+
+#ifdef CUSTOM_ACTIONS
+  // DES-EMA custom actions integration
+  GList* files_list = fm_file_info_list_peek_head_link(files);
+  GList* items = fm_get_actions_for_files(files_list);
+  if(items) {
+    GList* l;
+    for(l=items; l; l=l->next) {
+      FmFileActionItem* item = FM_FILE_ACTION_ITEM(l->data);
+      if(l == items && item
+         && !(fm_file_action_item_is_action(item)
+              && !(fm_file_action_item_get_target(item) & FM_FILE_ACTION_TARGET_CONTEXT))) {
+        addSeparator(); // before all custom actions
+      }
+      addCustomActionItem(this, item);
+    }
+  }
+  g_list_foreach(items, (GFunc)fm_file_action_item_unref, NULL);
+  g_list_free(items);
+#endif
+  // archiver integration
+  // FIXME: we need to modify upstream libfm to include some Qt-based archiver programs.
+  if(!allVirtual_) {
+    if(sameType_) {
+      FmArchiver* archiver = fm_archiver_get_default();
+      if(archiver) {
+        if(fm_archiver_is_mime_type_supported(archiver, fm_mime_type_get_type(mime_type))) {
+          if(cwd_ && archiver->extract_to_cmd) {
+            QAction* action = new QAction(tr("Extract to..."), this);
+            connect(action, &QAction::triggered, this, &FileMenu::onExtract);
+            addAction(action);
+          }
+          if(archiver->extract_cmd) {
+            QAction* action = new QAction(tr("Extract Here"), this);
+            connect(action, &QAction::triggered, this, &FileMenu::onExtractHere);
+            addAction(action);
+          }
+        }
+        else {
+          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_);
+}
+
+#ifdef CUSTOM_ACTIONS
+void FileMenu::addCustomActionItem(QMenu* menu, FmFileActionItem* item) {
+  if(!item) { // separator
+    addSeparator();
+    return;
+  }
+
+  // this action is not for context menu
+  if(fm_file_action_item_is_action(item) && !(fm_file_action_item_get_target(item) & FM_FILE_ACTION_TARGET_CONTEXT))
+      return;
+
+  CustomAction* action = new CustomAction(item, menu);
+  menu->addAction(action);
+  if(fm_file_action_item_is_menu(item)) {
+    GList* subitems = fm_file_action_item_get_sub_items(item);
+    if (subitems != NULL) {
+      QMenu* submenu = new QMenu(menu);
+      for(GList* l = subitems; l; l = l->next) {
+        FmFileActionItem* subitem = FM_FILE_ACTION_ITEM(l->data);
+        addCustomActionItem(submenu, subitem);
+      }
+      action->setMenu(submenu);
+    }
+  }
+  else if(fm_file_action_item_is_action(item)) {
+    connect(action, &QAction::triggered, this, &FileMenu::onCustomActionTrigerred);
+  }
+}
+#endif
+
+void FileMenu::onOpenTriggered() {
+  if(fileLauncher_) {
+    fileLauncher_->launchFiles(NULL, files_);
+  }
+  else { // use the default launcher
+    Fm::FileLauncher launcher;
+    launcher.launchFiles(NULL, files_);
+  }
+}
+
+void FileMenu::onOpenWithTriggered() {
+  AppChooserDialog dlg(NULL);
+  if(sameType_) {
+    dlg.setMimeType(fm_file_info_get_mime_type(info_));
+  }
+  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) {
+    GAppInfo* app = dlg.selectedApp();
+    if(app) {
+      openFilesWithApp(app);
+      g_object_unref(app);
+    }
+  }
+}
+
+void FileMenu::openFilesWithApp(GAppInfo* app) {
+  FmPathList* paths = fm_path_list_new_from_file_info_list(files_);
+  GList* uris = NULL;
+  for(GList* l = fm_path_list_peek_head_link(paths); l; l = l->next) {
+    FmPath* path = FM_PATH(l->data);
+    char* uri = fm_path_to_uri(path);
+    uris = g_list_prepend(uris, uri);
+  }
+  fm_path_list_unref(paths);
+  fm_app_info_launch_uris(app, uris, NULL, NULL);
+  g_list_foreach(uris, (GFunc)g_free, NULL);
+  g_list_free(uris);
+}
+
+void FileMenu::onApplicationTriggered() {
+  AppInfoAction* action = static_cast<AppInfoAction*>(sender());
+  openFilesWithApp(action->appInfo());
+}
+
+#ifdef CUSTOM_ACTIONS
+void FileMenu::onCustomActionTrigerred() {
+  CustomAction* action = static_cast<CustomAction*>(sender());
+  FmFileActionItem* item = action->item();
+
+  GList* files = fm_file_info_list_peek_head_link(files_);
+  char* output = NULL;
+  /* g_debug("item: %s is activated, id:%s", fm_file_action_item_get_name(item),
+      fm_file_action_item_get_id(item)); */
+  fm_file_action_item_launch(item, NULL, files, &output);
+  if(output) {
+    QMessageBox::information(this, tr("Output"), QString::fromUtf8(output));
+    g_free(output);
+  }
+}
+#endif
+
+void FileMenu::onFilePropertiesTriggered() {
+  FilePropsDialog::showForFiles(files_);
+}
+
+void FileMenu::onCopyTriggered() {
+  FmPathList* paths = fm_path_list_new_from_file_info_list(files_);
+  Fm::copyFilesToClipboard(paths);
+  fm_path_list_unref(paths);
+}
+
+void FileMenu::onCutTriggered() {
+  FmPathList* paths = fm_path_list_new_from_file_info_list(files_);
+  Fm::cutFilesToClipboard(paths);
+  fm_path_list_unref(paths);
+}
+
+void FileMenu::onDeleteTriggered() {
+  FmPathList* paths = fm_path_list_new_from_file_info_list(files_);
+  if(useTrash_)
+    FileOperation::trashFiles(paths, confirmTrash_);
+  else
+    FileOperation::deleteFiles(paths, confirmDelete_);
+  fm_path_list_unref(paths);
+}
+
+void FileMenu::onUnTrashTriggered() {
+  FmPathList* paths = fm_path_list_new_from_file_info_list(files_);
+  FileOperation::unTrashFiles(paths);
+  fm_path_list_unref(paths);
+}
+
+void FileMenu::onPasteTriggered() {
+  Fm::pasteFilesFromClipboard(cwd_);
+}
+
+void FileMenu::onRenameTriggered() {
+  for(GList* l = fm_file_info_list_peek_head_link(files_); l; l = l->next) {
+    FmFileInfo* info = FM_FILE_INFO(l->data);
+    Fm::renameFile(info, NULL);
+  }
+}
+
+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) {
+    FmPathList* paths = fm_path_list_new_from_file_info_list(files_);
+    fm_archiver_create_archive(archiver, NULL, paths);
+    fm_path_list_unref(paths);
+  }
+}
+
+void FileMenu::onExtract() {
+  FmArchiver* archiver = fm_archiver_get_default();
+  if(archiver) {
+    FmPathList* paths = fm_path_list_new_from_file_info_list(files_);
+    fm_archiver_extract_archives(archiver, NULL, paths);
+    fm_path_list_unref(paths);
+  }
+}
+
+void FileMenu::onExtractHere() {
+  FmArchiver* archiver = fm_archiver_get_default();
+  if(archiver) {
+    FmPathList* paths = fm_path_list_new_from_file_info_list(files_);
+    fm_archiver_extract_archives_to(archiver, NULL, paths, cwd_);
+    fm_path_list_unref(paths);
+  }
+}
+
+} // namespace Fm
diff --git a/src/filemenu.h b/src/filemenu.h
new file mode 100644 (file)
index 0000000..45e9e69
--- /dev/null
@@ -0,0 +1,218 @@
+/*
+ * 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>
+
+class QAction;
+
+struct _FmFileActionItem;
+
+namespace Fm {
+
+class FileLauncher;
+
+class LIBFM_QT_API FileMenu : public QMenu {
+Q_OBJECT
+
+public:
+  explicit FileMenu(FmFileInfoList* files, FmFileInfo* info, FmPath* cwd, QWidget* parent = 0);
+  explicit FileMenu(FmFileInfoList* files, FmFileInfo* info, FmPath* cwd, const QString& title, QWidget* parent = 0);
+  ~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_;
+  }
+
+  FmFileInfoList* files() {
+    return files_;
+  }
+
+  FmFileInfo* firstFile() {
+    return info_;
+  }
+
+  FmPath* cwd() {
+    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 createMenu(FmFileInfoList* files, FmFileInfo* info, FmPath* cwd);
+#ifdef CUSTOM_ACTIONS
+  void addCustomActionItem(QMenu* menu, struct _FmFileActionItem* item);
+#endif
+  void openFilesWithApp(GAppInfo* app);
+
+protected Q_SLOTS:
+  void onOpenTriggered();
+  void onOpenWithTriggered();
+  void onFilePropertiesTriggered();
+  void onApplicationTriggered();
+#ifdef CUSTOM_ACTIONS
+  void onCustomActionTrigerred();
+#endif
+  void onCompress();
+  void onExtract();
+  void onExtractHere();
+
+  void onCutTriggered();
+  void onCopyTriggered();
+  void onPasteTriggered();
+  void onRenameTriggered();
+  void onDeleteTriggered();
+  void onUnTrashTriggered();
+
+private:
+  FmFileInfoList* files_;
+  FmFileInfo* info_;
+  FmPath* cwd_;
+  bool useTrash_;
+  bool confirmDelete_;
+  bool confirmTrash_; // Confirm before moving files into "trash can"
+
+  bool sameType_;
+  bool sameFilesystem_;
+  bool allVirtual_;
+  bool allTrash_;
+
+  QAction* openAction_;
+  QAction* openWithMenuAction_;
+  QAction* openWithAction_;
+  QAction* separator1_;
+  QAction* createAction_;
+  QAction* separator2_;
+  QAction* cutAction_;
+  QAction* copyAction_;
+  QAction* pasteAction_;
+  QAction* deleteAction_;
+  QAction* unTrashAction_;
+  QAction* renameAction_;
+  QAction* separator3_;
+  QAction* propertiesAction_;
+
+  FileLauncher* fileLauncher_;
+};
+
+}
+
+#endif // FM_FILEMENU_H
diff --git a/src/filemenu_p.h b/src/filemenu_p.h
new file mode 100644 (file)
index 0000000..73a6831
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * 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>
+
+namespace Fm {
+
+class AppInfoAction : public QAction {
+  Q_OBJECT
+public:
+  explicit AppInfoAction(GAppInfo* app, QObject* parent = 0):
+    QAction(QString::fromUtf8(g_app_info_get_name(app)), parent),
+    appInfo_(G_APP_INFO(g_object_ref(app))) {
+    setToolTip(QString::fromUtf8(g_app_info_get_description(app)));
+    GIcon* gicon = g_app_info_get_icon(app);
+    QIcon icon = IconTheme::icon(gicon);
+    setIcon(icon);
+  }
+
+  virtual ~AppInfoAction() {
+    if(appInfo_)
+      g_object_unref(appInfo_);
+  }
+
+  GAppInfo* appInfo() const {
+    return appInfo_;
+  }
+
+private:
+  GAppInfo* appInfo_;
+};
+
+} // namespace Fm
+
+#endif
diff --git a/src/fileoperation.cpp b/src/fileoperation.cpp
new file mode 100644 (file)
index 0000000..0d63772
--- /dev/null
@@ -0,0 +1,307 @@
+/*
+ * 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>
+
+namespace Fm {
+
+#define SHOW_DLG_DELAY  1000
+
+FileOperation::FileOperation(Type type, FmPathList* srcFiles, QObject* parent):
+  QObject(parent),
+  job_(fm_file_ops_job_new((FmFileOpType)type, srcFiles)),
+  dlg(NULL),
+  destPath(NULL),
+  srcPaths(fm_path_list_ref(srcFiles)),
+  uiTimer(NULL),
+  elapsedTimer_(NULL),
+  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 = NULL;
+  }
+  if(elapsedTimer_) {
+    delete elapsedTimer_;
+    elapsedTimer_ = NULL;
+  }
+
+  if(job_) {
+    disconnectJob();
+    g_object_unref(job_);
+  }
+
+  if(srcPaths)
+    fm_path_list_unref(srcPaths);
+
+  if(destPath)
+    fm_path_unref(destPath);
+}
+
+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);
+  }
+}
+
+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 = NULL;
+  }
+
+  if(dlg) {
+    dlg->done(QDialog::Accepted);
+    delete dlg;
+    dlg = NULL;
+  }
+  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! */
+    FmPathList* 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 */
+      /* FIXME: parent window might be already destroyed! */
+      QWidget* parent = NULL; // 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(unable_to_trash, false);
+      }
+    }
+  }
+  g_object_unref(job_);
+  job_ = NULL;
+
+  if(autoDestroy_)
+    delete this;
+}
+
+// static
+FileOperation* FileOperation::copyFiles(FmPathList* srcFiles, FmPath* dest, QWidget* parent) {
+  FileOperation* op = new FileOperation(FileOperation::Copy, srcFiles);
+  op->setDestination(dest);
+  op->run();
+  return op;
+}
+
+// static
+FileOperation* FileOperation::moveFiles(FmPathList* srcFiles, FmPath* dest, QWidget* parent) {
+  FileOperation* op = new FileOperation(FileOperation::Move, srcFiles);
+  op->setDestination(dest);
+  op->run();
+  return op;
+}
+
+//static
+FileOperation* FileOperation::symlinkFiles(FmPathList* srcFiles, FmPath* dest, QWidget* parent) {
+  FileOperation* op = new FileOperation(FileOperation::Link, srcFiles);
+  op->setDestination(dest);
+  op->run();
+  return op;
+}
+
+//static
+FileOperation* FileOperation::deleteFiles(FmPathList* 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 NULL;
+  }
+
+  FileOperation* op = new FileOperation(FileOperation::Delete, srcFiles);
+  op->run();
+  return op;
+}
+
+//static
+FileOperation* FileOperation::trashFiles(FmPathList* 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 NULL;
+  }
+
+  FileOperation* op = new FileOperation(FileOperation::Trash, srcFiles);
+  op->run();
+  return op;
+}
+
+//static
+FileOperation* FileOperation::unTrashFiles(FmPathList* srcFiles, QWidget* parent) {
+  FileOperation* op = new FileOperation(FileOperation::UnTrash, srcFiles);
+  op->run();
+  return op;
+}
+
+// static
+FileOperation* FileOperation::changeAttrFiles(FmPathList* srcFiles, QWidget* parent) {
+  //TODO
+  FileOperation* op = new FileOperation(FileOperation::ChangeAttr, srcFiles);
+  op->run();
+  return op;
+}
+
+
+} // namespace Fm
diff --git a/src/fileoperation.h b/src/fileoperation.h
new file mode 100644 (file)
index 0000000..7f8e67a
--- /dev/null
@@ -0,0 +1,164 @@
+/*
+ * 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>
+
+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, FmPathList* srcFiles, QObject* parent = 0);
+  virtual ~FileOperation();
+
+  void setDestination(FmPath* dest) {
+    destPath = fm_path_ref(dest);
+    fm_file_ops_job_set_dest(job_, 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(FmPathList* srcFiles, FmPath* dest, QWidget* parent = 0);
+  static FileOperation* moveFiles(FmPathList* srcFiles, FmPath* dest, QWidget* parent = 0);
+  static FileOperation* symlinkFiles(FmPathList* srcFiles, FmPath* dest, QWidget* parent = 0);
+  static FileOperation* deleteFiles(FmPathList* srcFiles, bool promp = true, QWidget* parent = 0);
+  static FileOperation* trashFiles(FmPathList* srcFiles, bool promp = true, QWidget* parent = 0);
+  static FileOperation* unTrashFiles(FmPathList* srcFiles, QWidget* parent = 0);
+  static FileOperation* changeAttrFiles(FmPathList* 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_ != NULL)) {
+      lastElapsed_ += elapsedTimer_->elapsed();
+      elapsedTimer_->invalidate();
+    }
+  }
+
+  void resumeElapsedTimer() {
+    if(Q_LIKELY(elapsedTimer_ != NULL)) {
+      elapsedTimer_->start();
+    }
+  }
+
+  qint64 elapsedTime() {
+    if(Q_LIKELY(elapsedTimer_ != NULL)) {
+      return lastElapsed_ + elapsedTimer_->elapsed();
+    }
+    return 0;
+  }
+
+private Q_SLOTS:
+  void onUiTimeout();
+
+private:
+  FmFileOpsJob* job_;
+  FileOperationDialog* dlg;
+  FmPath* destPath;
+  FmPathList* srcPaths;
+  QTimer* uiTimer;
+  QElapsedTimer* elapsedTimer_;
+  qint64 lastElapsed_;
+  bool updateRemainingTime_;
+  QString curFile;
+  bool autoDestroy_;
+};
+
+}
+
+#endif // FM_FILEOPERATION_H
diff --git a/src/fileoperationdialog.cpp b/src/fileoperationdialog.cpp
new file mode 100644 (file)
index 0000000..9aa7111
--- /dev/null
@@ -0,0 +1,178 @@
+/*
+ * 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 <QMessageBox>
+#include "ui_file-operation-dialog.h"
+
+namespace Fm {
+
+FileOperationDialog::FileOperationDialog(FileOperation* _operation):
+  QDialog(NULL),
+  operation(_operation),
+  defaultOption(-1) {
+
+  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(FmPath* dest) {
+  char* pathStr = fm_path_display_name(dest, false);
+  ui->dest->setText(QString::fromUtf8(pathStr));
+  g_free(pathStr);
+}
+
+void FileOperationDialog::setSourceFiles(FmPathList* srcFiles) {
+  GList* l;
+  for(l = fm_path_list_peek_head_link(srcFiles); l; l = l->next) {
+    FmPath* path = FM_PATH(l->data);
+    char* pathStr = fm_path_display_name(path, false);
+    ui->sourceFiles->addItem(QString::fromUtf8(pathStr));
+    g_free(pathStr);
+  }
+}
+
+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) {
+    QMessageBox::critical(this, tr("Error"), QString::fromUtf8(err->message));
+    if(severity == FM_JOB_ERROR_CRITICAL)
+      return FM_JOB_ABORT;
+  }
+  return FM_JOB_CONTINUE;
+}
+
+void FileOperationDialog::setCurFile(QString cur_file) {
+  ui->curFile->setText(cur_file);
+}
+
+void FileOperationDialog::setPercent(unsigned int percent) {
+  ui->progressBar->setValue(percent);
+}
+
+void FileOperationDialog::setRemainingTime(unsigned int sec) {
+  unsigned int min = 0;
+  unsigned int hr = 0;
+  if(sec > 60) {
+    min = sec / 60;
+    sec %= 60;
+    if(min > 60) {
+      hr = min / 60;
+      min %= 60;
+    }
+  }
+  ui->timeRemaining->setText(QString("%1:%2:%3")
+                             .arg(hr, 2, 10, QChar('0'))
+                             .arg(min, 2, 10, QChar('0'))
+                             .arg(sec, 2, 10, QChar('0')));
+}
+
+void FileOperationDialog::setPrepared() {
+}
+
+void FileOperationDialog::reject() {
+  operation->cancel();
+  QDialog::reject();
+}
+
+
+} // namespace Fm
diff --git a/src/fileoperationdialog.h b/src/fileoperationdialog.h
new file mode 100644 (file)
index 0000000..664f701
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * 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 <QDialog>
+#include <libfm/fm.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(FmPathList* srcFiles);
+  void setDestPath(FmPath* 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 setRemainingTime(unsigned int sec);
+
+  virtual void reject();
+
+private:
+  Ui::FileOperationDialog* ui;
+  FileOperation* operation;
+  int defaultOption;
+};
+
+}
+
+#endif // FM_FILEOPERATIONDIALOG_H
diff --git a/src/fileopsjob.h b/src/fileopsjob.h
new file mode 100644 (file)
index 0000000..e2bbe71
--- /dev/null
@@ -0,0 +1,193 @@
+/*
+ * 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_FILE_OPS_JOB_H__
+#define __LIBFM_QT_FM_FILE_OPS_JOB_H__
+
+#include <libfm/fm.h>
+#include <QObject>
+#include <QtGlobal>
+#include "libfmqtglobals.h"
+#include "job.h"
+
+namespace Fm {
+
+
+class LIBFM_QT_API FileOpsJob: public Job {
+public:
+
+
+  FileOpsJob(FmFileOpType type, FmPathList* files) {
+    dataPtr_ = reinterpret_cast<GObject*>(fm_file_ops_job_new(type, files));
+  }
+
+
+  // default constructor
+  FileOpsJob() {
+    dataPtr_ = nullptr;
+  }
+
+
+  FileOpsJob(FmFileOpsJob* dataPtr){
+    dataPtr_ = dataPtr != nullptr ? reinterpret_cast<GObject*>(g_object_ref(dataPtr)) : nullptr;
+  }
+
+
+  // copy constructor
+  FileOpsJob(const FileOpsJob& other) {
+    dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<GObject*>(g_object_ref(other.dataPtr_)) : nullptr;
+  }
+
+
+  // move constructor
+  FileOpsJob(FileOpsJob&& other) {
+    dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
+  }
+
+
+
+  // create a wrapper for the data pointer without increasing the reference count
+  static FileOpsJob wrapPtr(FmFileOpsJob* dataPtr) {
+    FileOpsJob obj;
+    obj.dataPtr_ = reinterpret_cast<GObject*>(dataPtr);
+    return obj;
+  }
+
+  // disown the managed data pointer
+  FmFileOpsJob* takeDataPtr() {
+    FmFileOpsJob* data = reinterpret_cast<FmFileOpsJob*>(dataPtr_);
+    dataPtr_ = nullptr;
+    return data;
+  }
+
+  // get the raw pointer wrapped
+  FmFileOpsJob* dataPtr() {
+    return reinterpret_cast<FmFileOpsJob*>(dataPtr_);
+  }
+
+  // automatic type casting
+  operator FmFileOpsJob*() {
+    return dataPtr();
+  }
+
+  // automatic type casting
+  operator void*() {
+    return dataPtr();
+  }
+
+
+  // copy assignment
+  FileOpsJob& operator=(const FileOpsJob& 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
+  FileOpsJob& operator=(FileOpsJob&& other) {
+    dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
+    return *this;
+  }
+
+  bool isNull() {
+    return (dataPtr_ == nullptr);
+  }
+
+  // methods
+
+  FmFileOpOption getOptions(void) {
+    return fm_file_ops_job_get_options(dataPtr());
+  }
+
+
+  FmFileOpOption askRename(GFile* src, GFileInfo* src_inf, GFile* dest, GFile** new_dest) {
+    return fm_file_ops_job_ask_rename(dataPtr(), src, src_inf, dest, new_dest);
+  }
+
+
+  void emitPercent(void) {
+    fm_file_ops_job_emit_percent(dataPtr());
+  }
+
+
+  void emitCurFile(const char* cur_file) {
+    fm_file_ops_job_emit_cur_file(dataPtr(), cur_file);
+  }
+
+
+  void emitPrepared(void) {
+    fm_file_ops_job_emit_prepared(dataPtr());
+  }
+
+
+  void setTarget(const char* url) {
+    fm_file_ops_job_set_target(dataPtr(), url);
+  }
+
+
+  void setHidden(gboolean hidden) {
+    fm_file_ops_job_set_hidden(dataPtr(), hidden);
+  }
+
+
+  void setIcon(GIcon* icon) {
+    fm_file_ops_job_set_icon(dataPtr(), icon);
+  }
+
+
+  void setDisplayName(const char* name) {
+    fm_file_ops_job_set_display_name(dataPtr(), name);
+  }
+
+
+  void setChown(gint uid, gint gid) {
+    fm_file_ops_job_set_chown(dataPtr(), uid, gid);
+  }
+
+
+  void setChmod(mode_t new_mode, mode_t new_mode_mask) {
+    fm_file_ops_job_set_chmod(dataPtr(), new_mode, new_mode_mask);
+  }
+
+
+  void setRecursive(gboolean recursive) {
+    fm_file_ops_job_set_recursive(dataPtr(), recursive);
+  }
+
+
+  FmPath* getDest(void) {
+    return fm_file_ops_job_get_dest(dataPtr());
+  }
+
+
+  void setDest(FmPath* dest) {
+    fm_file_ops_job_set_dest(dataPtr(), dest);
+  }
+
+
+
+};
+
+
+}
+
+#endif // __LIBFM_QT_FM_FILE_OPS_JOB_H__
diff --git a/src/filepropsdialog.cpp b/src/filepropsdialog.cpp
new file mode 100644 (file)
index 0000000..f14e1c4
--- /dev/null
@@ -0,0 +1,476 @@
+/*
+ * 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 <qdial.h>
+#include <sys/types.h>
+#include <time.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(FmFileInfoList* files, QWidget* parent, Qt::WindowFlags f):
+  QDialog(parent, f),
+  fileInfos_(fm_file_info_list_ref(files)),
+  fileInfo(fm_file_info_list_peek_head(files)),
+  singleType(fm_file_info_list_is_same_type(files)),
+  singleFile(fm_file_info_list_get_length(files) == 1 ? true:false),
+  mimeType(NULL) {
+
+  setAttribute(Qt::WA_DeleteOnClose);
+
+  ui = new Ui::FilePropsDialog();
+  ui->setupUi(this);
+
+  if(singleType) {
+    mimeType = fm_mime_type_ref(fm_file_info_get_mime_type(fileInfo));
+  }
+
+  FmPathList* paths = fm_path_list_new_from_file_info_list(files);
+  deepCountJob = fm_deep_count_job_new(paths, FM_DC_JOB_DEFAULT);
+  fm_path_list_unref(paths);
+
+  initGeneralPage();
+  initPermissionsPage();
+}
+
+FilePropsDialog::~FilePropsDialog() {
+  if(fileInfos_)
+    fm_file_info_list_unref(fileInfos_);
+
+  // Stop the timer if it's still running
+  if(fileSizeTimer) {
+    fileSizeTimer->stop();
+    delete fileSizeTimer;
+    fileSizeTimer = NULL;
+  }
+
+  // Cancel the indexing job if it hasn't finished
+  if(deepCountJob) {
+    g_signal_handlers_disconnect_by_func(deepCountJob, (gpointer)G_CALLBACK(onDeepCountJobFinished), this);
+    fm_job_cancel(FM_JOB(deepCountJob));
+    g_object_unref(deepCountJob);
+    deepCountJob = NULL;
+  }
+
+  // And finally delete the dialog's UI
+  delete ui;
+}
+
+void FilePropsDialog::initApplications() {
+  if(singleType && mimeType && !fm_file_info_is_dir(fileInfo)) {
+    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 = fm_file_info_get_uid(fileInfo);
+  gid = fm_file_info_get_gid(fileInfo);
+  mode_t mode = fm_file_info_get_mode(fileInfo);
+  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 = fm_file_info_is_native(fileInfo);
+  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
+  GList* l;
+  for(l = fm_file_info_list_peek_head_link(fileInfos_)->next; l; l = l->next) {
+    FmFileInfo* fi = FM_FILE_INFO(l->data);
+    if(allNative && !fm_file_info_is_native(fi))
+      allNative = false; // not all of the files are native
+
+    mode_t fi_mode = fm_file_info_get_mode(fi);
+    if(S_ISDIR(fi_mode))
+      hasDir = true; // the files list contains dir(s)
+
+    if(uid != DIFFERENT_UIDS && uid != fm_file_info_get_uid(fi))
+      uid = DIFFERENT_UIDS; // not all files have the same owner
+    if(gid != DIFFERENT_GIDS && gid != fm_file_info_get_gid(fi))
+      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
+    FmIcon* icon = NULL;
+    // 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 = fm_file_info_get_icon(fileInfo);
+    }
+    if(mimeType) {
+      if(!icon) // get an icon from mime type if needed
+        icon = fm_mime_type_get_icon(mimeType);
+      ui->fileType->setText(QString::fromUtf8(fm_mime_type_get_desc(mimeType)));
+      ui->mimeType->setText(QString::fromUtf8(fm_mime_type_get_type(mimeType)));
+    }
+    if(icon) {
+      ui->iconButton->setIcon(IconTheme::icon(icon));
+    }
+
+    if(singleFile && fm_file_info_is_symlink(fileInfo)) {
+      ui->target->setText(QString::fromUtf8(fm_file_info_get_target(fileInfo)));
+    }
+    else {
+      ui->target->hide();
+      ui->targetLabel->hide();
+    }
+  } // 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
+    FmPath* parent_path = fm_path_get_parent(fm_file_info_get_path(fileInfo));
+    char* parent_str = parent_path ? fm_path_display_name(parent_path, true) : NULL;
+
+    ui->fileName->setText(QString::fromUtf8(fm_file_info_get_disp_name(fileInfo)));
+    if(parent_str) {
+      ui->location->setText(QString::fromUtf8(parent_str));
+      g_free(parent_str);
+    }
+    else
+      ui->location->clear();
+
+    ui->lastModified->setText(QString::fromUtf8(fm_file_info_get_disp_mtime(fileInfo)));
+
+    // FIXME: need to encapsulate this in an libfm API.
+    time_t atime;
+    struct tm tm;
+    atime = fm_file_info_get_atime(fileInfo);
+    localtime_r(&atime, &tm);
+    char buf[128];
+    strftime(buf, sizeof(buf), "%x %R", &tm);
+    ui->lastAccessed->setText(QString::fromUtf8(buf));
+  }
+  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);
+  g_signal_connect(deepCountJob, "finished", G_CALLBACK(onDeepCountJobFinished), this);
+  fm_job_run_async(FM_JOB(deepCountJob));
+}
+
+/*static */ void FilePropsDialog::onDeepCountJobFinished(FmDeepCountJob* job, FilePropsDialog* pThis) {
+
+  pThis->onFileSizeTimerTimeout(); // update file size display
+
+  // free the job
+  g_object_unref(pThis->deepCountJob);
+  pThis->deepCountJob = NULL;
+
+  // stop the timer
+  if(pThis->fileSizeTimer) {
+    pThis->fileSizeTimer->stop();
+    delete pThis->fileSizeTimer;
+    pThis->fileSizeTimer = NULL;
+  }
+}
+
+void FilePropsDialog::onFileSizeTimerTimeout() {
+  if(deepCountJob && !fm_job_is_cancelled(FM_JOB(deepCountJob))) {
+    char size_str[128];
+    fm_file_size_to_str(size_str, sizeof(size_str), deepCountJob->total_size,
+                        fm_config->si_unit);
+    // 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 = QString::fromUtf8(size_str) %
+      QString(" (%1 B)").arg(deepCountJob->total_size);
+      // tr(" (%n) byte(s)", "", deepCountJob->total_size);
+    ui->fileSize->setText(str);
+
+    fm_file_size_to_str(size_str, sizeof(size_str), deepCountJob->total_ondisk_size,
+                        fm_config->si_unit);
+    str = QString::fromUtf8(size_str) %
+      QString(" (%1 B)").arg(deepCountJob->total_ondisk_size);
+      // tr(" (%n) byte(s)", "", deepCountJob->total_ondisk_size);
+    ui->onDiskSize->setText(str);
+  }
+}
+
+void FilePropsDialog::accept() {
+
+  // applications
+  if(mimeType && ui->openWith->isChanged()) {
+    GAppInfo* currentApp = ui->openWith->selectedApp();
+    g_app_info_set_as_default_for_type(currentApp, fm_mime_type_get_type(mimeType), NULL);
+  }
+
+  // check if chown or chmod is needed
+  guint32 newUid = uidFromName(ui->owner->text());
+  guint32 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) {
+    FmPathList* paths = fm_path_list_new_from_file_info_list(fileInfos_);
+    FileOperation* op = new FileOperation(FileOperation::ChangeAttr, paths);
+    fm_path_list_unref(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(QString::fromUtf8(fm_file_info_get_disp_name(fileInfo)) != new_name) {
+      FmPath* path = fm_file_info_get_path(fileInfo);
+      GFile* gf = fm_path_to_gfile(path);
+      GFile* parent_gf = g_file_get_parent(gf);
+      GFile* dest = g_file_get_child(G_FILE(parent_gf), new_name.toLocal8Bit().constData());
+      g_object_unref(parent_gf);
+      GError* err = NULL;
+      if(!g_file_move(gf, dest,
+                      GFileCopyFlags(G_FILE_COPY_ALL_METADATA |
+                                     G_FILE_COPY_NO_FALLBACK_FOR_MOVE |
+                                     G_FILE_COPY_NOFOLLOW_SYMLINKS),
+                      NULL, NULL, NULL, &err)) {
+        QMessageBox::critical(this, QObject::tr("Error"), err->message);
+        g_error_free(err);
+      }
+      g_object_unref(dest);
+      g_object_unref(gf);
+    }
+  }
+
+  QDialog::accept();
+}
+
+void FilePropsDialog::initOwner() {
+  if(allNative) {
+    if(uid != DIFFERENT_UIDS)
+      ui->owner->setText(uidToName(uid));
+    if(gid != DIFFERENT_GIDS)
+      ui->ownerGroup->setText(gidToName(gid));
+
+    if(geteuid() != 0) { // on local filesystems, only root can do chown.
+      ui->owner->setEnabled(false);
+      ui->ownerGroup->setEnabled(false);
+    }
+  }
+}
+
+
+} // namespace Fm
diff --git a/src/filepropsdialog.h b/src/filepropsdialog.h
new file mode 100644 (file)
index 0000000..257f926
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * 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>
+
+namespace Ui {
+  class FilePropsDialog;
+};
+
+namespace Fm {
+
+class LIBFM_QT_API FilePropsDialog : public QDialog {
+Q_OBJECT
+
+public:
+  explicit FilePropsDialog(FmFileInfoList* files, QWidget* parent = 0, Qt::WindowFlags f = 0);
+  virtual ~FilePropsDialog();
+
+  virtual void accept();
+
+  static FilePropsDialog* showForFile(FmFileInfo* file, QWidget* parent = 0) {
+    FmFileInfoList* files = fm_file_info_list_new();
+    fm_file_info_list_push_tail(files, file);
+    FilePropsDialog* dlg = showForFiles(files, parent);
+    fm_file_info_list_unref(files);
+    return dlg;
+  }
+
+  static FilePropsDialog* showForFiles(FmFileInfoList* files, QWidget* parent = 0) {
+    FilePropsDialog* dlg = new FilePropsDialog(files, parent);
+    dlg->show();
+    return dlg;
+  }
+
+private:
+  void initGeneralPage();
+  void initApplications();
+  void initPermissionsPage();
+  void initOwner();
+
+  static void onDeepCountJobFinished(FmDeepCountJob* job, FilePropsDialog* pThis);
+
+private Q_SLOTS:
+  void onFileSizeTimerTimeout();
+
+private:
+  Ui::FilePropsDialog* ui;
+  FmFileInfoList* fileInfos_; // list of all file infos
+  FmFileInfo* 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)
+
+  FmMimeType* 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;
+
+  FmDeepCountJob* deepCountJob; // job used to count total size
+  QTimer* fileSizeTimer;
+};
+
+}
+
+#endif // FM_FILEPROPSDIALOG_H
diff --git a/src/filesearch.ui b/src/filesearch.ui
new file mode 100644 (file)
index 0000000..2919edb
--- /dev/null
@@ -0,0 +1,568 @@
+<?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>
+           </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>&amp;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>&amp;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&amp;ve</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QCheckBox" name="contentRegExp">
+            <property name="text">
+             <string>&amp;Use regular expression</string>
+            </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>
diff --git a/src/filesearchdialog.cpp b/src/filesearchdialog.cpp
new file mode 100644 (file)
index 0000000..512aca2
--- /dev/null
@@ -0,0 +1,145 @@
+/*
+ * 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_ = std::move(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;
+  }
+}
+
+}
diff --git a/src/filesearchdialog.h b/src/filesearchdialog.h
new file mode 100644 (file)
index 0000000..dc6dc08
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * 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:
+  FileSearchDialog(QStringList paths = QStringList(), QWidget * parent = 0, Qt::WindowFlags f = 0);
+  ~FileSearchDialog();
+
+  Path searchUri() const {
+    return searchUri_;
+  }
+
+  virtual void accept();
+
+private Q_SLOTS:
+  void onAddPath();
+  void onRemovePath();
+
+private:
+  Ui::SearchDialog* ui;
+  Path searchUri_;
+};
+
+}
+
+#endif // FM_FILESEARCHDIALOG_H
diff --git a/src/fm-search.c b/src/fm-search.c
new file mode 100644 (file)
index 0000000..4298848
--- /dev/null
@@ -0,0 +1,317 @@
+/*
+ * 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;
+}
diff --git a/src/fm-search.h b/src/fm-search.h
new file mode 100644 (file)
index 0000000..c99f71d
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * 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_ */
diff --git a/src/folder.h b/src/folder.h
new file mode 100644 (file)
index 0000000..355b6d7
--- /dev/null
@@ -0,0 +1,228 @@
+/*
+ * 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_H__
+#define __LIBFM_QT_FM_FOLDER_H__
+
+#include <libfm/fm.h>
+#include <QObject>
+#include <QtGlobal>
+#include "libfmqtglobals.h"
+
+
+namespace Fm {
+
+
+class LIBFM_QT_API Folder {
+public:
+
+
+  // default constructor
+  Folder() {
+    dataPtr_ = nullptr;
+  }
+
+
+  Folder(FmFolder* dataPtr){
+    dataPtr_ = dataPtr != nullptr ? reinterpret_cast<GObject*>(g_object_ref(dataPtr)) : nullptr;
+  }
+
+
+  // copy constructor
+  Folder(const Folder& other) {
+    dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<GObject*>(g_object_ref(other.dataPtr_)) : nullptr;
+  }
+
+
+  // move constructor
+  Folder(Folder&& other) {
+    dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
+  }
+
+
+  // destructor
+  virtual ~Folder() {
+    if(dataPtr_ != nullptr) {
+      g_object_unref(dataPtr_);
+    }
+  }
+
+
+  // create a wrapper for the data pointer without increasing the reference count
+  static Folder wrapPtr(FmFolder* dataPtr) {
+    Folder obj;
+    obj.dataPtr_ = reinterpret_cast<GObject*>(dataPtr);
+    return obj;
+  }
+
+  // disown the managed data pointer
+  FmFolder* takeDataPtr() {
+    FmFolder* data = reinterpret_cast<FmFolder*>(dataPtr_);
+    dataPtr_ = nullptr;
+    return data;
+  }
+
+  // get the raw pointer wrapped
+  FmFolder* dataPtr() {
+    return reinterpret_cast<FmFolder*>(dataPtr_);
+  }
+
+  // automatic type casting
+  operator FmFolder*() {
+    return dataPtr();
+  }
+
+  // automatic type casting
+  operator void*() {
+    return dataPtr();
+  }
+
+
+  // copy assignment
+  Folder& operator=(const Folder& 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
+  Folder& operator=(Folder&& other) {
+    dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
+    return *this;
+  }
+
+  bool isNull() {
+    return (dataPtr_ == nullptr);
+  }
+
+  // methods
+
+  bool makeDirectory(const char* name, GError** error) {
+    return fm_folder_make_directory(dataPtr(), name, error);
+  }
+
+
+  void queryFilesystemInfo(void) {
+    fm_folder_query_filesystem_info(dataPtr());
+  }
+
+
+  bool getFilesystemInfo(guint64* total_size, guint64* free_size) {
+    return fm_folder_get_filesystem_info(dataPtr(), total_size, free_size);
+  }
+
+
+  void reload(void) {
+    fm_folder_reload(dataPtr());
+  }
+
+
+  bool isIncremental(void) {
+    return fm_folder_is_incremental(dataPtr());
+  }
+
+
+  bool isValid(void) {
+    return fm_folder_is_valid(dataPtr());
+  }
+
+
+  bool isLoaded(void) {
+    return fm_folder_is_loaded(dataPtr());
+  }
+
+
+  FmFileInfo* getFileByName(const char* name) {
+    return fm_folder_get_file_by_name(dataPtr(), name);
+  }
+
+
+  bool isEmpty(void) {
+    return fm_folder_is_empty(dataPtr());
+  }
+
+
+  FmFileInfoList* getFiles(void) {
+    return fm_folder_get_files(dataPtr());
+  }
+
+
+  FmPath* getPath(void) {
+    return fm_folder_get_path(dataPtr());
+  }
+
+
+  FmFileInfo* getInfo(void) {
+    return fm_folder_get_info(dataPtr());
+  }
+
+
+  void unblockUpdates(void) {
+    fm_folder_unblock_updates(dataPtr());
+  }
+
+
+  void blockUpdates(void) {
+    fm_folder_block_updates(dataPtr());
+  }
+
+
+  static Folder findByPath(FmPath* path) {
+    return Folder::wrapPtr(fm_folder_find_by_path(path));
+  }
+
+
+  static Folder fromUri(const char* uri) {
+    return Folder::wrapPtr(fm_folder_from_uri(uri));
+  }
+
+
+  static Folder fromPathName(const char* path) {
+    return Folder::wrapPtr(fm_folder_from_path_name(path));
+  }
+
+
+  static Folder fromGfile(GFile* gf) {
+    return Folder::wrapPtr(fm_folder_from_gfile(gf));
+  }
+
+
+  static Folder fromPath(FmPath* path) {
+    return Folder::wrapPtr(fm_folder_from_path(path));
+  }
+
+
+  // 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_FOLDER_H__
diff --git a/src/folderconfig.h b/src/folderconfig.h
new file mode 100644 (file)
index 0000000..61ebb40
--- /dev/null
@@ -0,0 +1,198 @@
+/*
+ * 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"
+
+
+namespace Fm {
+
+
+class LIBFM_QT_API FolderConfig {
+public:
+
+
+  FolderConfig(FmPath* path) {
+    dataPtr_ = reinterpret_cast<FmFolderConfig*>(fm_folder_config_open(path));
+  }
+
+
+  // default constructor
+  FolderConfig() {
+    dataPtr_ = nullptr;
+  }
+
+
+  // move constructor
+  FolderConfig(FolderConfig&& other) {
+    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) {
+    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__
diff --git a/src/folderitemdelegate.cpp b/src/folderitemdelegate.cpp
new file mode 100644 (file)
index 0000000..be8c01b
--- /dev/null
@@ -0,0 +1,254 @@
+/*
+ * 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 <QStyleOptionViewItem>
+#include <QApplication>
+#include <QIcon>
+#include <QTextLayout>
+#include <QTextOption>
+#include <QTextLine>
+#include <QDebug>
+
+namespace Fm {
+
+FolderItemDelegate::FolderItemDelegate(QAbstractItemView* view, QObject* parent):
+  QStyledItemDelegate(parent ? parent : view),
+  view_(view),
+  symlinkIcon_(QIcon::fromTheme("emblem-symbolic-link")),
+  fileInfoRole_(Fm::FolderModel::FileInfoRole),
+  fmIconRole_(-1) {
+}
+
+FolderItemDelegate::~FolderItemDelegate() {
+
+}
+
+QSize FolderItemDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const {
+  QVariant value = index.data(Qt::SizeHintRole);
+  if(value.isValid())
+    return qvariant_cast<QSize>(value);
+  if(option.decorationPosition == QStyleOptionViewItem::Top ||
+    option.decorationPosition == QStyleOptionViewItem::Bottom) {
+
+    QStyleOptionViewItem opt = option;
+    initStyleOption(&opt, index);
+    opt.decorationAlignment = Qt::AlignHCenter|Qt::AlignTop;
+    opt.displayAlignment = Qt::AlignTop|Qt::AlignHCenter;
+
+    // "opt.decorationSize" may be smaller than the requested size because
+    // "QStyledItemDelegate::initStyleOption()" uses "QIcon::actualSize()" to set it
+    // (see Qt -> qstyleditemdelegate.cpp). So, we always get decorationSize from "option".
+    Q_ASSERT(gridSize_ != QSize());
+    QRectF textRect(0, 0, gridSize_.width(), gridSize_.height() - option.decorationSize.height());
+    drawText(nullptr, opt, textRect); // passing NULL for painter will calculate the bounding rect only.
+    int width = qMax((int)textRect.width(), option.decorationSize.width());
+    int height = option.decorationSize.height() + textRect.height();
+    return QSize(width, height);
+  }
+  return QStyledItemDelegate::sizeHint(option, index);
+}
+
+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;
+}
+
+// special thanks to Razor-qt developer Alec Moskvin(amoskvin) for providing the fix!
+void FolderItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const {
+  Q_ASSERT(index.isValid());
+  FmFileInfo* file = static_cast<FmFileInfo*>(index.data(fileInfoRole_).value<void*>());
+  FmIcon* fmicon = static_cast<FmIcon*>(index.data(fmIconRole_).value<void*>());
+  if(fmicon == nullptr && file != nullptr) {
+    fmicon = fm_file_info_get_icon(file);
+  }
+  QList<Icon> emblems = fmicon != nullptr ? IconTheme::emblems(fmicon) : QList<Icon>();
+  bool isSymlink = file && fm_file_info_is_symlink(file);
+  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());
+    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));
+    painter->drawPixmap(iconPos + QPoint(margin.width(), margin.height()), pixmap);
+
+    // draw some emblems for the item if needed
+    if(isSymlink)
+      painter->drawPixmap(iconPos, symlinkIcon_.pixmap(option.decorationSize / 2, iconMode));
+    if(!emblems.isEmpty()) {
+      QPoint emblemPos(opt.rect.x() + opt.rect.width() / 2, opt.rect.y() + option.decorationSize.height() / 2);
+      QIcon emblem = IconTheme::icon(emblems.first().dataPtr());
+      painter->drawPixmap(emblemPos, emblem.pixmap(option.decorationSize / 2, iconMode));
+    }
+
+    // draw the text
+    // The text rect dimensions should be exactly as they were in sizeHint()
+    QRectF textRect(opt.rect.x() - (gridSize_.width() - opt.rect.width()) / 2,
+                    opt.rect.y() + option.decorationSize.height(),
+                    gridSize_.width(),
+                    gridSize_.height() - option.decorationSize.height());
+    drawText(painter, opt, textRect);
+    painter->restore();
+  }
+  else {
+    // let QStyledItemDelegate does its default painting
+    QStyledItemDelegate::paint(painter, option, index);
+
+    // draw emblems if needed
+    if(isSymlink || !emblems.isEmpty()) {
+      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 {
+        QPoint iconPos(opt.rect.x() + option.decorationSize.width() / 2, opt.rect.y() + opt.rect.height() / 2);
+        QIcon emblem = IconTheme::icon(emblems.first().dataPtr());
+        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);
+  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 ? QPalette::Normal : 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 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);
+    }
+  }
+}
+
+
+} // namespace Fm
diff --git a/src/folderitemdelegate.h b/src/folderitemdelegate.h
new file mode 100644 (file)
index 0000000..9bf06a7
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * 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>
+#include <QAbstractItemView>
+
+namespace Fm {
+
+class LIBFM_QT_API FolderItemDelegate : public QStyledItemDelegate {
+  Q_OBJECT
+public:
+  explicit FolderItemDelegate(QAbstractItemView* view, QObject* parent = nullptr);
+  virtual ~FolderItemDelegate();
+
+  inline void setGridSize(QSize size) {
+    gridSize_ = size;
+  }
+
+  inline QSize gridSize() const {
+    return gridSize_;
+  }
+
+  int fileInfoRole() {
+    return fileInfoRole_;
+  }
+
+  void setFileInfoRole(int role) {
+    fileInfoRole_ = role;
+  }
+
+  int fmIconRole() {
+    return fmIconRole_;
+  }
+
+  void setFmIconRole(int role) {
+    fmIconRole_ = role;
+  }
+
+  virtual QSize sizeHint(const QStyleOptionViewItem & option, const QModelIndex & index) const;
+  virtual void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const;
+
+private:
+  void drawText(QPainter* painter, QStyleOptionViewItem& opt, QRectF& textRect) const;
+  static QIcon::Mode iconModeFromState(QStyle::State state);
+
+private:
+  QAbstractItemView* view_;
+  QIcon symlinkIcon_;
+  QSize gridSize_;
+  int fileInfoRole_;
+  int fmIconRole_;
+};
+
+}
+
+#endif // FM_FOLDERITEMDELEGATE_H
diff --git a/src/foldermenu.cpp b/src/foldermenu.cpp
new file mode 100644 (file)
index 0000000..a5fb5b7
--- /dev/null
@@ -0,0 +1,301 @@
+/*
+ * 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
+#ifdef CUSTOM_ACTIONS
+#include "customaction_p.h"
+#include <QMessageBox>
+#endif
+
+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);
+
+#ifdef CUSTOM_ACTIONS
+  FmFileInfo* folderInfo = view_->folderInfo();
+  if(folderInfo) {
+    GList *single_list = NULL;
+    single_list = g_list_prepend(single_list, (GList*)folderInfo);
+    GList* items = fm_get_actions_for_files(single_list);
+    if(items) {
+      GList* l;
+      for(l=items; l; l=l->next) {
+        FmFileActionItem* item = FM_FILE_ACTION_ITEM(l->data);
+        if(l == items && item
+           && !(fm_file_action_item_is_action(item)
+                && !(fm_file_action_item_get_target(item) & FM_FILE_ACTION_TARGET_CONTEXT))) {
+          addSeparator(); // before all custom actions
+        }
+        addCustomActionItem(this, item);
+      }
+    }
+    g_list_foreach(items, (GFunc)fm_file_action_item_unref, NULL);
+    g_list_free(items);
+  }
+#endif
+
+  separator4_ = addSeparator();
+
+  propertiesAction_ = new QAction(tr("Folder Pr&operties"), this);
+  addAction(propertiesAction_);
+  connect(propertiesAction_, &QAction::triggered, this, &FolderMenu::onPropertiesActionTriggered);
+}
+
+FolderMenu::~FolderMenu() {
+}
+
+#ifdef CUSTOM_ACTIONS
+void FolderMenu::addCustomActionItem(QMenu* menu, FmFileActionItem* item) {
+  if(!item) return;
+  if(fm_file_action_item_is_action(item) && !(fm_file_action_item_get_target(item) & FM_FILE_ACTION_TARGET_CONTEXT))
+    return;
+
+  CustomAction* action = new CustomAction(item, menu);
+  menu->addAction(action);
+  if(fm_file_action_item_is_menu(item)) {
+    GList* subitems = fm_file_action_item_get_sub_items(item);
+    if (subitems != NULL) {
+      QMenu* submenu = new QMenu(menu);
+      for(GList* l = subitems; l; l = l->next) {
+        FmFileActionItem* subitem = FM_FILE_ACTION_ITEM(l->data);
+        addCustomActionItem(submenu, subitem);
+      }
+      action->setMenu(submenu);
+    }
+  }
+  else if(fm_file_action_item_is_action(item)) {
+    connect(action, &QAction::triggered, this, &FolderMenu::onCustomActionTrigerred);
+  }
+}
+
+void FolderMenu::onCustomActionTrigerred() {
+  CustomAction* action = static_cast<CustomAction*>(sender());
+  FmFileActionItem* item = action->item();
+
+  FmFileInfo* folderInfo = view_->folderInfo();
+  if(folderInfo) {
+    GList *single_list = NULL;
+    single_list = g_list_prepend(single_list, (GList*)folderInfo);
+    char* output = NULL;
+    fm_file_action_item_launch(item, NULL, single_list, &output);
+    if(output) {
+      QMessageBox::information(this, tr("Output"), QString::fromUtf8(output));
+      g_free(output);
+    }
+  }
+}
+#endif
+
+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() {
+  FmPath* 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() {
+  FmFileInfo* folderInfo = view_->folderInfo();
+
+  if(folderInfo)
+    FilePropsDialog::showForFile(folderInfo);
+}
+
+} // namespace Fm
diff --git a/src/foldermenu.h b/src/foldermenu.h
new file mode 100644 (file)
index 0000000..8308153
--- /dev/null
@@ -0,0 +1,138 @@
+/*
+ * 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"
+#ifdef CUSTOM_ACTIONS
+#include <libfm/fm-actions.h>
+#endif
+
+class QAction;
+
+namespace Fm {
+
+class FolderView;
+
+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:
+#ifdef CUSTOM_ACTIONS
+  void addCustomActionItem(QMenu* menu, FmFileActionItem* item);
+#endif
+
+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();
+#ifdef CUSTOM_ACTIONS
+  void onCustomActionTrigerred();
+#endif
+
+private:
+  void createSortMenu();
+  void addSortMenuItem(QString title, int id);
+
+private:
+  FolderView* view_;
+  QAction* createAction_;
+  QAction* separator1_;
+  QAction* pasteAction_;
+  QAction* separator2_;
+  QAction* selectAllAction_;
+  QAction* invertSelectionAction_;
+  QAction* separator3_;
+  QAction* sortAction_;
+  QActionGroup* sortActionGroup_;
+  QMenu* sortMenu_;
+  QAction* sortActions_[FolderModel::NumOfColumns];
+  QAction* actionAscending_;
+  QAction* actionDescending_;
+  QAction* showHiddenAction_;
+  QAction* separator4_;
+  QAction* propertiesAction_;
+};
+
+}
+
+#endif // FM_FOLDERMENU_H
diff --git a/src/foldermodel.cpp b/src/foldermodel.cpp
new file mode 100644 (file)
index 0000000..8f6f603
--- /dev/null
@@ -0,0 +1,561 @@
+/*
+ * 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 <QtAlgorithms>
+#include <QVector>
+#include <qmimedata.h>
+#include <QMimeData>
+#include <QByteArray>
+#include <QPixmap>
+#include <QPainter>
+#include "utilities.h"
+#include "fileoperation.h"
+#include "thumbnailloader.h"
+
+namespace Fm {
+
+FolderModel::FolderModel() :
+  folder_(nullptr) {
+/*
+    ColumnIcon,
+    ColumnName,
+    ColumnFileType,
+    ColumnMTime,
+    NumOfColumns
+*/
+  thumbnailRefCounts.reserve(4);
+
+  // reload all icons when the icon theme is changed
+  connect(IconTheme::instance(), &IconTheme::changed, this, &FolderModel::updateIcons);
+}
+
+FolderModel::~FolderModel() {
+  qDebug("delete FolderModel");
+
+  if(folder_)
+    setFolder(nullptr);
+
+  // if the thumbnail requests list is not empty, cancel them
+  if(!thumbnailResults.empty()) {
+    Q_FOREACH(FmThumbnailLoader* res, thumbnailResults) {
+      ThumbnailLoader::cancel(res);
+    }
+  }
+}
+
+void FolderModel::setFolder(FmFolder* new_folder) {
+  if(folder_) {
+    removeAll();        // remove old items
+    g_signal_handlers_disconnect_by_func(folder_, gpointer(onStartLoading), this);
+    g_signal_handlers_disconnect_by_func(folder_, gpointer(onFinishLoading), this);
+    g_signal_handlers_disconnect_by_func(folder_, gpointer(onFilesAdded), this);
+    g_signal_handlers_disconnect_by_func(folder_, gpointer(onFilesChanged), this);
+    g_signal_handlers_disconnect_by_func(folder_, gpointer(onFilesRemoved), this);
+    g_object_unref(folder_);
+  }
+  if(new_folder) {
+    folder_ = FM_FOLDER(g_object_ref(new_folder));
+    g_signal_connect(folder_, "start-loading", G_CALLBACK(onStartLoading), this);
+    g_signal_connect(folder_, "finish-loading", G_CALLBACK(onFinishLoading), this);
+    g_signal_connect(folder_, "files-added", G_CALLBACK(onFilesAdded), this);
+    g_signal_connect(folder_, "files-changed", G_CALLBACK(onFilesChanged), this);
+    g_signal_connect(folder_, "files-removed", G_CALLBACK(onFilesRemoved), this);
+    // handle the case if the folder is already loaded
+    if(fm_folder_is_loaded(folder_))
+      insertFiles(0, fm_folder_get_files(folder_));
+  }
+  else
+    folder_ = nullptr;
+}
+
+void FolderModel::onStartLoading(FmFolder* folder, gpointer user_data) {
+  FolderModel* model = static_cast<FolderModel*>(user_data);
+  // remove all items
+  model->removeAll();
+}
+
+void FolderModel::onFinishLoading(FmFolder* folder, gpointer user_data) {
+  Q_UNUSED(folder)
+  Q_UNUSED(user_data)
+}
+
+void FolderModel::onFilesAdded(FmFolder* folder, GSList* files, gpointer user_data) {
+  FolderModel* model = static_cast<FolderModel*>(user_data);
+  int n_files = g_slist_length(files);
+  model->beginInsertRows(QModelIndex(), model->items.count(), model->items.count() + n_files - 1);
+  for(GSList* l = files; l; l = l->next) {
+    FmFileInfo* info = FM_FILE_INFO(l->data);
+    FolderModelItem item(info);
+/*
+    if(fm_file_info_is_hidden(info)) {
+      model->hiddenItems.append(item);
+      continue;
+    }
+*/
+    model->items.append(item);
+  }
+  model->endInsertRows();
+}
+
+//static
+void FolderModel::onFilesChanged(FmFolder* folder, GSList* files, gpointer user_data) {
+  FolderModel* model = static_cast<FolderModel*>(user_data);
+  for(GSList* l = files; l; l = l->next) {
+    FmFileInfo* info = FM_FILE_INFO(l->data);
+    int row;
+    QList<FolderModelItem>::iterator it = model->findItemByFileInfo(info, &row);
+    if(it != model->items.end()) {
+      FolderModelItem& item = *it;
+      // try to update the item
+      item.displayName = QString::fromUtf8(fm_file_info_get_disp_name(info));
+      item.updateIcon();
+      item.thumbnails.clear();
+      QModelIndex index = model->createIndex(row, 0, &item);
+      Q_EMIT model->dataChanged(index, index);
+    }
+  }
+}
+
+//static
+void FolderModel::onFilesRemoved(FmFolder* folder, GSList* files, gpointer user_data) {
+  FolderModel* model = static_cast<FolderModel*>(user_data);
+  for(GSList* l = files; l; l = l->next) {
+    FmFileInfo* info = FM_FILE_INFO(l->data);
+    const char* name = fm_file_info_get_name(info);
+    int row;
+    QList<FolderModelItem>::iterator it = model->findItemByName(name, &row);
+    if(it != model->items.end()) {
+      model->beginRemoveRows(QModelIndex(), row, row);
+      model->items.erase(it);
+      model->endRemoveRows();
+    }
+  }
+}
+
+void FolderModel::insertFiles(int row, FmFileInfoList* files) {
+  int n_files = fm_file_info_list_get_length(files);
+  beginInsertRows(QModelIndex(), row, row + n_files - 1);
+  for(GList* l = fm_file_info_list_peek_head_link(files); l; l = l->next) {
+    FolderModelItem item(FM_FILE_INFO(l->data));
+    items.append(item);
+  }
+  endInsertRows();
+}
+
+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());
+}
+
+FmFileInfo* 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);
+  FmFileInfo* info = item->info;
+
+  switch(role) {
+    case Qt::ToolTipRole:
+      return QVariant(item->displayName);
+    case Qt::DisplayRole:  {
+      switch(index.column()) {
+        case ColumnFileName: {
+          return QVariant(item->displayName);
+        }
+        case ColumnFileType: {
+          FmMimeType* mime = fm_file_info_get_mime_type(info);
+          const char* desc = fm_mime_type_get_desc(mime);
+          return QString::fromUtf8(desc);
+        }
+        case ColumnFileMTime: {
+          const char* name = fm_file_info_get_disp_mtime(info);
+          return QString::fromUtf8(name);
+        }
+        case ColumnFileSize: {
+          const char* name = fm_file_info_get_disp_size(info);
+          return QString::fromUtf8(name);
+        }
+        case ColumnFileOwner: {
+          const char* name = fm_file_info_get_disp_owner(info);
+          return QString::fromUtf8(name);
+        }
+      }
+    }
+    case Qt::DecorationRole: {
+      if(index.column() == 0) {
+        // QPixmap pix = IconTheme::loadIcon(fm_file_info_get_icon(info), iconSize_);
+        return QVariant(item->icon);
+        // return QVariant(pix);
+      }
+      break;
+    }
+    case FileInfoRole:
+      return qVariantFromValue((void*)info);
+  }
+  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);
+  }
+  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(FmPath* path, int* row) {
+  QList<FolderModelItem>::iterator it = items.begin();
+  int i = 0;
+  while(it != items.end()) {
+    FolderModelItem& item = *it;
+    FmPath* item_path = fm_file_info_get_path(item.info);
+    if(fm_path_equal(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;
+    const char* item_name = fm_file_info_get_name(item.info);
+    if(strcmp(name, item_name) == 0) {
+      *row = i;
+      return it;
+    }
+    ++it;
+    ++i;
+  }
+  return items.end();
+}
+
+QList< FolderModelItem >::iterator FolderModel::findItemByFileInfo(FmFileInfo* info, int* row) {
+  QList<FolderModelItem>::iterator it = items.begin();
+  int i = 0;
+  while(it != items.end()) {
+    FolderModelItem& item = *it;
+    if(item.info == 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) {
+      FmPath* path = fm_file_info_get_path(item->info);
+      if(path) {
+        char* uri = fm_path_to_uri(path);
+        urilist.append(uri);
+        urilist.append('\n');
+        g_free(uri);
+      }
+    }
+  }
+  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_)
+    return false;
+  FmPath* destPath;
+  if(parent.isValid()) { // drop on an item
+    FmFileInfo* info;
+    if(row == -1 && column == -1)
+      info = fileInfoFromIndex(parent);
+    else {
+      QModelIndex itemIndex = parent.child(row, column);
+      info = fileInfoFromIndex(itemIndex);
+    }
+    if(info)
+      destPath = fm_file_info_get_path(info);
+    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);
+    FmPathList* 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:
+        fm_path_list_unref(srcPaths);
+        return false;
+    }
+    fm_path_list_unref(srcPaths);
+    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) {
+  QVector<QPair<int, int>>::iterator it = thumbnailRefCounts.begin();
+  while (it != thumbnailRefCounts.end()) {
+    if (it->first == size) {
+      ++it->second;
+      return;
+    } else ++it;
+  }
+  thumbnailRefCounts.append(QPair<int, int>(size, 1));
+}
+
+// ask the model to free cached thumbnails of the specified size
+void FolderModel::releaseThumbnails(int size) {
+  QVector<QPair<int, int> >::iterator it;
+  for(it = thumbnailRefCounts.begin(); it != thumbnailRefCounts.end(); ++it) {
+    if(it->first == size) {
+      break;
+    }
+  }
+  if(it != thumbnailRefCounts.end()) {
+    --it->second;
+    if(it->second == 0) {
+      thumbnailRefCounts.erase(it);
+
+      // remove thumbnails that ara queued for loading from thumbnailResults
+      QLinkedList<FmThumbnailLoader*>::iterator it;
+      for(it = thumbnailResults.begin(); it != thumbnailResults.end();) {
+        QLinkedList<FmThumbnailLoader*>::iterator next = it + 1;
+        FmThumbnailLoader* res = *it;
+        if(ThumbnailLoader::size(res) == size) {
+          ThumbnailLoader::cancel(res);
+          thumbnailResults.erase(it);
+        }
+        it = next;
+      }
+
+      // 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);
+      }
+    }
+  }
+}
+
+void FolderModel::onThumbnailLoaded(FmThumbnailLoader* res, gpointer user_data) {
+  FolderModel* pThis = reinterpret_cast<FolderModel*>(user_data);
+  QLinkedList<FmThumbnailLoader*>::iterator it;
+  for(it = pThis->thumbnailResults.begin(); it != pThis->thumbnailResults.end(); ++it) {
+    if(*it == res) { // the thumbnail result is in our list
+      pThis->thumbnailResults.erase(it); // remove it from the list
+      FmFileInfo* info = ThumbnailLoader::fileInfo(res);
+      int row = -1;
+      // find the model item this thumbnail belongs to
+      QList<FolderModelItem>::iterator it = pThis->findItemByFileInfo(info, &row);
+      if(it != pThis->items.end()) {
+        // the file is found in our model
+        FolderModelItem& item = *it;
+        QModelIndex index = pThis->createIndex(row, 0, (void*)&item);
+        // store the image in the folder model item.
+        int size = ThumbnailLoader::size(res);
+        QImage image = ThumbnailLoader::image(res);
+        FolderModelItem::Thumbnail* thumbnail = item.findThumbnail(size);
+        thumbnail->image = image;
+        // qDebug("thumbnail loaded for: %s, size: %d", item.displayName.toUtf8().constData(), size);
+        if(image.isNull())
+          thumbnail->status = FolderModelItem::ThumbnailFailed;
+        else {
+          thumbnail->status = FolderModelItem::ThumbnailLoaded;
+          // FIXME: due to bugs in Qt's QStyledItemDelegate, if the image width and height
+          // are not the same, painting errors will happen. It's quite unfortunate.
+          // Let's do some padding to make its width and height equals.
+          // This greatly decrease performance :-(
+          // Later if we can re-implement our own item delegate, this can be avoided.
+          QPixmap pixmap = QPixmap(size, size);
+          pixmap.fill(QColor(0, 0, 0, 0)); // fill the pixmap with transparent color (alpha:0)
+          QPainter painter(&pixmap);
+          int x = (size - image.width()) / 2;
+          int y = (size - image.height()) / 2;
+          painter.drawImage(QPoint(x, y), image); // draw the image to the pixmap at center.
+          // FIXME: should we cache QPixmap instead for performance reason?
+          thumbnail->image = pixmap.toImage(); // convert it back to image
+
+          // tell the world that we have the thumbnail loaded
+          Q_EMIT pThis->thumbnailLoaded(index, size);
+        }
+      }
+      break;
+    }
+  }
+}
+
+// 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);
+    // qDebug("FolderModel::thumbnailFromIndex: %d, %s", thumbnail->status, item->displayName.toUtf8().constData());
+    switch(thumbnail->status) {
+      case FolderModelItem::ThumbnailNotChecked: {
+        // load the thumbnail
+        FmThumbnailLoader* res = ThumbnailLoader::load(item->info, size, onThumbnailLoaded, this);
+        thumbnailResults.push_back(res);
+        thumbnail->status = FolderModelItem::ThumbnailLoading;
+        break;
+      }
+      case FolderModelItem::ThumbnailLoaded:
+        return thumbnail->image;
+      default:;
+    }
+  }
+  return QImage();
+}
+
+void FolderModel::updateIcons() {
+  QList<FolderModelItem>::iterator it = items.begin();
+  for(;it != items.end(); ++it) {
+    (*it).updateIcon();
+  }
+}
+
+
+} // namespace Fm
diff --git a/src/foldermodel.h b/src/foldermodel.h
new file mode 100644 (file)
index 0000000..4ae0edb
--- /dev/null
@@ -0,0 +1,121 @@
+/*
+ * 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 <QIcon>
+#include <QImage>
+#include <libfm/fm.h>
+#include <QList>
+#include <QVector>
+#include <QLinkedList>
+#include <QPair>
+#include "foldermodelitem.h"
+
+namespace Fm {
+
+class LIBFM_QT_API FolderModel : public QAbstractListModel {
+Q_OBJECT
+public:
+
+  enum Role {
+    FileInfoRole = Qt::UserRole
+  };
+
+  enum ColumnId {
+    ColumnFileName,
+    ColumnFileType,
+    ColumnFileSize,
+    ColumnFileMTime,
+    ColumnFileOwner,
+    NumOfColumns
+  };
+
+public:
+  FolderModel();
+  virtual ~FolderModel();
+
+  FmFolder* folder() {
+    return folder_;
+  }
+  void setFolder(FmFolder* new_folder);
+
+  FmPath* path() {
+    return folder_ ? fm_folder_get_path(folder_) : NULL;
+  }
+
+  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);
+
+  FmFileInfo* 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);
+
+Q_SIGNALS:
+  void thumbnailLoaded(const QModelIndex& index, int size);
+
+public Q_SLOTS:
+  void updateIcons();
+
+protected:
+  static void onStartLoading(FmFolder* folder, gpointer user_data);
+  static void onFinishLoading(FmFolder* folder, gpointer user_data);
+  static void onFilesAdded(FmFolder* folder, GSList* files, gpointer user_data);
+  static void onFilesChanged(FmFolder* folder, GSList* files, gpointer user_data);
+  static void onFilesRemoved(FmFolder* folder, GSList* files, gpointer user_data);
+  static void onThumbnailLoaded(FmThumbnailLoader *res, gpointer user_data);
+
+  void insertFiles(int row, FmFileInfoList* files);
+  void removeAll();
+  QList<FolderModelItem>::iterator findItemByPath(FmPath* path, int* row);
+  QList<FolderModelItem>::iterator findItemByName(const char* name, int* row);
+  QList<FolderModelItem>::iterator findItemByFileInfo(FmFileInfo* info, int* row);
+
+private:
+  FmFolder* folder_;
+  // FIXME: should we use a hash table here so item lookup becomes much faster?
+  QList<FolderModelItem> items;
+
+  // record what size of thumbnails we should cache in an array of <size, refCount> pairs.
+  QVector<QPair<int, int> > thumbnailRefCounts;
+  QLinkedList<FmThumbnailLoader*> thumbnailResults;
+};
+
+}
+
+#endif // FM_FOLDERMODEL_H
diff --git a/src/foldermodelitem.cpp b/src/foldermodelitem.cpp
new file mode 100644 (file)
index 0000000..ef1a24b
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * 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"
+
+namespace Fm {
+
+FolderModelItem::FolderModelItem(FmFileInfo* _info):
+  info(fm_file_info_ref(_info)) {
+  displayName = QString::fromUtf8(fm_file_info_get_disp_name(info));
+  icon = IconTheme::icon(fm_file_info_get_icon(_info));
+  thumbnails.reserve(2);
+}
+
+FolderModelItem::FolderModelItem(const FolderModelItem& other) {
+  info = other.info ? fm_file_info_ref(other.info) : NULL;
+  displayName = QString::fromUtf8(fm_file_info_get_disp_name(info));
+  icon = other.icon;
+  thumbnails = other.thumbnails;
+}
+
+FolderModelItem::~FolderModelItem() {
+  if(info)
+    fm_file_info_unref(info);
+}
+
+// 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) {
+  QVector<Thumbnail>::iterator it;
+  for(it = thumbnails.begin(); it != thumbnails.end(); ++it) {
+    if(it->size == size) { // an image of the same size is found
+      return it;
+    }
+  }
+  if(it == thumbnails.end()) {
+    Thumbnail thumbnail;
+    thumbnail.status = ThumbnailNotChecked;
+    thumbnail.size = size;
+    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;
+    }
+  }
+}
+
+#if 0
+// cache the thumbnail of the specified size in the folder item
+void FolderModelItem::setThumbnail(int size, QImage image) {
+  QVector<Thumbnail>::iterator it;
+  for(it = thumbnails.begin(); it != thumbnails.end(); ++it) {
+    if(it->size == size) { // an image of the same size already exists
+      it->image = image; // replace it
+      it->status = ThumbnailLoaded;
+      break;
+    }
+  }
+  if(it == thumbnails.end()) { // the image is not found
+    Thumbnail thumbnail;
+    thumbnail.size = size;
+    thumbnail.status = ThumbnailLoaded;
+    thumbnail.image = image;
+    thumbnails.append(thumbnail); // add a new entry
+  }
+}
+#endif
+
+
+} // namespace Fm
diff --git a/src/foldermodelitem.h b/src/foldermodelitem.h
new file mode 100644 (file)
index 0000000..7274057
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * 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"
+
+namespace Fm {
+
+class LIBFM_QT_API FolderModelItem {
+public:
+
+  enum ThumbnailStatus {
+    ThumbnailNotChecked,
+    ThumbnailLoading,
+    ThumbnailLoaded,
+    ThumbnailFailed
+  };
+
+  struct Thumbnail {
+    int size;
+    ThumbnailStatus status;
+    QImage image;
+  };
+
+public:
+  FolderModelItem(FmFileInfo* _info);
+  FolderModelItem(const FolderModelItem& other);
+  virtual ~FolderModelItem();
+
+  Thumbnail* findThumbnail(int size);
+  // void setThumbnail(int size, QImage image);
+  void removeThumbnail(int size);
+
+  void updateIcon() {
+    icon = IconTheme::icon(fm_file_info_get_icon(info));
+  }
+
+  QString displayName;
+  QIcon icon;
+  FmFileInfo* info;
+  QVector<Thumbnail> thumbnails;
+};
+
+}
+
+#endif // FM_FOLDERMODELITEM_H
diff --git a/src/folderview.cpp b/src/folderview.cpp
new file mode 100644 (file)
index 0000000..7f1f642
--- /dev/null
@@ -0,0 +1,1039 @@
+/*
+ * 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 <QTimer>
+#include <QDate>
+#include <QDebug>
+#include <QMimeData>
+#include <QHoverEvent>
+#include <QApplication>
+#include <QScrollBar>
+#include <QMetaType>
+#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"
+
+Q_DECLARE_OPAQUE_POINTER(FmFileInfo*)
+
+using namespace Fm;
+
+FolderViewListView::FolderViewListView(QWidget* parent):
+  QListView(parent),
+  activationAllowed_(true) {
+  connect(this, &QListView::activated, this, &FolderViewListView::activation);
+}
+
+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);
+}
+
+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()) {
+    // FIXME: this hack only improves the usability partially. We still need more precise sizeHint handling.
+    // FolderItemDelegate* delegate = static_cast<FolderItemDelegate*>(itemDelegateForColumn(FolderModel::ColumnFileName));
+    // Q_ASSERT(delegate != nullptr);
+    // We use the grid size - (2, 2) as the size of the bounding rectangle of the whole item.
+    // The width of the text label hence is gridSize.width - 2, and the width and height of the icon is from iconSize().
+    QRect visRect = visualRect(index); // visibal area on the screen
+    QSize itemSize = gridSize();
+    itemSize.setWidth(itemSize.width() - 2);
+    itemSize.setHeight(itemSize.height() - 2);
+    QSize _iconSize = iconSize();
+    int textHeight = itemSize.height() - _iconSize.height();
+    if(point.y() < visRect.bottom() - textHeight) {
+      // the point is in the icon area, not over the text label
+      int iconXMargin = (itemSize.width() - _iconSize.width()) / 2;
+      if(point.x() < (visRect.left() + iconXMargin) || point.x() > (visRect.right() - iconXMargin))
+       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;
+}
+
+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);
+
+  connect(this, &QTreeView::activated, this, &FolderViewTreeView::activation);
+}
+
+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::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();
+  int desiredWidth = 0;
+
+  // get the width that every column want
+  int numCols = headerView->count();
+  if(numCols > 0) {
+    int* widths = new int[numCols]; // array to store the widths every column needs
+    int column;
+    for(column = 0; column < numCols; ++column) {
+      int columnId = headerView->logicalIndex(column);
+      // get the size that the column needs
+      widths[column] = sizeHintForColumn(columnId);
+      // 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(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) {
+  QTreeView::dataChanged(topLeft, bottomRight);
+  // 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(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);
+}
+
+FolderView::~FolderView() {
+}
+
+void FolderView::onItemActivated(QModelIndex index) {
+  if(index.isValid() && index.model()) {
+    QVariant data = index.model()->data(index, FolderModel::FileInfoRole);
+    FmFileInfo* info = (FmFileInfo*)data.value<void*>();
+    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;
+
+  QItemSelectionModel* selModel = selectionModel();
+  int nSel = 0;
+  if(viewMode() == DetailedListMode)
+    nSel = selModel->selectedRows().count();
+  else {
+    nSel = selModel->selectedIndexes().count();
+  }
+  // qDebug()<<"selected:" << nSel;
+  Q_EMIT selChanged(nSel); // FIXME: this is inefficient
+}
+
+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::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];
+
+  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
+    FolderItemDelegate* 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
+    FolderItemDelegate* 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);
+
+    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
+      break;
+    }
+    default:
+      ; // do not use grid size
+  }
+  if(mode == IconMode || mode == ThumbnailMode)
+    listView->setGridSize(grid + 2 * itemDelegateMargins_); // the default spacing is 6(=2x3) px
+  else
+    listView->setGridSize(grid);
+  FolderItemDelegate* delegate = static_cast<FolderItemDelegate*>(listView->itemDelegateForColumn(FolderModel::ColumnFileName));
+  delegate->setGridSize(grid);
+}
+
+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);
+    FmFileInfo* info = reinterpret_cast<FmFileInfo*>(data.value<void*>());
+    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::PathList FolderView::selectedFilePaths() const {
+  if(model_) {
+    QModelIndexList selIndexes = mode == DetailedListMode ? selectedRows() : selectedIndexes();
+    if(!selIndexes.isEmpty()) {
+      PathList paths;
+      QModelIndexList::const_iterator it;
+      for(it = selIndexes.begin(); it != selIndexes.end(); ++it) {
+        FmFileInfo* file = model_->fileInfoFromIndex(*it);
+        paths.pushTail(fm_file_info_get_path(file));
+      }
+      return paths;
+    }
+  }
+  return nullptr;
+}
+
+QModelIndex FolderView::indexFromFolderPath(FmPath* folderPath) const {
+  if(!model_ || !folderPath) return QModelIndex();
+  QModelIndex index;
+  int count = model_->rowCount();
+  for(int row = 0; row < count; ++row) {
+    index = model_->index(row, 0);
+    FmFileInfo* info = model_->fileInfoFromIndex(index);
+    if(info && fm_file_info_is_dir(info) && fm_path_equal(folderPath,fm_file_info_get_path(info)))
+      return index;
+  }
+  return QModelIndex();
+}
+
+Fm::FileInfoList FolderView::selectedFiles() const {
+  if(model_) {
+    QModelIndexList selIndexes = mode == DetailedListMode ? selectedRows() : selectedIndexes();
+    if(!selIndexes.isEmpty()) {
+      FileInfoList files;
+      QModelIndexList::const_iterator it;
+      for(it = selIndexes.constBegin(); it != selIndexes.constEnd(); ++it) {
+        FmFileInfo* file = model_->fileInfoFromIndex(*it);
+        files.pushTail(file);
+      }
+      return files;
+    }
+  }
+  return nullptr;
+}
+
+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.
+    // This will trigger one selectionChanged event per row, which is very inefficient,
+    // but we have no other choices to workaround the Qt bug.
+    // I'll report a Qt bug for this later.
+    if(model_) {
+      int rowCount = model_->rowCount();
+      for(int row = 0; row < rowCount; ++row) {
+        QModelIndex index = model_->index(row, 0);
+        selectionModel()->select(index, 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 xworkaround.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.
+      Path filePath = Path(path()).newChild(basename);
+      QByteArray fileUri = filePath.toUri();
+      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);
+          if(!selectionModel()->hasSelection())
+            selectionModel()->setCurrentIndex(index, QItemSelectionModel::Current);
+        }
+        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:
+      // 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, FmFileInfo* fileInfo) {
+  if(type == ActivatedClick) {
+    if(fileLauncher_) {
+      GList* files = g_list_append(nullptr, fileInfo);
+      fileLauncher_->launchFiles(nullptr, files);
+      g_list_free(files);
+    }
+  }
+  else if(type == ContextMenuClick) {
+    FmPath* folderPath = nullptr;
+    FileInfoList files = selectedFiles();
+    if (!files.isNull()) {
+      FmFileInfo* first = files.peekHead();
+      if (files.getLength() == 1 && fm_file_info_is_dir(first))
+        folderPath = fm_file_info_get_path(first);
+    }
+    if (!folderPath)
+      folderPath = path();
+    QMenu* menu = nullptr;
+    if(fileInfo) {
+      // show context menu
+      FileInfoList files = selectedFiles();
+      if (!files.isNull()) {
+        Fm::FileMenu* fileMenu = new Fm::FileMenu(files.dataPtr(), fileInfo, folderPath);
+        fileMenu->setFileLauncher(fileLauncher_);
+        prepareFileMenu(fileMenu);
+        menu = fileMenu;
+      }
+    }
+    else {
+      Fm::FolderMenu* folderMenu = new Fm::FolderMenu(this);
+      prepareFolderMenu(folderMenu);
+      menu = folderMenu;
+    }
+    if (menu) {
+      menu->exec(QCursor::pos());
+      delete menu;
+    }
+  }
+}
+
+void FolderView::prepareFileMenu(FileMenu* menu) {
+}
+
+void FolderView::prepareFolderMenu(FolderMenu* menu) {
+}
+
diff --git a/src/folderview.h b/src/folderview.h
new file mode 100644 (file)
index 0000000..5173d94
--- /dev/null
@@ -0,0 +1,181 @@
+/*
+ * 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 "fileinfo.h"
+#include "path.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);
+  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);
+
+  FmFolder* folder() {
+    return model_ ? static_cast<FolderModel*>(model_->sourceModel())->folder() : NULL;
+  }
+
+  FmFileInfo* folderInfo() {
+    FmFolder* _folder = folder();
+    return _folder ? fm_folder_get_info(_folder) : NULL;
+  }
+
+  FmPath* path() {
+    FmFolder* _folder = folder();
+    return _folder ? fm_folder_get_path(_folder) : NULL;
+  }
+
+  QItemSelectionModel* selectionModel() const;
+  Fm::FileInfoList selectedFiles() const;
+  Fm::PathList selectedFilePaths() const;
+  QModelIndex indexFromFolderPath(FmPath* 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, FmFileInfo* fileInfo);
+
+private Q_SLOTS:
+  void onAutoSelectionTimeout();
+  void onSelChangedTimeout();
+
+Q_SIGNALS:
+  void clicked(int type, FmFileInfo* file);
+  void clickedBack();
+  void clickedForward();
+  void selChanged(int n_sel);
+  void sortChanged();
+
+private:
+
+  QAbstractItemView* view;
+  ProxyFolderModel* model_;
+  ViewMode mode;
+  QSize iconSize_[NumViewModes];
+  FileLauncher* fileLauncher_;
+  int autoSelectionDelay_;
+  QTimer* autoSelectionTimer_;
+  QModelIndex lastAutoSelectionIndex_;
+  QTimer* selChangedTimer_;
+  // the cell margins in the icon and thumbnail modes
+  QSize itemDelegateMargins_;
+};
+
+}
+
+#endif // FM_FOLDERVIEW_H
diff --git a/src/folderview_p.h b/src/folderview_p.h
new file mode 100644 (file)
index 0000000..91c6b81
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * 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 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);
+
+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 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);
+  virtual void reset();
+
+  virtual void resizeEvent(QResizeEvent* event);
+  void queueLayoutColumns();
+
+Q_SIGNALS:
+  void activatedFiltered(const QModelIndex &index);
+
+private Q_SLOTS:
+  void layoutColumns();
+  void activation(const QModelIndex &index);
+  void onSortFilterChanged();
+
+private:
+  bool doingLayout_;
+  QTimer* layoutTimer_;
+  bool activationAllowed_;
+};
+
+
+} // namespace Fm
+
+#endif // FM_FOLDERVIEW_P_H
diff --git a/src/fontbutton.cpp b/src/fontbutton.cpp
new file mode 100644 (file)
index 0000000..f4599de
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * 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
diff --git a/src/fontbutton.h b/src/fontbutton.h
new file mode 100644 (file)
index 0000000..1caf19b
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * 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
diff --git a/src/icon.h b/src/icon.h
new file mode 100644 (file)
index 0000000..794fdc9
--- /dev/null
@@ -0,0 +1,168 @@
+/*
+ * 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_ICON_H__
+#define __LIBFM_QT_FM_ICON_H__
+
+#include <libfm/fm.h>
+#include <QObject>
+#include <QtGlobal>
+#include "libfmqtglobals.h"
+
+
+namespace Fm {
+
+
+class LIBFM_QT_API Icon {
+public:
+
+
+  // default constructor
+  Icon() {
+    dataPtr_ = nullptr;
+  }
+
+
+  Icon(FmIcon* dataPtr){
+    dataPtr_ = dataPtr != nullptr ? reinterpret_cast<FmIcon*>(fm_icon_ref(dataPtr)) : nullptr;
+  }
+
+
+  // copy constructor
+  Icon(const Icon& other) {
+    dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<FmIcon*>(fm_icon_ref(other.dataPtr_)) : nullptr;
+  }
+
+
+  // move constructor
+  Icon(Icon&& other) {
+    dataPtr_ = reinterpret_cast<FmIcon*>(other.takeDataPtr());
+  }
+
+
+  // destructor
+  ~Icon() {
+    if(dataPtr_ != nullptr) {
+      fm_icon_unref(dataPtr_);
+    }
+  }
+
+
+  // create a wrapper for the data pointer without increasing the reference count
+  static Icon wrapPtr(FmIcon* dataPtr) {
+    Icon obj;
+    obj.dataPtr_ = reinterpret_cast<FmIcon*>(dataPtr);
+    return obj;
+  }
+
+  // disown the managed data pointer
+  FmIcon* takeDataPtr() {
+    FmIcon* data = reinterpret_cast<FmIcon*>(dataPtr_);
+    dataPtr_ = nullptr;
+    return data;
+  }
+
+  // get the raw pointer wrapped
+  FmIcon* dataPtr() {
+    return reinterpret_cast<FmIcon*>(dataPtr_);
+  }
+
+  // automatic type casting
+  operator FmIcon*() {
+    return dataPtr();
+  }
+
+  // automatic type casting
+  operator void*() {
+    return dataPtr();
+  }
+
+
+  // copy assignment
+  Icon& operator=(const Icon& other) {
+    if(dataPtr_ != nullptr) {
+      fm_icon_unref(dataPtr_);
+    }
+    dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<FmIcon*>(fm_icon_ref(other.dataPtr_)) : nullptr;
+    return *this;
+  }
+
+
+  // move assignment
+  Icon& operator=(Icon&& other) {
+    dataPtr_ = reinterpret_cast<FmIcon*>(other.takeDataPtr());
+    return *this;
+  }
+
+  bool isNull() {
+    return (dataPtr_ == nullptr);
+  }
+
+  // methods
+
+  static void unloadCache( ) {
+    fm_icon_unload_cache();
+  }
+
+
+  static void resetUserDataCache(GQuark quark) {
+    fm_icon_reset_user_data_cache(quark);
+  }
+
+
+  static void unloadUserDataCache( ) {
+    fm_icon_unload_user_data_cache();
+  }
+
+
+  static void setUserDataDestroy(GDestroyNotify func) {
+    fm_icon_set_user_data_destroy(func);
+  }
+
+
+  void setUserData(gpointer user_data) {
+    fm_icon_set_user_data(dataPtr(), user_data);
+  }
+
+
+  gpointer getUserData(void) {
+    return fm_icon_get_user_data(dataPtr());
+  }
+
+
+  static Icon fromName(const char* name) {
+    return Icon::wrapPtr(fm_icon_from_name(name));
+  }
+
+
+  static Icon fromGicon(GIcon* gicon) {
+    return Icon::wrapPtr(fm_icon_from_gicon(gicon));
+  }
+
+
+
+private:
+  FmIcon* dataPtr_; // data pointer for the underlying C struct
+
+};
+
+
+}
+
+#endif // __LIBFM_QT_FM_ICON_H__
diff --git a/src/icontheme.cpp b/src/icontheme.cpp
new file mode 100644 (file)
index 0000000..36542b9
--- /dev/null
@@ -0,0 +1,183 @@
+/*
+ * 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>
+
+namespace Fm {
+
+class IconCacheData {
+public:
+    QIcon qicon;
+    QList<Icon> emblems;
+};
+
+static IconTheme* theIconTheme = NULL; // the global single instance of IconTheme.
+static const char* fallbackNames[] = {"unknown", "application-octet-stream", NULL};
+
+static void fmIconDataDestroy(gpointer user_data) {
+  IconCacheData* data = reinterpret_cast<IconCacheData*>(user_data);
+  delete data;
+}
+
+IconTheme::IconTheme():
+  currentThemeName_(QIcon::themeName()) {
+  // NOTE: only one instance is allowed
+  Q_ASSERT(theIconTheme == NULL);
+  Q_ASSERT(qApp != NULL); // QApplication should exists before contructing IconTheme.
+
+  theIconTheme = this;
+  fm_icon_set_user_data_destroy(reinterpret_cast<GDestroyNotify>(fmIconDataDestroy));
+  fallbackIcon_ = iconFromNames(fallbackNames);
+
+  // 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_icon_reset_user_data_cache(fm_qdata_id);
+
+    theIconTheme->fallbackIcon_ = iconFromNames(fallbackNames);
+    Q_EMIT theIconTheme->changed();
+  }
+}
+
+QIcon IconTheme::iconFromNames(const char* const* names) {
+  const gchar* const* name;
+  // qDebug("names: %p", names);
+  for(name = names; *name; ++name) {
+    // qDebug("icon name=%s", *name);
+    QString qname = *name;
+    QIcon qicon = QIcon::fromTheme(qname);
+    if(!qicon.isNull()) {
+      return qicon;
+    }
+  }
+  return QIcon();
+}
+
+QIcon IconTheme::convertFromGIconWithoutEmblems(GIcon* gicon) {
+  if(G_IS_THEMED_ICON(gicon)) {
+    const gchar * const * names = g_themed_icon_get_names(G_THEMED_ICON(gicon));
+    QIcon icon = iconFromNames(names);
+    if(!icon.isNull())
+      return icon;
+  }
+  else if(G_IS_FILE_ICON(gicon)) {
+    GFile* file = g_file_icon_get_file(G_FILE_ICON(gicon));
+    char* fpath = g_file_get_path(file);
+    QString path = fpath;
+    g_free(fpath);
+    return QIcon(path);
+  }
+  return theIconTheme->fallbackIcon_;
+}
+
+
+// static
+IconCacheData* IconTheme::ensureCacheData(FmIcon* fmicon) {
+  IconCacheData* data = reinterpret_cast<IconCacheData*>(fm_icon_get_user_data(fmicon));
+  if(!data) { // we don't have a cache yet
+    data = new IconCacheData();
+    GIcon* gicon = G_ICON(fmicon);
+    if(G_IS_EMBLEMED_ICON(gicon)) { // special handling for emblemed icon
+      GList* emblems = g_emblemed_icon_get_emblems(G_EMBLEMED_ICON(gicon));
+      for(GList* l = emblems; l; l = l->next) {
+        GIcon* emblem_gicon = g_emblem_get_icon(G_EMBLEM(l->data));
+        data->emblems.append(Icon::fromGicon(emblem_gicon));
+      }
+      gicon = g_emblemed_icon_get_icon(G_EMBLEMED_ICON(gicon));  // get an emblemless GIcon
+    }
+    data->qicon = convertFromGIconWithoutEmblems(gicon);
+    fm_icon_set_user_data(fmicon, data); // store it in FmIcon
+  }
+  return data;
+}
+
+//static
+QIcon IconTheme::icon(FmIcon* fmicon) {
+  IconCacheData* data = ensureCacheData(fmicon);
+  return data->qicon;
+}
+
+//static
+QIcon IconTheme::icon(GIcon* gicon) {
+  if(G_IS_EMBLEMED_ICON(gicon)) // get an emblemless GIcon
+    gicon = g_emblemed_icon_get_icon(G_EMBLEMED_ICON(gicon));
+  if(G_IS_THEMED_ICON(gicon)) {
+    FmIcon* fmicon = fm_icon_from_gicon(gicon);
+    QIcon qicon = icon(fmicon);
+    fm_icon_unref(fmicon);
+    return qicon;
+  }
+  else if(G_IS_FILE_ICON(gicon)) {
+    // we do not map GFileIcon to FmIcon deliberately.
+    return convertFromGIconWithoutEmblems(gicon);
+  }
+  return theIconTheme->fallbackIcon_;
+}
+
+// static
+QList<Icon> IconTheme::emblems(FmIcon* fmicon) {
+  IconCacheData* data = ensureCacheData(fmicon);
+  return data->emblems;
+}
+
+//static
+QList<Icon> IconTheme::emblems(GIcon* gicon) {
+  if(G_IS_EMBLEMED_ICON(gicon)) {  // if this gicon contains emblems
+    Icon fmicon = Icon::fromGicon(gicon);
+    return emblems(fmicon.dataPtr());
+  }
+  return QList<Icon>();
+}
+
+// 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.
+  if(event->type() == QEvent::StyleChange) {
+    checkChanged(); // check if the icon theme is changed
+  }
+  return QObject::eventFilter(obj, event);
+}
+
+
+} // namespace Fm
diff --git a/src/icontheme.h b/src/icontheme.h
new file mode 100644 (file)
index 0000000..df613ef
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * 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"
+#include "icon.h"
+
+namespace Fm {
+
+// NOTE:
+// Qt seems to has its own QIcon pixmap caching mechanism internally.
+// Besides, it also caches QIcon objects created by QIcon::fromTheme().
+// So maybe we should not duplicate the work.
+// See http://qt.gitorious.org/qt/qt/blobs/4.8/src/gui/image/qicon.cpp
+// QPixmap QPixmapIconEngine::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state).
+// In addition, QPixmap is actually stored in X11 server, not client side.
+// Hence maybe we should not cache too many pixmaps, I guess?
+// Let's have Qt do its work and only translate GIcon to QIcon here.
+
+// Nice article about QPixmap from KDE: http://techbase.kde.org/Development/Tutorials/Graphics/Performance
+
+class IconCacheData;
+
+class LIBFM_QT_API IconTheme: public QObject {
+  Q_OBJECT
+public:
+  IconTheme();
+  ~IconTheme();
+
+  static IconTheme* instance();
+  static QIcon icon(FmIcon* fmicon);
+  static QIcon icon(GIcon* gicon);
+  static QList<Icon> emblems(FmIcon* fmicon);
+  static QList<Icon> emblems(GIcon* gicon);
+
+  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);
+  static QIcon convertFromGIconWithoutEmblems(GIcon* gicon);
+  static QIcon iconFromNames(const char * const * names);
+  static IconCacheData* ensureCacheData(FmIcon* fmicon);
+
+protected:
+  QIcon fallbackIcon_;
+  QString currentThemeName_;
+};
+
+}
+
+#endif // FM_ICONTHEME_H
diff --git a/src/job.h b/src/job.h
new file mode 100644 (file)
index 0000000..12e337b
--- /dev/null
+++ b/src/job.h
@@ -0,0 +1,225 @@
+/*
+ * 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 "libfmqtglobals.h"
+
+
+namespace Fm {
+
+
+class LIBFM_QT_API Job {
+public:
+
+
+  // default constructor
+  Job() {
+    dataPtr_ = nullptr;
+  }
+
+
+  Job(FmJob* dataPtr){
+    dataPtr_ = dataPtr != nullptr ? reinterpret_cast<GObject*>(g_object_ref(dataPtr)) : nullptr;
+  }
+
+
+  // copy constructor
+  Job(const Job& other) {
+    dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<GObject*>(g_object_ref(other.dataPtr_)) : nullptr;
+  }
+
+
+  // move constructor
+  Job(Job&& other) {
+    dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
+  }
+
+
+  // destructor
+  virtual ~Job() {
+    if(dataPtr_ != nullptr) {
+      g_object_unref(dataPtr_);
+    }
+  }
+
+
+  // create a wrapper for the data pointer without increasing the reference count
+  static Job wrapPtr(FmJob* dataPtr) {
+    Job obj;
+    obj.dataPtr_ = reinterpret_cast<GObject*>(dataPtr);
+    return obj;
+  }
+
+  // disown the managed data pointer
+  FmJob* takeDataPtr() {
+    FmJob* data = reinterpret_cast<FmJob*>(dataPtr_);
+    dataPtr_ = nullptr;
+    return data;
+  }
+
+  // get the raw pointer wrapped
+  FmJob* dataPtr() {
+    return reinterpret_cast<FmJob*>(dataPtr_);
+  }
+
+  // automatic type casting
+  operator FmJob*() {
+    return dataPtr();
+  }
+
+  // automatic type casting
+  operator void*() {
+    return dataPtr();
+  }
+
+
+  // copy assignment
+  Job& operator=(const Job& 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
+  Job& operator=(Job&& other) {
+    dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
+    return *this;
+  }
+
+  bool isNull() {
+    return (dataPtr_ == nullptr);
+  }
+
+  // methods
+
+  void resume(void) {
+    fm_job_resume(dataPtr());
+  }
+
+
+  bool pause(void) {
+    return fm_job_pause(dataPtr());
+  }
+
+
+  int askValist(const char* question, va_list options) {
+    return fm_job_ask_valist(dataPtr(), question, options);
+  }
+
+
+  int askv(const char* question, gchar* const* options) {
+    return fm_job_askv(dataPtr(), question, options);
+  }
+
+
+  int ask(const char* question, ... ) {
+    
+    int ret;
+    va_list args;
+    va_start (args, question);
+    ret = fm_job_ask_valist(dataPtr(), question, args);
+    va_end (args);
+    return ret;
+
+  }
+
+
+  FmJobErrorAction emitError(GError* err, FmJobErrorSeverity severity) {
+    return fm_job_emit_error(dataPtr(), err, severity);
+  }
+
+
+  void finish(void) {
+    fm_job_finish(dataPtr());
+  }
+
+
+  void setCancellable(GCancellable* cancellable) {
+    fm_job_set_cancellable(dataPtr(), cancellable);
+  }
+
+
+  GCancellable* getCancellable(void) {
+    return fm_job_get_cancellable(dataPtr());
+  }
+
+
+  void initCancellable(void) {
+    fm_job_init_cancellable(dataPtr());
+  }
+
+
+  gpointer callMainThread(FmJobCallMainThreadFunc func, gpointer user_data) {
+    return fm_job_call_main_thread(dataPtr(), func, user_data);
+  }
+
+
+  void cancel(void) {
+    fm_job_cancel(dataPtr());
+  }
+
+
+  bool runSyncWithMainloop(void) {
+    return fm_job_run_sync_with_mainloop(dataPtr());
+  }
+
+
+  bool runSync(void) {
+    return fm_job_run_sync(dataPtr());
+  }
+
+
+  bool runAsync(void) {
+    return fm_job_run_async(dataPtr());
+  }
+
+
+  bool isRunning(void) {
+    return fm_job_is_running(dataPtr());
+  }
+
+
+  bool isCancelled(void) {
+    return fm_job_is_cancelled(dataPtr());
+  }
+
+
+  // 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_JOB_H__
diff --git a/src/libfm-qt.pc.in b/src/libfm-qt.pc.in
new file mode 100644 (file)
index 0000000..11e35a3
--- /dev/null
@@ -0,0 +1,12 @@
+prefix=@CMAKE_INSTALL_PREFIX@
+exec_prefix=${prefix}
+libdir=${exec_prefix}/lib
+includedir=${prefix}/include
+
+Name: libfm-qt
+Description: A Qt/glib/gio-based lib used to develop file managers providing some file management utilities. (This is a Qt port of the original libfm library)
+URL: http://pcmanfm.sourceforge.net/
+Requires: @REQUIRED_QT@ libfm >= 1.2.0
+Version: @LIBFM_QT_VERSION@
+Libs: -L${libdir} -lfm -l@LIBFM_QT_LIBRARY_NAME@
+Cflags: -I${includedir}
diff --git a/src/libfmqt.cpp b/src/libfmqt.cpp
new file mode 100644 (file)
index 0000000..63127c2
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * 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 "icontheme.h"
+#include "thumbnailloader.h"
+#include "xdndworkaround.h"
+
+namespace Fm {
+
+struct LibFmQtData {
+  LibFmQtData();
+  ~LibFmQtData();
+
+  IconTheme* iconTheme;
+  ThumbnailLoader* thumbnailLoader;
+  QTranslator translator;
+  XdndWorkaround workaround;
+  int refCount;
+  Q_DISABLE_COPY(LibFmQtData)
+};
+
+static LibFmQtData* theLibFmData = NULL;
+
+LibFmQtData::LibFmQtData(): refCount(1) {
+#if !GLIB_CHECK_VERSION(2, 36, 0)
+  g_type_init();
+#endif
+  fm_init(NULL);
+  // turn on glib debug message
+  // g_setenv("G_MESSAGES_DEBUG", "all", true);
+  iconTheme = new IconTheme();
+  thumbnailLoader = new ThumbnailLoader();
+  translator.load("libfm-qt_" + QLocale::system().name(), LIBFM_QT_DATA_DIR "/translations");
+}
+
+LibFmQtData::~LibFmQtData() {
+  delete iconTheme;
+  delete thumbnailLoader;
+  fm_finalize();
+}
+
+LibFmQt::LibFmQt() {
+  if(!theLibFmData) {
+    theLibFmData = new LibFmQtData();
+  }
+  else
+    ++theLibFmData->refCount;
+  d = theLibFmData;
+}
+
+LibFmQt::~LibFmQt() {
+  if(--d->refCount == 0) {
+    delete d;
+    theLibFmData = NULL;
+  }
+}
+
+QTranslator* LibFmQt::translator() {
+  return &d->translator;
+}
+
+} // namespace Fm
diff --git a/src/libfmqt.h b/src/libfmqt.h
new file mode 100644 (file)
index 0000000..83ffd8e
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * 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:
+  LibFmQt();
+  ~LibFmQt();
+
+  QTranslator* translator();
+
+private:
+  LibFmQt(LibFmQt& other); // disable copy
+  LibFmQtData* d;
+};
+
+}
+
+#endif // FM_APPLICATION_H
diff --git a/src/libfmqtglobals.h b/src/libfmqtglobals.h
new file mode 100644 (file)
index 0000000..1a6d4fe
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * 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
diff --git a/src/list.h b/src/list.h
new file mode 100644 (file)
index 0000000..e207004
--- /dev/null
@@ -0,0 +1,153 @@
+/*
+ * 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_LIST_H__
+#define __LIBFM_QT_FM_LIST_H__
+
+#include <libfm/fm.h>
+#include <QObject>
+#include <QtGlobal>
+#include "libfmqtglobals.h"
+
+
+namespace Fm {
+
+
+class LIBFM_QT_API List {
+public:
+
+
+  List(FmListFuncs* funcs) {
+    dataPtr_ = reinterpret_cast<FmList*>(fm_list_new(funcs));
+  }
+
+
+  // default constructor
+  List() {
+    dataPtr_ = nullptr;
+  }
+
+
+  List(FmList* dataPtr){
+    dataPtr_ = dataPtr != nullptr ? reinterpret_cast<FmList*>(fm_list_ref(dataPtr)) : nullptr;
+  }
+
+
+  // copy constructor
+  List(const List& other) {
+    dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<FmList*>(fm_list_ref(other.dataPtr_)) : nullptr;
+  }
+
+
+  // move constructor
+  List(List&& other) {
+    dataPtr_ = reinterpret_cast<FmList*>(other.takeDataPtr());
+  }
+
+
+  // destructor
+  ~List() {
+    if(dataPtr_ != nullptr) {
+      fm_list_unref(dataPtr_);
+    }
+  }
+
+
+  // create a wrapper for the data pointer without increasing the reference count
+  static List wrapPtr(FmList* dataPtr) {
+    List obj;
+    obj.dataPtr_ = reinterpret_cast<FmList*>(dataPtr);
+    return obj;
+  }
+
+  // disown the managed data pointer
+  FmList* takeDataPtr() {
+    FmList* data = reinterpret_cast<FmList*>(dataPtr_);
+    dataPtr_ = nullptr;
+    return data;
+  }
+
+  // get the raw pointer wrapped
+  FmList* dataPtr() {
+    return reinterpret_cast<FmList*>(dataPtr_);
+  }
+
+  // automatic type casting
+  operator FmList*() {
+    return dataPtr();
+  }
+
+  // automatic type casting
+  operator void*() {
+    return dataPtr();
+  }
+
+
+  // copy assignment
+  List& operator=(const List& other) {
+    if(dataPtr_ != nullptr) {
+      fm_list_unref(dataPtr_);
+    }
+    dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<FmList*>(fm_list_ref(other.dataPtr_)) : nullptr;
+    return *this;
+  }
+
+
+  // move assignment
+  List& operator=(List&& other) {
+    dataPtr_ = reinterpret_cast<FmList*>(other.takeDataPtr());
+    return *this;
+  }
+
+  bool isNull() {
+    return (dataPtr_ == nullptr);
+  }
+
+  // methods
+
+  void deleteLink(GList* l_) {
+    fm_list_delete_link(dataPtr(), l_);
+  }
+
+
+  void removeAll(gpointer data) {
+    fm_list_remove_all(dataPtr(), data);
+  }
+
+
+  void remove(gpointer data) {
+    fm_list_remove(dataPtr(), data);
+  }
+
+
+  void clear(void) {
+    fm_list_clear(dataPtr());
+  }
+
+
+
+private:
+  FmList* dataPtr_; // data pointer for the underlying C struct
+
+};
+
+
+}
+
+#endif // __LIBFM_QT_FM_LIST_H__
diff --git a/src/mimetype.h b/src/mimetype.h
new file mode 100644 (file)
index 0000000..5cf402d
--- /dev/null
@@ -0,0 +1,163 @@
+/*
+ * 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_MIME_TYPE_H__
+#define __LIBFM_QT_FM_MIME_TYPE_H__
+
+#include <libfm/fm.h>
+#include <QObject>
+#include <QtGlobal>
+#include "libfmqtglobals.h"
+
+
+namespace Fm {
+
+
+class LIBFM_QT_API MimeType {
+public:
+
+
+  // default constructor
+  MimeType() {
+    dataPtr_ = nullptr;
+  }
+
+
+  MimeType(FmMimeType* dataPtr){
+    dataPtr_ = dataPtr != nullptr ? reinterpret_cast<FmMimeType*>(fm_mime_type_ref(dataPtr)) : nullptr;
+  }
+
+
+  // copy constructor
+  MimeType(const MimeType& other) {
+    dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<FmMimeType*>(fm_mime_type_ref(other.dataPtr_)) : nullptr;
+  }
+
+
+  // move constructor
+  MimeType(MimeType&& other) {
+    dataPtr_ = reinterpret_cast<FmMimeType*>(other.takeDataPtr());
+  }
+
+
+  // destructor
+  ~MimeType() {
+    if(dataPtr_ != nullptr) {
+      fm_mime_type_unref(dataPtr_);
+    }
+  }
+
+
+  // create a wrapper for the data pointer without increasing the reference count
+  static MimeType wrapPtr(FmMimeType* dataPtr) {
+    MimeType obj;
+    obj.dataPtr_ = reinterpret_cast<FmMimeType*>(dataPtr);
+    return obj;
+  }
+
+  // disown the managed data pointer
+  FmMimeType* takeDataPtr() {
+    FmMimeType* data = reinterpret_cast<FmMimeType*>(dataPtr_);
+    dataPtr_ = nullptr;
+    return data;
+  }
+
+  // get the raw pointer wrapped
+  FmMimeType* dataPtr() {
+    return reinterpret_cast<FmMimeType*>(dataPtr_);
+  }
+
+  // automatic type casting
+  operator FmMimeType*() {
+    return dataPtr();
+  }
+
+  // automatic type casting
+  operator void*() {
+    return dataPtr();
+  }
+
+
+  // copy assignment
+  MimeType& operator=(const MimeType& other) {
+    if(dataPtr_ != nullptr) {
+      fm_mime_type_unref(dataPtr_);
+    }
+    dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<FmMimeType*>(fm_mime_type_ref(other.dataPtr_)) : nullptr;
+    return *this;
+  }
+
+
+  // move assignment
+  MimeType& operator=(MimeType&& other) {
+    dataPtr_ = reinterpret_cast<FmMimeType*>(other.takeDataPtr());
+    return *this;
+  }
+
+  bool isNull() {
+    return (dataPtr_ == nullptr);
+  }
+
+  // methods
+
+  void removeThumbnailer(gpointer thumbnailer) {
+    fm_mime_type_remove_thumbnailer(dataPtr(), thumbnailer);
+  }
+
+
+  void addThumbnailer(gpointer thumbnailer) {
+    fm_mime_type_add_thumbnailer(dataPtr(), thumbnailer);
+  }
+
+
+  GList* getThumbnailersList(void) {
+    return fm_mime_type_get_thumbnailers_list(dataPtr());
+  }
+
+
+  FmIcon* getIcon(void) {
+    return fm_mime_type_get_icon(dataPtr());
+  }
+
+
+  static MimeType fromName(const char* type) {
+    return MimeType::wrapPtr(fm_mime_type_from_name(type));
+  }
+
+
+  static MimeType fromNativeFile(const char* file_path, const char* base_name, struct stat* pstat) {
+    return MimeType::wrapPtr(fm_mime_type_from_native_file(file_path, base_name, pstat));
+  }
+
+
+  static MimeType fromFileName(const char* ufile_name) {
+    return MimeType::wrapPtr(fm_mime_type_from_file_name(ufile_name));
+  }
+
+
+
+private:
+  FmMimeType* dataPtr_; // data pointer for the underlying C struct
+
+};
+
+
+}
+
+#endif // __LIBFM_QT_FM_MIME_TYPE_H__
diff --git a/src/mount-operation-password.ui b/src/mount-operation-password.ui
new file mode 100644 (file)
index 0000000..72f1b30
--- /dev/null
@@ -0,0 +1,215 @@
+<?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 &amp;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&amp;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>&amp;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>&amp;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>&amp;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 &amp;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 &amp;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 &amp;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>
diff --git a/src/mountoperation.cpp b/src/mountoperation.cpp
new file mode 100644 (file)
index 0000000..b7fb9cc
--- /dev/null
@@ -0,0 +1,227 @@
+/*
+ * 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(NULL),
+  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 = NULL;
+    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 = NULL;
+    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 = NULL;
+    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 = NULL;
+    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 = NULL;
+    g_mount_unmount_with_operation_finish(mount, res, &error);
+    (*pThis)->handleFinish(error);
+  }
+  delete pThis;
+}
+
+void MountOperation::handleFinish(GError* error) {
+  qDebug("operation finished: %p", 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(NULL, 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 != NULL ? 1 : 0);
+    eventLoop = NULL;
+  }
+
+  if(error)
+    g_error_free(error);
+
+  // free ourself here!!
+  if(autoDestroy_)
+    deleteLater();
+}
+
+void MountOperation::prepareUnmount(GMount* mount) {
+  /* ensure that CWD is not on the mounted filesystem. */
+  char* cwd_str = g_get_current_dir();
+  GFile* cwd = g_file_new_for_path(cwd_str);
+  GFile* root = g_mount_get_root(mount);
+  g_free(cwd_str);
+  /* FIXME: This cannot cover 100% cases since symlinks are not checked.
+    * There may be other cases that cwd is actually under mount root
+    * but checking prefix is not enough. We already did our best, though. */
+  if(g_file_has_prefix(cwd, root))
+      g_chdir("/");
+  g_object_unref(cwd);
+  g_object_unref(root);
+}
+
+// block the operation used an internal QEventLoop and returns
+// only after the whole operation is finished.
+bool MountOperation::wait() {
+  QEventLoop loop;
+  eventLoop = &loop;
+  int exitCode = loop.exec();
+  return exitCode == 0 ? true : false;
+}
+
+} // namespace Fm
diff --git a/src/mountoperation.h b/src/mountoperation.h
new file mode 100644 (file)
index 0000000..00b5849
--- /dev/null
@@ -0,0 +1,155 @@
+/*
+ * 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>
+
+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(FmPath* path) {
+    GFile* gf = fm_path_to_gfile(path);
+    g_file_mount_enclosing_volume(gf, G_MOUNT_MOUNT_NONE, op, cancellable_, (GAsyncReadyCallback)onMountFileFinished, new QPointer<MountOperation>(this));
+    g_object_unref(gf);
+  }
+
+  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 = NULL);
+
+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
diff --git a/src/mountoperationpassworddialog.cpp b/src/mountoperationpassworddialog.cpp
new file mode 100644 (file)
index 0000000..f2783e3
--- /dev/null
@@ -0,0 +1,125 @@
+/*
+ * 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().first()->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);
+  }
+  if(!needUserName) {
+    ui->username->setEnabled(false);
+  }
+  if(!needPassword) {
+    ui->password->setEnabled(false);
+  }
+  if(!needDomain) {
+    ui->domain->hide();
+    ui->domainLabel->hide();
+  }
+  if(canSavePassword) {
+    ui->sessionPassword->setChecked(true);
+  }
+  else {
+    ui->storePassword->setEnabled(false);
+    ui->sessionPassword->setEnabled(false);
+    ui->forgetPassword->setChecked(true);
+  }
+}
+
+MountOperationPasswordDialog::~MountOperationPasswordDialog() {
+  delete ui;
+}
+
+void MountOperationPasswordDialog::onAnonymousToggled(bool checked) {
+  // disable username/password entries if anonymous mode is used
+  bool useUserPassword = !checked;
+  if(needUserName)
+    ui->username->setEnabled(useUserPassword);
+  if(needPassword)
+    ui->password->setEnabled(useUserPassword);
+  if(needDomain)
+    ui->domain->setEnabled(useUserPassword);
+
+  if(canSavePassword) {
+    ui->forgetPassword->setEnabled(useUserPassword);
+    ui->sessionPassword->setEnabled(useUserPassword);
+    ui->storePassword->setEnabled(useUserPassword);
+  }
+}
+
+void MountOperationPasswordDialog::setMessage(QString message) {
+  ui->message->setText(message);
+}
+
+void MountOperationPasswordDialog::setDefaultDomain(QString domain) {
+  ui->domain->setText(domain);
+}
+
+void MountOperationPasswordDialog::setDefaultUser(QString user) {
+  ui->username->setText(user);
+}
+
+void MountOperationPasswordDialog::done(int r) {
+  GMountOperation* gmop = mountOperation->mountOperation();
+
+  if(r == QDialog::Accepted) {
+
+    if(needUserName)
+      g_mount_operation_set_username(gmop, ui->username->text().toUtf8());
+    if(needDomain)
+      g_mount_operation_set_domain(gmop, ui->domain->text().toUtf8());
+    if(needPassword)
+      g_mount_operation_set_password(gmop, ui->password->text().toUtf8());
+    if(canAnonymous)
+      g_mount_operation_set_anonymous(gmop, ui->Anonymous->isChecked());
+
+    g_mount_operation_reply(gmop, G_MOUNT_OPERATION_HANDLED);
+  }
+  else {
+    g_mount_operation_reply(gmop, G_MOUNT_OPERATION_ABORTED);
+  }
+  QDialog::done(r);
+}
+
+} // namespace Fm
diff --git a/src/mountoperationpassworddialog_p.h b/src/mountoperationpassworddialog_p.h
new file mode 100644 (file)
index 0000000..10ff58b
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * 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
diff --git a/src/mountoperationquestiondialog.cpp b/src/mountoperationquestiondialog.cpp
new file mode 100644 (file)
index 0000000..a409e28
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * 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 and
+    // handle their clicked() signals in our own slots.
+    // When anyone of the buttons is clicked, exec() always returns "accept".
+    QPushButton* button = new QPushButton(QString::fromUtf8(choices[i]));
+    addButton(button, QMessageBox::AcceptRole);
+    choiceButtons[i] = button;
+  }
+  connect(this, &MountOperationQuestionDialog::buttonClicked, this, &MountOperationQuestionDialog::onButtonClicked);
+}
+
+MountOperationQuestionDialog::~MountOperationQuestionDialog() {
+  delete []choiceButtons;
+}
+
+void MountOperationQuestionDialog::done(int r) {
+  if(r != QDialog::Accepted) {
+    GMountOperation* op = mountOperation->mountOperation();
+    g_mount_operation_reply(op, G_MOUNT_OPERATION_ABORTED);
+  }
+  QDialog::done(r);
+}
+
+void MountOperationQuestionDialog::onButtonClicked(QAbstractButton* button) {
+  GMountOperation* op = mountOperation->mountOperation();
+  for(int i = 0; i < choiceCount; ++i) {
+    if(choiceButtons[i] == button) {
+      g_mount_operation_set_choice(op, i);
+      g_mount_operation_reply(op, G_MOUNT_OPERATION_HANDLED);
+      break;
+    }
+  }
+}
+
+} // namespace Fm
diff --git a/src/mountoperationquestiondialog_p.h b/src/mountoperationquestiondialog_p.h
new file mode 100644 (file)
index 0000000..b0a4bdb
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * 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 <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);
+
+private Q_SLOTS:
+  void onButtonClicked(QAbstractButton* button);
+
+private:
+  MountOperation* mountOperation;
+  QAbstractButton** choiceButtons;
+  int choiceCount;
+};
+
+}
+
+#endif // FM_MOUNTOPERATIONQUESTIONDIALOG_H
diff --git a/src/navhistory.h b/src/navhistory.h
new file mode 100644 (file)
index 0000000..f88ef4c
--- /dev/null
@@ -0,0 +1,192 @@
+/*
+ * 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_NAV_HISTORY_H__
+#define __LIBFM_QT_FM_NAV_HISTORY_H__
+
+#include <libfm/fm.h>
+#include <QObject>
+#include <QtGlobal>
+#include "libfmqtglobals.h"
+
+
+namespace Fm {
+
+
+class LIBFM_QT_API NavHistory {
+public:
+
+
+  NavHistory(void ) {
+    dataPtr_ = reinterpret_cast<GObject*>(fm_nav_history_new());
+  }
+
+
+  NavHistory(FmNavHistory* dataPtr){
+    dataPtr_ = dataPtr != nullptr ? reinterpret_cast<GObject*>(g_object_ref(dataPtr)) : nullptr;
+  }
+
+
+  // copy constructor
+  NavHistory(const NavHistory& other) {
+    dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<GObject*>(g_object_ref(other.dataPtr_)) : nullptr;
+  }
+
+
+  // move constructor
+  NavHistory(NavHistory&& other) {
+    dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
+  }
+
+
+  // destructor
+  virtual ~NavHistory() {
+    if(dataPtr_ != nullptr) {
+      g_object_unref(dataPtr_);
+    }
+  }
+
+
+  // create a wrapper for the data pointer without increasing the reference count
+  static NavHistory wrapPtr(FmNavHistory* dataPtr) {
+    NavHistory obj;
+    obj.dataPtr_ = reinterpret_cast<GObject*>(dataPtr);
+    return obj;
+  }
+
+  // disown the managed data pointer
+  FmNavHistory* takeDataPtr() {
+    FmNavHistory* data = reinterpret_cast<FmNavHistory*>(dataPtr_);
+    dataPtr_ = nullptr;
+    return data;
+  }
+
+  // get the raw pointer wrapped
+  FmNavHistory* dataPtr() {
+    return reinterpret_cast<FmNavHistory*>(dataPtr_);
+  }
+
+  // automatic type casting
+  operator FmNavHistory*() {
+    return dataPtr();
+  }
+
+  // automatic type casting
+  operator void*() {
+    return dataPtr();
+  }
+
+
+  // copy assignment
+  NavHistory& operator=(const NavHistory& 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
+  NavHistory& operator=(NavHistory&& other) {
+    dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
+    return *this;
+  }
+
+  bool isNull() {
+    return (dataPtr_ == nullptr);
+  }
+
+  // methods
+
+  void setMax(guint num) {
+    fm_nav_history_set_max(dataPtr(), num);
+  }
+
+
+  void clear(void) {
+    fm_nav_history_clear(dataPtr());
+  }
+
+
+  void chdir(FmPath* path, gint old_scroll_pos) {
+    fm_nav_history_chdir(dataPtr(), path, old_scroll_pos);
+  }
+
+
+  bool canBack(void) {
+    return fm_nav_history_can_back(dataPtr());
+  }
+
+
+  int getScrollPos(void) {
+    return fm_nav_history_get_scroll_pos(dataPtr());
+  }
+
+
+  FmPath* goTo(guint n, gint old_scroll_pos) {
+    return fm_nav_history_go_to(dataPtr(), n, old_scroll_pos);
+  }
+
+
+  FmPath* getNthPath(guint n) {
+    return fm_nav_history_get_nth_path(dataPtr(), n);
+  }
+
+
+  unsigned int getCurIndex(void) {
+    return fm_nav_history_get_cur_index(dataPtr());
+  }
+
+
+  void jump(GList* l, int old_scroll_pos) {
+    fm_nav_history_jump(dataPtr(), l, old_scroll_pos);
+  }
+
+
+  void forward(int old_scroll_pos) {
+    fm_nav_history_forward(dataPtr(), old_scroll_pos);
+  }
+
+
+  bool canForward(void) {
+    return fm_nav_history_can_forward(dataPtr());
+  }
+
+
+  void back(int old_scroll_pos) {
+    fm_nav_history_back(dataPtr(), old_scroll_pos);
+  }
+
+
+  // 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_NAV_HISTORY_H__
diff --git a/src/path.h b/src/path.h
new file mode 100644 (file)
index 0000000..dccf91e
--- /dev/null
@@ -0,0 +1,441 @@
+/*
+ * 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__
diff --git a/src/pathbar.cpp b/src/pathbar.cpp
new file mode 100644 (file)
index 0000000..e5d9a34
--- /dev/null
@@ -0,0 +1,295 @@
+/*
+ * 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 <QPushButton>
+#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, 0);
+        Q_EMIT middleClickChdir(btn->pathElement().dataPtr());
+      }
+  }
+}
+
+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());
+  }
+}
+
+void PathBar::onButtonToggled(bool checked) {
+  if(checked) {
+    PathButton* btn = static_cast<PathButton*>(sender());
+    scrollArea_->ensureWidgetVisible(btn, 0); // make the button visible
+
+    currentPath_ = btn->pathElement();
+    // qDebug("chdir: %s", currentPath_.displayName(false));
+    Q_EMIT chdir(currentPath_.dataPtr());
+  }
+}
+
+
+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(Path path) {
+  if(!currentPath_.isNull() && !path.isNull() && currentPath_ == path) // same path, do nothing
+    return;
+
+  currentPath_ = path;
+  int buttonCount = buttonsLayout_->count() - 1; // the last item is a spacer
+  // check if we already have a button for this path (FIXME: this loop is inefficient)
+  for(int i = buttonCount - 1; i >= 0; --i) {
+    PathButton* btn = static_cast<PathButton*>(buttonsLayout_->itemAt(i)->widget());
+    if(btn->pathElement() == path) {  // we have a button for this path
+      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. */
+
+  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;
+  }
+
+  Path pathElement = path;
+  // create new buttons for the new path
+  while(!pathElement.isNull()) {
+    // qDebug("%s", pathElement.displayName(false));
+    PathButton* btn = new PathButton(pathElement, buttonsWidget_);
+    btn->show();
+    connect(btn, &QPushButton::toggled, this, &PathBar::onButtonToggled);
+    pathElement = pathElement.getParent();
+    buttonsLayout_->insertWidget(0, btn);
+  }
+  buttonCount = buttonsLayout_->count();
+  if(buttonCount) {
+    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);
+  }
+  buttonsLayout_->addStretch(1);
+
+  // 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();
+  setUpdatesEnabled(true);
+}
+
+void PathBar::openEditor() {
+  if(tempPathEdit_ == nullptr) {
+    tempPathEdit_ = new PathEdit(this);
+    layout()->replaceWidget(scrollArea_, tempPathEdit_, Qt::FindDirectChildrenOnly);
+    scrollArea_->hide();
+    scrollToStart_->setVisible(false);
+    scrollToEnd_->setVisible(false);
+    char* pathStr = currentPath_.toStr();
+    tempPathEdit_->setText(pathStr);
+    g_free(pathStr);
+
+    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);
+  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() {
+  char* pathStr = currentPath_.toStr();
+  QApplication::clipboard()->setText(pathStr);
+  g_free(pathStr);
+}
+
+void PathBar::onReturnPressed() {
+  QByteArray pathStr = tempPathEdit_->text().toLocal8Bit();
+  Path path = Path::newForDisplayName(pathStr.constData());
+  setPath(path);
+}
+
+void PathBar::setArrowEnabledState(int value) {
+  if(buttonsLayout_->sizeHint().width() > width()) {
+    QScrollBar* sb = scrollArea_->horizontalScrollBar();
+    scrollToStart_->setEnabled(value != sb->minimum());
+    scrollToEnd_->setEnabled(value != sb->maximum());
+  }
+}
+
+
+} // namespace Fm
diff --git a/src/pathbar.h b/src/pathbar.h
new file mode 100644 (file)
index 0000000..0ef163e
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * 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 "path.h"
+
+class QToolButton;
+class QScrollArea;
+class QPushButton;
+class QHBoxLayout;
+
+namespace Fm {
+
+class PathEdit;
+
+class LIBFM_QT_API PathBar: public QWidget {
+  Q_OBJECT
+public:
+  explicit PathBar(QWidget *parent = 0);
+
+  Path path() {
+    return currentPath_;
+  }
+
+  void setPath(Path path);
+
+Q_SIGNALS:
+  void chdir(FmPath* path);
+  void middleClickChdir(FmPath* 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();
+
+protected:
+  void resizeEvent(QResizeEvent* event);
+  void wheelEvent (QWheelEvent* event);
+  void mousePressEvent(QMouseEvent *event);
+  void contextMenuEvent(QContextMenuEvent *event);
+
+private:
+  void updateScrollButtonVisibility();
+
+private:
+  QToolButton* scrollToStart_;
+  QToolButton* scrollToEnd_;
+  QScrollArea* scrollArea_;
+  QWidget* buttonsWidget_;
+  QHBoxLayout* buttonsLayout_;
+  PathEdit* tempPathEdit_;
+
+  Path currentPath_;   // currently active path
+};
+
+} // namespace Fm
+
+#endif // FM_PATHBAR_H
diff --git a/src/pathbar_p.h b/src/pathbar_p.h
new file mode 100644 (file)
index 0000000..6d2298f
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * 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 "path.h"
+
+namespace Fm {
+
+class PathButton: public QToolButton {
+  Q_OBJECT
+public:
+  PathButton(Fm::Path pathElement, QWidget* parent = nullptr):
+    QToolButton(parent),
+    pathElement_(pathElement) {
+
+      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));
+
+      char* label = pathElement.displayBasename();
+      setText(label);
+      g_free(label);
+
+      if(pathElement.getParent().isNull()) { /* this element is root */
+        QIcon icon = QIcon::fromTheme("drive-harddisk");
+        setIcon(icon);
+      }
+  }
+
+  Path pathElement() {
+    return pathElement_;
+  }
+
+  void setPathElement(Path pathElement) {
+    pathElement_ = pathElement;
+  }
+
+  void changeEvent(QEvent* event) override {
+    QToolButton::changeEvent(event);
+    if(event->type() == QEvent::StyleChange) {
+      int icnSize = style()->pixelMetric(QStyle::PM_ToolBarIconSize);
+      setIconSize(QSize(icnSize, icnSize));
+    }
+  }
+
+private:
+  Path pathElement_;
+};
+
+} // namespace Fm
+
+#endif // FM_PATHBAR_P_H
diff --git a/src/pathedit.cpp b/src/pathedit.cpp
new file mode 100644 (file)
index 0000000..50b866d
--- /dev/null
@@ -0,0 +1,218 @@
+/*
+ * 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 <libfm/fm.h>
+
+namespace Fm {
+
+void PathEditJob::runJob() {
+  GError *err = NULL;
+  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 = NULL;
+        }
+        else /* EOF */
+          break;
+      }
+    }
+    g_file_enumerator_close(enu, cancellable, NULL);
+    g_object_unref(enu);
+  }
+  // finished! let's update the UI in the main thread
+  Q_EMIT finished();
+}
+
+
+PathEdit::PathEdit(QWidget* parent):
+  QLineEdit(parent),
+  completer_(new QCompleter()),
+  model_(new QStringListModel()),
+  cancellable_(NULL) {
+  setCompleter(completer_);
+  completer_->setModel(model_);
+  connect(this, &PathEdit::textChanged, this, &PathEdit::onTextChanged);
+}
+
+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::onTextChanged(const QString& text) {
+  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(thread, &QThread::started, job, &PathEditJob::runJob);
+  connect(thread, &QThread::finished, thread, &QObject::deleteLater);
+  connect(thread, &QThread::finished, job, &QObject::deleteLater);
+  connect(job, &PathEditJob::finished, this, &PathEdit::onJobFinished);
+  thread->start(QThread::LowPriority);
+}
+
+void PathEdit::freeCompleter() {
+  if(cancellable_) {
+    g_cancellable_cancel(cancellable_);
+    g_object_unref(cancellable_);
+    cancellable_ = NULL;
+  }
+  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_ = NULL;
+  }
+}
+
+} // namespace Fm
diff --git a/src/pathedit.h b/src/pathedit.h
new file mode 100644 (file)
index 0000000..950bcf6
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * 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);
+
+private:
+  void autoComplete();
+  void reloadCompleter(bool triggeredByFocusInEvent = false);
+  void freeCompleter();
+  void onJobFinished();
+
+private:
+  QCompleter* completer_;
+  QStringListModel* model_;
+  QString currentPrefix_;
+  GCancellable* cancellable_;
+};
+
+}
+
+#endif // FM_PATHEDIT_H
diff --git a/src/pathedit_p.h b/src/pathedit_p.h
new file mode 100644 (file)
index 0000000..b91629c
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * 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
diff --git a/src/placesmodel.cpp b/src/placesmodel.cpp
new file mode 100644 (file)
index 0000000..d5b0ac7
--- /dev/null
@@ -0,0 +1,624 @@
+/*
+ * 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 "utilities.h"
+#include "placesmodelitem.h"
+
+namespace Fm {
+
+PlacesModel::PlacesModel(QObject* parent):
+  QStandardItemModel(parent),
+  showApplications_(true),
+  showDesktop_(true),
+  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_path_get_home());
+  placesRoot->appendRow(homeItem);
+
+  desktopItem = new PlacesModelItem("user-desktop", tr("Desktop"), fm_path_get_desktop());
+  placesRoot->appendRow(desktopItem);
+
+  createTrashItem();
+
+  FmPath* path;
+  // FIXME: add an option to hide network:///
+  if(true) {
+    path = fm_path_new_for_uri("computer:///");
+    computerItem = new PlacesModelItem("computer", tr("Computer"), path);
+    fm_path_unref(path);
+    placesRoot->appendRow(computerItem);
+  }
+  else
+    computerItem = NULL;
+
+  // 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.
+  GIcon* gicon = g_themed_icon_new_from_names((char**)applicaion_icon_names, G_N_ELEMENTS(applicaion_icon_names));
+  FmIcon* fmicon = fm_icon_from_gicon(gicon);
+  g_object_unref(gicon);
+  applicationsItem = new PlacesModelItem(fmicon, tr("Applications"), fm_path_get_apps_menu());
+  fm_icon_unref(fmicon);
+  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.
+    gicon = g_themed_icon_new_from_names((char**)network_icon_names, G_N_ELEMENTS(network_icon_names));
+    fmicon = fm_icon_from_gicon(gicon);
+    g_object_unref(gicon);
+    path = fm_path_new_for_uri("network:///");
+    networkItem = new PlacesModelItem(fmicon, tr("Network"), path);
+    fm_icon_unref(fmicon);
+    fm_path_unref(path);
+    placesRoot->appendRow(networkItem);
+  }
+  else
+    networkItem = NULL;
+
+  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_dup();
+  loadBookmarks();
+  g_signal_connect(bookmarks, "changed", G_CALLBACK(onBookmarksChanged), this);
+
+  // update some icons when the icon theme is changed
+  connect(IconTheme::instance(), &IconTheme::changed, this, &PlacesModel::updateIcons);
+}
+
+void PlacesModel::loadBookmarks() {
+  GList* allBookmarks = fm_bookmarks_get_all(bookmarks);
+  for(GList* l = allBookmarks; l; l = l->next) {
+    FmBookmarkItem* bm_item = (FmBookmarkItem*)l->data;
+    PlacesModelBookmarkItem* item = new PlacesModelBookmarkItem(bm_item);
+    bookmarksRoot->appendRow(item);
+  }
+  g_list_free_full(allBookmarks, (GDestroyNotify)fm_bookmark_item_unref);
+}
+
+PlacesModel::~PlacesModel() {
+  if(bookmarks) {
+    g_signal_handlers_disconnect_by_func(bookmarks, (gpointer)onBookmarksChanged, this);
+    g_object_unref(bookmarks);
+  }
+  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, NULL,
+                            [](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
+            GFileInfo* inf = g_file_query_info_finish(data->gf, res, NULL);
+            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, G_FILE_ATTRIBUTE_TRASH_ITEM_COUNT);
+                const char* icon_name = n > 0 ? "user-trash-full" : "user-trash";
+                FmIcon* icon = fm_icon_from_name(icon_name);
+                _this->trashItem_->setIcon(icon);
+                fm_icon_unref(icon);
+              }
+              g_object_unref(inf);
+            }
+        }
+        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, NULL)) {
+    g_object_unref(gf);
+    trashItem_ = NULL;
+    trashMonitor_ = NULL;
+    return;
+  }
+  trashItem_ = new PlacesModelItem("user-trash", tr("Trash"), fm_path_get_trash());
+
+  trashMonitor_ = fm_monitor_directory(gf, NULL);
+  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_ = NULL;
+      }
+      placesRoot->removeRow(trashItem_->row()); // delete trashItem_;
+      trashItem_ = NULL;
+    }
+  }
+}
+
+PlacesModelItem* PlacesModel::itemFromPath(FmPath* 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, FmPath* path) {
+  int rowCount = rootItem->rowCount();
+  for(int i = 0; i < rowCount; ++i) {
+    PlacesModelItem* item = static_cast<PlacesModelItem*>(rootItem->child(i, 0));
+    if(fm_path_equal(item->path(), path))
+      return item;
+  }
+  return NULL;
+}
+
+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 NULL;
+}
+
+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 NULL;
+}
+
+PlacesModelBookmarkItem* PlacesModel::itemFromBookmark(FmBookmarkItem* 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 NULL;
+}
+
+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.
+      GFile* gf = g_mount_get_root(mount);
+      FmPath* path = fm_path_new_for_gfile(gf);
+      g_object_unref(gf);
+      item->setPath(path);
+      if(path)
+        fm_path_unref(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(FmBookmarks* bookmarks, PlacesModel* pThis) {
+  // remove all items
+  pThis->bookmarksRoot->removeRows(0, pThis->bookmarksRoot->rowCount());
+  pThis->loadBookmarks();
+}
+
+void PlacesModel::updateIcons() {
+  // the icon theme is changed and we need to update the icons
+  PlacesModelItem* item;
+  int row;
+  int n = placesRoot->rowCount();
+  for(row = 0; row < n; ++row) {
+    item = static_cast<PlacesModelItem*>(placesRoot->child(row));
+    item->updateIcon();
+  }
+  n = devicesRoot->rowCount();
+  for(row = 0; row < n; ++row) {
+    item = static_cast<PlacesModelItem*>(devicesRoot->child(row));
+    item->updateIcon();
+  }
+}
+
+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<void*>(item->fileInfo());
+      case FmIconRole:
+        return QVariant::fromValue<void*>(item->icon());
+      }
+    }
+  }
+  return QStandardItemModel::data(index, role);
+}
+
+
+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 = NULL;
+    stream >> oldPos >> pathStr;
+    // find the source bookmark item being dragged
+    GList* allBookmarks = fm_bookmarks_get_all(bookmarks);
+    FmBookmarkItem* draggedItem = static_cast<FmBookmarkItem*>(g_list_nth_data(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.
+    if(!draggedItem || !fm_path_equal_str(draggedItem->path, pathStr, -1)) {
+      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
+      fm_bookmarks_reorder(bookmarks, 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
+        FmPathList* paths = pathListFromQUrls(data->urls());
+        for(GList* l = fm_path_list_peek_head_link(paths); l; l = l->next) {
+          FmPath* path = FM_PATH(l->data);
+          GFile* gf = fm_path_to_gfile(path);
+          // FIXME: this is a blocking call
+          if(g_file_query_file_type(gf, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
+                                    NULL) == G_FILE_TYPE_DIRECTORY) {
+            char* disp_name = fm_path_display_basename(path);
+            fm_bookmarks_insert(bookmarks, path, disp_name, row);
+            g_free(disp_name);
+          }
+          g_object_unref(gf);
+          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.
+      char* pathStr = fm_path_to_str(bookmarkItem->path());
+      stream << index.row() << pathStr;
+      g_free(pathStr);
+      mime->setData("application/x-bookmark-row", data);
+      return mime;
+    }
+  }
+  return NULL;
+}
+
+QStringList PlacesModel::mimeTypes() const {
+  return QStringList() << "application/x-bookmark-row" << "text/uri-list";
+}
+
+Qt::DropActions PlacesModel::supportedDropActions() const {
+  return QStandardItemModel::supportedDropActions();
+}
+
+
+} // namespace Fm
diff --git a/src/placesmodel.h b/src/placesmodel.h
new file mode 100644 (file)
index 0000000..4aba709
--- /dev/null
@@ -0,0 +1,140 @@
+/*
+ * 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>
+
+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:
+    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_ != NULL;
+  }
+  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;
+
+public Q_SLOTS:
+  void updateIcons();
+  void updateTrash();
+
+protected:
+
+  PlacesModelItem* itemFromPath(FmPath* path);
+  PlacesModelItem* itemFromPath(QStandardItem* rootItem, FmPath* path);
+  PlacesModelVolumeItem* itemFromVolume(GVolume* volume);
+  PlacesModelMountItem* itemFromMount(GMount* mount);
+  PlacesModelBookmarkItem* itemFromBookmark(FmBookmarkItem* 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 onBookmarksChanged(FmBookmarks* bookmarks, PlacesModel* pThis);
+
+  static void onTrashChanged(GFileMonitor *monitor, GFile *gf, GFile *other, GFileMonitorEvent evt, PlacesModel* pThis);
+
+private:
+  FmBookmarks* 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_;
+};
+
+}
+
+#endif // FM_PLACESMODEL_H
diff --git a/src/placesmodelitem.cpp b/src/placesmodelitem.cpp
new file mode 100644 (file)
index 0000000..1de2471
--- /dev/null
@@ -0,0 +1,191 @@
+/*
+ * 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(),
+  path_(NULL),
+  fileInfo_(NULL),
+  icon_(NULL) {
+}
+
+PlacesModelItem::PlacesModelItem(const char* iconName, QString title, FmPath* path):
+  QStandardItem(title),
+  path_(path ? fm_path_ref(path) : NULL),
+  fileInfo_(NULL),
+  icon_(fm_icon_from_name(iconName)) {
+  if(icon_)
+    QStandardItem::setIcon(IconTheme::icon(icon_));
+  setEditable(false);
+}
+
+PlacesModelItem::PlacesModelItem(FmIcon* icon, QString title, FmPath* path):
+  QStandardItem(title),
+  path_(path ? fm_path_ref(path) : NULL),
+  fileInfo_(NULL),
+  icon_(icon ? fm_icon_ref(icon) : NULL) {
+  if(icon_)
+    QStandardItem::setIcon(IconTheme::icon(icon));
+  setEditable(false);
+}
+
+PlacesModelItem::PlacesModelItem(QIcon icon, QString title, FmPath* path):
+  QStandardItem(icon, title),
+  path_(path ? fm_path_ref(path) : NULL),
+  fileInfo_(NULL),
+  icon_(NULL) {
+  setEditable(false);
+}
+
+PlacesModelItem::~PlacesModelItem() {
+  if(path_)
+    fm_path_unref(path_);
+  if(fileInfo_)
+    g_object_unref(fileInfo_);
+  if(icon_)
+    fm_icon_unref(icon_);
+}
+
+void PlacesModelItem::setPath(FmPath* path) {
+  if(path_)
+    fm_path_unref(path_);
+  path_ = path ? fm_path_ref(path) : NULL;
+}
+
+void PlacesModelItem::setIcon(FmIcon* icon) {
+  if(icon_)
+    fm_icon_unref(icon_);
+  if(icon) {
+    icon_ = fm_icon_ref(icon);
+    QStandardItem::setIcon(IconTheme::icon(icon_));
+  }
+  else {
+    icon_ = NULL;
+    QStandardItem::setIcon(QIcon());
+  }
+}
+
+void PlacesModelItem::setIcon(GIcon* gicon) {
+  FmIcon* icon = gicon ? fm_icon_from_gicon(gicon) : NULL;
+  setIcon(icon);
+  fm_icon_unref(icon);
+}
+
+void PlacesModelItem::updateIcon() {
+  if(icon_)
+    QStandardItem::setIcon(IconTheme::icon(icon_));
+}
+
+QVariant PlacesModelItem::data(int role) const {
+  // we use a QPixmap from FmIcon cache rather than QIcon object for decoration role.
+  return QStandardItem::data(role);
+}
+
+void PlacesModelItem::setFileInfo(FmFileInfo* fileInfo) {
+  // FIXME: how can we correctly update icon?
+  if(fileInfo_)
+    fm_file_info_unref(fileInfo_);
+
+  if(fileInfo) {
+    fileInfo_ = fm_file_info_ref(fileInfo);
+  }
+  else
+    fileInfo_ = NULL;
+}
+
+PlacesModelBookmarkItem::PlacesModelBookmarkItem(FmBookmarkItem* bm_item):
+  PlacesModelItem(QIcon::fromTheme("folder"), QString::fromUtf8(bm_item->name), bm_item->path),
+  bookmarkItem_(fm_bookmark_item_ref(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
+  GIcon* gicon = g_volume_get_icon(volume_);
+  setIcon(gicon);
+  g_object_unref(gicon);
+
+  // set dir path
+  GMount* mount = g_volume_get_mount(volume_);
+  if(mount) {
+    GFile* mount_root = g_mount_get_root(mount);
+    FmPath* mount_path = fm_path_new_for_gfile(mount_root);
+    setPath(mount_path);
+    fm_path_unref(mount_path);
+    g_object_unref(mount_root);
+    g_object_unref(mount);
+  }
+  else {
+    setPath(NULL);
+  }
+}
+
+
+bool PlacesModelVolumeItem::isMounted() {
+  GMount* mount = g_volume_get_mount(volume_);
+  if(mount)
+    g_object_unref(mount);
+  return mount != NULL ? 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
+  GFile* mount_root = g_mount_get_root(mount_);
+  FmPath* mount_path = fm_path_new_for_gfile(mount_root);
+  setPath(mount_path);
+  fm_path_unref(mount_path);
+  g_object_unref(mount_root);
+
+  // set icon
+  GIcon* gicon = g_mount_get_icon(mount_);
+  setIcon(gicon);
+  g_object_unref(gicon);
+}
+
+}
diff --git a/src/placesmodelitem.h b/src/placesmodelitem.h
new file mode 100644 (file)
index 0000000..d788bbb
--- /dev/null
@@ -0,0 +1,130 @@
+/*
+ * 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>
+
+namespace Fm {
+
+// model item
+class LIBFM_QT_API PlacesModelItem : public QStandardItem {
+public:
+  enum Type {
+    Places = QStandardItem::UserType + 1,
+    Volume,
+    Mount,
+    Bookmark
+  };
+
+public:
+  PlacesModelItem();
+  PlacesModelItem(QIcon icon, QString title, FmPath* path = NULL);
+  PlacesModelItem(const char* iconName, QString title, FmPath* path = NULL);
+  PlacesModelItem(FmIcon* icon, QString title, FmPath* path = NULL);
+  ~PlacesModelItem();
+
+  FmFileInfo* fileInfo() {
+    return fileInfo_;
+  }
+  void setFileInfo(FmFileInfo* fileInfo);
+
+  FmPath* path() {
+    return path_;
+  }
+  void setPath(FmPath* path);
+
+  FmIcon* icon() {
+    return icon_;
+  }
+  void setIcon(FmIcon* icon);
+  void setIcon(GIcon* gicon);
+  void updateIcon();
+
+  QVariant data(int role = Qt::UserRole + 1) const;
+
+  virtual int type() const {
+    return Places;
+  }
+
+private:
+  FmPath* path_;
+  FmFileInfo* fileInfo_;
+  FmIcon* 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(FmBookmarkItem* bm_item);
+  virtual ~PlacesModelBookmarkItem() {
+    if(bookmarkItem_)
+      fm_bookmark_item_unref(bookmarkItem_);
+  }
+  FmBookmarkItem* bookmark() const {
+    return bookmarkItem_;
+  }
+private:
+  FmBookmarkItem* bookmarkItem_;
+};
+
+}
+
+#endif // FM_PLACESMODELITEM_H
diff --git a/src/placesview.cpp b/src/placesview.cpp
new file mode 100644 (file)
index 0000000..d38abe9
--- /dev/null
@@ -0,0 +1,431 @@
+/*
+ * 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),
+  currentPath_(NULL) {
+  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->setFmIconRole(PlacesModel::FmIconRole);
+  setItemDelegateForColumn(0, delegate);
+
+  // FIXME: we may share this model amont all views
+  model_ = new PlacesModel(this);
+  setModel(model_);
+
+  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() {
+  if(currentPath_)
+    fm_path_unref(currentPath_);
+  // 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) {
+    FmPath* 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(FmPath* path) {
+  if(currentPath_)
+    fm_path_unref(currentPath_);
+  if(path) {
+    currentPath_ = fm_path_ref(path);
+    // TODO: search for item with the path in model_ and select it.
+    PlacesModelItem* item = model_->itemFromPath(currentPath_);
+    if(item) {
+      selectionModel()->select(item->index(), QItemSelectionModel::SelectCurrent|QItemSelectionModel::Rows);
+    }
+    else
+      clearSelection();
+  }
+  else {
+    currentPath_ = NULL;
+    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() {
+  FmPathList* files = fm_path_list_new();
+  fm_path_list_push_tail(files, fm_path_get_trash());
+  Fm::FileOperation::deleteFiles(files);
+  fm_path_list_unref(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) {
+    FmBookmarkItem* bookmarkItem = item->bookmark();
+    FmBookmarks* bookmarks = fm_bookmarks_dup();
+    fm_bookmarks_reorder(bookmarks, bookmarkItem, row - 1);
+    g_object_unref(bookmarks);
+  }
+}
+
+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()) {
+    FmBookmarkItem* bookmarkItem = item->bookmark();
+    FmBookmarks* bookmarks = fm_bookmarks_dup();
+    fm_bookmarks_reorder(bookmarks, bookmarkItem, row + 1);
+    g_object_unref(bookmarks);
+  }
+}
+
+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()));
+  FmBookmarkItem* bookmarkItem = item->bookmark();
+  FmBookmarks* bookmarks = fm_bookmarks_dup();
+  fm_bookmarks_remove(bookmarks, bookmarkItem);
+  g_object_unref(bookmarks);
+}
+
+// virtual
+void PlacesView::commitData(QWidget * editor) {
+  QTreeView::commitData(editor);
+  PlacesModelBookmarkItem* item = static_cast<PlacesModelBookmarkItem*>(model_->itemFromIndex(currentIndex()));
+  FmBookmarkItem* bookmarkItem = item->bookmark();
+  FmBookmarks* bookmarks = fm_bookmarks_dup();
+  // rename bookmark
+  fm_bookmarks_rename(bookmarks, bookmarkItem, item->text().toUtf8().constData());
+  g_object_unref(bookmarks);
+}
+
+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: {
+        FmPath* path = item->path();
+        if(path && fm_path_equal(fm_path_get_trash(), path)) {
+          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
diff --git a/src/placesview.h b/src/placesview.h
new file mode 100644 (file)
index 0000000..902ac3d
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+ * 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>
+
+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(FmPath* path);
+  FmPath* currentPath() {
+    return currentPath_;
+  }
+
+  // libfm-gtk compatible alias
+  FmPath* getCwd() {
+    return currentPath();
+  }
+
+  void chdir(FmPath* path) {
+    setCurrentPath(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, FmPath* 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:
+  PlacesModel* model_;
+  FmPath* currentPath_;
+};
+
+}
+
+#endif // FM_PLACESVIEW_H
diff --git a/src/proxyfoldermodel.cpp b/src/proxyfoldermodel.cpp
new file mode 100644 (file)
index 0000000..54d1a19
--- /dev/null
@@ -0,0 +1,281 @@
+/*
+ * 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);
+}
+
+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) {
+    // 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();
+  }
+}
+
+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());
+    FmFileInfo* 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) {
+    FmFileInfo* leftInfo = srcModel->fileInfoFromIndex(left);
+    FmFileInfo* rightInfo = srcModel->fileInfoFromIndex(right);
+
+    if(Q_UNLIKELY(!leftInfo || !rightInfo)) {
+      // In theory, this should not happen, but it's safer to add the null check.
+      // This is reported in https://github.com/lxde/pcmanfm-qt/issues/205
+      return false;
+    }
+
+    if(folderFirst_) {
+      bool leftIsFolder = (bool)fm_file_info_is_dir(leftInfo);
+      bool rightIsFolder = (bool)fm_file_info_is_dir(rightInfo);
+      if(leftIsFolder != rightIsFolder)
+        return sortOrder() == Qt::AscendingOrder ? leftIsFolder : rightIsFolder;
+    }
+
+    switch(sortColumn()) {
+      case FolderModel::ColumnFileName:
+        if(sortCaseSensitivity() == Qt::CaseSensitive) {
+          // fm_file_info_get_collate_key_nocasefold() uses g_utf8_casefold() from glib internally, which
+          // is only an approximation not working correctly in some locales.
+          // FIXME: we may use QCollator (since Qt 5.2) for this, but the performance impact is unknown
+          return strcmp(fm_file_info_get_collate_key_nocasefold(leftInfo), fm_file_info_get_collate_key_nocasefold(rightInfo)) < 0;
+          /*
+          QCollator coll;
+          coll.setCaseSensitivity(Qt::CaseSensitive);
+          coll.setIgnorePunctuation(false);
+          coll.setNumericMode(true);
+          return coll.compare(QString::fromUtf8(fm_file_info_get_disp_name(leftInfo)), QString::fromUtf8(fm_file_info_get_disp_name(rightInfo))) < 0;
+          */
+        }
+        else {
+          // linguistic case insensitive ordering
+          return strcmp(fm_file_info_get_collate_key(leftInfo), fm_file_info_get_collate_key(rightInfo)) < 0;
+        }
+      case FolderModel::ColumnFileMTime:
+        return fm_file_info_get_mtime(leftInfo) < fm_file_info_get_mtime(rightInfo);
+      case FolderModel::ColumnFileSize:
+        return fm_file_info_get_size(leftInfo) < fm_file_info_get_size(rightInfo);
+      case FolderModel::ColumnFileOwner:
+        // TODO: sort by owner
+        break;
+      case FolderModel::ColumnFileType:
+        break;
+    }
+  }
+  return QSortFilterProxyModel::lessThan(left, right);
+}
+
+FmFileInfo* ProxyFolderModel::fileInfoFromIndex(const QModelIndex& index) const {
+  FolderModel* srcModel = static_cast<FolderModel*>(sourceModel());
+  if(srcModel) {
+    QModelIndex srcIndex = mapToSource(index);
+    return srcModel->fileInfoFromIndex(srcIndex);
+  }
+  return NULL;
+}
+
+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().constData());
+
+  if(size == thumbnailSize_) { // if a thumbnail of the size we want is loaded
+    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
diff --git a/src/proxyfoldermodel.h b/src/proxyfoldermodel.h
new file mode 100644 (file)
index 0000000..ed4dec3
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+ * 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>
+
+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, FmFileInfo* 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) {
+    QSortFilterProxyModel::setSortCaseSensitivity(cs);
+    Q_EMIT sortFilterChanged();
+  }
+
+  bool showThumbnails() {
+    return showThumbnails_;
+  }
+  void setShowThumbnails(bool show);
+
+  int thumbnailSize() {
+    return thumbnailSize_;
+  }
+  void setThumbnailSize(int size);
+
+  FmFileInfo* fileInfoFromIndex(const QModelIndex& index) 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:
+
+private:
+  bool showHidden_;
+  bool folderFirst_;
+  bool showThumbnails_;
+  int thumbnailSize_;
+  QList<ProxyFolderModelFilter*> filters_;
+};
+
+}
+
+#endif // FM_PROXYFOLDERMODEL_H
diff --git a/src/rename-dialog.ui b/src/rename-dialog.ui
new file mode 100644 (file)
index 0000000..2b0c123
--- /dev/null
@@ -0,0 +1,204 @@
+<?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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;There is already a file with the same name in this location.&lt;/span&gt;&lt;/p&gt;&lt;p&gt;Do you want to replace the existing file?&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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>&amp;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>
diff --git a/src/renamedialog.cpp b/src/renamedialog.cpp
new file mode 100644 (file)
index 0000000..054752b
--- /dev/null
@@ -0,0 +1,140 @@
+/*
+ * 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 "icontheme.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 = IconTheme::icon(srcIcon);
+  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 = IconTheme::icon(destIcon);
+  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
diff --git a/src/renamedialog.h b/src/renamedialog.h
new file mode 100644 (file)
index 0000000..f979584
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * 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
diff --git a/src/sidepane.cpp b/src/sidepane.cpp
new file mode 100644 (file)
index 0000000..959245d
--- /dev/null
@@ -0,0 +1,247 @@
+/*
+ * 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),
+  currentPath_(NULL),
+  view_(NULL),
+  combo_(NULL),
+  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() {
+  if(currentPath_)
+    fm_path_unref(currentPath_);
+  // qDebug("delete SidePane");
+}
+
+void SidePane::onPlacesViewChdirRequested(int type, FmPath* path) {
+  Q_EMIT chdirRequested(type, path);
+}
+
+void SidePane::onDirTreeViewChdirRequested(int type, FmPath* path) {
+  Q_EMIT chdirRequested(type, path);
+}
+
+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(FmPath* path) {
+  Q_ASSERT(path != NULL);
+  if(currentPath_)
+    fm_path_unref(currentPath_);
+  currentPath_ = fm_path_ref(path);
+  switch(mode_) {
+    case ModePlaces:
+      static_cast<PlacesView*>(view_)->setCurrentPath(path);
+      break;
+    case ModeDirTree:
+      static_cast<DirTreeView*>(view_)->setCurrentPath(path);
+      break;
+    default:;
+  }
+}
+
+SidePane::Mode SidePane::modeByName(const char* str) {
+  if(str == NULL)
+    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 NULL;
+  }
+}
+
+#if 0 // FIXME: are these APIs from libfm-qt needed?
+
+QString SidePane::modeLabel(SidePane::Mode mode) {
+  switch(mode) {
+  case ModePlaces:
+    return tr("Places");
+  case ModeDirTree:
+    return tr("Directory Tree");
+  }
+  return QString();
+}
+
+QString SidePane::modeTooltip(SidePane::Mode mode) {
+  switch(mode) {
+  case ModePlaces:
+    return tr("Shows list of common places, devices, and bookmarks in sidebar");
+  case ModeDirTree:
+    return tr("Shows tree of directories in sidebar");
+  }
+  return QString();
+}
+#endif
+
+bool SidePane::setHomeDir(const char* home_dir) {
+  if(view_ == NULL)
+    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() {
+  // TODO
+  DirTreeModel* model = new DirTreeModel(view_);
+  FmFileInfoJob* job = fm_file_info_job_new(NULL, FM_FILE_INFO_JOB_NONE);
+  model->setShowHidden(showHidden_);
+
+  GList* l;
+  /* query FmFileInfo for home dir and root dir, and then,
+    * add them to dir tree model */
+  fm_file_info_job_add(job, fm_path_get_home());
+  fm_file_info_job_add(job, fm_path_get_root());
+  /* FIXME: maybe it's cleaner to use run_async here? */
+  fm_job_run_sync_with_mainloop(FM_JOB(job));
+  for(l = fm_file_info_list_peek_head_link(job->file_infos); l; l = l->next) {
+      FmFileInfo* fi = FM_FILE_INFO(l->data);
+      model->addRoot(fi);
+  }
+  g_object_unref(job);
+
+  static_cast<DirTreeView*>(view_)->setModel(model);
+}
+
+void SidePane::setMode(Mode mode) {
+  if(mode == mode_)
+    return;
+
+  if(view_) {
+    delete view_;
+    view_ = NULL;
+    //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::onPlacesViewChdirRequested);
+    break;
+  }
+  case ModeDirTree: {
+    DirTreeView* dirTreeView = new Fm::DirTreeView(this);
+    view_ = dirTreeView;
+    initDirTree();
+    dirTreeView->setIconSize(iconSize_);
+    dirTreeView->setCurrentPath(currentPath_);
+    connect(dirTreeView, &DirTreeView::chdirRequested, this, &SidePane::onDirTreeViewChdirRequested);
+    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_ == NULL || 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
diff --git a/src/sidepane.h b/src/sidepane.h
new file mode 100644 (file)
index 0000000..3657413
--- /dev/null
@@ -0,0 +1,134 @@
+/*
+ * 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>
+
+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() {
+    return iconSize_;
+  }
+
+  void setIconSize(QSize size);
+
+  FmPath* currentPath() {
+    return currentPath_;
+  }
+
+  void setCurrentPath(FmPath* path);
+
+  void setMode(Mode mode);
+
+  Mode mode() {
+    return mode_;
+  }
+
+  QWidget* view() {
+    return view_;
+  }
+
+  const char *modeName(Mode mode);
+
+  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() {
+    return showHidden_;
+  }
+
+  bool setHomeDir(const char *home_dir);
+
+  // libfm-gtk compatible alias
+  FmPath* getCwd() {
+    return currentPath();
+  }
+
+  void chdir(FmPath* path) {
+    setCurrentPath(path);
+  }
+
+Q_SIGNALS:
+  void chdirRequested(int type, FmPath* path);
+  void openFolderInNewWindowRequested(FmPath* path);
+  void openFolderInNewTabRequested(FmPath* path);
+  void openFolderInTerminalRequested(FmPath* path);
+  void createNewFolderRequested(FmPath* path);
+  void modeChanged(Fm::SidePane::Mode mode);
+
+  void prepareFileMenu(Fm::FileMenu* menu); // emit before showing a Fm::FileMenu
+
+protected Q_SLOTS:
+  void onPlacesViewChdirRequested(int type, FmPath* path);
+  void onDirTreeViewChdirRequested(int type, FmPath* path);
+  void onComboCurrentIndexChanged(int current);
+
+private:
+  void initDirTree();
+
+private:
+  FmPath* currentPath_;
+  QWidget* view_;
+  QComboBox* combo_;
+  QVBoxLayout* verticalLayout;
+  QSize iconSize_;
+  Mode mode_;
+  bool showHidden_;
+};
+
+}
+
+#endif // FM_SIDEPANE_H
diff --git a/src/templates.h b/src/templates.h
new file mode 100644 (file)
index 0000000..fa55979
--- /dev/null
@@ -0,0 +1,158 @@
+/*
+ * 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) {
+    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) {
+    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__
diff --git a/src/terminal.h b/src/terminal.h
new file mode 100644 (file)
index 0000000..cd7a31c
--- /dev/null
@@ -0,0 +1,143 @@
+/*
+ * 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_TERMINAL_H__
+#define __LIBFM_QT_FM_TERMINAL_H__
+
+#include <libfm/fm.h>
+#include <QObject>
+#include <QtGlobal>
+#include "libfmqtglobals.h"
+
+
+namespace Fm {
+
+
+class LIBFM_QT_API Terminal {
+public:
+
+
+  // default constructor
+  Terminal() {
+    dataPtr_ = nullptr;
+  }
+
+
+  Terminal(FmTerminal* dataPtr){
+    dataPtr_ = dataPtr != nullptr ? reinterpret_cast<GObject*>(g_object_ref(dataPtr)) : nullptr;
+  }
+
+
+  // copy constructor
+  Terminal(const Terminal& other) {
+    dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<GObject*>(g_object_ref(other.dataPtr_)) : nullptr;
+  }
+
+
+  // move constructor
+  Terminal(Terminal&& other) {
+    dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
+  }
+
+
+  // destructor
+  virtual ~Terminal() {
+    if(dataPtr_ != nullptr) {
+      g_object_unref(dataPtr_);
+    }
+  }
+
+
+  // create a wrapper for the data pointer without increasing the reference count
+  static Terminal wrapPtr(FmTerminal* dataPtr) {
+    Terminal obj;
+    obj.dataPtr_ = reinterpret_cast<GObject*>(dataPtr);
+    return obj;
+  }
+
+  // disown the managed data pointer
+  FmTerminal* takeDataPtr() {
+    FmTerminal* data = reinterpret_cast<FmTerminal*>(dataPtr_);
+    dataPtr_ = nullptr;
+    return data;
+  }
+
+  // get the raw pointer wrapped
+  FmTerminal* dataPtr() {
+    return reinterpret_cast<FmTerminal*>(dataPtr_);
+  }
+
+  // automatic type casting
+  operator FmTerminal*() {
+    return dataPtr();
+  }
+
+  // automatic type casting
+  operator void*() {
+    return dataPtr();
+  }
+
+
+  // copy assignment
+  Terminal& operator=(const Terminal& 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
+  Terminal& operator=(Terminal&& other) {
+    dataPtr_ = reinterpret_cast<GObject*>(other.takeDataPtr());
+    return *this;
+  }
+
+  bool isNull() {
+    return (dataPtr_ == nullptr);
+  }
+
+  // methods
+
+  static bool launch(const gchar* dir, GError** error) {
+    return fm_terminal_launch(dir, error);
+  }
+
+
+  static Terminal dupDefault(GError** error) {
+    return Terminal::wrapPtr(fm_terminal_dup_default(error));
+  }
+
+
+  // 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_TERMINAL_H__
diff --git a/src/thumbnailer.h b/src/thumbnailer.h
new file mode 100644 (file)
index 0000000..bbf3db2
--- /dev/null
@@ -0,0 +1,158 @@
+/*
+ * 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_THUMBNAILER_H__
+#define __LIBFM_QT_FM_THUMBNAILER_H__
+
+#include <libfm/fm.h>
+#include <QObject>
+#include <QtGlobal>
+#include "libfmqtglobals.h"
+
+
+namespace Fm {
+
+
+class LIBFM_QT_API Thumbnailer {
+public:
+
+
+  // default constructor
+  Thumbnailer() {
+    dataPtr_ = nullptr;
+  }
+
+
+  Thumbnailer(FmThumbnailer* dataPtr){
+    dataPtr_ = dataPtr != nullptr ? reinterpret_cast<FmThumbnailer*>(fm_thumbnailer_ref(dataPtr)) : nullptr;
+  }
+
+
+  // copy constructor
+  Thumbnailer(const Thumbnailer& other) {
+    dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<FmThumbnailer*>(fm_thumbnailer_ref(other.dataPtr_)) : nullptr;
+  }
+
+
+  // move constructor
+  Thumbnailer(Thumbnailer&& other) {
+    dataPtr_ = reinterpret_cast<FmThumbnailer*>(other.takeDataPtr());
+  }
+
+
+  // destructor
+  ~Thumbnailer() {
+    if(dataPtr_ != nullptr) {
+      fm_thumbnailer_unref(dataPtr_);
+    }
+  }
+
+
+  // create a wrapper for the data pointer without increasing the reference count
+  static Thumbnailer wrapPtr(FmThumbnailer* dataPtr) {
+    Thumbnailer obj;
+    obj.dataPtr_ = reinterpret_cast<FmThumbnailer*>(dataPtr);
+    return obj;
+  }
+
+  // disown the managed data pointer
+  FmThumbnailer* takeDataPtr() {
+    FmThumbnailer* data = reinterpret_cast<FmThumbnailer*>(dataPtr_);
+    dataPtr_ = nullptr;
+    return data;
+  }
+
+  // get the raw pointer wrapped
+  FmThumbnailer* dataPtr() {
+    return reinterpret_cast<FmThumbnailer*>(dataPtr_);
+  }
+
+  // automatic type casting
+  operator FmThumbnailer*() {
+    return dataPtr();
+  }
+
+  // automatic type casting
+  operator void*() {
+    return dataPtr();
+  }
+
+
+  // copy assignment
+  Thumbnailer& operator=(const Thumbnailer& other) {
+    if(dataPtr_ != nullptr) {
+      fm_thumbnailer_unref(dataPtr_);
+    }
+    dataPtr_ = other.dataPtr_ != nullptr ? reinterpret_cast<FmThumbnailer*>(fm_thumbnailer_ref(other.dataPtr_)) : nullptr;
+    return *this;
+  }
+
+
+  // move assignment
+  Thumbnailer& operator=(Thumbnailer&& other) {
+    dataPtr_ = reinterpret_cast<FmThumbnailer*>(other.takeDataPtr());
+    return *this;
+  }
+
+  bool isNull() {
+    return (dataPtr_ == nullptr);
+  }
+
+  // methods
+
+  static void checkUpdate( ) {
+    fm_thumbnailer_check_update();
+  }
+
+
+  void free(void) {
+    fm_thumbnailer_free(dataPtr());
+  }
+
+
+  bool launchForUri(const char* uri, const char* output_file, guint size) {
+    return fm_thumbnailer_launch_for_uri(dataPtr(), uri, output_file, size);
+  }
+
+
+  GPid launchForUriAsync(const char* uri, const char* output_file, guint size, GError** error) {
+    return fm_thumbnailer_launch_for_uri_async(dataPtr(), uri, output_file, size, error);
+  }
+
+
+  char* commandForUri(const char* uri, const char* output_file, guint size) {
+    return fm_thumbnailer_command_for_uri(dataPtr(), uri, output_file, size);
+  }
+
+
+  static Thumbnailer newFromKeyfile(const char* id, GKeyFile* kf) {
+    return Thumbnailer::wrapPtr(fm_thumbnailer_new_from_keyfile(id, kf));
+  }
+
+
+
+private:
+  FmThumbnailer* dataPtr_; // data pointer for the underlying C struct
+
+};
+
+
+}
+
+#endif // __LIBFM_QT_FM_THUMBNAILER_H__
diff --git a/src/thumbnailloader.cpp b/src/thumbnailloader.cpp
new file mode 100644 (file)
index 0000000..8b46cdb
--- /dev/null
@@ -0,0 +1,199 @@
+/*
+ * 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 "thumbnailloader.h"
+#include <new>
+#include <QByteArray>
+#include <QScopedArrayPointer>
+
+namespace Fm {
+
+// FmQImageWrapper is a GObject used to wrap QImage objects and use in glib-based libfm
+#define FM_TYPE_QIMAGE_WRAPPER              (fm_qimage_wrapper_get_type())
+#define FM_QIMAGE_WRAPPER(obj)              (G_TYPE_CHECK_INSTANCE_CAST((obj),\
+FM_TYPE_QIMAGE_WRAPPER, FmQImageWrapper))
+#define FM_QIMAGE_WRAPPER_CLASS(klass)      (G_TYPE_CHECK_CLASS_CAST((klass),\
+FM_TYPE_QIMAGE_WRAPPER, FmQImageWrapperClass))
+#define FM_IS_QIMAGE_WRAPPER(obj)           (G_TYPE_CHECK_INSTANCE_TYPE((obj),\
+FM_TYPE_QIMAGE_WRAPPER))
+#define FM_IS_QIMAGE_WRAPPER_CLASS(klass)   (G_TYPE_CHECK_CLASS_TYPE((klass),\
+FM_TYPE_QIMAGE_WRAPPER))
+#define FM_QIMAGE_WRAPPER_GET_CLASS(obj)    (G_TYPE_INSTANCE_GET_CLASS((obj),\
+FM_TYPE_QIMAGE_WRAPPER, FmQImageWrapperClass))
+
+typedef struct _FmQImageWrapper         FmQImageWrapper;
+typedef struct _FmQImageWrapperClass        FmQImageWrapperClass;
+
+struct _FmQImageWrapper {
+  GObject parent;
+  QImage image;
+};
+
+struct _FmQImageWrapperClass {
+  GObjectClass parent_class;
+};
+
+GType       fm_qimage_wrapper_get_type(void);
+GObject*    fm_qimage_wrapper_new(void);
+static void fm_qimage_wrapper_finalize(GObject *self);
+
+G_DEFINE_TYPE(FmQImageWrapper, fm_qimage_wrapper, G_TYPE_OBJECT)
+
+static void fm_qimage_wrapper_class_init(FmQImageWrapperClass *klass) {
+  GObjectClass* object_class = G_OBJECT_CLASS(klass);
+  object_class->finalize = fm_qimage_wrapper_finalize;
+}
+
+static void fm_qimage_wrapper_init(FmQImageWrapper *self) {
+  // placement new for QImage
+  new(&self->image) QImage();
+}
+
+static void fm_qimage_wrapper_finalize(GObject *self) {
+  FmQImageWrapper *wrapper = FM_QIMAGE_WRAPPER(self);
+  // placement delete
+  wrapper->image.~QImage();
+}
+
+GObject *fm_qimage_wrapper_new(QImage& image) {
+  FmQImageWrapper *wrapper = (FmQImageWrapper*)g_object_new(FM_TYPE_QIMAGE_WRAPPER, NULL);
+  wrapper->image = image;
+  return (GObject*)wrapper;
+}
+
+ThumbnailLoader* ThumbnailLoader::theThumbnailLoader = NULL;
+bool ThumbnailLoader::localFilesOnly_ = true;
+int ThumbnailLoader::maxThumbnailFileSize_ = 0;
+
+ThumbnailLoader::ThumbnailLoader() {
+  // apply the settings to libfm
+  fm_config->thumbnail_local = localFilesOnly_;
+  fm_config->thumbnail_max = maxThumbnailFileSize_;
+
+  FmThumbnailLoaderBackend qt_backend = {
+    readImageFromFile,
+    readImageFromStream,
+    writeImage,
+    scaleImage,
+    rotateImage,
+    getImageWidth,
+    getImageHeight,
+    getImageText,
+    setImageText
+  };
+  fm_thumbnail_loader_set_backend(&qt_backend);
+}
+
+ThumbnailLoader::~ThumbnailLoader() {
+
+}
+
+GObject* ThumbnailLoader::readImageFromFile(const char* filename) {
+  QImage image;
+  image.load(QString(filename));
+  // qDebug("readImageFromFile: %s, %d", filename, image.isNull());
+  return image.isNull() ? NULL : fm_qimage_wrapper_new(image);
+}
+
+GObject* ThumbnailLoader::readImageFromStream(GInputStream* stream, guint64 len, GCancellable* cancellable) {
+  // qDebug("readImageFromStream: %p, %llu", stream, len);
+  // FIXME: should we set a limit here? Otherwise if len is too large, we can run out of memory.
+  QScopedArrayPointer<unsigned char> buffer(new unsigned char[len]); // allocate enough buffer
+  unsigned char* pbuffer = buffer.data();
+  unsigned int totalReadSize = 0;
+  while(!g_cancellable_is_cancelled(cancellable) && totalReadSize < len) {
+    int bytesToRead = totalReadSize + 4096 > len ? len - totalReadSize : 4096;
+    gssize readSize = g_input_stream_read(stream, pbuffer, bytesToRead, cancellable, NULL);
+    if(readSize == 0) // end of file
+      break;
+    else if(readSize == -1) // error
+      return NULL;
+    totalReadSize += readSize;
+    pbuffer += readSize;
+  }
+  QImage image;
+  image.loadFromData(buffer.data(), totalReadSize);
+  return image.isNull() ? NULL : fm_qimage_wrapper_new(image);
+}
+
+gboolean ThumbnailLoader::writeImage(GObject* image, const char* filename) {
+  FmQImageWrapper* wrapper = FM_QIMAGE_WRAPPER(image);
+  if(wrapper == NULL || wrapper->image.isNull())
+    return FALSE;
+  return (gboolean)wrapper->image.save(filename, "PNG");
+}
+
+GObject* ThumbnailLoader::scaleImage(GObject* ori_pix, int new_width, int new_height) {
+  // qDebug("scaleImage: %d, %d", new_width, new_height);
+  FmQImageWrapper* ori_wrapper = FM_QIMAGE_WRAPPER(ori_pix);
+  QImage scaled = ori_wrapper->image.scaled(new_width, new_height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
+  return scaled.isNull() ? NULL : fm_qimage_wrapper_new(scaled);
+}
+
+GObject* ThumbnailLoader::rotateImage(GObject* image, int degree) {
+  FmQImageWrapper* wrapper = FM_QIMAGE_WRAPPER(image);
+  // 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.
+  QImage rotated = wrapper->image.transformed(QMatrix().rotate(360 - degree));
+  return rotated.isNull() ? NULL : fm_qimage_wrapper_new(rotated);
+}
+
+int ThumbnailLoader::getImageWidth(GObject* image) {
+  FmQImageWrapper* wrapper = FM_QIMAGE_WRAPPER(image);
+  return wrapper->image.width();
+}
+
+int ThumbnailLoader::getImageHeight(GObject* image) {
+  FmQImageWrapper* wrapper = FM_QIMAGE_WRAPPER(image);
+  return wrapper->image.height();
+}
+
+char* ThumbnailLoader::getImageText(GObject* image, const char* key) {
+  FmQImageWrapper* wrapper = FM_QIMAGE_WRAPPER(image);
+  QByteArray text = wrapper->image.text(key).toLatin1();
+  return (char*)g_memdup(text.constData(), text.length());
+}
+
+gboolean ThumbnailLoader::setImageText(GObject* image, const char* key, const char* val) {
+  FmQImageWrapper* wrapper = FM_QIMAGE_WRAPPER(image);
+  // NOTE: we might receive image=NULL sometimes with older versions of libfm.
+  if(Q_LIKELY(wrapper != NULL)) {
+    wrapper->image.setText(key, val);
+  }
+  return TRUE;
+}
+
+QImage ThumbnailLoader::image(FmThumbnailLoader* result) {
+  FmQImageWrapper* wrapper = FM_QIMAGE_WRAPPER(fm_thumbnail_loader_get_data(result));
+  if(wrapper) {
+    return wrapper->image;
+  }
+  return QImage();
+}
+
+
+} // namespace Fm
diff --git a/src/thumbnailloader.h b/src/thumbnailloader.h
new file mode 100644 (file)
index 0000000..37a8960
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * 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_THUMBNAILLOADER_H
+#define FM_THUMBNAILLOADER_H
+
+#include "libfmqtglobals.h"
+#include <QImage>
+#include <libfm/fm.h>
+#include <gio/gio.h>
+
+namespace Fm {
+
+class LIBFM_QT_API ThumbnailLoader {
+
+public:
+  ThumbnailLoader();
+  virtual ~ThumbnailLoader();
+
+  static ThumbnailLoader* instance() {
+    return theThumbnailLoader;
+  }
+
+  static FmThumbnailLoader* load(FmFileInfo* fileInfo, int size, FmThumbnailLoaderCallback callback, gpointer user_data) {
+    // qDebug("load thumbnail: %s", fm_file_info_get_disp_name(fileInfo));
+    return fm_thumbnail_loader_load(fileInfo, size, callback, user_data);
+  }
+
+  static FmFileInfo* fileInfo(FmThumbnailLoader* result) {
+    return fm_thumbnail_loader_get_file_info(result);
+  }
+
+  static void cancel(FmThumbnailLoader* result) {
+    fm_thumbnail_loader_cancel(result);
+  }
+
+  static QImage image(FmThumbnailLoader* result);
+
+  static int size(FmThumbnailLoader* result) {
+    return fm_thumbnail_loader_get_size(result);
+  }
+
+  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_;
+  }
+
+private:
+  static GObject* readImageFromFile(const char* filename);
+  static GObject* readImageFromStream(GInputStream* stream, guint64 len, GCancellable* cancellable);
+  static gboolean writeImage(GObject* image, const char* filename);
+  static GObject* scaleImage(GObject* ori_pix, int new_width, int new_height);
+  static int getImageWidth(GObject* image);
+  static int getImageHeight(GObject* image);
+  static char* getImageText(GObject* image, const char* key);
+  static gboolean setImageText(GObject* image, const char* key, const char* val);
+  static GObject* rotateImage(GObject* image, int degree);
+
+private:
+  static ThumbnailLoader* theThumbnailLoader;
+  static bool localFilesOnly_;
+  static int maxThumbnailFileSize_;
+};
+
+}
+
+#endif // FM_THUMBNAILLOADER_H
diff --git a/src/utilities.cpp b/src/utilities.cpp
new file mode 100644 (file)
index 0000000..5aa2fd0
--- /dev/null
@@ -0,0 +1,324 @@
+/*
+ * 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 {
+
+FmPathList* pathListFromQUrls(QList<QUrl> urls) {
+  QList<QUrl>::const_iterator it;
+  FmPathList* pathList = fm_path_list_new();
+
+  for(it = urls.begin(); it != urls.end(); ++it) {
+    QUrl url = *it;
+    FmPath* path = fm_path_new_for_uri(url.toString().toUtf8());
+    fm_path_list_push_tail(pathList, path);
+    fm_path_unref(path);
+  }
+
+  return pathList;
+}
+
+void pasteFilesFromClipboard(FmPath* destPath, QWidget* parent) {
+  QClipboard* clipboard = QApplication::clipboard();
+  const QMimeData* data = clipboard->mimeData();
+  bool isCut = false;
+  FmPathList* paths = NULL;
+
+  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 = fm_path_list_new_from_uri_list(eol + 1);
+    }
+  }
+
+  if(!paths && data->hasUrls()) {
+    // The KDE way
+    paths = Fm::pathListFromQUrls(data->urls());
+    QByteArray cut = data->data("x-kde-cut-selection");
+
+    if(!cut.isEmpty() && cut.at(0) == '1')
+      isCut = true;
+  }
+
+  if(paths) {
+    if(isCut)
+      FileOperation::moveFiles(paths, destPath, parent);
+    else
+      FileOperation::copyFiles(paths, destPath, parent);
+
+    fm_path_list_unref(paths);
+  }
+}
+
+void copyFilesToClipboard(FmPathList* files) {
+  QClipboard* clipboard = QApplication::clipboard();
+  QMimeData* data = new QMimeData();
+  char* urilist = fm_path_list_to_uri_list(files);
+  // Gnome, LXDE, and XFCE
+  data->setData("x-special/gnome-copied-files", QByteArray("copy\n") + QByteArray(urilist));
+  // The KDE way
+  data->setData("text/uri-list", urilist);
+  // data.setData("x-kde-cut-selection", "0");
+  g_free(urilist);
+  clipboard->setMimeData(data);
+}
+
+void cutFilesToClipboard(FmPathList* files) {
+  QClipboard* clipboard = QApplication::clipboard();
+  QMimeData* data = new QMimeData();
+  char* urilist = fm_path_list_to_uri_list(files);
+  // Gnome, LXDE, and XFCE
+  data->setData("x-special/gnome-copied-files", QByteArray("cut\n") + QByteArray(urilist));
+  // The KDE way
+  data->setData("text/uri-list", urilist);
+  data->setData("x-kde-cut-selection", "1");
+  g_free(urilist);
+  clipboard->setMimeData(data);
+}
+
+void renameFile(FmFileInfo *file, QWidget *parent) {
+  FmPath* path = fm_file_info_get_path(file);
+  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?
+  QString old_name = QString::fromLocal8Bit(fm_path_get_basename(path));
+  dlg.setTextValue(old_name);
+
+  if(fm_file_info_is_dir(file)) // 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;
+
+  GFile* gf = fm_path_to_gfile(path);
+  GFile* parent_gf = g_file_get_parent(gf);
+  GFile* dest = g_file_get_child(G_FILE(parent_gf), new_name.toLocal8Bit().constData());
+  g_object_unref(parent_gf);
+
+  GError* err = NULL;
+  if(!g_file_move(gf, dest,
+                  GFileCopyFlags(G_FILE_COPY_ALL_METADATA |
+                                 G_FILE_COPY_NO_FALLBACK_FOR_MOVE |
+                                 G_FILE_COPY_NOFOLLOW_SYMLINKS),
+                  NULL, /* make this cancellable later. */
+                  NULL, NULL, &err)) {
+    QMessageBox::critical(parent, QObject::tr("Error"), err->message);
+    g_error_free(err);
+  }
+
+  g_object_unref(dest);
+  g_object_unref(gf);
+}
+
+// templateFile is a file path used as a template of the new file.
+void createFileOrFolder(CreateFileType type, FmPath* 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, NULL));
+  }
+  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;
+
+  GFile* parent_gf = fm_path_to_gfile(parentDir);
+  GFile* dest_gf = g_file_get_child(G_FILE(parent_gf), new_name.toLocal8Bit().constData());
+  g_object_unref(parent_gf);
+
+  GError* err = NULL;
+  switch(type) {
+  case CreateNewTextFile: {
+    GFileOutputStream* f = g_file_create(dest_gf, G_FILE_CREATE_NONE, NULL, &err);
+    if(f) {
+      g_output_stream_close(G_OUTPUT_STREAM(f), NULL, NULL);
+      g_object_unref(f);
+    }
+    break;
+  }
+  case CreateNewFolder:
+    g_file_make_directory(dest_gf, NULL, &err);
+    break;
+  case CreateWithTemplate:
+    fm_template_create_file(templ, dest_gf, &err, false);
+    break;
+  }
+  g_object_unref(dest_gf);
+
+  if(err) {
+    if(err->domain == G_IO_ERROR && err->code == G_IO_ERROR_EXISTS) {
+      g_error_free(err);
+      err = NULL;
+      goto _retry;
+    }
+
+    QMessageBox::critical(parent, QObject::tr("Error"), err->message);
+    g_error_free(err);
+  }
+}
+
+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 == NULL))
+    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, NULL);
+  g_object_unref(gf);
+  return ret;
+}
+
+
+} // namespace Fm
diff --git a/src/utilities.h b/src/utilities.h
new file mode 100644 (file)
index 0000000..b6a3b35
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * 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 <QClipboard>
+#include <QUrl>
+#include <QList>
+#include <libfm/fm.h>
+#include <sys/types.h>
+
+class QDialog;
+
+namespace Fm {
+
+LIBFM_QT_API FmPathList* pathListFromQUrls(QList<QUrl> urls);
+
+LIBFM_QT_API void pasteFilesFromClipboard(FmPath* destPath, QWidget* parent = 0);
+
+LIBFM_QT_API void copyFilesToClipboard(FmPathList* files);
+
+LIBFM_QT_API void cutFilesToClipboard(FmPathList* files);
+
+LIBFM_QT_API void renameFile(FmFileInfo* file, QWidget* parent = 0);
+
+enum CreateFileType {
+  CreateNewFolder,
+  CreateNewTextFile,
+  CreateWithTemplate
+};
+
+LIBFM_QT_API void createFileOrFolder(CreateFileType type, FmPath* parentDir, FmTemplate* templ = NULL, 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);
+
+}
+
+#endif // FM_UTILITIES_H
diff --git a/src/utilities_p.h b/src/utilities_p.h
new file mode 100644 (file)
index 0000000..7852f99
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * 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
diff --git a/src/utils.h b/src/utils.h
new file mode 100644 (file)
index 0000000..109146d
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * 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__
diff --git a/src/xdndworkaround.cpp b/src/xdndworkaround.cpp
new file mode 100644 (file)
index 0000000..ef8238e
--- /dev/null
@@ -0,0 +1,284 @@
+/*
+ * 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, NULL);
+  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 *)&notify);
+      return true; // stop Qt 5 from touching the event
+    }
+  }
+  return false; // let Qt handle this
+}
+
+bool XdndWorkaround::genericEvent(xcb_ge_generic_event_t* event) {
+  // check this is an xinput event
+  if(xinput2Enabled_ && event->extension == xinputOpCode_) {
+    if(event->event_type == XI_ButtonRelease)
+      buttonRelease();
+  }
+  return false;
+}
+
+void XdndWorkaround::buttonRelease() {
+  QDragManager* mgr = QDragManager::self();
+  lastDrag_ = mgr->object();
+  // qDebug() << "BUTTON RELEASE!!!!" << xcbDrag()->canDrop() << lastDrag_;
+}
+
+#endif // QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)
diff --git a/src/xdndworkaround.h b/src/xdndworkaround.h
new file mode 100644 (file)
index 0000000..1a05d79
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * 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:
+  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