From b47fefe73e41b78fcee4804008bcec2941a4b4c7 Mon Sep 17 00:00:00 2001 From: Alf Gaida Date: Sun, 24 Feb 2019 18:31:01 +0000 Subject: [PATCH] Import libfm-qt_0.14.1.orig.tar.xz [dgit import orig libfm-qt_0.14.1.orig.tar.xz] --- AUTHORS | 6 + CHANGELOG | 736 +++++ CMakeLists.txt | 91 + Doxyfile.in | 1890 +++++++++++++ LICENSE | 458 +++ README.md | 52 + cmake/fm-qt-config.cmake.in | 41 + data/CMakeLists.txt | 10 + data/archivers.list | 42 + data/libfm-qt-mimetypes.xml | 52 + data/terminals.list | 80 + src/CMakeLists.txt | 281 ++ src/app-chooser-dialog.ui | 183 ++ src/appchoosercombobox.cpp | 143 + src/appchoosercombobox.h | 64 + src/appchooserdialog.cpp | 287 ++ src/appchooserdialog.h | 78 + src/applaunchcontext.cpp | 61 + src/applaunchcontext.h | 50 + src/appmenuview.cpp | 162 ++ src/appmenuview.h | 74 + src/appmenuview_p.h | 71 + src/bookmarkaction.cpp | 32 + src/bookmarkaction.h | 49 + src/browsehistory.cpp | 93 + src/browsehistory.h | 131 + src/cachedfoldermodel.cpp | 67 + src/cachedfoldermodel.h | 55 + src/colorbutton.cpp | 53 + src/colorbutton.h | 55 + src/core/archiver.cpp | 174 ++ src/core/archiver.h | 69 + src/core/basicfilelauncher.cpp | 392 +++ src/core/basicfilelauncher.h | 72 + src/core/bookmarks.cpp | 217 ++ src/core/bookmarks.h | 87 + src/core/cstrptr.h | 42 + src/core/deletejob.cpp | 158 ++ src/core/deletejob.h | 33 + src/core/dirlistjob.cpp | 180 ++ src/core/dirlistjob.h | 65 + src/core/filechangeattrjob.cpp | 324 +++ src/core/filechangeattrjob.h | 145 + src/core/fileinfo.cpp | 453 +++ src/core/fileinfo.h | 289 ++ src/core/fileinfo_p.h | 10 + src/core/fileinfojob.cpp | 50 + src/core/fileinfojob.h | 45 + src/core/filelinkjob.cpp | 9 + src/core/filelinkjob.h | 16 + src/core/filemonitor.cpp | 9 + src/core/filemonitor.h | 26 + src/core/fileoperationjob.cpp | 102 + src/core/fileoperationjob.h | 96 + src/core/filepath.cpp | 21 + src/core/filepath.h | 177 ++ src/core/filesysteminfojob.cpp | 24 + src/core/filesysteminfojob.h | 45 + src/core/filetransferjob.cpp | 652 +++++ src/core/filetransferjob.h | 58 + src/core/folder.cpp | 938 ++++++ src/core/folder.h | 198 ++ src/core/folderconfig.cpp | 275 ++ src/core/folderconfig.h | 80 + src/core/gioptrs.h | 141 + src/core/gobjectptr.h | 104 + src/core/iconinfo.cpp | 176 ++ src/core/iconinfo.h | 112 + src/core/iconinfo_p.h | 133 + src/core/job.cpp | 57 + src/core/job.h | 119 + src/core/job_p.h | 26 + src/core/legacy/fm-app-info.c | 523 ++++ src/core/legacy/fm-app-info.h | 47 + src/core/legacy/fm-config.c | 88 + src/core/legacy/fm-config.h | 199 ++ src/core/legacy/glib-compat.h | 86 + src/core/mimetype.cpp | 64 + src/core/mimetype.h | 172 ++ src/core/templates.cpp | 136 + src/core/templates.h | 100 + src/core/terminal.cpp | 183 ++ src/core/terminal.h | 26 + src/core/thumbnailer.cpp | 141 + src/core/thumbnailer.h | 37 + src/core/thumbnailjob.cpp | 293 ++ src/core/thumbnailjob.h | 78 + src/core/totalsizejob.cpp | 141 + src/core/totalsizejob.h | 56 + src/core/trashjob.cpp | 74 + src/core/trashjob.h | 30 + src/core/untrashjob.cpp | 75 + src/core/untrashjob.h | 22 + src/core/userinfocache.cpp | 47 + src/core/userinfocache.h | 82 + src/core/vfs/fm-file.c | 118 + src/core/vfs/fm-file.h | 100 + src/core/vfs/fm-xml-file.c | 1519 ++++++++++ src/core/vfs/fm-xml-file.h | 122 + src/core/vfs/vfs-menu.c | 3137 +++++++++++++++++++++ src/core/vfs/vfs-search.c | 1359 +++++++++ src/core/volumemanager.cpp | 111 + src/core/volumemanager.h | 237 ++ src/createnewmenu.cpp | 175 ++ src/createnewmenu.h | 64 + src/customaction_p.h | 52 + src/customactions/fileaction.cpp | 615 ++++ src/customactions/fileaction.h | 156 + src/customactions/fileactioncondition.cpp | 508 ++++ src/customactions/fileactioncondition.h | 123 + src/customactions/fileactionprofile.cpp | 124 + src/customactions/fileactionprofile.h | 45 + src/dirtreemodel.cpp | 233 ++ src/dirtreemodel.h | 99 + src/dirtreemodelitem.cpp | 416 +++ src/dirtreemodelitem.h | 97 + src/dirtreeview.cpp | 340 +++ src/dirtreeview.h | 93 + src/dndactionmenu.cpp | 67 + src/dndactionmenu.h | 47 + src/dnddest.cpp | 72 + src/dnddest.h | 52 + src/edit-bookmarks.ui | 143 + src/editbookmarksdialog.cpp | 106 + src/editbookmarksdialog.h | 53 + src/exec-file.ui | 163 ++ src/execfiledialog.cpp | 85 + src/execfiledialog_p.h | 57 + src/file-operation-dialog.ui | 192 ++ src/file-props.ui | 800 ++++++ src/filedialog.cpp | 991 +++++++ src/filedialog.h | 221 ++ src/filedialog.ui | 151 + src/filedialoghelper.cpp | 290 ++ src/filedialoghelper.h | 62 + src/filelauncher.cpp | 116 + src/filelauncher.h | 55 + src/filemenu.cpp | 434 +++ src/filemenu.h | 214 ++ src/filemenu_p.h | 56 + src/fileoperation.cpp | 425 +++ src/fileoperation.h | 183 ++ src/fileoperationdialog.cpp | 208 ++ src/fileoperationdialog.h | 71 + src/fileoperationdialog_p.h | 69 + src/filepropsdialog.cpp | 600 ++++ src/filepropsdialog.h | 99 + src/filesearch.ui | 574 ++++ src/filesearchdialog.cpp | 201 ++ src/filesearchdialog.h | 73 + src/fm-search.c | 317 +++ src/fm-search.h | 89 + src/folderitemdelegate.cpp | 517 ++++ src/folderitemdelegate.h | 132 + src/foldermenu.cpp | 300 ++ src/foldermenu.h | 130 + src/foldermodel.cpp | 566 ++++ src/foldermodel.h | 154 + src/foldermodelitem.cpp | 146 + src/foldermodelitem.h | 94 + src/folderview.cpp | 1747 ++++++++++++ src/folderview.h | 214 ++ src/folderview_p.h | 144 + src/fontbutton.cpp | 58 + src/fontbutton.h | 54 + src/libfm-qt.pc.in | 12 + src/libfmqt.cpp | 104 + src/libfmqt.h | 46 + src/libfmqtglobals.h | 25 + src/mount-operation-password.ui | 215 ++ src/mountoperation.cpp | 253 ++ src/mountoperation.h | 161 ++ src/mountoperationpassworddialog.cpp | 138 + src/mountoperationpassworddialog_p.h | 64 + src/mountoperationquestiondialog.cpp | 68 + src/mountoperationquestiondialog_p.h | 50 + src/pathbar.cpp | 352 +++ src/pathbar.h | 90 + src/pathbar_p.h | 78 + src/pathedit.cpp | 245 ++ src/pathedit.h | 65 + src/pathedit_p.h | 54 + src/placesmodel.cpp | 633 +++++ src/placesmodel.h | 146 + src/placesmodelitem.cpp | 171 ++ src/placesmodelitem.h | 133 + src/placesview.cpp | 635 +++++ src/placesview.h | 149 + src/proxyfoldermodel.cpp | 327 +++ src/proxyfoldermodel.h | 119 + src/rename-dialog.ui | 204 ++ src/renamedialog.cpp | 148 + src/renamedialog.h | 84 + src/sidepane.cpp | 231 ++ src/sidepane.h | 133 + src/tests/test-filedialog.cpp | 48 + src/tests/test-folder.cpp | 44 + src/tests/test-folderview.cpp | 48 + src/tests/test-placesview.cpp | 20 + src/tests/test-volumemanager.cpp | 24 + src/translations/CMakeLists.txt | 3 + src/translations/libfm-qt.ts | 1547 ++++++++++ src/translations/libfm-qt_ar.ts | 1560 ++++++++++ src/translations/libfm-qt_ca.ts | 1561 ++++++++++ src/translations/libfm-qt_cs.ts | 1560 ++++++++++ src/translations/libfm-qt_cy.ts | 1547 ++++++++++ src/translations/libfm-qt_da.ts | 1560 ++++++++++ src/translations/libfm-qt_de.ts | 1560 ++++++++++ src/translations/libfm-qt_el.ts | 1560 ++++++++++ src/translations/libfm-qt_en_GB.ts | 1548 ++++++++++ src/translations/libfm-qt_es.ts | 1560 ++++++++++ src/translations/libfm-qt_fr.ts | 1560 ++++++++++ src/translations/libfm-qt_gl.ts | 1560 ++++++++++ src/translations/libfm-qt_he.ts | 1560 ++++++++++ src/translations/libfm-qt_hu.ts | 1559 ++++++++++ src/translations/libfm-qt_id.ts | 1560 ++++++++++ src/translations/libfm-qt_it.ts | 1560 ++++++++++ src/translations/libfm-qt_ja.ts | 1560 ++++++++++ src/translations/libfm-qt_lt.ts | 1560 ++++++++++ src/translations/libfm-qt_nb_NO.ts | 1560 ++++++++++ src/translations/libfm-qt_nl.ts | 1560 ++++++++++ src/translations/libfm-qt_pl.ts | 1560 ++++++++++ src/translations/libfm-qt_pt.ts | 1560 ++++++++++ src/translations/libfm-qt_pt_BR.ts | 1560 ++++++++++ src/translations/libfm-qt_ru.ts | 1560 ++++++++++ src/translations/libfm-qt_tr.ts | 1560 ++++++++++ src/translations/libfm-qt_uk.ts | 1558 ++++++++++ src/translations/libfm-qt_zh_CN.ts | 1560 ++++++++++ src/translations/libfm-qt_zh_TW.ts | 1560 ++++++++++ src/utilities.cpp | 374 +++ src/utilities.h | 92 + src/utilities_p.h | 79 + src/utils.h | 34 + src/xdndworkaround.cpp | 292 ++ src/xdndworkaround.h | 90 + 235 files changed, 85902 insertions(+) create mode 100644 AUTHORS create mode 100644 CHANGELOG create mode 100644 CMakeLists.txt create mode 100644 Doxyfile.in create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cmake/fm-qt-config.cmake.in create mode 100644 data/CMakeLists.txt create mode 100644 data/archivers.list create mode 100644 data/libfm-qt-mimetypes.xml create mode 100644 data/terminals.list create mode 100644 src/CMakeLists.txt create mode 100644 src/app-chooser-dialog.ui create mode 100644 src/appchoosercombobox.cpp create mode 100644 src/appchoosercombobox.h create mode 100644 src/appchooserdialog.cpp create mode 100644 src/appchooserdialog.h create mode 100644 src/applaunchcontext.cpp create mode 100644 src/applaunchcontext.h create mode 100644 src/appmenuview.cpp create mode 100644 src/appmenuview.h create mode 100644 src/appmenuview_p.h create mode 100644 src/bookmarkaction.cpp create mode 100644 src/bookmarkaction.h create mode 100644 src/browsehistory.cpp create mode 100644 src/browsehistory.h create mode 100644 src/cachedfoldermodel.cpp create mode 100644 src/cachedfoldermodel.h create mode 100644 src/colorbutton.cpp create mode 100644 src/colorbutton.h create mode 100644 src/core/archiver.cpp create mode 100644 src/core/archiver.h create mode 100644 src/core/basicfilelauncher.cpp create mode 100644 src/core/basicfilelauncher.h create mode 100644 src/core/bookmarks.cpp create mode 100644 src/core/bookmarks.h create mode 100644 src/core/cstrptr.h create mode 100644 src/core/deletejob.cpp create mode 100644 src/core/deletejob.h create mode 100644 src/core/dirlistjob.cpp create mode 100644 src/core/dirlistjob.h create mode 100644 src/core/filechangeattrjob.cpp create mode 100644 src/core/filechangeattrjob.h create mode 100644 src/core/fileinfo.cpp create mode 100644 src/core/fileinfo.h create mode 100644 src/core/fileinfo_p.h create mode 100644 src/core/fileinfojob.cpp create mode 100644 src/core/fileinfojob.h create mode 100644 src/core/filelinkjob.cpp create mode 100644 src/core/filelinkjob.h create mode 100644 src/core/filemonitor.cpp create mode 100644 src/core/filemonitor.h create mode 100644 src/core/fileoperationjob.cpp create mode 100644 src/core/fileoperationjob.h create mode 100644 src/core/filepath.cpp create mode 100644 src/core/filepath.h create mode 100644 src/core/filesysteminfojob.cpp create mode 100644 src/core/filesysteminfojob.h create mode 100644 src/core/filetransferjob.cpp create mode 100644 src/core/filetransferjob.h create mode 100644 src/core/folder.cpp create mode 100644 src/core/folder.h create mode 100644 src/core/folderconfig.cpp create mode 100644 src/core/folderconfig.h create mode 100644 src/core/gioptrs.h create mode 100644 src/core/gobjectptr.h create mode 100644 src/core/iconinfo.cpp create mode 100644 src/core/iconinfo.h create mode 100644 src/core/iconinfo_p.h create mode 100644 src/core/job.cpp create mode 100644 src/core/job.h create mode 100644 src/core/job_p.h create mode 100644 src/core/legacy/fm-app-info.c create mode 100644 src/core/legacy/fm-app-info.h create mode 100644 src/core/legacy/fm-config.c create mode 100644 src/core/legacy/fm-config.h create mode 100644 src/core/legacy/glib-compat.h create mode 100644 src/core/mimetype.cpp create mode 100644 src/core/mimetype.h create mode 100644 src/core/templates.cpp create mode 100644 src/core/templates.h create mode 100644 src/core/terminal.cpp create mode 100644 src/core/terminal.h create mode 100644 src/core/thumbnailer.cpp create mode 100644 src/core/thumbnailer.h create mode 100644 src/core/thumbnailjob.cpp create mode 100644 src/core/thumbnailjob.h create mode 100644 src/core/totalsizejob.cpp create mode 100644 src/core/totalsizejob.h create mode 100644 src/core/trashjob.cpp create mode 100644 src/core/trashjob.h create mode 100644 src/core/untrashjob.cpp create mode 100644 src/core/untrashjob.h create mode 100644 src/core/userinfocache.cpp create mode 100644 src/core/userinfocache.h create mode 100644 src/core/vfs/fm-file.c create mode 100644 src/core/vfs/fm-file.h create mode 100644 src/core/vfs/fm-xml-file.c create mode 100644 src/core/vfs/fm-xml-file.h create mode 100644 src/core/vfs/vfs-menu.c create mode 100644 src/core/vfs/vfs-search.c create mode 100644 src/core/volumemanager.cpp create mode 100644 src/core/volumemanager.h create mode 100644 src/createnewmenu.cpp create mode 100644 src/createnewmenu.h create mode 100644 src/customaction_p.h create mode 100644 src/customactions/fileaction.cpp create mode 100644 src/customactions/fileaction.h create mode 100644 src/customactions/fileactioncondition.cpp create mode 100644 src/customactions/fileactioncondition.h create mode 100644 src/customactions/fileactionprofile.cpp create mode 100644 src/customactions/fileactionprofile.h create mode 100644 src/dirtreemodel.cpp create mode 100644 src/dirtreemodel.h create mode 100644 src/dirtreemodelitem.cpp create mode 100644 src/dirtreemodelitem.h create mode 100644 src/dirtreeview.cpp create mode 100644 src/dirtreeview.h create mode 100644 src/dndactionmenu.cpp create mode 100644 src/dndactionmenu.h create mode 100644 src/dnddest.cpp create mode 100644 src/dnddest.h create mode 100644 src/edit-bookmarks.ui create mode 100644 src/editbookmarksdialog.cpp create mode 100644 src/editbookmarksdialog.h create mode 100644 src/exec-file.ui create mode 100644 src/execfiledialog.cpp create mode 100644 src/execfiledialog_p.h create mode 100644 src/file-operation-dialog.ui create mode 100644 src/file-props.ui create mode 100644 src/filedialog.cpp create mode 100644 src/filedialog.h create mode 100644 src/filedialog.ui create mode 100644 src/filedialoghelper.cpp create mode 100644 src/filedialoghelper.h create mode 100644 src/filelauncher.cpp create mode 100644 src/filelauncher.h create mode 100644 src/filemenu.cpp create mode 100644 src/filemenu.h create mode 100644 src/filemenu_p.h create mode 100644 src/fileoperation.cpp create mode 100644 src/fileoperation.h create mode 100644 src/fileoperationdialog.cpp create mode 100644 src/fileoperationdialog.h create mode 100644 src/fileoperationdialog_p.h create mode 100644 src/filepropsdialog.cpp create mode 100644 src/filepropsdialog.h create mode 100644 src/filesearch.ui create mode 100644 src/filesearchdialog.cpp create mode 100644 src/filesearchdialog.h create mode 100644 src/fm-search.c create mode 100644 src/fm-search.h create mode 100644 src/folderitemdelegate.cpp create mode 100644 src/folderitemdelegate.h create mode 100644 src/foldermenu.cpp create mode 100644 src/foldermenu.h create mode 100644 src/foldermodel.cpp create mode 100644 src/foldermodel.h create mode 100644 src/foldermodelitem.cpp create mode 100644 src/foldermodelitem.h create mode 100644 src/folderview.cpp create mode 100644 src/folderview.h create mode 100644 src/folderview_p.h create mode 100644 src/fontbutton.cpp create mode 100644 src/fontbutton.h create mode 100644 src/libfm-qt.pc.in create mode 100644 src/libfmqt.cpp create mode 100644 src/libfmqt.h create mode 100644 src/libfmqtglobals.h create mode 100644 src/mount-operation-password.ui create mode 100644 src/mountoperation.cpp create mode 100644 src/mountoperation.h create mode 100644 src/mountoperationpassworddialog.cpp create mode 100644 src/mountoperationpassworddialog_p.h create mode 100644 src/mountoperationquestiondialog.cpp create mode 100644 src/mountoperationquestiondialog_p.h create mode 100644 src/pathbar.cpp create mode 100644 src/pathbar.h create mode 100644 src/pathbar_p.h create mode 100644 src/pathedit.cpp create mode 100644 src/pathedit.h create mode 100644 src/pathedit_p.h create mode 100644 src/placesmodel.cpp create mode 100644 src/placesmodel.h create mode 100644 src/placesmodelitem.cpp create mode 100644 src/placesmodelitem.h create mode 100644 src/placesview.cpp create mode 100644 src/placesview.h create mode 100644 src/proxyfoldermodel.cpp create mode 100644 src/proxyfoldermodel.h create mode 100644 src/rename-dialog.ui create mode 100644 src/renamedialog.cpp create mode 100644 src/renamedialog.h create mode 100644 src/sidepane.cpp create mode 100644 src/sidepane.h create mode 100644 src/tests/test-filedialog.cpp create mode 100644 src/tests/test-folder.cpp create mode 100644 src/tests/test-folderview.cpp create mode 100644 src/tests/test-placesview.cpp create mode 100644 src/tests/test-volumemanager.cpp create mode 100644 src/translations/CMakeLists.txt create mode 100644 src/translations/libfm-qt.ts create mode 100644 src/translations/libfm-qt_ar.ts create mode 100644 src/translations/libfm-qt_ca.ts create mode 100644 src/translations/libfm-qt_cs.ts create mode 100644 src/translations/libfm-qt_cy.ts create mode 100644 src/translations/libfm-qt_da.ts create mode 100644 src/translations/libfm-qt_de.ts create mode 100644 src/translations/libfm-qt_el.ts create mode 100644 src/translations/libfm-qt_en_GB.ts create mode 100644 src/translations/libfm-qt_es.ts create mode 100644 src/translations/libfm-qt_fr.ts create mode 100644 src/translations/libfm-qt_gl.ts create mode 100644 src/translations/libfm-qt_he.ts create mode 100644 src/translations/libfm-qt_hu.ts create mode 100644 src/translations/libfm-qt_id.ts create mode 100644 src/translations/libfm-qt_it.ts create mode 100644 src/translations/libfm-qt_ja.ts create mode 100644 src/translations/libfm-qt_lt.ts create mode 100644 src/translations/libfm-qt_nb_NO.ts create mode 100644 src/translations/libfm-qt_nl.ts create mode 100644 src/translations/libfm-qt_pl.ts create mode 100644 src/translations/libfm-qt_pt.ts create mode 100644 src/translations/libfm-qt_pt_BR.ts create mode 100644 src/translations/libfm-qt_ru.ts create mode 100644 src/translations/libfm-qt_tr.ts create mode 100644 src/translations/libfm-qt_uk.ts create mode 100644 src/translations/libfm-qt_zh_CN.ts create mode 100644 src/translations/libfm-qt_zh_TW.ts create mode 100644 src/utilities.cpp create mode 100644 src/utilities.h create mode 100644 src/utilities_p.h create mode 100644 src/utils.h create mode 100644 src/xdndworkaround.cpp create mode 100644 src/xdndworkaround.h diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..a95b73b --- /dev/null +++ b/AUTHORS @@ -0,0 +1,6 @@ +Upstream Authors: + LXQt team: https://lxqt.org + Hong Jen Yee (PCMan) + +Copyright: + Copyright (c) 2013-2018 LXQt team diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..00ff42b --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,736 @@ +libfm-qt-0.14.1 / 2019-02-24 +============================ + + * Bumped API version to 0.14.1 + * Aged ABI version to 6.1.0 + * Respect sorting/selection order when launching files + * Generate libfm-qt.pc correctly + * Fix recursive search when a dir is matched + * Fix crash on showing Properties dialog in special cases + * Case-insensitive and match-containing file name completer for file dialog + * Give window parents to some app-modal dialogs + * Gray out cut files in detailed and compact modes + * Added custom widths and visibility for detailed columns + * Added missing sort-by-group action + * Do not rely on the order of folder menu actions. instead check their data + * pathbar: fix an issue with a slot recursive call + * Make setting trust possible anytime + * Define a macro for "metadata::trust". + * Add rubber-band selection to detailed list view + * Updated translations + +libfm-qt-0.14.0 / 2019-01-25 +============================ + + * Completely dropped libfm dependency. + * An option for showing full names instead of display names. + * An option for shadowing hidden icons. + * Fixed handling of mounts and SMB. + * Fixed overwrite prompt in LXQt file dialog. + * Fixed rare showing of nonexistent files. + * Moved Qt file dialog helper into libfm-qt. + * Support adding metadata for trusting executables and add an emblem to untrusted desktop files inside home. + * Fixed a problem in selecting newly created files. + * Fixed a random crash by adding context to folder lambda connections of file dialog. + * Selection/deselection corner marks on mouse-over. + * Ensure visibility of toggled path button on resizing window. + * Use target URI for symlink thumbnails. + * Return the correct path in "FileInfo::path()" by giving the path itself to the ctor of "FileInfo" wherever possible. + * Fixed an infinite loop on trashing a file that couldn't be trashed. + * Added dir file number to Properties dialog. + * Added device usge bar to Properties dialog. + * Volume tooltips on side-pane. + * Fix selection on file creation in detailed list view. + * Removed senseless "Canceled Operation" messages. + * Let items be selected by typing in detaled list view too. + * Made path-edit completer insensitive to case. + * Allow compressing a selection of different mimetypes in the context menu. + * Span first column of Places pane (not for Devices), so that names aren't elided unnecessarily. + * Fixed randomly missing eject button with mounting. + * Don't launch unselected current item on pressing Enter. + * Don't update trash icon too often. + * Fixed crash on launching desktop files with empty Exec. + * Fixes for template actions (showing them sorted). + * Prevent a directory from being copied into itself (or into a subdir of itself); show an error message instead. + * Fixed a problem in deletion queues -- while monitoring a directory -- by preventing file info jobs from overlapping each other. + * Allow item activation by pressing ENTER when the cursor is over its selection corner. + * Fixed mimetype filters in LXQt file dialog. + * Fixed view icons with HDPI scaling. + +libfm-qt-0.13.1 / 2018-06-02 +============================ + + * Bump patch version to 1 + * It's supposedly fixed in Qt-5.11.1 + * Disconnect old source model + +libfm-qt-0.13.0 / 2018-05-21 +============================ + + * Release 0.13.0: Update changelog + * Bumped minor version to 13 + * Fixed shortcut detection + * Delete CachedFolderModel immediately on unreffing it + * Fix crashes in new templates code + * Remove the C++ wrapper for the old FmPath struct in libfm. + * Avoid using FmPath from libfm in FmSearch. + * Removed redundant code + * Modify Fm::BasicFileLauncher APIs to use Fm::FileInfoPtr consistently. + * Add typedef FileInfoPtr for std::shared_ptr since it's so frequently used. + * Correctly handle the mounting of mountable paths and correctly parse the target URIs of shortcuts. + * Add new APIs Fm::MountOperation::mountEnclosingVolume() and Fm::MountOperation::mountMountable() and deprecate Fm::MountOperation::mount(). + * Also cover local shortcuts + * Added "reject" to exec dialog + * Delete some libfm compatibility wrappers. + * Delete the unused old FmTemplate wrapper. + * Create the template items in the "Create New" menu with the new C++ file template APIs. + * Port template support to C++ (Fm::Templates class). + * Add convinient functions in Fm::FileOperation to set dest paths for file transfer jobs. + * Made job async again + * Fixed launching of desktop files + * Cleanup + * CMake: Prevent in-source builds + * Fix Fm::FileInfo::isDir() and isExecutableType() to make its behavior consistent with libfm. + * Port file lancher to C++ and Implement Fm::BasicFileLauncher base class. * Migrate Fm::FileLauncher to the new C++ implementation. + * Port archiver integration to C++ (#182) + * Fully port all file operations to C++ 11 (#181) + * Add kitty to terminals.list + * Italic font for hidden items + * Select multiple files + * No visible cursor in File Properties labels + * Just corrected a misspelling + * Elided labels for file operation dialog + * fix namespace, fixes lxqt/libfm-qt/issues/174 + * Misc link fixes + * Fixes some pathes after repo move + * Fix a cause of crash in `AppChooserComboBox` + * build: Bump version + * Drop Fm::IconTheme + * iconinfo: Properly handle multiple names + * Some code cleanup + * Fixed custom action execution mode + * Don't drop on files + * Prevent possible c++11 range-loop container detach + * See `.bak` and `.old` as backup extensions; also follow GLib + * Ensure that rename editor has opaque background + * Use special/custom folder icons for bookmarks + * Added two missing cases of `mapFromSource()` + * Support hiding items in Places side-pane + * Fixed sorting by type/owner + * Fix lambda connections in filedialog (#159) + * Drop Q_FOREACH + * Fix memory leak in thumbnail loading (#150) + * Be more tolerant + * Fix the "Folders" key in custom actions + * Add Group column and don't use owner's full name + * Fix comparison of integers of different signs + * Guarantee 64-bit time attributes (#148) + * Added a proxy setting for backup as hidden (#145) + * Fixed the logic of queued deletion + * Track folders containing cut files only with QString + * Copy selected pathbar text to selection clipboard + * Smooth scrolling for icon and thumbnail views + * cmake: Handle CMP0071 + * Prepare libfm-qt for bulk rename + * No change queue of files in the deletion queue + * FileInfo: Fix potential SEGFAULT + * Fix two devices for one mount + * Always wait for the folder to load before selecting + * Really cancel multiple renaming on cancelling + * Prevent a potential crash in xdndworkaround + * Fix wrong gray-out of cut files + * Merge side-pane with its surroundings + * Prompt dialog specifically for desktop files + * Remove unnecessary noise + +libfm-qt-0.12.0 / 2017-10-21 +============================ + + * Release 0.12.0: Update changelog + * Add data transferred to file operation dialog. + * Bump versions + * Disable context-menu actions that cannot be used + * Don't export github templates + * Fix partially visible toggled path buttons + * Add functions to get and set search settings + * Fix mistakes in listview column width calculation + * Add archiver separator only when needed + * Add a separator before archiver actions + * Enable XDS subfolder drop + * UI improvements for Fm::MountOperationPasswordDialog() + * Respect inactiveness when drawing text + * Grey out files that have been Ctrl-X'ed (#88) + * Ignore button for error dialog + * Inline renaming for detailed list view (#110) + * Remove redundant code. + * Prefer local paths if they exist + * Removed QFileInfo (as @PCMan recommended) + * Simplification, optimization and a fix + * Really focus text entry on showing dialog + * Two small fixes + * Keep selection on reloading (if not CPU-intensive) + * Added back/forward buttons and fixed 3 issues + * Reload button, hidden shortcut and a fix + * Implement FileDialog::selectMimeTypeFilter() and QString FileDialog::selectedMimeTypeFilter(). + * Initialize folder_ to null + * Fixed the quote issue + * Always preserve explicitly set labels + * Update OK button text and state when needed + * Initialize FileInfo::isShortcut_ (#113) + * Set the selected index current + * Fixd open/save and overwrite prompt + * Set open/save text + * Several important fixes + * Added a missing change + * Preliminary fixes + * Hide UI implementation details for Fm::FileDialog. + * Revert the backward incompatible changes in the constructor of Fm::FolderView. + * Fix a bug in creating new folder for Fm::FileDialog. + * Implement toolbar and quick view mode switches for the Fm::FileDialog class. + * Correctly check file types and test the existence of the selected files as needed. + * Correctly handle item activation. + * Correctly handle filename selection for Fm::FileDialog. + * Correctly handle selected files. + * Implement filename filtering for Fm::FileDialog. + * Check nullity of FileInfo before calling FolderMenu + * Arrange Custom Actions + * Support custom folder icons + * Fix multiple pasting of the same cut file(s) + * Fix KDE clipboard tag for cut file(s) + * Add a basic skeleton for Fm::FileDialog. + * Check nullity of QMimeData (#109) + * MountOperationQuestionDialog: Fix handling responses + * Fix all height issues in horizontal layouts (#103) + * Removed a redundant variable (left there from old tests) + * Fix major bugs in Directory Tree + * Consider desktop text color, now that everything is done here + * Inline Renaming + * Fix compact view regression (#102) + * Fix detailed list crash on double clicking folders + * Removed my garbage + * Fixed issues about spacings and click region + * Make Fm::FolderItemDelegate support painting text shadows and margins so it can completely replace PCManFM::DesktopItemDelegate. + * Avoid using grid size on QListView since this disables any spacing settings. + * liblxqt make no sense for libfm-qt + * Copied issue template + * Add noexcept to move constructors and operator=() so they can be used with STL containers. + * FolderView: Optimize selectAll() (#97) + * Emit fileSizeChanged when needed + * Drops Qt5Core_VERSION_STRING (#96) + * Update size column info (#90) + * Fix Detailed List view DND (#94) + * folderview: Don't allow D&D by Back or Forward + * More fixes (#87) + * Added a missing change signal (#86) + * Fix single items when seaching (#85) + * Check for nullity of IconInfo (#84) + * Address compiler warnings + * Remove addRoots() return type + * Remove the unused data/translations/ entry + * Fix broken folder unmount message caused by incorrect FilePath & GFile* comparison. (#80) + * Remove some superfluous semicolons that lead to pedantic warnings (#79) + * Ensure one item per file (#77) + * Fix the broken filesystem status (free disk space) display. (#78) + * Don't make items current on mouseover (#72) + * Fixes a FTBFS in superbuild mode (#75) + * Replace start tilde in PathEdit (#73) + * Really cancel pending thumbnail jobs on chdir (#74) + * Move fixes (#70) + * Fix invalid pointers (#69) + * Continue file selection on next/previous row (#76) + * Code reformat: use 4-space indentation to match the coding style of other LXQt components. + * Make all constructors explicit so we don't get unwanted object construction by C++. + * Prevent a crash since GObjectPtr's move ctor frees resources + * GObjectPtr: Detect & handle "self-assignment" + * Fix compatibility with Qt 5.6. + * No thumbnail for thumbnails + * Fix thumbnail update + * Fixed `PathBar::setPath()` + * Use real name for renaming + * Prevent a crash plus fallback icon + * Fix custom actions + * volumemanager: Return IconInfo as shared_ptr + * FolderModelItem: Check IconInfo existence + * Bookmarks: Check validity of insert position + * Fix a potential crash of bookmark items when the format of the bookmark file is not correct. + * Only load desktop entry files for native filesystems. + * Fix the missing icon and display name handling of desktop entry files in Fm::FileInfo. + * IconEngine: Use weak_ptr for the parent IconInfo + * PathBar: Avoid leak upon QLayout::replaceWidget() + * Use const iterators + * Use the new lxqt-build-tools new FindExif module + * Fix the incorrect header inclusion in fileoperation.cpp. + * Fix incorrect #include of libfmqtglobals.h. + * Fix a bug when copying to and pasting from "x-special/gnome-copied-files" mime-type. + * Fix bugs in the Custom Actions C++ port. + * Try to port libfm custom actions to C++. + * Try to update the content of a folder after its mount status is changed. Handle cancelled dir listing job properly. + * Rename namespace Fm2 to Fm. + * Remove unused header files of the old C API wrappers. + * Fix bugs in search:// support and finish porting file searching to C++. Fix several bugs in Fm2::FileInfo which caused by null parent dir. + * Add a missing test case for places view. + * Try to add support for menu:// and search:// URI scheme implemented in libfm. + * Correctly destroy Fm2::Job objects when their worker threads terminate. + * Fix incorrect handling of PathBar button creation which causes infinite loop when the underlying GFile implementation contains bugs. + * Fix incorrect path of application menu URI. + * Fix QThread related bugs in PathEdit which causes memory leaks. + * Fix a bug in DirTreeModelItem causing crashes. Also speed up batch insertion of large amount of items. + * Use const iterators (#61) + * Fix the broken folder reload(). + * Make all Fm2::Job derived classes reimplement exec() instead of run() method. The new run() method will emit finished() signal automatically when exec() returns. + * Fix memory leaks caused by incorrect use of QThread. + * Fix a memory leak in Fm::ThumbnailJob. + * Fix memory leaks caused by broken cache. + * Fix wrong size of generated thumbnails by external thumbnailers. + * Fix memory bugs in Fm2::GErrorPtr and improve the error handling of Fm2::Job and Fm2::Folder. + * Fix some errors related to incorrect use of std::remove() in Fm2::Folder. Replace QList with std::vector and use binary search when inserting items for the Fm::DirTreeModelItem. + * Change the handling of Fm::FolderView::selChanged signal to make it more efficient. + * Port to the new Fm2::TotalSizeJob API. + * Fix compatibility with libfm custom actions. + * Add some compatibility API which helps migrating old C APIs to the new C++ implementation. + * Convert datetime to locale-aware strings using QDateTime. + * Use QCollator to perform file sorting. + * Fix detailed view. + * Finish porting DirTreeModel to the new API. Fix bugs in Fm2::FilePath and Fm2::FileInfo APIs. + * Port the libfm terminal emulator related APIs to C++. + * Rename some methods in Fm2::Folder and Fm2::FileInfo for consistency. + * Port to the new IconInfo API and deprecate manual icon update when the theme is changed. + * Rename Fm::Icon to Fm::IconInfo. + * Port emblem support to the new libfm C++ API. + * Remove unused files, including some old APIs. Replace QVector in BrowseHistory with STL vector. + * Fix a bug in Fm::FileMenu. + * Port file-click handling to the new C++ API. + * Fix bugs in Fm::PathBar getting wrong path when a path button is toggled. + * Remove Fm::FilePath(const char* path_str) to avoid ambiguity. + * Replace all NULL with C++ 11 nullptr; + * Fix FilePath related errors caused by incomplete porting. + * Make Fm::FolderConfig support the new Fm::FilePath class. + * Fix Fm::BookmarkAction to use the new C++ API. + * Fix missing icons of places view caused by memory errors. + * Fix memory errors in Fm2::Bookmarks::reorder(). Add a small test case for the places view. + * Share the same places model among all views. + * Port most of the existing UI-related APIs to the new C++ APIs (excluding the file operations). + * Port the path bar to the new Fm2 API. + * Implement VolumeManager class which is a QObject wrapper of gio GVolumeMonitor. + * Add some getters for Volume and Mount classes. + * Properly associate external thumbnailers with mimetypes they support and fix thumbnail generation from thumbnailers. + * Start porting thumbnail loaders to the new C++ APIs. Add new Fm::ThumbnailJob used to load thumbnails for a given file list. Add commonDirPath paramter to Fm::FileInfoJob to reduce memory usage. + * Add the missing test case for folder view. + * Start porting Fm::FolderModel and Fm::FolderView to the new libfm core c++ API. + * Work in progress. + * Add a c++ wrapper for GFileMonitor. Add LIBFM_QT_API declaration for all public headers. + * Port error handling mechanism of FmJob to C++ and improve the GError wrapper class. + * Bump year + * Add gioptrs.h which defines smart pointer types wrapping gio related data types. Add some basic skeleton for various I/O jobs classes. + * Start porting Copyjob and add basic skeleton for untrash and symlink jobs. + * Finish porting FmFolder to C++. + * Add a very simple test case to test the new Fm core C++ code. Fix bugs in smart pointers and do empty base class optimization for CStrPtr. + * Improve Fm::Folder. + * Rename UserInfo to UserInfoCache. + * Port Fm::Bookmarks to C++. + * Port FmDeepCountJob to C++. + * Add basic skeletion to Fm::VolumeManager. + * Implement Fm2::UserInfo, which is a cache for uid/gid name mapping. + * Add basic skeleton for other C++ classes waiting for porting. + * Add GSignalHandler to do automatic signal/slot connection management with type safety for GObject. + * Add basic skeleton for the C++ 11 port of FmFileInfoJob. + * Try to port Fm::Folder and Fm::DirListJob to Qt5 and C++ 11. + * Try to port FmIcon, FmFileInfo, and FmMimeType of libfm to clean C++. + * Add smart pointer for GObject-derived classes and add Fm::FilePath class which wraps GFile. + +libfm-qt-0.11.2 / 2016-12-21 +============================ + + * Release 0.11.2: Update changelog + * Fix enabled state of path arrows on starting (#58) + * bump patch version (#56) + * Use QByteArray::constData() where we can (#57) + * Updates lxqt-build-tools required version + * Bump ABI so version numbers preparing for a new release. + * Fix Pathbar Paint on Menu Pop-Up + * Code cleanup and refactor for Fm::PathBar. + * Added another condition + * Added a missing condition (typo) + * Scroll Pathbar with Mouse Wheel + * Reduct flickering of the path bar when creating path buttons. + * Code simplification by removing unnecessary signal/slot usage. + * Path Button Middle Click + * Enable auto-repeat for pathbar scroll buttons. + * Make the path bar buttons aware of style changes. + * Use widget style instead of app style + * Align Path Buttons + * Move FindXCB.cmake to lxqt-build-tools + * Adds superbuild/intree support + * Removes not needed dependency check + * Set CMP0024 policy usage to NEW + * Updates target_include_directories() stuff + * Drops GLib library detection + * Use the new FindMenuCache CMake module + * Use the new FindFm CMake module + * Check for FolderModelItem info (and FmPath) + * Add Fm::PathBar::editingFinished() signal. + * Select the current path when editing the path bar. + * Enable path editing and popup menu for the button-style path bar. + * Properly set styles of path buttons. + * Remove unnecessary debug messages. + * Try to implement the Fm::PathBar which shows a filesystem path as buttons. + * Adds Build PROJECT_NAME with Qt version message + * Move LIBFM_DATA_DIR to pcmanfm repo. + * Refactors CUSTOM_ACTIONS compile definition + * Refactors LIBFM_DATA_DIR compile definition + * Drop add_definitions. Use target_compile_definitions. + * Removes duplicated symbols visibility settings + * README.md: Add build dependency lxqt-build-tools + * Use the new lxqt-build-tools package + * Restore symlink emblem + * Remove empty files + * Try to refactor the emblemed icon support in a more generalized way. Reuse FolderItemDelegate to paint the emblemed icons in Fm::PlacesView to prevent code duplication. APIs changes: * Add Fm::IconTheme::emblems() and cache emblem info in the cache. * Add Fm::FolderItemDelegate::setFileInfoRole() and Fm::FolderItemDelegate::setFmIconRole() * Cache multiple emblems rather than getting the first one only (but only paint the first one now). * Remove icon sizes from Fm::PlacesModel and Fm::PlacesModelItems to maintain MVC design pattern and backward incompatibility. * Expose two role IDs in Fm::PlacesModel: FileInfoRole and FmIconRole so the views can access these data. + * Show File Emblems + * Emblem For (Encrypted) Volume Icons + * Remove cpack (#44) + * Also Consider GEmblemedIcon (#41) + +libfm-qt-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 + +libfm-qt-0.11.0 / 2016-02-27 +============================ + + * Bump version number and ABI version and preparing for the initial release. + * Redirect focus operation on the folderview to the actual widget + * Use grid layout to have proper properties alignment + * Focus automatically on the first field of the filesearch dialog + * Add (folder) custom actions to foldermenu. + * Update czech translation (by Petr Balíček ) + * Fix compiling with Qt < 5.4 + * Add supports for freedesktop Xdnd direct save (XDS) protocol (closes #pcmanfm-qt/298). * Move Xdnd workarounds from pcmanfm-qt to libfm-qt. + * Protected FolderView methods for getting and setting item delegate margins + * Perform auto-completion for the path bar when the user press Tab key. This closes pcmanfm-qt/#201. + * Disable unused options in file search dialog + * Let the style engine draw the text selection rectangle. + * Fix file search with smaller than option + * Fix target language in Lithuanian translation file + * Fix memory leak if reading stream for image failed + * Update German translation + * Polish translation updated + * Polish translation updated + * Add a missing type casting to fix a compiler warning. + * Relicense libfm-qt to LGPL v.2.1. + * Avoid using blocking calls when initializing the trash can to speed up startup. + * Updated Russian translation Removed ru_RU file + * Create DnD menu with supported actions only + * make createAction public so can be hidden from view + * Adds Runtime and Devel install COMPONENT + * Quote everything that could break due to whitespaces + * Use CMake GenerateExportHeader to generate ABI header file + * Use no exceptions also with Clang + * Uses CMAKE_VISIBILITY and CMAKE_CXX_VISIBILITY flags + * Use QString() instead of "" + * Fix duplicated mtp mounts in the side pane by adding workarounds for potential bugs of gvfs. + * Replace g_io_scheduler which is deprecated with QThread in Fm::PathEdit. * Fix compiler warnings. + * Adds .gitignore + * Makes Release the default BUILD_TYPE + * Adds INSTALL package configuration file + * Removes XCB from the dependencies + * Adds target_include_directories() for the INSTALL_INTERFACE. + * Removes CMAKE_CURRENT_BINARY_DIR usage + * Removes QTX_INCLUDE_DIRS + * Removes Qt Creator .user file + * Updates libraries dependencies + * Adds REQUIRED_PACKAGE_VERSION variables + * Adds generation of TARGETS CMake file + * Creates and installs an package version file + * Renames and moves LIBRARY_NAME variable to the top CMakeLists + * Removes sub-project + * Rename the library to libfm-qt and fix installed header files. + * Split pcmanfm-qt into two parts and move libfm-qt to its own repository. + * Update French translation + * Italian translation updates + * Fix a crash triggered when unmounting a volume from the side pane. + * Avoid duplicated volumes and mounts in the side panel. (This fixes the mtp:// problem.) + * Fix missing null pointer check in Fm::Path::parent() and use nullptr instead of NULL in path.h. + * Code cleanup, «using» directive only if necessary + * Upgrade of pcmanfm-qt to C++11 + * hu translations updated + * Fixed several problems with item selection and alignment + * Remove unnecessary qDebug traces + * Update translations. + * The signal QAbstractItemView::iconSizeChanged is only available after Qt 5.5. Add workarounds for older Qt versions. + * Add more null pointer checks in the thumbnail loader to avoid crashes caused by older versions of libfm. + * Update translations + * Avoid the column resizing tricks for the detailed list view when the view contains no columns. + * Improve the column width computation for the detailed view + * PlacesView: activate on click on the second column + * SidePane: reduce size of button's column width + * Added a filterbar + Handle virtually hidden files + * Russian translation update + * Update cs_CZ translation with the strings provided by petrbal in pull request #218. + * Allow adding or removing paths in the file search dialog. Fix bugs in searching for documents. + * Try to implement file searching by using the new Fm::FileSearchDialog class. + * Fix a incorrecy free() in fm_search_free() API. + * Add Fm::Path::take() API to take the ownership of a raw FmPath pointer. + * Add class Fm::FileSearchDialog used to show a dialog for searching files. + * Add FmSearch API which is used to build a search:// URI. (implemented in C and might become part of libfm later). + * Fix #195 - Crash when rightclick on item in trash. + * Add a null check for FmFileInfo in Fm::ProxyFolderModel::lessThan(). This closes #205. + * Fix (workaround) for right-click crash in placesview. + * Russian translation: update + * Italian translation: add desktop entry files, adjust TS files + * placesview: middle-click correct item to activate (fix of segfault) + * Check for null pointers. + * Select the folder from where we have gone up. + * Paste into folder from its context menu. + * libfm-qt: updated german translation + * libfm-qt: lupdated translation files + * Add Greek (el) translation + * added support for mouse back/forward buttons + * Update German translation + * Add new signal prepareFileMenu() to Fm::SidePane and Fm::DirTree so there's a chance to customize the file menu before its shown. + * Port some missing config options from the gtk+ version of pcmanfm. + * Also show hidden dirs in the directory tree when the "Show Hidden" option in the menu is turned on. + * Fix #190 - Column layout is not always updated. + * Create New menu actions, context menu in tree side pane, #163. + * Store side pane mode setting, #157. + * Fixes an translation regression + * Updates translations + * Uses LXQt lxqt_translate_ts() to handle translations + * Fix lxde/lxqt#447 - missing actions in Places' context menus + * Remove trailing whitespaces + * polishing German translation + * Add menu items and shortcuts for new folder and blank file, fixes #163. + * Display folders first when active and sort order descending, fixes #179. + * Avoid space wasted by incorrect decoration in detailed view columns, fixes #177. + * Avoid flickering column header while resizing manually, fixes #176. + * Hungarian translation + * Fix #627 - long startup time. (This blocking is caused by checking the availability of "network:///".) + * Enable text selection in file properties dialog + * Fix some memory leaks reported by valgrind. + * Fix warnings reported by cppcheck. + * Fix warnings reported by scan-build. + * Sort indicators in detailed view, store column and order in settings, fixes #109. + * Fix lxde/lxqt#512 - pcmanfm-qt: cannot delete to trash. + * Polish translations added + * Use 'user-trash' icon for 'Move to Trash' + * The "Custom" option in the application chooser combo box inside file properties dialog is broken. Fix by preventing recursive signal handler invocation. + * The file property dialog does not show correct default applications. Fix a bug in AppChooserComboBox::setMimeType() that does incorrect app comparison. + * When converting an UID or GID to its name, show the number as string when the user or group does not exists. + * Add more null checks. + * Portuguese update + * Add very basic "remaining time" display to the progress dialog. Fix lxde/lxqt#463 - Progress dialog of pcmanfm-qt does not show remaining time. + * Fix lxde/pcmanfm-qt#120 - Foucs "Rename" button when file name changed. + * Remove unnecessary '\n' charactor from the translated strings. + * Fix translations (the newly added string comes from the translation of libfm). + * Improve trash can handling: provide an option to delete the files if moving to trashcan fails. + * Fix broken filenames of translation files. + * More migration to Qt5 new signal/slot syntax for better type safety & speed. + * Migrade to new Qt5 signal/slot syntax for better type safety and speed. + * Fix the broken sorting option. + * Fix lxde/lxqt#448 - PCmanFM-QT renaming place bookmarks does nothing. + * Support linguistic sorting of file names. This fixes #105. + * Update the folder model & view after files are changed. + * Open folders in new tabs by middle clicking on items in the side pane. + * Portuguese update + * Fix a crash of the context menu of places view caused by change of items. + * Save the result of "Edit bookmarks" to gtk+3 bookmarks file instead of the deprecated ~/.gtkbookmarks file. This fixes bug #112 partially. + * Add spanish translations + * Update Japanese translation + * Add German translation + * add Japanese translation + * Implement "UnTrash" for files in trash:/// and close lxde/lxqt#136. + * Add Russian translation + * Drop Qt4 support in code + * Clean up CMakeLists.txt and drop Qt4 support + * New files added from LXDE Pootle server based on templates + * Commit from LXDE Pootle server by user Julius22.: 1007 of 1008 strings translated (2 need review). + * Commit from LXDE Pootle server by user Julius22.: 709 of 1008 strings translated (2 need review). + * Commit from LXDE Pootle server by user mbouzada.: 364 of 364 strings translated (0 need review). + * New files added from LXDE Pootle server based on templates + * Add cs_CZ translation for libfm-qt. + * Commit from LXDE Pootle server by user dforsi.: 364 of 364 strings translated (0 need review). + * Commit from LXDE Pootle server by user dforsi.: 358 of 364 strings translated (0 need review). + * Bump package version number and library soversion to prepare for 0.2 release. + * Fix #85 - Scrolling doesn't work in compact view. + * Hide UI elements that are not usable and disable trash can when gvfs is not available. * Add new API Fm::isUriSchemeSupported(). + * Avoid showing the popup menu when moving desktop items. + * Improve handling of file selection and fixing FolderView::selectAll(), which is reported in #45. Delay the handling of selectionChanged() signal to avoid too frequent UI updates. + * Little adjustment for the grid of the folder view to decrease unnecessary margins. + * Use a new way to optimize the size of filename display based on current view mode and font size. This also fixes lxde/lxde-qt #198 - PCmanFM-qt incorrectly displays 256x256 Thumbnails. + * Fully support single click activation and auto-selection with associated options added to the preference dialog. + * Add single click and auto-selection on hover support to Fm::FolderView. + * New files added from LXDE Pootle server based on templates + * New files added from LXDE Pootle server based on templates + * Improve update of translations to avoid unnecessary regeneration of ts files. + * Improve handling of fallback icons. This closes bug #57. + * Translations are lost accidentally in a previous commit. Restore them all. + * Fix a crash in Fm::PlacesModel when gvfs is not available. This closes bug #35 - Ctrl+W closes all windows. + * Do not detect filename extension and select the whole filename by default while renaming directories. This closes bug #71 - Don't try to detect extensions on directories. * API changed: Fm::renameFile() now accepect FmFileInfo as its first parameter. + * Fix bug #80 - make execute in context menu doesn't do change permissions. + * Revert "fixed selection issue #45" This patch breaks copying files by DND in icon view mode and moving desktop icons. + * Use qss instead of QPalette to set the background color of ColorButton. This fixed bug #192 of lxde-qt. + * Rename the library from libfm-qt to libfm-qt5 when built with Qt5. + * fixed selection issue #45 + * Fix middle click position calculation in detailed view mode + * Fix crash when context menu is requested but selection is empty + * Activate view items only if clicked with left mouse button + * Do not emit activated signal when keyboard modifiers are on. + * Splits the checks for needed libraries + * Removes duplicated include_directories() entry + * Make sure clang compiler does not complain + * Install pkgconfig file of libfm-qt to correct location in FreeBSD + * Fix missing return values in several methods. + * Avoid endless popups of error dialogs when there are errors launching files. + * Save thumbnails as png files correctly. + * Remember custom positions for desktop icons and fix #29. + * Add template support to the folder context menus and fix #39. + * Show "owner" in the detailed list view mode. * Fix a crash when switching to detailed list mode in qt5. + * Use xcb to set EWMH window type hint to the desktop window in Qt5. * Some more cleanup for the CMakeList.txt files + * Add initial support for Qt5. + * Try to fix #36 again. + * Fix a seg fault caused by the widget being deleted during glib signal handling. + * Code cleanup, removing unnecessary header inclusion to speed up compilation. + * Avoid further handling of MountOperation in the gio finished callback if the object is deleted. + * Use modeless dialogs for app chooser and error reporting in Fm::FileLauncher and Fm::FileMenu. + * Add an small emblem for symlinks (using icon name "emblem-symbolic-link"). Fix bug #27. + * Add missing file to git. + * Move internal implementation details to private headers which are not installed to the system. + * Implement Fm::AppChooserDialog and Fm::AppMenuView classes. * Add / menu item to Fm::FileMenu. * Add custom app to Fm::AppChooserComboBox. + * Add Fm::AppChooserComboBox and use it in Fm::FilePropsDialog. + * Redesign Fm::FileLauncher APIs to make it more usable. * Add Fm::FileMenu::setFileLauncher() and Fm::FolderView::setFileLauncher() APIs. * Move PCManFM::View::onFileClick() and other popup menu handling to Fm::FolderView. + * Improve Fm::FileLaucher to make it easy to derive subclasses. * Implement a special dialog for opening executable files (Fix bug #13 - it does not launch executables) + * Fix bug #28 - Tash can icon does not refresh when the trash can changes its empty/full status + * Load autocompletion list for Fm::PathEdit only when the widget has the keyboard focus to avoid unnecessary I/O. + * Add proper popup menu items for selected folders and fix #20 and #19. * Some code refactors, adding openFolders() and openFolderInTerminal() to Application class. + * Fix #25 - Amount of items in the folder is not refreshed when the folder content changes. * Update status bar text properly on switching tab pages, selection changes, and folder content changes. + * Fix the broken compiler definitions caused by previous commit. + * Fix bug #22 - Do not select file extension by default on file rename. * Avoid installing private headers (*_p.h files) + * Setup pcmanfm-qt to support optional Custom Actions Menubar detects if libfm was built with vala or not if so a fm-actions.h will exist and support will be compiled in if not, will still compile with no actions menu + * Allow installation path configuration with standard CMake X_INSTALL_DIR + * Support reordering bookmark items in the places view with DND. + * Support adding bookmarks to the places view using drag and drop + * Preparing for implementing dnd for places view. + * Improve the usability of icon view mode, fixing github bug #24. + * Fix crashes caused by invalid pointer access. + * Switch current dir of the folder view correctly with dir tree view in the side pane. + * Finish chdir operation for Fm::DirTreeView. + * Support hiding hidden folders from DirTreeModel. + * Move some methods from DirTreeModel to DirTreeModelItem and fix some row updating problems. + * Implement dynamic folder loading/unloading when expanding or collapsing dir tree nodes. * Enable horizontal scrollbar of dir tree view. + * Move some code from Fm::DirTreeModel to Fm::DirTreeModelItem. + * Partially implement Fm::DirTreeView and Fm::DirTreeModel. (not finished yet) + * Fix an invalid pointer + * Implment different modes for Fm::SidePane, matching libfm-qtk design. * Add basic skeleton for dir tree view/model. + * Fix the cosmetic defect introduced by the eject buttons in the places view. + * Add eject buttons to mounted volumes in the places side pane. + * Add a wrapper class Fm::Path for FmPath C struct. + * Initialize icon_ member of PlacesModelItem correctly. + * Fix fallback icon when a platform plugin is abscent. * Make Fm::IconTheme::checkUpdate() a static function. + * Remove xsettings support. Use a cleaner way to detect config changes by monitor StyleChange events. * Add Fm::IconTheme::changed() signal which is emitted when the icon theme name is changed. * Replace nested Fm::PlacesModel::Item and related classes with their own separate toplevel classes. + * Fix the icon for files of unknown mime types, again. + * Fix the icon for files of unknown mime types. + * Add DES-EMA custom actions to the popup menus. + * Make it safe to create multiple Fm::LibFmQt objects and only initialize libfm once. + * Fix incorrect export symbols and use GNUInstallDirs for installation destination + * Use the latest libfm thumbnailer APIs. + * Fix #3614873 - Thumbnails in icon view shown upside down for some jpegs. + * Adopt recent changes of libfm. FmIcon is essentially identical to GIcon now. + * Add a UI file for application chooser dialog. + * Correctly handle display names of folders in path entry auto-completion. + * Add a global header and add proper definition for LIBFM_QT_API macro. + * Add "Empty trash" and fix a memory leak. + * Fix memory leaks for bookmarks. Fix the broken "Network" item in places. + * Reduce memory usage: Paint the folder items with our own code instead of using a dirty hacks duplicating pixmaps. + * Reduce of size of QPixmapCache in the hope of decreasing memory usage. + * Add fallback icons for places item "applications" and "network". + * Add class Fm::CachedFolderModel, a convinient way to share Fm::FolderModel objects and reduce memory usage. + * Resize the columns of detailed list view when items are inserted or removed. + * Optimize column widths in detailed list mode when the view is resized. + * Only show thumbnails for the first column in detailed list mode. + * Use new "automoc" feature of cmake 2.8.6 and remove cumbersome #include "*.moc" code. + * Trivial fix. + * Add additional custom filter support to ProxyFolderModel. + * Fix some memory leaks. + * Fix some compiler errors and update translations. + * Support the latest libfm trunk. Remove -fpermissive compiler flag and fix compiler errors/warnings. + * Adopt new libfm thumbnail APIs. + * Add soname 0.0.0 for libfm-qt, preparing for 0.1 release. + * Fix crashes caused by incorrect deletion of dialog objects. + * Enable thumbnail related settings. + * Update zh_TW translations and translation templates. + * Add Portuguese translation (pt). + * Add Lithuanian translation (lt_LT). + * Adopt the latest thumbnail API in libfm (thumbnail branch) to speed up loading. + * Workardound incorrect thumbnail painting caused by bug of QStyledItemDelegate. :-( + * Fix a crash caused by accessing data for invalid model index. + * Fix a crash caused by accessing data for invalid model index. + * Add basic thumbnail support (need the latest thumbnail branch of libfm). + * Add archiver integration for file context menus. + * Add archiver integration to file context menus. + * Add mnemonics for menu items. Make confirm dialog before delete and trash can optional. + * Update side pane according to current dir. Little fix. + * Implement "Open in Terminal" and "Open as Root". + * Implement "Auto Run" for newly inserted removable devices. + * Add "Edit Bookmarks" dialog. + * Implement "Invert Selection". Little fix of UI, add a Tool menu to main window. + * Implement "Create New" menu in the folder popup menu. + * Modify make rules for translations. Avoid deleting generated ts files when "make clean". Fix a small error in zh_TW translation. + * Add auto-completion to path entry bar. + * Rename Fm::Application to Fm::LibFmQt to decrease confusion. Set required Qt version to 4.6. + * Load translation files correctly for pcmanfm-qt and libfm-qt. + * Add basic skeleton for i18n (using Qt QTranslator & Qt Linguist). + * Add separate CMakeLists.txt files for pcmanfm and libfm-qt. Hide more implementation details from libfm-qt headers. + * Fix copyright notice in all source files. + * Install a pkgconfig file for libfm-qt for use in other projects. + * Fix a memory error caused by incorrect array size. Fix incorrect spacing of icons. + * Finish chown and chmod supports. + * Try to add file opermission settings UI. + * Implement very basic drag and drop support. + * Supress the incorrect default dnd handling of QListView. + * Try to implement Dnd. + * Finish desktop preferences. + * Improve desktop preferences and apply settings (partially done). + * Add desktop preferences dialog. + * Apply side pane icon size correctly. Add basic skeleton for archiver integration. + * Set shortcuts for frequently used menu options. Implement "rename file" support. Hide tabs when there is only one tab left (optional). + * Delete windows properly when they're closed with setAttribute(Qt::WA_DeleteOnClose); Apply settings to windows after clicking OK in the preference dialog. + * Improve preferences dialog. Change base class of SidePane to QWidget. + * Sync the state of folder popup menu and main menu bar. + * Implement sort options for main window. + * Fix file sorting options for Fm::FolderMenu. + * Correctly implement browse history and fix crashes. + * Add very simple browse history (back/forward) handling. + * Apply gcc visiblility attributes to export less symbols. + * Correctly handle file rename/overwrite during file operations. + * Exclude unnecessary files from CPack. + * Improve folder popup menu. + * Add folder popup menu. Some UI polishing. + * Fix a little crash. + * Fix crashes when turning off desktop manager. + * Show popup menu for blank area of folders. + * Do some refactor to make Fm::FolderView cleaner. Add PCManFM::View to do file manager-specific operations. + * Move files for libfm-qt and pcmanfm-qt to separate subdirs. diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..63702d7 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,91 @@ +cmake_minimum_required(VERSION 3.1.0 FATAL_ERROR) +# CMP0000: Call the cmake_minimum_required() command at the beginning of the top-level +# CMakeLists.txt file even before calling the project() command. +# The cmake_minimum_required(VERSION) command implicitly invokes the cmake_policy(VERSION) +# command to specify that the current project code is written for the given range of CMake +# versions. +project(libfm-qt) + +set(LIBFM_QT_LIBRARY_NAME "fm-qt" CACHE STRING "fm-qt") + +set(LIBFM_QT_API_VERSION_MAJOR 0) +set(LIBFM_QT_API_VERSION_MINOR 14) +set(LIBFM_QT_API_VERSION_PATCH 1) +set(LIBFM_QT_API_VERSION ${LIBFM_QT_API_VERSION_MAJOR}.${LIBFM_QT_API_VERSION_MINOR}.${LIBFM_QT_API_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" +# https://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: 6, revision: 0, age: 0 => version: 6.0.0 +set(LIBFM_QT_ABI_VERSION "6.1.0") +set(LIBFM_QT_SOVERSION "6") + +set(GLIB_MINIMUM_VERSION "2.50.0") +set(LIBMENUCACHE_MINIMUM_VERSION "1.1.0") +set(LXQTBT_MINIMUM_VERSION "0.6.0") +set(QT_MINIMUM_VERSION "5.7.1") + +find_package(Qt5Widgets "${QT_MINIMUM_VERSION}" REQUIRED) +find_package(Qt5LinguistTools "${QT_MINIMUM_VERSION}" REQUIRED) +find_package(Qt5X11Extras "${QT_MINIMUM_VERSION}" REQUIRED) + +find_package(lxqt-build-tools "${LXQTBT_MINIMUM_VERSION}" REQUIRED) +find_package(GLIB "${GLIB_MINIMUM_VERSION}" REQUIRED COMPONENTS gio gio-unix gobject gthread) +find_package(MenuCache "${LIBMENUCACHE_MINIMUM_VERSION}" REQUIRED) +find_package(Exif REQUIRED) +find_package(XCB REQUIRED) + +message(STATUS "Building ${PROJECT_NAME} with Qt ${Qt5Core_VERSION}") + +option(UPDATE_TRANSLATIONS "Update source translation translations/*.ts files" OFF) +include(GNUInstallDirs) +include(GenerateExportHeader) +include(CMakePackageConfigHelpers) +include(LXQtPreventInSourceBuilds) +include(LXQtTranslateTs) +include(LXQtTranslateDesktop) +include(LXQtCompilerSettings NO_POLICY_SCOPE) + +set(CMAKE_AUTOMOC TRUE) +set(CMAKE_AUTOUIC ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +write_basic_package_version_file( + "${CMAKE_BINARY_DIR}/${LIBFM_QT_LIBRARY_NAME}-config-version.cmake" + VERSION ${LIBFM_QT_API_VERSION} + COMPATIBILITY AnyNewerVersion +) + +install(FILES + "${CMAKE_BINARY_DIR}/${LIBFM_QT_LIBRARY_NAME}-config-version.cmake" + DESTINATION "${CMAKE_INSTALL_DATADIR}/cmake/${LIBFM_QT_LIBRARY_NAME}" + COMPONENT Devel +) + +add_subdirectory(src) +add_subdirectory(data) + +# add Doxygen support to generate API docs +# References: +# https://majewsky.wordpress.com/2010/08/14/tip-of-the-day-cmake-and-doxygen/ +option(BUILD_DOCUMENTATION "Use Doxygen to create the HTML based API documentation" OFF) +if(BUILD_DOCUMENTATION) + find_package(Doxygen REQUIRED) + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in" "${CMAKE_CURRENT_BINARY_DIR}/Doxyfile" @ONLY) + add_custom_target(doc ALL + ${DOXYGEN_EXECUTABLE} "${CMAKE_CURRENT_BINARY_DIR}/Doxyfile" + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" + COMMENT "Generating API documentation with Doxygen" VERBATIM + ) + install(DIRECTORY + "${CMAKE_CURRENT_BINARY_DIR}/docs" + DESTINATION "${CMAKE_INSTALL_DOCDIR}" + COMPONENT Devel + ) +endif() diff --git a/Doxyfile.in b/Doxyfile.in new file mode 100644 index 0000000..9b102f0 --- /dev/null +++ b/Doxyfile.in @@ -0,0 +1,1890 @@ +# Doxyfile 1.8.4 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed +# in front of the TAG it is preceding . +# All text after a hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists items can also be appended using: +# TAG + = value [value, ...] +# Values that contain spaces should be placed between quotes (" "). + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the config file +# that follow. The default is UTF-8 which is also the encoding used for all +# text before the first occurrence of this tag. Doxygen uses libiconv (or the +# iconv built into libc) for the transcoding. See +# https://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 https://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by doxygen, so you +# can mix doxygen, HTML, and XML commands with Markdown formatting. +# Disable only in case of backward compatibilities issues. + +MARKDOWN_SUPPORT = YES + +# When enabled doxygen tries to link words that correspond to documented +# classes, or namespaces to their corresponding documentation. Such a link can +# be prevented in individual cases by by putting a % sign in front of the word +# or globally by setting AUTOLINK_SUPPORT to NO. + +AUTOLINK_SUPPORT = YES + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should +# set this tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); v.s. +# func(std::string) {}). This also makes the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. + +BUILTIN_STL_SUPPORT = NO + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip sources only. +# Doxygen will parse them like normal C++ but will assume all classes use public +# instead of private inheritance when no explicit protection keyword is present. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate +# getter and setter methods for a property. Setting this option to YES (the +# default) will make doxygen replace the get and set methods by a property in +# the documentation. This will only work if the methods are indeed getting or +# setting a simple type. If this is not the case, or you want to show the +# methods anyway, you should set this option to NO. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES, then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. + +DISTRIBUTE_GROUP_DOC = NO + +# Set the SUBGROUPING tag to YES (the default) to allow class member groups of +# the same type (for instance a group of public functions) to be put as a +# subgroup of that type (e.g. under the Public Functions section). Set it to +# NO to prevent subgrouping. Alternatively, this can be done per class using +# the \nosubgrouping command. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and +# unions are shown inside the group in which they are included (e.g. using +# @ingroup) instead of on a separate page (for HTML and Man pages) or +# section (for LaTeX and RTF). + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and +# unions with only public data fields or simple typedef fields will be shown +# inline in the documentation of the scope in which they are defined (i.e. file, +# namespace, or group documentation), provided this scope is documented. If set +# to NO (the default), structs, classes, and unions are shown on a separate +# page (for HTML and Man pages) or section (for LaTeX and RTF). + +INLINE_SIMPLE_STRUCTS = NO + +# When TYPEDEF_HIDES_STRUCT is enabled, a typedef of a struct, union, or enum +# is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically +# be useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. + +TYPEDEF_HIDES_STRUCT = NO + +# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This +# cache is used to resolve symbols given their name and scope. Since this can +# be an expensive process and often the same symbol appear multiple times in +# the code, doxygen keeps a cache of pre-resolved symbols. If the cache is too +# small doxygen will become slower. If the cache is too large, memory is wasted. +# The cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid +# range is 0..9, the default is 0, corresponding to a cache size of 2^16 = 65536 +# symbols. + +LOOKUP_CACHE_SIZE = 0 + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in +# documentation are documented, even if no documentation was available. +# Private class members and static file members will be hidden unless +# the EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES + +EXTRACT_ALL = NO + +# If the EXTRACT_PRIVATE tag is set to YES all private members of a class +# will be included in the documentation. + +EXTRACT_PRIVATE = NO + +# If the EXTRACT_PACKAGE tag is set to YES all members with package or internal +# scope will be included in the documentation. + +EXTRACT_PACKAGE = NO + +# If the EXTRACT_STATIC tag is set to YES all static members of a file +# will be included in the documentation. + +EXTRACT_STATIC = NO + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) +# defined locally in source files will be included in the documentation. +# If set to NO only classes defined in header files are included. + +EXTRACT_LOCAL_CLASSES = YES + +# This flag is only useful for Objective-C code. When set to YES local +# methods, which are defined in the implementation section but not in +# the interface are included in the documentation. +# If set to NO (the default) only methods in the interface are included. + +EXTRACT_LOCAL_METHODS = NO + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base +# name of the file that contains the anonymous namespace. By default +# anonymous namespaces are hidden. + +EXTRACT_ANON_NSPACES = NO + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all +# undocumented members of documented classes, files or namespaces. +# If set to NO (the default) these members will be included in the +# various overviews, but no documentation section is generated. +# This option has no effect if EXTRACT_ALL is enabled. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. +# If set to NO (the default) these classes will be included in the various +# overviews. This option has no effect if EXTRACT_ALL is enabled. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all +# friend (class|struct|union) declarations. +# If set to NO (the default) these declarations will be included in the +# documentation. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any +# documentation blocks found inside the body of a function. +# If set to NO (the default) these blocks will be appended to the +# function's detailed documentation block. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation +# that is typed after a \internal command is included. If the tag is set +# to NO (the default) then the documentation will be excluded. +# Set it to YES to include the internal documentation. + +INTERNAL_DOCS = NO + +# If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate +# file names in lower-case letters. If set to YES upper-case letters are also +# allowed. This is useful if you have classes or files whose names only differ +# in case and if your file system supports case sensitive file names. Windows +# and Mac users are advised to set this option to NO. + +CASE_SENSE_NAMES = YES + +# If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen +# will show members with their full class and namespace scopes in the +# documentation. If set to YES the scope will be hidden. + +HIDE_SCOPE_NAMES = NO + +# If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen +# will put a list of the files that are included by a file in the documentation +# of that file. + +SHOW_INCLUDE_FILES = YES + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then Doxygen +# will list include files with double quotes in the documentation +# rather than with sharp brackets. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES (the default) then a tag [inline] +# is inserted in the documentation for inline members. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen +# will sort the (detailed) documentation of file and class members +# alphabetically by member name. If set to NO the members will appear in +# declaration order. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the +# brief documentation of file, namespace and class members alphabetically +# by member name. If set to NO (the default) the members will appear in +# declaration order. + +SORT_BRIEF_DOCS = NO + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen +# will sort the (brief and detailed) documentation of class members so that +# constructors and destructors are listed first. If set to NO (the default) +# the constructors will appear in the respective orders defined by +# SORT_MEMBER_DOCS and SORT_BRIEF_DOCS. +# This tag will be ignored for brief docs if SORT_BRIEF_DOCS is set to NO +# and ignored for detailed docs if SORT_MEMBER_DOCS is set to NO. + +SORT_MEMBERS_CTORS_1ST = NO + +# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the +# hierarchy of group names into alphabetical order. If set to NO (the default) +# the group names will appear in their defined order. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be +# sorted by fully-qualified names, including namespaces. If set to +# NO (the default), the class list will be sorted only by class name, +# not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the +# alphabetical list. + +SORT_BY_SCOPE_NAME = NO + +# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to +# do proper type resolution of all parameters of a function it will reject a +# match between the prototype and the implementation of a member function even +# if there is only one candidate or it is obvious which candidate to choose +# by doing a simple string match. By disabling STRICT_PROTO_MATCHING doxygen +# will still accept a match between prototype and implementation in such cases. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable (YES) or +# disable (NO) the todo list. This list is created by putting \todo +# commands in the documentation. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable (YES) or +# disable (NO) the test list. This list is created by putting \test +# commands in the documentation. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or +# disable (NO) the bug list. This list is created by putting \bug +# commands in the documentation. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or +# disable (NO) the deprecated list. This list is created by putting +# \deprecated commands in the documentation. + +GENERATE_DEPRECATEDLIST = YES + +# The ENABLED_SECTIONS tag can be used to enable conditional +# documentation sections, marked by \if section-label ... \endif +# and \cond section-label ... \endcond blocks. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines +# the initial value of a variable or macro consists of for it to appear in +# the documentation. If the initializer consists of more lines than specified +# here it will be hidden. Use a value of 0 to hide initializers completely. +# The appearance of the initializer of individual variables and macros in the +# documentation can be controlled using \showinitializer or \hideinitializer +# command in the documentation regardless of this setting. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated +# at the bottom of the documentation of classes and structs. If set to YES the +# list will mention the files that were used to generate the documentation. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. +# This will remove the Files entry from the Quick Index and from the +# Folder Tree View (if specified). The default is YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the +# Namespaces page. +# This will remove the Namespaces entry from the Quick Index +# and from the Folder Tree View (if specified). The default is YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command , where is the value of +# the FILE_VERSION_FILTER tag, and is the name of an input file +# provided by doxygen. Whatever the program writes to standard output +# is used as the file version. See the manual for examples. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents doxygen's defaults, run doxygen with the -l option. +# You can optionally specify a file name after the option, if omitted +# DoxygenLayout.xml will be used as the name of the layout file. + +LAYOUT_FILE = + +# The CITE_BIB_FILES tag can be used to specify one or more bib files +# containing the references data. This must be a list of .bib files. The +# .bib extension is automatically appended if omitted. Using this command +# requires the bibtex tool to be installed. See also +# https://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 https://www.gnu.org/software/libiconv for +# the list of possible encodings. + +INPUT_ENCODING = UTF-8 + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp +# and *.h) to filter out the source-files in the directories. If left +# blank the following patterns are tested: +# *.c *.cc *.cxx *.cpp *.c++ *.d *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh +# *.hxx *.hpp *.h++ *.idl *.odl *.cs *.php *.php3 *.inc *.m *.mm *.dox *.py +# *.f90 *.f *.for *.vhd *.vhdl + +FILE_PATTERNS = + +# The RECURSIVE tag can be used to turn specify whether or not subdirectories +# should be searched for input files as well. Possible values are YES and NO. +# If left blank NO is used. + +RECURSIVE = NO + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# Note that relative paths are relative to the directory from which doxygen is +# run. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. Note that the wildcards are matched +# against the file with absolute path, so to exclude all test directories +# for example use the pattern */test/* + +EXCLUDE_PATTERNS = */*_p.h + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# AClass::ANamespace, ANamespace::*Test + +EXCLUDE_SYMBOLS = LibFmQtData + +# The EXAMPLE_PATH tag can be used to specify one or more files or +# directories that contain example code fragments that are included (see +# the \include command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp +# and *.h) to filter out the source-files in the directories. If left +# blank all files are included. + +EXAMPLE_PATTERNS = + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude +# commands irrespective of the value of the RECURSIVE tag. +# Possible values are YES and NO. If left blank NO is used. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or +# directories that contain image that are included in the documentation (see +# the \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command , where +# is the value of the INPUT_FILTER tag, and is the name of an +# input file. Doxygen will then use the output that the filter program writes +# to standard output. +# If FILTER_PATTERNS is specified, this tag will be ignored. +# Note that the filter must not add or remove lines; it is applied before the +# code is scanned, but not when the output code is generated. If lines are added +# or removed, the anchors will not be placed correctly. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. +# Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. +# The filters are a list of the form: +# pattern = filter (like *.cpp = my_cpp_filter). See INPUT_FILTER for further +# info on how filters are used. If FILTER_PATTERNS is empty or if +# non of the patterns match the file name, INPUT_FILTER is applied. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will be used to filter the input files when producing source +# files to browse (i.e. when SOURCE_BROWSER is set to YES). + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) +# and it is also possible to disable source filtering for a specific pattern +# using *.ext = (so without naming a filter). This option only has effect when +# FILTER_SOURCE_FILES is enabled. + +FILTER_SOURCE_PATTERNS = + +# If the USE_MD_FILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want reuse the introduction page also for the doxygen output. + +USE_MDFILE_AS_MAINPAGE = + +#--------------------------------------------------------------------------- +# configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will +# be generated. Documented entities will be cross-referenced with these sources. +# Note: To get rid of all source code in the generated output, make sure also +# VERBATIM_HEADERS is set to NO. + +SOURCE_BROWSER = NO + +# Setting the INLINE_SOURCES tag to YES will include the body +# of functions and classes directly in the documentation. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct +# doxygen to hide any special comment blocks from generated source code +# fragments. Normal C, C++ and Fortran comments will always remain visible. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES +# then for each documented function all documented +# functions referencing it will be listed. + +REFERENCED_BY_RELATION = NO + +# If the REFERENCES_RELATION tag is set to YES +# then for each documented function all documented entities +# called/used by that function will be listed. + +REFERENCES_RELATION = NO + +# If the REFERENCES_LINK_SOURCE tag is set to YES (the default) +# and SOURCE_BROWSER tag is set to YES, then the hyperlinks from +# functions in REFERENCES_RELATION and REFERENCED_BY_RELATION lists will +# link to the source code. +# Otherwise they will link to the documentation. + +REFERENCES_LINK_SOURCE = YES + +# If the USE_HTAGS tag is set to YES then the references to source code +# will point to the HTML generated by the htags(1) tool instead of doxygen +# built-in source browser. The htags tool is part of GNU's global source +# tagging system (see http://www.gnu.org/software/global/global.html). You +# will need version 4.8.6 or higher. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen +# will generate a verbatim copy of the header file for each class for +# which an include is specified. Set to NO to disable this. + +VERBATIM_HEADERS = YES + +#--------------------------------------------------------------------------- +# configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index +# of all compounds will be generated. Enable this if the project +# contains a lot of classes, structs, unions or interfaces. + +ALPHABETICAL_INDEX = YES + +# If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then +# the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns +# in which this list will be split (can be a number in the range [1..20]) + +COLS_IN_ALPHA_INDEX = 5 + +# In case all classes in a project start with a common prefix, all +# classes will be put under the same header in the alphabetical index. +# The IGNORE_PREFIX tag can be used to specify one or more prefixes that +# should be ignored while generating the index headers. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES (the default) Doxygen will +# generate HTML output. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `html' will be used as the default path. + +HTML_OUTPUT = html + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for +# each generated HTML page (for example: .htm,.php,.asp). If it is left blank +# doxygen will generate files with .html extension. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a personal HTML header for +# each generated HTML page. If it is left blank doxygen will generate a +# standard header. Note that when using a custom header you are responsible +# for the proper inclusion of any scripts and style sheets that doxygen +# needs, which is dependent on the configuration options used. +# It is advised to generate a default header using "doxygen -w html +# header.html footer.html stylesheet.css YourConfigFile" and then modify +# that header. Note that the header is subject to change so you typically +# have to redo this when upgrading to a newer version of doxygen or when +# changing the value of configuration settings such as GENERATE_TREEVIEW! + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a personal HTML footer for +# each generated HTML page. If it is left blank doxygen will generate a +# standard footer. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading +# style sheet that is used by each HTML page. It can be used to +# fine-tune the look of the HTML output. If left blank doxygen will +# generate a default style sheet. Note that it is recommended to use +# HTML_EXTRA_STYLESHEET instead of this one, as it is more robust and this +# tag will in the future become obsolete. + +HTML_STYLESHEET = + +# The HTML_EXTRA_STYLESHEET tag can be used to specify an additional +# user-defined cascading style sheet that is included after the standard +# style sheets created by doxygen. Using this option one can overrule +# certain style aspects. This is preferred over using HTML_STYLESHEET +# since it does not replace the standard style sheet and is therefor more +# robust against future updates. Doxygen will copy the style sheet file to +# the output directory. + +HTML_EXTRA_STYLESHEET = + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that +# the files will be copied as-is; there are no commands or markers available. + +HTML_EXTRA_FILES = + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. +# Doxygen will adjust the colors in the style sheet and background images +# according to this color. Hue is specified as an angle on a colorwheel, +# see https://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 https://developer.apple.com/tools/creatingdocsetswithdoxygen.html +# for more information. + +GENERATE_DOCSET = NO + +# When GENERATE_DOCSET tag is set to YES, this tag determines the name of the +# feed. A documentation feed provides an umbrella under which multiple +# documentation sets from a single provider (such as a company or product suite) +# can be grouped. + +DOCSET_FEEDNAME = "Doxygen generated docs" + +# When GENERATE_DOCSET tag is set to YES, this tag specifies a string that +# should uniquely identify the documentation set bundle. This should be a +# reverse domain-name style string, e.g. com.mycompany.MyDocSet. Doxygen +# will append .docset to the name. + +DOCSET_BUNDLE_ID = org.doxygen.Project + +# When GENERATE_PUBLISHER_ID tag specifies a string that should uniquely +# identify the documentation publisher. This should be a reverse domain-name +# style string, e.g. com.mycompany.MyDocSet.documentation. + +DOCSET_PUBLISHER_ID = org.doxygen.Publisher + +# The GENERATE_PUBLISHER_NAME tag identifies the documentation publisher. + +DOCSET_PUBLISHER_NAME = Publisher + +# If the GENERATE_HTMLHELP tag is set to YES, additional index files +# will be generated that can be used as input for tools like the +# Microsoft HTML help workshop to generate a compiled HTML help file (.chm) +# of the generated HTML documentation. + +GENERATE_HTMLHELP = NO + +# If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can +# be used to specify the file name of the resulting .chm file. You +# can add a path in front of the file if the result should not be +# written to the html output directory. + +CHM_FILE = + +# If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can +# be used to specify the location (absolute path including file name) of +# the HTML help compiler (hhc.exe). If non-empty doxygen will try to run +# the HTML help compiler on the generated index.hhp. + +HHC_LOCATION = + +# If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag +# controls if a separate .chi index file is generated (YES) or that +# it should be included in the master .chm file (NO). + +GENERATE_CHI = NO + +# If the GENERATE_HTMLHELP tag is set to YES, the CHM_INDEX_ENCODING +# is used to encode HtmlHelp index (hhk), content (hhc) and project file +# content. + +CHM_INDEX_ENCODING = + +# If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag +# controls whether a binary table of contents is generated (YES) or a +# normal table of contents (NO) in the .chm file. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members +# to the contents of the HTML help documentation and to the tree view. + +TOC_EXPAND = NO + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated +# that can be used as input for Qt's qhelpgenerator to generate a +# Qt Compressed Help (.qch) of the generated HTML documentation. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can +# be used to specify the file name of the resulting .qch file. +# The path specified is relative to the HTML output folder. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating +# Qt Help Project output. For more information please see +# http://doc.trolltech.com/qthelpproject.html#namespace + +QHP_NAMESPACE = org.doxygen.Project + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating +# Qt Help Project output. For more information please see +# http://doc.trolltech.com/qthelpproject.html#virtual-folders + +QHP_VIRTUAL_FOLDER = doc + +# If QHP_CUST_FILTER_NAME is set, it specifies the name of a custom filter to +# add. For more information please see +# http://doc.trolltech.com/qthelpproject.html#custom-filters + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILT_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see +# +# Qt Help Project / Custom Filters. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's +# filter section matches. +# +# Qt Help Project / Filter Attributes. + +QHP_SECT_FILTER_ATTRS = + +# If the GENERATE_QHP tag is set to YES, the QHG_LOCATION tag can +# be used to specify the location of Qt's qhelpgenerator. +# If non-empty doxygen will try to run qhelpgenerator on the generated +# .qhp file. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files +# will be generated, which together with the HTML files, form an Eclipse help +# plugin. To install this plugin and make it available under the help contents +# menu in Eclipse, the contents of the directory containing the HTML and XML +# files needs to be copied into the plugins directory of eclipse. The name of +# the directory within the plugins directory should be the same as +# the ECLIPSE_DOC_ID value. After copying Eclipse needs to be restarted before +# the help appears. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have +# this name. + +ECLIPSE_DOC_ID = org.doxygen.Project + +# The DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) +# at top of each HTML page. The value NO (the default) enables the index and +# the value YES disables it. Since the tabs have the same information as the +# navigation tree you can set this option to NO if you already set +# GENERATE_TREEVIEW to YES. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. +# If the tag value is set to YES, a side panel will be generated +# containing a tree-like index structure (just like the one that +# is generated for HTML Help). For this to work a browser that supports +# JavaScript, DHTML, CSS and frames is required (i.e. any modern browser). +# Windows users are probably better off using the HTML help feature. +# Since the tree basically has the same information as the tab index you +# could consider to set DISABLE_INDEX to NO when enabling this option. + +GENERATE_TREEVIEW = NO + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values +# (range [0,1..20]) that doxygen will group on one line in the generated HTML +# documentation. Note that a value of 0 will completely suppress the enum +# values from appearing in the overview section. + +ENUM_VALUES_PER_LINE = 4 + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be +# used to set the initial width (in pixels) of the frame in which the tree +# is shown. + +TREEVIEW_WIDTH = 250 + +# When the EXT_LINKS_IN_WINDOW option is set to YES doxygen will open +# links to external symbols imported via tag files in a separate window. + +EXT_LINKS_IN_WINDOW = NO + +# Use this tag to change the font size of Latex formulas included +# as images in the HTML documentation. The default is 10. Note that +# when you change the font size after a successful doxygen run you need +# to manually remove any form_*.png images from the HTML output directory +# to force them to be regenerated. + +FORMULA_FONTSIZE = 10 + +# Use the FORMULA_TRANPARENT tag to determine whether or not the images +# generated for formulas are transparent PNGs. Transparent PNGs are +# not supported properly for IE 6.0, but are supported on all modern browsers. +# Note that when changing this option you need to delete any form_*.png files +# in the HTML output before the changes have effect. + +FORMULA_TRANSPARENT = YES + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax +# (see http://www.mathjax.org) which uses client side Javascript for the +# rendering instead of using prerendered bitmaps. Use this if you do not +# have LaTeX installed or if you want to formulas look prettier in the HTML +# output. When enabled you may also need to install MathJax separately and +# configure the path to it using the MATHJAX_RELPATH option. + +USE_MATHJAX = NO + +# When MathJax is enabled you can set the default output format to be used for +# the MathJax output. Supported types are HTML-CSS, NativeMML (i.e. MathML) and +# SVG. The default value is HTML-CSS, which is slower, but has the best +# compatibility. + +MATHJAX_FORMAT = HTML-CSS + +# When MathJax is enabled you need to specify the location relative to the +# HTML output directory using the MATHJAX_RELPATH option. The destination +# directory should contain the MathJax.js script. For instance, if the mathjax +# directory is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to +# the MathJax Content Delivery Network so you can quickly see the result without +# installing MathJax. +# However, it is strongly recommended to install a local +# copy of MathJax from http://www.mathjax.org before deployment. + +MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest + +# The MATHJAX_EXTENSIONS tag can be used to specify one or MathJax extension +# names that should be enabled during MathJax rendering. + +MATHJAX_EXTENSIONS = + +# The MATHJAX_CODEFILE tag can be used to specify a file with javascript +# pieces of code that will be used on startup of the MathJax code. + +MATHJAX_CODEFILE = + +# When the SEARCHENGINE tag is enabled doxygen will generate a search box +# for the HTML output. The underlying search engine uses javascript +# and DHTML and should work on any modern browser. Note that when using +# HTML help (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets +# (GENERATE_DOCSET) there is already a search function so this one should +# typically be disabled. For large projects the javascript based search engine +# can be slow, then enabling SERVER_BASED_SEARCH may provide a better solution. + +SEARCHENGINE = YES + +# When the SERVER_BASED_SEARCH tag is enabled the search engine will be +# implemented using a web server instead of a web client using Javascript. +# There are two flavours of web server based search depending on the +# EXTERNAL_SEARCH setting. When disabled, doxygen will generate a PHP script for +# searching and an index file used by the script. When EXTERNAL_SEARCH is +# enabled the indexing and searching needs to be provided by external tools. +# See the manual for details. + +SERVER_BASED_SEARCH = NO + +# When EXTERNAL_SEARCH is enabled doxygen will no longer generate the PHP +# script for searching. Instead the search results are written to an XML file +# which needs to be processed by an external indexer. Doxygen will invoke an +# external search engine pointed to by the SEARCHENGINE_URL option to obtain +# the search results. Doxygen ships with an example indexer (doxyindexer) and +# search engine (doxysearch.cgi) which are based on the open source search +# engine library Xapian. See the manual for configuration details. + +EXTERNAL_SEARCH = NO + +# The SEARCHENGINE_URL should point to a search engine hosted by a web server +# which will returned the search results when EXTERNAL_SEARCH is enabled. +# Doxygen ships with an example search engine (doxysearch) which is based on +# the open source search engine library Xapian. See the manual for configuration +# details. + +SEARCHENGINE_URL = + +# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed +# search data is written to a file for indexing by an external tool. With the +# SEARCHDATA_FILE tag the name of this file can be specified. + +SEARCHDATA_FILE = searchdata.xml + +# When SERVER_BASED_SEARCH AND EXTERNAL_SEARCH are both enabled the +# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is +# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple +# projects and redirect the results back to the right project. + +EXTERNAL_SEARCH_ID = + +# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen +# projects other than the one defined by this configuration file, but that are +# all added to the same external search index. Each project needs to have a +# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id +# of to a relative location where the documentation can be found. +# The format is: EXTRA_SEARCH_MAPPINGS = id1 = loc1 id2 = loc2 ... + +EXTRA_SEARCH_MAPPINGS = + +#--------------------------------------------------------------------------- +# configuration options related to the LaTeX output +#--------------------------------------------------------------------------- + +# If the GENERATE_LATEX tag is set to YES (the default) Doxygen will +# generate Latex output. + +GENERATE_LATEX = NO + +# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `latex' will be used as the default path. + +LATEX_OUTPUT = latex + +# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be +# invoked. If left blank `latex' will be used as the default command name. +# Note that when enabling USE_PDFLATEX this option is only used for +# generating bitmaps for formulas in the HTML output, but not in the +# Makefile that is written to the output directory. + +LATEX_CMD_NAME = latex + +# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to +# generate index for LaTeX. If left blank `makeindex' will be used as the +# default command name. + +MAKEINDEX_CMD_NAME = makeindex + +# If the COMPACT_LATEX tag is set to YES Doxygen generates more compact +# LaTeX documents. This may be useful for small projects and may help to +# save some trees in general. + +COMPACT_LATEX = NO + +# The PAPER_TYPE tag can be used to set the paper type that is used +# by the printer. Possible values are: a4, letter, legal and +# executive. If left blank a4 will be used. + +PAPER_TYPE = a4 + +# The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX +# packages that should be included in the LaTeX output. + +EXTRA_PACKAGES = + +# The LATEX_HEADER tag can be used to specify a personal LaTeX header for +# the generated latex document. The header should contain everything until +# the first chapter. If it is left blank doxygen will generate a +# standard header. Notice: only use this tag if you know what you are doing! + +LATEX_HEADER = + +# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for +# the generated latex document. The footer should contain everything after +# the last chapter. If it is left blank doxygen will generate a +# standard footer. Notice: only use this tag if you know what you are doing! + +LATEX_FOOTER = + +# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images +# or other source files which should be copied to the LaTeX output directory. +# Note that the files will be copied as-is; there are no commands or markers +# available. + +LATEX_EXTRA_FILES = + +# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated +# is prepared for conversion to pdf (using ps2pdf). The pdf file will +# contain links (just like the HTML output) instead of page references +# This makes the output suitable for online browsing using a pdf viewer. + +PDF_HYPERLINKS = YES + +# If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of +# plain latex in the generated Makefile. Set this option to YES to get a +# higher quality PDF documentation. + +USE_PDFLATEX = YES + +# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode. +# command to the generated LaTeX files. This will instruct LaTeX to keep +# running if errors occur, instead of asking the user for help. +# This option is also used when generating formulas in HTML. + +LATEX_BATCHMODE = NO + +# If LATEX_HIDE_INDICES is set to YES then doxygen will not +# include the index chapters (such as File Index, Compound Index, etc.) +# in the output. + +LATEX_HIDE_INDICES = NO + +# If LATEX_SOURCE_CODE is set to YES then doxygen will include +# source code with syntax highlighting in the LaTeX output. +# Note that which sources are shown also depends on other settings +# such as SOURCE_BROWSER. + +LATEX_SOURCE_CODE = NO + +# The LATEX_BIB_STYLE tag can be used to specify the style to use for the +# bibliography, e.g. plainnat, or ieeetr. The default style is "plain". See +# http://en.wikipedia.org/wiki/BibTeX for more info. + +LATEX_BIB_STYLE = plain + +#--------------------------------------------------------------------------- +# configuration options related to the RTF output +#--------------------------------------------------------------------------- + +# If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output +# The RTF output is optimized for Word 97 and may not look very pretty with +# other RTF readers or editors. + +GENERATE_RTF = NO + +# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `rtf' will be used as the default path. + +RTF_OUTPUT = rtf + +# If the COMPACT_RTF tag is set to YES Doxygen generates more compact +# RTF documents. This may be useful for small projects and may help to +# save some trees in general. + +COMPACT_RTF = NO + +# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated +# will contain hyperlink fields. The RTF file will +# contain links (just like the HTML output) instead of page references. +# This makes the output suitable for online browsing using WORD or other +# programs which support those fields. +# Note: wordpad (write) and others do not support links. + +RTF_HYPERLINKS = NO + +# Load style sheet definitions from file. Syntax is similar to doxygen's +# config file, i.e. a series of assignments. You only have to provide +# replacements, missing definitions are set to their default value. + +RTF_STYLESHEET_FILE = + +# Set optional variables used in the generation of an rtf document. +# Syntax is similar to doxygen's config file. + +RTF_EXTENSIONS_FILE = + +#--------------------------------------------------------------------------- +# configuration options related to the man page output +#--------------------------------------------------------------------------- + +# If the GENERATE_MAN tag is set to YES (the default) Doxygen will +# generate man pages + +GENERATE_MAN = NO + +# The MAN_OUTPUT tag is used to specify where the man pages will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `man' will be used as the default path. + +MAN_OUTPUT = man + +# The MAN_EXTENSION tag determines the extension that is added to +# the generated man pages (default is the subroutine's section .3) + +MAN_EXTENSION = .3 + +# If the MAN_LINKS tag is set to YES and Doxygen generates man output, +# then it will generate one additional man file for each entity +# documented in the real man page(s). These additional files +# only source the real man page, but without them the man command +# would be unable to find the correct page. The default is NO. + +MAN_LINKS = NO + +#--------------------------------------------------------------------------- +# configuration options related to the XML output +#--------------------------------------------------------------------------- + +# If the GENERATE_XML tag is set to YES Doxygen will +# generate an XML file that captures the structure of +# the code including all documentation. + +GENERATE_XML = NO + +# The XML_OUTPUT tag is used to specify where the XML pages will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be +# put in front of it. If left blank `xml' will be used as the default path. + +XML_OUTPUT = xml + +# The XML_SCHEMA tag can be used to specify an XML schema, +# which can be used by a validating XML parser to check the +# syntax of the XML files. + +XML_SCHEMA = + +# The XML_DTD tag can be used to specify an XML DTD, +# which can be used by a validating XML parser to check the +# syntax of the XML files. + +XML_DTD = + +# If the XML_PROGRAMLISTING tag is set to YES Doxygen will +# dump the program listings (including syntax highlighting +# and cross-referencing information) to the XML output. Note that +# enabling this will significantly increase the size of the XML output. + +XML_PROGRAMLISTING = YES + +#--------------------------------------------------------------------------- +# configuration options related to the DOCBOOK output +#--------------------------------------------------------------------------- + +# If the GENERATE_DOCBOOK tag is set to YES Doxygen will generate DOCBOOK files +# that can be used to generate PDF. + +GENERATE_DOCBOOK = NO + +# The DOCBOOK_OUTPUT tag is used to specify where the DOCBOOK pages will be put. +# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in +# front of it. If left blank docbook will be used as the default path. + +DOCBOOK_OUTPUT = docbook + +#--------------------------------------------------------------------------- +# configuration options for the AutoGen Definitions output +#--------------------------------------------------------------------------- + +# If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will +# generate an AutoGen Definitions (see autogen.sf.net) file +# that captures the structure of the code including all +# documentation. Note that this feature is still experimental +# and incomplete at the moment. + +GENERATE_AUTOGEN_DEF = NO + +#--------------------------------------------------------------------------- +# configuration options related to the Perl module output +#--------------------------------------------------------------------------- + +# If the GENERATE_PERLMOD tag is set to YES Doxygen will +# generate a Perl module file that captures the structure of +# the code including all documentation. Note that this +# feature is still experimental and incomplete at the +# moment. + +GENERATE_PERLMOD = NO + +# If the PERLMOD_LATEX tag is set to YES Doxygen will generate +# the necessary Makefile rules, Perl scripts and LaTeX code to be able +# to generate PDF and DVI output from the Perl module output. + +PERLMOD_LATEX = NO + +# If the PERLMOD_PRETTY tag is set to YES the Perl module output will be +# nicely formatted so it can be parsed by a human reader. +# This is useful +# if you want to understand what is going on. +# On the other hand, if this +# tag is set to NO the size of the Perl module output will be much smaller +# and Perl will parse it just the same. + +PERLMOD_PRETTY = YES + +# The names of the make variables in the generated doxyrules.make file +# are prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. +# This is useful so different doxyrules.make files included by the same +# Makefile don't overwrite each other's variables. + +PERLMOD_MAKEVAR_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the preprocessor +#--------------------------------------------------------------------------- + +# If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will +# evaluate all C-preprocessor directives found in the sources and include +# files. + +ENABLE_PREPROCESSING = YES + +# If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro +# names in the source code. If set to NO (the default) only conditional +# compilation will be performed. Macro expansion can be done in a controlled +# way by setting EXPAND_ONLY_PREDEF to YES. + +MACRO_EXPANSION = NO + +# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES +# then the macro expansion is limited to the macros specified with the +# PREDEFINED and EXPAND_AS_DEFINED tags. + +EXPAND_ONLY_PREDEF = NO + +# If the SEARCH_INCLUDES tag is set to YES (the default) the includes files +# pointed to by INCLUDE_PATH will be searched when a #include is found. + +SEARCH_INCLUDES = YES + +# The INCLUDE_PATH tag can be used to specify one or more directories that +# contain include files that are not input files but should be processed by +# the preprocessor. + +INCLUDE_PATH = + +# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard +# patterns (like *.h and *.hpp) to filter out the header-files in the +# directories. If left blank, the patterns specified with FILE_PATTERNS will +# be used. + +INCLUDE_FILE_PATTERNS = + +# The PREDEFINED tag can be used to specify one or more macro names that +# are defined before the preprocessor is started (similar to the -D option of +# gcc). The argument of the tag is a list of macros of the form: name +# or name = definition (no spaces). If the definition and the = are +# omitted = 1 is assumed. To prevent a macro definition from being +# undefined via #undef or recursively expanded use the : = operator +# instead of the = operator. + +PREDEFINED = + +# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then +# this tag can be used to specify a list of macro names that should be expanded. +# The macro definition that is found in the sources will be used. +# Use the PREDEFINED tag if you want to use a different macro definition that +# overrules the definition found in the source code. + +EXPAND_AS_DEFINED = + +# If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then +# doxygen's preprocessor will remove all references to function-like macros +# that are alone on a line, have an all uppercase name, and do not end with a +# semicolon, because these will confuse the parser if not removed. + +SKIP_FUNCTION_MACROS = YES + +#--------------------------------------------------------------------------- +# Configuration::additions related to external references +#--------------------------------------------------------------------------- + +# The TAGFILES option can be used to specify one or more tagfiles. For each +# tag file the location of the external documentation should be added. The +# format of a tag file without this location is as follows: +# +# TAGFILES = file1 file2 ... +# Adding location for the tag files is done as follows: +# +# TAGFILES = file1 = loc1 "file2 = loc2" ... +# where "loc1" and "loc2" can be relative or absolute paths +# or URLs. Note that each tag file must have a unique name (where the name does +# NOT include the path). If a tag file is not located in the directory in which +# doxygen is run, you must also specify the path to the tagfile here. + +TAGFILES = + +# When a file name is specified after GENERATE_TAGFILE, doxygen will create +# a tag file that is based on the input files it reads. + +GENERATE_TAGFILE = + +# If the ALLEXTERNALS tag is set to YES all external classes will be listed +# in the class index. If set to NO only the inherited external classes +# will be listed. + +ALLEXTERNALS = NO + +# If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed +# in the modules index. If set to NO, only the current project's groups will +# be listed. + +EXTERNAL_GROUPS = YES + +# If the EXTERNAL_PAGES tag is set to YES all external pages will be listed +# in the related pages index. If set to NO, only the current project's +# pages will be listed. + +EXTERNAL_PAGES = YES + +# The PERL_PATH should be the absolute path and name of the perl script +# interpreter (i.e. the result of `which perl'). + +PERL_PATH = /usr/bin/perl + +#--------------------------------------------------------------------------- +# Configuration options related to the dot tool +#--------------------------------------------------------------------------- + +# If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will +# generate a inheritance diagram (in HTML, RTF and LaTeX) for classes with base +# or super classes. Setting the tag to NO turns the diagrams off. Note that +# this option also works with HAVE_DOT disabled, but it is recommended to +# install and use dot, since it yields more powerful graphs. + +CLASS_DIAGRAMS = YES + +# You can define message sequence charts within doxygen comments using the \msc +# command. Doxygen will then run the mscgen tool (see +# http://www.mcternan.me.uk/mscgen/) to produce the chart and insert it in the +# documentation. The MSCGEN_PATH tag allows you to specify the directory where +# the mscgen tool resides. If left empty the tool is assumed to be found in the +# default search path. + +MSCGEN_PATH = + +# If set to YES, the inheritance and collaboration graphs will hide +# inheritance and usage relations if the target is undocumented +# or is not a class. + +HIDE_UNDOC_RELATIONS = YES + +# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is +# available from the path. This tool is part of Graphviz, a graph visualization +# toolkit from AT&T and Lucent Bell Labs. The other options in this section +# have no effect if this option is set to NO (the default) + +HAVE_DOT = NO + +# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is +# allowed to run in parallel. When set to 0 (the default) doxygen will +# base this on the number of processors available in the system. You can set it +# explicitly to a value larger than 0 to get control over the balance +# between CPU load and processing speed. + +DOT_NUM_THREADS = 0 + +# By default doxygen will use the Helvetica font for all dot files that +# doxygen generates. When you want a differently looking font you can specify +# the font name using DOT_FONTNAME. You need to make sure dot is able to find +# the font, which can be done by putting it in a standard location or by setting +# the DOTFONTPATH environment variable or by setting DOT_FONTPATH to the +# directory containing the font. + +DOT_FONTNAME = Helvetica + +# The DOT_FONTSIZE tag can be used to set the size of the font of dot graphs. +# The default size is 10pt. + +DOT_FONTSIZE = 10 + +# By default doxygen will tell dot to use the Helvetica font. +# If you specify a different font using DOT_FONTNAME you can use DOT_FONTPATH to +# set the path where dot can find it. + +DOT_FONTPATH = + +# If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for each documented class showing the direct and +# indirect inheritance relations. Setting this tag to YES will force the +# CLASS_DIAGRAMS tag to NO. + +CLASS_GRAPH = YES + +# If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for each documented class showing the direct and +# indirect implementation dependencies (inheritance, containment, and +# class references variables) of the class with other documented classes. + +COLLABORATION_GRAPH = YES + +# If the GROUP_GRAPHS and HAVE_DOT tags are set to YES then doxygen +# will generate a graph for groups, showing the direct groups dependencies + +GROUP_GRAPHS = YES + +# If the UML_LOOK tag is set to YES doxygen will generate inheritance and +# collaboration diagrams in a style similar to the OMG's Unified Modeling +# Language. + +UML_LOOK = NO + +# If the UML_LOOK tag is enabled, the fields and methods are shown inside +# the class node. If there are many fields or methods and many nodes the +# graph may become too big to be useful. The UML_LIMIT_NUM_FIELDS +# threshold limits the number of items for each type to make the size more +# manageable. Set this to 0 for no limit. Note that the threshold may be +# exceeded by 50% before the limit is enforced. + +UML_LIMIT_NUM_FIELDS = 10 + +# If set to YES, the inheritance and collaboration graphs will show the +# relations between templates and their instances. + +TEMPLATE_RELATIONS = NO + +# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT +# tags are set to YES then doxygen will generate a graph for each documented +# file showing the direct and indirect include dependencies of the file with +# other documented files. + +INCLUDE_GRAPH = YES + +# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and +# HAVE_DOT tags are set to YES then doxygen will generate a graph for each +# documented header file showing the documented files that directly or +# indirectly include this file. + +INCLUDED_BY_GRAPH = YES + +# If the CALL_GRAPH and HAVE_DOT options are set to YES then +# doxygen will generate a call dependency graph for every global function +# or class method. Note that enabling this option will significantly increase +# the time of a run. So in most cases it will be better to enable call graphs +# for selected functions only using the \callgraph command. + +CALL_GRAPH = NO + +# If the CALLER_GRAPH and HAVE_DOT tags are set to YES then +# doxygen will generate a caller dependency graph for every global function +# or class method. Note that enabling this option will significantly increase +# the time of a run. So in most cases it will be better to enable caller +# graphs for selected functions only using the \callergraph command. + +CALLER_GRAPH = NO + +# If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen +# will generate a graphical hierarchy of all classes instead of a textual one. + +GRAPHICAL_HIERARCHY = YES + +# If the DIRECTORY_GRAPH and HAVE_DOT tags are set to YES +# then doxygen will show the dependencies a directory has on other directories +# in a graphical way. The dependency relations are determined by the #include +# relations between the files in the directories. + +DIRECTORY_GRAPH = YES + +# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images +# generated by dot. Possible values are svg, png, jpg, or gif. +# If left blank png will be used. If you choose svg you need to set +# HTML_FILE_EXTENSION to xhtml in order to make the SVG files +# visible in IE 9+ (other browsers do not have this requirement). + +DOT_IMAGE_FORMAT = png + +# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to +# enable generation of interactive SVG images that allow zooming and panning. +# Note that this requires a modern browser other than Internet Explorer. +# Tested and working are Firefox, Chrome, Safari, and Opera. For IE 9+ you +# need to set HTML_FILE_EXTENSION to xhtml in order to make the SVG files +# visible. Older versions of IE do not have SVG support. + +INTERACTIVE_SVG = NO + +# The tag DOT_PATH can be used to specify the path where the dot tool can be +# found. If left blank, it is assumed the dot tool can be found in the path. + +DOT_PATH = + +# The DOTFILE_DIRS tag can be used to specify one or more directories that +# contain dot files that are included in the documentation (see the +# \dotfile command). + +DOTFILE_DIRS = + +# The MSCFILE_DIRS tag can be used to specify one or more directories that +# contain msc files that are included in the documentation (see the +# \mscfile command). + +MSCFILE_DIRS = + +# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of +# nodes that will be shown in the graph. If the number of nodes in a graph +# becomes larger than this value, doxygen will truncate the graph, which is +# visualized by representing a node as a red box. Note that doxygen if the +# number of direct children of the root node in a graph is already larger than +# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note +# that the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH. + +DOT_GRAPH_MAX_NODES = 50 + +# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the +# graphs generated by dot. A depth value of 3 means that only nodes reachable +# from the root by following a path via at most 3 edges will be shown. Nodes +# that lay further from the root node will be omitted. Note that setting this +# option to 1 or 2 may greatly reduce the computation time needed for large +# code bases. Also note that the size of a graph can be further restricted by +# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction. + +MAX_DOT_GRAPH_DEPTH = 0 + +# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent +# background. This is disabled by default, because dot on Windows does not +# seem to support this out of the box. Warning: Depending on the platform used, +# enabling this option may lead to badly anti-aliased labels on the edges of +# a graph (i.e. they become hard to read). + +DOT_TRANSPARENT = NO + +# Set the DOT_MULTI_TARGETS tag to YES allow dot to generate multiple output +# files in one run (i.e. multiple -o and -T options on the command line). This +# makes dot run faster, but since only newer versions of dot (>1.8.10) +# support this, this feature is disabled by default. + +DOT_MULTI_TARGETS = YES + +# If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will +# generate a legend page explaining the meaning of the various boxes and +# arrows in the dot generated graphs. + +GENERATE_LEGEND = YES + +# If the DOT_CLEANUP tag is set to YES (the default) Doxygen will +# remove the intermediate dot files that are used to generate +# the various graphs. + +DOT_CLEANUP = YES diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..20fb9c7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,458 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS diff --git a/README.md b/README.md new file mode 100644 index 0000000..1be82fa --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# libfm-qt + +## Overview + +libfm-qt is the Qt port of libfm, a library providing components to build +desktop file managers which belongs to [LXDE](https://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/lxqt/lxqt-build-tools) and optionally Git +to pull latest VCS checkouts. The localization files were outsourced to +repository [lxqt-l10n](https://github.com/lxqt/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/lxqt/pcmanfm-qt/issues. + + +### Translation (Weblate) + + +Translation status + diff --git a/cmake/fm-qt-config.cmake.in b/cmake/fm-qt-config.cmake.in new file mode 100644 index 0000000..a44783f --- /dev/null +++ b/cmake/fm-qt-config.cmake.in @@ -0,0 +1,41 @@ +#============================================================================= +# Copyright 2015 Luís Pereira +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#============================================================================= + +@PACKAGE_INIT@ + +if (CMAKE_VERSION VERSION_LESS 3.0.2) + message(FATAL_ERROR \"fm-qt requires at least CMake version 3.0.2\") +endif() + +include(CMakeFindDependencyMacro) + +if (NOT TARGET @LIBFM_QT_LIBRARY_NAME@) + if (POLICY CMP0024) + cmake_policy(SET CMP0024 NEW) + endif() + include("${CMAKE_CURRENT_LIST_DIR}/@LIBFM_QT_LIBRARY_NAME@-targets.cmake") +endif() diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt new file mode 100644 index 0000000..9e9cd2c --- /dev/null +++ b/data/CMakeLists.txt @@ -0,0 +1,10 @@ +install(FILES + "archivers.list" + "terminals.list" + DESTINATION "${CMAKE_INSTALL_DATADIR}/libfm-qt" +) + +install(FILES + "libfm-qt-mimetypes.xml" + DESTINATION "${CMAKE_INSTALL_DATADIR}/mime/packages" +) diff --git a/data/archivers.list b/data/archivers.list new file mode 100644 index 0000000..e1afe7e --- /dev/null +++ b/data/archivers.list @@ -0,0 +1,42 @@ +[lxqt-archiver] +create=lxqt-archiver --add %U +extract=lxqt-archiver --extract %U +extract_to=lxqt-archiver --extract-to %d %U +mime_types=application/x-7z-compressed;application/x-7z-compressed-tar;application/x-ace;application/x-alz;application/x-ar;application/x-arj;application/x-bzip;application/x-bzip-compressed-tar;application/x-bzip1;application/x-bzip1-compressed-tar;application/x-cabinet;application/x-cbr;application/x-cbz;application/x-cd-image;application/x-compress;application/x-compressed-tar;application/x-cpio;application/x-deb;application/x-ear;application/x-ms-dos-executable;application/x-gtar;application/x-gzip;application/x-gzpostscript;application/x-java-archive;application/x-lha;application/x-lhz;application/x-lzip;application/x-lzip-compressed-tar;application/x-lzma;application/x-lzma-compressed-tar;application/x-lzop;application/x-lzop-compressed-tar;application/x-rar;application/x-rar-compressed;application/vnd.rar;application/x-rpm;application/x-rzip;application/x-tar;application/x-tarz;application/x-stuffit;application/x-war;application/x-xz;application/x-xz-compressed-tar;application/x-zip;application/x-zip-compressed;application/x-zoo;application/zip;multipart/x-zip; +supports_uris=true + +[file-roller] +create=file-roller --add %U +extract=file-roller --extract %U +extract_to=file-roller --extract-to %d %U +mime_types=application/x-7z-compressed;application/x-7z-compressed-tar;application/x-ace;application/x-alz;application/x-ar;application/x-arj;application/x-bzip;application/x-bzip-compressed-tar;application/x-bzip1;application/x-bzip1-compressed-tar;application/x-cabinet;application/x-cbr;application/x-cbz;application/x-cd-image;application/x-compress;application/x-compressed-tar;application/x-cpio;application/x-deb;application/x-ear;application/x-ms-dos-executable;application/x-gtar;application/x-gzip;application/x-gzpostscript;application/x-java-archive;application/x-lha;application/x-lhz;application/x-lzip;application/x-lzip-compressed-tar;application/x-lzma;application/x-lzma-compressed-tar;application/x-lzop;application/x-lzop-compressed-tar;application/x-rar;application/x-rar-compressed;application/vnd.rar;application/x-rpm;application/x-rzip;application/x-tar;application/x-tarz;application/x-stuffit;application/x-war;application/x-xz;application/x-xz-compressed-tar;application/x-zip;application/x-zip-compressed;application/x-zoo;application/zip;multipart/x-zip; +supports_uris=true + +[xarchiver] +create=xarchiver --add-to %F +extract=xarchiver --extract %F +extract_to=xarchiver --extract-to %d %F +mime_types=application/x-arj;application/arj;application/x-bzip;application/x-bzip-compressed-tar;application/x-gzip;application/x-rar;application/x-rar-compressed;application/vnd.rar;application/x-tar;application/x-zip;application/x-zip-compressed;application/zip;multipart/x-zip;application/x-7z-compressed;application/x-compressed-tar;application/x-bzip2;application/x-bzip2-compressed-tar;application/x-lzma-compressed-tar;application/x-lzma;application/x-deb;application/deb;application/vnd.debian.binary-package;application/x-xz;application/x-xz-compressed-tar;application/x-rpm;application/x-source-rpm;application/x-lzop;application/x-lzop-compressed-tar;application/x-tzo;application/x-war;application/x-compress;application/x-tarz;application/x-java-archive;application/x-lha;application/x-lhz; + +[squeeze] +create=squeeze --new %F +extract=squeeze --extract %F +extract_to=squeeze --extract-to %d %F +mime_types=application/x-bzip-compressed-tar;application/x-bzip2-compressed-tar;application/x-compressed-tar;application/x-tar;application/x-tarz;application/x-tzo;application/x-zip;application/x-zip-compressed;application/zip;application/x-rar;application/vnd.rar;application/x-gzip;application/x-bzip;application/x-lzop;application/x-compress; + +[engrampa] +create=engrampa --add %U +extract=engrampa --extract %U +extract_to=engrampa --extract-to %d %U +mime_types=application/x-7z-compressed;application/x-7z-compressed-tar;application/x-ace;application/x-alz;application/x-ar;application/x-arj;application/x-bzip;application/x-bzip-compressed-tar;application/x-bzip1;application/x-bzip1-compressed-tar;application/x-cabinet;application/x-cbr;application/x-cbz;application/x-cd-image;application/x-compress;application/x-compressed-tar;application/x-cpio;application/x-deb;application/x-ear;application/x-ms-dos-executable;application/x-gtar;application/x-gzip;application/x-gzpostscript;application/x-java-archive;application/x-lha;application/x-lhz;application/x-lzip;application/x-lzip-compressed-tar;application/x-lzma;application/x-lzma-compressed-tar;application/x-lzop;application/x-lzop-compressed-tar;application/x-rar;application/x-rar-compressed;application/vnd.rar;application/x-rpm;application/x-rzip;application/x-tar;application/x-tarz;application/x-stuffit;application/x-war;application/x-xz;application/x-xz-compressed-tar;application/x-zip;application/x-zip-compressed;application/x-zoo;application/zip;multipart/x-zip; +supports_uris=true + +# The KDE archiver Ark +# Here we use %F instead of %U since KDE programs do not know the URI provided by gvfs. +# GIO will pass FUSE-based file paths to the KDE programs, which should still work. +[ark] +create=ark --add --dialog %F +extract=ark --batch --dialog %F +extract_to=ark --batch --destination %d %F +mime_types=application/x-tar;application/x-compressed-tar;application/x-bzip-compressed-tar;application/x-tarz;application/x-xz-compressed-tar;application/x-lzma-compressed-tar;application/x-deb;application/x-cd-image;application/x-bcpio;application/x-cpio;application/x-cpio-compressed;application/x-sv4cpio;application/x-sv4crc;application/x-rpm;application/x-source-rpm;application/vnd.ms-cab-compressed;application/x-servicepack;application/x-rar;application/vnd.rar;application/x-7z-compressed;application/x-java-archive;application/zip;application/x-compress;application/x-gzip;application/x-bzip;application/x-bzip2;application/x-lzma;application/x-xz;application/lha;application/x-lha;application/maclha; +supports_uris=true diff --git a/data/libfm-qt-mimetypes.xml b/data/libfm-qt-mimetypes.xml new file mode 100644 index 0000000..7f509a9 --- /dev/null +++ b/data/libfm-qt-mimetypes.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + Windows installer + Windows 安裝程式 + + + + + MS VBScript + + + + + C# source + C# 程式碼 + + + + + 應用程式捷徑 + + + + + + + + + + + + + + diff --git a/data/terminals.list b/data/terminals.list new file mode 100644 index 0000000..3c49a10 --- /dev/null +++ b/data/terminals.list @@ -0,0 +1,80 @@ +[xterm] +open_arg=-e +noclose_arg=-hold -e +desktop_id=xterm.desktop + +[uxterm] +open_arg=-e +noclose_arg=-hold -e + +[lxterminal] +open_arg=-e +desktop_id=lxterminal.desktop + +[konsole] +open_arg=-e +noclose_arg=--noclose -e +desktop_id=konsole.desktop + +[xfce4-terminal] +open_arg=-x +noclose_arg=--hold -x +desktop_id=xfce4-terminal.desktop + +[terminator] +open_arg=-x +desktop_id=terminator.desktop + +[rxvt] +open_arg=-e + +[urxvt] +open_arg=-e +noclose_arg=-hold -e +desktop_id=rxvt-unicode.desktop + +[eterm] +open_arg=-e +noclose_arg=--pause -e +desktop_id=eterm.desktop + +[gnome-terminal] +open_arg=-x +desktop_id=gnome-terminal.desktop + +[wterm] +open_arg=-e + +[roxterm] +open_arg=-e +desktop_id=roxterm.desktop + +[sakura] +open_arg=-e +desktop_id=sakura.desktop + +[qterminal] +open_arg=-e +desktop_id=qterminal.desktop + +[lilyterm] +open_arg=-e +noclose_arg=--hold -e +desktop_id=lilyterm.desktop + +[urxvtc] +open_arg=-e +noclose_arg=-hold -e + +[terminology] +open_arg=-e +noclose_arg=--hold -e +desktop_id=terminology.desktop + +[termite] +open_arg=-e +noclose_arg=--hold -e +desktop_id=termite.desktop + +[kitty] +desktop_id=kitty.desktop diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..2058156 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,281 @@ +set(libfm_core_SRCS + # gio gvfs implementations + core/vfs/fm-file.c + core/vfs/fm-file.h + core/vfs/fm-xml-file.c + core/vfs/fm-xml-file.h + core/vfs/vfs-menu.c + core/vfs/vfs-search.c + # other legacy C code + core/legacy/fm-config.c + core/legacy/fm-app-info.c + # core data structures + core/gobjectptr.h + core/filepath.cpp + core/iconinfo.cpp + core/mimetype.cpp + core/fileinfo.cpp + core/folder.cpp + core/folderconfig.cpp + core/filemonitor.cpp + # i/o jobs + core/job.cpp + core/filetransferjob.cpp + core/deletejob.cpp + core/dirlistjob.cpp + core/filechangeattrjob.cpp + core/fileinfojob.cpp + core/filelinkjob.cpp + core/fileoperationjob.cpp + core/filesysteminfojob.cpp + core/job.cpp + core/totalsizejob.cpp + core/trashjob.cpp + core/untrashjob.cpp + core/thumbnailjob.cpp + # extra desktop services + core/bookmarks.cpp + core/basicfilelauncher.cpp + core/volumemanager.cpp + core/userinfocache.cpp + core/thumbnailer.cpp + core/terminal.cpp + core/archiver.cpp + core/templates.cpp + # custom actions + customactions/fileaction.cpp + customactions/fileactionprofile.cpp + customactions/fileactioncondition.cpp +) + +set(libfm_SRCS + ${libfm_core_SRCS} + libfmqt.cpp + bookmarkaction.cpp + sidepane.cpp + filelauncher.cpp + foldermodel.cpp + foldermodelitem.cpp + cachedfoldermodel.cpp + proxyfoldermodel.cpp + folderview.cpp + folderitemdelegate.cpp + createnewmenu.cpp + filemenu.cpp + foldermenu.cpp + filepropsdialog.cpp + applaunchcontext.cpp + placesview.cpp + placesmodel.cpp + placesmodelitem.cpp + dirtreeview.cpp + dirtreemodel.cpp + dirtreemodelitem.cpp + dnddest.cpp + mountoperation.cpp + mountoperationpassworddialog.cpp + mountoperationquestiondialog.cpp + fileoperation.cpp + fileoperationdialog.cpp + renamedialog.cpp + pathedit.cpp + pathbar.cpp + colorbutton.cpp + fontbutton.cpp + browsehistory.cpp + utilities.cpp + dndactionmenu.cpp + editbookmarksdialog.cpp + execfiledialog.cpp + appchoosercombobox.cpp + appmenuview.cpp + appchooserdialog.cpp + filesearchdialog.cpp + filedialog.cpp + fm-search.c # might be moved to libfm later + xdndworkaround.cpp + filedialoghelper.cpp +) + +set(libfm_UIS + file-props.ui + file-operation-dialog.ui + rename-dialog.ui + mount-operation-password.ui + edit-bookmarks.ui + exec-file.ui + app-chooser-dialog.ui + filesearch.ui + filedialog.ui +) + +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" +) + +add_library(${LIBFM_QT_LIBRARY_NAME} SHARED + ${libfm_SRCS} + ${libfm_UIS} + ${QM_FILES} +) + +install(EXPORT + "${LIBFM_QT_LIBRARY_NAME}-targets" + DESTINATION "${CMAKE_INSTALL_DATADIR}/cmake/${LIBFM_QT_LIBRARY_NAME}" + COMPONENT Devel +) + +target_link_libraries(${LIBFM_QT_LIBRARY_NAME} + Qt5::Widgets + Qt5::X11Extras + ${GLIB_LIBRARIES} + ${GLIB_GIO_LIBRARIES} + ${GLIB_GOBJECT_LIBRARIES} + ${GLIB_GTHREAD_LIBRARIES} + ${MENUCACHE_LIBRARIES} + ${XCB_LIBRARIES} + ${EXIF_LIBRARIES} +) + +# set libtool soname +set_target_properties(${LIBFM_QT_LIBRARY_NAME} PROPERTIES + VERSION ${LIBFM_QT_ABI_VERSION} + SOVERSION ${LIBFM_QT_SOVERSION} +) + +target_include_directories(${LIBFM_QT_LIBRARY_NAME} + PRIVATE "${Qt5Gui_PRIVATE_INCLUDE_DIRS}" + core/legacy + PUBLIC + "${GLIB_INCLUDE_DIRS}" + "${GLIB_GIO_UNIX_INCLUDE_DIR}" + "${MENUCACHE_INCLUDE_DIRS}" + "${XCB_INCLUDE_DIRS}" + "${EXIF_INCLUDE_DIRS}" + INTERFACE + "$" + "$" +) + +target_compile_definitions(${LIBFM_QT_LIBRARY_NAME} + PRIVATE "LIBFM_QT_DATA_DIR=\"${LIBFM_QT_DATA_DIR}\"" + "GETTEXT_PACKAGE=\"\"" + PUBLIC "QT_NO_KEYWORDS" +) + +install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/${LIBFM_QT_LIBRARY_NAME}_export.h" + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/libfm-qt" + COMPONENT Devel +) + +# install include header files (FIXME: can we make this cleaner? should dir name be versioned?) +install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/ + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/libfm-qt" + COMPONENT Devel + FILES_MATCHING PATTERN "*.h" +) + +generate_export_header(${LIBFM_QT_LIBRARY_NAME} + EXPORT_MACRO_NAME LIBFM_QT_API +) + +# InTree build +file(COPY ${CMAKE_CURRENT_BINARY_DIR}/${LIBFM_QT_LIBRARY_NAME}_export.h + DESTINATION "${LIBFM_QT_INTREE_INCLUDE_DIR}/libfm-qt" +) + +file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/ + DESTINATION "${LIBFM_QT_INTREE_INCLUDE_DIR}/libfm-qt" + FILES_MATCHING PATTERN "*.h" +) + +configure_package_config_file( + "${PROJECT_SOURCE_DIR}/cmake/fm-qt-config.cmake.in" + "${CMAKE_BINARY_DIR}/${LIBFM_QT_LIBRARY_NAME}-config.cmake" + INSTALL_DESTINATION "${CMAKE_INSTALL_DATADIR}/cmake/${LIBFM_QT_LIBRARY_NAME}" +) + +install(FILES + "${CMAKE_BINARY_DIR}/${LIBFM_QT_LIBRARY_NAME}-config.cmake" + DESTINATION "${CMAKE_INSTALL_DATADIR}/cmake/${LIBFM_QT_LIBRARY_NAME}" + COMPONENT Devel +) + +# FIXME: add libtool version to the lib (soname) later. +# FIXME: only export public symbols + +install(TARGETS ${LIBFM_QT_LIBRARY_NAME} + DESTINATION "${CMAKE_INSTALL_LIBDIR}" + EXPORT "${LIBFM_QT_LIBRARY_NAME}-targets" + LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" + PUBLIC_HEADER + COMPONENT Runtime +) + +export(TARGETS ${LIBFM_QT_LIBRARY_NAME} + FILE "${CMAKE_BINARY_DIR}/${LIBFM_QT_LIBRARY_NAME}-targets.cmake" + EXPORT_LINK_INTERFACE_LIBRARIES +) + +# install a pkgconfig file for libfm-qt +set(REQUIRED_QT "Qt5Widgets >= ${QT_MINIMUM_VERSION} Qt5X11Extras >= ${QT_MINIMUM_VERSION}") +configure_file(libfm-qt.pc.in lib${LIBFM_QT_LIBRARY_NAME}.pc @ONLY) +# FreeBSD loves to install files to different locations +# https://www.freebsd.org/doc/handbook/dirstructure.html +if(${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") + install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/lib${LIBFM_QT_LIBRARY_NAME}.pc" + DESTINATION libdata/pkgconfig + COMPONENT Devel + ) +else() + install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/lib${LIBFM_QT_LIBRARY_NAME}.pc" + DESTINATION "${CMAKE_INSTALL_LIBDIR}/pkgconfig" + COMPONENT Devel + ) +endif() + +# prevent the generated files from being deleted during make cleaner +set_directory_properties(PROPERTIES CLEAN_NO_CUSTOM true) + + +set(TEST_LIBRARIES + Qt5::Core + Qt5::Widgets + ${FM_LIBRARIES} + ${LIBFM_QT_LIBRARY_NAME} +) +# some simple test cases +add_executable("test-folder" + tests/test-folder.cpp +) +target_link_libraries("test-folder" ${TEST_LIBRARIES}) + +add_executable("test-folderview" + tests/test-folderview.cpp +) +target_link_libraries("test-folderview" ${TEST_LIBRARIES}) + +add_executable("test-filedialog" + tests/test-filedialog.cpp +) +target_link_libraries("test-filedialog" ${TEST_LIBRARIES}) + +add_executable("test-volumemanager" + tests/test-volumemanager.cpp +) +target_link_libraries("test-volumemanager" ${TEST_LIBRARIES}) + +add_executable("test-placesview" + tests/test-placesview.cpp +) +target_link_libraries("test-placesview" ${TEST_LIBRARIES}) + diff --git a/src/app-chooser-dialog.ui b/src/app-chooser-dialog.ui new file mode 100644 index 0000000..ef1ebee --- /dev/null +++ b/src/app-chooser-dialog.ui @@ -0,0 +1,183 @@ + + + AppChooserDialog + + + + 0 + 0 + 432 + 387 + + + + Choose an Application + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + + + + + 0 + 1 + + + + 0 + + + + Installed Applications + + + + + + + + + + Custom Command + + + + + + Command line to execute: + + + + + + + + + + Application name: + + + + + + + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + + + Qt::RichText + + + + + + + false + + + Keep terminal window open after command execution + + + + + + + Execute in terminal emulator + + + + + + + + + + + Set selected application as default action of this file type + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + Fm::AppMenuView + QTreeView +
appmenuview.h
+
+
+ + + + buttonBox + accepted() + AppChooserDialog + accept() + + + 227 + 359 + + + 157 + 274 + + + + + buttonBox + rejected() + AppChooserDialog + reject() + + + 295 + 365 + + + 286 + 274 + + + + + useTerminal + toggled(bool) + keepTermOpen + setEnabled(bool) + + + 72 + 260 + + + 79 + 282 + + + + +
diff --git a/src/appchoosercombobox.cpp b/src/appchoosercombobox.cpp new file mode 100644 index 0000000..24efa31 --- /dev/null +++ b/src/appchoosercombobox.cpp @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "appchoosercombobox.h" +#include "appchooserdialog.h" +#include "utilities.h" +#include "core/iconinfo.h" + +namespace Fm { + +AppChooserComboBox::AppChooserComboBox(QWidget* parent): + QComboBox(parent), + defaultAppIndex_(-1), + prevIndex_(0), + blockOnCurrentIndexChanged_(false) { + + // the new Qt5 signal/slot syntax cannot handle overloaded methods by default + // hence a type-casting is needed here. really ugly! + // reference: https://forum.qt.io/topic/20998/qt5-new-signals-slots-syntax-does-not-work-solved + connect((QComboBox*)this, static_cast(&QComboBox::currentIndexChanged), this, &AppChooserComboBox::onCurrentIndexChanged); +} + +AppChooserComboBox::~AppChooserComboBox() { +} + +void AppChooserComboBox::setMimeType(std::shared_ptr mimeType) { + clear(); + defaultApp_.reset(); + appInfos_.clear(); + + mimeType_ = std::move(mimeType); + if(mimeType_) { + const char* typeName = mimeType_->name(); + defaultApp_ = Fm::GAppInfoPtr{g_app_info_get_default_for_type(typeName, FALSE), false}; + GList* appInfos_glist = g_app_info_get_all_for_type(typeName); + int i = 0; + for(GList* l = appInfos_glist; l; l = l->next, ++i) { + Fm::GAppInfoPtr app{G_APP_INFO(l->data), false}; + GIcon* gicon = g_app_info_get_icon(app.get()); + addItem(gicon ? Fm::IconInfo::fromGIcon(gicon)->qicon(): QIcon(), g_app_info_get_name(app.get())); + if(g_app_info_equal(app.get(), defaultApp_.get())) { + defaultAppIndex_ = i; + } + appInfos_.push_back(std::move(app)); + } + g_list_free(appInfos_glist); + } + // add "Other applications" item + insertSeparator(count()); + addItem(tr("Customize")); + if(defaultAppIndex_ != -1) { + setCurrentIndex(defaultAppIndex_); + } +} + +// returns the currently selected app. +Fm::GAppInfoPtr AppChooserComboBox::selectedApp() const { + // the elements of appInfos_ and the combo indexes before "Customize" + // always have a one-to-one correspondence + int idx = currentIndex(); + return idx >= 0 && !appInfos_.empty() ? appInfos_[idx] : Fm::GAppInfoPtr{}; +} + +bool AppChooserComboBox::isChanged() const { + return (defaultAppIndex_ != currentIndex()); +} + +void AppChooserComboBox::onCurrentIndexChanged(int index) { + if(index == -1 || index == prevIndex_ || blockOnCurrentIndexChanged_) { + return; + } + + // the last item is "Customize" + if(index == (count() - 1)) { + /* TODO: let the user choose an app or add custom actions here. */ + QWidget* toplevel = topLevelWidget(); + AppChooserDialog dlg(mimeType_, toplevel); + dlg.setWindowModality(Qt::WindowModal); + dlg.setCanSetDefault(false); + if(dlg.exec() == QDialog::Accepted) { + auto app = dlg.selectedApp(); + if(app) { + /* see if it's already in the list to prevent duplication */ + auto found = std::find_if(appInfos_.cbegin(), appInfos_.cend(), [&](const Fm::GAppInfoPtr& item) { + return g_app_info_equal(app.get(), item.get()); + }); + + // inserting new items or change current index will recursively trigger onCurrentIndexChanged. + // we need to block our handler to prevent recursive calls. + blockOnCurrentIndexChanged_ = true; + /* if it's already in the list, select it */ + if(found != appInfos_.cend()) { + auto pos = found - appInfos_.cbegin(); + setCurrentIndex(pos); + } + else { /* if it's not found, add it to the list */ + auto it = appInfos_.insert(appInfos_.cbegin(), std::move(app)); + GIcon* gicon = g_app_info_get_icon(it->get()); + insertItem(0, Fm::IconInfo::fromGIcon(gicon)->qicon(), g_app_info_get_name(it->get())); + setCurrentIndex(0); + } + blockOnCurrentIndexChanged_ = false; + return; + } + } + + // block our handler to prevent recursive calls. + blockOnCurrentIndexChanged_ = true; + // restore to previously selected item + setCurrentIndex(prevIndex_); + blockOnCurrentIndexChanged_ = false; + } + else { + prevIndex_ = index; + } +} + + +#if 0 +/* get a list of custom apps added with app-chooser. +* the returned GList is owned by the combo box and shouldn't be freed. */ +const GList* AppChooserComboBox::customApps() { + +} +#endif + +} // namespace Fm diff --git a/src/appchoosercombobox.h b/src/appchoosercombobox.h new file mode 100644 index 0000000..4250298 --- /dev/null +++ b/src/appchoosercombobox.h @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_APPCHOOSERCOMBOBOX_H +#define FM_APPCHOOSERCOMBOBOX_H + +#include "libfmqtglobals.h" +#include + +#include + +#include "core/mimetype.h" +#include "core/gioptrs.h" + +namespace Fm { + +class LIBFM_QT_API AppChooserComboBox : public QComboBox { + Q_OBJECT +public: + ~AppChooserComboBox(); + explicit AppChooserComboBox(QWidget* parent); + + void setMimeType(std::shared_ptr mimeType); + + const std::shared_ptr& mimeType() const { + return mimeType_; + } + + Fm::GAppInfoPtr selectedApp() const; + // const GList* customApps(); + + bool isChanged() const; + +private Q_SLOTS: + void onCurrentIndexChanged(int index); + +private: + std::shared_ptr mimeType_; + std::vector appInfos_; // applications used to open the file type + Fm::GAppInfoPtr defaultApp_; // default application used to open the file type + int defaultAppIndex_; + int prevIndex_; + bool blockOnCurrentIndexChanged_; +}; + +} + +#endif // FM_APPCHOOSERCOMBOBOX_H diff --git a/src/appchooserdialog.cpp b/src/appchooserdialog.cpp new file mode 100644 index 0000000..e07243e --- /dev/null +++ b/src/appchooserdialog.cpp @@ -0,0 +1,287 @@ +/* + * Copyright 2010-2014 Hong Jen Yee (PCMan) + * Copyright 2012-2013 Andriy Grytsenko (LStranger) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "appchooserdialog.h" +#include "ui_app-chooser-dialog.h" +#include +#include +#include + +namespace Fm { + +AppChooserDialog::AppChooserDialog(std::shared_ptr mimeType, QWidget* parent, Qt::WindowFlags f): + QDialog(parent, f), + ui(new Ui::AppChooserDialog()), + mimeType_{std::move(mimeType)}, + canSetDefault_(true) { + ui->setupUi(this); + + connect(ui->appMenuView, &AppMenuView::selectionChanged, this, &AppChooserDialog::onSelectionChanged); + connect(ui->tabWidget, &QTabWidget::currentChanged, this, &AppChooserDialog::onTabChanged); + + if(!ui->appMenuView->isAppSelected()) { + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); // disable OK button + } +} + +AppChooserDialog::~AppChooserDialog() { + delete ui; +} + +bool AppChooserDialog::isSetDefault() const { + return ui->setDefault->isChecked(); +} + +static void on_temp_appinfo_destroy(gpointer data, GObject* /*objptr*/) { + char* filename = (char*)data; + if(g_unlink(filename) < 0) { + g_critical("failed to remove %s", filename); + } + /* else + qDebug("temp file %s removed", filename); */ + g_free(filename); +} + +static GAppInfo* app_info_create_from_commandline(const char* commandline, + const char* application_name, + const char* bin_name, + const char* mime_type, + gboolean terminal, gboolean keep) { + GAppInfo* app = nullptr; + char* dirname = g_build_filename(g_get_user_data_dir(), "applications", nullptr); + const char* app_basename = strrchr(bin_name, '/'); + + if(app_basename) { + app_basename++; + } + else { + app_basename = bin_name; + } + if(g_mkdir_with_parents(dirname, 0700) == 0) { + char* filename = g_strdup_printf("%s/userapp-%s-XXXXXX.desktop", dirname, app_basename); + int fd = g_mkstemp(filename); + if(fd != -1) { + GString* content = g_string_sized_new(256); + g_string_printf(content, + "[" G_KEY_FILE_DESKTOP_GROUP "]\n" + G_KEY_FILE_DESKTOP_KEY_TYPE "=" G_KEY_FILE_DESKTOP_TYPE_APPLICATION "\n" + G_KEY_FILE_DESKTOP_KEY_NAME "=%s\n" + G_KEY_FILE_DESKTOP_KEY_EXEC "=%s\n" + G_KEY_FILE_DESKTOP_KEY_CATEGORIES "=Other;\n" + G_KEY_FILE_DESKTOP_KEY_NO_DISPLAY "=true\n", + application_name, + commandline + ); + if(mime_type) + g_string_append_printf(content, + G_KEY_FILE_DESKTOP_KEY_MIME_TYPE "=%s\n", + mime_type); + g_string_append_printf(content, + G_KEY_FILE_DESKTOP_KEY_TERMINAL "=%s\n", + terminal ? "true" : "false"); + if(terminal) + g_string_append_printf(content, "X-KeepTerminal=%s\n", + keep ? "true" : "false"); + close(fd); /* g_file_set_contents() may fail creating duplicate */ + if(g_file_set_contents(filename, content->str, content->len, nullptr)) { + char* fbname = g_path_get_basename(filename); + app = G_APP_INFO(g_desktop_app_info_new(fbname)); + g_free(fbname); + /* if there is mime_type set then created application will be + saved for the mime type (see fm_choose_app_for_mime_type() + below) but if not then we should remove this temp. file */ + if(!mime_type || !application_name[0]) + /* save the name so this file will be removed later */ + g_object_weak_ref(G_OBJECT(app), on_temp_appinfo_destroy, + g_strdup(filename)); + } + else { + g_unlink(filename); + } + g_string_free(content, TRUE); + } + g_free(filename); + } + g_free(dirname); + return app; +} + +inline static char* get_binary(const char* cmdline, gboolean* arg_found) { + /* see if command line contains %f, %F, %u, or %U. */ + const char* p = strstr(cmdline, " %"); + if(p) { + if(!strchr("fFuU", *(p + 2))) { + p = nullptr; + } + } + if(arg_found) { + *arg_found = (p != nullptr); + } + if(p) { + return g_strndup(cmdline, p - cmdline); + } + else { + return g_strdup(cmdline); + } +} + +GAppInfo* AppChooserDialog::customCommandToApp() { + GAppInfo* app = nullptr; + QByteArray cmdline = ui->cmdLine->text().toLocal8Bit(); + QByteArray app_name = ui->appName->text().toUtf8(); + if(!cmdline.isEmpty()) { + gboolean arg_found = FALSE; + char* bin1 = get_binary(cmdline.constData(), &arg_found); + qDebug("bin1 = %s", bin1); + /* see if command line contains %f, %F, %u, or %U. */ + if(!arg_found) { /* append %f if no %f, %F, %u, or %U was found. */ + cmdline += " %f"; + } + + /* FIXME: is there any better way to do this? */ + /* We need to ensure that no duplicated items are added */ + if(mimeType_) { + MenuCache* menu_cache; + /* see if the command is already in the list of known apps for this mime-type */ + GList* apps = g_app_info_get_all_for_type(mimeType_->name()); + GList* l; + for(l = apps; l; l = l->next) { + GAppInfo* app2 = G_APP_INFO(l->data); + const char* cmd = g_app_info_get_commandline(app2); + char* bin2 = get_binary(cmd, nullptr); + if(g_strcmp0(bin1, bin2) == 0) { + app = G_APP_INFO(g_object_ref(app2)); + qDebug("found in app list"); + g_free(bin2); + break; + } + g_free(bin2); + } + g_list_foreach(apps, (GFunc)g_object_unref, nullptr); + g_list_free(apps); + if(app) { + goto _out; + } + + /* see if this command can be found in menu cache */ + menu_cache = menu_cache_lookup("applications.menu"); + if(menu_cache) { + MenuCacheDir* root_dir = menu_cache_dup_root_dir(menu_cache); + if(root_dir) { + GSList* all_apps = menu_cache_list_all_apps(menu_cache); + GSList* l; + for(l = all_apps; l; l = l->next) { + MenuCacheApp* ma = MENU_CACHE_APP(l->data); + const char* exec = menu_cache_app_get_exec(ma); + char* bin2; + if(exec == nullptr) { + g_warning("application %s has no Exec statement", menu_cache_item_get_id(MENU_CACHE_ITEM(ma))); + continue; + } + bin2 = get_binary(exec, nullptr); + if(g_strcmp0(bin1, bin2) == 0) { + app = G_APP_INFO(g_desktop_app_info_new(menu_cache_item_get_id(MENU_CACHE_ITEM(ma)))); + qDebug("found in menu cache"); + menu_cache_item_unref(MENU_CACHE_ITEM(ma)); + g_free(bin2); + break; + } + menu_cache_item_unref(MENU_CACHE_ITEM(ma)); + g_free(bin2); + } + g_slist_free(all_apps); + menu_cache_item_unref(MENU_CACHE_ITEM(root_dir)); + } + menu_cache_unref(menu_cache); + } + if(app) { + goto _out; + } + } + + /* FIXME: g_app_info_create_from_commandline force the use of %f or %u, so this is not we need */ + app = app_info_create_from_commandline(cmdline.constData(), app_name.constData(), bin1, + mimeType_ ? mimeType_->name() : nullptr, + ui->useTerminal->isChecked(), ui->keepTermOpen->isChecked()); +_out: + g_free(bin1); + } + return app; +} + +void AppChooserDialog::accept() { + QDialog::accept(); + + if(ui->tabWidget->currentIndex() == 0) { + selectedApp_ = ui->appMenuView->selectedApp(); + } + else { // custom command line + selectedApp_ = customCommandToApp(); + } + + if(selectedApp_) { + if(mimeType_ && g_app_info_get_name(selectedApp_.get())) { + /* add this app to the mime-type */ +#if GLIB_CHECK_VERSION(2, 27, 6) + g_app_info_set_as_last_used_for_type(selectedApp_.get(), mimeType_->name(), nullptr); +#else + g_app_info_add_supports_type(selectedApp_.get(), mimeType_->name(), nullptr); +#endif + /* if need to set default */ + if(ui->setDefault->isChecked()) { + g_app_info_set_as_default_for_type(selectedApp_.get(), mimeType_->name(), nullptr); + } + } + } +} + +void AppChooserDialog::onSelectionChanged() { + bool isAppSelected = ui->appMenuView->isAppSelected(); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(isAppSelected); +} + +void AppChooserDialog::setMimeType(std::shared_ptr mimeType) { + mimeType_ = std::move(mimeType); + if(mimeType_) { + QString text = tr("Select an application to open \"%1\" files") + .arg(QString::fromUtf8(mimeType_->desc())); + ui->fileTypeHeader->setText(text); + } + else { + ui->fileTypeHeader->hide(); + ui->setDefault->hide(); + } +} + +void AppChooserDialog::setCanSetDefault(bool value) { + canSetDefault_ = value; + ui->setDefault->setVisible(value); +} + +void AppChooserDialog::onTabChanged(int index) { + if(index == 0) { // app menu view + onSelectionChanged(); + } + else if(index == 1) { // custom command + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); + } +} + +} // namespace Fm diff --git a/src/appchooserdialog.h b/src/appchooserdialog.h new file mode 100644 index 0000000..9dccebf --- /dev/null +++ b/src/appchooserdialog.h @@ -0,0 +1,78 @@ +/* + * Copyright 2010-2014 Hong Jen Yee (PCMan) + * Copyright 2012-2013 Andriy Grytsenko (LStranger) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_APPCHOOSERDIALOG_H +#define FM_APPCHOOSERDIALOG_H + +#include +#include "libfmqtglobals.h" + +#include "core/mimetype.h" +#include "core/gioptrs.h" + +namespace Ui { +class AppChooserDialog; +} + +namespace Fm { + +class LIBFM_QT_API AppChooserDialog : public QDialog { + Q_OBJECT +public: + explicit AppChooserDialog(std::shared_ptr mimeType, QWidget* parent = nullptr, Qt::WindowFlags f = 0); + ~AppChooserDialog(); + + virtual void accept(); + + void setMimeType(std::shared_ptr mimeType); + + const std::shared_ptr& mimeType() const { + return mimeType_; + } + + void setCanSetDefault(bool value); + + bool canSetDefault() const { + return canSetDefault_; + } + + const Fm::GAppInfoPtr& selectedApp() const { + return selectedApp_; + } + + bool isSetDefault() const; + +private: + GAppInfo* customCommandToApp(); + +private Q_SLOTS: + void onSelectionChanged(); + void onTabChanged(int index); + +private: + Ui::AppChooserDialog* ui; + std::shared_ptr mimeType_; + bool canSetDefault_; + Fm::GAppInfoPtr selectedApp_; +}; + +} + +#endif // FM_APPCHOOSERDIALOG_H diff --git a/src/applaunchcontext.cpp b/src/applaunchcontext.cpp new file mode 100644 index 0000000..e599485 --- /dev/null +++ b/src/applaunchcontext.cpp @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "applaunchcontext.h" +#include +#include + +typedef struct _FmAppLaunchContext { + GAppLaunchContext parent; +}FmAppLaunchContext; + +G_DEFINE_TYPE(FmAppLaunchContext, fm_app_launch_context, G_TYPE_APP_LAUNCH_CONTEXT) + +static char* fm_app_launch_context_get_display(GAppLaunchContext * /*context*/, GAppInfo * /*info*/, GList * /*files*/) { + Display* dpy = QX11Info::display(); + if(dpy) { + char* xstr = DisplayString(dpy); + return g_strdup(xstr); + } + return nullptr; +} + +static char* fm_app_launch_context_get_startup_notify_id(GAppLaunchContext * /*context*/, GAppInfo * /*info*/, GList * /*files*/) { + return nullptr; +} + +static void fm_app_launch_context_class_init(FmAppLaunchContextClass* klass) { + GAppLaunchContextClass* app_launch_class = G_APP_LAUNCH_CONTEXT_CLASS(klass); + app_launch_class->get_display = fm_app_launch_context_get_display; + app_launch_class->get_startup_notify_id = fm_app_launch_context_get_startup_notify_id; +} + +static void fm_app_launch_context_init(FmAppLaunchContext* /*context*/) { +} + +FmAppLaunchContext* fm_app_launch_context_new_for_widget(QWidget* /*widget*/) { + FmAppLaunchContext* context = (FmAppLaunchContext*)g_object_new(FM_TYPE_APP_LAUNCH_CONTEXT, nullptr); + return context; +} + +FmAppLaunchContext* fm_app_launch_context_new() { + FmAppLaunchContext* context = (FmAppLaunchContext*)g_object_new(FM_TYPE_APP_LAUNCH_CONTEXT, nullptr); + return context; +} diff --git a/src/applaunchcontext.h b/src/applaunchcontext.h new file mode 100644 index 0000000..606c3b6 --- /dev/null +++ b/src/applaunchcontext.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_APP_LAUNCHCONTEXT_H +#define FM_APP_LAUNCHCONTEXT_H + +#include "libfmqtglobals.h" +#include +#include + +#define FM_TYPE_APP_LAUNCH_CONTEXT (fm_app_launch_context_get_type()) +#define FM_APP_LAUNCH_CONTEXT(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj),\ + FM_TYPE_APP_LAUNCH_CONTEXT, FmAppLaunchContext)) +#define FM_APP_LAUNCH_CONTEXT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass),\ + FM_TYPE_APP_LAUNCH_CONTEXT, FmAppLaunchContextClass)) +#define FM_IS_APP_LAUNCH_CONTEXT(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj),\ + FM_TYPE_APP_LAUNCH_CONTEXT)) +#define FM_IS_APP_LAUNCH_CONTEXT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass),\ + FM_TYPE_APP_LAUNCH_CONTEXT)) +#define FM_APP_LAUNCH_CONTEXT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj),\ + FM_TYPE_APP_LAUNCH_CONTEXT, FmAppLaunchContextClass)) + +typedef struct _FmAppLaunchContext FmAppLaunchContext; + +typedef struct _FmAppLaunchContextClass { + GAppLaunchContextClass parent; +}FmAppLaunchContextClass; + +FmAppLaunchContext* fm_app_launch_context_new(); +FmAppLaunchContext* fm_app_launch_context_new_for_widget(QWidget* widget); +GType fm_app_launch_context_get_type(); + +#endif // FM_APPLAUNCHCONTEXT_H diff --git a/src/appmenuview.cpp b/src/appmenuview.cpp new file mode 100644 index 0000000..f3ace63 --- /dev/null +++ b/src/appmenuview.cpp @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "appmenuview.h" +#include +#include "appmenuview_p.h" +#include "core/filepath.h" + +#include + +namespace Fm { + +AppMenuView::AppMenuView(QWidget* parent): + QTreeView(parent), + model_(new QStandardItemModel()), + menu_cache(nullptr), + menu_cache_reload_notify(nullptr) { + + setHeaderHidden(true); + setSelectionMode(SingleSelection); + + // initialize model + // TODO: share one model among all app menu view widgets + // ensure that we're using lxmenu-data (FIXME: should we do this?) + QByteArray oldenv = qgetenv("XDG_MENU_PREFIX"); + qputenv("XDG_MENU_PREFIX", "lxde-"); + menu_cache = menu_cache_lookup("applications.menu"); + // if(!oldenv.isEmpty()) + qputenv("XDG_MENU_PREFIX", oldenv); // restore the original value if needed + + if(menu_cache) { + MenuCacheDir* dir = menu_cache_dup_root_dir(menu_cache); + menu_cache_reload_notify = menu_cache_add_reload_notify(menu_cache, _onMenuCacheReload, this); + if(dir) { /* content of menu is already loaded */ + addMenuItems(nullptr, dir); + menu_cache_item_unref(MENU_CACHE_ITEM(dir)); + } + } + setModel(model_); + connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &AppMenuView::selectionChanged); + selectionModel()->select(model_->index(0, 0), QItemSelectionModel::SelectCurrent); +} + +AppMenuView::~AppMenuView() { + delete model_; + if(menu_cache) { + if(menu_cache_reload_notify) { + menu_cache_remove_reload_notify(menu_cache, menu_cache_reload_notify); + } + menu_cache_unref(menu_cache); + } +} + +void AppMenuView::addMenuItems(QStandardItem* parentItem, MenuCacheDir* dir) { + GSList* l; + GSList* list; + /* Iterate over all menu items in this directory. */ + for(l = list = menu_cache_dir_list_children(dir); l != nullptr; l = l->next) { + /* Get the menu item. */ + MenuCacheItem* menuItem = MENU_CACHE_ITEM(l->data); + switch(menu_cache_item_get_type(menuItem)) { + case MENU_CACHE_TYPE_NONE: + case MENU_CACHE_TYPE_SEP: + break; + case MENU_CACHE_TYPE_APP: + case MENU_CACHE_TYPE_DIR: { + AppMenuViewItem* newItem = new AppMenuViewItem(menuItem); + if(parentItem) { + parentItem->insertRow(parentItem->rowCount(), newItem); + } + else { + model_->insertRow(model_->rowCount(), newItem); + } + + if(menu_cache_item_get_type(menuItem) == MENU_CACHE_TYPE_DIR) { + addMenuItems(newItem, MENU_CACHE_DIR(menuItem)); + } + break; + } + } + } + g_slist_free_full(list, (GDestroyNotify)menu_cache_item_unref); +} + +void AppMenuView::onMenuCacheReload(MenuCache* mc) { + MenuCacheDir* dir = menu_cache_dup_root_dir(mc); + model_->clear(); + /* FIXME: preserve original selection */ + if(dir) { + addMenuItems(nullptr, dir); + menu_cache_item_unref(MENU_CACHE_ITEM(dir)); + selectionModel()->select(model_->index(0, 0), QItemSelectionModel::SelectCurrent); + } +} + +bool AppMenuView::isAppSelected() const { + AppMenuViewItem* item = selectedItem(); + return (item && item->isApp()); +} + +AppMenuViewItem* AppMenuView::selectedItem() const { + QModelIndexList selected = selectedIndexes(); + if(!selected.isEmpty()) { + AppMenuViewItem* item = static_cast(model_->itemFromIndex(selected.first() + )); + return item; + } + return nullptr; +} + +Fm::GAppInfoPtr AppMenuView::selectedApp() const { + const char* id = selectedAppDesktopId(); + return Fm::GAppInfoPtr{id ? G_APP_INFO(g_desktop_app_info_new(id)) : nullptr, false}; +} + +QByteArray AppMenuView::selectedAppDesktopFilePath() const { + AppMenuViewItem* item = selectedItem(); + if(item && item->isApp()) { + char* path = menu_cache_item_get_file_path(item->item()); + QByteArray ret(path); + g_free(path); + return ret; + } + return QByteArray(); +} + +const char* AppMenuView::selectedAppDesktopId() const { + AppMenuViewItem* item = selectedItem(); + if(item && item->isApp()) { + return menu_cache_item_get_id(item->item()); + } + return nullptr; +} + +FilePath AppMenuView::selectedAppDesktopPath() const { + AppMenuViewItem* item = selectedItem(); + FilePath path; + if(item && item->isApp()) { + char* mpath = menu_cache_dir_make_path(MENU_CACHE_DIR(item)); + path = FilePath::fromUri("menu://applications/").relativePath(mpath + 13 /* skip "/Applications" */); + g_free(mpath); + } + return path; +} + +} // namespace Fm diff --git a/src/appmenuview.h b/src/appmenuview.h new file mode 100644 index 0000000..8ab78e2 --- /dev/null +++ b/src/appmenuview.h @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_APPMENUVIEW_H +#define FM_APPMENUVIEW_H + +#include +#include "libfmqtglobals.h" +#include + +#include "core/gioptrs.h" +#include "core/filepath.h" + +class QStandardItemModel; +class QStandardItem; + +namespace Fm { + +class AppMenuViewItem; + +class LIBFM_QT_API AppMenuView : public QTreeView { + Q_OBJECT +public: + explicit AppMenuView(QWidget* parent = nullptr); + ~AppMenuView(); + + Fm::GAppInfoPtr selectedApp() const; + + const char* selectedAppDesktopId() const; + + QByteArray selectedAppDesktopFilePath() const; + + FilePath selectedAppDesktopPath() const; + + bool isAppSelected() const; + +Q_SIGNALS: + void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected); + +private: + void addMenuItems(QStandardItem* parentItem, MenuCacheDir* dir); + void onMenuCacheReload(MenuCache* mc); + static void _onMenuCacheReload(MenuCache* mc, gpointer user_data) { + static_cast(user_data)->onMenuCacheReload(mc); + } + + AppMenuViewItem* selectedItem() const; + +private: + // gboolean fm_app_menu_view_is_item_app(, GtkTreeIter* it); + QStandardItemModel* model_; + MenuCache* menu_cache; + MenuCacheNotifyId menu_cache_reload_notify; +}; + +} + +#endif // FM_APPMENUVIEW_H diff --git a/src/appmenuview_p.h b/src/appmenuview_p.h new file mode 100644 index 0000000..7dd3d53 --- /dev/null +++ b/src/appmenuview_p.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_APPMENUVIEW_P_H +#define FM_APPMENUVIEW_P_H + +#include +#include +#include "core/iconinfo.h" + +namespace Fm { + +class AppMenuViewItem : public QStandardItem { +public: + explicit AppMenuViewItem(MenuCacheItem* item): + item_(menu_cache_item_ref(item)) { + std::shared_ptr icon; + if(menu_cache_item_get_icon(item)) { + icon = Fm::IconInfo::fromName(menu_cache_item_get_icon(item)); + } + setText(menu_cache_item_get_name(item)); + setEditable(false); + setDragEnabled(false); + if(icon) { + setIcon(icon->qicon()); + } + } + + ~AppMenuViewItem() { + menu_cache_item_unref(item_); + } + + MenuCacheItem* item() { + return item_; + } + + int type() const { + return menu_cache_item_get_type(item_); + } + + bool isApp() { + return type() == MENU_CACHE_TYPE_APP; + } + + bool isDir() { + return type() == MENU_CACHE_TYPE_DIR; + } + +private: + MenuCacheItem* item_; +}; + +} + +#endif // FM_APPMENUVIEW_P_H diff --git a/src/bookmarkaction.cpp b/src/bookmarkaction.cpp new file mode 100644 index 0000000..502126a --- /dev/null +++ b/src/bookmarkaction.cpp @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "bookmarkaction.h" + +namespace Fm { + +BookmarkAction::BookmarkAction(std::shared_ptr item, QObject* parent): + QAction(parent), + item_(std::move(item)) { + + setText(item_->name()); +} + +} // namespace Fm diff --git a/src/bookmarkaction.h b/src/bookmarkaction.h new file mode 100644 index 0000000..08fc73f --- /dev/null +++ b/src/bookmarkaction.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef BOOKMARKACTION_H +#define BOOKMARKACTION_H + +#include "libfmqtglobals.h" +#include +#include "core/bookmarks.h" + +namespace Fm { + +// action used to create bookmark menu items +class LIBFM_QT_API BookmarkAction : public QAction { +public: + explicit BookmarkAction(std::shared_ptr item, QObject* parent = 0); + + const std::shared_ptr& bookmark() const { + return item_; + } + + const Fm::FilePath& path() const { + return item_->path(); + } + +private: + std::shared_ptr item_; +}; + +} + +#endif // BOOKMARKACTION_H diff --git a/src/browsehistory.cpp b/src/browsehistory.cpp new file mode 100644 index 0000000..54671fb --- /dev/null +++ b/src/browsehistory.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "browsehistory.h" + +namespace Fm { + +BrowseHistory::BrowseHistory(): + currentIndex_(0), + maxCount_(10) { +} + +BrowseHistory::~BrowseHistory() { +} + +void BrowseHistory::add(Fm::FilePath path, int scrollPos) { + int lastIndex = items_.size() - 1; + if(currentIndex_ < lastIndex) { + // if we're not at the last item, remove items after the current one. + items_.erase(items_.cbegin() + currentIndex_ + 1, items_.cend()); + } + + if(items_.size() + 1 > static_cast(maxCount_)) { + // if there are too many items, remove the oldest one. + // FIXME: what if currentIndex_ == 0? remove the last item instead? + if(currentIndex_ == 0) { + items_.erase(items_.cbegin() + lastIndex); + } + else { + items_.erase(items_.cbegin()); + --currentIndex_; + } + } + // add a path and current scroll position to browse history + items_.push_back(BrowseHistoryItem(path, scrollPos)); + currentIndex_ = items_.size() - 1; +} + +void BrowseHistory::setCurrentIndex(int index) { + if(index >= 0 && static_cast(index) < items_.size()) { + currentIndex_ = index; + // FIXME: should we emit a signal for the change? + } +} + +bool BrowseHistory::canBackward() const { + return (currentIndex_ > 0); +} + +int BrowseHistory::backward() { + if(canBackward()) { + --currentIndex_; + } + return currentIndex_; +} + +bool BrowseHistory::canForward() const { + return (static_cast(currentIndex_) + 1 < items_.size()); +} + +int BrowseHistory::forward() { + if(canForward()) { + ++currentIndex_; + } + return currentIndex_; +} + +void BrowseHistory::setMaxCount(int maxCount) { + maxCount_ = maxCount; + if(items_.size() > static_cast(maxCount)) { + // TODO: remove some items + } +} + + +} // namespace Fm diff --git a/src/browsehistory.h b/src/browsehistory.h new file mode 100644 index 0000000..2a99ab9 --- /dev/null +++ b/src/browsehistory.h @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_BROWSEHISTORY_H +#define FM_BROWSEHISTORY_H + +#include "libfmqtglobals.h" +#include + +#include "core/filepath.h" + +namespace Fm { + +// class used to story browsing history of folder views +// We use this class to replace FmNavHistory provided by libfm since +// the original Libfm API is hard to use and confusing. + +class LIBFM_QT_API BrowseHistoryItem { +public: + + explicit BrowseHistoryItem(): + scrollPos_(0) { + } + + explicit BrowseHistoryItem(Fm::FilePath path, int scrollPos = 0): + path_(std::move(path)), + scrollPos_(scrollPos) { + } + + BrowseHistoryItem(const BrowseHistoryItem& other) = default; + + ~BrowseHistoryItem() { + } + + BrowseHistoryItem& operator=(const BrowseHistoryItem& other) { + path_ = other.path_; + scrollPos_ = other.scrollPos_; + return *this; + } + + Fm::FilePath path() const { + return path_; + } + + int scrollPos() const { + return scrollPos_; + } + + void setScrollPos(int pos) { + scrollPos_ = pos; + } + +private: + Fm::FilePath path_; + int scrollPos_; + // TODO: we may need to store current selection as well. +}; + +class LIBFM_QT_API BrowseHistory { + +public: + BrowseHistory(); + virtual ~BrowseHistory(); + + int currentIndex() const { + return currentIndex_; + } + void setCurrentIndex(int index); + + Fm::FilePath currentPath() const { + return items_[currentIndex_].path(); + } + + int currentScrollPos() const { + return items_[currentIndex_].scrollPos(); + } + + BrowseHistoryItem& currentItem() { + return items_[currentIndex_]; + } + + size_t size() const { + return items_.size(); + } + + BrowseHistoryItem& at(int index) { + return items_[index]; + } + + void add(Fm::FilePath path, int scrollPos = 0); + + bool canForward() const; + + bool canBackward() const; + + int backward(); + + int forward(); + + int maxCount() const { + return maxCount_; + } + + void setMaxCount(int maxCount); + +private: + std::vector items_; + int currentIndex_; + int maxCount_; +}; + +} + +#endif // FM_BROWSEHISTORY_H diff --git a/src/cachedfoldermodel.cpp b/src/cachedfoldermodel.cpp new file mode 100644 index 0000000..defa398 --- /dev/null +++ b/src/cachedfoldermodel.cpp @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "cachedfoldermodel.h" + +namespace Fm { + +CachedFolderModel::CachedFolderModel(const std::shared_ptr& folder): + FolderModel(), + refCount(1) { + FolderModel::setFolder(folder); +} + +CachedFolderModel::~CachedFolderModel() { + // qDebug("delete CachedFolderModel"); +} + +CachedFolderModel* CachedFolderModel::modelFromFolder(const std::shared_ptr& folder) { + QVariant cache = folder->property(cacheKey); + CachedFolderModel* model = cache.value(); + if(model) { + model->ref(); + } + else { + model = new CachedFolderModel(folder); + cache = QVariant::fromValue(model); + folder->setProperty(cacheKey, cache); + } + return model; +} + +CachedFolderModel* CachedFolderModel::modelFromPath(const Fm::FilePath& path) { + auto folder = Fm::Folder::fromPath(path); + if(folder) { + CachedFolderModel* model = modelFromFolder(folder); + return model; + } + return nullptr; +} + +void CachedFolderModel::unref() { + // qDebug("unref cache"); + --refCount; + if(refCount <= 0) { + folder()->setProperty(cacheKey, QVariant()); + delete(this); + } +} + + +} // namespace Fm diff --git a/src/cachedfoldermodel.h b/src/cachedfoldermodel.h new file mode 100644 index 0000000..ccd7a3e --- /dev/null +++ b/src/cachedfoldermodel.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_CACHEDFOLDERMODEL_H +#define FM_CACHEDFOLDERMODEL_H + +#include "libfmqtglobals.h" +#include "foldermodel.h" + +#include "core/folder.h" + +namespace Fm { + +// FIXME: deprecate CachedFolderModel later (ugly API design with manual ref()/unref()) +class LIBFM_QT_API CachedFolderModel : public FolderModel { + Q_OBJECT +public: + explicit CachedFolderModel(const std::shared_ptr& folder); + void ref() { + ++refCount; + } + void unref(); + + static CachedFolderModel* modelFromFolder(const std::shared_ptr& folder); + static CachedFolderModel* modelFromPath(const Fm::FilePath& path); + +private: + virtual ~CachedFolderModel(); + +private: + int refCount; + constexpr static const char* cacheKey = "CachedFolderModel"; +}; + + +} + +#endif // FM_CACHEDFOLDERMODEL_H diff --git a/src/colorbutton.cpp b/src/colorbutton.cpp new file mode 100644 index 0000000..ac95b45 --- /dev/null +++ b/src/colorbutton.cpp @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "colorbutton.h" +#include + +namespace Fm { + +ColorButton::ColorButton(QWidget* parent): QPushButton(parent) { + connect(this, &QPushButton::clicked, this, &ColorButton::onClicked); +} + +ColorButton::~ColorButton() { + +} + +void ColorButton::onClicked() { + QColorDialog dlg(color_); + if(dlg.exec() == QDialog::Accepted) { + setColor(dlg.selectedColor()); + } +} + +void ColorButton::setColor(const QColor& color) { + if(color != color_) { + color_ = color; + // use qss instead of QPalette to set the background color + // otherwise, this won't work when using the gtk style. + QString style = QString("QPushButton{background-color:%1;}").arg(color.name()); + setStyleSheet(style); + Q_EMIT changed(); + } +} + + +} // namespace Fm diff --git a/src/colorbutton.h b/src/colorbutton.h new file mode 100644 index 0000000..d5fa89d --- /dev/null +++ b/src/colorbutton.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_COLORBUTTON_H +#define FM_COLORBUTTON_H + +#include "libfmqtglobals.h" +#include +#include + +namespace Fm { + +class LIBFM_QT_API ColorButton : public QPushButton { + Q_OBJECT + +public: + explicit ColorButton(QWidget* parent = 0); + virtual ~ColorButton(); + + void setColor(const QColor&); + + QColor color() const { + return color_; + } + +Q_SIGNALS: + void changed(); + +private Q_SLOTS: + void onClicked(); + +private: + QColor color_; +}; + +} + +#endif // FM_COLORBUTTON_H diff --git a/src/core/archiver.cpp b/src/core/archiver.cpp new file mode 100644 index 0000000..b38990e --- /dev/null +++ b/src/core/archiver.cpp @@ -0,0 +1,174 @@ +#include "libfmqtglobals.h" +#include "archiver.h" + +#include +#include +#include + +#include + +namespace Fm { + +Archiver* Archiver::defaultArchiver_ = nullptr; // static +std::vector> Archiver::allArchivers_; // static + +Archiver::Archiver() { +} + +bool Archiver::isMimeTypeSupported(const char* type) { + char** p; + if(G_UNLIKELY(!type)) { + return false; + } + for(p = mimeTypes_.get(); *p; ++p) { + if(strcmp(*p, type) == 0) { + return true; + } + } + return false; +} + +bool Archiver::launchProgram(GAppLaunchContext* ctx, const char* cmd, const FilePathList& files, const FilePath& dir) { + char* _cmd = NULL; + const char* dir_place_holder; + GKeyFile* dummy; + + if(dir.isValid() && (dir_place_holder = strstr(cmd, "%d"))) { + CStrPtr dir_str; + int len; + if(strstr(cmd, "%U") || strstr(cmd, "%u")) { /* supports URI */ + dir_str = dir.uri(); + } + else { + dir_str = dir.localPath(); + } + + // FIXME: remove libfm dependency here + /* replace all % with %% so encoded URI can be handled correctly when parsing Exec key. */ + std::string percentEscapedDir; + for(auto p = dir_str.get(); *p; ++p) { + percentEscapedDir += *p; + if(*p == '%') { + percentEscapedDir += '%'; + } + } + + /* quote the path or URI */ + dir_str = CStrPtr{g_shell_quote(percentEscapedDir.c_str())}; + + len = strlen(cmd) - 2 + strlen(dir_str.get()) + 1; + _cmd = (char*)g_malloc(len); + len = (dir_place_holder - cmd); + strncpy(_cmd, cmd, len); + strcpy(_cmd + len, dir_str.get()); + strcat(_cmd, dir_place_holder + 2); + cmd = _cmd; + } + + /* create a fake key file to cheat GDesktopAppInfo */ + dummy = g_key_file_new(); + g_key_file_set_string(dummy, G_KEY_FILE_DESKTOP_GROUP, "Type", "Application"); + g_key_file_set_string(dummy, G_KEY_FILE_DESKTOP_GROUP, "Name", program_.get()); + + /* replace all % with %% so encoded URI can be handled correctly when parsing Exec key. */ + g_key_file_set_string(dummy, G_KEY_FILE_DESKTOP_GROUP, "Exec", cmd); + GAppInfoPtr app{reinterpret_cast(g_desktop_app_info_new_from_keyfile(dummy)), false}; + + g_key_file_free(dummy); + g_debug("cmd = %s", cmd); + if(app) { + GList* uris = NULL; + for(auto& file: files) { + uris = g_list_prepend(uris, g_strdup(file.uri().get())); + } + g_app_info_launch_uris(app.get(), uris, ctx, NULL); + g_list_foreach(uris, (GFunc)g_free, NULL); + g_list_free(uris); + } + g_free(_cmd); + return true; +} + +bool Archiver::createArchive(GAppLaunchContext* ctx, const FilePathList& files) { + if(createCmd_ && !files.empty()) { + launchProgram(ctx, createCmd_.get(), files, FilePath{}); + } + return false; +} + +bool Archiver::extractArchives(GAppLaunchContext* ctx, const FilePathList& files) { + if(extractCmd_ && !files.empty()) { + launchProgram(ctx, extractCmd_.get(), files, FilePath{}); + } + return false; +} + +bool Archiver::extractArchivesTo(GAppLaunchContext* ctx, const FilePathList& files, const FilePath& dest_dir) { + if(extractToCmd_ && !files.empty()) { + launchProgram(ctx, extractToCmd_.get(), files, dest_dir); + } + return false; +} + +// static +Archiver* Archiver::defaultArchiver() { + allArchivers(); // to have a preliminary default archiver + return defaultArchiver_; +} + +void Archiver::setDefaultArchiverByName(const char *name) { + if(name) { + auto& all = allArchivers(); + for(auto& archiver: all) { + if(archiver->program_ && strcmp(archiver->program_.get(), name) == 0) { + defaultArchiver_ = archiver.get(); + break; + } + } + } +} + +// static +void Archiver::setDefaultArchiver(Archiver* archiver) { + if(archiver) { + defaultArchiver_ = archiver; + } +} + +// static +const std::vector >& Archiver::allArchivers() { + // load all archivers on demand + if(allArchivers_.empty()) { + GKeyFile* kf = g_key_file_new(); + if(g_key_file_load_from_file(kf, LIBFM_QT_DATA_DIR "/archivers.list", G_KEY_FILE_NONE, NULL)) { + gsize n_archivers; + CStrArrayPtr programs{g_key_file_get_groups(kf, &n_archivers)}; + if(programs) { + gsize i; + for(i = 0; i < n_archivers; ++i) { + auto program = programs[i]; + std::unique_ptr archiver{new Archiver{}}; + archiver->createCmd_ = CStrPtr{g_key_file_get_string(kf, program, "create", NULL)}; + archiver->extractCmd_ = CStrPtr{g_key_file_get_string(kf, program, "extract", NULL)}; + archiver->extractToCmd_ = CStrPtr{g_key_file_get_string(kf, program, "extract_to", NULL)}; + archiver->mimeTypes_ = CStrArrayPtr{g_key_file_get_string_list(kf, program, "mime_types", NULL, NULL)}; + archiver->program_ = CStrPtr{g_strdup(program)}; + + // if default archiver is not set, find the first program existing in the current system. + if(!defaultArchiver_) { + CStrPtr fullPath{g_find_program_in_path(program)}; + if(fullPath) { + defaultArchiver_ = archiver.get(); + } + } + + allArchivers_.emplace_back(std::move(archiver)); + } + } + } + g_key_file_free(kf); + } + return allArchivers_; +} + +} // namespace Fm diff --git a/src/core/archiver.h b/src/core/archiver.h new file mode 100644 index 0000000..ba5368d --- /dev/null +++ b/src/core/archiver.h @@ -0,0 +1,69 @@ +#ifndef ARCHIVER_H +#define ARCHIVER_H + +#include "../libfmqtglobals.h" +#include "filepath.h" +#include "gioptrs.h" + +#include +#include + +namespace Fm { + +class LIBFM_QT_API Archiver { +public: + Archiver(); + + bool isMimeTypeSupported(const char* type); + + bool canCreateArchive() const { + return createCmd_ != nullptr; + } + + bool createArchive(GAppLaunchContext* ctx, const FilePathList& files); + + bool canExtractArchives() const { + return extractCmd_ != nullptr; + } + + bool extractArchives(GAppLaunchContext* ctx, const FilePathList& files); + + bool canExtractArchivesTo() const { + return extractToCmd_ != nullptr; + } + + bool extractArchivesTo(GAppLaunchContext* ctx, const FilePathList& files, const FilePath& dest_dir); + + /* get default GUI archivers used by libfm */ + static Archiver* defaultArchiver(); + + /* set default GUI archivers used by libfm */ + static void setDefaultArchiverByName(const char* name); + + /* set default GUI archivers used by libfm */ + static void setDefaultArchiver(Archiver* archiver); + + /* get a list of FmArchiver* of all GUI archivers known to libfm */ + static const std::vector>& allArchivers(); + + const char* program() const { + return program_.get(); + } + +private: + bool launchProgram(GAppLaunchContext* ctx, const char* cmd, const FilePathList& files, const FilePath &dir); + +private: + CStrPtr program_; + CStrPtr createCmd_; + CStrPtr extractCmd_; + CStrPtr extractToCmd_; + CStrArrayPtr mimeTypes_; + + static Archiver* defaultArchiver_; + static std::vector> allArchivers_; +}; + +} // namespace Fm + +#endif // ARCHIVER_H diff --git a/src/core/basicfilelauncher.cpp b/src/core/basicfilelauncher.cpp new file mode 100644 index 0000000..6f185c0 --- /dev/null +++ b/src/core/basicfilelauncher.cpp @@ -0,0 +1,392 @@ +#include "basicfilelauncher.h" +#include "fileinfojob.h" +#include "mountoperation.h" + +#include +#include + +#include +#include + +#include +#include +#include + +#include "legacy/fm-app-info.h" + +namespace Fm { + +BasicFileLauncher::BasicFileLauncher(): + quickExec_{false} { +} + +BasicFileLauncher::~BasicFileLauncher() { +} + +bool BasicFileLauncher::launchFiles(const FileInfoList& fileInfos, GAppLaunchContext* ctx) { + std::unordered_map mimeTypeToFiles; + FileInfoList folderInfos; + FilePathList pathsToLaunch; + // classify files according to different mimetypes + for(auto& fileInfo : fileInfos) { + /* + qDebug("path: %s, type: %s, target: %s, isDir: %i, isShortcut: %i, isMountable: %i, isDesktopEntry: %i", + fileInfo->path().toString().get(), fileInfo->mimeType()->name(), fileInfo->target().c_str(), + fileInfo->isDir(), fileInfo->isShortcut(), fileInfo->isMountable(), fileInfo->isDesktopEntry()); + */ + if(fileInfo->isMountable()) { + if(fileInfo->target().empty()) { + // the mountable is not yet mounted so we have no target URI. + GErrorPtr err{G_IO_ERROR, G_IO_ERROR_NOT_MOUNTED, + QObject::tr("The path is not mounted.")}; + if(!showError(ctx, err, fileInfo->path(), fileInfo)) { + // the user fail to handle the error, skip this file. + continue; + } + + // we do not have the target path in the FileInfo object. + // try to launch our path again to query the new file info later so we can get the mounted target URI. + pathsToLaunch.emplace_back(fileInfo->path()); + } + else { + // we have the target path, launch it later + pathsToLaunch.emplace_back(FilePath::fromPathStr(fileInfo->target().c_str())); + } + } + else if(fileInfo->isDesktopEntry()) { + // launch the desktop entry + launchDesktopEntry(fileInfo, FilePathList{}, ctx); + } + else if(fileInfo->isExecutableType()) { + // directly execute the file + launchExecutable(fileInfo, ctx); + } + else if(fileInfo->isShortcut()) { + // for shortcuts, launch their targets instead + auto path = handleShortcut(fileInfo, ctx); + if(path.isValid()) { + pathsToLaunch.emplace_back(path); + } + } + else if(fileInfo->isDir()) { + folderInfos.emplace_back(fileInfo); + } + else { + auto& mimeType = fileInfo->mimeType(); + mimeTypeToFiles[mimeType->name()].emplace_back(fileInfo); + } + } + + // open folders + if(!folderInfos.empty()) { + GErrorPtr err; + openFolder(ctx, folderInfos, err); + } + + // open files of different mime-types with their default app + for(auto& typeFiles : mimeTypeToFiles) { + auto& mimeType = typeFiles.first; + auto& files = typeFiles.second; + GErrorPtr err; + GAppInfoPtr app{g_app_info_get_default_for_type(mimeType.c_str(), false), false}; + if(!app) { + app = chooseApp(files, mimeType.c_str(), err); + } + if(app) { + launchWithApp(app.get(), files.paths(), ctx); + } + } + + if(!pathsToLaunch.empty()) { + launchPaths(pathsToLaunch, ctx); + } + + return true; +} + +bool BasicFileLauncher::launchPaths(FilePathList paths, GAppLaunchContext* ctx) { + // FIXME: blocking with an event loop is not a good design :-( + QEventLoop eventLoop; + auto job = new FileInfoJob{paths}; + job->setAutoDelete(false); // do not automatically delete the job since we want its results later. + + GObjectPtr ctxPtr{ctx}; + + // error handling (for example: handle path not mounted error) + QObject::connect(job, &FileInfoJob::error, + &eventLoop, [this, job, ctx](const GErrorPtr & err, Job::ErrorSeverity /* severity */ , Job::ErrorAction &act) { + auto path = job->currentPath(); + if(showError(ctx, err, path, nullptr)) { + // the user handled the error and ask for retry + act = Job::ErrorAction::RETRY; + } + }, Qt::BlockingQueuedConnection); // BlockingQueuedConnection is required here to pause the job and wait for user response + + QObject::connect(job, &FileInfoJob::finished, + [&eventLoop]() { + // exit the event loop when the job is done + eventLoop.exit(); + }); + + // run the job in another thread to not block the UI + job->runAsync(); + + // blocking until the job is done with a event loop + eventLoop.exec(); + + // launch the file info + launchFiles(job->files(), ctx); + + delete job; + return false; +} + +GAppInfoPtr BasicFileLauncher::chooseApp(const FileInfoList& /* fileInfos */, const char* /*mimeType*/, GErrorPtr& /* err */) { + return GAppInfoPtr{}; +} + +bool BasicFileLauncher::openFolder(GAppLaunchContext* ctx, const FileInfoList& folderInfos, GErrorPtr& err) { + auto app = chooseApp(folderInfos, "inode/directory", err); + if(app) { + launchWithApp(app.get(), folderInfos.paths(), ctx); + } + else { + showError(ctx, err); + } + return false; +} + +BasicFileLauncher::ExecAction BasicFileLauncher::askExecFile(const FileInfoPtr & /* file */) { + return ExecAction::DIRECT_EXEC; +} + +bool BasicFileLauncher::showError(GAppLaunchContext* /* ctx */, const GErrorPtr & /* err */, const FilePath& /* path */, const FileInfoPtr& /* info */) { + return false; +} + +int BasicFileLauncher::ask(const char* /* msg */, char* const* /* btn_labels */, int default_btn) { + return default_btn; +} + +bool BasicFileLauncher::launchWithApp(GAppInfo* app, const FilePathList& paths, GAppLaunchContext* ctx) { + GList* uris = nullptr; + for(auto& path : paths) { + auto uri = path.uri(); + uris = g_list_prepend(uris, uri.release()); + } + GErrorPtr err; + bool ret = bool(g_app_info_launch_uris(app, uris, ctx, &err)); + g_list_foreach(uris, reinterpret_cast(g_free), nullptr); + g_list_free(uris); + if(!ret) { + // FIXME: show error for all files + showError(ctx, err, paths.empty() ? FilePath{} : paths[0]); + } + return ret; +} + + +bool BasicFileLauncher::launchDesktopEntry(const FileInfoPtr &fileInfo, const FilePathList &paths, GAppLaunchContext* ctx) { + /* treat desktop entries as executables */ + auto target = fileInfo->target(); + CStrPtr filename; + const char* desktopEntryName = nullptr; + FilePathList shortcutTargetPaths; + if(fileInfo->isExecutableType()) { + auto act = (quickExec_ || fileInfo->isTrustable()) ? ExecAction::DIRECT_EXEC : askExecFile(fileInfo); + switch(act) { + case ExecAction::EXEC_IN_TERMINAL: + case ExecAction::DIRECT_EXEC: { + if(fileInfo->isShortcut()) { + auto path = handleShortcut(fileInfo, ctx); + if(path.isValid()) { + shortcutTargetPaths.emplace_back(path); + } + } + else { + if(target.empty()) { + filename = fileInfo->path().localPath(); + } + desktopEntryName = !target.empty() ? target.c_str() : filename.get(); + } + break; + } + case ExecAction::OPEN_WITH_DEFAULT_APP: + return launchWithDefaultApp(fileInfo, ctx); + case ExecAction::CANCEL: + return false; + default: + return false; + } + } + /* make exception for desktop entries under menu */ + else if(fileInfo->isNative() /* an exception */ || + fileInfo->path().hasUriScheme("menu")) { + if(target.empty()) { + filename = fileInfo->path().localPath(); + } + desktopEntryName = !target.empty() ? target.c_str() : filename.get(); + } + + if(desktopEntryName) { + return launchDesktopEntry(desktopEntryName, paths, ctx); + } + if(!shortcutTargetPaths.empty()) { + launchPaths(shortcutTargetPaths, ctx); + } + return false; +} + +bool BasicFileLauncher::launchDesktopEntry(const char *desktopEntryName, const FilePathList &paths, GAppLaunchContext *ctx) { + bool ret = false; + GAppInfo* app; + + /* Let GDesktopAppInfo try first. */ + if(g_path_is_absolute(desktopEntryName)) { + app = G_APP_INFO(g_desktop_app_info_new_from_filename(desktopEntryName)); + } + else { + app = G_APP_INFO(g_desktop_app_info_new(desktopEntryName)); + } + /* we handle Type=Link in FmFileInfo so if GIO failed then + it cannot be launched in fact */ + + if(app) { + // don't call launchWithApp() because it calls g_app_info_launch_uris(), + // which uses the hard-coded terminal list of GLib -> gdesktopappinfo.c + GList* uris = nullptr; + for(auto& path : paths) { + auto uri = path.uri(); + uris = g_list_prepend(uris, uri.release()); + } + GErrorPtr err; + ret = bool(fm_app_info_launch(app, uris, ctx, &err)); + g_list_foreach(uris, reinterpret_cast(g_free), nullptr); + g_list_free(uris); + if(!ret) { + // FIXME: show error for all files + showError(ctx, err, paths.empty() ? FilePath{} : paths[0]); + } + } + else { + QString msg = QObject::tr("Invalid desktop entry file: '%1'").arg(desktopEntryName); + GErrorPtr err{G_IO_ERROR, G_IO_ERROR_FAILED, msg}; + showError(ctx, err); + } + return ret; +} + +FilePath BasicFileLauncher::handleShortcut(const FileInfoPtr& fileInfo, GAppLaunchContext* ctx) { + auto target = fileInfo->target(); + + // if we know the target is a dir, we are not going to open it using other apps + // for example: `network:///smb-root' is a shortcut targeting `smb:///' and it's also a dir + if(fileInfo->isDir()) { + qDebug("shortcut is dir: %s", target.c_str()); + return FilePath::fromPathStr(target.c_str()); + } + + auto scheme = CStrPtr{g_uri_parse_scheme(target.c_str())}; + if(scheme) { + // collect the uri schemes we support + if(strcmp(scheme.get(), "file") == 0 + || strcmp(scheme.get(), "trash") == 0 + || strcmp(scheme.get(), "network") == 0 + || strcmp(scheme.get(), "computer") == 0 + || strcmp(scheme.get(), "menu") == 0) { + return FilePath::fromUri(target.c_str()); + } + else { + // ask gio to launch the default handler for the uri scheme + if(GAppInfoPtr app{g_app_info_get_default_for_uri_scheme(scheme.get()), false}) { + FilePathList uris{FilePath::fromUri(target.c_str())}; + launchWithApp(app.get(), uris, ctx); + } + else { + GErrorPtr err{G_IO_ERROR, G_IO_ERROR_FAILED, + QObject::tr("No default application is set to launch '%1'") + .arg(target.c_str())}; + showError(nullptr, err); + } + } + } + else { + // see it as a local path + return FilePath::fromLocalPath(target.c_str()); + } + return FilePath(); +} + +bool BasicFileLauncher::launchExecutable(const FileInfoPtr &fileInfo, GAppLaunchContext* ctx) { + /* if it's an executable file, directly execute it. */ + auto filename = fileInfo->path().localPath(); + /* FIXME: we need to use eaccess/euidaccess here. */ + if(g_file_test(filename.get(), G_FILE_TEST_IS_EXECUTABLE)) { + auto act = (quickExec_ || fileInfo->isTrustable()) ? ExecAction::DIRECT_EXEC : askExecFile(fileInfo); + int flags = G_APP_INFO_CREATE_NONE; + switch(act) { + case ExecAction::EXEC_IN_TERMINAL: + flags |= G_APP_INFO_CREATE_NEEDS_TERMINAL; + /* Falls through. */ + case ExecAction::DIRECT_EXEC: { + /* filename may contain spaces. Fix #3143296 */ + CStrPtr quoted{g_shell_quote(filename.get())}; + // FIXME: remove libfm dependency + GAppInfoPtr app{fm_app_info_create_from_commandline(quoted.get(), nullptr, GAppInfoCreateFlags(flags), nullptr)}; + if(app) { + CStrPtr run_path{g_path_get_dirname(filename.get())}; + CStrPtr cwd; + /* bug #3589641: scripts are ran from $HOME. + since GIO launcher is kinda ugly - it has + no means to set running directory so we + do workaround - change directory to it */ + if(run_path && strcmp(run_path.get(), ".")) { + cwd = CStrPtr{g_get_current_dir()}; + if(chdir(run_path.get()) != 0) { + cwd.reset(); + // show errors + QString msg = QObject::tr("Cannot set working directory to '%1': %2").arg(run_path.get()).arg(g_strerror(errno)); + GErrorPtr err{G_IO_ERROR, g_io_error_from_errno(errno), msg}; + showError(ctx, err); + } + } + + // FIXME: remove libfm dependency + GErrorPtr err; + if(!fm_app_info_launch(app.get(), nullptr, ctx, &err)) { + showError(ctx, err); + } + if(cwd) { /* return back */ + if(chdir(cwd.get()) != 0) { + g_warning("fm_launch_files(): chdir() failed"); + } + } + return true; + } + break; + } + case ExecAction::OPEN_WITH_DEFAULT_APP: + return launchWithDefaultApp(fileInfo, ctx); + case ExecAction::CANCEL: + default: + break; + } + } + return false; +} + +bool BasicFileLauncher::launchWithDefaultApp(const FileInfoPtr &fileInfo, GAppLaunchContext* ctx) { + FileInfoList files; + files.emplace_back(fileInfo); + GErrorPtr err; + GAppInfoPtr app{g_app_info_get_default_for_type(fileInfo->mimeType()->name(), false), false}; + if(app) { + return launchWithApp(app.get(), files.paths(), ctx); + } + else { + showError(ctx, err, fileInfo->path()); + } + return false; +} + +} // namespace Fm diff --git a/src/core/basicfilelauncher.h b/src/core/basicfilelauncher.h new file mode 100644 index 0000000..3b1545d --- /dev/null +++ b/src/core/basicfilelauncher.h @@ -0,0 +1,72 @@ +#ifndef BASICFILELAUNCHER_H +#define BASICFILELAUNCHER_H + +#include "../libfmqtglobals.h" + +#include "fileinfo.h" +#include "filepath.h" +#include "mimetype.h" + +#include + +namespace Fm { + +class LIBFM_QT_API BasicFileLauncher { +public: + + enum class ExecAction { + NONE, + DIRECT_EXEC, + EXEC_IN_TERMINAL, + OPEN_WITH_DEFAULT_APP, + CANCEL + }; + + explicit BasicFileLauncher(); + virtual ~BasicFileLauncher(); + + bool launchFiles(const FileInfoList &fileInfos, GAppLaunchContext* ctx = nullptr); + + bool launchPaths(FilePathList paths, GAppLaunchContext* ctx = nullptr); + + bool launchDesktopEntry(const FileInfoPtr &fileInfo, const FilePathList& paths = FilePathList{}, GAppLaunchContext* ctx = nullptr); + + bool launchDesktopEntry(const char* desktopEntryName, const FilePathList& paths = FilePathList{}, GAppLaunchContext* ctx = nullptr); + + bool launchWithDefaultApp(const FileInfoPtr& fileInfo, GAppLaunchContext* ctx = nullptr); + + bool launchWithApp(GAppInfo* app, const FilePathList& paths, GAppLaunchContext* ctx = nullptr); + + bool launchExecutable(const FileInfoPtr &fileInfo, GAppLaunchContext* ctx = nullptr); + + bool quickExec() const { + return quickExec_; + } + + void setQuickExec(bool value) { + quickExec_ = value; + } + +protected: + + virtual GAppInfoPtr chooseApp(const FileInfoList& fileInfos, const char* mimeType, GErrorPtr& err); + + virtual bool openFolder(GAppLaunchContext* ctx, const FileInfoList& folderInfos, GErrorPtr& err); + + virtual bool showError(GAppLaunchContext* ctx, const GErrorPtr& err, const FilePath& path = FilePath{}, const FileInfoPtr& info = FileInfoPtr{}); + + virtual ExecAction askExecFile(const FileInfoPtr& file); + + virtual int ask(const char* msg, char* const* btn_labels, int default_btn); + +private: + + FilePath handleShortcut(const FileInfoPtr &fileInfo, GAppLaunchContext* ctx = nullptr); + +private: + bool quickExec_; // Don't ask options on launch executable file +}; + +} // namespace Fm + +#endif // BASICFILELAUNCHER_H diff --git a/src/core/bookmarks.cpp b/src/core/bookmarks.cpp new file mode 100644 index 0000000..93996c8 --- /dev/null +++ b/src/core/bookmarks.cpp @@ -0,0 +1,217 @@ +#include "bookmarks.h" +#include "cstrptr.h" +#include +#include +#include + +namespace Fm { + +std::weak_ptr Bookmarks::globalInstance_; + +static inline CStrPtr get_legacy_bookmarks_file(void) { + return CStrPtr{g_build_filename(g_get_home_dir(), ".gtk-bookmarks", nullptr)}; +} + +static inline CStrPtr get_new_bookmarks_file(void) { + return CStrPtr{g_build_filename(g_get_user_config_dir(), "gtk-3.0", "bookmarks", nullptr)}; +} + +BookmarkItem::BookmarkItem(const FilePath& path, const QString name): + path_{path}, + name_{name} { + if(name_.isEmpty()) { // if the name is not specified, use basename of the path + name_ = path_.baseName().get(); + } + // We cannot rely on FileInfos to set bookmark icons because there is no guarantee + // that FileInfos already exist, while their creation is costly. Therefore, we have + // to get folder icons directly, as is done at `FileInfo::setFromGFileInfo` and more. + auto local_path = path.localPath(); + auto dot_dir = CStrPtr{g_build_filename(local_path.get(), ".directory", nullptr)}; + if(g_file_test(dot_dir.get(), G_FILE_TEST_IS_REGULAR)) { + GKeyFile* kf = g_key_file_new(); + if(g_key_file_load_from_file(kf, dot_dir.get(), G_KEY_FILE_NONE, nullptr)) { + CStrPtr icon_name{g_key_file_get_string(kf, "Desktop Entry", "Icon", nullptr)}; + if(icon_name) { + icon_ = IconInfo::fromName(icon_name.get()); + } + } + g_key_file_free(kf); + } + if(!icon_ || !icon_->isValid()) { + // first check some standard folders that are shared by Qt and GLib + if(path_ == FilePath::homeDir()) { + icon_ = IconInfo::fromName("user-home"); + } + else if (path_.parent() == FilePath::homeDir()) { + QString folderPath = QString::fromUtf8(path_.toString().get()); + if(folderPath == QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)) { + icon_ = IconInfo::fromName("user-desktop"); + } + else if(folderPath == QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)) { + icon_ = IconInfo::fromName("folder-documents"); + } + else if(folderPath == QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)) { + icon_ = IconInfo::fromName("folder-download"); + } + else if(folderPath == QStandardPaths::writableLocation(QStandardPaths::MusicLocation)) { + icon_ = IconInfo::fromName("folder-music"); + } + else if(folderPath == QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)) { + icon_ = IconInfo::fromName("folder-pictures"); + } + else if(folderPath == QStandardPaths::writableLocation(QStandardPaths::MoviesLocation)) { + icon_ = IconInfo::fromName("folder-videos"); + } + } + // fall back to the default folder icon + if(!icon_ || !icon_->isValid()) { + icon_ = IconInfo::fromName("folder"); + } + } +} + +Bookmarks::Bookmarks(QObject* parent): + QObject(parent), + idle_handler{false} { + + /* trying the gtk-3.0 first and use it if it exists */ + auto fpath = get_new_bookmarks_file(); + file = FilePath::fromLocalPath(fpath.get()); + load(); + if(items_.empty()) { /* not found, use legacy file */ + fpath = get_legacy_bookmarks_file(); + file = FilePath::fromLocalPath(fpath.get()); + load(); + } + mon = GObjectPtr{g_file_monitor_file(file.gfile().get(), G_FILE_MONITOR_NONE, nullptr, nullptr), false}; + if(mon) { + g_signal_connect(mon.get(), "changed", G_CALLBACK(_onFileChanged), this); + } +} + +Bookmarks::~Bookmarks() { + if(mon) { + g_signal_handlers_disconnect_by_data(mon.get(), this); + } +} + +const std::shared_ptr& Bookmarks::insert(const FilePath& path, const QString& name, int pos) { + const auto insert_pos = (pos < 0 || static_cast(pos) > items_.size()) ? items_.cend() : items_.cbegin() + pos; + auto it = items_.insert(insert_pos, std::make_shared(path, name)); + queueSave(); + return *it; +} + +void Bookmarks::remove(const std::shared_ptr& item) { + items_.erase(std::remove(items_.begin(), items_.end(), item), items_.end()); + queueSave(); +} + +void Bookmarks::reorder(const std::shared_ptr& item, int pos) { + auto old_it = std::find(items_.cbegin(), items_.cend(), item); + if(old_it == items_.cend()) + return; + std::shared_ptr newItem = item; + auto old_pos = old_it - items_.cbegin(); + items_.erase(old_it); + if(old_pos < pos) + --pos; + auto new_it = items_.cbegin() + pos; + if(new_it > items_.cend()) + new_it = items_.cend(); + items_.insert(new_it, std::move(newItem)); + queueSave(); +} + +void Bookmarks::rename(const std::shared_ptr& item, QString new_name) { + auto it = std::find_if(items_.cbegin(), items_.cend(), [item](const std::shared_ptr& elem) { + return elem->path() == item->path(); + }); + if(it != items_.cend()) { + // create a new item to replace the old one + // we do not modify the old item directly since this data structure is shared with others + it = items_.insert(it, std::make_shared(item->path(), new_name)); + items_.erase(it + 1); // remove the old item + queueSave(); + } +} + +std::shared_ptr Bookmarks::globalInstance() { + auto bookmarks = globalInstance_.lock(); + if(!bookmarks) { + bookmarks = std::make_shared(); + globalInstance_ = bookmarks; + } + return bookmarks; +} + +void Bookmarks::save() { + std::string buf; + // G_LOCK(bookmarks); + for(auto& item: items_) { + auto uri = item->path().uri(); + buf += uri.get(); + buf += ' '; + buf += item->name().toUtf8().constData(); + buf += '\n'; + } + idle_handler = false; + // G_UNLOCK(bookmarks); + GError* err = nullptr; + if(!g_file_replace_contents(file.gfile().get(), buf.c_str(), buf.length(), nullptr, + FALSE, G_FILE_CREATE_NONE, nullptr, nullptr, &err)) { + g_critical("%s", err->message); + g_error_free(err); + } + /* we changed bookmarks list, let inform who interested in that */ + Q_EMIT changed(); +} + +void Bookmarks::load() { + auto fpath = file.localPath(); + FILE* f; + char buf[1024]; + /* load the file */ + f = fopen(fpath.get(), "r"); + if(f) { + while(fgets(buf, 1024, f)) { + // format of each line in the bookmark file: + // \n + char* sep; + sep = strchr(buf, '\n'); + if(sep) { + *sep = '\0'; + } + + QString name; + sep = strchr(buf, ' '); // find the separator between URI and name + if(sep) { + *sep = '\0'; + name = sep + 1; + } + auto uri = buf; + if(uri[0] != '\0') { + items_.push_back(std::make_shared(FilePath::fromUri(uri), name)); + } + } + fclose(f); + } +} + +void Bookmarks::onFileChanged(GFileMonitor* /*mon*/, GFile* /*gf*/, GFile* /*other*/, GFileMonitorEvent /*evt*/) { + // reload the bookmarks + items_.clear(); + load(); + Q_EMIT changed(); +} + + +void Bookmarks::queueSave() { + if(!idle_handler) { + QTimer::singleShot(0, this, &Bookmarks::save); + idle_handler = true; + } +} + + +} // namespace Fm diff --git a/src/core/bookmarks.h b/src/core/bookmarks.h new file mode 100644 index 0000000..7b5af79 --- /dev/null +++ b/src/core/bookmarks.h @@ -0,0 +1,87 @@ +#ifndef FM2_BOOKMARKS_H +#define FM2_BOOKMARKS_H + +#include +#include "gobjectptr.h" +#include "filepath.h" +#include "iconinfo.h" + +namespace Fm { + +class LIBFM_QT_API BookmarkItem { +public: + friend class Bookmarks; + + explicit BookmarkItem(const FilePath& path, const QString name); + + const QString& name() const { + return name_; + } + + const FilePath& path() const { + return path_; + } + + const std::shared_ptr& icon() const { + return icon_; + } + +private: + void setName(const QString& name) { + name_ = name; + } + +private: + FilePath path_; + QString name_; + std::shared_ptr icon_; +}; + + +class LIBFM_QT_API Bookmarks : public QObject { + Q_OBJECT +public: + explicit Bookmarks(QObject* parent = 0); + + ~Bookmarks(); + + const std::shared_ptr &insert(const FilePath& path, const QString& name, int pos); + + void remove(const std::shared_ptr& item); + + void reorder(const std::shared_ptr &item, int pos); + + void rename(const std::shared_ptr& item, QString new_name); + + const std::vector>& items() const { + return items_; + } + + static std::shared_ptr globalInstance(); + +Q_SIGNALS: + void changed(); + +private Q_SLOTS: + void save(); + +private: + void load(); + void queueSave(); + + static void _onFileChanged(GFileMonitor* mon, GFile* gf, GFile* other, GFileMonitorEvent evt, Bookmarks* _this) { + _this->onFileChanged(mon, gf, other, evt); + } + void onFileChanged(GFileMonitor* mon, GFile* gf, GFile* other, GFileMonitorEvent evt); + +private: + FilePath file; + GObjectPtr mon; + std::vector> items_; + static std::weak_ptr globalInstance_; + bool idle_handler; +}; + +} // namespace Fm + +#endif // FM2_BOOKMARKS_H diff --git a/src/core/cstrptr.h b/src/core/cstrptr.h new file mode 100644 index 0000000..5c06f0a --- /dev/null +++ b/src/core/cstrptr.h @@ -0,0 +1,42 @@ +#ifndef FM2_CSTRPTR_H +#define FM2_CSTRPTR_H + +#include +#include + +namespace Fm { + +struct CStrDeleter { + void operator()(char* ptr) const { + g_free(ptr); + } +}; + +// smart pointer for C string (char*) which should be freed by free() +typedef std::unique_ptr CStrPtr; + +struct CStrHash { + std::size_t operator()(const char* str) const { + return g_str_hash(str); + } +}; + +struct CStrEqual { + bool operator()(const char* str1, const char* str2) const { + return g_str_equal(str1, str2); + } +}; + +struct CStrVDeleter { + void operator()(char** ptr) const { + g_strfreev(ptr); + } +}; + +// smart pointer for C string array (char**) which should be freed by g_strfreev() of glib +typedef std::unique_ptr CStrArrayPtr; + + +} // namespace Fm + +#endif // FM2_CSTRPTR_H diff --git a/src/core/deletejob.cpp b/src/core/deletejob.cpp new file mode 100644 index 0000000..6c3c550 --- /dev/null +++ b/src/core/deletejob.cpp @@ -0,0 +1,158 @@ +#include "deletejob.h" +#include "totalsizejob.h" +#include "fileinfo_p.h" + +namespace Fm { + +bool DeleteJob::deleteFile(const FilePath& path, GFileInfoPtr inf) { + ErrorAction act = ErrorAction::CONTINUE; + while(!inf) { + GErrorPtr err; + inf = GFileInfoPtr{ + g_file_query_info(path.gfile().get(), "standard::*", + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err), + false + }; + if(err) { + act = emitError(err, ErrorSeverity::SEVERE); + if(act == ErrorAction::ABORT) { + return false; + } + if(act != ErrorAction::RETRY) { + break; + } + } + } + + // TODO: get parent dir of the current path. + // if there is a Fm::Folder object created for it, block the update for the folder temporarily. + + /* currently processed file. */ + setCurrentFile(path); + + if(g_file_info_get_file_type(inf.get()) == G_FILE_TYPE_DIRECTORY) { + // delete the content of the dir prior to deleting itself + deleteDirContent(path, inf); + } + + bool isTrashRoot = false; + // special handling for trash:/// + if(!path.isNative() && g_strcmp0(path.uriScheme().get(), "trash") == 0) { + // little trick: basename of trash root is / + auto basename = path.baseName(); + if(basename && basename[0] == G_DIR_SEPARATOR) { + isTrashRoot = true; + } + } + + bool hasError = false; + while(!isCancelled()) { + GErrorPtr err; + // try to delete the path directly (but don't delete if it's trash:///) + if(isTrashRoot || g_file_delete(path.gfile().get(), cancellable().get(), &err)) { + break; + } + if(err) { + // FIXME: error handling + /* if it's non-empty dir then descent into it then try again */ + /* trash root gives G_IO_ERROR_PERMISSION_DENIED */ + if(err.domain() == G_IO_ERROR && err.code() == G_IO_ERROR_NOT_EMPTY) { + deleteDirContent(path, inf); + } + else if(err.domain() == G_IO_ERROR && err.code() == G_IO_ERROR_PERMISSION_DENIED) { + /* special case for trash:/// */ + /* FIXME: is there any better way to handle this? */ + auto scheme = path.uriScheme(); + if(g_strcmp0(scheme.get(), "trash") == 0) { + break; + } + } + act = emitError(err, ErrorSeverity::MODERATE); + if(act != ErrorAction::RETRY) { + hasError = true; + break; + } + } + } + + addFinishedAmount(g_file_info_get_size(inf.get()), 1); + + return !hasError; +} + +bool DeleteJob::deleteDirContent(const FilePath& path, GFileInfoPtr inf) { + GErrorPtr err; + GFileEnumeratorPtr enu { + g_file_enumerate_children(path.gfile().get(), defaultGFileInfoQueryAttribs, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err), + false + }; + if(!enu) { + emitError(err, ErrorSeverity::MODERATE); + return false; + } + + bool hasError = false; + while(!isCancelled()) { + inf = GFileInfoPtr{ + g_file_enumerator_next_file(enu.get(), cancellable().get(), &err), + false + }; + if(inf) { + auto subPath = path.child(g_file_info_get_name(inf.get())); + if(!deleteFile(subPath, inf)) { + continue; + } + } + else { + if(err) { + emitError(err, ErrorSeverity::MODERATE); + /* ErrorAction::RETRY is not supported here */ + hasError = true; + } + else { /* EOF */ + } + break; + } + } + g_file_enumerator_close(enu.get(), nullptr, nullptr); + return !hasError; +} + + +DeleteJob::DeleteJob(const FilePathList &paths): paths_{paths} { + setCalcProgressUsingSize(false); +} + +DeleteJob::DeleteJob(FilePathList &&paths): paths_{paths} { + setCalcProgressUsingSize(false); +} + +DeleteJob::~DeleteJob() { +} + +void DeleteJob::exec() { + /* prepare the job, count total work needed with FmDeepCountJob */ + TotalSizeJob totalSizeJob{paths_, TotalSizeJob::Flags::PREPARE_DELETE}; + connect(&totalSizeJob, &TotalSizeJob::error, this, &DeleteJob::error); + connect(this, &DeleteJob::cancelled, &totalSizeJob, &TotalSizeJob::cancel); + totalSizeJob.run(); + + if(isCancelled()) { + return; + } + + setTotalAmount(totalSizeJob.totalSize(), totalSizeJob.fileCount()); + Q_EMIT preparedToRun(); + + for(auto& path : paths_) { + if(isCancelled()) { + break; + } + deleteFile(path, GFileInfoPtr{nullptr}); + } +} + +} // namespace Fm diff --git a/src/core/deletejob.h b/src/core/deletejob.h new file mode 100644 index 0000000..8ad2321 --- /dev/null +++ b/src/core/deletejob.h @@ -0,0 +1,33 @@ +#ifndef FM2_DELETEJOB_H +#define FM2_DELETEJOB_H + +#include "../libfmqtglobals.h" +#include "fileoperationjob.h" +#include "filepath.h" +#include "gioptrs.h" + +namespace Fm { + +class LIBFM_QT_API DeleteJob : public Fm::FileOperationJob { + Q_OBJECT +public: + explicit DeleteJob(const FilePathList& paths); + + explicit DeleteJob(FilePathList&& paths); + + ~DeleteJob(); + +protected: + void exec() override; + +private: + bool deleteFile(const FilePath& path, GFileInfoPtr inf); + bool deleteDirContent(const FilePath& path, GFileInfoPtr inf); + +private: + FilePathList paths_; +}; + +} // namespace Fm + +#endif // FM2_DELETEJOB_H diff --git a/src/core/dirlistjob.cpp b/src/core/dirlistjob.cpp new file mode 100644 index 0000000..abecff9 --- /dev/null +++ b/src/core/dirlistjob.cpp @@ -0,0 +1,180 @@ +#include "dirlistjob.h" +#include +#include "fileinfo_p.h" +#include "gioptrs.h" +#include + +namespace Fm { + +DirListJob::DirListJob(const FilePath& path, Flags _flags, const std::shared_ptr& cutFilesHashSet): + dir_path{path}, flags{_flags}, cutFilesHashSet_{cutFilesHashSet} { +} + +void DirListJob::exec() { + GErrorPtr err; + GFileInfoPtr dir_inf; + GFilePtr dir_gfile = dir_path.gfile(); + // FIXME: these are hacks for search:/// URI implemented by libfm which contains some bugs + bool isFileSearch = dir_path.hasUriScheme("search"); + if(isFileSearch) { + // NOTE: The GFile instance changes its URI during file enumeration (bad design). + // So we create a copy here to avoid channging the gfile stored in dir_path. + // FIXME: later we should refactor file search and remove this dirty hack. + dir_gfile = GFilePtr{g_file_dup(dir_gfile.get())}; + } +_retry: + err.reset(); + dir_inf = GFileInfoPtr{ + g_file_query_info(dir_gfile.get(), defaultGFileInfoQueryAttribs, + G_FILE_QUERY_INFO_NONE, cancellable().get(), &err), + false + }; + if(!dir_inf) { + ErrorAction act = emitError(err, ErrorSeverity::MODERATE); + if(act == ErrorAction::RETRY) { + err.reset(); + goto _retry; + } + return; + } + + if(g_file_info_get_file_type(dir_inf.get()) != G_FILE_TYPE_DIRECTORY) { + auto path_str = dir_path.toString(); + err = GErrorPtr{ + G_IO_ERROR, + G_IO_ERROR_NOT_DIRECTORY, + tr("The specified directory '%1' is not valid").arg(path_str.get()) + }; + emitError(err, ErrorSeverity::CRITICAL); + return; + } + else { + std::lock_guard lock{mutex_}; + dir_fi = std::make_shared(dir_inf, dir_path); + } + + FileInfoList foundFiles; + /* check if FS is R/O and set attr. into inf */ + // FIXME: _fm_file_info_job_update_fs_readonly(gf, inf, nullptr, nullptr); + err.reset(); + GFileEnumeratorPtr enu = GFileEnumeratorPtr{ + g_file_enumerate_children(dir_gfile.get(), defaultGFileInfoQueryAttribs, + G_FILE_QUERY_INFO_NONE, cancellable().get(), &err), + false + }; + if(enu) { + // qDebug() << "START LISTING:" << dir_path.toString().get(); + while(!isCancelled()) { + err.reset(); + GFileInfoPtr inf{g_file_enumerator_next_file(enu.get(), cancellable().get(), &err), false}; + if(inf) { +#if 0 + FmPath* dir, *sub; + GFile* child; + if(G_UNLIKELY(job->flags & FM_DIR_LIST_JOB_DIR_ONLY)) { + /* FIXME: handle symlinks */ + if(g_file_info_get_file_type(inf) != G_FILE_TYPE_DIRECTORY) { + g_object_unref(inf); + continue; + } + } +#endif + // virtual folders may return children not within them + // For example: the search:/// URI implemented by libfm might return files from different folders during enumeration. + // So here we call g_file_enumerator_get_container() to get the real parent path rather than simply using dir_path. + // This is not the behaviour of gio, but the extensions by libfm might do this. + // FIXME: after we port these vfs implementation from libfm, we can redesign this. + FilePath realParentPath = FilePath{g_file_enumerator_get_container(enu.get()), true}; + if(isFileSearch) { // this is a file sarch job (search:/// URI) + // FIXME: redesign file search and remove this dirty hack + // the libfm implementation of search:/// URI returns a customized GFile implementation that does not behave normally. + // let's get its actual URI and re-create a normal gio GFile instance from it. + realParentPath = FilePath::fromUri(realParentPath.uri().get()); + } +#if 0 + if(g_file_info_get_file_type(inf) == G_FILE_TYPE_DIRECTORY) + /* for dir: check if its FS is R/O and set attr. into inf */ + { + _fm_file_info_job_update_fs_readonly(child, inf, nullptr, nullptr); + } + fi = fm_file_info_new_from_g_file_data(child, inf, sub); +#endif + auto fileInfo = std::make_shared(inf, FilePath(), realParentPath); + if(emit_files_found) { + // Q_EMIT filesFound(); + } + + if(cutFilesHashSet_ + && cutFilesHashSet_->count(fileInfo->path().hash()) > 0) { + fileInfo->bindCutFiles(cutFilesHashSet_); + } + + foundFiles.push_back(std::move(fileInfo)); + } + else { + if(err) { + ErrorAction act = emitError(err, ErrorSeverity::MILD); + /* ErrorAction::RETRY is not supported. */ + if(act == ErrorAction::ABORT) { + cancel(); + } + } + /* otherwise it's EOL */ + break; + } + } + err.reset(); + g_file_enumerator_close(enu.get(), cancellable().get(), &err); + } + else { + emitError(err, err.domain() == G_IO_ERROR && err.code() == G_IO_ERROR_CANCELLED + ? ErrorSeverity::MILD // may happen at Folder::reload() + : ErrorSeverity::CRITICAL); + } + + // qDebug() << "END LISTING:" << dir_path.toString().get(); + if(!foundFiles.empty()) { + std::lock_guard lock{mutex_}; + files_.swap(foundFiles); + } +} + +#if 0 +//FIXME: incremental.. + +static gboolean emit_found_files(gpointer user_data) { + /* this callback is called from the main thread */ + FmDirListJob* job = FM_DIR_LIST_JOB(user_data); + /* g_print("emit_found_files: %d\n", g_slist_length(job->files_to_add)); */ + + if(g_source_is_destroyed(g_main_current_source())) { + return FALSE; + } + g_signal_emit(job, signals[FILES_FOUND], 0, job->files_to_add); + g_slist_free_full(job->files_to_add, (GDestroyNotify)fm_file_info_unref); + job->files_to_add = nullptr; + job->delay_add_files_handler = 0; + return FALSE; +} + +static gpointer queue_add_file(FmJob* fmjob, gpointer user_data) { + FmDirListJob* job = FM_DIR_LIST_JOB(fmjob); + FmFileInfo* file = FM_FILE_INFO(user_data); + /* this callback is called from the main thread */ + /* g_print("queue_add_file: %s\n", fm_file_info_get_disp_name(file)); */ + job->files_to_add = g_slist_prepend(job->files_to_add, fm_file_info_ref(file)); + if(job->delay_add_files_handler == 0) + job->delay_add_files_handler = g_timeout_add_seconds_full(G_PRIORITY_LOW, + 1, emit_found_files, g_object_ref(job), g_object_unref); + return nullptr; +} + +void fm_dir_list_job_add_found_file(FmDirListJob* job, FmFileInfo* file) { + fm_file_info_list_push_tail(job->files, file); + if(G_UNLIKELY(job->emit_files_found)) { + fm_job_call_main_thread(FM_JOB(job), queue_add_file, file); + } +} +#endif + +} // namespace Fm diff --git a/src/core/dirlistjob.h b/src/core/dirlistjob.h new file mode 100644 index 0000000..e578521 --- /dev/null +++ b/src/core/dirlistjob.h @@ -0,0 +1,65 @@ +#ifndef FM2_DIRLISTJOB_H +#define FM2_DIRLISTJOB_H + +#include "../libfmqtglobals.h" +#include +#include "job.h" +#include "filepath.h" +#include "gobjectptr.h" +#include "fileinfo.h" + +namespace Fm { + +class LIBFM_QT_API DirListJob : public Job { + Q_OBJECT +public: + enum Flags { + FAST = 0, + DIR_ONLY = 1 << 0, + DETAILED = 1 << 1 + }; + + explicit DirListJob(const FilePath& path, Flags flags, const std::shared_ptr& cutFilesHashSet = nullptr); + + FileInfoList& files() { + return files_; + } + + void setIncremental(bool set); + + bool incremental() const { + return emit_files_found; + } + + FilePath dirPath() const { + std::lock_guard lock{mutex_}; + return dir_path; + } + + std::shared_ptr dirInfo() const { + std::lock_guard lock{mutex_}; + return dir_fi; + } + +Q_SIGNALS: + void filesFound(FileInfoList& foundFiles); + +protected: + + void exec() override; + +private: + mutable std::mutex mutex_; + FilePath dir_path; + Flags flags; + std::shared_ptr dir_fi; + FileInfoList files_; + const std::shared_ptr cutFilesHashSet_; + bool emit_files_found; + // guint delay_add_files_handler; + // GSList* files_to_add; +}; + +} // namespace Fm + +#endif // FM2_DIRLISTJOB_H diff --git a/src/core/filechangeattrjob.cpp b/src/core/filechangeattrjob.cpp new file mode 100644 index 0000000..8ed2abb --- /dev/null +++ b/src/core/filechangeattrjob.cpp @@ -0,0 +1,324 @@ +#include "filechangeattrjob.h" +#include "totalsizejob.h" + +#include + +namespace Fm { + +static const char query[] = G_FILE_ATTRIBUTE_STANDARD_TYPE"," + G_FILE_ATTRIBUTE_STANDARD_NAME"," + G_FILE_ATTRIBUTE_UNIX_GID"," + G_FILE_ATTRIBUTE_UNIX_UID"," + G_FILE_ATTRIBUTE_UNIX_MODE"," + G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME; + +FileChangeAttrJob::FileChangeAttrJob(FilePathList paths): + paths_{std::move(paths)}, + recursive_{false}, + // chmod + fileModeEnabled_{false}, + newMode_{0}, + newModeMask_{0}, + // chown + ownerEnabled_{false}, + uid_{0}, + groupEnabled_{false}, + gid_{0}, + // Display name + displayNameEnabled_{false}, + // icon + iconEnabled_{false}, + // hidden + hiddenEnabled_{false}, + hidden_{false}, + // target uri + targetUriEnabled_{false} { + + // the progress of chmod/chown is not related to file size + setCalcProgressUsingSize(false); +} + +void FileChangeAttrJob::exec() { + // count total amount of the work + if(recursive_) { + TotalSizeJob totalSizeJob{paths_}; + connect(&totalSizeJob, &TotalSizeJob::error, this, &FileChangeAttrJob::error); + connect(this, &FileChangeAttrJob::cancelled, &totalSizeJob, &TotalSizeJob::cancel); + totalSizeJob.run(); + std::uint64_t totalSize, totalCount; + totalSizeJob.totalAmount(totalSize, totalCount); + setTotalAmount(totalSize, totalCount); + } + else { + setTotalAmount(paths_.size(), paths_.size()); + } + + // ready to start + Q_EMIT preparedToRun(); + + // do the actual change attrs job + for(auto& path : paths_) { + if(isCancelled()) { + break; + } + GErrorPtr err; + GFileInfoPtr info{ + g_file_query_info(path.gfile().get(), query, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err), + false + }; + if(info) { + processFile(path, info); + } + else { + handleError(err, path, info); + } + } +} + +bool FileChangeAttrJob::processFile(const FilePath& path, const GFileInfoPtr& info) { + setCurrentFile(path); + bool ret = true; + + if(ownerEnabled_) { + changeFileOwner(path, info, uid_); + } + if(groupEnabled_) { + changeFileGroup(path, info, gid_); + } + if(fileModeEnabled_) { + changeFileMode(path, info, newMode_, newModeMask_); + } + /* change display name, icon, hidden, target */ + if(displayNameEnabled_ && !displayName().empty()) { + changeFileDisplayName(path, info, displayName_.c_str()); + } + if(iconEnabled_ && icon_) { + changeFileIcon(path, info, icon_); + } + if(hiddenEnabled_) { + changeFileHidden(path, info, hidden_); + } + if(targetUriEnabled_ && !targetUri_.empty()) { + changeFileTargetUri(path, info, targetUri_.c_str()); + } + + // FIXME: do not use size 1 here. + addFinishedAmount(1, 1); + + // recursively apply to subfolders + auto type = g_file_info_get_file_type(info.get()); + if(!isCancelled() && recursive_ && type == G_FILE_TYPE_DIRECTORY) { + bool retry; + do { + retry = false; + GErrorPtr err; + GFileEnumeratorPtr enu{ + g_file_enumerate_children(path.gfile().get(), query, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err), + false + }; + if(enu) { + while(!isCancelled()) { + err.reset(); + GFileInfoPtr childInfo{g_file_enumerator_next_file(enu.get(), cancellable().get(), &err), false}; + if(childInfo) { + auto childPath = path.child(g_file_info_get_name(childInfo.get())); + ret = processFile(childPath, childInfo); + if(!ret) { /* _fm_file_ops_job_change_attr_file() failed */ + break; + } + } + else { + if(err) { + handleError(err, path, info, ErrorSeverity::MILD); + retry = false; + /* FM_JOB_RETRY is not supported here */ + } + else { /* EOF */ + break; + } + } + } + g_file_enumerator_close(enu.get(), cancellable().get(), nullptr); + } + else { + retry = handleError(err, path, info); + } + } while(!isCancelled() && retry); + } + return ret; +} + +bool FileChangeAttrJob::handleError(GErrorPtr &err, const FilePath &path, const GFileInfoPtr &info, ErrorSeverity severity) { + auto act = emitError(err, severity); + if (act == ErrorAction::RETRY) { + err.reset(); + return true; + } + return false; +} + +bool FileChangeAttrJob::changeFileOwner(const FilePath& path, const GFileInfoPtr& info, uid_t uid) { + /* change owner */ + bool ret = false; + bool retry; + do { + GErrorPtr err; + if(!g_file_set_attribute_uint32(path.gfile().get(), G_FILE_ATTRIBUTE_UNIX_UID, + uid, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err)) { + retry = handleError(err, path, info, ErrorSeverity::MILD); + err.reset(); + } + else { + ret = true; + retry = false; + } + } while(retry && !isCancelled()); + return ret; +} + +bool FileChangeAttrJob::changeFileGroup(const FilePath& path, const GFileInfoPtr& info, gid_t gid) { + /* change group */ + bool ret = false; + bool retry; + do { + GErrorPtr err; + if(!g_file_set_attribute_uint32(path.gfile().get(), G_FILE_ATTRIBUTE_UNIX_GID, + gid, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err)) { + retry = handleError(err, path, info, ErrorSeverity::MILD); + err.reset(); + } + else { + ret = true; + retry = false; + } + } while(retry && !isCancelled()); + return ret; +} + +bool FileChangeAttrJob::changeFileMode(const FilePath& path, const GFileInfoPtr& info, mode_t newMode, mode_t newModeMask) { + bool ret = false; + /* change mode */ + if(newModeMask) { + guint32 mode = g_file_info_get_attribute_uint32(info.get(), G_FILE_ATTRIBUTE_UNIX_MODE); + mode &= ~newModeMask; + mode |= (newMode & newModeMask); + + auto type = g_file_info_get_file_type(info.get()); + /* FIXME: this behavior should be optional. */ + /* treat dirs with 'r' as 'rx' */ + if(type == G_FILE_TYPE_DIRECTORY) { + if((newModeMask & S_IRUSR) && (mode & S_IRUSR)) { + mode |= S_IXUSR; + } + if((newModeMask & S_IRGRP) && (mode & S_IRGRP)) { + mode |= S_IXGRP; + } + if((newModeMask & S_IROTH) && (mode & S_IROTH)) { + mode |= S_IXOTH; + } + } + + /* new mode */ + bool retry; + do { + GErrorPtr err; + if(!g_file_set_attribute_uint32(path.gfile().get(), G_FILE_ATTRIBUTE_UNIX_MODE, + mode, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err)) { + retry = handleError(err, path, info, ErrorSeverity::MILD); + err.reset(); + } + else { + ret = true; + retry = false; + } + } while(retry && !isCancelled()); + } + return ret; + +} + +bool FileChangeAttrJob::changeFileDisplayName(const FilePath& path, const GFileInfoPtr& info, const char* displayName) { + bool ret = false; + bool retry; + do { + GErrorPtr err; + if(!g_file_set_display_name(path.gfile().get(), displayName, cancellable().get(), &err)) { + retry = handleError(err, path, info, ErrorSeverity::MILD); + err.reset(); + } + else { + ret = true; + retry = false; + } + } while(retry && !isCancelled()); + return ret; +} + +bool FileChangeAttrJob::changeFileIcon(const FilePath& path, const GFileInfoPtr& info, GIconPtr& icon) { + bool ret = false; + bool retry; + do { + GErrorPtr err; + if(!g_file_set_attribute(path.gfile().get(), G_FILE_ATTRIBUTE_STANDARD_ICON, + G_FILE_ATTRIBUTE_TYPE_OBJECT, icon.get(), + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err)) { + retry = handleError(err, path, info, ErrorSeverity::MILD); + err.reset(); + } + else { + ret = true; + retry = false; + } + } while(retry && !isCancelled()); + return ret; +} + +bool FileChangeAttrJob::changeFileHidden(const FilePath& path, const GFileInfoPtr& info, bool hidden) { + bool ret = false; + bool retry; + do { + GErrorPtr err; + gboolean g_hidden = hidden; + if(!g_file_set_attribute(path.gfile().get(), G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN, + G_FILE_ATTRIBUTE_TYPE_BOOLEAN, &g_hidden, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err)) { + retry = handleError(err, path, info, ErrorSeverity::MILD); + err.reset(); + } + else { + ret = true; + retry = false; + } + } while(retry && !isCancelled()); + return ret; +} + +bool FileChangeAttrJob::changeFileTargetUri(const FilePath& path, const GFileInfoPtr& info, const char* targetUri) { + bool ret = false; + bool retry; + do { + GErrorPtr err; + if(!g_file_set_attribute_string(path.gfile().get(), G_FILE_ATTRIBUTE_STANDARD_TARGET_URI, + targetUri, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err)) { + retry = handleError(err, path, info, ErrorSeverity::MILD); + err.reset(); + } + else { + ret = true; + retry = false; + } + } while(retry && !isCancelled()); + return ret; +} + +} // namespace Fm diff --git a/src/core/filechangeattrjob.h b/src/core/filechangeattrjob.h new file mode 100644 index 0000000..3f15863 --- /dev/null +++ b/src/core/filechangeattrjob.h @@ -0,0 +1,145 @@ +#ifndef FM2_FILECHANGEATTRJOB_H +#define FM2_FILECHANGEATTRJOB_H + +#include "../libfmqtglobals.h" +#include "fileoperationjob.h" +#include "gioptrs.h" + +#include + +#include + +namespace Fm { + +class LIBFM_QT_API FileChangeAttrJob : public Fm::FileOperationJob { + Q_OBJECT +public: + explicit FileChangeAttrJob(FilePathList paths); + + void setFileModeEnabled(bool enabled) { + fileModeEnabled_ = enabled; + } + void setFileMode(mode_t newMode, mode_t newModeMask) { + newMode_ = newMode; + newModeMask_ = newModeMask; + } + + bool ownerEnabled() const { + return ownerEnabled_; + } + void setOwnerEnabled(bool enabled) { + ownerEnabled_ = enabled; + } + void setOwner(uid_t uid) { + uid_ = uid; + } + + bool groupEnabled() const { + return groupEnabled_; + } + void setGroupEnabled(bool groupEnabled) { + groupEnabled_ = groupEnabled; + } + void setGroup(gid_t gid) { + gid_ = gid; + } + + // This only work for change attr jobs. + void setRecursive(bool recursive) { + recursive_ = recursive; + } + + void setHiddenEnabled(bool enabled) { + hiddenEnabled_ = enabled; + } + void setHidden(bool hidden) { + hidden_ = hidden; + } + + bool iconEnabled() const { + return iconEnabled_; + } + void setIconEnabled(bool iconEnabled) { + iconEnabled_ = iconEnabled; + } + + bool displayNameEnabled() const { + return displayNameEnabled_; + } + void setDisplayNameEnabled(bool displayNameEnabled) { + displayNameEnabled_ = displayNameEnabled; + } + + const std::string& displayName() const { + return displayName_; + } + void setDisplayName(const std::string& displayName) { + displayName_ = displayName; + } + + bool targetUriEnabled() const { + return targetUriEnabled_; + } + void setTargetUriEnabled(bool targetUriEnabled) { + targetUriEnabled_ = targetUriEnabled; + } + + const std::string& targetUri() const { + return targetUri_; + } + void setTargetUri(const std::string& value) { + targetUri_ = value; + } + + +protected: + void exec() override; + +private: + bool processFile(const FilePath& path, const GFileInfoPtr& info); + bool handleError(GErrorPtr& err, const FilePath& path, const GFileInfoPtr& info, ErrorSeverity severity = ErrorSeverity::MODERATE); + + bool changeFileOwner(const FilePath& path, const GFileInfoPtr& info, uid_t uid); + bool changeFileGroup(const FilePath& path, const GFileInfoPtr& info, gid_t gid); + bool changeFileMode(const FilePath& path, const GFileInfoPtr& info, mode_t newMode, mode_t newModeMask); + bool changeFileDisplayName(const FilePath& path, const GFileInfoPtr& info, const char* displayName); + bool changeFileIcon(const FilePath& path, const GFileInfoPtr& info, GIconPtr& icon); + bool changeFileHidden(const FilePath& path, const GFileInfoPtr& info, bool hidden); + bool changeFileTargetUri(const FilePath& path, const GFileInfoPtr& info, const char* targetUri_); + +private: + FilePathList paths_; + bool recursive_; + + // chmod + bool fileModeEnabled_; + mode_t newMode_; + mode_t newModeMask_; + + // chown + bool ownerEnabled_; + uid_t uid_; + + bool groupEnabled_; + gid_t gid_; + + // Display name + bool displayNameEnabled_; + std::string displayName_; + + // icon + bool iconEnabled_; + GIconPtr icon_; + + // hidden + bool hiddenEnabled_; + bool hidden_; + + // target uri + bool targetUriEnabled_; + std::string targetUri_; +}; + +} // namespace Fm + +#endif // FM2_FILECHANGEATTRJOB_H diff --git a/src/core/fileinfo.cpp b/src/core/fileinfo.cpp new file mode 100644 index 0000000..380835e --- /dev/null +++ b/src/core/fileinfo.cpp @@ -0,0 +1,453 @@ +#include "fileinfo.h" +#include "fileinfo_p.h" +#include + +#define METADATA_TRUST "metadata::trust" + +namespace Fm { + +const char defaultGFileInfoQueryAttribs[] = "standard::*," + "unix::*," + "time::*," + "access::*," + "id::filesystem," + "metadata::emblems," + METADATA_TRUST; + +FileInfo::FileInfo() { + // FIXME: initialize numeric data members +} + +FileInfo::FileInfo(const GFileInfoPtr& inf, const FilePath& filePath, const FilePath& parentDirPath) { + setFromGFileInfo(inf, filePath, parentDirPath); +} + +FileInfo::~FileInfo() { +} + +void FileInfo::setFromGFileInfo(const GObjectPtr& inf, const FilePath& filePath, const FilePath& parentDirPath) { + inf_ = inf; + filePath_ = filePath; + if (filePath_ && filePath_.hasParent()) { + dirPath_ = filePath_.parent(); + } + else { + dirPath_ = parentDirPath; + } + const char* tmp, *uri; + GIcon* gicon; + GFileType type; + + if (const char * name = g_file_info_get_name(inf.get())) + name_ = name; + + dispName_ = g_file_info_get_display_name(inf.get()); + + size_ = g_file_info_get_size(inf.get()); + + tmp = g_file_info_get_content_type(inf.get()); + if(tmp) { + mimeType_ = MimeType::fromName(tmp); + } + + mode_ = g_file_info_get_attribute_uint32(inf.get(), G_FILE_ATTRIBUTE_UNIX_MODE); + + uid_ = gid_ = -1; + if(g_file_info_has_attribute(inf.get(), G_FILE_ATTRIBUTE_UNIX_UID)) { + uid_ = g_file_info_get_attribute_uint32(inf.get(), G_FILE_ATTRIBUTE_UNIX_UID); + } + if(g_file_info_has_attribute(inf.get(), G_FILE_ATTRIBUTE_UNIX_GID)) { + gid_ = g_file_info_get_attribute_uint32(inf.get(), G_FILE_ATTRIBUTE_UNIX_GID); + } + + type = g_file_info_get_file_type(inf.get()); + if(0 == mode_) { /* if UNIX file mode is not available, compose a fake one. */ + switch(type) { + case G_FILE_TYPE_REGULAR: + mode_ |= S_IFREG; + break; + case G_FILE_TYPE_DIRECTORY: + mode_ |= S_IFDIR; + break; + case G_FILE_TYPE_SYMBOLIC_LINK: + mode_ |= S_IFLNK; + break; + case G_FILE_TYPE_SHORTCUT: + break; + case G_FILE_TYPE_MOUNTABLE: + break; + case G_FILE_TYPE_SPECIAL: + if(mode_) { + break; + } + /* if it's a special file but it doesn't have UNIX mode, compose a fake one. */ + if(strcmp(tmp, "inode/chardevice") == 0) { + mode_ |= S_IFCHR; + } + else if(strcmp(tmp, "inode/blockdevice") == 0) { + mode_ |= S_IFBLK; + } + else if(strcmp(tmp, "inode/fifo") == 0) { + mode_ |= S_IFIFO; + } +#ifdef S_IFSOCK + else if(strcmp(tmp, "inode/socket") == 0) { + mode_ |= S_IFSOCK; + } +#endif + break; + case G_FILE_TYPE_UNKNOWN: + ; + } + } + + if(g_file_info_has_attribute(inf.get(), G_FILE_ATTRIBUTE_ACCESS_CAN_READ)) { + isAccessible_ = g_file_info_get_attribute_boolean(inf.get(), G_FILE_ATTRIBUTE_ACCESS_CAN_READ); + } + else + /* assume it's accessible */ + { + isAccessible_ = true; + } + + if(g_file_info_has_attribute(inf.get(), G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE)) { + isWritable_ = g_file_info_get_attribute_boolean(inf.get(), G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE); + } + else + /* assume it's writable */ + { + isWritable_ = true; + } + + if(g_file_info_has_attribute(inf.get(), G_FILE_ATTRIBUTE_ACCESS_CAN_DELETE)) { + isDeletable_ = g_file_info_get_attribute_boolean(inf.get(), G_FILE_ATTRIBUTE_ACCESS_CAN_DELETE); + } + else + /* assume it's deletable */ + { + isDeletable_ = true; + } + + isShortcut_ = (type == G_FILE_TYPE_SHORTCUT); + isMountable_ = (type == G_FILE_TYPE_MOUNTABLE); + + /* special handling for symlinks */ + if(g_file_info_get_is_symlink(inf.get())) { + mode_ &= ~S_IFMT; /* reset type */ + mode_ |= S_IFLNK; /* set type to symlink */ + goto _file_is_symlink; + } + + switch(type) { + case G_FILE_TYPE_SHORTCUT: + /* Falls through. */ + case G_FILE_TYPE_MOUNTABLE: + uri = g_file_info_get_attribute_string(inf.get(), G_FILE_ATTRIBUTE_STANDARD_TARGET_URI); + if(uri) { + if(g_str_has_prefix(uri, "file:///")) { + auto filename = CStrPtr{g_filename_from_uri(uri, nullptr, nullptr)}; + target_ = filename.get(); + } + else { + target_ = uri; + } + if(!mimeType_) { + mimeType_ = MimeType::guessFromFileName(target_.c_str()); + } + } + + /* if the mime-type is not determined or is unknown */ + if(G_UNLIKELY(!mimeType_ || mimeType_->isUnknownType())) { + /* FIXME: is this appropriate? */ + if(type == G_FILE_TYPE_SHORTCUT) { + mimeType_ = MimeType::inodeShortcut(); + } + else { + mimeType_ = MimeType::inodeMountPoint(); + } + } + break; + case G_FILE_TYPE_DIRECTORY: + if(!mimeType_) { + mimeType_ = MimeType::inodeDirectory(); + } + isReadOnly_ = false; /* default is R/W */ + if(g_file_info_has_attribute(inf.get(), G_FILE_ATTRIBUTE_FILESYSTEM_READONLY)) { + isReadOnly_ = g_file_info_get_attribute_boolean(inf.get(), G_FILE_ATTRIBUTE_FILESYSTEM_READONLY); + } + /* directories should be writable to be deleted by user */ + if(isReadOnly_ || !isWritable_) { + isDeletable_ = false; + } + break; + case G_FILE_TYPE_SYMBOLIC_LINK: +_file_is_symlink: + uri = g_file_info_get_symlink_target(inf.get()); + if(uri) { + if(g_str_has_prefix(uri, "file:///")) { + auto filename = CStrPtr{g_filename_from_uri(uri, nullptr, nullptr)}; + target_ = filename.get(); + } + else { + target_ = uri; + } + if(!mimeType_) { + mimeType_ = MimeType::guessFromFileName(target_.c_str()); + } + } + /* Falls through. */ + /* continue with absent mime type */ + default: /* G_FILE_TYPE_UNKNOWN G_FILE_TYPE_REGULAR G_FILE_TYPE_SPECIAL */ + if(G_UNLIKELY(!mimeType_)) { + if(!mimeType_) { + mimeType_ = MimeType::guessFromFileName(name_.c_str()); + } + } + } + + if(!mimeType_) { + mimeType_ = MimeType::fromName("application/octet-stream"); + } + + /* if there is a custom folder icon, use it */ + if(isNative() && type == G_FILE_TYPE_DIRECTORY) { + auto local_path = path().localPath(); + auto dot_dir = CStrPtr{g_build_filename(local_path.get(), ".directory", nullptr)}; + if(g_file_test(dot_dir.get(), G_FILE_TEST_IS_REGULAR)) { + GKeyFile* kf = g_key_file_new(); + if(g_key_file_load_from_file(kf, dot_dir.get(), G_KEY_FILE_NONE, nullptr)) { + CStrPtr icon_name{g_key_file_get_string(kf, "Desktop Entry", "Icon", nullptr)}; + if(icon_name) { + auto dot_icon = IconInfo::fromName(icon_name.get()); + if(dot_icon && dot_icon->isValid()) { + icon_ = dot_icon; + } + } + } + g_key_file_free(kf); + } + } + + if(!icon_) { + /* try file-specific icon first */ + gicon = g_file_info_get_icon(inf.get()); + if(gicon) { + icon_ = IconInfo::fromGIcon(gicon); + } + } + +#if 0 + /* set "locked" icon on unaccesible folder */ + else if(!accessible && type == G_FILE_TYPE_DIRECTORY) { + icon = g_object_ref(icon_locked_folder); + } + else { + icon = g_object_ref(fm_mime_type_get_icon(mime_type)); + } +#endif + + /* if the file has emblems, add them to the icon */ + auto emblem_names = g_file_info_get_attribute_stringv(inf.get(), "metadata::emblems"); + if(emblem_names) { + auto n_emblems = g_strv_length(emblem_names); + for(int i = n_emblems - 1; i >= 0; --i) { + emblems_.emplace_front(Fm::IconInfo::fromName(emblem_names[i])); + } + } + + tmp = g_file_info_get_attribute_string(inf.get(), G_FILE_ATTRIBUTE_ID_FILESYSTEM); + filesystemId_ = g_intern_string(tmp); + + mtime_ = g_file_info_get_attribute_uint64(inf.get(), G_FILE_ATTRIBUTE_TIME_MODIFIED); + atime_ = g_file_info_get_attribute_uint64(inf.get(), G_FILE_ATTRIBUTE_TIME_ACCESS); + ctime_ = g_file_info_get_attribute_uint64(inf.get(), G_FILE_ATTRIBUTE_TIME_CHANGED); + isHidden_ = g_file_info_get_is_hidden(inf.get()); + // g_file_info_get_is_backup() does not cover ".bak" and ".old". + // NOTE: Here, dispName_ is not modified for desktop entries yet. + isBackup_ = g_file_info_get_is_backup(inf.get()) + || dispName_.endsWith(QLatin1String(".bak")) + || dispName_.endsWith(QLatin1String(".old")); + isNameChangeable_ = true; /* GVFS tends to ignore this attribute */ + isIconChangeable_ = isHiddenChangeable_ = false; + if(g_file_info_has_attribute(inf.get(), G_FILE_ATTRIBUTE_ACCESS_CAN_RENAME)) { + isNameChangeable_ = g_file_info_get_attribute_boolean(inf.get(), G_FILE_ATTRIBUTE_ACCESS_CAN_RENAME); + } + + // special handling for desktop entry files (show the name and icon defined in the desktop entry instead) + if(isNative() && G_UNLIKELY(isDesktopEntry())) { + auto local_path = path().localPath(); + GKeyFile* kf = g_key_file_new(); + if(g_key_file_load_from_file(kf, local_path.get(), G_KEY_FILE_NONE, nullptr)) { + /* check if type is correct and supported */ + CStrPtr type{g_key_file_get_string(kf, "Desktop Entry", "Type", nullptr)}; + if(type) { + // Type == "Link" + if(strcmp(type.get(), G_KEY_FILE_DESKTOP_TYPE_LINK) == 0) { + CStrPtr uri{g_key_file_get_string(kf, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_URL, nullptr)}; + if(uri) { + isShortcut_ = true; + target_ = uri.get(); + } + } + } + CStrPtr icon_name{g_key_file_get_string(kf, "Desktop Entry", "Icon", nullptr)}; + if(icon_name) { + icon_ = IconInfo::fromName(icon_name.get()); + } + /* Use title of the desktop entry for display */ + CStrPtr displayName{g_key_file_get_locale_string(kf, "Desktop Entry", "Name", nullptr, nullptr)}; + if(displayName) { + dispName_ = displayName.get(); + } + /* handle 'Hidden' key to set hidden attribute */ + if(!isHidden_) { + isHidden_ = g_key_file_get_boolean(kf, "Desktop Entry", "Hidden", nullptr); + } + } + g_key_file_free(kf); + } + + if(!icon_ && mimeType_) + icon_ = mimeType_->icon(); + +#if 0 + GFile* _gf = nullptr; + GFileAttributeInfoList* list; + auto list = g_file_query_settable_attributes(gf, nullptr, nullptr); + if(G_LIKELY(list)) { + if(g_file_attribute_info_list_lookup(list, G_FILE_ATTRIBUTE_STANDARD_ICON)) { + icon_is_changeable = true; + } + if(g_file_attribute_info_list_lookup(list, G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN)) { + hidden_is_changeable = true; + } + g_file_attribute_info_list_unref(list); + } + if(G_UNLIKELY(_gf)) { + g_object_unref(_gf); + } +#endif +} + +void FileInfo::bindCutFiles(const std::shared_ptr& cutFilesHashSet) { + cutFilesHashSet_ = cutFilesHashSet; +} + +bool FileInfo::canThumbnail() const { + /* We cannot use S_ISREG here as this exclude all symlinks */ + if(size_ == 0 || /* don't generate thumbnails for empty files */ + !(mode_ & S_IFREG) || + isDesktopEntry() || + isUnknownType()) { + return false; + } + return true; +} + +/* full path of the file is required by this function */ +bool FileInfo::isExecutableType() const { + if(isDesktopEntry()) { + /* treat desktop entries as executables if + they are native and have read permission */ + if(isNative() && (mode_ & (S_IRUSR|S_IRGRP|S_IROTH))) { + if(isShortcut() && !target_.empty()) { + /* handle shortcuts from desktop to menu entries: + first check for entries in /usr/share/applications and such + which may be considered as a safe desktop entry path + then check if that is a shortcut to a native file + otherwise it is a link to a file under menu:// */ + if (!g_str_has_prefix(target_.c_str(), "/usr/share/")) { + auto target = FilePath::fromPathStr(target_.c_str()); + bool is_native = target.isNative(); + if (is_native) { + return true; + } + } + } + else { + return true; + } + } + return false; + } + else if(isText()) { /* g_content_type_can_be_executable reports text files as executables too */ + /* We don't execute remote files nor files in trash */ + if(isNative() && (mode_ & (S_IXOTH | S_IXGRP | S_IXUSR))) { + /* it has executable bits so lets check shell-bang */ + auto pathStr = path().toString(); + int fd = open(pathStr.get(), O_RDONLY); + if(fd >= 0) { + char buf[2]; + ssize_t rdlen = read(fd, &buf, 2); + close(fd); + if(rdlen == 2 && buf[0] == '#' && buf[1] == '!') { + return true; + } + } + } + return false; + } + return mimeType_->canBeExecutable(); +} + +bool FileInfo::isTrustable() const { + if(isExecutableType()) { + /* to avoid GIO assertion warning: */ + if(g_file_info_get_attribute_type(inf_.get(), METADATA_TRUST) == G_FILE_ATTRIBUTE_TYPE_STRING) { + if(const auto data = g_file_info_get_attribute_string(inf_.get(), METADATA_TRUST)) { + return (strcmp(data, "true") == 0); + } + } + } + return false; +} + +void FileInfo::setTrustable(bool trust) const { + if(!isExecutableType()) { + return; // METADATA_TRUST is only for executables + } + GObjectPtr info {g_file_info_new()}; // used to set only this attribute + if(trust) { + g_file_info_set_attribute_string(info.get(), METADATA_TRUST, "true"); + g_file_info_set_attribute_string(inf_.get(), METADATA_TRUST, "true"); + } + else { + g_file_info_set_attribute(info.get(), METADATA_TRUST, G_FILE_ATTRIBUTE_TYPE_INVALID, nullptr); + g_file_info_set_attribute(inf_.get(), METADATA_TRUST, G_FILE_ATTRIBUTE_TYPE_INVALID, nullptr); + } + g_file_set_attributes_from_info(path().gfile().get(), + info.get(), + G_FILE_QUERY_INFO_NONE, + nullptr, nullptr); +} + + +bool FileInfoList::isSameType() const { + if(!empty()) { + auto& item = front(); + for(auto it = cbegin() + 1; it != cend(); ++it) { + auto& item2 = *it; + if(item->mimeType() != item2->mimeType()) { + return false; + } + } + } + return true; +} + +bool FileInfoList::isSameFilesystem() const { + if(!empty()) { + auto& item = front(); + for(auto it = cbegin() + 1; it != cend(); ++it) { + auto& item2 = *it; + if(item->filesystemId() != item2->filesystemId()) { + return false; + } + } + } + return true; +} + + + +} // namespace Fm diff --git a/src/core/fileinfo.h b/src/core/fileinfo.h new file mode 100644 index 0000000..2a2d134 --- /dev/null +++ b/src/core/fileinfo.h @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2016 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef __LIBFM_QT_FM2_FILE_INFO_H__ +#define __LIBFM_QT_FM2_FILE_INFO_H__ + +#include +#include +#include "../libfmqtglobals.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "gioptrs.h" +#include "filepath.h" +#include "iconinfo.h" +#include "mimetype.h" + + +namespace Fm { + +class FileInfoList; +typedef std::set HashSet; + +class LIBFM_QT_API FileInfo { +public: + + explicit FileInfo(); + + explicit FileInfo(const GFileInfoPtr& inf, const FilePath& filePath, const FilePath& parentDirPath = FilePath()); + + virtual ~FileInfo(); + + bool canSetHidden() const { + return isHiddenChangeable_; + } + + bool canSetIcon() const { + return isIconChangeable_; + } + + bool canSetName() const { + return isNameChangeable_; + } + + bool canThumbnail() const; + + gid_t gid() const { + return gid_; + } + + uid_t uid() const { + return uid_; + } + + const char* filesystemId() const { + return filesystemId_; + } + + const std::shared_ptr& icon() const { + return icon_; + } + + const std::shared_ptr& mimeType() const { + return mimeType_; + } + + quint64 ctime() const { + return ctime_; + } + + + quint64 atime() const { + return atime_; + } + + quint64 mtime() const { + return mtime_; + } + + const std::string& target() const { + return target_; + } + + bool isWritableDirectory() const { + return (!isReadOnly_ && isDir()); + } + + bool isAccessible() const { + return isAccessible_; + } + + bool isWritable() const { + return isWritable_; + } + + bool isDeletable() const { + return isDeletable_; + } + + bool isExecutableType() const; + + bool isBackup() const { + return isBackup_; + } + + bool isHidden() const { + // FIXME: we might treat backup files as hidden + return isHidden_; + } + + bool isUnknownType() const { + return mimeType_->isUnknownType(); + } + + bool isDesktopEntry() const { + return mimeType_->isDesktopEntry(); + } + + bool isText() const { + return mimeType_->isText(); + } + + bool isImage() const { + return mimeType_->isImage(); + } + + bool isMountable() const { + return isMountable_; + } + + bool isShortcut() const { + return isShortcut_; + } + + bool isSymlink() const { + return S_ISLNK(mode_) ? true : false; + } + + bool isDir() const { + return S_ISDIR(mode_) || mimeType_->isDir(); + } + + bool isNative() const { + return dirPath_ ? dirPath_.isNative() : path().isNative(); + } + + bool isCut() const { + return !cutFilesHashSet_.expired(); + } + + mode_t mode() const { + return mode_; + } + + uint64_t realSize() const { + return blksize_ *blocks_; + } + + uint64_t size() const { + return size_; + } + + const std::string& name() const { + return name_; + } + + const QString& displayName() const { + return dispName_; + } + + QString description() const { + return QString::fromUtf8(mimeType_ ? mimeType_->desc() : ""); + } + + FilePath path() const { + return filePath_ ? filePath_ : dirPath_ ? dirPath_.child(name_.c_str()) : FilePath::fromPathStr(name_.c_str()); + } + + const FilePath& dirPath() const { + return dirPath_; + } + + void setFromGFileInfo(const GFileInfoPtr& inf, const FilePath& filePath, const FilePath& parentDirPath); + + void bindCutFiles(const std::shared_ptr& cutFilesHashSet); + + const std::forward_list>& emblems() const { + return emblems_; + } + + bool isTrustable() const; + + void setTrustable(bool trust) const; + +private: + GObjectPtr inf_; + std::string name_; + QString dispName_; + + FilePath filePath_; + FilePath dirPath_; + + mode_t mode_; + const char* filesystemId_; + uid_t uid_; + gid_t gid_; + uint64_t size_; + quint64 mtime_; + quint64 atime_; + quint64 ctime_; + + uint64_t blksize_; + uint64_t blocks_; + + std::shared_ptr mimeType_; + std::shared_ptr icon_; + std::forward_list> emblems_; + + std::string target_; /* target of shortcut or mountable. */ + + bool isShortcut_ : 1; /* TRUE if file is shortcut type */ + bool isMountable_ : 1; /* TRUE if file is mountable type */ + bool isAccessible_ : 1; /* TRUE if can be read by user */ + bool isWritable_ : 1; /* TRUE if can be written to by user */ + bool isDeletable_ : 1; /* TRUE if can be deleted by user */ + bool isHidden_ : 1; /* TRUE if file is hidden */ + bool isBackup_ : 1; /* TRUE if file is backup */ + bool isNameChangeable_ : 1; /* TRUE if name can be changed */ + bool isIconChangeable_ : 1; /* TRUE if icon can be changed */ + bool isHiddenChangeable_ : 1; /* TRUE if hidden can be changed */ + bool isReadOnly_ : 1; /* TRUE if host FS is R/O */ + + std::weak_ptr cutFilesHashSet_; + // std::vector> extraData_; +}; + + +class LIBFM_QT_API FileInfoList: public std::vector> { +public: + + bool isSameType() const; + + bool isSameFilesystem() const; + + FilePathList paths() const { + FilePathList ret; + for(auto& file: *this) { + ret.push_back(file->path()); + } + return ret; + } +}; + +// smart pointer to FileInfo objects (once created, FileInfo objects should stay immutable so const is needed here) +typedef std::shared_ptr FileInfoPtr; + +typedef std::pair FileInfoPair; + +} + +Q_DECLARE_METATYPE(std::shared_ptr) + + +#endif // __LIBFM_QT_FM2_FILE_INFO_H__ diff --git a/src/core/fileinfo_p.h b/src/core/fileinfo_p.h new file mode 100644 index 0000000..f7a51a6 --- /dev/null +++ b/src/core/fileinfo_p.h @@ -0,0 +1,10 @@ +#ifndef FILEINFO_P_H +#define FILEINFO_P_H + +namespace Fm { + + extern const char defaultGFileInfoQueryAttribs[]; + +} // namespace Fm + +#endif // FILEINFO_P_H diff --git a/src/core/fileinfojob.cpp b/src/core/fileinfojob.cpp new file mode 100644 index 0000000..7742011 --- /dev/null +++ b/src/core/fileinfojob.cpp @@ -0,0 +1,50 @@ +#include "fileinfojob.h" +#include "fileinfo_p.h" + +namespace Fm { + +FileInfoJob::FileInfoJob(FilePathList paths, const std::shared_ptr& cutFilesHashSet): + Job(), + paths_{std::move(paths)}, + cutFilesHashSet_{cutFilesHashSet} { +} + +void FileInfoJob::exec() { + for(const auto& path: paths_) { + if(isCancelled()) { + break; + } + currentPath_ = path; + + bool retry; + do { + retry = false; + GErrorPtr err; + GFileInfoPtr inf{ + g_file_query_info(path.gfile().get(), defaultGFileInfoQueryAttribs, + G_FILE_QUERY_INFO_NONE, cancellable().get(), &err), + false + }; + if(inf) { + auto fileInfoPtr = std::make_shared(inf, path); + + // FIXME: this is not elegant + if(cutFilesHashSet_ + && cutFilesHashSet_->count(path.hash())) { + fileInfoPtr->bindCutFiles(cutFilesHashSet_); + } + + results_.push_back(fileInfoPtr); + Q_EMIT gotInfo(path, results_.back()); + } + else { + auto act = emitError(err); + if(act == Job::ErrorAction::RETRY) { + retry = true; + } + } + } while(retry && !isCancelled()); + } +} + +} // namespace Fm diff --git a/src/core/fileinfojob.h b/src/core/fileinfojob.h new file mode 100644 index 0000000..374a7f1 --- /dev/null +++ b/src/core/fileinfojob.h @@ -0,0 +1,45 @@ +#ifndef FM2_FILEINFOJOB_H +#define FM2_FILEINFOJOB_H + +#include "../libfmqtglobals.h" +#include "job.h" +#include "filepath.h" +#include "fileinfo.h" + +namespace Fm { + + +class LIBFM_QT_API FileInfoJob : public Job { + Q_OBJECT +public: + + explicit FileInfoJob(FilePathList paths, const std::shared_ptr& cutFilesHashSet = nullptr); + + const FilePathList& paths() const { + return paths_; + } + + const FileInfoList& files() const { + return results_; + } + + const FilePath& currentPath() const { + return currentPath_; + } + +Q_SIGNALS: + void gotInfo(const FilePath& path, std::shared_ptr& info); + +protected: + void exec() override; + +private: + FilePathList paths_; + FileInfoList results_; + const std::shared_ptr cutFilesHashSet_; + FilePath currentPath_; +}; + +} // namespace Fm + +#endif // FM2_FILEINFOJOB_H diff --git a/src/core/filelinkjob.cpp b/src/core/filelinkjob.cpp new file mode 100644 index 0000000..253768e --- /dev/null +++ b/src/core/filelinkjob.cpp @@ -0,0 +1,9 @@ +#include "filelinkjob.h" + +namespace Fm { + +FileLinkJob::FileLinkJob() { + +} + +} // namespace Fm diff --git a/src/core/filelinkjob.h b/src/core/filelinkjob.h new file mode 100644 index 0000000..965d089 --- /dev/null +++ b/src/core/filelinkjob.h @@ -0,0 +1,16 @@ +#ifndef FM2_FILELINKJOB_H +#define FM2_FILELINKJOB_H + +#include "../libfmqtglobals.h" +#include "fileoperationjob.h" + +namespace Fm { + +class LIBFM_QT_API FileLinkJob : public Fm::FileOperationJob { +public: + explicit FileLinkJob(); +}; + +} // namespace Fm + +#endif // FM2_FILELINKJOB_H diff --git a/src/core/filemonitor.cpp b/src/core/filemonitor.cpp new file mode 100644 index 0000000..68c6b6b --- /dev/null +++ b/src/core/filemonitor.cpp @@ -0,0 +1,9 @@ +#include "filemonitor.h" + +namespace Fm { + +FileMonitor::FileMonitor() { + +} + +} // namespace Fm diff --git a/src/core/filemonitor.h b/src/core/filemonitor.h new file mode 100644 index 0000000..9a1485d --- /dev/null +++ b/src/core/filemonitor.h @@ -0,0 +1,26 @@ +#ifndef FM2_FILEMONITOR_H +#define FM2_FILEMONITOR_H + +#include "../libfmqtglobals.h" +#include +#include "gioptrs.h" +#include "filepath.h" + +namespace Fm { + +class LIBFM_QT_API FileMonitor: public QObject { + Q_OBJECT +public: + + explicit FileMonitor(); + +Q_SIGNALS: + + +private: + GFileMonitorPtr monitor_; +}; + +} // namespace Fm + +#endif // FM2_FILEMONITOR_H diff --git a/src/core/fileoperationjob.cpp b/src/core/fileoperationjob.cpp new file mode 100644 index 0000000..c2fbb26 --- /dev/null +++ b/src/core/fileoperationjob.cpp @@ -0,0 +1,102 @@ +#include "fileoperationjob.h" + +namespace Fm { + +FileOperationJob::FileOperationJob(): + hasTotalAmount_{false}, + calcProgressUsingSize_{true}, + totalSize_{0}, + totalCount_{0}, + finishedSize_{0}, + finishedCount_{0}, + currentFileSize_{0}, + currentFileFinished_{0} { +} + +bool FileOperationJob::totalAmount(uint64_t& fileSize, uint64_t& fileCount) const { + std::lock_guard lock{mutex_}; + if(hasTotalAmount_) { + fileSize = totalSize_; + fileCount = totalCount_; + } + return hasTotalAmount_; +} + +bool FileOperationJob::currentFileProgress(FilePath& path, uint64_t& totalSize, uint64_t& finishedSize) const { + std::lock_guard lock{mutex_}; + if(currentFile_.isValid()) { + path = currentFile_; + totalSize = currentFileSize_; + finishedSize = currentFileFinished_; + } + return currentFile_.isValid(); +} + +double FileOperationJob::progress() const { + std::lock_guard lock{mutex_}; + double finishedRatio; + if(calcProgressUsingSize_) { + finishedRatio = totalSize_ > 0 ? double(finishedSize_ + currentFileFinished_) / totalSize_ : 0.0; + } + else { + finishedRatio = totalCount_ > 0 ? double(finishedCount_) / totalCount_ : 0.0; + } + + if(finishedRatio > 1.0) { + finishedRatio = 1.0; + } + return finishedRatio; +} + +FileOperationJob::FileExistsAction FileOperationJob::askRename(const FileInfo &src, const FileInfo &dest, FilePath &newDest) { + FileExistsAction action = SKIP; + Q_EMIT fileExists(src, dest, action, newDest); + return action; +} + +bool FileOperationJob::finishedAmount(uint64_t& finishedSize, uint64_t& finishedCount) const { + std::lock_guard lock{mutex_}; + if(hasTotalAmount_) { + finishedSize = finishedSize_; + finishedCount = finishedCount_; + } + return hasTotalAmount_; +} + +void FileOperationJob::setTotalAmount(uint64_t fileSize, uint64_t fileCount) { + std::lock_guard lock{mutex_}; + hasTotalAmount_ = true; + totalSize_ = fileSize; + totalCount_ = fileCount; +} + +void FileOperationJob::setFinishedAmount(uint64_t finishedSize, uint64_t finishedCount) { + std::lock_guard lock{mutex_}; + finishedSize_ = finishedSize; + finishedCount_ = finishedCount; +} + +void FileOperationJob::addFinishedAmount(uint64_t finishedSize, uint64_t finishedCount) { + std::lock_guard lock{mutex_}; + finishedSize_ += finishedSize; + finishedCount_ += finishedCount; +} + +FilePath FileOperationJob::currentFile() const { + std::lock_guard lock{mutex_}; + auto ret = currentFile_; + return ret; +} + +void FileOperationJob::setCurrentFile(const FilePath& path) { + std::lock_guard lock{mutex_}; + currentFile_ = path; +} + +void FileOperationJob::setCurrentFileProgress(uint64_t totalSize, uint64_t finishedSize) { + std::lock_guard lock{mutex_}; + currentFileSize_ = totalSize; + currentFileFinished_ = finishedSize; +} + +} // namespace Fm diff --git a/src/core/fileoperationjob.h b/src/core/fileoperationjob.h new file mode 100644 index 0000000..f491a4b --- /dev/null +++ b/src/core/fileoperationjob.h @@ -0,0 +1,96 @@ +#ifndef FM2_FILEOPERATIONJOB_H +#define FM2_FILEOPERATIONJOB_H + +#include "../libfmqtglobals.h" +#include "job.h" +#include +#include +#include +#include "fileinfo.h" +#include "filepath.h" + +namespace Fm { + +class LIBFM_QT_API FileOperationJob : public Fm::Job { + Q_OBJECT +public: + enum FileExistsAction { + CANCEL = 0, + OVERWRITE = 1<<0, + RENAME = 1<<1, + SKIP = 1<<2, + SKIP_ERROR = 1<<3 + }; + + explicit FileOperationJob(); + + // get total amount of work to do + bool totalAmount(std::uint64_t& fileSize, std::uint64_t& fileCount) const; + + // get currently finished job amount + bool finishedAmount(std::uint64_t& finishedSize, std::uint64_t& finishedCount) const; + + // get the current file + FilePath currentFile() const; + + // get progress of the current file + bool currentFileProgress(FilePath& path, std::uint64_t& totalSize, std::uint64_t& finishedSize) const; + + // is the job calculate progress based on file size or file counts + bool calcProgressUsingSize() const { + return calcProgressUsingSize_; + } + + // get currently finished amount (0.0 to 1.0) + virtual double progress() const; + +Q_SIGNALS: + + void preparedToRun(); + + // void currentFile(const char* file); + + // void progress(uint32_t percent); + + // to correctly handle the signal, connect with Qt::BlockingQueuedConnection so it's a sync call. + void fileExists(const FileInfo& src, const FileInfo& dest, FileExistsAction& response, FilePath& newDest); + +protected: + + FileExistsAction askRename(const FileInfo& src, const FileInfo& dest, FilePath& newDest); + + void setTotalAmount(std::uint64_t fileSize, std::uint64_t fileCount); + + void setFinishedAmount(std::uint64_t finishedSize, std::uint64_t finishedCount); + + void addFinishedAmount(std::uint64_t finishedSize, std::uint64_t finishedCount); + + void setCurrentFile(const FilePath &path); + + void setCurrentFileProgress(uint64_t totalSize, uint64_t finishedSize); + + void setCalcProgressUsingSize(bool value) { + calcProgressUsingSize_ = value; + } + + std::mutex& mutex() { + return mutex_; + } + +private: + bool hasTotalAmount_; + bool calcProgressUsingSize_; + std::uint64_t totalSize_; + std::uint64_t totalCount_; + std::uint64_t finishedSize_; + std::uint64_t finishedCount_; + + FilePath currentFile_; + std::uint64_t currentFileSize_; + std::uint64_t currentFileFinished_; + mutable std::mutex mutex_; +}; + +} // namespace Fm + +#endif // FM2_FILEOPERATIONJOB_H diff --git a/src/core/filepath.cpp b/src/core/filepath.cpp new file mode 100644 index 0000000..36a7910 --- /dev/null +++ b/src/core/filepath.cpp @@ -0,0 +1,21 @@ +#include "filepath.h" +#include +#include +#include + +namespace Fm { + +FilePath FilePath::homeDir_; + +const FilePath &FilePath::homeDir() { + if(!homeDir_) { + const char* home = getenv("HOME"); + if(!home) { + home = g_get_home_dir(); + } + homeDir_ = FilePath::fromLocalPath(home); + } + return homeDir_; +} + +} // namespace Fm diff --git a/src/core/filepath.h b/src/core/filepath.h new file mode 100644 index 0000000..a0ce899 --- /dev/null +++ b/src/core/filepath.h @@ -0,0 +1,177 @@ +#ifndef FM2_FILEPATH_H +#define FM2_FILEPATH_H + +#include "../libfmqtglobals.h" +#include "gobjectptr.h" +#include "cstrptr.h" +#include +#include +#include + +namespace Fm { + +class LIBFM_QT_API FilePath { +public: + + explicit FilePath() { + } + + explicit FilePath(GFile* gfile, bool add_ref): gfile_{gfile, add_ref} { + } + + FilePath(const FilePath& other): FilePath{} { + *this = other; + } + + FilePath(FilePath&& other) noexcept: FilePath{} { + *this = other; + } + + static FilePath fromUri(const char* uri) { + return FilePath{g_file_new_for_uri(uri), false}; + } + + static FilePath fromLocalPath(const char* path) { + return FilePath{g_file_new_for_path(path), false}; + } + + static FilePath fromDisplayName(const char* path) { + return FilePath{g_file_parse_name(path), false}; + } + + static FilePath fromPathStr(const char* path_str) { + return FilePath{g_file_new_for_commandline_arg(path_str), false}; + } + + bool isValid() const { + return gfile_ != nullptr; + } + + unsigned int hash() const { + return g_file_hash(gfile_.get()); + } + + CStrPtr baseName() const { + return CStrPtr{g_file_get_basename(gfile_.get())}; + } + + CStrPtr localPath() const { + return CStrPtr{g_file_get_path(gfile_.get())}; + } + + CStrPtr uri() const { + return CStrPtr{g_file_get_uri(gfile_.get())}; + } + + CStrPtr toString() const { + if(isNative()) { + return localPath(); + } + return uri(); + } + + // a human readable UTF-8 display name for the path + CStrPtr displayName() const { + return CStrPtr{g_file_get_parse_name(gfile_.get())}; + } + + FilePath parent() const { + return FilePath{g_file_get_parent(gfile_.get()), false}; + } + + bool hasParent() const { + return g_file_has_parent(gfile_.get(), nullptr); + } + + bool isParentOf(const FilePath& other) const { + return g_file_has_parent(other.gfile_.get(), gfile_.get()); + } + + bool isPrefixOf(const FilePath& other) const { + return g_file_has_prefix(other.gfile_.get(), gfile_.get()); + } + + FilePath child(const char* name) const { + return FilePath{g_file_get_child(gfile_.get(), name), false}; + } + + CStrPtr relativePathStr(const FilePath& descendant) const { + return CStrPtr{g_file_get_relative_path(gfile_.get(), descendant.gfile_.get())}; + } + + FilePath relativePath(const char* relPath) const { + return FilePath{g_file_resolve_relative_path(gfile_.get(), relPath), false}; + } + + bool isNative() const { + return g_file_is_native(gfile_.get()); + } + + bool hasUriScheme(const char* scheme) const { + return g_file_has_uri_scheme(gfile_.get(), scheme); + } + + CStrPtr uriScheme() const { + return CStrPtr{g_file_get_uri_scheme(gfile_.get())}; + } + + const GObjectPtr& gfile() const { + return gfile_; + } + + FilePath& operator = (const FilePath& other) { + gfile_ = other.gfile_; + return *this; + } + + FilePath& operator = (const FilePath&& other) noexcept { + gfile_ = std::move(other.gfile_); + return *this; + } + + bool operator == (const FilePath& other) const { + return operator==(other.gfile_.get()); + } + + bool operator == (GFile* other_gfile) const { + if(gfile_ == other_gfile) { + return true; + } + if(gfile_ && other_gfile) { + return g_file_equal(gfile_.get(), other_gfile); + } + return false; + } + + bool operator != (const FilePath& other) const { + return !operator==(other); + } + + bool operator != (std::nullptr_t) const { + return gfile_ != nullptr; + } + + operator bool() const { + return gfile_ != nullptr; + } + + static const FilePath& homeDir(); + +private: + GObjectPtr gfile_; + static FilePath homeDir_; +}; + +struct FilePathHash { + std::size_t operator() (const FilePath& path) const { + return path.hash(); + } +}; + +typedef std::vector FilePathList; + +} // namespace Fm + +Q_DECLARE_METATYPE(Fm::FilePath) + +#endif // FM2_FILEPATH_H diff --git a/src/core/filesysteminfojob.cpp b/src/core/filesysteminfojob.cpp new file mode 100644 index 0000000..6c3aa69 --- /dev/null +++ b/src/core/filesysteminfojob.cpp @@ -0,0 +1,24 @@ +#include "filesysteminfojob.h" +#include "gobjectptr.h" + +namespace Fm { + +void FileSystemInfoJob::exec() { + GObjectPtr inf = GObjectPtr{ + g_file_query_filesystem_info( + path_.gfile().get(), + G_FILE_ATTRIBUTE_FILESYSTEM_SIZE"," + G_FILE_ATTRIBUTE_FILESYSTEM_FREE, + cancellable().get(), nullptr), + false + }; + if(!inf) + return; + if(g_file_info_has_attribute(inf.get(), G_FILE_ATTRIBUTE_FILESYSTEM_SIZE)) { + size_ = g_file_info_get_attribute_uint64(inf.get(), G_FILE_ATTRIBUTE_FILESYSTEM_SIZE); + freeSize_ = g_file_info_get_attribute_uint64(inf.get(), G_FILE_ATTRIBUTE_FILESYSTEM_FREE); + isAvailable_ = true; + } +} + +} // namespace Fm diff --git a/src/core/filesysteminfojob.h b/src/core/filesysteminfojob.h new file mode 100644 index 0000000..59c963a --- /dev/null +++ b/src/core/filesysteminfojob.h @@ -0,0 +1,45 @@ +#ifndef FM2_FILESYSTEMINFOJOB_H +#define FM2_FILESYSTEMINFOJOB_H + +#include "../libfmqtglobals.h" +#include "job.h" +#include "filepath.h" + +namespace Fm { + +class LIBFM_QT_API FileSystemInfoJob : public Job { + Q_OBJECT +public: + explicit FileSystemInfoJob(const FilePath& path): + path_{path}, + isAvailable_{false}, + size_{0}, + freeSize_{0} { + } + + bool isAvailable() const { + return isAvailable_; + } + + uint64_t size() const { + return size_; + } + + uint64_t freeSize() const { + return freeSize_; + } + +protected: + + void exec() override; + +private: + FilePath path_; + bool isAvailable_; + uint64_t size_; + uint64_t freeSize_; +}; + +} // namespace Fm + +#endif // FM2_FILESYSTEMINFOJOB_H diff --git a/src/core/filetransferjob.cpp b/src/core/filetransferjob.cpp new file mode 100644 index 0000000..4039c58 --- /dev/null +++ b/src/core/filetransferjob.cpp @@ -0,0 +1,652 @@ +#include "filetransferjob.h" +#include "totalsizejob.h" +#include "fileinfo_p.h" + +namespace Fm { + +FileTransferJob::FileTransferJob(FilePathList srcPaths, Mode mode): + FileOperationJob{}, + srcPaths_{std::move(srcPaths)}, + mode_{mode} { +} + +FileTransferJob::FileTransferJob(FilePathList srcPaths, FilePathList destPaths, Mode mode): + FileTransferJob{std::move(srcPaths), mode} { + destPaths_ = std::move(destPaths); +} + +FileTransferJob::FileTransferJob(FilePathList srcPaths, const FilePath& destDirPath, Mode mode): + FileTransferJob{std::move(srcPaths), mode} { + setDestDirPath(destDirPath); +} + +void FileTransferJob::setSrcPaths(FilePathList srcPaths) { + srcPaths_ = std::move(srcPaths); +} + +void FileTransferJob::setDestPaths(FilePathList destPaths) { + destPaths_ = std::move(destPaths); +} + +void FileTransferJob::setDestDirPath(const FilePath& destDirPath) { + destPaths_.clear(); + destPaths_.reserve(srcPaths_.size()); + for(const auto& srcPath: srcPaths_) { + FilePath destPath; + if(mode_ == Mode::LINK && !srcPath.isNative()) { + // special handling for URLs + auto fullBasename = srcPath.baseName(); + char* basename = fullBasename.get(); + char* dname = nullptr; + // if we drop URI query onto native filesystem, omit query part + if(!srcPath.isNative()) { + dname = strchr(basename, '?'); + } + // if basename consist only from query then use first part of it + if(dname == basename) { + basename++; + dname = strchr(basename, '&'); + } + + CStrPtr _basename; + if(dname) { + _basename = CStrPtr{g_strndup(basename, dname - basename)}; + dname = strrchr(_basename.get(), G_DIR_SEPARATOR); + g_debug("cutting '%s' to '%s'", basename, dname ? &dname[1] : _basename.get()); + if(dname) { + basename = &dname[1]; + } + else { + basename = _basename.get(); + } + } + destPath = destDirPath.child(basename); + } + else { + destPath = destDirPath.child(srcPath.baseName().get()); + } + destPaths_.emplace_back(std::move(destPath)); + } +} + +void FileTransferJob::gfileCopyProgressCallback(goffset current_num_bytes, goffset total_num_bytes, FileTransferJob* _this) { + _this->setCurrentFileProgress(total_num_bytes, current_num_bytes); +} + +bool FileTransferJob::moveFileSameFs(const FilePath& srcPath, const GFileInfoPtr& srcInfo, FilePath& destPath) { + int flags = G_FILE_COPY_ALL_METADATA | G_FILE_COPY_NOFOLLOW_SYMLINKS; + GErrorPtr err; + bool retry; + do { + retry = false; + err.reset(); + // do the file operation + if(!g_file_move(srcPath.gfile().get(), destPath.gfile().get(), GFileCopyFlags(flags), cancellable().get(), + nullptr, this, &err)) { + retry = handleError(err, srcPath, srcInfo, destPath, flags); + } + else { + return true; + } + } while(retry && !isCancelled()); + return false; +} + +bool FileTransferJob::copyRegularFile(const FilePath& srcPath, const GFileInfoPtr& srcInfo, FilePath& destPath) { + int flags = G_FILE_COPY_ALL_METADATA | G_FILE_COPY_NOFOLLOW_SYMLINKS; + GErrorPtr err; + bool retry; + do { + retry = false; + err.reset(); + + // reset progress of the current file (only for copy) + auto size = g_file_info_get_size(srcInfo.get()); + setCurrentFileProgress(size, 0); + + // do the file operation + if(!g_file_copy(srcPath.gfile().get(), destPath.gfile().get(), GFileCopyFlags(flags), cancellable().get(), + (GFileProgressCallback)&gfileCopyProgressCallback, this, &err)) { + retry = handleError(err, srcPath, srcInfo, destPath, flags); + } + else { + return true; + } + } while(retry && !isCancelled()); + return false; +} + +bool FileTransferJob::copySpecialFile(const FilePath& srcPath, const GFileInfoPtr& srcInfo, FilePath &destPath) { + bool ret = false; + // only handle FIFO for local files + if(srcPath.isNative() && destPath.isNative()) { + auto src_path = srcPath.localPath(); + struct stat src_st; + int r; + r = lstat(src_path.get(), &src_st); + if(r == 0) { + // Handle FIFO on native file systems. + if(S_ISFIFO(src_st.st_mode)) { + auto dest_path = destPath.localPath(); + if(mkfifo(dest_path.get(), src_st.st_mode) == 0) { + ret = true; + } + } + // FIXME: how about block device, char device, and socket? + } + } + if(!ret) { + GErrorPtr err; + g_set_error(&err, G_IO_ERROR, G_IO_ERROR_FAILED, + ("Cannot copy file '%s': not supported"), + g_file_info_get_display_name(srcInfo.get())); + emitError(err, ErrorSeverity::MODERATE); + } + return ret; +} + +bool FileTransferJob::copyDirContent(const FilePath& srcPath, GFileInfoPtr srcInfo, FilePath& destPath, bool skip) { + bool ret = false; + // copy dir content + GErrorPtr err; + auto enu = GFileEnumeratorPtr{ + g_file_enumerate_children(srcPath.gfile().get(), + defaultGFileInfoQueryAttribs, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err), + false}; + if(enu) { + int n_children = 0; + int n_copied = 0; + ret = true; + while(!isCancelled()) { + err.reset(); + GFileInfoPtr inf{g_file_enumerator_next_file(enu.get(), cancellable().get(), &err), false}; + if(inf) { + ++n_children; + const char* name = g_file_info_get_name(inf.get()); + FilePath childPath = srcPath.child(name); + bool child_ret = copyFile(childPath, inf, destPath, name, skip); + if(child_ret) { + ++n_copied; + } + else { + ret = false; + } + } + else { + if(err) { + // fail to read directory content + // NOTE: since we cannot read the source dir, we cannot calculate the progress correctly, either. + emitError(err, ErrorSeverity::MODERATE); + err.reset(); + /* ErrorAction::RETRY is not supported here */ + ret = false; + } + else { /* EOF is reached */ + /* all files are successfully copied. */ + if(isCancelled()) { + ret = false; + } + else { + /* some files are not copied */ + if(n_children != n_copied) { + /* if the copy actions are skipped deliberately, it's ok */ + if(!skip) { + ret = false; + } + } + /* else job->skip_dir_content is true */ + } + break; + } + } + } + g_file_enumerator_close(enu.get(), nullptr, &err); + } + else { + if(err) { + emitError(err, ErrorSeverity::MODERATE); + } + } + return ret; +} + +bool FileTransferJob::makeDir(const FilePath& srcPath, GFileInfoPtr srcInfo, FilePath& destPath) { + if(isCancelled()) { + return false; + } + + bool mkdir_done = false; + do { + GErrorPtr err; + mkdir_done = g_file_make_directory_with_parents(destPath.gfile().get(), cancellable().get(), &err); + if(!mkdir_done) { + if(err->domain == G_IO_ERROR && (err->code == G_IO_ERROR_EXISTS || + err->code == G_IO_ERROR_INVALID_FILENAME || + err->code == G_IO_ERROR_FILENAME_TOO_LONG)) { + GFileInfoPtr destInfo = GFileInfoPtr { + g_file_query_info(destPath.gfile().get(), + defaultGFileInfoQueryAttribs, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), nullptr), + false + }; + if(!destInfo) { + // FIXME: error handling + break; + } + + FilePath newDestPath; + FileExistsAction opt = askRename(FileInfo{srcInfo, srcPath}, FileInfo{destInfo, destPath}, newDestPath); + switch(opt) { + case FileOperationJob::RENAME: + destPath = std::move(newDestPath); + break; + case FileOperationJob::SKIP: + /* when a dir is skipped, we need to know its total size to calculate correct progress */ + mkdir_done = true; /* pretend that dir creation succeeded */ + break; + case FileOperationJob::OVERWRITE: + mkdir_done = true; /* pretend that dir creation succeeded */ + break; + case FileOperationJob::CANCEL: + cancel(); + return false; + case FileOperationJob::SKIP_ERROR: ; /* FIXME */ + } + } + else { + ErrorAction act = emitError(err, ErrorSeverity::MODERATE); + if(act != ErrorAction::RETRY) { + break; + } + } + } + } while(!mkdir_done && !isCancelled()); + + bool chmod_done = false; + if(mkdir_done && !isCancelled()) { + mode_t mode = g_file_info_get_attribute_uint32(srcInfo.get(), G_FILE_ATTRIBUTE_UNIX_MODE); + if(mode) { + mode |= (S_IRUSR | S_IWUSR); /* ensure we have rw permission to this file. */ + do { + GErrorPtr err; + // chmod the newly created dir properly + // if(!fm_job_is_cancelled(fmjob) && !job->skip_dir_content) + chmod_done = g_file_set_attribute_uint32(destPath.gfile().get(), + G_FILE_ATTRIBUTE_UNIX_MODE, + mode, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err); + if(!chmod_done) { + ErrorAction act = emitError(err, ErrorSeverity::MODERATE); + if(act != ErrorAction::RETRY) { + break; + } + /* FIXME: some filesystems may not support this. */ + } + } while(!chmod_done && !isCancelled()); + } + } + return mkdir_done && chmod_done; +} + +bool FileTransferJob::handleError(GErrorPtr &err, const FilePath &srcPath, const GFileInfoPtr &srcInfo, FilePath &destPath, int& flags) { + bool retry = false; + /* handle existing files or file name conflict */ + if(err.domain() == G_IO_ERROR && (err.code() == G_IO_ERROR_EXISTS || + err.code() == G_IO_ERROR_INVALID_FILENAME || + err.code() == G_IO_ERROR_FILENAME_TOO_LONG)) { + flags &= ~G_FILE_COPY_OVERWRITE; + + // get info of the existing file + GFileInfoPtr destInfo = GFileInfoPtr { + g_file_query_info(destPath.gfile().get(), + defaultGFileInfoQueryAttribs, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), nullptr), + false + }; + + // ask the user to rename or overwrite the existing file + if(!isCancelled() && destInfo) { + FilePath newDestPath; + FileExistsAction opt = askRename(FileInfo{srcInfo, srcPath}, + FileInfo{destInfo, destPath}, + newDestPath); + switch(opt) { + case FileOperationJob::RENAME: + // try a new file name + if(newDestPath.isValid()) { + destPath = std::move(newDestPath); + // FIXME: handle the error when newDestPath is invalid. + } + retry = true; + break; + case FileOperationJob::OVERWRITE: + // overwrite existing file + flags |= G_FILE_COPY_OVERWRITE; + retry = true; + err.reset(); + break; + case FileOperationJob::CANCEL: + // cancel the whole job. + cancel(); + break; + case FileOperationJob::SKIP: + // skip current file and don't copy it + case FileOperationJob::SKIP_ERROR: ; /* FIXME */ + retry = false; + break; + } + err.reset(); + } + } + + // show error message + if(!isCancelled() && err) { + ErrorAction act = emitError(err, ErrorSeverity::MODERATE); + err.reset(); + if(act == ErrorAction::RETRY) { + // the user wants retry the operation again + retry = true; + } + const bool is_no_space = (err.domain() == G_IO_ERROR && err.code() == G_IO_ERROR_NO_SPACE); + /* FIXME: ask to leave partial content? */ + if(is_no_space) { + // run out of disk space. delete the partial content we copied. + g_file_delete(destPath.gfile().get(), cancellable().get(), nullptr); + } + } + return retry; +} + +bool FileTransferJob::processPath(const FilePath& srcPath, const FilePath& destDirPath, const char* destFileName) { + GErrorPtr err; + GFileInfoPtr srcInfo = GFileInfoPtr { + g_file_query_info(srcPath.gfile().get(), + defaultGFileInfoQueryAttribs, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err), + false + }; + if(!srcInfo || isCancelled()) { + // FIXME: report error + return false; + } + + bool ret; + switch(mode_) { + case Mode::MOVE: + ret = moveFile(srcPath, srcInfo, destDirPath, destFileName); + break; + case Mode::COPY: { + bool deleteSrc = false; + ret = copyFile(srcPath, srcInfo, destDirPath, destFileName, deleteSrc); + break; + } + case Mode::LINK: + ret = linkFile(srcPath, srcInfo, destDirPath, destFileName); + break; + default: + ret = false; + break; + } + return ret; +} + +bool FileTransferJob::moveFile(const FilePath &srcPath, const GFileInfoPtr &srcInfo, const FilePath &destDirPath, const char *destFileName) { + setCurrentFile(srcPath); + + GErrorPtr err; + GFileInfoPtr destDirInfo = GFileInfoPtr { + g_file_query_info(destDirPath.gfile().get(), + "id::filesystem", + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err), + false + }; + + if(!destDirInfo || isCancelled()) { + // FIXME: report errors + return false; + } + + // If src and dest are on the same filesystem, do move. + // Exception: if src FS is trash:///, we always do move + // Otherwise, do copy & delete src files. + auto src_fs = g_file_info_get_attribute_string(srcInfo.get(), "id::filesystem"); + auto dest_fs = g_file_info_get_attribute_string(destDirInfo.get(), "id::filesystem"); + bool ret; + if(src_fs && dest_fs && (strcmp(src_fs, dest_fs) == 0 || g_str_has_prefix(src_fs, "trash"))) { + // src and dest are on the same filesystem + auto destPath = destDirPath.child(destFileName); + ret = moveFileSameFs(srcPath, srcInfo, destPath); + + // increase current progress + // FIXME: it's not appropriate to calculate the progress of move operations using file size + // since the time required to move a file is not related to it's file size. + auto size = g_file_info_get_size(srcInfo.get()); + addFinishedAmount(size, 1); + } + else { + // cross device/filesystem move: copy & delete + ret = copyFile(srcPath, srcInfo, destDirPath, destFileName); + // NOTE: do not need to increase progress here since it's done by copyPath(). + } + return ret; +} + +bool FileTransferJob::copyFile(const FilePath& srcPath, const GFileInfoPtr& srcInfo, const FilePath& destDirPath, const char* destFileName, bool skip) { + setCurrentFile(srcPath); + + auto size = g_file_info_get_size(srcInfo.get()); + bool success = false; + setCurrentFileProgress(size, 0); + + auto destPath = destDirPath.child(destFileName); + auto file_type = g_file_info_get_file_type(srcInfo.get()); + if(!skip) { + switch(file_type) { + case G_FILE_TYPE_DIRECTORY: + // prevent a dir from being copied into itself + if(srcPath.isPrefixOf(destPath)) { + GErrorPtr err = GErrorPtr{ + G_IO_ERROR, + G_IO_ERROR_NOT_SUPPORTED, + tr("Cannot copy a directory into itself!") + }; + emitError(err, ErrorSeverity::MODERATE); + } + else { + success = makeDir(srcPath, srcInfo, destPath); + } + break; + case G_FILE_TYPE_SPECIAL: + success = copySpecialFile(srcPath, srcInfo, destPath); + break; + default: + success = copyRegularFile(srcPath, srcInfo, destPath); + break; + } + } + else { // skip the file + success = true; + } + + if(success) { + // finish copying the file + addFinishedAmount(size, 1); + setCurrentFileProgress(0, 0); + + // recursively copy dir content + if(file_type == G_FILE_TYPE_DIRECTORY) { + success = copyDirContent(srcPath, srcInfo, destPath, skip); + } + + if(!skip && success && mode_ == Mode::MOVE) { + // delete the source file for cross-filesystem move + GErrorPtr err; + if(g_file_delete(srcPath.gfile().get(), cancellable().get(), &err)) { + // FIXME: add some file size to represent the amount of work need to delete a file + addFinishedAmount(1, 1); + } + else { + success = false; + } + } + } + return success; +} + +bool FileTransferJob::linkFile(const FilePath &srcPath, const GFileInfoPtr &srcInfo, const FilePath &destDirPath, const char *destFileName) { + setCurrentFile(srcPath); + + bool ret = false; + // cannot create links on non-native filesystems + if(!destDirPath.isNative()) { + auto msg = tr("Cannot create a link on non-native filesystem"); + GErrorPtr err{g_error_new_literal(G_IO_ERROR, G_IO_ERROR_FAILED, msg.toUtf8().constData())}; + emitError(err, ErrorSeverity::CRITICAL); + return false; + } + + if(srcPath.isNative()) { + // create symlinks for native files + auto destPath = destDirPath.child(destFileName); + ret = createSymlink(srcPath, srcInfo, destPath); + } + else { + // ensure that the dest file has *.desktop filename extension. + CStrPtr desktopEntryFileName{g_strconcat(destFileName, ".desktop", nullptr)}; + auto destPath = destDirPath.child(desktopEntryFileName.get()); + ret = createShortcut(srcPath, srcInfo, destPath); + } + + // update progress + // FIXME: increase the progress by 1 byte is not appropriate + addFinishedAmount(1, 1); + return ret; +} + +bool FileTransferJob::createSymlink(const FilePath &srcPath, const GFileInfoPtr &srcInfo, FilePath &destPath) { + bool ret = false; + auto src = srcPath.localPath(); + int flags = 0; + GErrorPtr err; + bool retry; + do { + retry = false; + if(flags & G_FILE_COPY_OVERWRITE) { // overwrite existing file + // creating symlink cannot overwrite existing files directly, so we delete the existing file first. + g_file_delete(destPath.gfile().get(), cancellable().get(), nullptr); + } + if(!g_file_make_symbolic_link(destPath.gfile().get(), src.get(), cancellable().get(), &err)) { + retry = handleError(err, srcPath, srcInfo, destPath, flags); + } + else { + ret = true; + break; + } + } while(!isCancelled() && retry); + return ret; +} + +bool FileTransferJob::createShortcut(const FilePath &srcPath, const GFileInfoPtr &srcInfo, FilePath &destPath) { + bool ret = false; + const char* iconName = nullptr; + GIcon* icon = g_file_info_get_icon(srcInfo.get()); + if(icon && G_IS_THEMED_ICON(icon)) { + auto iconNames = g_themed_icon_get_names(G_THEMED_ICON(icon)); + if(iconNames && iconNames[0]) { + iconName = iconNames[0]; + } + } + + CStrPtr srcPathUri; + auto uri = g_file_info_get_attribute_string(srcInfo.get(), G_FILE_ATTRIBUTE_STANDARD_TARGET_URI); + if(!uri) { + srcPathUri = srcPath.uri(); + uri = srcPathUri.get(); + } + + CStrPtr srcPathDispName; + auto name = g_file_info_get_display_name(srcInfo.get()); + if(!name) { + srcPathDispName = srcPath.displayName(); + name = srcPathDispName.get(); + } + + GKeyFile* kf = g_key_file_new(); + if(kf) { + g_key_file_set_string(kf, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_TYPE, "Link"); + g_key_file_set_string(kf, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_NAME, name); + if(iconName) { + g_key_file_set_string(kf, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_ICON, iconName); + } + if(uri) { + g_key_file_set_string(kf, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_URL, uri); + } + gsize contentLen; + CStrPtr content{g_key_file_to_data(kf, &contentLen, nullptr)}; + g_key_file_free(kf); + + int flags = 0; + if(content) { + bool retry; + GErrorPtr err; + do { + retry = false; + if(flags & G_FILE_COPY_OVERWRITE) { // overwrite existing file + g_file_delete(destPath.gfile().get(), cancellable().get(), nullptr); + } + + if(!g_file_replace_contents(destPath.gfile().get(), content.get(), contentLen, nullptr, false, G_FILE_CREATE_NONE, nullptr, cancellable().get(), &err)) { + retry = handleError(err, srcPath, srcInfo, destPath, flags); + err.reset(); + } + else { + ret = true; + } + } while(!isCancelled() && retry); + ret = true; + } + } + return ret; +} + + +void FileTransferJob::exec() { + // calculate the total size of files to copy + auto totalSizeFlags = (mode_ == Mode::COPY ? TotalSizeJob::DEFAULT : TotalSizeJob::PREPARE_MOVE); + TotalSizeJob totalSizeJob{srcPaths_, totalSizeFlags}; + connect(&totalSizeJob, &TotalSizeJob::error, this, &FileTransferJob::error); + connect(this, &FileTransferJob::cancelled, &totalSizeJob, &TotalSizeJob::cancel); + totalSizeJob.run(); + if(isCancelled()) { + return; + } + + // ready to start + setTotalAmount(totalSizeJob.totalSize(), totalSizeJob.fileCount()); + Q_EMIT preparedToRun(); + + if(srcPaths_.size() != destPaths_.size()) { + qWarning("error: srcPaths.size() != destPaths.size() when copying files"); + return; + } + + // copy the files + for(size_t i = 0; i < srcPaths_.size(); ++i) { + if(isCancelled()) { + break; + } + const auto& srcPath = srcPaths_[i]; + const auto& destPath = destPaths_[i]; + auto destDirPath = destPath.parent(); + processPath(srcPath, destDirPath, destPath.baseName().get()); + } +} + + +} // namespace Fm diff --git a/src/core/filetransferjob.h b/src/core/filetransferjob.h new file mode 100644 index 0000000..7c3d8a0 --- /dev/null +++ b/src/core/filetransferjob.h @@ -0,0 +1,58 @@ +#ifndef FM2_COPYJOB_H +#define FM2_COPYJOB_H + +#include "../libfmqtglobals.h" +#include "fileoperationjob.h" +#include "gioptrs.h" + +namespace Fm { + +class LIBFM_QT_API FileTransferJob : public Fm::FileOperationJob { + Q_OBJECT +public: + + enum class Mode { + COPY, + MOVE, + LINK + }; + + explicit FileTransferJob(FilePathList srcPaths, Mode mode = Mode::COPY); + explicit FileTransferJob(FilePathList srcPaths, FilePathList destPaths, Mode mode = Mode::COPY); + explicit FileTransferJob(FilePathList srcPaths, const FilePath &destDirPath, Mode mode = Mode::COPY); + + void setSrcPaths(FilePathList srcPaths); + void setDestPaths(FilePathList destPaths); + void setDestDirPath(const FilePath &destDirPath); + +protected: + void exec() override; + +private: + bool processPath(const FilePath& srcPath, const FilePath& destPath, const char *destFileName); + bool moveFile(const FilePath &srcPath, const GFileInfoPtr &srcInfo, const FilePath &destDirPath, const char *destFileName); + bool copyFile(const FilePath &srcPath, const GFileInfoPtr &srcInfo, const FilePath &destDirPath, const char *destFileName, bool skip = false); + bool linkFile(const FilePath &srcPath, const GFileInfoPtr &srcInfo, const FilePath &destDirPath, const char *destFileName); + + bool moveFileSameFs(const FilePath &srcPath, const GFileInfoPtr& srcInfo, FilePath &destPath); + bool copyRegularFile(const FilePath &srcPath, const GFileInfoPtr& srcInfo, FilePath &destPath); + bool copySpecialFile(const FilePath &srcPath, const GFileInfoPtr& srcInfo, FilePath& destPath); + bool copyDirContent(const FilePath &srcPath, GFileInfoPtr srcInfo, FilePath &destPath, bool skip = false); + bool makeDir(const FilePath &srcPath, GFileInfoPtr srcInfo, FilePath &destPath); + bool createSymlink(const FilePath &srcPath, const GFileInfoPtr& srcInfo, FilePath& destPath); + bool createShortcut(const FilePath &srcPath, const GFileInfoPtr& srcInfo, FilePath& destPath); + + bool handleError(GErrorPtr& err, const FilePath &srcPath, const GFileInfoPtr &srcInfo, FilePath &destPath, int& flags); + + static void gfileCopyProgressCallback(goffset current_num_bytes, goffset total_num_bytes, FileTransferJob* _this); + +private: + FilePathList srcPaths_; + FilePathList destPaths_; + Mode mode_; +}; + + +} // namespace Fm + +#endif // FM2_COPYJOB_H diff --git a/src/core/folder.cpp b/src/core/folder.cpp new file mode 100644 index 0000000..813b5ad --- /dev/null +++ b/src/core/folder.cpp @@ -0,0 +1,938 @@ +/* + * fm-folder.c + * + * Copyright 2009 - 2012 Hong Jen Yee (PCMan) + * Copyright 2012-2016 Andriy Grytsenko (LStranger) + * + * This file is a part of the Libfm library. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "folder.h" +#include +#include +#include +#include + +#include "dirlistjob.h" +#include "filesysteminfojob.h" +#include "fileinfojob.h" + +namespace Fm { + +std::unordered_map, FilePathHash> Folder::cache_; +QString Folder::cutFilesDirPath_; +QString Folder::lastCutFilesDirPath_; +std::shared_ptr Folder::cutFilesHashSet_; +std::mutex Folder::mutex_; + +Folder::Folder(): + dirlist_job{nullptr}, + fsInfoJob_{nullptr}, + volumeManager_{VolumeManager::globalInstance()}, + /* for file monitor */ + has_idle_reload_handler{0}, + has_idle_update_handler{false}, + pending_change_notify{false}, + filesystem_info_pending{false}, + wants_incremental{false}, + stop_emission{false}, /* don't set it 1 bit to not lock other bits */ + /* filesystem info - set in query thread, read in main */ + fs_total_size{0}, + fs_free_size{0}, + has_fs_info{false}, + defer_content_test{false} { + + connect(volumeManager_.get(), &VolumeManager::mountAdded, this, &Folder::onMountAdded); + connect(volumeManager_.get(), &VolumeManager::mountRemoved, this, &Folder::onMountRemoved); +} + +Folder::Folder(const FilePath& path): Folder() { + dirPath_ = path; +} + +Folder::~Folder() { + if(dirMonitor_) { + g_signal_handlers_disconnect_by_data(dirMonitor_.get(), this); + dirMonitor_.reset(); + } + + if(dirlist_job) { + dirlist_job->cancel(); + } + + // cancel any file info job in progress. + for(auto job: fileinfoJobs_) { + job->cancel(); + } + + if(fsInfoJob_) { + fsInfoJob_->cancel(); + } + + // We store a weak_ptr instead of shared_ptr in the hash table, so the hash table + // does not own a reference to the folder. When the last reference to Folder is + // freed, we need to remove its hash table entry. + std::lock_guard lock{mutex_}; + auto it = cache_.find(dirPath_); + if(it != cache_.end()) { + cache_.erase(it); + } +} + +// static +std::shared_ptr Folder::fromPath(const FilePath& path) { + std::lock_guard lock{mutex_}; + auto it = cache_.find(path); + if(it != cache_.end()) { + auto folder = it->second.lock(); + if(folder) { + return folder; + } + else { // FIXME: is this possible? + cache_.erase(it); + } + } + auto folder = std::make_shared(path); + folder->reload(); + cache_.emplace(path, folder); + return folder; +} + +bool Folder::makeDirectory(const char* /*name*/, GError** /*error*/) { + // TODO: + // FIXME: what the API is used for in the original libfm C API? + return false; +} + +bool Folder::isIncremental() const { + return wants_incremental; +} + +bool Folder::isValid() const { + return dirInfo_ != nullptr; +} + +bool Folder::isLoaded() const { + return (dirlist_job == nullptr); +} + +std::shared_ptr Folder::fileByName(const char* name) const { + auto it = files_.find(name); + if(it != files_.end()) { + return it->second; + } + return nullptr; +} + +bool Folder::isEmpty() const { + return files_.empty(); +} + +FileInfoList Folder::files() const { + FileInfoList ret; + ret.reserve(files_.size()); + for(const auto& item : files_) { + ret.push_back(item.second); + } + return ret; +} + + +const FilePath& Folder::path() const { + auto pathStr = dirPath_.toString(); + // qDebug() << this << "FOLDER_PATH:" << pathStr.get() << dirPath_.gfile().get(); + //assert(!g_str_has_prefix(pathStr.get(), "file:")); + return dirPath_; +} + +const std::shared_ptr& Folder::info() const { + return dirInfo_; +} + +#if 0 +void Folder::init(FmFolder* folder) { + files = fm_file_info_list_new(); + G_LOCK(hash); + if(G_UNLIKELY(hash_uses == 0)) { + hash = g_hash_table_new((GHashFunc)fm_path_hash, (GEqualFunc)fm_path_equal); + volume_monitor = g_volume_monitor_get(); + if(G_LIKELY(volume_monitor)) { + g_signal_connect(volume_monitor, "mount-added", G_CALLBACK(on_mount_added), nullptr); + g_signal_connect(volume_monitor, "mount-removed", G_CALLBACK(on_mount_removed), nullptr); + } + } + hash_uses++; + G_UNLOCK(hash); +} +#endif + +void Folder::onIdleReload() { + /* check if folder still exists */ + reload(); + // G_LOCK(query); + has_idle_reload_handler = false; + // G_UNLOCK(query); +} + +void Folder::queueReload() { + // G_LOCK(query); + if(!has_idle_reload_handler) { + has_idle_reload_handler = true; + QTimer::singleShot(0, this, &Folder::onIdleReload); + } + // G_UNLOCK(query); +} + +void Folder::onFileInfoFinished() { + FileInfoJob* job = static_cast(sender()); + fileinfoJobs_.erase(std::find(fileinfoJobs_.cbegin(), fileinfoJobs_.cend(), job)); + + /* NOTE: The pending changes should be processed in the order reported by GIO; + otherwise; the final file info might be wrong. Here, we ensure that the next + pending changes are processed only after the current info job is finished. */ + + if(job->isCancelled()) { + has_idle_update_handler = false; // allow future updates + return; + } + + // process the changes accumulated during this info job + if(filesystem_info_pending // means a pending change; see "onFileSystemInfoFinished()" + || !paths_to_update.empty() || !paths_to_add.empty() || !paths_to_del.empty()) { + QTimer::singleShot(0, this, &Folder::processPendingChanges); + } + // there's no pending change at the moment; let the next one be processed + else { + has_idle_update_handler = false; + } + + FileInfoList files_to_add; + FileInfoList files_to_delete; + std::vector files_to_update; + + const auto& paths = job->paths(); + const auto& infos = job->files(); + auto path_it = paths.cbegin(); + auto info_it = infos.cbegin(); + for(; path_it != paths.cend() && info_it != infos.cend(); ++path_it, ++info_it) { + const auto& path = *path_it; + const auto& info = *info_it; + + if(path == dirPath_) { // got the info for the folder itself. + dirInfo_ = info; + } + else { + auto it = files_.find(info->path().baseName().get()); + if(it != files_.end()) { // the file already exists, update + files_to_update.push_back(std::make_pair(it->second, info)); + } + else { // newly added + files_to_add.push_back(info); + } + files_[info->path().baseName().get()] = info; + } + } + if(!files_to_add.empty()) { + Q_EMIT filesAdded(files_to_add); + } + if(!files_to_update.empty()) { + Q_EMIT filesChanged(files_to_update); + } + Q_EMIT contentChanged(); +} + +void Folder::processPendingChanges() { + // FmFileInfoJob* job = nullptr; + std::lock_guard lock{mutex_}; + + // idle_handler = 0; + /* if we were asked to block updates let delay it for now */ + if(stop_emission) { + has_idle_update_handler = false; + return; + } + + FileInfoJob* info_job = nullptr; + if(!paths_to_update.empty() || !paths_to_add.empty()) { + FilePathList paths; + paths.insert(paths.end(), paths_to_add.cbegin(), paths_to_add.cend()); + paths.insert(paths.end(), paths_to_update.cbegin(), paths_to_update.cend()); + info_job = new FileInfoJob{paths, hasCutFiles() ? cutFilesHashSet_ : nullptr}; + paths_to_update.clear(); + paths_to_add.clear(); + } + else { + // let the next pending changes be processed; see "onFileInfoFinished()" + has_idle_update_handler = false; + } + + if(info_job) { + fileinfoJobs_.push_back(info_job); + info_job->setAutoDelete(true); + connect(info_job, &FileInfoJob::finished, this, &Folder::onFileInfoFinished, Qt::BlockingQueuedConnection); + info_job->runAsync(); +#if 0 + pending_jobs = g_slist_prepend(pending_jobs, job); + if(!fm_job_run_async(FM_JOB(job))) { + pending_jobs = g_slist_remove(pending_jobs, job); + g_object_unref(job); + g_critical("failed to start folder update job"); + } +#endif + } + + // process deletion + if(!paths_to_del.empty()) { + FileInfoList deleted_files; + for(const auto &path: paths_to_del) { + auto name = path.baseName(); + auto it = files_.find(name.get()); + if(it != files_.end()) { + deleted_files.push_back(it->second); + files_.erase(it); + } + } + if(!deleted_files.empty()) { + Q_EMIT filesRemoved(deleted_files); + Q_EMIT contentChanged(); + } + paths_to_del.clear(); + } + + if(pending_change_notify) { + Q_EMIT changed(); + /* update volume info */ + queryFilesystemInfo(); + pending_change_notify = false; + } + + if(filesystem_info_pending) { + Q_EMIT fileSystemChanged(); + filesystem_info_pending = false; + } +} + +/* should be called only with G_LOCK(lists) on! */ +void Folder::queueUpdate() { + // qDebug() << "queue_update:" << !has_idle_handler << paths_to_add.size() << paths_to_update.size() << paths_to_del.size(); + if(!has_idle_update_handler) { + QTimer::singleShot(0, this, &Folder::processPendingChanges); + has_idle_update_handler = true; + } +} + + +/* NOTE: When queuing files for addition/update/deletion in the following functions, + the currently detected files (namely, "files_") should not be taken into account + because they might be changed soon due to a previous call to queueUpdate(). */ + +/* returns true if reference was taken from path */ +bool Folder::eventFileAdded(const FilePath &path) { + bool added = true; + // G_LOCK(lists); + if(std::find(paths_to_del.cbegin(), paths_to_del.cend(), path) != paths_to_del.cend()) { + // if the file was going to be deleted, its addition means an update, + // so remove it from the deletion queue and add it to the update queue + paths_to_del.erase(std::remove(paths_to_del.begin(), paths_to_del.end(), path), paths_to_del.cend()); + if(std::find(paths_to_update.cbegin(), paths_to_update.cend(), path) == paths_to_update.cend()) { + paths_to_update.push_back(path); + } + } + else if(std::find(paths_to_add.cbegin(), paths_to_add.cend(), path) == paths_to_add.cend()) { + paths_to_add.push_back(path); + } + else { // file already queued for adding, don't duplicate + added = false; + } + + if(added) { + queueUpdate(); + } + // G_UNLOCK(lists); + return added; +} + +bool Folder::eventFileChanged(const FilePath &path) { + bool added; + // G_LOCK(lists); + if(std::find(paths_to_update.cbegin(), paths_to_update.cend(), path) == paths_to_update.cend() + && std::find(paths_to_add.cbegin(), paths_to_add.cend(), path) == paths_to_add.cend()) { + paths_to_update.push_back(path); + added = true; + queueUpdate(); + } + else { + added = false; + } + // G_UNLOCK(lists); + return added; +} + +void Folder::eventFileDeleted(const FilePath& path) { + bool deleted = true; + // qDebug() << "delete " << path.baseName().get(); + // G_LOCK(lists); + if(std::find(paths_to_add.cbegin(), paths_to_add.cend(), path) != paths_to_add.cend()) { + // if the file was going to be added, just remove it from the addition queue + paths_to_add.erase(std::remove(paths_to_add.begin(), paths_to_add.end(), path), paths_to_add.cend()); + } + else if(std::find(paths_to_del.cbegin(), paths_to_del.cend(), path) == paths_to_del.cend()) { + paths_to_del.push_back(path); + // the update queue should be cancelled for a file that is going to be deleted + paths_to_update.erase(std::remove(paths_to_update.begin(), paths_to_update.end(), path), paths_to_update.cend()); + } + else { + deleted = false; + } + + if(deleted) { + queueUpdate(); + } + // G_UNLOCK(lists); +} + + +void Folder::onDirChanged(GFileMonitorEvent evt) { + switch(evt) { + case G_FILE_MONITOR_EVENT_PRE_UNMOUNT: + /* g_debug("folder is going to be unmounted"); */ + break; + case G_FILE_MONITOR_EVENT_UNMOUNTED: + Q_EMIT unmount(); + /* g_debug("folder is unmounted"); */ + queueReload(); + break; + case G_FILE_MONITOR_EVENT_DELETED: + Q_EMIT removed(); + /* g_debug("folder is deleted"); */ + break; + case G_FILE_MONITOR_EVENT_CREATED: + queueReload(); + break; + case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED: + case G_FILE_MONITOR_EVENT_CHANGED: { + std::lock_guard lock{mutex_}; + pending_change_notify = true; + if(std::find(paths_to_update.cbegin(), paths_to_update.cend(), dirPath_) != paths_to_update.cend()) { + paths_to_update.push_back(dirPath_); + queueUpdate(); + } + /* g_debug("folder is changed"); */ + break; + } +#if GLIB_CHECK_VERSION(2,24,0) + case G_FILE_MONITOR_EVENT_MOVED: +#endif + case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT: + ; + default: + break; + } +} + +void Folder::onFileChangeEvents(GFileMonitor* /*monitor*/, GFile* gf, GFile* /*other_file*/, GFileMonitorEvent evt) { + /* const char* names[]={ + "G_FILE_MONITOR_EVENT_CHANGED", + "G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT", + "G_FILE_MONITOR_EVENT_DELETED", + "G_FILE_MONITOR_EVENT_CREATED", + "G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED", + "G_FILE_MONITOR_EVENT_PRE_UNMOUNT", + "G_FILE_MONITOR_EVENT_UNMOUNTED" + }; */ + if(dirPath_ == gf) { + onDirChanged(evt); + return; + } + else { + std::lock_guard lock{mutex_}; + auto path = FilePath{gf, true}; + /* NOTE: sometimes, for unknown reasons, GFileMonitor gives us the + * same event of the same file for multiple times. So we need to + * check for duplications ourselves here. */ + switch(evt) { + case G_FILE_MONITOR_EVENT_CREATED: + eventFileAdded(path); + break; + case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED: + case G_FILE_MONITOR_EVENT_CHANGED: + eventFileChanged(path); + break; + case G_FILE_MONITOR_EVENT_DELETED: + eventFileDeleted(path); + break; + default: + break; + } + } +} + +// checks whether there were cut files here +// and if there were, invalidates this last cut path +bool Folder::hadCutFilesUnset() { + if(lastCutFilesDirPath_ == dirPath_.toString().get()) { + lastCutFilesDirPath_ = QString(); + return true; + } + return false; +} + +bool Folder::hasCutFiles() { + return cutFilesHashSet_ + && !cutFilesHashSet_->empty() + && cutFilesDirPath_ == dirPath_.toString().get(); +} + +void Folder::setCutFiles(const std::shared_ptr& cutFilesHashSet) { + if(cutFilesHashSet_ && !cutFilesHashSet_->empty()) { + lastCutFilesDirPath_ = cutFilesDirPath_; + } + cutFilesDirPath_ = dirPath_.toString().get(); + cutFilesHashSet_ = cutFilesHashSet; +} + +void Folder::onDirListFinished() { + DirListJob* job = static_cast(sender()); + if(job->isCancelled()) { // this is a cancelled job, ignore! + if(job == dirlist_job) { + dirlist_job = nullptr; + Q_EMIT finishLoading(); // this was the last job until now + } + return; + } + dirInfo_ = job->dirInfo(); + + FileInfoList files_to_add; + std::vector files_to_update; + const auto& infos = job->files(); + + // with "search://", there is no update for infos and all of them should be added + if(strcmp(dirPath_.uriScheme().get(), "search") == 0) { + files_to_add = infos; + for(auto& file: files_to_add) { + files_[file->path().baseName().get()] = file; + } + } + else { + auto info_it = infos.cbegin(); + for(; info_it != infos.cend(); ++info_it) { + const auto& info = *info_it; + auto it = files_.find(info->path().baseName().get()); + if(it != files_.end()) { + files_to_update.push_back(std::make_pair(it->second, info)); + } + else { + files_to_add.push_back(info); + } + files_[info->path().baseName().get()] = info; + } + } + + if(!files_to_add.empty()) { + Q_EMIT filesAdded(files_to_add); + } + if(!files_to_update.empty()) { + Q_EMIT filesChanged(files_to_update); + } + +#if 0 + if(dirlist_job->isCancelled() && !wants_incremental) { + GList* l; + for(l = fm_file_info_list_peek_head_link(job->files); l; l = l->next) { + FmFileInfo* inf = (FmFileInfo*)l->data; + files = g_slist_prepend(files, inf); + fm_file_info_list_push_tail(files, inf); + } + if(G_LIKELY(files)) { + GSList* l; + + G_LOCK(lists); + if(defer_content_test && fm_path_is_native(dir_path)) + /* we got only basic info on content, schedule update it now */ + for(l = files; l; l = l->next) + files_to_update = g_slist_prepend(files_to_update, + fm_path_ref(fm_file_info_get_path(l->data))); + G_UNLOCK(lists); + g_signal_emit(folder, signals[FILES_ADDED], 0, files); + g_slist_free(files); + } + + if(job->dir_fi) { + dir_fi = fm_file_info_ref(job->dir_fi); + } + + /* Some new files are created while FmDirListJob is loading the folder. */ + G_LOCK(lists); + if(G_UNLIKELY(files_to_add)) { + /* This should be a very rare case. Could this happen? */ + GSList* l; + for(l = files_to_add; l;) { + FmPath* path = l->data; + GSList* next = l->next; + if(_Folder::get_file_by_path(folder, path)) { + /* we already have the file. remove it from files_to_add, + * and put it in files_to_update instead. + * No strdup for name is needed here. We steal + * the string from files_to_add.*/ + files_to_update = g_slist_prepend(files_to_update, path); + files_to_add = g_slist_delete_link(files_to_add, l); + } + l = next; + } + } + G_UNLOCK(lists); + } + else if(!dir_fi && job->dir_fi) + /* we may need dir_fi for incremental folders too */ + { + dir_fi = fm_file_info_ref(job->dir_fi); + } + g_object_unref(dirlist_job); +#endif + + dirlist_job = nullptr; + Q_EMIT finishLoading(); +} + +#if 0 + + +void on_dirlist_job_files_found(FmDirListJob* job, GSList* files, gpointer user_data) { + FmFolder* folder = FM_FOLDER(user_data); + GSList* l; + for(l = files; l; l = l->next) { + FmFileInfo* file = FM_FILE_INFO(l->data); + fm_file_info_list_push_tail(files, file); + } + if(G_UNLIKELY(!dir_fi && job->dir_fi)) + /* we may want info while folder is still loading */ + { + dir_fi = fm_file_info_ref(job->dir_fi); + } + g_signal_emit(folder, signals[FILES_ADDED], 0, files); +} + +ErrorAction on_dirlist_job_error(FmDirListJob* job, GError* err, FmJobErrorSeverity severity, FmFolder* folder) { + guint ret; + /* it's possible that some signal handlers tries to free the folder + * when errors occurs, so let's g_object_ref here. */ + g_object_ref(folder); + g_signal_emit(folder, signals[ERROR], 0, err, (guint)severity, &ret); + g_object_unref(folder); + return ret; +} + +void free_dirlist_job(FmFolder* folder) { + if(wants_incremental) { + g_signal_handlers_disconnect_by_func(dirlist_job, on_dirlist_job_files_found, folder); + } + g_signal_handlers_disconnect_by_func(dirlist_job, on_dirlist_job_finished, folder); + g_signal_handlers_disconnect_by_func(dirlist_job, on_dirlist_job_error, folder); + fm_job_cancel(FM_JOB(dirlist_job)); + g_object_unref(dirlist_job); + dirlist_job = nullptr; +} + +#endif + + +void Folder::reload() { + // cancel in-progress jobs if there are any + if(dirlist_job) { + dirlist_job->cancel(); + } + GError* err = nullptr; + // cancel directory monitoring + if(dirMonitor_) { + g_signal_handlers_disconnect_by_data(dirMonitor_.get(), this); + dirMonitor_.reset(); + } + + /* clear all update-lists now, see SF bug #919 - if update comes before + listing job is finished, a duplicate may be created in the folder */ + if(has_idle_update_handler) { + // FIXME: cancel the idle handler + paths_to_add.clear(); + paths_to_update.clear(); + paths_to_del.clear(); + + // cancel any file info job in progress. + for(auto job: fileinfoJobs_) { + job->cancel(); + disconnect(job, &FileInfoJob::finished, this, &Folder::onFileInfoFinished); + } + fileinfoJobs_.clear(); + + // ensure future changes will be processed + has_idle_update_handler = false; + } + + /* remove all existing files */ + if(!files_.empty()) { + // FIXME: this is not very efficient :( + auto tmp = files(); + files_.clear(); + Q_EMIT filesRemoved(tmp); + } + + /* Tell the world that we're about to reload the folder. + * It might be a good idea for users of the folder to disconnect + * from the folder temporarily and reconnect to it again after + * the folder complete the loading. This might reduce some + * unnecessary signal handling and UI updates. */ + Q_EMIT startLoading(); + + dirInfo_.reset(); // clear dir info + + /* also re-create a new file monitor */ + // mon = GFileMonitorPtr{fm_monitor_directory(dir_path.gfile().get(), &err), false}; + // FIXME: should we make this cancellable? + dirMonitor_ = GFileMonitorPtr{ + g_file_monitor_directory(dirPath_.gfile().get(), G_FILE_MONITOR_WATCH_MOUNTS, nullptr, &err), + false + }; + + if(dirMonitor_) { + g_signal_connect(dirMonitor_.get(), "changed", G_CALLBACK(_onFileChangeEvents), this); + } + else { + qDebug("file monitor cannot be created: %s", err->message); + g_error_free(err); + } + + Q_EMIT contentChanged(); + + /* run a new dir listing job */ + // FIXME: + // defer_content_test = fm_config->defer_content_test; + dirlist_job = new DirListJob(dirPath_, defer_content_test ? DirListJob::FAST : DirListJob::DETAILED, + hasCutFiles() ? cutFilesHashSet_ : nullptr); + dirlist_job->setAutoDelete(true); + connect(dirlist_job, &DirListJob::error, this, &Folder::error, Qt::BlockingQueuedConnection); + connect(dirlist_job, &DirListJob::finished, this, &Folder::onDirListFinished, Qt::BlockingQueuedConnection); + +#if 0 + if(wants_incremental) { + g_signal_connect(dirlist_job, "files-found", G_CALLBACK(on_dirlist_job_files_found), folder); + } + fm_dir_list_job_set_incremental(dirlist_job, wants_incremental); +#endif + + dirlist_job->runAsync(); + + /* also reload filesystem info. + * FIXME: is this needed? */ + queryFilesystemInfo(); +} + +#if 0 + +/** + * Folder::is_incremental + * @folder: folder to test + * + * Checks if a folder is incrementally loaded. + * After an FmFolder object is obtained from calling Folder::from_path(), + * if it's not yet loaded, it begins loading the content of the folder + * and emits "start-loading" signal. Most of the time, the info of the + * files in the folder becomes available only after the folder is fully + * loaded. That means, after the "finish-loading" signal is emitted. + * Before the loading is finished, Folder::get_files() returns nothing. + * You can tell if a folder is still being loaded with Folder::is_loaded(). + * + * However, for some special FmFolder types, such as the ones handling + * search:// URIs, we want to access the file infos while the folder is + * still being loaded (the search is still ongoing). + * The content of the folder grows incrementally and Folder::get_files() + * returns files currently being loaded even when the folder is not + * fully loaded. This is what we called incremental. + * Folder::is_incremental() tells you if the FmFolder has this feature. + * + * Returns: %true if @folder is incrementally loaded + * + * Since: 1.0.2 + */ +bool Folder::is_incremental(FmFolder* folder) { + return wants_incremental; +} + +#endif + +bool Folder::getFilesystemInfo(uint64_t* total_size, uint64_t* free_size) const { + if(has_fs_info) { + *total_size = fs_total_size; + *free_size = fs_free_size; + return true; + } + return false; +} + + +void Folder::onFileSystemInfoFinished() { + FileSystemInfoJob* job = static_cast(sender()); + if(job->isCancelled() || job != fsInfoJob_) { // this is a cancelled job, ignore! + fsInfoJob_ = nullptr; + has_fs_info = false; + return; + } + has_fs_info = job->isAvailable(); + fs_total_size = job->size(); + fs_free_size = job->freeSize(); + filesystem_info_pending = true; + fsInfoJob_ = nullptr; + queueUpdate(); +} + + +void Folder::queryFilesystemInfo() { + // G_LOCK(query); + if(fsInfoJob_) + return; + fsInfoJob_ = new FileSystemInfoJob{dirPath_}; + fsInfoJob_->setAutoDelete(true); + connect(fsInfoJob_, &FileSystemInfoJob::finished, this, &Folder::onFileSystemInfoFinished, Qt::BlockingQueuedConnection); + + fsInfoJob_->runAsync(); + // G_UNLOCK(query); +} + + +#if 0 +/** + * Folder::block_updates + * @folder: folder to apply + * + * Blocks emitting signals for changes in folder, i.e. if some file was + * added, changed, or removed in folder after this API, no signal will be + * sent until next call to Folder::unblock_updates(). + * + * Since: 1.2.0 + */ +void Folder::block_updates(FmFolder* folder) { + /* g_debug("Folder::block_updates %p", folder); */ + G_LOCK(lists); + /* just set the flag */ + stop_emission = true; + G_UNLOCK(lists); +} + +/** + * Folder::unblock_updates + * @folder: folder to apply + * + * Unblocks emitting signals for changes in folder. If some changes were + * in folder after previous call to Folder::block_updates() then these + * changes will be sent after this call. + * + * Since: 1.2.0 + */ +void Folder::unblock_updates(FmFolder* folder) { + /* g_debug("Folder::unblock_updates %p", folder); */ + G_LOCK(lists); + stop_emission = false; + /* query update now */ + queue_update(folder); + G_UNLOCK(lists); + /* g_debug("Folder::unblock_updates OK"); */ +} + +/** + * Folder::make_directory + * @folder: folder to apply + * @name: display name for new directory + * @error: (allow-none) (out): location to save error + * + * Creates new directory in given @folder. + * + * Returns: %true in case of success. + * + * Since: 1.2.0 + */ +bool Folder::make_directory(FmFolder* folder, const char* name, GError** error) { + GFile* dir, *gf; + FmPath* path; + bool ok; + + dir = fm_path_to_gfile(dir_path); + gf = g_file_get_child_for_display_name(dir, name, error); + g_object_unref(dir); + if(gf == nullptr) { + return false; + } + ok = g_file_make_directory(gf, nullptr, error); + if(ok) { + path = fm_path_new_for_gfile(gf); + if(!_Folder::event_file_added(folder, path)) { + fm_path_unref(path); + } + } + g_object_unref(gf); + return ok; +} + +void Folder::content_changed(FmFolder* folder) { + if(has_fs_info && !fs_info_not_avail) { + Folder::query_filesystem_info(folder); + } +} + +#endif + +/* NOTE: + * GFileMonitor has some significant limitations: + * 1. Currently it can correctly emit unmounted event for a directory. + * 2. After a directory is unmounted, its content changes. + * Inotify does not fire events for this so a forced reload is needed. + * 3. If a folder is empty, and later a filesystem is mounted to the + * folder, its content should reflect the content of the newly mounted + * filesystem. However, GFileMonitor and inotify do not emit events + * for this case. A forced reload might be needed for this case as well. + * 4. Some limitations come from Linux/inotify. If FAM/gamin is used, + * the condition may be different. More testing is needed. + */ +void Folder::onMountAdded(const Mount& mnt) { + /* If a filesystem is mounted over an existing folder, + * we need to refresh the content of the folder to reflect + * the changes. Besides, we need to create a new GFileMonitor + * for the newly-mounted filesystem as the inode already changed. + * GFileMonitor cannot detect this kind of changes caused by mounting. + * So let's do it ourselves. */ + auto mountRoot = mnt.root(); + if(mountRoot.isPrefixOf(dirPath_)) { + queueReload(); + } + /* g_debug("FmFolder::mount_added"); */ +} + +void Folder::onMountRemoved(const Mount& mnt) { + /* g_debug("FmFolder::mount_removed"); */ + + /* NOTE: gvfs does not emit unmount signals for remote folders since + * GFileMonitor does not support remote filesystems at all. + * So here is the side effect, no unmount notifications. + * We need to generate the signal ourselves. */ + if(!dirMonitor_) { + // this is only needed when we don't have a GFileMonitor + auto mountRoot = mnt.root(); + if(mountRoot.isPrefixOf(dirPath_)) { + // if the current folder is under the unmounted path, generate the event ourselves + onDirChanged(G_FILE_MONITOR_EVENT_UNMOUNTED); + } + } +} + +} // namespace Fm diff --git a/src/core/folder.h b/src/core/folder.h new file mode 100644 index 0000000..098d218 --- /dev/null +++ b/src/core/folder.h @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2016 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef __LIBFM2_QT_FM_FOLDER_H__ +#define __LIBFM2_QT_FM_FOLDER_H__ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include "../libfmqtglobals.h" + +#include "gioptrs.h" +#include "fileinfo.h" +#include "job.h" +#include "volumemanager.h" + +namespace Fm { + +class DirListJob; +class FileSystemInfoJob; +class FileInfoJob; + + +class LIBFM_QT_API Folder: public QObject { + Q_OBJECT +public: + + explicit Folder(); + + explicit Folder(const FilePath& path); + + virtual ~Folder(); + + static std::shared_ptr fromPath(const FilePath& path); + + bool makeDirectory(const char* name, GError** error); + + void queryFilesystemInfo(); + + bool getFilesystemInfo(uint64_t* total_size, uint64_t* free_size) const; + + void reload(); + + bool isIncremental() const; + + bool isValid() const; + + bool isLoaded() const; + + std::shared_ptr fileByName(const char* name) const; + + bool isEmpty() const; + + FileInfoList files() const; + + const FilePath& path() const; + + const std::shared_ptr &info() const; + + bool hadCutFilesUnset(); + + bool hasCutFiles(); + + void setCutFiles(const std::shared_ptr& cutFilesHashSet); + + void forEachFile(std::function&)> func) const { + std::lock_guard lock{mutex_}; + for(auto it = files_.begin(); it != files_.end(); ++it) { + func(it->second); + } + } + +Q_SIGNALS: + void startLoading(); + + void finishLoading(); + + void filesAdded(FileInfoList& addedFiles); + + void filesChanged(std::vector& changePairs); + + void filesRemoved(FileInfoList& removedFiles); + + void removed(); + + void changed(); + + void unmount(); + + void contentChanged(); + + void fileSystemChanged(); + + // FIXME: this API design is bad. We leave this here to be compatible with the old libfm C API. + // It might be better to remember the error state while loading the folder, and let the user of the + // API handle the error on finish. + void error(const GErrorPtr& err, Job::ErrorSeverity severity, Job::ErrorAction& response); + +private: + + static void _onFileChangeEvents(GFileMonitor* monitor, GFile* file, GFile* other_file, GFileMonitorEvent event_type, Folder* _this) { + _this->onFileChangeEvents(monitor, file, other_file, event_type); + } + void onFileChangeEvents(GFileMonitor* monitor, GFile* file, GFile* other_file, GFileMonitorEvent event_type); + void onDirChanged(GFileMonitorEvent event_type); + + void queueUpdate(); + void queueReload(); + + bool eventFileAdded(const FilePath &path); + bool eventFileChanged(const FilePath &path); + void eventFileDeleted(const FilePath &path); + +private Q_SLOTS: + + void processPendingChanges(); + + void onDirListFinished(); + + void onFileSystemInfoFinished(); + + void onFileInfoFinished(); + + void onIdleReload(); + + void onMountAdded(const Mount& mnt); + + void onMountRemoved(const Mount& mnt); + +private: + FilePath dirPath_; + GFileMonitorPtr dirMonitor_; + + std::shared_ptr dirInfo_; + DirListJob* dirlist_job; + std::vector fileinfoJobs_; + FileSystemInfoJob* fsInfoJob_; + + std::shared_ptr volumeManager_; + + /* for file monitor */ + bool has_idle_reload_handler; + bool has_idle_update_handler; + FilePathList paths_to_add; + FilePathList paths_to_update; + FilePathList paths_to_del; + // GSList* pending_jobs; + bool pending_change_notify; + bool filesystem_info_pending; + + bool wants_incremental; + bool stop_emission; /* don't set it 1 bit to not lock other bits */ + + // NOTE: Here, FileInfo::path().baseName().get() should be used as the key value, not FileInfo::name(), + // because the latter is not always the same as the former and the former will be used for comparison. + std::unordered_map, std::hash> files_; + + /* filesystem info - set in query thread, read in main */ + uint64_t fs_total_size; + uint64_t fs_free_size; + GCancellablePtr fs_size_cancellable; + + bool has_fs_info : 1; + bool defer_content_test : 1; + + static std::unordered_map, FilePathHash> cache_; + static QString cutFilesDirPath_; + static QString lastCutFilesDirPath_; + static std::shared_ptr cutFilesHashSet_; + static std::mutex mutex_; +}; + +} + +#endif // __LIBFM_QT_FM2_FOLDER_H__ diff --git a/src/core/folderconfig.cpp b/src/core/folderconfig.cpp new file mode 100644 index 0000000..1619c87 --- /dev/null +++ b/src/core/folderconfig.cpp @@ -0,0 +1,275 @@ +/** + * SECTION:fm-folder-config + * @short_description: Folder specific settings cache. + * @title: FmFolderConfig + * + * @include: libfm/fm.h + * + * This API represents access to folder-specific configuration settings. + * Each setting is a key/value pair. To use it the descriptor should be + * opened first, then required operations performed, then closed. Each + * opened descriptor holds a lock on the cache so it is not adviced to + * keep it somewhere. + */ + +#include "folderconfig.h" + +#include +#include +#include + +namespace Fm { + +CStrPtr FolderConfig::globalConfigFile_; + +// FIXME: sharing the same keyfile object everywhere is problematic +// FIXME: this is MT-unsafe +static GKeyFile* fc_cache = nullptr; +static bool fc_cache_changed = FALSE; + +FolderConfig::FolderConfig(): + keyFile_{nullptr}, + changed_{false} { +} + +FolderConfig::FolderConfig(const Fm::FilePath& path): FolderConfig{} { + (void)open(path); +} + +FolderConfig::~FolderConfig() { + if(isOpened()) { + GErrorPtr err; + close(err); + } +} + +bool FolderConfig::open(const Fm::FilePath& path) { + if(isOpened()) { // the config is already opened + return false; + } + + changed_ = FALSE; + if(path.isNative()) { + /* clear .directory file first */ + auto sub_path = path.child(".directory"); + configFilePath_ = sub_path.toString(); + + // FIXME: this only works for local filesystem and it's a blocking call. :-( + if(g_file_test(configFilePath_.get(), G_FILE_TEST_EXISTS)) { + keyFile_ = g_key_file_new(); + if(g_key_file_load_from_file(keyFile_, configFilePath_.get(), + GKeyFileFlags(G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS), + nullptr) && + g_key_file_has_group(keyFile_, "File Manager")) { + group_ = CStrPtr{g_strdup("File Manager")}; + return true; + } + g_key_file_free(keyFile_); + } + } + + // No per-folder config file. + // use the global key file instead and use the folder path as group key + configFilePath_.reset(); + group_ = path.toString(); + + // FIXME: we should use ref counting here. glib 2.36+ supports g_key_file_ref() + keyFile_ = fc_cache; + return true; +} + + +bool FolderConfig::close(GErrorPtr& err) { + bool ret = TRUE; + if(!isOpened()) { + return false; + } + + if(configFilePath_) { + if(changed_) { + char* out; + gsize len; + + out = g_key_file_to_data(keyFile_, &len, &err); + if(!out || !g_file_set_contents(configFilePath_.get(), out, len, &err)) { + ret = FALSE; + } + g_free(out); + } + configFilePath_.reset(); + g_key_file_free(keyFile_); + } + else { + group_.reset(); + if(changed_) { + fc_cache_changed = TRUE; + } + } + keyFile_ = nullptr; + return ret; +} + +bool FolderConfig::isOpened() const { + return keyFile_ != nullptr; +} + +bool FolderConfig::isEmpty() { + return !g_key_file_has_group(keyFile_, group_.get()); +} + +bool FolderConfig::getInteger(const char* key, int* val) { + GErrorPtr err; + auto ret = g_key_file_get_integer(keyFile_, group_.get(), key, &err); + if(err) { + return false; + } + *val = ret; + return true; +} + +bool FolderConfig::getUint64(const char* key, uint64_t *val) { + GError* error = nullptr; +#if GLIB_CHECK_VERSION(2, 26, 0) + guint64 ret = g_key_file_get_uint64(keyFile_, group_.get(), key, &error); +#else + gchar* s, *end; + guint64 ret; + + s = g_key_file_get_value(keyFile_, group_.get(), key, &error); +#endif + if(error) { + g_error_free(error); + return FALSE; + } +#if !GLIB_CHECK_VERSION(2, 26, 0) + ret = g_ascii_strtoull(s, &end, 10); + if(*s == '\0' || *end != '\0') { + g_free(s); + return FALSE; + } + g_free(s); +#endif + *val = ret; + return TRUE; +} + +bool FolderConfig::getDouble(const char* key, + double* val) { + GError* error = nullptr; + double ret = g_key_file_get_double(keyFile_, group_.get(), key, &error); + if(error) { + g_error_free(error); + return FALSE; + } + *val = ret; + return TRUE; +} + +bool FolderConfig::getBoolean(const char* key, bool* val) { + GErrorPtr err; + auto ret = g_key_file_get_boolean(keyFile_, group_.get(), key, &err); + if(err) { + return false; + } + *val = ret; + return true; +} + +char* FolderConfig::getString(const char* key) { + return g_key_file_get_string(keyFile_, group_.get(), key, nullptr); +} + +char** FolderConfig::getStringList(const char* key, gsize* length) { + return g_key_file_get_string_list(keyFile_, group_.get(), key, length, nullptr); +} + + +void FolderConfig::setInteger(const char* key, int val) { + changed_ = TRUE; + g_key_file_set_integer(keyFile_, group_.get(), key, val); +} + +void FolderConfig::setUint64(const char* key, uint64_t val) { + changed_ = TRUE; +#if GLIB_CHECK_VERSION(2, 26, 0) + g_key_file_set_uint64(keyFile_, group_.get(), key, val); +#else + gchar* result = g_strdup_printf("%" G_GUINT64_FORMAT, val); + g_key_file_set_value(keyFile_, group_.get(), key, result); + g_free(result); +#endif +} + +void FolderConfig::setDouble(const char* key, double val) { + changed_ = TRUE; + g_key_file_set_double(keyFile_, group_.get(), key, val); +} + +void FolderConfig::setBoolean(const char* key, bool val) { + changed_ = TRUE; + g_key_file_set_boolean(keyFile_, group_.get(), key, val); +} + +void FolderConfig::setString(const char* key, const char* string) { + changed_ = TRUE; + g_key_file_set_string(keyFile_, group_.get(), key, string); +} + +void FolderConfig::setStringList(const char* key, + const gchar* const list[], gsize length) { + changed_ = TRUE; + g_key_file_set_string_list(keyFile_, group_.get(), key, list, length); +} + +void FolderConfig::removeKey(const char* key) { + changed_ = TRUE; + g_key_file_remove_key(keyFile_, group_.get(), key, nullptr); +} + +void FolderConfig::purge() { + changed_ = TRUE; + g_key_file_remove_group(keyFile_, group_.get(), nullptr); +} + +// static +void FolderConfig::saveCache(void) { + char* out; + gsize len; + + /* if per-directory cache was changed since last invocation then save it */ + if(fc_cache_changed && (out = g_key_file_to_data(fc_cache, &len, nullptr))) { + /* FIXME: create dir */ + /* create temp file with settings */ + GFilePtr gfile{g_file_new_for_path(globalConfigFile_.get()), false}; + GErrorPtr err; + /* do safe replace now, the file is important enough to be lost */ + if(g_file_replace_contents(gfile.get(), out, len, nullptr, true, G_FILE_CREATE_PRIVATE, nullptr, nullptr, &err)) { + fc_cache_changed = FALSE; + } + else { + g_warning("cannot save %s: %s", globalConfigFile_.get(), err->message); + } + g_free(out); + } +} + +// static +void FolderConfig::finalize(void) { + saveCache(); + g_key_file_free(fc_cache); + fc_cache = nullptr; +} + +// static +void FolderConfig::init(const char* globalConfigFile) { + globalConfigFile_ = CStrPtr{g_strdup(globalConfigFile)}; + fc_cache = g_key_file_new(); + if(!g_key_file_load_from_file(fc_cache, globalConfigFile_.get(), G_KEY_FILE_NONE, nullptr)) { + // fail to load the config file. + // fallback to the legacy libfm config file for backward compatibility + CStrPtr legacyConfigFlie{g_build_filename(g_get_user_config_dir(), "libfm/dir-settings.conf", nullptr)}; + g_key_file_load_from_file(fc_cache, legacyConfigFlie.get(), G_KEY_FILE_NONE, nullptr); + } +} + +} // namespace Fm diff --git a/src/core/folderconfig.h b/src/core/folderconfig.h new file mode 100644 index 0000000..3959ae2 --- /dev/null +++ b/src/core/folderconfig.h @@ -0,0 +1,80 @@ +#ifndef FOLDERCONFIG_H +#define FOLDERCONFIG_H + +#include "../libfmqtglobals.h" + +#include "filepath.h" +#include "gioptrs.h" + +#include + +namespace Fm { + +class LIBFM_QT_API FolderConfig { +public: + FolderConfig(); + + explicit FolderConfig(const Fm::FilePath& path); + + ~FolderConfig(); + + bool open(const Fm::FilePath& path); + + bool close(GErrorPtr& err); + + bool isOpened() const; + + void purge(void); + + void removeKey(const char* key); + + void setStringList(const char* key, const gchar* const list[], gsize length); + + void setString(const char* key, const char* string); + + void setBoolean(const char* key, bool val); + + void setDouble(const char* key, double val); + + void setUint64(const char* key, std::uint64_t val); + + void setInteger(const char* key, int val); + + char** getStringList(const char* key, gsize* length); + + char* getString(const char* key); + + bool getBoolean(const char* key, bool* val); + + bool getDouble(const char* key, double* val); + + bool getUint64(const char* key, std::uint64_t* val); + + bool getInteger(const char* key, int* val); + + bool isEmpty(void); + + // this needs to be called before using Fm::FolderConfig + static void init(const char* globalConfigFile); + + static void finalize(); + + static void saveCache(void); + +// the object cannot be copied. +private: + FolderConfig(const FolderConfig& other) = delete; + FolderConfig& operator=(const FolderConfig& other) = delete; + +private: + GKeyFile *keyFile_; + CStrPtr group_; /* allocated if not in cache */ + CStrPtr configFilePath_; /* NULL if in cache */ + bool changed_; + + static CStrPtr globalConfigFile_; +}; + +} // namespace Fm + +#endif // FOLDERCONFIG_H diff --git a/src/core/gioptrs.h b/src/core/gioptrs.h new file mode 100644 index 0000000..ae22602 --- /dev/null +++ b/src/core/gioptrs.h @@ -0,0 +1,141 @@ +#ifndef GIOPTRS_H +#define GIOPTRS_H +// define smart pointers for GIO data types + +#include +#include +#include "gobjectptr.h" +#include "cstrptr.h" + +namespace Fm { + +typedef GObjectPtr GFilePtr; +typedef GObjectPtr GFileInfoPtr; +typedef GObjectPtr GFileMonitorPtr; +typedef GObjectPtr GCancellablePtr; +typedef GObjectPtr GFileEnumeratorPtr; + +typedef GObjectPtr GInputStreamPtr; +typedef GObjectPtr GFileInputStreamPtr; +typedef GObjectPtr GOutputStreamPtr; +typedef GObjectPtr GFileOutputStreamPtr; + +typedef GObjectPtr GIconPtr; + +typedef GObjectPtr GVolumeMonitorPtr; +typedef GObjectPtr GVolumePtr; +typedef GObjectPtr GMountPtr; + +typedef GObjectPtr GAppInfoPtr; + + +class GErrorPtr { +public: + GErrorPtr(): err_{nullptr} { + } + + GErrorPtr(GError*&& err) noexcept: err_{err} { + err = nullptr; + } + + GErrorPtr(const GErrorPtr& other) = delete; + + GErrorPtr(GErrorPtr&& other) noexcept: err_{other.err_} { + other.err_ = nullptr; + } + + GErrorPtr(std::uint32_t domain, unsigned int code, const char* msg): + GErrorPtr{g_error_new_literal(domain, code, msg)} { + } + + GErrorPtr(std::uint32_t domain, unsigned int code, const QString& msg): + GErrorPtr{domain, code, msg.toUtf8().constData()} { + } + + ~GErrorPtr() { + reset(); + } + + std::uint32_t domain() const { + if(err_ != nullptr) { + return err_->domain; + } + return 0; + } + + unsigned int code() const { + if(err_ != nullptr) { + return err_->code; + } + return 0; + } + + QString message() const { + if(err_ != nullptr) { + return err_->message; + } + return QString(); + } + + void reset() { + if(err_) { + g_error_free(err_); + } + err_ = nullptr; + } + + GError* get() const { + return err_; + } + + GErrorPtr& operator = (const GErrorPtr& other) = delete; + + GErrorPtr& operator = (GErrorPtr&& other) noexcept { + reset(); + err_ = other.err_; + other.err_ = nullptr; + return *this; + } + + GErrorPtr& operator = (GError*&& err) { + reset(); + err_ = err; + err_ = nullptr; + return *this; + } + + GError** operator&() { + return &err_; + } + + GError* operator->() { + return err_; + } + + const GError* operator->() const { + return err_; + } + + bool operator == (const GErrorPtr& other) const { + return err_ == other.err_; + } + + bool operator == (GError* err) const { + return err_ == err; + } + + bool operator != (std::nullptr_t) const { + return err_ != nullptr; + } + + operator bool() const { + return err_ != nullptr; + } + +private: + GError* err_; +}; + +} //namespace Fm + +#endif // GIOPTRS_H diff --git a/src/core/gobjectptr.h b/src/core/gobjectptr.h new file mode 100644 index 0000000..333bcc2 --- /dev/null +++ b/src/core/gobjectptr.h @@ -0,0 +1,104 @@ +#ifndef FM2_GOBJECTPTR_H +#define FM2_GOBJECTPTR_H + +#include "../libfmqtglobals.h" +#include +#include +#include +#include + +namespace Fm { + +template +class LIBFM_QT_API GObjectPtr { +public: + + explicit GObjectPtr(): gobj_{nullptr} { + } + + explicit GObjectPtr(T* gobj, bool add_ref = true): gobj_{gobj} { + if(gobj_ != nullptr && add_ref) + g_object_ref(gobj_); + } + + GObjectPtr(const GObjectPtr& other): gobj_{other.gobj_ ? reinterpret_cast(g_object_ref(other.gobj_)) : nullptr} { + } + + GObjectPtr(GObjectPtr&& other) noexcept: gobj_{other.release()} { + } + + ~GObjectPtr() { + if(gobj_ != nullptr) + g_object_unref(gobj_); + } + + T* get() const { + return gobj_; + } + + T* release() { + T* tmp = gobj_; + gobj_ = nullptr; + return tmp; + } + + void reset() { + if(gobj_ != nullptr) + g_object_unref(gobj_); + gobj_ = nullptr; + } + + GObjectPtr& operator = (const GObjectPtr& other) { + if (*this == other) + return *this; + + if(gobj_ != nullptr) + g_object_unref(gobj_); + gobj_ = other.gobj_ ? reinterpret_cast(g_object_ref(other.gobj_)) : nullptr; + return *this; + } + + GObjectPtr& operator = (GObjectPtr&& other) noexcept { + if (this == &other) + return *this; + + if(gobj_ != nullptr) + g_object_unref(gobj_); + gobj_ = other.release(); + return *this; + } + + GObjectPtr& operator = (T* gobj) { + if (*this == gobj) + return *this; + + if(gobj_ != nullptr) + g_object_unref(gobj_); + gobj_ = gobj ? reinterpret_cast(g_object_ref(gobj_)) : nullptr; + return *this; + } + + bool operator == (const GObjectPtr& other) const { + return gobj_ == other.gobj_; + } + + bool operator == (T* gobj) const { + return gobj_ == gobj; + } + + bool operator != (std::nullptr_t) const { + return gobj_ != nullptr; + } + + operator bool() const { + return gobj_ != nullptr; + } + +private: + mutable T* gobj_; +}; + + +} // namespace Fm + +#endif // FM2_GOBJECTPTR_H diff --git a/src/core/iconinfo.cpp b/src/core/iconinfo.cpp new file mode 100644 index 0000000..49c2eed --- /dev/null +++ b/src/core/iconinfo.cpp @@ -0,0 +1,176 @@ +#include "iconinfo.h" +#include "iconinfo_p.h" +#include + +namespace Fm { + +std::unordered_map, IconInfo::GIconHash, IconInfo::GIconEqual> IconInfo::cache_; +std::mutex IconInfo::mutex_; +QList IconInfo::fallbackQicons_; + +static const char* fallbackIconNames[] = { + "unknown", + "application-octet-stream", + "application-x-generic", + "text-x-generic", + nullptr +}; + +static QIcon getFirst(const QList & icons) +{ + for (const auto & icon : icons) { + if (!icon.isNull()) + return icon; + } + return QIcon{}; +} + +IconInfo::IconInfo(const char* name): + gicon_{g_themed_icon_new(name), false} { +} + +IconInfo::IconInfo(const GIconPtr gicon): + gicon_{std::move(gicon)} { +} + +IconInfo::~IconInfo() { +} + +// static +std::shared_ptr IconInfo::fromName(const char* name) { + GObjectPtr gicon{g_themed_icon_new(name), false}; + return fromGIcon(gicon); +} + +// static +std::shared_ptr IconInfo::fromGIcon(GIconPtr gicon) { + if(Q_LIKELY(gicon)) { + std::lock_guard lock{mutex_}; + auto it = cache_.find(gicon.get()); + if(it != cache_.end()) { + return it->second; + } + // not found in the cache, create a new entry for it. + auto icon = std::make_shared(std::move(gicon)); + cache_.insert(std::make_pair(icon->gicon_.get(), icon)); + return icon; + } + return std::shared_ptr{}; +} + +void IconInfo::updateQIcons() { + std::lock_guard lock{mutex_}; + for(auto& elem: cache_) { + auto& info = elem.second; + info->internalQicons_.clear(); + } +} + +QIcon IconInfo::qicon(const bool& transparent) const { + if(Q_LIKELY(!transparent)) { + if(Q_UNLIKELY(qicon_.isNull() && gicon_)) { + if(!G_IS_FILE_ICON(gicon_.get())) { + qicon_ = QIcon(new IconEngine{shared_from_this()}); + } + else { + qicon_ = getFirst(internalQicons_); + } + } + } + else { // transparent == true + if(Q_UNLIKELY(qiconTransparent_.isNull() && gicon_)) { + if(!G_IS_FILE_ICON(gicon_.get())) { + qiconTransparent_ = QIcon(new IconEngine{shared_from_this(), transparent}); + } + else { + qiconTransparent_ = getFirst(internalQicons_); + } + } + } + return !transparent ? qicon_ : qiconTransparent_; +} + +QList IconInfo::qiconsFromNames(const char* const* names) { + QList icons; + // qDebug("names: %p", names); + for(const gchar* const* name = names; *name; ++name) { + // qDebug("icon name=%s", *name); + icons.push_back(QIcon::fromTheme(*name)); + } + return icons; +} + +std::forward_list> IconInfo::emblems() const { + std::forward_list> result; + if(hasEmblems()) { + const GList* emblems_glist = g_emblemed_icon_get_emblems(G_EMBLEMED_ICON(gicon_.get())); + for(auto l = emblems_glist; l; l = l->next) { + auto gemblem = G_EMBLEM(l->data); + GIconPtr gemblem_icon{g_emblem_get_icon(gemblem), true}; + result.emplace_front(fromGIcon(gemblem_icon)); + } + result.reverse(); + } + return result; +} + +QIcon IconInfo::internalQicon() const { + QIcon ret_icon; + if(Q_UNLIKELY(internalQicons_.isEmpty())) { + GIcon* gicon = gicon_.get(); + if(G_IS_EMBLEMED_ICON(gicon_.get())) { + gicon = g_emblemed_icon_get_icon(G_EMBLEMED_ICON(gicon)); + } + if(G_IS_THEMED_ICON(gicon)) { + const gchar* const* names = g_themed_icon_get_names(G_THEMED_ICON(gicon)); + internalQicons_ = qiconsFromNames(names); + } + else if(G_IS_FILE_ICON(gicon)) { + GFile* file = g_file_icon_get_file(G_FILE_ICON(gicon)); + CStrPtr fpath{g_file_get_path(file)}; + internalQicons_.push_back(QIcon(fpath.get())); + } + + } + + ret_icon = getFirst(internalQicons_); + + // fallback to default icon + if(Q_UNLIKELY(ret_icon.isNull())) { + if(Q_UNLIKELY(fallbackQicons_.isEmpty())) { + fallbackQicons_ = qiconsFromNames(fallbackIconNames); + } + ret_icon = getFirst(fallbackQicons_); + } + return ret_icon; +} + +// compatibility function for leagcy libfm +// FIXME: deprecate this later. +extern "C" GIcon* _fm_icon_from_name(const char* name) { + GIcon* gicon = nullptr; + if(G_LIKELY(name)) { + gchar *dot; + if(g_path_is_absolute(name)) { + GFile* gicon_file = g_file_new_for_path(name); + gicon = g_file_icon_new(gicon_file); + g_object_unref(gicon_file); + } + else if(G_UNLIKELY((dot = strrchr((char*)name, '.')) != NULL && dot > name && + (g_ascii_strcasecmp(&dot[1], "png") == 0 + || g_ascii_strcasecmp(&dot[1], "svg") == 0 + || g_ascii_strcasecmp(&dot[1], "xpm") == 0))) { + /* some desktop entries have invalid icon name which contains + suffix so let strip the suffix from such invalid name */ + dot = g_strndup(name, dot - name); + gicon = g_themed_icon_new_with_default_fallbacks(dot); + g_free(dot); + } + else { + gicon = g_themed_icon_new_with_default_fallbacks(name); + } + } + return gicon; +} + +} // namespace Fm diff --git a/src/core/iconinfo.h b/src/core/iconinfo.h new file mode 100644 index 0000000..ad21f0c --- /dev/null +++ b/src/core/iconinfo.h @@ -0,0 +1,112 @@ +/* + * fm-icon.h + * + * Copyright 2009 Hong Jen Yee (PCMan) + * Copyright 2013 Andriy Grytsenko (LStranger) + * + * This file is a part of the Libfm library. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +#ifndef __FM2_ICON_INFO_H__ +#define __FM2_ICON_INFO_H__ + +#include "../libfmqtglobals.h" +#include +#include "gioptrs.h" +#include +#include +#include +#include +#include + + +namespace Fm { + +class LIBFM_QT_API IconInfo: public std::enable_shared_from_this { +public: + friend class IconEngine; + + explicit IconInfo() {} + + explicit IconInfo(const char* name); + + explicit IconInfo(const GIconPtr gicon); + + ~IconInfo(); + + static std::shared_ptr fromName(const char* name); + + static std::shared_ptr fromGIcon(GIconPtr gicon); + + static std::shared_ptr fromGIcon(GIcon* gicon) { + return fromGIcon(GIconPtr{gicon, true}); + } + + static void updateQIcons(); + + GIconPtr gicon() const { + return gicon_; + } + + QIcon qicon(const bool& transparent = false) const; + + bool hasEmblems() const { + return G_IS_EMBLEMED_ICON(gicon_.get()); + } + + std::forward_list> emblems() const; + + bool isValid() const { + return gicon_ != nullptr; + } + +private: + + static QList qiconsFromNames(const char* const* names); + + // actual QIcon loaded by QIcon::fromTheme + QIcon internalQicon() const; + + struct GIconHash { + std::size_t operator()(GIcon* gicon) const { + return g_icon_hash(gicon); + } + }; + + struct GIconEqual { + bool operator()(GIcon* gicon1, GIcon* gicon2) const { + return g_icon_equal(gicon1, gicon2); + } + }; + +private: + GIconPtr gicon_; + mutable QIcon qicon_; + mutable QIcon qiconTransparent_; + mutable QList internalQicons_; + + static std::unordered_map, GIconHash, GIconEqual> cache_; + static std::mutex mutex_; + static QList fallbackQicons_; +}; + +} // namespace Fm + +Q_DECLARE_METATYPE(std::shared_ptr) + +#endif /* __FM2_ICON_INFO_H__ */ diff --git a/src/core/iconinfo_p.h b/src/core/iconinfo_p.h new file mode 100644 index 0000000..d2c3109 --- /dev/null +++ b/src/core/iconinfo_p.h @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2016 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_ICONENGINE_H +#define FM_ICONENGINE_H + +#include +#include +#include "../libfmqtglobals.h" +#include "iconinfo.h" +#include + +namespace Fm { + +class IconEngine: public QIconEngine { +public: + + IconEngine(std::shared_ptr info, const bool& transparent = false); + + ~IconEngine(); + + virtual QSize actualSize(const QSize& size, QIcon::Mode mode, QIcon::State state) override; + + // not supported + virtual void addFile(const QString& /*fileName*/, const QSize& /*size*/, QIcon::Mode /*mode*/, QIcon::State /*state*/) override {} + + // not supported + virtual void addPixmap(const QPixmap& /*pixmap*/, QIcon::Mode /*mode*/, QIcon::State /*state*/) override {} + + virtual QIconEngine* clone() const override; + + virtual QString key() const override; + + virtual void paint(QPainter* painter, const QRect& rect, QIcon::Mode mode, QIcon::State state) override; + + virtual QPixmap pixmap(const QSize& size, QIcon::Mode mode, QIcon::State state) override; + + virtual void virtual_hook(int id, void* data) override; + +private: + std::weak_ptr info_; + bool transparent_; +}; + +IconEngine::IconEngine(std::shared_ptr info, const bool& transparent): + info_{info}, transparent_{transparent} { +} + +IconEngine::~IconEngine() { +} + +QSize IconEngine::actualSize(const QSize& size, QIcon::Mode mode, QIcon::State state) { + auto info = info_.lock(); + return info ? info->internalQicon().actualSize(size, mode, state) : QSize{}; +} + +QIconEngine* IconEngine::clone() const { + IconEngine* engine = new IconEngine(info_.lock()); + return engine; +} + +QString IconEngine::key() const { + return QStringLiteral("Fm::IconEngine"); +} + +void IconEngine::paint(QPainter* painter, const QRect& rect, QIcon::Mode mode, QIcon::State state) { + auto info = info_.lock(); + if(info) { + if(transparent_) { + painter->save(); + painter->setOpacity(0.45); + } + info->internalQicon().paint(painter, rect, Qt::AlignCenter, mode, state); + if(transparent_) { + painter->restore(); + } + } +} + +QPixmap IconEngine::pixmap(const QSize& size, QIcon::Mode mode, QIcon::State state) { + auto info = info_.lock(); + return info ? info->internalQicon().pixmap(size, mode, state) : QPixmap{}; +} + +void IconEngine::virtual_hook(int id, void* data) { + auto info = info_.lock(); + switch(id) { + case QIconEngine::AvailableSizesHook: { + auto* args = reinterpret_cast(data); + args->sizes = info ? info->internalQicon().availableSizes(args->mode, args->state) : QList{}; + break; + } + case QIconEngine::IconNameHook: { + QString* result = reinterpret_cast(data); + *result = info ? info->internalQicon().name() : QString{}; + break; + } +#if QT_VERSION >= QT_VERSION_CHECK(5, 7, 0) + case QIconEngine::IsNullHook: { + bool* result = reinterpret_cast(data); + *result = info ? info->internalQicon().isNull() : true; + break; + } +#endif +#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0) + case QIconEngine::ScaledPixmapHook: { + auto* arg = reinterpret_cast(data); + arg->pixmap = info ? info->internalQicon().pixmap(arg->size, arg->mode, arg->state) : QPixmap{}; + break; + } +#endif + } +} + +} // namespace Fm + +#endif // FM_ICONENGINE_H diff --git a/src/core/job.cpp b/src/core/job.cpp new file mode 100644 index 0000000..e597975 --- /dev/null +++ b/src/core/job.cpp @@ -0,0 +1,57 @@ +#include "job.h" +#include "job_p.h" + +namespace Fm { + +Job::Job(): + paused_{false}, + cancellable_{g_cancellable_new(), false}, + cancellableHandler_{g_signal_connect(cancellable_.get(), "cancelled", G_CALLBACK(_onCancellableCancelled), this)} { +} + +Job::~Job() { + if(cancellable_) { + g_cancellable_disconnect(cancellable_.get(), cancellableHandler_); + } +} + +void Job::runAsync(QThread::Priority priority) { + auto thread = new JobThread(this); + connect(thread, &QThread::finished, thread, &QThread::deleteLater); + if(autoDelete()) { + connect(this, &Job::finished, this, &Job::deleteLater); + } + thread->start(priority); +} + +void Job::cancel() { + g_cancellable_cancel(cancellable_.get()); +} + +void Job::run() { + exec(); + Q_EMIT finished(); +} + + +Job::ErrorAction Job::emitError(const GErrorPtr &err, Job::ErrorSeverity severity) { + ErrorAction response = ErrorAction::CONTINUE; + // if the error is already handled, don't emit it. + if(err.domain() == G_IO_ERROR && err.code() == G_IO_ERROR_FAILED_HANDLED) { + return response; + } + Q_EMIT error(err, severity, response); + + if(severity == ErrorSeverity::CRITICAL || response == ErrorAction::ABORT) { + cancel(); + } + else if(response == ErrorAction::RETRY ) { + /* If the job is already cancelled, retry is not allowed. */ + if(isCancelled() || (err.domain() == G_IO_ERROR && err.code() == G_IO_ERROR_CANCELLED)) { + response = ErrorAction::CONTINUE; + } + } + return response; +} + +} // namespace Fm diff --git a/src/core/job.h b/src/core/job.h new file mode 100644 index 0000000..944a436 --- /dev/null +++ b/src/core/job.h @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2016 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef __LIBFM_QT_FM_JOB_H__ +#define __LIBFM_QT_FM_JOB_H__ + +#include +#include +#include +#include +#include +#include +#include "gobjectptr.h" +#include "gioptrs.h" +#include "../libfmqtglobals.h" + + +namespace Fm { + +/* + * Fm::Job can be used in several different modes. + * 1. run with QThreadPool::start() + * 2. call runAsync(), which will create a new QThread and move the object to the thread. + * 3. create a new QThread, and connect the started() signal to the slot Job::run() + * 4. Directly call Job::run(), which executes synchrounously as a normal blocking call +*/ + +class LIBFM_QT_API Job: public QObject, public QRunnable { + Q_OBJECT +public: + + enum class ErrorAction{ + CONTINUE, + RETRY, + ABORT + }; + + enum class ErrorSeverity { + UNKNOWN, + WARNING, + MILD, + MODERATE, + SEVERE, + CRITICAL + }; + + explicit Job(); + + virtual ~Job(); + + bool isCancelled() const { + return g_cancellable_is_cancelled(cancellable_.get()); + } + + void runAsync(QThread::Priority priority = QThread::InheritPriority); + + bool pause(); + + void resume(); + + const GCancellablePtr& cancellable() const { + return cancellable_; + } + +Q_SIGNALS: + void cancelled(); + + void finished(); + + // this signal should be connected with Qt::BlockingQueuedConnection + void error(const GErrorPtr& err, ErrorSeverity severity, ErrorAction& response); + +public Q_SLOTS: + + void cancel(); + + void run() override; + +protected: + ErrorAction emitError(const GErrorPtr& err, ErrorSeverity severity = ErrorSeverity::MODERATE); + + // all derived job subclasses should do their work in this method. + virtual void exec() = 0; + +private: + static void _onCancellableCancelled(GCancellable* cancellable, Job* _this) { + _this->onCancellableCancelled(cancellable); + } + + void onCancellableCancelled(GCancellable* /*cancellable*/) { + Q_EMIT cancelled(); + } + +private: + bool paused_; + GCancellablePtr cancellable_; + gulong cancellableHandler_; +}; + + +} + +#endif // __LIBFM_QT_FM_JOB_H__ diff --git a/src/core/job_p.h b/src/core/job_p.h new file mode 100644 index 0000000..d893d5f --- /dev/null +++ b/src/core/job_p.h @@ -0,0 +1,26 @@ +#ifndef JOB_P_H +#define JOB_P_H + +#include +#include "job.h" + +namespace Fm { + +class JobThread: public QThread { + Q_OBJECT +public: + JobThread(Job* job): job_{job} { + } + +protected: + + void run() override { + job_->run(); + } + + Job* job_; +}; + +} // namespace Fm + +#endif // JOB_P_H diff --git a/src/core/legacy/fm-app-info.c b/src/core/legacy/fm-app-info.c new file mode 100644 index 0000000..921c6cb --- /dev/null +++ b/src/core/legacy/fm-app-info.c @@ -0,0 +1,523 @@ +/* + * fm-app-info.c + * + * Copyright 2010 Hong Jen Yee (PCMan) + * Copyright 2012-2015 Andriy Grytsenko (LStranger) + * + * This file is a part of the Libfm library. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/** + * SECTION:fm-app-info + * @short_description: FM application launch handlers + * @title: GAppInfo extensions + * + * @include: libfm/fm.h + * + */ + +#include "fm-app-info.h" +#include "fm-config.h" + +#include +#include + +static void append_file_to_cmd(GFile* gf, GString* cmd) +{ + char* file = g_file_get_path(gf); + char* quote; + if(file == NULL) /* trash:// gvfs is incomplete in resolving it */ + { + /* we can retrieve real path from GVFS >= 1.13.3 */ + if(g_file_has_uri_scheme(gf, "trash")) + { + GFileInfo *inf; + const char *orig_path; + + inf = g_file_query_info(gf, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI, + G_FILE_QUERY_INFO_NONE, NULL, NULL); + if(inf == NULL) /* failed */ + return; + orig_path = g_file_info_get_attribute_string(inf, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI); + if(orig_path != NULL) /* success */ + file = g_filename_from_uri(orig_path, NULL, NULL); + g_object_unref(inf); + } + if(file == NULL) + return; + } + quote = g_shell_quote(file); + g_string_append(cmd, quote); + g_string_append_c(cmd, ' '); + g_free(quote); + g_free(file); +} + +static void append_uri_to_cmd(GFile* gf, GString* cmd) +{ + char* uri = NULL; + if(!g_file_has_uri_scheme(gf, "file")) + { + /* When gvfs-fuse is in use, try to convert all URIs that are not file:// to + * their corresponding FUSE-mounted local paths. GDesktopAppInfo internally does this, too. + * With this, non-gtk+ applications can correctly open files in gvfs-mounted remote filesystems. + */ + char* path = g_file_get_path(gf); + if(path) + { + uri = g_filename_to_uri(path, NULL, NULL); + g_free(path); + } + } + if(!uri) + { + uri = g_file_get_uri(gf); + } + + if(uri) + { + char* quote = g_shell_quote(uri); + g_string_append(cmd, quote); + g_string_append_c(cmd, ' '); + g_free(quote); + g_free(uri); + } +} + +static char* expand_exec_macros(GAppInfo* app, const char* full_desktop_path, + GKeyFile* kf, GList** gfiles, GList **launching) +{ + GString* cmd; + const char* exec = g_app_info_get_commandline(app); + const char* p; + GFile *file = NULL; + GList *fl = NULL; + + if (exec == NULL) + return NULL; + cmd = g_string_sized_new(1024); + for(p = exec; *p; ++p) + { + if(*p == '%') + { + ++p; + if(!*p) + break; + switch(*p) + { + case 'f': + if (file == NULL && *gfiles) + { + *launching = *gfiles; + file = G_FILE((*gfiles)->data); + *gfiles = g_list_remove_link(*gfiles, *gfiles); + } + if (file) + append_file_to_cmd(file, cmd); + break; + case 'F': + if (*gfiles) + *launching = fl = *gfiles; + *gfiles = NULL; + g_list_foreach(fl, (GFunc)append_file_to_cmd, cmd); + break; + case 'u': + if (file == NULL && *gfiles) + { + *launching = *gfiles; + file = G_FILE((*gfiles)->data); + *gfiles = g_list_remove_link(*gfiles, *gfiles); + } + if (file) + append_uri_to_cmd(file, cmd); + break; + case 'U': + if (*gfiles) + *launching = fl = *gfiles; + *gfiles = NULL; + g_list_foreach(fl, (GFunc)append_uri_to_cmd, cmd); + break; + case '%': + g_string_append_c(cmd, '%'); + break; + case 'i': + if (kf == NULL) + break; + { + char* icon_name = g_key_file_get_locale_string(kf, "Desktop Entry", + "Icon", NULL, NULL); + if(icon_name) + { + g_string_append(cmd, "--icon "); + g_string_append(cmd, icon_name); + g_free(icon_name); + } + break; + } + case 'c': + { + const char* name = g_app_info_get_name(app); + if(name) + { + char *quoted = g_shell_quote(name); + g_string_append(cmd, quoted); + g_free(quoted); + } + break; + } + case 'k': + /* append the file path of the desktop file */ + if(full_desktop_path) + { + /* FIXME: how about quoting here? */ + char* desktop_location = g_path_get_dirname(full_desktop_path); + g_string_append(cmd, desktop_location); + g_free(desktop_location); + } + break; + } + } + else + g_string_append_c(cmd, *p); + } + + /* if files are provided but the Exec key doesn't contain %f, %F, %u, or %U */ + if(*gfiles && !*launching) + { + /* treat as %f */ + *launching = *gfiles; + file = G_FILE((*gfiles)->data); + *gfiles = g_list_remove_link(*gfiles, *gfiles); + g_string_append_c(cmd, ' '); + append_file_to_cmd(file, cmd); + } + + return g_string_free(cmd, FALSE); +} + +struct ChildSetup +{ + char* display; + char* sn_id; + pid_t pgid; +}; + +static void child_setup(gpointer user_data) +{ + struct ChildSetup* data = (struct ChildSetup*)user_data; + if(data->display) + g_setenv ("DISPLAY", data->display, TRUE); + if(data->sn_id) + g_setenv ("DESKTOP_STARTUP_ID", data->sn_id, TRUE); + /* Move child to grandparent group so it will not die with parent */ + setpgid(0, data->pgid); +} + +static void child_watch(GPid pid, gint status, gpointer user_data) +{ + /* + * Ensure that we don't double fork and break pkexec + */ + g_spawn_close_pid(pid); +} + +// defined in terminal.cpp +char* expand_terminal(char* cmd, gboolean keep_open, GError** error); + + +static gboolean do_launch(GAppInfo* appinfo, const char* full_desktop_path, + GKeyFile* kf, GList** inp, GAppLaunchContext* ctx, + GError** err) +{ + gboolean ret = FALSE; + GList *gfiles = NULL; + char* cmd, *path; + char** argv; + int argc; + gboolean use_terminal; + GAppInfoCreateFlags flags; + GPid pid; + + cmd = expand_exec_macros(appinfo, full_desktop_path, kf, inp, &gfiles); + g_print("%s\n", cmd); + if (cmd == NULL || cmd[0] == '\0') + { + g_free(cmd); + /* FIXME: localize the string below in 1.3.0 */ + g_set_error_literal(err, G_IO_ERROR, G_IO_ERROR_FAILED, + "Desktop entry contains no valid Exec line"); + return FALSE; + } + /* FIXME: do check for TryExec/Exec */ + if(G_LIKELY(kf)) + use_terminal = g_key_file_get_boolean(kf, "Desktop Entry", "Terminal", NULL); + else + { + flags = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(appinfo), "flags")); + use_terminal = (flags & G_APP_INFO_CREATE_NEEDS_TERMINAL) != 0; + } + + if(use_terminal) + { + /* FIXME: is it right key to mark this option? */ + gboolean keep_open = FALSE; + char* term_cmd; + + if(G_LIKELY(kf)) + keep_open = g_key_file_get_boolean(kf, "Desktop Entry", + "X-KeepTerminal", NULL); + term_cmd = expand_terminal(cmd, keep_open, err); + g_free(cmd); + if(!term_cmd) + { + g_list_free(gfiles); + return FALSE; + } + cmd = term_cmd; + } + + g_debug("launch command: <%s>", cmd); + if(g_shell_parse_argv(cmd, &argc, &argv, err)) + { + struct ChildSetup data; + if(ctx) + { + gboolean use_sn; + if(G_LIKELY(kf) && g_key_file_has_key(kf, "Desktop Entry", "StartupNotify", NULL)) + use_sn = g_key_file_get_boolean(kf, "Desktop Entry", "StartupNotify", NULL); + else if(fm_config->force_startup_notify) + { + /* if the app doesn't explicitly ask us not to use sn, + * and fm_config->force_startup_notify is TRUE, then + * use it by default, unless it's a console app. */ + use_sn = !use_terminal; /* we only use sn for GUI apps by default */ + /* FIXME: console programs should use sn_id of terminal emulator instead. */ + } + else + use_sn = FALSE; + data.display = g_app_launch_context_get_display(ctx, appinfo, gfiles); + + if(use_sn) + data.sn_id = g_app_launch_context_get_startup_notify_id(ctx, appinfo, gfiles); + else + data.sn_id = NULL; + } + else + { + data.display = NULL; + data.sn_id = NULL; + } + g_debug("sn_id = %s", data.sn_id); + + if(G_LIKELY(kf)) + path = g_key_file_get_string(kf, "Desktop Entry", "Path", NULL); + else + path = NULL; + + data.pgid = getpgid(getppid()); + /* only absolute path is usable, ignore others */ + ret = g_spawn_async((path && path[0] == '/') ? path : NULL, argv, NULL, + G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD, + child_setup, &data, &pid, err); + if (ret) + /* Ensure that we don't double fork and break pkexec */ + g_child_watch_add(pid, child_watch, NULL); + else if (data.sn_id) + /* Notify launch context about failure */ + g_app_launch_context_launch_failed(ctx, data.sn_id); + + g_free(path); + g_free(data.display); + g_free(data.sn_id); + + g_strfreev(argv); + } + g_free(cmd); + g_list_free(gfiles); + return ret; +} + +/** + * fm_app_info_launch + * @appinfo: application info to launch + * @files: (element-type GFile): files to use in run substitutions + * @launch_context: (allow-none): a launch context + * @error: (out) (allow-none): location to store error + * + * Launches desktop application doing substitutions in application info. + * + * Returns: %TRUE if application was launched. + * + * Since: 0.1.15 + */ +gboolean fm_app_info_launch(GAppInfo *appinfo, GList *files, + GAppLaunchContext *launch_context, GError **error) +{ + gboolean supported = FALSE, ret = FALSE; + GList *launch_list = g_list_copy(files); + if(G_IS_DESKTOP_APP_INFO(appinfo)) + { + const char *id; + +#if GLIB_CHECK_VERSION(2,24,0) + /* if GDesktopAppInfo knows the filename then let use it */ + id = g_desktop_app_info_get_filename(G_DESKTOP_APP_INFO(appinfo)); + if(id) /* this is a desktop entry file */ + { + /* load the desktop entry file to obtain more info */ + GKeyFile* kf = g_key_file_new(); + supported = g_key_file_load_from_file(kf, id, 0, NULL); + if(supported) do { + ret = do_launch(appinfo, id, kf, &launch_list, launch_context, error); + } while (launch_list && ret); + g_key_file_free(kf); + id = NULL; + } + else /* otherwise try application id */ +#endif + id = g_app_info_get_id(appinfo); + if(id) /* this is an installed application */ + { + /* load the desktop entry file to obtain more info */ + GKeyFile* kf = g_key_file_new(); + char* rel_path = g_strconcat("applications/", id, NULL); + char* full_desktop_path; + supported = g_key_file_load_from_data_dirs(kf, rel_path, + &full_desktop_path, 0, NULL); + g_free(rel_path); + if(supported) + { + do { + ret = do_launch(appinfo, full_desktop_path, kf, &launch_list, + launch_context, error); + } while (launch_list && ret); + g_free(full_desktop_path); + } + g_key_file_free(kf); + } + else + { +#if GLIB_CHECK_VERSION(2,24,0) + if (!supported) /* it was launched otherwise, see above */ +#endif + { + /* If this is created with fm_app_info_create_from_commandline() */ + if(g_object_get_data(G_OBJECT(appinfo), "flags")) + { + supported = TRUE; + do { + ret = do_launch(appinfo, NULL, NULL, &launch_list, + launch_context, error); + } while (launch_list && ret); + } + } + } + } + else + supported = FALSE; + g_list_free(launch_list); + + if(!supported) /* fallback to GAppInfo::launch */ + return g_app_info_launch(appinfo, files, launch_context, error); + return ret; +} + +/** + * fm_app_info_launch_uris + * @appinfo: application info to launch + * @uris: (element-type char *): URIs to use in run substitutions + * @launch_context: (allow-none): a launch context + * @error: (out) (allow-none): location to store error + * + * Launches desktop application doing substitutions in application info. + * + * Returns: %TRUE if application was launched. + * + * Since: 0.1.15 + */ +gboolean fm_app_info_launch_uris(GAppInfo *appinfo, GList *uris, + GAppLaunchContext *launch_context, GError **error) +{ + GList* gfiles = NULL; + gboolean ret; + + for(;uris; uris = uris->next) + { + GFile* gf = g_file_new_for_uri((char*)uris->data); + if(gf) + gfiles = g_list_prepend(gfiles, gf); + } + + gfiles = g_list_reverse(gfiles); + ret = fm_app_info_launch(appinfo, gfiles, launch_context, error); + + g_list_foreach(gfiles, (GFunc)g_object_unref, NULL); + g_list_free(gfiles); + return ret; +} + +/** + * fm_app_info_launch_default_for_uri + * @uri: the uri to show + * @launch_context: (allow-none): a launch context + * @error: (out) (allow-none): location to store error + * + * Utility function that launches the default application + * registered to handle the specified uri. Synchronous I/O + * is done on the uri to detect the type of the file if + * required. + * + * Returns: %TRUE if application was launched. + * + * Since: 0.1.15 + */ +gboolean fm_app_info_launch_default_for_uri(const char *uri, + GAppLaunchContext *launch_context, + GError **error) +{ + /* FIXME: implement this */ + return g_app_info_launch_default_for_uri(uri, launch_context, error); +} + +/** + * fm_app_info_create_from_commandline + * @commandline: the commandline to use + * @application_name: (allow-none): the application name, or %NULL to use @commandline + * @flags: flags that can specify details of the created #GAppInfo + * @error: (out) (allow-none): location to store error + * + * Creates a new #GAppInfo from the given information. + * + * Note that for @commandline, the quoting rules of the Exec key of the + * freedesktop.org Desktop + * Entry Specification are applied. For example, if the @commandline contains + * percent-encoded URIs, the percent-character must be doubled in order to prevent it from + * being swallowed by Exec key unquoting. See the specification for exact quoting rules. + * + * Returns: (transfer full): new #GAppInfo for given command. + * + * Since: 0.1.15 + */ +GAppInfo* fm_app_info_create_from_commandline(const char *commandline, + const char *application_name, + GAppInfoCreateFlags flags, + GError **error) +{ + GAppInfo* app = g_app_info_create_from_commandline(commandline, application_name, flags, error); + g_object_set_data(G_OBJECT(app), "flags", GUINT_TO_POINTER(flags)); + return app; +} diff --git a/src/core/legacy/fm-app-info.h b/src/core/legacy/fm-app-info.h new file mode 100644 index 0000000..bae52c5 --- /dev/null +++ b/src/core/legacy/fm-app-info.h @@ -0,0 +1,47 @@ +/* + * fm-app-info.h + * + * Copyright 2010 Hong Jen Yee (PCMan) + * + * This file is a part of the Libfm library. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef __FM_APP_INFO_H__ +#define __FM_APP_INFO_H__ + +#include + +G_BEGIN_DECLS + +gboolean fm_app_info_launch(GAppInfo *appinfo, GList *files, + GAppLaunchContext *launch_context, GError **error); + +gboolean fm_app_info_launch_uris(GAppInfo *appinfo, GList *uris, + GAppLaunchContext *launch_context, GError **error); + +gboolean fm_app_info_launch_default_for_uri(const char *uri, + GAppLaunchContext *launch_context, + GError **error); + +GAppInfo* fm_app_info_create_from_commandline(const char *commandline, + const char *application_name, + GAppInfoCreateFlags flags, + GError **error); + +G_END_DECLS + +#endif /* __FM_APP_INFO_H__ */ diff --git a/src/core/legacy/fm-config.c b/src/core/legacy/fm-config.c new file mode 100644 index 0000000..6e0522a --- /dev/null +++ b/src/core/legacy/fm-config.c @@ -0,0 +1,88 @@ +/* + * fm-config.c + * + * Copyright 2009 PCMan + * Copyright 2009 Juergen Hoetzel + * Copyright 2012-2014 Andriy Grytsenko (LStranger) + * Copyright 2016 Mamoru TASAKA + * + * This file is a part of the Libfm library. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/** + * SECTION:fm-config + * @short_description: Configuration file support for applications that use libfm. + * @title: FmConfig + * + * @include: libfm/fm.h + * + * The #FmConfig represents basic configuration options that are used by + * libfm classes and methods. Methods of class #FmConfig allow use either + * default file (~/.config/libfm/libfm.conf) or another one to load the + * configuration and to save it. + */ + +#include "fm-config.h" + +/* global config object */ +static FmConfig globalConfig_; +FmConfig* fm_config = &globalConfig_; + +void fm_config_init() { + FmConfig *self = fm_config; + self->single_click = FM_CONFIG_DEFAULT_SINGLE_CLICK; + self->auto_selection_delay = FM_CONFIG_DEFAULT_AUTO_SELECTION_DELAY; + self->use_trash = FM_CONFIG_DEFAULT_USE_TRASH; + self->confirm_del = FM_CONFIG_DEFAULT_CONFIRM_DEL; + self->confirm_trash = FM_CONFIG_DEFAULT_CONFIRM_TRASH; + self->big_icon_size = FM_CONFIG_DEFAULT_BIG_ICON_SIZE; + self->small_icon_size = FM_CONFIG_DEFAULT_SMALL_ICON_SIZE; + self->pane_icon_size = FM_CONFIG_DEFAULT_PANE_ICON_SIZE; + self->thumbnail_size = FM_CONFIG_DEFAULT_THUMBNAIL_SIZE; + self->show_thumbnail = FM_CONFIG_DEFAULT_SHOW_THUMBNAIL; + self->thumbnail_local = FM_CONFIG_DEFAULT_THUMBNAIL_LOCAL; + self->thumbnail_max = FM_CONFIG_DEFAULT_THUMBNAIL_MAX; + /* show_internal_volumes defaulted to FALSE */ + /* si_unit defaulted to FALSE */ + /* terminal and archiver defaulted to NULL */ + /* drop_default_action defaulted to 0 */ + /* modules_blacklist and modules_whitelist defaulted to NULL */ + /* format_cmd defaulted to NULL */ + /* list_view_size_units defaulted to NULL */ + /* saved_search defaulted to NULL */ + self->advanced_mode = FALSE; + self->force_startup_notify = FM_CONFIG_DEFAULT_FORCE_S_NOTIFY; + self->backup_as_hidden = FM_CONFIG_DEFAULT_BACKUP_HIDDEN; + self->no_usb_trash = FM_CONFIG_DEFAULT_NO_USB_TRASH; + self->no_child_non_expandable = FM_CONFIG_DEFAULT_NO_EXPAND_EMPTY; + self->show_full_names = FM_CONFIG_DEFAULT_SHOW_FULL_NAMES; + self->shadow_hidden = FM_CONFIG_DEFAULT_SHADOW_HIDDEN; + self->only_user_templates = FM_CONFIG_DEFAULT_ONLY_USER_TEMPLATES; + self->template_run_app = FM_CONFIG_DEFAULT_TEMPLATE_RUN_APP; + self->template_type_once = FM_CONFIG_DEFAULT_TEMPL_TYPE_ONCE; + self->defer_content_test = FM_CONFIG_DEFAULT_DEFER_CONTENT_TEST; + self->quick_exec = FM_CONFIG_DEFAULT_QUICK_EXEC; + self->places_home = FM_CONFIG_DEFAULT_PLACES_HOME; + self->places_desktop = FM_CONFIG_DEFAULT_PLACES_DESKTOP; + self->places_root = FM_CONFIG_DEFAULT_PLACES_ROOT; + self->places_computer = FM_CONFIG_DEFAULT_PLACES_COMPUTER; + self->places_trash = FM_CONFIG_DEFAULT_PLACES_TRASH; + self->places_applications = FM_CONFIG_DEFAULT_PLACES_APPLICATIONS; + self->places_network = FM_CONFIG_DEFAULT_PLACES_NETWORK; + self->places_unmounted = FM_CONFIG_DEFAULT_PLACES_UNMOUNTED; + self->smart_desktop_autodrop = FM_CONFIG_DEFAULT_SMART_DESKTOP_AUTODROP; +} diff --git a/src/core/legacy/fm-config.h b/src/core/legacy/fm-config.h new file mode 100644 index 0000000..ec834f6 --- /dev/null +++ b/src/core/legacy/fm-config.h @@ -0,0 +1,199 @@ +/* + * fm-config.h + * + * Copyright 2009 PCMan + * Copyright 2009 Juergen Hoetzel + * Copyright 2012-2014 Andriy Grytsenko (LStranger) + * + * This file is a part of the Libfm library. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +#ifndef __FM_CONFIG_H__ +#define __FM_CONFIG_H__ + +#include +#include + +G_BEGIN_DECLS + +typedef struct _FmConfig FmConfig; + +#define FM_CONFIG_DEFAULT_SINGLE_CLICK FALSE +#define FM_CONFIG_DEFAULT_USE_TRASH TRUE +#define FM_CONFIG_DEFAULT_CONFIRM_DEL TRUE +#define FM_CONFIG_DEFAULT_CONFIRM_TRASH TRUE +#define FM_CONFIG_DEFAULT_NO_USB_TRASH TRUE + +#define FM_CONFIG_DEFAULT_BIG_ICON_SIZE 48 +#define FM_CONFIG_DEFAULT_SMALL_ICON_SIZE 16 +#define FM_CONFIG_DEFAULT_PANE_ICON_SIZE 16 +#define FM_CONFIG_DEFAULT_THUMBNAIL_SIZE 128 + +#define FM_CONFIG_DEFAULT_SHOW_THUMBNAIL TRUE +#define FM_CONFIG_DEFAULT_THUMBNAIL_LOCAL TRUE +#define FM_CONFIG_DEFAULT_THUMBNAIL_MAX 2048 + +#define FM_CONFIG_DEFAULT_FORCE_S_NOTIFY TRUE +#define FM_CONFIG_DEFAULT_BACKUP_HIDDEN TRUE +#define FM_CONFIG_DEFAULT_NO_EXPAND_EMPTY FALSE +#define FM_CONFIG_DEFAULT_SHOW_FULL_NAMES FALSE +#define FM_CONFIG_DEFAULT_ONLY_USER_TEMPLATES FALSE +#define FM_CONFIG_DEFAULT_TEMPLATE_RUN_APP FALSE +#define FM_CONFIG_DEFAULT_TEMPL_TYPE_ONCE FALSE +#define FM_CONFIG_DEFAULT_SHADOW_HIDDEN FALSE +#define FM_CONFIG_DEFAULT_DEFER_CONTENT_TEST FALSE +#define FM_CONFIG_DEFAULT_QUICK_EXEC FALSE +#define FM_CONFIG_DEFAULT_SMART_DESKTOP_AUTODROP TRUE + +#define FM_CONFIG_DEFAULT_PLACES_HOME TRUE +#define FM_CONFIG_DEFAULT_PLACES_DESKTOP TRUE +#define FM_CONFIG_DEFAULT_PLACES_ROOT FALSE +#define FM_CONFIG_DEFAULT_PLACES_COMPUTER FALSE +#define FM_CONFIG_DEFAULT_PLACES_TRASH TRUE +#define FM_CONFIG_DEFAULT_PLACES_APPLICATIONS TRUE +#define FM_CONFIG_DEFAULT_PLACES_NETWORK FALSE +#define FM_CONFIG_DEFAULT_PLACES_UNMOUNTED TRUE + +#define FM_CONFIG_DEFAULT_AUTO_SELECTION_DELAY 600 + + +/** + * FmConfig: + * @terminal: command line to launch terminal emulator + * @archiver: desktop_id of the archiver used + * @big_icon_size: size of big icons + * @small_icon_size: size of small icons + * @pane_icon_size: size of side pane icons + * @thumbnail_size: size of thumbnail icons + * @thumbnail_max: show thumbnails only for files not bigger than this, in KB or Kpix + * @auto_selection_delay: (since 1.2.0) delay for autoselection in single-click mode, in ms + * @drop_default_action: (since 1.2.0) default action on drop (see #FmDndDestDropAction) + * @single_click: single click to open file + * @use_trash: delete file to trash can + * @confirm_del: ask before deleting files + * @confirm_trash: (since 1.2.0) ask before moving files to trash can + * @show_thumbnail: show thumbnails + * @thumbnail_local: show thumbnails for local files only + * @show_internal_volumes: show system internal volumes in side pane. (udisks-only) + * @si_unit: use SI prefix for file sizes + * @advanced_mode: enable advanced features for experienced user + * @force_startup_notify: (since 1.0.1) use startup notify by default + * @backup_as_hidden: (since 1.0.1) treat backup files as hidden + * @no_usb_trash: (since 1.0.1) don't create trash folder on removable media + * @no_child_non_expandable: (since 1.0.1) hide expanders on empty folder + * @show_full_names: (since 1.2.0) always show full names in Icon View mode + * @shadow_hidden: (since 1.2.0) show icons of hidden files shadowed in the view + * @places_home: (since 1.2.0) show 'Home' item in Places + * @places_desktop: (since 1.2.0) show 'Desktop' item in Places + * @places_applications: (since 1.2.0) show 'Applications' item in Places + * @places_trash: (since 1.2.0) show 'Trash' item in Places + * @places_root: (since 1.2.0) show '/' item in Places + * @places_computer: (since 1.2.0) show 'My computer' item in Places + * @places_network: (since 1.2.0) show 'Network' item in Places + * @places_unmounted: (since 1.2.0) show unmounted internal volumes in Places + * @only_user_templates: (since 1.2.0) show only user defined templates in 'Create...' menu + * @template_run_app: (since 1.2.0) run default application after creation from template + * @template_type_once: (since 1.2.0) use only one template of each MIME type + * @defer_content_test: (since 1.2.0) defer test for content type on folder loading + * @quick_exec: (since 1.2.0) don't ask user for action on executable launch + * @modules_blacklist: (since 1.2.0) list of modules (mask in form "type:name") to never load + * @modules_whitelist: (since 1.2.0) list of excemptions from @modules_blacklist + * @list_view_size_units: (since 1.2.0) file size units in list view: h, k, M, G + * @format_cmd: (since 1.2.0) command to format the volume (device will be added) + * @smart_desktop_autodrop: (since 1.2.0) enable "smart shortcut" auto-action for ~/Desktop + * @saved_search: (since 1.2.0) internal saved data of fm_launch_search_simple() + */ +struct _FmConfig +{ + /*< private >*/ + GObject parent; + char *_cfg_name; + + /*< public >*/ + char* terminal; + char* archiver; + + gint big_icon_size; + gint small_icon_size; + gint pane_icon_size; + gint thumbnail_size; + gint thumbnail_max; + gint auto_selection_delay; + gint drop_default_action; + + gboolean single_click; + gboolean use_trash; + gboolean confirm_del; + gboolean confirm_trash; + gboolean show_thumbnail; + gboolean thumbnail_local; + gboolean show_internal_volumes; + gboolean si_unit; + gboolean advanced_mode; + gboolean force_startup_notify; + gboolean backup_as_hidden; + gboolean no_usb_trash; + gboolean no_child_non_expandable; + gboolean show_full_names; + gboolean shadow_hidden; + + gboolean places_home; + gboolean places_desktop; + gboolean places_applications; + gboolean places_trash; + gboolean places_root; + gboolean places_computer; + gboolean places_network; + gboolean places_unmounted; + + gboolean only_user_templates; + gboolean template_run_app; + gboolean template_type_once; + gboolean defer_content_test; + gboolean quick_exec; + + gchar **modules_blacklist; + gchar **modules_whitelist; + /*< private >*/ + gchar **system_modules_blacklist; /* concatenated from system, don't save! */ + /*< public >*/ + + gchar *list_view_size_units; + gchar *format_cmd; + + gboolean smart_desktop_autodrop; + gchar *saved_search; + /*< private >*/ + gpointer _reserved1; /* reserved space for updates until next ABI */ + gpointer _reserved2; + gpointer _reserved3; + gpointer _reserved4; + gpointer _reserved5; + gpointer _reserved6; + gpointer _reserved7; + GFileMonitor *_cfg_mon; +}; + +/* global config object */ +G_MODULE_EXPORT extern FmConfig* fm_config; + +void fm_config_init(); + +G_END_DECLS + +#endif /* __FM_CONFIG_H__ */ diff --git a/src/core/legacy/glib-compat.h b/src/core/legacy/glib-compat.h new file mode 100644 index 0000000..61b17f7 --- /dev/null +++ b/src/core/legacy/glib-compat.h @@ -0,0 +1,86 @@ +/* + * glib-compat.h + * + * Copyright 2011 Hong Jen Yee (PCMan) + * + * This file is a part of the Libfm library. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef __GLIB_COMPAT_H__ +#define __GLIB_COMPAT_H__ +#include +#include + +G_BEGIN_DECLS + +/* GLib prior 2.24 have no such macro */ +#ifndef G_DEFINE_INTERFACE +# define G_DEFINE_INTERFACE(TN, t_n, T_P) \ +static void t_n##_default_init (TN##Interface *klass); \ +GType t_n##_get_type (void) \ +{ \ + static volatile gsize g_define_type_id__volatile = 0; \ + if (g_once_init_enter (&g_define_type_id__volatile)) \ + { \ + GType g_define_type_id = \ + g_type_register_static_simple (G_TYPE_INTERFACE, \ + g_intern_static_string (#TN), \ + sizeof (TN##Interface), \ + (GClassInitFunc)t_n##_default_init, \ + 0, \ + (GInstanceInitFunc)NULL, \ + (GTypeFlags) 0); \ + if (T_P) \ + g_type_interface_add_prerequisite (g_define_type_id, T_P); \ + g_once_init_leave (&g_define_type_id__volatile, g_define_type_id); \ + } \ + return g_define_type_id__volatile; \ +} /* closes t_n##_get_type() */ +#endif /* G_DEFINE_INTERFACE */ + +#if !GLIB_CHECK_VERSION(2, 28, 0) + +/* This API was added in glib 2.28 */ + +#define g_slist_free_full(slist, free_func) \ +{ \ +g_slist_foreach(slist, (GFunc)free_func, NULL); \ +g_slist_free(slist); \ +} + +#define g_list_free_full(list, free_func) \ +{ \ +g_list_foreach(list, (GFunc)free_func, NULL); \ +g_list_free(list); \ +} + +#endif + +#if !GLIB_CHECK_VERSION(2, 34, 0) +/* This useful API was added in glib 2.34 */ +static inline GSList *g_slist_copy_deep(GSList *list, GCopyFunc func, gpointer user_data) +{ + GSList *new_list = g_slist_copy(list), *l; + for(l = new_list; l; l = l->next) + l->data = func(l->data, user_data); + return new_list; +} +#endif + +G_END_DECLS + +#endif diff --git a/src/core/mimetype.cpp b/src/core/mimetype.cpp new file mode 100644 index 0000000..19345bb --- /dev/null +++ b/src/core/mimetype.cpp @@ -0,0 +1,64 @@ +#include "mimetype.h" +#include + +#include +#include + +using namespace std; + +namespace Fm { + +std::unordered_map, CStrHash, CStrEqual> MimeType::cache_; +std::mutex MimeType::mutex_; + +std::shared_ptr MimeType::inodeDirectory_; // inode/directory +std::shared_ptr MimeType::inodeShortcut_; // inode/x-shortcut +std::shared_ptr MimeType::inodeMountPoint_; // inode/mount-point +std::shared_ptr MimeType::desktopEntry_; // application/x-desktop + + +MimeType::MimeType(const char* typeName): + name_{g_strdup(typeName)}, + desc_{nullptr} { + + GObjectPtr gicon{g_content_type_get_icon(typeName), false}; + if(strcmp(typeName, "inode/directory") == 0) + g_themed_icon_prepend_name(G_THEMED_ICON(gicon.get()), "folder"); + else if(g_content_type_can_be_executable(typeName)) + g_themed_icon_append_name(G_THEMED_ICON(gicon.get()), "application-x-executable"); + + icon_ = IconInfo::fromGIcon(gicon); +} + +MimeType::~MimeType () { +} + +//static +std::shared_ptr MimeType::fromName(const char* typeName) { + std::shared_ptr ret; + std::lock_guard lock(mutex_); + auto it = cache_.find(typeName); + if(it == cache_.end()) { + ret = std::make_shared(typeName); + cache_.insert(std::make_pair(ret->name_.get(), ret)); + } + else { + ret = it->second; + } + return ret; +} + +// static +std::shared_ptr MimeType::guessFromFileName(const char* fileName) { + gboolean uncertain; + /* let skip scheme and host from non-native names */ + auto uri_scheme = g_strstr_len(fileName, -1, "://"); + if(uri_scheme) + fileName = strchr(uri_scheme + 3, '/'); + if(fileName == nullptr) + fileName = "unknown"; + auto type = CStrPtr{g_content_type_guess(fileName, nullptr, 0, &uncertain)}; + return fromName(type.get()); +} + +} // namespace Fm diff --git a/src/core/mimetype.h b/src/core/mimetype.h new file mode 100644 index 0000000..887ce08 --- /dev/null +++ b/src/core/mimetype.h @@ -0,0 +1,172 @@ +/* + * fm-mime-type.h + * + * Copyright 2009 - 2012 Hong Jen Yee (PCMan) + * + * This file is a part of the Libfm library. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef _FM2_MIME_TYPE_H_ +#define _FM2_MIME_TYPE_H_ + +#include "../libfmqtglobals.h" +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "cstrptr.h" +#include "gobjectptr.h" +#include "iconinfo.h" +#include "thumbnailer.h" + +namespace Fm { + +class LIBFM_QT_API MimeType { +public: + friend class Thumbnailer; + + explicit MimeType(const char* typeName); + + MimeType() = delete; + + ~MimeType(); + + std::shared_ptr firstThumbnailer() const { + std::lock_guard lock{mutex_}; + return thumbnailers_.empty() ? nullptr : thumbnailers_.front(); + } + + void forEachThumbnailer(std::function&)> func) const { + std::lock_guard lock{mutex_}; + for(auto& thumbnailer: thumbnailers_) { + if(func(thumbnailer)) { + break; + } + } + } + + const std::shared_ptr& icon() const { + return icon_; + } + + const char* name() const { + return name_.get(); + } + + const char* desc() const { + if(!desc_) { + desc_ = CStrPtr{g_content_type_get_description(name_.get())}; + } + return desc_.get(); + } + + static std::shared_ptr fromName(const char* typeName); + + static std::shared_ptr guessFromFileName(const char* fileName); + + bool isUnknownType() const { + return g_content_type_is_unknown(name_.get()); + } + + bool isDesktopEntry() const { + return this == desktopEntry().get(); + } + + bool isText() const { + return g_content_type_is_a(name_.get(), "text/plain"); + } + + bool isImage() const { + return !std::strncmp("image/", name_.get(), 6); + } + + bool isMountable() const { + return this == inodeMountPoint().get(); + } + + bool isShortcut() const { + return this == inodeShortcut().get(); + } + + bool isDir() const { + return this == inodeDirectory().get(); + } + + bool canBeExecutable() const { + return g_content_type_can_be_executable(name_.get()); + } + + static std::shared_ptr inodeDirectory() { // inode/directory + if(!inodeDirectory_) + inodeDirectory_ = fromName("inode/directory"); + return inodeDirectory_; + } + + static std::shared_ptr inodeShortcut() { // inode/x-shortcut + if(!inodeShortcut_) + inodeShortcut_ = fromName("inode/x-shortcut"); + return inodeShortcut_; + } + + static std::shared_ptr inodeMountPoint() { // inode/mount-point + if(!inodeMountPoint_) + inodeMountPoint_ = fromName("inode/mount-point"); + return inodeMountPoint_; + } + + static std::shared_ptr desktopEntry() { // application/x-desktop + if(!desktopEntry_) + desktopEntry_ = fromName("application/x-desktop"); + return desktopEntry_; + } + +private: + void removeThumbnailer(std::shared_ptr& thumbnailer) { + std::lock_guard lock{mutex_}; + thumbnailers_.remove(thumbnailer); + } + + void addThumbnailer(std::shared_ptr thumbnailer) { + std::lock_guard lock{mutex_}; + thumbnailers_.push_front(std::move(thumbnailer)); + } + +private: + std::shared_ptr icon_; + CStrPtr name_; + mutable CStrPtr desc_; + std::forward_list> thumbnailers_; + static std::unordered_map, CStrHash, CStrEqual> cache_; + static std::mutex mutex_; + + static std::shared_ptr inodeDirectory_; // inode/directory + static std::shared_ptr inodeShortcut_; // inode/x-shortcut + static std::shared_ptr inodeMountPoint_; // inode/mount-point + static std::shared_ptr desktopEntry_; // application/x-desktop +}; + + +} // namespace Fm + +#endif diff --git a/src/core/templates.cpp b/src/core/templates.cpp new file mode 100644 index 0000000..fbe358e --- /dev/null +++ b/src/core/templates.cpp @@ -0,0 +1,136 @@ +#include "templates.h" +#include "gioptrs.h" + +#include +#include + +using namespace std; + +namespace Fm { + +std::weak_ptr Templates::globalInstance_; + +TemplateItem::TemplateItem(std::shared_ptr file): fileInfo_{file} { +} + +FilePath TemplateItem::filePath() const { + auto& target = fileInfo_->target(); + if(fileInfo_->isDesktopEntry() && !target.empty()) { + if(target[0] == '/') { // target is an absolute path + return FilePath::fromLocalPath(target.c_str()); + } + else { // resolve relative path + return fileInfo_->dirPath().relativePath(target.c_str()); + } + } + return fileInfo_->path(); +} + +Templates::Templates() : QObject() { + auto* data_dirs = g_get_system_data_dirs(); + // system-wide template dirs + for(auto data_dir = data_dirs; *data_dir; ++data_dir) { + CStrPtr dir_name{g_build_filename(*data_dir, "templates", nullptr)}; + addTemplateDir(dir_name.get()); + } + + // user-specific template dir + CStrPtr dir_name{g_build_filename(g_get_user_data_dir(), "templates", nullptr)}; + addTemplateDir(dir_name.get()); + + // $XDG_TEMPLATES_DIR (FIXME: this might change at runtime) + const gchar *special_dir = g_get_user_special_dir(G_USER_DIRECTORY_TEMPLATES); + if (special_dir) { + addTemplateDir(special_dir); + } +} + +shared_ptr Templates::globalInstance() { + auto templates = globalInstance_.lock(); + if(!templates) { + templates = make_shared(); + globalInstance_ = templates; + } + return templates; +} + +void Templates::addTemplateDir(const char* dirPathName) { + auto dir_path = FilePath::fromLocalPath(dirPathName); + if(dir_path.isValid()) { + auto folder = Folder::fromPath(dir_path); + if(folder->isLoaded()) { + const auto files = folder->files(); + for(auto& file : files) { + items_.emplace_back(std::make_shared(file)); + } + } + connect(folder.get(), &Folder::filesAdded, this, &Templates::onFilesAdded); + connect(folder.get(), &Folder::filesChanged, this, &Templates::onFilesChanged); + connect(folder.get(), &Folder::filesRemoved, this, &Templates::onFilesRemoved); + connect(folder.get(), &Folder::removed, this, &Templates::onTemplateDirRemoved); + templateFolders_.emplace_back(std::move(folder)); + } +} + +void Templates::onFilesAdded(FileInfoList& addedFiles) { + for(auto& file : addedFiles) { + // FIXME: we do not support subdirs right now (only XFCE supports this) + if(file->isHidden() || file->isDir()) { + continue; + } + items_.emplace_back(std::make_shared(file)); + // emit a signal for the addition + Q_EMIT itemAdded(items_.back()); + } +} + +void Templates::onFilesChanged(std::vector& changePairs) { + for(auto& change: changePairs) { + auto& old_file = change.first; + auto& new_file = change.second; + auto it = std::find_if(items_.begin(), items_.end(), [&](const std::shared_ptr& item) { + return item->fileInfo() == old_file; + }); + if(it != items_.end()) { + // emit a signal for the change + auto old = *it; + *it = std::make_shared(new_file); + Q_EMIT itemChanged(old, *it); + } + } +} + +void Templates::onFilesRemoved(FileInfoList& removedFiles) { + for(auto& file : removedFiles) { + auto filePath = file->path(); + auto it = std::remove_if(items_.begin(), items_.end(), [&](const std::shared_ptr& item) { + return item->fileInfo() == file; + }); + for(auto removed_it = it; it != items_.end(); ++it) { + // emit a signal for the removal + Q_EMIT itemRemoved(*removed_it); + } + items_.erase(it, items_.end()); + } +} + +void Templates::onTemplateDirRemoved() { + // the whole template dir is removed + auto folder = static_cast(sender()); + if(!folder) { + return; + } + auto dirPath = folder->path(); + + // remote all files under this dir + auto it = std::remove_if(items_.begin(), items_.end(), [&](const std::shared_ptr& item) { + return dirPath.isPrefixOf(item->filePath()); + }); + for(auto removed_it = it; it != items_.end(); ++it) { + // emit a signal for the removal + Q_EMIT itemRemoved(*removed_it); + } + items_.erase(it, items_.end()); +} + +} // namespace Fm diff --git a/src/core/templates.h b/src/core/templates.h new file mode 100644 index 0000000..6f656dd --- /dev/null +++ b/src/core/templates.h @@ -0,0 +1,100 @@ +#ifndef TEMPLATES_H +#define TEMPLATES_H + +#include "../libfmqtglobals.h" + +#include +#include +#include +#include "folder.h" +#include "fileinfo.h" +#include "mimetype.h" +#include "iconinfo.h" + +namespace Fm { + +class LIBFM_QT_API TemplateItem { +public: + explicit TemplateItem(std::shared_ptr fileInfo); + + QString displayName() const { + return fileInfo_->displayName(); + } + + const std::string& name() const { + return fileInfo_->name(); + } + + std::shared_ptr icon() const { + return fileInfo_->icon(); + } + + std::shared_ptr fileInfo() const { + return fileInfo_; + } + + std::shared_ptr mimeType() const { + return fileInfo_->mimeType(); + } + + FilePath filePath() const; + +private: + std::shared_ptr fileInfo_; +}; + + +class LIBFM_QT_API Templates : public QObject { + Q_OBJECT +public: + explicit Templates(); + + // FIXME: the first call to this method will get no templates since dir loading is in progress. + static std::shared_ptr globalInstance(); + + void forEachItem(std::function&)> func) const { + for(const auto& item : items_) { + func(item); + } + } + + std::vector> items() const { + std::vector> tmp_items; + for(auto& item: items_) { + tmp_items.emplace_back(item); + } + return tmp_items; + } + + bool hasTemplates() const { + return !items_.empty(); + } + +Q_SIGNALS: + void itemAdded(const std::shared_ptr& item); + + void itemChanged(const std::shared_ptr& oldItem, const std::shared_ptr& newItem); + + void itemRemoved(const std::shared_ptr& item); + +private: + void addTemplateDir(const char* dirPathName); + +private Q_SLOTS: + void onFilesAdded(FileInfoList& addedFiles); + + void onFilesChanged(std::vector& changePairs); + + void onFilesRemoved(FileInfoList& removedFiles); + + void onTemplateDirRemoved(); + +private: + std::vector> items_; + std::vector> templateFolders_; + static std::weak_ptr globalInstance_; +}; + +} // namespace Fm + +#endif // TEMPLATES_H diff --git a/src/core/terminal.cpp b/src/core/terminal.cpp new file mode 100644 index 0000000..807c7bd --- /dev/null +++ b/src/core/terminal.cpp @@ -0,0 +1,183 @@ +#include "terminal.h" + +namespace Fm { + +#include +#include +#include +#include + +static std::string defaultTerminalName{"xterm"}; + +#if !GLIB_CHECK_VERSION(2, 28, 0) && !HAVE_DECL_ENVIRON +extern char** environ; +#endif + +static void child_setup(gpointer user_data) { + /* Move child to grandparent group so it will not die with parent */ + setpgid(0, (pid_t)(gsize)user_data); +} + +bool launchTerminal(const char* programName, const FilePath& workingDir, Fm::GErrorPtr& error) { + /* read system terminals file */ + GKeyFile* kf = g_key_file_new(); + if(!g_key_file_load_from_file(kf, LIBFM_QT_DATA_DIR "/terminals.list", G_KEY_FILE_NONE, &error)) { + g_key_file_free(kf); + return false; + } + auto launch = g_key_file_get_string(kf, programName, "launch", nullptr); + auto desktop_id = g_key_file_get_string(kf, programName, "desktop_id", nullptr); + + GDesktopAppInfo* appinfo = nullptr; + if(desktop_id) { + appinfo = g_desktop_app_info_new(desktop_id); + } + + const gchar* cmd; + gchar* _cmd = nullptr; + if(appinfo) { + cmd = g_app_info_get_commandline(G_APP_INFO(appinfo)); + } + else if(launch) { + cmd = _cmd = g_strdup_printf("%s %s", programName, launch); + } + else { + cmd = programName; + } + +#if 0 // FIXME: what's this? + if(custom_args) { + cmd = g_strdup_printf("%s %s", cmd, custom_args); + g_free(_cmd); + _cmd = (char*)cmd; + } +#endif + + char** argv; + int argc; + if(!g_shell_parse_argv(cmd, &argc, &argv, nullptr)) { + argv = nullptr; + } + g_free(_cmd); + + if(appinfo) { + g_object_unref(appinfo); + } + if(!argv) { /* parsing failed */ + return false; + } + char** envp; +#if GLIB_CHECK_VERSION(2, 28, 0) + envp = g_get_environ(); +#else + envp = g_strdupv(environ); +#endif + + auto dir = workingDir ? workingDir.localPath() : nullptr; + if(dir) { +#if GLIB_CHECK_VERSION(2, 32, 0) + envp = g_environ_setenv(envp, "PWD", dir.get(), TRUE); +#else + char** env = envp; + + if(env) while(*env != nullptr) { + if(strncmp(*env, "PWD=", 4) == 0) { + break; + } + env++; + } + if(env == nullptr || *env == nullptr) { + gint length; + + length = envp ? g_strv_length(envp) : 0; + envp = g_renew(gchar*, envp, length + 2); + env = &envp[length]; + env[1] = nullptr; + } + else { + g_free(*env); + } + *env = g_strdup_printf("PWD=%s", dir); +#endif + } + + bool ret = g_spawn_async(dir.get(), argv, envp, G_SPAWN_SEARCH_PATH, + child_setup, (gpointer)(gsize)getpgid(getppid()), + nullptr, &error); + g_strfreev(argv); + g_strfreev(envp); + g_key_file_free(kf); + return ret; +} + +std::vector allKnownTerminals() { + std::vector terminals; + GKeyFile* kf = g_key_file_new(); + if(g_key_file_load_from_file(kf, LIBFM_QT_DATA_DIR "/terminals.list", G_KEY_FILE_NONE, nullptr)) { + gsize n; + auto programs = g_key_file_get_groups(kf, &n); + terminals.reserve(n); + for(auto name = programs; *name; ++name) { + terminals.emplace_back(*name); + } + g_free(programs); + } + g_key_file_free(kf); + return terminals; +} + +// used by fm-app-info.c +extern "C" char* expand_terminal(char* cmd, gboolean keep_open, GError** error) { + const char* program = nullptr; + CStrPtr open_arg; + CStrPtr noclose_arg; + CStrPtr custom_args; + + /* read system terminals file */ + GKeyFile* kf = g_key_file_new(); + if(g_key_file_load_from_file(kf, LIBFM_QT_DATA_DIR "/terminals.list", G_KEY_FILE_NONE, error)) { + if(g_key_file_has_group(kf, defaultTerminalName.c_str())) { + program = defaultTerminalName.c_str(); + open_arg = CStrPtr{g_key_file_get_string(kf, program, "open_arg", nullptr)}; + noclose_arg = CStrPtr{g_key_file_get_string(kf, program, "noclose_arg", nullptr)}; + custom_args = CStrPtr{g_key_file_get_string(kf, program, "custom_args", nullptr)}; + } + } + g_key_file_free(kf); + + const char* opts; + /* if %s is not found, fallback to -e */ + /* bug #3457335: Crash on application start with Terminal=true. */ + /* fallback to xterm if a terminal emulator is not found. */ + if(!program) { + /* FIXME: we should not hard code xterm here. :-( + * It's better to prompt the user and let he or she set + * his preferred terminal emulator. */ + program = "xterm"; + open_arg = CStrPtr{g_strdup("-e")}; + } + + if(keep_open && noclose_arg) + opts = noclose_arg.get(); + else + opts = open_arg.get(); + + char* ret = nullptr; + if(custom_args) + ret = g_strdup_printf("%s %s %s %s", program, custom_args.get(), + opts, cmd); + else + ret = g_strdup_printf("%s %s %s", program, opts, cmd); + + return ret; +} + +const std::string defaultTerminal() { + return defaultTerminalName; +} + +void setDefaultTerminal(std::string program) { + defaultTerminalName = program; +} + +} // namespace Fm diff --git a/src/core/terminal.h b/src/core/terminal.h new file mode 100644 index 0000000..db6f528 --- /dev/null +++ b/src/core/terminal.h @@ -0,0 +1,26 @@ +#ifndef TERMINAL_H +#define TERMINAL_H + +#include "../libfmqtglobals.h" +#include "gioptrs.h" +#include "filepath.h" +#include +#include + +namespace Fm { + +LIBFM_QT_API bool launchTerminal(const char* programName, const FilePath& workingDir, GErrorPtr& error); + +LIBFM_QT_API std::vector allKnownTerminals(); + +LIBFM_QT_API const std::string defaultTerminal(); + +LIBFM_QT_API void setDefaultTerminal(std::string program); + +inline bool launchDefaultTerminal(const FilePath& workingDir, GErrorPtr& error) { + return launchTerminal(defaultTerminal().c_str(), workingDir, error); +} + +} // namespace Fm + +#endif // TERMINAL_H diff --git a/src/core/thumbnailer.cpp b/src/core/thumbnailer.cpp new file mode 100644 index 0000000..ab2c80d --- /dev/null +++ b/src/core/thumbnailer.cpp @@ -0,0 +1,141 @@ +#include "thumbnailer.h" +#include "mimetype.h" +#include +#include +#include +#include + +namespace Fm { + +std::mutex Thumbnailer::mutex_; +std::vector> Thumbnailer::allThumbnailers_; + +Thumbnailer::Thumbnailer(const char* id, GKeyFile* kf): + id_{g_strdup(id)}, + try_exec_{g_key_file_get_string(kf, "Thumbnailer Entry", "TryExec", nullptr)}, + exec_{g_key_file_get_string(kf, "Thumbnailer Entry", "Exec", nullptr)} { +} + +CStrPtr Thumbnailer::commandForUri(const char* uri, const char* output_file, guint size) const { + if(exec_) { + /* FIXME: how to handle TryExec? */ + + /* parse the command line and do required substitutions according to: + * https://developer.gnome.org/integration-guide/stable/thumbnailer.html.en + */ + GString* cmd_line = g_string_sized_new(1024); + const char* p; + for(p = exec_.get(); *p; ++p) { + if(G_LIKELY(*p != '%')) { + g_string_append_c(cmd_line, *p); + } + else { + char* quoted; + ++p; + switch(*p) { + case '\0': + break; + case 's': + g_string_append_printf(cmd_line, "%d", size); + break; + case 'i': { + char* src_path = g_filename_from_uri(uri, nullptr, nullptr); + if(src_path) { + quoted = g_shell_quote(src_path); + g_string_append(cmd_line, quoted); + g_free(quoted); + g_free(src_path); + } + break; + } + case 'u': + quoted = g_shell_quote(uri); + g_string_append(cmd_line, quoted); + g_free(quoted); + break; + case 'o': + g_string_append(cmd_line, output_file); + break; + default: + g_string_append_c(cmd_line, '%'); + if(*p != '%') { + g_string_append_c(cmd_line, *p); + } + } + } + } + return CStrPtr{g_string_free(cmd_line, FALSE)}; + } + return nullptr; +} + +bool Thumbnailer::run(const char* uri, const char* output_file, int size) const { + auto cmd = commandForUri(uri, output_file, size); + qDebug() << cmd.get(); + int status; + bool ret = g_spawn_command_line_sync(cmd.get(), nullptr, nullptr, &status, nullptr); + return ret && status == 0; +} + +static void find_thumbnailers_in_data_dir(std::unordered_map& hash, const char* data_dir) { + CStrPtr dir_path{g_build_filename(data_dir, "thumbnailers", nullptr)}; + GDir* dir = g_dir_open(dir_path.get(), 0, nullptr); + if(dir) { + const char* basename; + while((basename = g_dir_read_name(dir)) != nullptr) { + /* we only want filenames with .thumbnailer extension */ + if(G_LIKELY(g_str_has_suffix(basename, ".thumbnailer"))) { + hash.insert(std::make_pair(basename, data_dir)); + } + } + g_dir_close(dir); + } +} + +void Thumbnailer::loadAll() { + const gchar* const* data_dirs = g_get_system_data_dirs(); + const gchar* const* data_dir; + + /* use a temporary hash table to collect thumbnailer basenames + * key: basename of thumbnailer entry file + * value: data dir the thumbnailer entry file is in */ + std::unordered_map hash; + + /* load user-specific thumbnailers */ + find_thumbnailers_in_data_dir(hash, g_get_user_data_dir()); + + /* load system-wide thumbnailers */ + for(data_dir = data_dirs; *data_dir; ++data_dir) { + find_thumbnailers_in_data_dir(hash, *data_dir); + } + + /* load all found thumbnailers */ + if(!hash.empty()) { + std::lock_guard lock{mutex_}; + GKeyFile* kf = g_key_file_new(); + for(auto& item: hash) { + auto& base_name = item.first; + auto& dir_path = item.second; + CStrPtr file_path{g_build_filename(dir_path, "thumbnailers", base_name.c_str(), nullptr)}; + if(g_key_file_load_from_file(kf, file_path.get(), G_KEY_FILE_NONE, nullptr)) { + auto thumbnailer = std::make_shared(base_name.c_str(), kf); + if(thumbnailer->exec_) { + char** mime_types = g_key_file_get_string_list(kf, "Thumbnailer Entry", "MimeType", nullptr, nullptr); + if(mime_types) { + for(char** name = mime_types; *name; ++name) { + auto mime_type = MimeType::fromName(*name); + if(mime_type) { + std::const_pointer_cast(mime_type)->addThumbnailer(thumbnailer); + } + } + g_strfreev(mime_types); + } + } + allThumbnailers_.push_back(std::move(thumbnailer)); + } + } + g_key_file_free(kf); + } +} + +} // namespace Fm diff --git a/src/core/thumbnailer.h b/src/core/thumbnailer.h new file mode 100644 index 0000000..91a21d7 --- /dev/null +++ b/src/core/thumbnailer.h @@ -0,0 +1,37 @@ +#ifndef FM2_THUMBNAILER_H +#define FM2_THUMBNAILER_H + +#include "../libfmqtglobals.h" +#include "cstrptr.h" +#include +#include +#include +#include + +namespace Fm { + +class MimeType; + +class LIBFM_QT_API Thumbnailer { +public: + explicit Thumbnailer(const char *id, GKeyFile *kf); + + CStrPtr commandForUri(const char* uri, const char* output_file, guint size) const; + + bool run(const char* uri, const char* output_file, int size) const; + + static void loadAll(); + +private: + CStrPtr id_; + CStrPtr try_exec_; /* FIXME: is this useful? */ + CStrPtr exec_; + //std::vector> mimeTypes_; + + static std::mutex mutex_; + static std::vector> allThumbnailers_; +}; + +} // namespace Fm + +#endif // FM2_THUMBNAILER_H diff --git a/src/core/thumbnailjob.cpp b/src/core/thumbnailjob.cpp new file mode 100644 index 0000000..bddd7de --- /dev/null +++ b/src/core/thumbnailjob.cpp @@ -0,0 +1,293 @@ +#include "thumbnailjob.h" +#include +#include +#include +#include +#include +#include +#include "thumbnailer.h" + +#include "core/legacy/fm-config.h" + +namespace Fm { + +QThreadPool* ThumbnailJob::threadPool_ = nullptr; + +bool ThumbnailJob::localFilesOnly_ = true; +int ThumbnailJob::maxThumbnailFileSize_ = 0; + +ThumbnailJob::ThumbnailJob(FileInfoList files, int size): + files_{std::move(files)}, + size_{size}, + md5Calc_{g_checksum_new(G_CHECKSUM_MD5)} { +} + +ThumbnailJob::~ThumbnailJob() { + g_checksum_free(md5Calc_); + // qDebug("delete ThumbnailJob"); +} + +void ThumbnailJob::exec() { + for(auto& file: files_) { + if(isCancelled()) { + break; + } + auto image = loadForFile(file); + Q_EMIT thumbnailLoaded(file, size_, image); + results_.emplace_back(std::move(image)); + } +} + +QImage ThumbnailJob::readImageFromStream(GInputStream* stream, size_t len) { + // FIXME: should we set a limit here? Otherwise if len is too large, we can run out of memory. + std::unique_ptr buffer{new unsigned char[len]}; // allocate enough buffer + unsigned char* pbuffer = buffer.get(); + size_t totalReadSize = 0; + while(!isCancelled() && totalReadSize < len) { + size_t bytesToRead = totalReadSize + 4096 > len ? len - totalReadSize : 4096; + gssize readSize = g_input_stream_read(stream, pbuffer, bytesToRead, cancellable_.get(), nullptr); + if(readSize == 0) { // end of file + break; + } + else if(readSize == -1) { // error + return QImage(); + } + totalReadSize += readSize; + pbuffer += readSize; + } + QImage image; + image.loadFromData(buffer.get(), totalReadSize); + return image; +} + +QImage ThumbnailJob::loadForFile(const std::shared_ptr &file) { + if(!file->canThumbnail()) { + return QImage(); + } + + // thumbnails are stored in $XDG_CACHE_HOME/thumbnails/large|normal|failed + QString thumbnailDir{g_get_user_cache_dir()}; + thumbnailDir += "/thumbnails/"; + + // don't make thumbnails for files inside the thumbnail directory + if(FilePath::fromLocalPath(thumbnailDir.toLocal8Bit().constData()).isParentOf(file->dirPath())) { + return QImage(); + } + + const char* subdir = size_ > 128 ? "large" : "normal"; + thumbnailDir += subdir; + + // generate base name of the thumbnail => {md5 of uri}.png + auto origPath = file->path(); + CStrPtr uri; + if(file->isSymlink()) { + // use the symlink target in the name to update the thumbnail + // if the file is changed to a symlink with the same time stamp + auto target = file->target(); + if(!target.empty()) { + uri = FilePath::fromLocalPath(target.c_str()).uri(); + } + } + if(!uri) { + uri = origPath.uri(); + } + + char thumbnailName[32 + 5]; + // calculate md5 hash for the uri of the original file + g_checksum_update(md5Calc_, reinterpret_cast(uri.get()), -1); + memcpy(thumbnailName, g_checksum_get_string(md5Calc_), 32); + memcpy(thumbnailName + 32, ".png", 5); + g_checksum_reset(md5Calc_); // reset the checksum calculator for next use + + QString thumbnailFilename = thumbnailDir; + thumbnailFilename += '/'; + thumbnailFilename += thumbnailName; + // qDebug() << "thumbnail:" << file->getName().c_str() << thumbnailFilename; + + // try to load the thumbnail file if it exists + QImage thumbnail{thumbnailFilename}; + if(thumbnail.isNull() || isThumbnailOutdated(file, thumbnail)) { + // the existing thumbnail cannot be loaded, generate a new one + + // create the thumbnail dir as needd (FIXME: Qt file I/O is slow) + QDir().mkpath(thumbnailDir); + + thumbnail = generateThumbnail(file, origPath, uri.get(), thumbnailFilename); + } + // resize to the size we need + if(thumbnail.width() > size_ || thumbnail.height() > size_) { + thumbnail = thumbnail.scaled(size_, size_, Qt::KeepAspectRatio, Qt::SmoothTransformation); + } + return thumbnail; +} + +bool ThumbnailJob::isSupportedImageType(const std::shared_ptr& mimeType) const { + if(mimeType->isImage()) { + auto supportedTypes = QImageReader::supportedMimeTypes(); + auto found = std::find(supportedTypes.cbegin(), supportedTypes.cend(), mimeType->name()); + if(found != supportedTypes.cend()) + return true; + } + return false; +} + +bool ThumbnailJob::isThumbnailOutdated(const std::shared_ptr& file, const QImage &thumbnail) const { + QString thumb_mtime = thumbnail.text("Thumb::MTime"); + return (thumb_mtime.isEmpty() || thumb_mtime.toULongLong() != file->mtime()); +} + +bool ThumbnailJob::readJpegExif(GInputStream *stream, QImage& thumbnail, int& rotate_degrees) { + /* try to extract thumbnails embedded in jpeg files */ + ExifLoader* exif_loader = exif_loader_new(); + while(!isCancelled()) { + unsigned char buf[4096]; + gssize read_size = g_input_stream_read(stream, buf, 4096, cancellable_.get(), nullptr); + if(read_size <= 0) { // EOF or error + break; + } + if(exif_loader_write(exif_loader, buf, read_size) == 0) { + break; // no more EXIF data + } + } + ExifData* exif_data = exif_loader_get_data(exif_loader); + exif_loader_unref(exif_loader); + if(exif_data) { + /* reference for EXIF orientation tag: + * https://www.impulseadventure.com/photo/exif-orientation.html */ + ExifEntry* orient_ent = exif_data_get_entry(exif_data, EXIF_TAG_ORIENTATION); + if(orient_ent) { /* orientation flag found in EXIF */ + gushort orient; + ExifByteOrder bo = exif_data_get_byte_order(exif_data); + /* bo == EXIF_BYTE_ORDER_INTEL ; */ + orient = exif_get_short(orient_ent->data, bo); + switch(orient) { + case 1: /* no rotation */ + rotate_degrees = 0; + break; + case 8: + rotate_degrees = 90; + break; + case 3: + rotate_degrees = 180; + break; + case 6: + rotate_degrees = 270; + break; + } + } + if(exif_data->data) { // if an embedded thumbnail is available, load it + thumbnail.loadFromData(exif_data->data, exif_data->size); + } + exif_data_unref(exif_data); + } + return !thumbnail.isNull(); +} + +QImage ThumbnailJob::generateThumbnail(const std::shared_ptr& file, const FilePath& origPath, const char* uri, const QString& thumbnailFilename) { + QImage result; + auto mime_type = file->mimeType(); + if(isSupportedImageType(mime_type)) { + GFileInputStreamPtr ins{g_file_read(origPath.gfile().get(), cancellable_.get(), nullptr), false}; + if(!ins) + return QImage(); + bool fromExif = false; + int rotate_degrees = 0; + if(strcmp(mime_type->name(), "image/jpeg") == 0) { // if this is a jpeg file + // try to get the thumbnail embedded in EXIF data + if(readJpegExif(G_INPUT_STREAM(ins.get()), result, rotate_degrees)) { + fromExif = true; + } + } + if(!fromExif) { // not able to generate a thumbnail from the EXIF data + // load the original file and do the scaling ourselves + g_seekable_seek(G_SEEKABLE(ins.get()), 0, G_SEEK_SET, cancellable_.get(), nullptr); + result = readImageFromStream(G_INPUT_STREAM(ins.get()), file->size()); + } + g_input_stream_close(G_INPUT_STREAM(ins.get()), nullptr, nullptr); + + if(!result.isNull()) { // the image is successfully loaded + // scale the image as needed + int target_size = size_ > 128 ? 256 : 128; + + // only scale the original image if it's too large + if(result.width() > target_size || result.height() > target_size) { + result = result.scaled(target_size, target_size, Qt::KeepAspectRatio, Qt::SmoothTransformation); + } + + if(rotate_degrees != 0) { + // degree values are 0, 90, 180, and 270 counterclockwise. + // In Qt, QMatrix does rotation counterclockwise as well. + // However, because the y axis of widget coordinate system is downward, + // the real effect of the coordinate transformation becomes clockwise rotation. + // So we need to use (360 - degree) here. + // Quote from QMatrix API doc: + // Note that if you apply a QMatrix to a point defined in widget + // coordinates, the direction of the rotation will be clockwise because + // the y-axis points downwards. + result = result.transformed(QMatrix().rotate(360 - rotate_degrees)); + } + + // save the generated thumbnail to disk (don't save png thumbnails for JPEG EXIF thumbnails since loading them is cheap) + if(!fromExif) { + result.setText("Thumb::MTime", QString::number(file->mtime())); + result.setText("Thumb::URI", uri); + result.save(thumbnailFilename, "PNG"); + } + // qDebug() << "save thumbnail:" << thumbnailFilename; + } + } + else { // the image format is not supported, try to find an external thumbnailer + // try all available external thumbnailers for it until sucess + int target_size = size_ > 128 ? 256 : 128; + file->mimeType()->forEachThumbnailer([&](const std::shared_ptr& thumbnailer) { + if(thumbnailer->run(uri, thumbnailFilename.toLocal8Bit().constData(), target_size)) { + result = QImage(thumbnailFilename); + } + return !result.isNull(); // return true on success, and forEachThumbnailer() will stop. + }); + + if(!result.isNull()) { + // Some thumbnailers did not write the proper metadata required by the xdg spec to the output (such as evince-thumbnailer) + // Here we waste some time to fix them so next time we don't need to re-generate these thumbnails. :-( + bool changed = false; + if(Q_UNLIKELY(result.text("Thumb::MTime").isEmpty())) { + result.setText("Thumb::MTime", QString::number(file->mtime())); + changed = true; + } + if(Q_UNLIKELY(result.text("Thumb::URI").isEmpty())) { + result.setText("Thumb::URI", uri); + changed = true; + } + if(Q_UNLIKELY(changed)) { + // save the modified PNG file containing metadata to a file. + result.save(thumbnailFilename, "PNG"); + } + } + } + return result; +} + +QThreadPool* ThumbnailJob::threadPool() { + if(Q_UNLIKELY(threadPool_ == nullptr)) { + threadPool_ = new QThreadPool(); + threadPool_->setMaxThreadCount(1); + } + return threadPool_; +} + +void ThumbnailJob::setLocalFilesOnly(bool value) { + localFilesOnly_ = value; + if(fm_config) { + fm_config->thumbnail_local = localFilesOnly_; + } +} + +void ThumbnailJob::setMaxThumbnailFileSize(int size) { + maxThumbnailFileSize_ = size; + if(fm_config) { + fm_config->thumbnail_max = maxThumbnailFileSize_; + } +} + + +} // namespace Fm diff --git a/src/core/thumbnailjob.h b/src/core/thumbnailjob.h new file mode 100644 index 0000000..632b14d --- /dev/null +++ b/src/core/thumbnailjob.h @@ -0,0 +1,78 @@ +#ifndef FM2_THUMBNAILJOB_H +#define FM2_THUMBNAILJOB_H + +#include "../libfmqtglobals.h" +#include "fileinfo.h" +#include "gioptrs.h" +#include "job.h" +#include + +namespace Fm { + +class LIBFM_QT_API ThumbnailJob: public Job { + Q_OBJECT +public: + + explicit ThumbnailJob(FileInfoList files, int size); + + ~ThumbnailJob(); + + int size() const { + return size_; + } + + static QThreadPool* threadPool(); + + static void setLocalFilesOnly(bool value); + + static bool localFilesOnly() { + return localFilesOnly_; + } + + static int maxThumbnailFileSize() { + return maxThumbnailFileSize_; + } + + static void setMaxThumbnailFileSize(int size); + + const std::vector& results() const { + return results_; + } + +Q_SIGNALS: + void thumbnailLoaded(const std::shared_ptr& file, int size, QImage thumbnail); + +protected: + + void exec() override; + +private: + + bool isSupportedImageType(const std::shared_ptr& mimeType) const; + + bool isThumbnailOutdated(const std::shared_ptr& file, const QImage& thumbnail) const; + + QImage generateThumbnail(const std::shared_ptr& file, const FilePath& origPath, const char* uri, const QString& thumbnailFilename); + + QImage readImageFromStream(GInputStream* stream, size_t len); + + QImage loadForFile(const std::shared_ptr& file); + + bool readJpegExif(GInputStream* stream, QImage& thumbnail, int& rotate_degrees); + +private: + FileInfoList files_; + int size_; + std::vector results_; + GCancellablePtr cancellable_; + GChecksum* md5Calc_; + + static QThreadPool* threadPool_; + + static bool localFilesOnly_; + static int maxThumbnailFileSize_; +}; + +} // namespace Fm + +#endif // FM2_THUMBNAILJOB_H diff --git a/src/core/totalsizejob.cpp b/src/core/totalsizejob.cpp new file mode 100644 index 0000000..090be86 --- /dev/null +++ b/src/core/totalsizejob.cpp @@ -0,0 +1,141 @@ +#include "totalsizejob.h" + +namespace Fm { + +static const char query_str[] = + G_FILE_ATTRIBUTE_STANDARD_TYPE"," + G_FILE_ATTRIBUTE_STANDARD_NAME"," + G_FILE_ATTRIBUTE_STANDARD_IS_VIRTUAL"," + G_FILE_ATTRIBUTE_STANDARD_SIZE"," + G_FILE_ATTRIBUTE_STANDARD_ALLOCATED_SIZE"," + G_FILE_ATTRIBUTE_ID_FILESYSTEM; + + +TotalSizeJob::TotalSizeJob(FilePathList paths, Flags flags): + paths_{std::move(paths)}, + flags_{flags}, + totalSize_{0}, + totalOndiskSize_{0}, + fileCount_{0}, + dest_fs_id{nullptr} { +} + + +void TotalSizeJob::exec(FilePath path, GFileInfoPtr inf) { + GFileType type; + const char* fs_id; + bool descend; + +_retry_query_info: + if(!inf) { + GErrorPtr err; + inf = GFileInfoPtr { + g_file_query_info(path.gfile().get(), query_str, + (flags_ & FOLLOW_LINKS) ? G_FILE_QUERY_INFO_NONE : G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err), + false + }; + if(!inf) { + ErrorAction act = emitError( err, ErrorSeverity::MILD); + err = nullptr; + if(act == ErrorAction::RETRY) { + goto _retry_query_info; + } + return; + } + } + if(isCancelled()) { + return; + } + + type = g_file_info_get_file_type(inf.get()); + descend = true; + + ++fileCount_; + /* SF bug #892: dir file size is not relevant in the summary */ + if(type != G_FILE_TYPE_DIRECTORY) { + totalSize_ += g_file_info_get_size(inf.get()); + } + totalOndiskSize_ += g_file_info_get_attribute_uint64(inf.get(), G_FILE_ATTRIBUTE_STANDARD_ALLOCATED_SIZE); + + /* prepare for moving across different devices */ + if(flags_ & PREPARE_MOVE) { + fs_id = g_file_info_get_attribute_string(inf.get(), G_FILE_ATTRIBUTE_ID_FILESYSTEM); + if(fs_id && dest_fs_id && (strcmp(fs_id, dest_fs_id) == 0 || g_str_has_prefix(fs_id, "trash"))) { + // same filesystem or move from trash:/// + descend = false; + } + else { + /* files on different device requires an additional 'delete' for the source file. */ + ++totalSize_; /* this is for the additional delete */ + ++totalOndiskSize_; + ++fileCount_; + descend = true; + } + } + + if(type == G_FILE_TYPE_DIRECTORY) { + /* check if we need to decends into the dir. */ + /* trash:/// doesn't support deleting files recursively (but we want to descend into trash root "trash:///" */ + if(flags_ & PREPARE_DELETE && path.hasUriScheme("trash") && path.baseName()[0] != '/') { + descend = false; + } + else { + /* only descends into files on the same filesystem */ + if(flags_ & SAME_FS) { + fs_id = g_file_info_get_attribute_string(inf.get(), G_FILE_ATTRIBUTE_ID_FILESYSTEM); + descend = (g_strcmp0(fs_id, dest_fs_id) == 0); + } + } + + inf = nullptr; + if(descend) { +_retry_enum_children: + GErrorPtr err; + auto enu = GFileEnumeratorPtr { + g_file_enumerate_children(path.gfile().get(), query_str, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), &err), + false + }; + if(enu) { + while(!isCancelled()) { + inf = GFileInfoPtr{g_file_enumerator_next_file(enu.get(), cancellable().get(), &err), false}; + if(inf) { + FilePath child = path.child(g_file_info_get_name(inf.get())); + exec(std::move(child), std::move(inf)); + } + else { + if(err) { /* error! */ + /* ErrorAction::RETRY is not supported */ + emitError( err, ErrorSeverity::MILD); + err = nullptr; + } + else { + /* EOF is reached, do nothing. */ + break; + } + } + } + g_file_enumerator_close(enu.get(), nullptr, nullptr); + } + else { + ErrorAction act = emitError( err, ErrorSeverity::MILD); + err = nullptr; + if(act == ErrorAction::RETRY) { + goto _retry_enum_children; + } + } + } + } +} + + +void TotalSizeJob::exec() { + for(auto& path : paths_) { + exec(path, GFileInfoPtr{}); + } +} + + +} // namespace Fm diff --git a/src/core/totalsizejob.h b/src/core/totalsizejob.h new file mode 100644 index 0000000..43c19c0 --- /dev/null +++ b/src/core/totalsizejob.h @@ -0,0 +1,56 @@ +#ifndef FM2_TOTALSIZEJOB_H +#define FM2_TOTALSIZEJOB_H + +#include "../libfmqtglobals.h" +#include "fileoperationjob.h" +#include "filepath.h" +#include +#include "gioptrs.h" + +namespace Fm { + +class LIBFM_QT_API TotalSizeJob : public Fm::FileOperationJob { + Q_OBJECT +public: + enum Flags { + DEFAULT = 0, + FOLLOW_LINKS = 1 << 0, + SAME_FS = 1 << 1, + PREPARE_MOVE = 1 << 2, + PREPARE_DELETE = 1 << 3 + }; + + explicit TotalSizeJob(FilePathList paths = FilePathList{}, Flags flags = DEFAULT); + + std::uint64_t totalSize() const { + return totalSize_; + } + + std::uint64_t totalOnDiskSize() const { + return totalOndiskSize_; + } + + unsigned int fileCount() const { + return fileCount_; + } + +protected: + + void exec() override; + +private: + void exec(FilePath path, GFileInfoPtr inf); + +private: + FilePathList paths_; + + int flags_; + std::uint64_t totalSize_; + std::uint64_t totalOndiskSize_; + unsigned int fileCount_; + const char* dest_fs_id; +}; + +} // namespace Fm + +#endif // FM2_TOTALSIZEJOB_H diff --git a/src/core/trashjob.cpp b/src/core/trashjob.cpp new file mode 100644 index 0000000..a79435d --- /dev/null +++ b/src/core/trashjob.cpp @@ -0,0 +1,74 @@ +#include "trashjob.h" + +#include "core/legacy/fm-config.h" + +namespace Fm { + +TrashJob::TrashJob(FilePathList paths): paths_{std::move(paths)} { + // calculate progress using finished file counts rather than their sizes + setCalcProgressUsingSize(false); +} + +void TrashJob::exec() { + setTotalAmount(paths_.size(), paths_.size()); + Q_EMIT preparedToRun(); + + /* FIXME: we shouldn't trash a file already in trash:/// */ + for(auto& path : paths_) { + if(isCancelled()) { + break; + } + + setCurrentFile(path); + + // TODO: get parent dir of the current path. + // if there is a Fm::Folder object created for it, block the update for the folder temporarily. + + for(;;) { // retry the i/o operation on errors + auto gf = path.gfile(); + bool ret = false; + // FIXME: do not depend on fm_config + if(fm_config->no_usb_trash) { + GMountPtr mnt{g_file_find_enclosing_mount(gf.get(), nullptr, nullptr), false}; + if(mnt) { + ret = g_mount_can_unmount(mnt.get()); /* TRUE if it's removable media */ + if(ret) { + unsupportedFiles_.push_back(path); + break; // don't trash the file + } + } + } + + // move the file to trash + GErrorPtr err; + ret = g_file_trash(gf.get(), cancellable().get(), &err); + if(ret) { // trash operation succeeded + break; + } + else { // failed + // if trashing is not supported by the file system + if(err.domain() == G_IO_ERROR && err.code() == G_IO_ERROR_NOT_SUPPORTED) { + unsupportedFiles_.push_back(path); + break; + } + else { + ErrorAction act = emitError(err, ErrorSeverity::MODERATE); + if(act == ErrorAction::RETRY) { + err.reset(); + } + else if(act == ErrorAction::ABORT) { + cancel(); + return; + } + else { + break; + } + } + } + } + addFinishedAmount(1, 1); + } +} + + +} // namespace Fm diff --git a/src/core/trashjob.h b/src/core/trashjob.h new file mode 100644 index 0000000..65d93cf --- /dev/null +++ b/src/core/trashjob.h @@ -0,0 +1,30 @@ +#ifndef FM2_TRASHJOB_H +#define FM2_TRASHJOB_H + +#include "../libfmqtglobals.h" +#include "fileoperationjob.h" +#include "filepath.h" + +namespace Fm { + +class LIBFM_QT_API TrashJob : public Fm::FileOperationJob { + Q_OBJECT +public: + explicit TrashJob(FilePathList paths); + + FilePathList unsupportedFiles() const { + return unsupportedFiles_; + } + +protected: + + void exec() override; + +private: + FilePathList paths_; + FilePathList unsupportedFiles_; +}; + +} // namespace Fm + +#endif // FM2_TRASHJOB_H diff --git a/src/core/untrashjob.cpp b/src/core/untrashjob.cpp new file mode 100644 index 0000000..7e0e0f6 --- /dev/null +++ b/src/core/untrashjob.cpp @@ -0,0 +1,75 @@ +#include "untrashjob.h" +#include "filetransferjob.h" + +namespace Fm { + +UntrashJob::UntrashJob(FilePathList srcPaths): + srcPaths_{std::move(srcPaths)} { +} + +void UntrashJob::exec() { + // preparing for the job + FilePathList validSrcPaths; + FilePathList origPaths; + for(auto& srcPath: srcPaths_) { + if(isCancelled()) { + break; + } + GErrorPtr err; + GFileInfoPtr srcInfo{ + g_file_query_info(srcPath.gfile().get(), + "trash::orig-path", + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + cancellable().get(), + &err), + false + }; + if(srcInfo) { + const char* orig_path_str = g_file_info_get_attribute_byte_string(srcInfo.get(), "trash::orig-path"); + if(orig_path_str) { + validSrcPaths.emplace_back(srcPath); + origPaths.emplace_back(FilePath::fromPathStr(orig_path_str)); + } + else { + g_set_error(&err, G_IO_ERROR, G_IO_ERROR_FAILED, + tr("Cannot untrash file '%s': original path not known").toUtf8().constData(), + g_file_info_get_display_name(srcInfo.get())); + // FIXME: do we need to retry here? + emitError(err, ErrorSeverity::MODERATE); + } + } + else { + // FIXME: do we need to retry here? + emitError(err); + } + } + + // collected original paths of the trashed files + // use the file transfer job to handle the actual file move + FileTransferJob fileTransferJob{std::move(validSrcPaths), std::move(origPaths), FileTransferJob::Mode::MOVE}; + // FIXME: + // I'm not sure why specifying Qt::DirectConnection is needed here since the caller & receiver are in the same thread. :-( + // However without this, the signals/slots here will cause deadlocks. + connect(&fileTransferJob, &FileTransferJob::preparedToRun, this, &UntrashJob::preparedToRun, Qt::DirectConnection); + connect(&fileTransferJob, &FileTransferJob::error, this, &UntrashJob::error, Qt::DirectConnection); + connect(&fileTransferJob, &FileTransferJob::fileExists, this, &UntrashJob::fileExists, Qt::DirectConnection); + + // cancel the file transfer subjob if the parent job is cancelled + connect(this, &UntrashJob::cancelled, &fileTransferJob, + [&fileTransferJob]() { + if(!fileTransferJob.isCancelled()) { + fileTransferJob.cancel(); + } + }, Qt::DirectConnection); + + // cancel the parent job if the file transfer subjob is cancelled + connect(&fileTransferJob, &FileTransferJob::cancelled, this, + [this]() { + if(!isCancelled()) { + cancel(); + } + }, Qt::DirectConnection); + fileTransferJob.run(); +} + +} // namespace Fm diff --git a/src/core/untrashjob.h b/src/core/untrashjob.h new file mode 100644 index 0000000..22a5129 --- /dev/null +++ b/src/core/untrashjob.h @@ -0,0 +1,22 @@ +#ifndef FM2_UNTRASHJOB_H +#define FM2_UNTRASHJOB_H + +#include "../libfmqtglobals.h" +#include "fileoperationjob.h" + +namespace Fm { + +class LIBFM_QT_API UntrashJob : public FileOperationJob { +public: + explicit UntrashJob(FilePathList srcPaths); + +protected: + void exec() override; + +private: + FilePathList srcPaths_; +}; + +} // namespace Fm + +#endif // FM2_UNTRASHJOB_H diff --git a/src/core/userinfocache.cpp b/src/core/userinfocache.cpp new file mode 100644 index 0000000..1c237f0 --- /dev/null +++ b/src/core/userinfocache.cpp @@ -0,0 +1,47 @@ +#include "userinfocache.h" +#include +#include + +namespace Fm { + +UserInfoCache* UserInfoCache::globalInstance_ = nullptr; +std::mutex UserInfoCache::mutex_; + +UserInfoCache::UserInfoCache() : QObject() { +} + +const std::shared_ptr& UserInfoCache::userFromId(uid_t uid) { + std::lock_guard lock{mutex_}; + auto it = users_.find(uid); + if(it != users_.end()) + return it->second; + std::shared_ptr user; + auto pw = getpwuid(uid); + if(pw) { + user = std::make_shared(uid, pw->pw_name, pw->pw_gecos); + } + return (users_[uid] = user); +} + +const std::shared_ptr& UserInfoCache::groupFromId(gid_t gid) { + std::lock_guard lock{mutex_}; + auto it = groups_.find(gid); + if(it != groups_.end()) + return it->second; + std::shared_ptr group; + auto gr = getgrgid(gid); + if(gr) { + group = std::make_shared(gid, gr->gr_name); + } + return (groups_[gid] = group); +} + +// static +UserInfoCache* UserInfoCache::globalInstance() { + std::lock_guard lock{mutex_}; + if(!globalInstance_) + globalInstance_ = new UserInfoCache(); + return globalInstance_; +} + +} // namespace Fm diff --git a/src/core/userinfocache.h b/src/core/userinfocache.h new file mode 100644 index 0000000..7338463 --- /dev/null +++ b/src/core/userinfocache.h @@ -0,0 +1,82 @@ +#ifndef FM2_USERINFOCACHE_H +#define FM2_USERINFOCACHE_H + +#include "../libfmqtglobals.h" +#include +#include +#include +#include +#include +#include + +namespace Fm { + +class LIBFM_QT_API UserInfo { +public: + explicit UserInfo(uid_t uid, const char* name, const char* realName): + uid_{uid}, name_{name}, realName_{realName} { + } + + uid_t uid() const { + return uid_; + } + + const QString& name() const { + return name_; + } + + const QString& realName() const { + return realName_; + } + +private: + uid_t uid_; + QString name_; + QString realName_; + +}; + +class LIBFM_QT_API GroupInfo { +public: + explicit GroupInfo(gid_t gid, const char* name): gid_{gid}, name_{name} { + } + + gid_t gid() const { + return gid_; + } + + const QString& name() const { + return name_; + } + +private: + gid_t gid_; + QString name_; +}; + +// FIXME: handle file changes + +class LIBFM_QT_API UserInfoCache : public QObject { + Q_OBJECT +public: + explicit UserInfoCache(); + + const std::shared_ptr& userFromId(uid_t uid); + + const std::shared_ptr& groupFromId(gid_t gid); + + static UserInfoCache* globalInstance(); + +Q_SIGNALS: + void changed(); + +private: + std::unordered_map> users_; + std::unordered_map> groups_; + static UserInfoCache* globalInstance_; + static std::mutex mutex_; +}; + +} // namespace Fm + +#endif // FM2_USERINFOCACHE_H diff --git a/src/core/vfs/fm-file.c b/src/core/vfs/fm-file.c new file mode 100644 index 0000000..5f0f217 --- /dev/null +++ b/src/core/vfs/fm-file.c @@ -0,0 +1,118 @@ +/* + * fm-file.c + * + * Copyright 2012-2013 Andriy Grytsenko (LStranger) + * + * This file is a part of the Libfm library. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/** + * SECTION:fm-file + * @short_description: Extensions for GFile interface. + * @title: FmFile + * + * @include: libfm/fm.h + * + * The #FmFile represents interface to build extensions to GFile which + * will handle schemas that are absent in Glib/GVFS - such as "search:". + * + * To use it the GFile implementation should also implement FmFile vtable + * calls. The implementation should be added to list of known schemes via + * call to fm_file_add_vfs() then calls such as fm_file_new_for_uri() can + * use it. + */ + +#ifdef HAVE_CONFIG_H +#include +#endif +#include "glib-compat.h" +#define CHECK_MODULES() // this is not needed for libfm-qt + +#include "fm-file.h" + +#include + +static GHashTable *schemes = NULL; + +#define FM_FILE_MODULE_MIN_VERSION 1 +#define FM_FILE_MODULE_MAX_VERSION 1 + +G_LOCK_DEFINE_STATIC(vfs); + +G_DEFINE_INTERFACE(FmFile, fm_file, G_TYPE_FILE) + +static gboolean fm_file_wants_incremental_false(GFile *unused) +{ + return FALSE; +} + +static void fm_file_default_init(FmFileInterface *iface) +{ + iface->wants_incremental = fm_file_wants_incremental_false; +} + + +static inline FmFileInitTable *fm_find_scheme(const char *name) +{ + FmFileInitTable *t; + CHECK_MODULES(); + G_LOCK(vfs); + t = (FmFileInitTable*)g_hash_table_lookup(schemes, name); + G_UNLOCK(vfs); + return t; +} + +/** + * fm_file_add_vfs + * @name: scheme to act upon + * @init: table of functions + * + * Adds VFS to list of extensions that will be applied on next call to + * fm_file_new_for_uri() or fm_file_new_for_commandline_arg(). The @name + * is a schema which will be handled by those calls. + * + * Since: 1.0.2 + */ +void fm_file_add_vfs(const char *name, FmFileInitTable *init) +{ + G_LOCK(vfs); + if(g_hash_table_lookup(schemes, name) == NULL) + g_hash_table_insert(schemes, g_strdup(name), init); + G_UNLOCK(vfs); +} + +/** + * fm_file_wants_incremental + * @file: file to inspect + * + * Checks if contents of directory @file cannot be retrieved at once so + * scanning it may be done in incremental manner for the best results. + * + * Returns: %TRUE if retrieve of contents of @file will be incremental. + * + * Since: 1.0.2 + */ +gboolean fm_file_wants_incremental(GFile* file) +{ + FmFileInterface *iface; + + g_return_val_if_fail(file != NULL, FALSE); + if(!FM_IS_FILE(file)) + return FALSE; + iface = FM_FILE_GET_IFACE(file); + return iface->wants_incremental ? iface->wants_incremental(file) : FALSE; +} diff --git a/src/core/vfs/fm-file.h b/src/core/vfs/fm-file.h new file mode 100644 index 0000000..37bf0d0 --- /dev/null +++ b/src/core/vfs/fm-file.h @@ -0,0 +1,100 @@ +/* + * fm-file.h + * + * Copyright 2012 Andriy Grytsenko (LStranger) + * + * This file is a part of the Libfm library. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef _FM_FILE_H_ +#define _FM_FILE_H_ 1 + +// #include "fm-module.h" + +#include +#include + +G_BEGIN_DECLS + +#define FM_TYPE_FILE (fm_file_get_type()) +#define FM_FILE(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj),\ + FM_TYPE_FILE, FmFile)) +#define FM_IS_FILE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj),\ + FM_TYPE_FILE)) +#define FM_FILE_GET_IFACE(obj) (G_TYPE_INSTANCE_GET_INTERFACE((obj),\ + FM_TYPE_FILE, FmFileInterface)) + +typedef struct _FmFile FmFile; /* Dummy typedef */ +typedef struct _FmFileInterface FmFileInterface; + +/** + * FmFileInterface: + * @wants_incremental: VTable func, see fm_file_wants_incremental() + */ +struct _FmFileInterface +{ + /*< private >*/ + GTypeInterface g_iface; + + /*< public >*/ + gboolean (*wants_incremental)(GFile* file); + + /*< private >*/ + gpointer _reserved1; + gpointer _reserved2; +}; + +typedef struct _FmFileInitTable FmFileInitTable; + +/** + * FmFileInitTable: + * @new_for_uri: function to create new #GFile object from URI + * + * Functions to initialize FmFile instance. + * + * This structure is used for "vfs" module initialization. The key for + * module of this type is scheme name to support. + */ +struct _FmFileInitTable +{ + /*< public >*/ + GFile * (*new_for_uri)(const char *uri); + /*< private >*/ + gpointer _reserved1; + gpointer _reserved2; +}; + +GType fm_file_get_type(void); + +void fm_file_add_vfs(const char *name, FmFileInitTable *init); + +/* VTable calls */ +gboolean fm_file_wants_incremental(GFile* file); + +/* Gfile functions replacements */ +GFile *fm_file_new_for_uri(const char *uri); +GFile *fm_file_new_for_commandline_arg(const char *arg); + +void _fm_file_init(void); +void _fm_file_finalize(void); + +/* for modules */ +#define FM_MODULE_vfs_VERSION 1 +// extern FmFileInitTable fm_module_init_vfs; + +G_END_DECLS +#endif /* _FM_FILE_H_ */ diff --git a/src/core/vfs/fm-xml-file.c b/src/core/vfs/fm-xml-file.c new file mode 100644 index 0000000..aab03c5 --- /dev/null +++ b/src/core/vfs/fm-xml-file.c @@ -0,0 +1,1519 @@ +/* + * fm-xml-file.c + * + * Copyright 2013 Andriy Grytsenko (LStranger) + * + * This file is a part of libfm-extra package. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/** + * SECTION:fm-xml-file + * @short_description: Simple XML parser. + * @title: FmXmlFile + * + * @include: libfm/fm-extra.h + * + * The FmXmlFile represents content of some XML file in form that can + * be altered and saved later. + * + * This parser has some simplifications on XML parsing: + * * Only UTF-8 encoding is supported + * * No user-defined entities, those should be converted externally + * * Processing instructions, comments and the doctype declaration are parsed but are not interpreted in any way + * The markup format does support: + * * Elements + * * Attributes + * * 5 standard entities: &amp; &lt; &gt; &quot; &apos; + * * Character references + * * Sections marked as CDATA + * + * The application should respect g_type_init() if this parser is used + * without usage of libfm. + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "fm-xml-file.h" + +#include +#include "glib-compat.h" + +#include +#include + +typedef struct +{ + gchar *name; + FmXmlFileHandler handler; + gboolean in_line : 1; +} FmXmlFileTagDesc; + +struct _FmXmlFile +{ + GObject parent; + GList *items; + GString *data; + char *comment_pre; + FmXmlFileItem *current_item; + FmXmlFileTagDesc *tags; /* tags[0].name contains DTD */ + guint n_tags; /* number of elements in tags */ + guint line, pos; +}; + +struct _FmXmlFileClass +{ + GObjectClass parent_class; +}; + +struct _FmXmlFileItem +{ + FmXmlFileTag tag; + union { + gchar *tag_name; /* only for tag == FM_XML_FILE_TAG_NOT_HANDLED */ + gchar *text; /* only for tag == FM_XML_FILE_TEXT, NULL if directive */ + }; + char **attribute_names; + char **attribute_values; + FmXmlFile *file; + FmXmlFileItem *parent; + GList **parent_list; /* points to file->items or to parent->children */ + GList *children; + gchar *comment; /* a little trick: it is equal to text if it is CDATA */ +}; + + +G_DEFINE_TYPE(FmXmlFile, fm_xml_file, G_TYPE_OBJECT); + +static void fm_xml_file_finalize(GObject *object) +{ + FmXmlFile *self; + guint i; + + g_return_if_fail(object != NULL); + g_return_if_fail(FM_IS_XML_FILE(object)); + + self = (FmXmlFile*)object; + self->current_item = NULL; /* we destroying it */ + while (self->items) + { + g_assert(((FmXmlFileItem*)self->items->data)->file == self); + g_assert(((FmXmlFileItem*)self->items->data)->parent == NULL); + fm_xml_file_item_destroy(self->items->data); + } + for (i = 0; i < self->n_tags; i++) + g_free(self->tags[i].name); + g_free(self->tags); + if (self->data) + g_string_free(self->data, TRUE); + g_free(self->comment_pre); + + G_OBJECT_CLASS(fm_xml_file_parent_class)->finalize(object); +} + +static void fm_xml_file_class_init(FmXmlFileClass *klass) +{ + GObjectClass *g_object_class; + + g_object_class = G_OBJECT_CLASS(klass); + g_object_class->finalize = fm_xml_file_finalize; +} + +static void fm_xml_file_init(FmXmlFile *self) +{ + self->tags = g_new0(FmXmlFileTagDesc, 1); + self->n_tags = 1; + self->line = 1; +} + +/** + * fm_xml_file_new + * @sibling: (allow-none): container to copy handlers data + * + * Creates new empty #FmXmlFile container. If @sibling is not %NULL + * then new container will have callbacks identical to set in @sibling. + * Use @sibling parameter if you need to work with few XML files that + * share the same schema or if you need to use the same tag ids for more + * than one file. + * + * Returns: (transfer full): newly created object. + * + * Since: 1.2.0 + */ +FmXmlFile *fm_xml_file_new(FmXmlFile *sibling) +{ + FmXmlFile *self; + FmXmlFileTag i; + + self = (FmXmlFile*)g_object_new(FM_XML_FILE_TYPE, NULL); + if (sibling && sibling->n_tags > 1) + { + self->n_tags = sibling->n_tags; + self->tags = g_renew(FmXmlFileTagDesc, self->tags, self->n_tags); + for (i = 1; i < self->n_tags; i++) + { + self->tags[i].name = g_strdup(sibling->tags[i].name); + self->tags[i].handler = sibling->tags[i].handler; + } + } + return self; +} + +/** + * fm_xml_file_set_handler + * @file: the parser container + * @tag: tag to use @handler for + * @handler: callback for @tag + * @in_line: %TRUE if tag should not receive new line by fm_xml_file_to_data() + * @error: (allow-none) (out): location to save error + * + * Sets @handler for @file to be called on parse when @tag is found + * in XML data. This function will fail if some handler for @tag was + * aready set, in this case @error will be set appropriately. + * + * Returns: id for the @tag. + * + * Since: 1.2.0 + */ +FmXmlFileTag fm_xml_file_set_handler(FmXmlFile *file, const char *tag, + FmXmlFileHandler handler, gboolean in_line, + GError **error) +{ + FmXmlFileTag i; + + g_return_val_if_fail(file != NULL && FM_IS_XML_FILE(file), FM_XML_FILE_TAG_NOT_HANDLED); + g_return_val_if_fail(handler != NULL, FM_XML_FILE_TAG_NOT_HANDLED); + g_return_val_if_fail(tag != NULL, FM_XML_FILE_TAG_NOT_HANDLED); + for (i = 1; i < file->n_tags; i++) + if (strcmp(file->tags[i].name, tag) == 0) + { + g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT, + _("Duplicate handler for tag <%s>"), tag); + return i; + } + file->tags = g_renew(FmXmlFileTagDesc, file->tags, i + 1); + file->tags[i].name = g_strdup(tag); + file->tags[i].handler = handler; + file->tags[i].in_line = in_line; + file->n_tags = i + 1; + /* g_debug("XML parser: added handler '%s' id %u", tag, (guint)i); */ + return i; +} + + +/* parse */ + +/* + * This function was taken from GLib sources and adapted to be used here. + * Copyright 2000, 2003 Red Hat, Inc. + * + * re-write the GString in-place, unescaping anything that escaped. + * most XML does not contain entities, or escaping. + */ +static gboolean +unescape_gstring_inplace (//GMarkupParseContext *context, + GString *string, + //gboolean *is_ascii, + guint *line_num, + guint *pos, + gboolean normalize_attribute, + GError **error) +{ + //char mask, *to; + char *to; + //int line_num = 1; + const char *from, *sol; + + //*is_ascii = FALSE; + + /* + * Meeks' theorum: unescaping can only shrink text. + * for < etc. this is obvious, for ￿ more + * thought is required, but this is patently so. + */ + //mask = 0; + for (from = to = string->str; *from != '\0'; from++, to++) + { + *to = *from; + + //mask |= *to; + if (*to == '\n') + { + (*line_num)++; + *pos = 0; + } + if (normalize_attribute && (*to == '\t' || *to == '\n')) + *to = ' '; + if (*to == '\r') + { + *to = normalize_attribute ? ' ' : '\n'; + if (from[1] == '\n') + { + from++; + (*line_num)++; + *pos = 0; + } + } + sol = from; + if (*from == '&') + { + from++; + if (*from == '#') + { + gboolean is_hex = FALSE; + gulong l; + gchar *end = NULL; + + from++; + + if (*from == 'x') + { + is_hex = TRUE; + from++; + } + + /* digit is between start and p */ + errno = 0; + if (is_hex) + l = strtoul (from, &end, 16); + else + l = strtoul (from, &end, 10); + + if (end == from || errno != 0) + { + g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + _("Failed to parse '%-.*s', which " + "should have been a digit " + "inside a character reference " + "(ê for example) - perhaps " + "the digit is too large"), + (int)(end - from), from); + return FALSE; + } + else if (*end != ';') + { + g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + _("Character reference did not end with a " + "semicolon; " + "most likely you used an ampersand " + "character without intending to start " + "an entity - escape ampersand as &")); + return FALSE; + } + else + { + /* characters XML 1.1 permits */ + if ((0 < l && l <= 0xD7FF) || + (0xE000 <= l && l <= 0xFFFD) || + (0x10000 <= l && l <= 0x10FFFF)) + { + gchar buf[8]; + memset (buf, 0, 8); + g_unichar_to_utf8 (l, buf); + strncpy (to, buf, 8); + to += strlen (buf) - 1; + from = end; + //if (l >= 0x80) /* not ascii */ + //mask |= 0x80; + } + else + { + g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + _("Character reference '%-.*s' does not " + "encode a permitted character"), + (int)(end - from), from); + return FALSE; + } + } + } + + else if (strncmp (from, "lt;", 3) == 0) + { + *to = '<'; + from += 2; + } + else if (strncmp (from, "gt;", 3) == 0) + { + *to = '>'; + from += 2; + } + else if (strncmp (from, "amp;", 4) == 0) + { + *to = '&'; + from += 3; + } + else if (strncmp (from, "quot;", 5) == 0) + { + *to = '"'; + from += 4; + } + else if (strncmp (from, "apos;", 5) == 0) + { + *to = '\''; + from += 4; + } + else + { + if (*from == ';') + g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + _("Empty entity '&;' seen; valid " + "entities are: & " < > '")); + else + { + const char *end = strchr (from, ';'); + if (end) + g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + _("Entity name '%-.*s' is not known"), + (int)(end-from), from); + else + g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + _("Entity did not end with a semicolon; " + "most likely you used an ampersand " + "character without intending to start " + "an entity - escape ampersand as &")); + } + return FALSE; + } + } + *pos += (from - sol) + 1; + } + + /* g_debug("unescape_gstring_inplace completed"); */ + g_assert (to - string->str <= (gint)string->len); + if (to - string->str != (gint)string->len) + g_string_truncate (string, to - string->str); + + //*is_ascii = !(mask & 0x80); + + return TRUE; +} + + +static inline void _update_file_ptr_part(FmXmlFile *file, const char *start, + const char *end) +{ + while (start < end) + { + if (*start == '\n') + { + file->line++; + file->pos = 0; + } + else + /* FIXME: advance by chars not bytes? */ + file->pos++; + start++; + } +} + +static inline void _update_file_ptr(FmXmlFile *file, int add_cols) +{ + guint i; + char *p; + + for (i = file->data->len, p = file->data->str; i > 0; i--, p++) + { + if (*p == '\n') + { + file->line++; + file->pos = 0; + } + else + /* FIXME: advance by chars not bytes? */ + file->pos++; + } + file->pos += add_cols; +} + +static inline gboolean _is_space(char c) +{ + return (c == ' ' || c == '\t' || c == '\n' || c == '\r'); +} + +/** + * fm_xml_file_parse_data + * @file: the parser container + * @text: data to parse + * @size: size of @text + * @error: (allow-none) (out): location to save error + * @user_data: data to pass to handlers + * + * Parses next chunk of @text data. Parsing stops at end of data or at any + * error. In latter case @error will be set appropriately. + * + * See also: fm_xml_file_finish_parse(). + * + * Returns: %FALSE if parsing failed. + * + * Since: 1.2.0 + */ +gboolean fm_xml_file_parse_data(FmXmlFile *file, const char *text, + gsize size, GError **error, gpointer user_data) +{ + gsize ptr, len; + char *dst, *end, *tag, *name, *value; + GString *buff; + FmXmlFileItem *item; + gboolean closing, selfdo; + FmXmlFileTag i; + char **attrib_names, **attrib_values; + guint attribs; + char quote; + + g_return_val_if_fail(file != NULL && FM_IS_XML_FILE(file), FALSE); +_restart: + if (size == 0) + return TRUE; + /* if file->data has '<' as first char then we stopped at tag */ + if (file->data && file->data->len && file->data->str[0] == '<') + { + for (ptr = 0; ptr < size; ptr++) + if (text[ptr] == '>') + break; + if (ptr == size) /* still no end of that tag */ + { + g_string_append_len(file->data, text, size); + return TRUE; + } + /* we got a complete tag, nice, let parse it */ + g_string_append_len(file->data, text, ptr); + ptr++; + text += ptr; + size -= ptr; + /* check for CDATA first */ + if (file->data->len >= 11 /* data->str, "data->str + file->data->len; + if (end[-2] != ']' || end[-1] != ']') /* find end of CDATA */ + { + g_string_append_c(file->data, '>'); + goto _restart; + } + if (file->current_item == NULL) /* CDATA at top level! */ + g_warning("FmXmlFile: line %u: junk CDATA in XML file ignored", + file->line); + else + { + item = fm_xml_file_item_new(FM_XML_FILE_TEXT); + item->text = item->comment = g_strndup(&file->data->str[9], + file->data->len - 11); + fm_xml_file_item_append_child(file->current_item, item); + } + _update_file_ptr(file, 1); + g_string_truncate(file->data, 0); + goto _restart; + } + /* check for comment */ + if (file->data->len >= 7 /* ", item->comment); + escaped = g_markup_escape_text(item->text, -1); + g_string_append(string, escaped); + g_free(escaped); + } + else /* processing directive */ + { + g_string_append_printf(string, "%s", prefix->str, item->comment); + *has_nl = TRUE; + } + return TRUE; + default: + if (item->tag >= file->n_tags) + { +_no_tag: + g_set_error_literal(error, G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT, + _("fm_xml_file_to_data: XML data error")); + return FALSE; + } + tag_name = file->tags[item->tag].name; +_do_tag: + /* do comment */ + if (item->comment != NULL) + g_string_append_printf(string, "%s", prefix->str, + item->comment); + else if (item->attribute_names == NULL && item->children == NULL && + file->tags[item->tag].in_line) + { + /* don't add prefix if it is simple tag such as
*/ + g_string_append_printf(string, "<%s/>", tag_name); + return TRUE; + } + /* start the tag */ + g_string_append_printf(string, "%s<%s", prefix->str, tag_name); + /* do attributes */ + if (item->attribute_names) + { + char **name = item->attribute_names; + char **value = item->attribute_values; + while (*name) + { + if (*value) + { + char *escaped = g_markup_escape_text(*value, -1); + g_string_append_printf(string, " %s='%s'", *name, escaped); + g_free(escaped); + } /* else error? */ + name++; + value++; + } + } + if (item->children == NULL) + { + /* handle empty tags such as */ + g_string_append(string, "/>"); + *has_nl = TRUE; + return TRUE; + } + g_string_append_c(string, '>'); + } + /* do with children */ + *has_nl = FALSE; /* to collect data from nested elements */ + g_string_append(prefix, " "); + for (l = item->children; l; l = l->next) + if (!_parser_item_to_gstring(file, string, l->data, prefix, has_nl, error)) + break; + g_string_truncate(prefix, prefix->len - 4); + if (l != NULL) /* failed */ + return FALSE; + /* close the tag */ + g_string_append_printf(string, "%s", (*has_nl) ? prefix->str : "", + tag_name); + *has_nl = TRUE; /* it was prefixed above */ + return TRUE; +} + +/** + * fm_xml_file_to_data + * @file: the parser container + * @text_size: (allow-none) (out): location to save size of returned data + * @error: (allow-none) (out): location to save error + * + * Prepares string representation (XML text) for the data that are in + * the container. Returned data should be freed with g_free() after + * usage. + * + * Returns: (transfer full): XML text representing data in @file. + * + * Since: 1.2.0 + */ +char *fm_xml_file_to_data(FmXmlFile *file, gsize *text_size, GError **error) +{ + GString *string, *prefix; + GList *l; + gboolean has_nl = FALSE; + + g_return_val_if_fail(file != NULL && FM_IS_XML_FILE(file), NULL); + string = g_string_sized_new(512); + prefix = g_string_new("\n"); + if (G_LIKELY(file->tags[0].name)) + g_string_printf(string, "", file->tags[0].name); + for (l = file->items; l; l = l->next) + if (!_parser_item_to_gstring(file, string, l->data, prefix, &has_nl, error)) + break; /* if failed then l != NULL */ + g_string_free(prefix, TRUE); + if (text_size) + *text_size = string->len; + return g_string_free(string, (l != NULL)); /* returns NULL if failed */ +} + + +/* item data accessor functions */ + +/** + * fm_xml_file_item_get_comment + * @item: the file element to inspect + * + * If an element @item has a comment ahead of it then retrieves that + * comment. The returned data are owned by @item and should not be freed + * nor otherwise altered by caller. + * + * Returns: (transfer none): comment or %NULL if no comment is set. + * + * Since: 1.2.0 + */ +const char *fm_xml_file_item_get_comment(FmXmlFileItem *item) +{ + g_return_val_if_fail(item != NULL, NULL); + return item->comment; +} + +/** + * fm_xml_file_item_get_children + * @item: the file element to inspect + * + * Retrieves list of children for @item that are known to the parser. + * Returned list should be freed by g_list_free() after usage. + * + * Note: any text between opening tag and closing tag such as + * |[ <Tag>Some text</Tag> ]| + * is presented as a child of special type #FM_XML_FILE_TEXT and can be + * retrieved from that child, not from the item representing <Tag>. + * + * See also: fm_xml_file_item_get_data(). + * + * Returns: (transfer container) (element-type FmXmlFileItem): children list. + * + * Since: 1.2.0 + */ +GList *fm_xml_file_item_get_children(FmXmlFileItem *item) +{ + g_return_val_if_fail(item != NULL, NULL); + return g_list_copy(item->children); +} + +/** + * fm_xml_file_item_find_child + * @item: the file element to inspect + * @tag: tag id to search among children + * + * Searches for first child of @item which have child with tag id @tag. + * Returned data are owned by file and should not be freed by caller. + * + * Returns: (transfer none): found child or %NULL if no such child was found. + * + * Since: 1.2.0 + */ +FmXmlFileItem *fm_xml_file_item_find_child(FmXmlFileItem *item, FmXmlFileTag tag) +{ + GList *l; + + for (l = item->children; l; l = l->next) + if (((FmXmlFileItem*)l->data)->tag == tag) + return (FmXmlFileItem*)l->data; + return NULL; +} + +/** + * fm_xml_file_item_get_tag + * @item: the file element to inspect + * + * Retrieves tag id of @item. + * + * Returns: tag id. + * + * Since: 1.2.0 + */ +FmXmlFileTag fm_xml_file_item_get_tag(FmXmlFileItem *item) +{ + g_return_val_if_fail(item != NULL, FM_XML_FILE_TAG_NOT_HANDLED); + return item->tag; +} + +/** + * fm_xml_file_item_get_data + * @item: the file element to inspect + * @text_size: (allow-none) (out): location to save data size + * + * Retrieves text data from @item of type #FM_XML_FILE_TEXT. Returned + * data are owned by file and should not be freed nor altered. + * + * Returns: (transfer none): text data or %NULL if @item isn't text data. + * + * Since: 1.2.0 + */ +const char *fm_xml_file_item_get_data(FmXmlFileItem *item, gsize *text_size) +{ + if (text_size) + *text_size = 0; + g_return_val_if_fail(item != NULL, NULL); + if (item->tag != FM_XML_FILE_TEXT) + return NULL; + if (text_size && item->text != NULL) + *text_size = strlen(item->text); + return item->text; +} + +/** + * fm_xml_file_item_get_parent + * @item: the file element to inspect + * + * Retrieves parent element of @item if the @item has one. Returned data + * are owned by file and should not be freed by caller. + * + * Returns: (transfer none): parent element or %NULL if element has no parent. + * + * Since: 1.2.0 + */ +FmXmlFileItem *fm_xml_file_item_get_parent(FmXmlFileItem *item) +{ + g_return_val_if_fail(item != NULL, NULL); + return item->parent; +} + +/** + * fm_xml_file_get_tag_name + * @file: the parser container + * @tag: the tag id to inspect + * + * Retrieves tag for its id. Returned data are owned by @file and should + * not be modified by caller. + * + * Returns: (transfer none): tag string representation. + * + * Since: 1.2.0 + */ +const char *fm_xml_file_get_tag_name(FmXmlFile *file, FmXmlFileTag tag) +{ + g_return_val_if_fail(file != NULL && FM_IS_XML_FILE(file), NULL); + g_return_val_if_fail(tag > 0 && tag < file->n_tags, NULL); + return file->tags[tag].name; +} + +/** + * fm_xml_file_item_get_tag_name + * @item: the file element to inspect + * + * Retrieves tag for its id. Returned data are owned by @item and should + * not be modified by caller. + * + * Returns: (transfer none): tag string representation or %NULL if @item is text. + * + * Since: 1.2.0 + */ +const char *fm_xml_file_item_get_tag_name(FmXmlFileItem *item) +{ + g_return_val_if_fail(item != NULL, NULL); + switch (item->tag) + { + case FM_XML_FILE_TEXT: + return NULL; + case FM_XML_FILE_TAG_NOT_HANDLED: + return item->tag_name; + default: + return item->file->tags[item->tag].name; + } +} diff --git a/src/core/vfs/fm-xml-file.h b/src/core/vfs/fm-xml-file.h new file mode 100644 index 0000000..e31f332 --- /dev/null +++ b/src/core/vfs/fm-xml-file.h @@ -0,0 +1,122 @@ +/* + * fm-xml-file.h + * + * Copyright 2013 Andriy Grytsenko (LStranger) + * + * This file is a part of libfm-extra package. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * 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_XML_FILE_H__ +#define __FM_XML_FILE_H__ 1 + +#include + +G_BEGIN_DECLS + +#define FM_XML_FILE_TYPE (fm_xml_file_get_type()) +#define FM_IS_XML_FILE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), FM_XML_FILE_TYPE)) + +typedef struct _FmXmlFile FmXmlFile; +typedef struct _FmXmlFileClass FmXmlFileClass; + +GType fm_xml_file_get_type(void); + +typedef struct _FmXmlFileItem FmXmlFileItem; +typedef guint FmXmlFileTag; + +/** + * FM_XML_FILE_TAG_NOT_HANDLED: + * + * Value of FmXmlFileTag which means this element has no handler installed. + */ +#define FM_XML_FILE_TAG_NOT_HANDLED 0 + +/** + * FM_XML_FILE_TEXT: + * + * Value of FmXmlFileTag which means this element has parsed character data. + */ +#define FM_XML_FILE_TEXT (FmXmlFileTag)-1 + +/** + * FmXmlFileHandler + * @item: XML element being parsed + * @children: (element-type FmXmlFileItem): elements found in @item + * @attribute_names: attributes names list for @item + * @attribute_values: attributes values list for @item + * @n_attributes: list length of @attribute_names and @attribute_values + * @line: current line number in the file (starting from 1) + * @pos: current pos number in the file (starting from 0) + * @error: (allow-none) (out): location to save error + * @user_data: data passed to fm_xml_file_parse_data() + * + * Callback for processing some element in XML file. + * It will be called at closing tag. + * + * Returns: %TRUE if no errors were found by handler. + * + * Since: 1.2.0 + */ +typedef gboolean (*FmXmlFileHandler)(FmXmlFileItem *item, GList *children, + char * const *attribute_names, + char * const *attribute_values, + guint n_attributes, gint line, gint pos, + GError **error, gpointer user_data); + +/* setup */ +FmXmlFile *fm_xml_file_new(FmXmlFile *sibling); +FmXmlFileTag fm_xml_file_set_handler(FmXmlFile *file, const char *tag, + FmXmlFileHandler handler, gboolean in_line, + GError **error); + +/* parse */ +gboolean fm_xml_file_parse_data(FmXmlFile *file, const char *text, + gsize size, GError **error, gpointer user_data); +GList *fm_xml_file_finish_parse(FmXmlFile *file, GError **error); +gint fm_xml_file_get_current_line(FmXmlFile *file, gint *pos); +const char *fm_xml_file_get_dtd(FmXmlFile *file); + +/* item manipulations */ +FmXmlFileItem *fm_xml_file_item_new(FmXmlFileTag tag); +void fm_xml_file_item_append_text(FmXmlFileItem *item, const char *text, + gssize text_size, gboolean cdata); +gboolean fm_xml_file_item_append_child(FmXmlFileItem *item, FmXmlFileItem *child); +void fm_xml_file_item_set_comment(FmXmlFileItem *item, const char *comment); +gboolean fm_xml_file_item_set_attribute(FmXmlFileItem *item, + const char *name, const char *value); +gboolean fm_xml_file_item_destroy(FmXmlFileItem *item); + +gboolean fm_xml_file_insert_before(FmXmlFileItem *item, FmXmlFileItem *new_item); +gboolean fm_xml_file_insert_first(FmXmlFile *file, FmXmlFileItem *new_item); + +/* save XML */ +void fm_xml_file_set_dtd(FmXmlFile *file, const char *dtd, GError **error); +char *fm_xml_file_to_data(FmXmlFile *file, gsize *text_size, GError **error); + +/* item data accessor functions */ +const char *fm_xml_file_item_get_comment(FmXmlFileItem *item); +GList *fm_xml_file_item_get_children(FmXmlFileItem *item); +FmXmlFileItem *fm_xml_file_item_find_child(FmXmlFileItem *item, FmXmlFileTag tag); +FmXmlFileTag fm_xml_file_item_get_tag(FmXmlFileItem *item); +const char *fm_xml_file_item_get_data(FmXmlFileItem *item, gsize *text_size); +FmXmlFileItem *fm_xml_file_item_get_parent(FmXmlFileItem *item); +const char *fm_xml_file_item_get_tag_name(FmXmlFileItem *item); +const char *fm_xml_file_get_tag_name(FmXmlFile *file, FmXmlFileTag tag); + +G_END_DECLS + +#endif /* __FM_XML_FILE_H__ */ diff --git a/src/core/vfs/vfs-menu.c b/src/core/vfs/vfs-menu.c new file mode 100644 index 0000000..12289ee --- /dev/null +++ b/src/core/vfs/vfs-menu.c @@ -0,0 +1,3137 @@ +/* + * fm-vfs-menu.c + * VFS for "menu://applications/" path using menu-cache library. + * + * Copyright 2012-2014 Andriy Grytsenko (LStranger) + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "fm-file.h" +#include "glib-compat.h" +// #include "fm-utils.h" +#include "fm-xml-file.h" + +#include +#include + +/* support for libmenu-cache 0.4.x */ +#ifndef MENU_CACHE_CHECK_VERSION +# ifdef HAVE_MENU_CACHE_DIR_LIST_CHILDREN +# define MENU_CACHE_CHECK_VERSION(_a,_b,_c) (_a == 0 && _b < 5) /* < 0.5.0 */ +# else +# define MENU_CACHE_CHECK_VERSION(_a,_b,_c) 0 /* not even 0.4.0 */ +# endif +#endif + +/* libmenu-cache is multithreaded since 0.4.x */ +#if MENU_CACHE_CHECK_VERSION(0, 4, 0) +# define RUN_WITH_MENU_CACHE(__func,__data) __func(__data) +#else +# define RUN_WITH_MENU_CACHE(__func,__data) fm_run_in_default_main_context(__func,__data) +#endif + +/* beforehand declarations */ +GFile *_fm_vfs_menu_new_for_uri(const char *uri); + + +/* ---- applications.menu manipulations ---- */ +typedef struct _FmMenuMenuTree FmMenuMenuTree; + +struct _FmMenuMenuTree +{ + FmXmlFile *menu; /* composite tree to analyze */ + char *file_path; /* current file */ + GCancellable *cancellable; + gint line, pos; /* we remember position in deepest file */ +}; + +G_LOCK_DEFINE_STATIC(menuTree); /* locks all .menu file access data below */ +static FmXmlFileTag menuTag_Menu = 0; /* tags that are supported */ +static FmXmlFileTag menuTag_Include = 0; +static FmXmlFileTag menuTag_Exclude = 0; +static FmXmlFileTag menuTag_Filename = 0; +static FmXmlFileTag menuTag_Category = 0; +static FmXmlFileTag menuTag_MergeFile = 0; +static FmXmlFileTag menuTag_Directory = 0; +static FmXmlFileTag menuTag_Name = 0; +static FmXmlFileTag menuTag_Deleted = 0; +static FmXmlFileTag menuTag_NotDeleted = 0; + +/* this handler does nothing, used just to remember its id */ +static gboolean _menu_xml_handler_pass(FmXmlFileItem *item, GList *children, + char * const *attribute_names, + char * const *attribute_values, + guint n_attributes, gint line, gint pos, + GError **error, gpointer user_data) +{ + FmMenuMenuTree *data = user_data; + return !g_cancellable_set_error_if_cancelled(data->cancellable, error); +} + +/* FIXME: handle ...... */ + +static inline const char *_get_menu_name(FmXmlFileItem *item) +{ + if (fm_xml_file_item_get_tag(item) != menuTag_Menu) /* skip not menu */ + return NULL; + item = fm_xml_file_item_find_child(item, menuTag_Name); + if (item == NULL) /* no Name tag? */ + return NULL; + item = fm_xml_file_item_find_child(item, FM_XML_FILE_TEXT); + if (item == NULL) /* empty Name tag? */ + return NULL; + return fm_xml_file_item_get_data(item, NULL); +} + +static FmXmlFileItem *_find_in_children(GList *list, const char *path) +{ + const char *ptr; + char *_ptr; + + if (list == NULL) + return NULL; + g_debug("menu tree: searching for '%s'", path); + ptr = strchr(path, '/'); + if (ptr == NULL) + { + ptr = path; + path = _ptr = NULL; + } + else + { + _ptr = g_strndup(path, ptr - path); + path = ptr + 1; + ptr = _ptr; + } + while (list) + { + const char *elem_name = _get_menu_name(list->data); + /* g_debug("got child %d: %s", fm_xml_file_item_get_tag(list->data), elem_name); */ + if (g_strcmp0(elem_name, ptr) == 0) + break; + else + list = list->next; + } + g_free(_ptr); + if (list && path) + { + FmXmlFileItem *item; + + list = fm_xml_file_item_get_children(list->data); + item = _find_in_children(list, path); + g_list_free(list); + return item; + } + return list ? list->data : NULL; +} + +/* returns only child */ +static FmXmlFileItem *_set_default_contents(FmXmlFile *file, const char *basename) +{ + FmXmlFileItem *item, *child; + char *path; + + /* set DTD */ + fm_xml_file_set_dtd(file, + "Menu PUBLIC '-//freedesktop//DTD Menu 1.0//EN'\n" + " 'http://www.freedesktop.org/standards/menu-spec/menu-1.0.dtd'", + NULL); + /* set content: + + Applications + /etc/xgd/menus/%s + */ + item = fm_xml_file_item_new(menuTag_Menu); + fm_xml_file_insert_first(file, item); + child = fm_xml_file_item_new(menuTag_Name); + fm_xml_file_item_append_text(child, "Applications", -1, FALSE); + fm_xml_file_item_append_child(item, child); + child = fm_xml_file_item_new(menuTag_MergeFile); + fm_xml_file_item_set_attribute(child, "type", "parent"); + /* FIXME: what is correct way to handle this? is it required at all? */ + path = g_strdup_printf("/etc/xgd/menus/%s", basename); + fm_xml_file_item_append_text(child, path, -1, FALSE); + g_free(path); + fm_xml_file_item_append_child(item, child); + return item; + /* FIXME: can errors happen above? */ +} + +/* tries to create ... path in children list and returns for it */ +static FmXmlFileItem *_create_path_in_tree(FmXmlFileItem *parent, const char *path) +{ + const char *ptr; + char *_ptr; + GList *list, *l; + FmXmlFileItem *item, *name; + + if (path == NULL) + return NULL; + list = fm_xml_file_item_get_children(parent); + /* g_debug("menu tree: creating '%s'", path); */ + ptr = strchr(path, '/'); + if (ptr == NULL) + { + ptr = path; + path = _ptr = NULL; + } + else + { + _ptr = g_strndup(path, ptr - path); + path = ptr + 1; + ptr = _ptr; + } + for (l = list; l; l = l->next) + { + const char *elem_name = _get_menu_name(l->data); + /* g_debug("got child %d: %s", fm_xml_file_item_get_tag(list->data), elem_name); */ + if (g_strcmp0(elem_name, ptr) == 0) + break; + } + if (l) /* subpath already exists */ + { + item = l->data; + g_list_free(list); + g_free(_ptr); + return _create_path_in_tree(item, path); + } + g_list_free(list); + /* create subtag */ + name = fm_xml_file_item_new(menuTag_Name); + fm_xml_file_item_append_text(name, ptr, -1, FALSE); + g_free(_ptr); + /* create and insert it */ + item = fm_xml_file_item_new(menuTag_Menu); + if (!fm_xml_file_item_append_child(parent, item) || + !fm_xml_file_item_append_child(item, name)) + { + /* FIXME: it cannot fail on newly created items! */ + fm_xml_file_item_destroy(name); + fm_xml_file_item_destroy(item); + return NULL; + } + /* path is NULL if it is a final subpath */ + return path ? _create_path_in_tree(item, path) : item; +} + +/* locks menuTree, sets fields in data, sets gf + returns "Applications" menu on success and NULL on failure */ +static FmXmlFileItem *_prepare_contents(FmMenuMenuTree *data, GCancellable *cancellable, + GError **error, GFile **gf) +{ + const char *xdg_menu_prefix; + char *contents; + gsize len; + GList *xml = NULL; + FmXmlFileItem *apps; + gboolean ok; + + /* do it in compatibility with lxpanel */ + xdg_menu_prefix = g_getenv("XDG_MENU_PREFIX"); + contents = g_strdup_printf("%sapplications.menu", + xdg_menu_prefix ? xdg_menu_prefix : "lxde-"); + data->file_path = g_build_filename(g_get_user_config_dir(), "menus", + contents, NULL); + *gf = g_file_new_for_path(data->file_path); + data->menu = fm_xml_file_new(NULL); + data->line = data->pos = -1; + data->cancellable = cancellable; + G_LOCK(menuTree); + /* set tags, ignore errors */ + menuTag_Menu = fm_xml_file_set_handler(data->menu, "Menu", + &_menu_xml_handler_pass, FALSE, NULL); + menuTag_Name = fm_xml_file_set_handler(data->menu, "Name", + &_menu_xml_handler_pass, FALSE, NULL); + menuTag_Deleted = fm_xml_file_set_handler(data->menu, "Deleted", + &_menu_xml_handler_pass, FALSE, NULL); + menuTag_NotDeleted = fm_xml_file_set_handler(data->menu, "NotDeleted", + &_menu_xml_handler_pass, FALSE, NULL); + menuTag_Directory = fm_xml_file_set_handler(data->menu, "Directory", + &_menu_xml_handler_pass, FALSE, NULL); + menuTag_Include = fm_xml_file_set_handler(data->menu, "Include", + &_menu_xml_handler_pass, FALSE, NULL); + menuTag_Exclude = fm_xml_file_set_handler(data->menu, "Exclude", + &_menu_xml_handler_pass, FALSE, NULL); + menuTag_Filename = fm_xml_file_set_handler(data->menu, "Filename", + &_menu_xml_handler_pass, FALSE, NULL); + menuTag_MergeFile = fm_xml_file_set_handler(data->menu, "MergeFile", + &_menu_xml_handler_pass, FALSE, NULL); + menuTag_Category = fm_xml_file_set_handler(data->menu, "Category", + &_menu_xml_handler_pass, FALSE, NULL); + if (!g_file_query_exists(*gf, cancellable)) + { + /* if file doesn't exist then it should be created with default contents */ + apps = _set_default_contents(data->menu, contents); + g_free(contents); + return apps; + } + g_free(contents); /* we used it temporarily */ + contents = NULL; + ok = g_file_load_contents(*gf, cancellable, &contents, &len, NULL, error); + if (!ok) + return NULL; + ok = fm_xml_file_parse_data(data->menu, contents, len, error, data); + g_free(contents); + if (ok) + xml = fm_xml_file_finish_parse(data->menu, error); + if (xml == NULL) /* error is set by failed function */ + { + if (data->line == -1) + data->line = fm_xml_file_get_current_line(data->menu, &data->pos); + g_prefix_error(error, _("XML file '%s' error (%d:%d): "), data->file_path, + data->line, data->pos); + } + else + { + apps = _find_in_children(xml, "Applications"); + g_list_free(xml); + if (apps) + return apps; + g_set_error_literal(error, G_FILE_ERROR, G_FILE_ERROR_NOENT, + _("XML file doesn't contain Applications root")); + } + return NULL; +} + +/* replaces invalid chars in path with minus sign */ +static inline char *_get_pathtag_for_path(const char *path) +{ + char *pathtag, *c; + + pathtag = g_strdup(path); + for (c = pathtag; *c; c++) + if (*c == '/' || *c == '\t' || *c == '\n' || *c == '\r' || *c == ' ') + *c = '-'; + return pathtag; +} + +static gboolean _save_new_menu_file(GFile *gf, FmXmlFile *file, + GCancellable *cancellable, + GError **error) +{ + gsize len; + char *contents = fm_xml_file_to_data(file, &len, error); + gboolean result = FALSE; + + if (contents == NULL) + return FALSE; + /* g_debug("new menu file: %s", contents); */ + result = g_file_replace_contents(gf, contents, len, NULL, FALSE, + G_FILE_CREATE_REPLACE_DESTINATION, NULL, + cancellable, error); + g_free(contents); + return result; +} + +#if MENU_CACHE_CHECK_VERSION(0, 5, 0) +/* changes .menu XML file */ +static gboolean _remove_directory(const char *path, GCancellable *cancellable, + GError **error) +{ + GList *xml = NULL, *it; + GFile *gf; + FmXmlFileItem *apps, *item; + FmMenuMenuTree data; + gboolean ok = TRUE; + + /* g_debug("deleting menu folder '%s'", path); */ + /* FIXME: check if there is that path in the XML tree before doing anything */ + apps = _prepare_contents(&data, cancellable, error, &gf); + if (apps == NULL) + { + /* either failed to load contents or cancelled */ + ok = FALSE; + } + else if ((xml = fm_xml_file_item_get_children(apps)) != NULL && + (item = _find_in_children(xml, path)) != NULL) + { + /* if path is found and has then replace it with */ + g_list_free(xml); + xml = fm_xml_file_item_get_children(item); + for (it = xml; it; it = it->next) + { + FmXmlFileTag tag = fm_xml_file_item_get_tag(it->data); + if (tag == menuTag_Deleted || tag == menuTag_NotDeleted) + fm_xml_file_item_destroy(it->data); + } + goto _add_deleted_tag; + } + else + { + /* else create path and add to it */ + item = _create_path_in_tree(apps, path); + if (item == NULL) + { + g_set_error(error, G_FILE_ERROR, G_FILE_ERROR_FAILED, + _("Cannot create XML definition for '%s'"), path); + ok = FALSE; + } + else + { + FmXmlFileItem *item2; + +_add_deleted_tag: + item2 = fm_xml_file_item_new(menuTag_Deleted); + fm_xml_file_item_set_comment(item2, "deleted by LibFM"); + fm_xml_file_item_append_child(item, item2); /* NOTE: it cannot fail */ + } + } + if (ok) + ok = _save_new_menu_file(gf, data.menu, cancellable, error); + G_UNLOCK(menuTree); + g_object_unref(gf); + g_object_unref(data.menu); + g_free(data.file_path); + g_list_free(xml); + return ok; +} + +/* changes .menu XML file */ +static gboolean _add_directory(const char *path, GCancellable *cancellable, + GError **error) +{ + GList *xml = NULL, *it; + GFile *gf; + FmXmlFileItem *apps, *item, *child; + FmMenuMenuTree data; + gboolean ok = TRUE; + + /* g_debug("adding menu folder '%s'", path); */ + /* FIXME: fail if such Menu Name already not deleted in XML tree */ + apps = _prepare_contents(&data, cancellable, error, &gf); + if (apps == NULL) + { + /* either failed to load contents or cancelled */ + ok = FALSE; + } + else if ((xml = fm_xml_file_item_get_children(apps)) != NULL && + (item = _find_in_children(xml, path)) != NULL) + { + /* "undelete" the directory: */ + /* if path is found and has then replace it with */ + g_list_free(xml); + xml = fm_xml_file_item_get_children(item); + ok = FALSE; /* it should be Deleted, otherwise error, see FIXME above */ + for (it = xml; it; it = it->next) + { + FmXmlFileTag tag = fm_xml_file_item_get_tag(it->data); + if (tag == menuTag_Deleted) + { + fm_xml_file_item_destroy(it->data); + ok = TRUE; /* see FIXME above */ + } + else if (tag == menuTag_NotDeleted) + { + fm_xml_file_item_destroy(it->data); + ok = FALSE; /* see FIXME above */ + } + } + if (!ok) /* see FIXME above */ + g_set_error(error, G_IO_ERROR, G_IO_ERROR_EXISTS, + _("Menu path '%s' already exists"), path); + else + { + child = fm_xml_file_item_new(menuTag_NotDeleted); + fm_xml_file_item_set_comment(child, "undeleted by LibFM"); + fm_xml_file_item_append_child(item, child); /* NOTE: it cannot fail */ + } + } + else + { + /* else create path and add content to it */ + item = _create_path_in_tree(apps, path); + if (item == NULL) + { + g_set_error(error, G_IO_ERROR, G_IO_ERROR_EXISTS, + _("Cannot create XML definition for '%s'"), path); + ok = FALSE; + } + else + { + char *pathtag, *dir, *contents; + GString *str; + + /* add */ + child = fm_xml_file_item_new(menuTag_NotDeleted); + fm_xml_file_item_append_child(item, child); + /* touch .directory file, ignore errors */ + dir = strrchr(path, '/'); /* use it as a storage for basename */ + if (dir) + dir++; + else + dir = (char *)path; + contents = g_strdup_printf("[" G_KEY_FILE_DESKTOP_GROUP "]\n" + G_KEY_FILE_DESKTOP_KEY_TYPE "=" G_KEY_FILE_DESKTOP_TYPE_DIRECTORY "\n" + G_KEY_FILE_DESKTOP_KEY_NAME "=%s", dir); + pathtag = _get_pathtag_for_path(path); + dir = g_build_filename(g_get_user_data_dir(), "desktop-directories", + pathtag, NULL); + str = g_string_new(dir); + g_free(dir); + g_string_append(str, ".directory"); + g_file_set_contents(str->str, contents, -1, NULL); + /* FIXME: report errors */ + g_free(contents); + /* add ..... */ + child = fm_xml_file_item_new(menuTag_Directory); + g_string_printf(str, "%s.directory", pathtag); + fm_xml_file_item_append_text(child, str->str, str->len, FALSE); + fm_xml_file_item_append_child(item, child); + /* add ...... */ + child = fm_xml_file_item_new(menuTag_Include); + fm_xml_file_item_append_child(item, child); + g_string_printf(str, "X-%s", pathtag); + g_free(pathtag); + item = fm_xml_file_item_new(menuTag_Category); /* reuse item var. */ + fm_xml_file_item_append_text(item, str->str, str->len, FALSE); + fm_xml_file_item_append_child(child, item); + g_string_free(str, TRUE); + /* ignoring errors since new created items cannot fail on append */ + } + } + if (ok) + ok = _save_new_menu_file(gf, data.menu, cancellable, error); + G_UNLOCK(menuTree); + g_object_unref(gf); + g_object_unref(data.menu); + g_free(data.file_path); + g_list_free(xml); + return ok; +} +#endif + +/* changes .menu XML file */ +static gboolean _add_application(const char *path, GCancellable *cancellable, + GError **error) +{ + const char *id; + char *dir; + GList *xml = NULL, *it; + GFile *gf; + FmXmlFileItem *apps, *item, *child; + FmMenuMenuTree data; + gboolean ok = TRUE; + + id = strrchr(path, '/'); + if (id == NULL) + { + dir = NULL; + id = path; + } + else + { + dir = g_strndup(path, id - path); + id++; + } + apps = _prepare_contents(&data, cancellable, error, &gf); + if (apps == NULL) + { + /* either failed to load contents or cancelled */ + ok = FALSE; + } + else if (dir == NULL) /* adding to root, use apps as target */ + { + item = apps; + goto _set; + } + else if ((xml = fm_xml_file_item_get_children(apps)) != NULL && + (item = _find_in_children(xml, dir)) != NULL) + { + /* already found that path */ + goto _set; + } + else + { + /* else create path and add content to it */ + item = _create_path_in_tree(apps, dir); + if (item == NULL) + { + g_set_error(error, G_IO_ERROR, G_IO_ERROR_EXISTS, + _("Cannot create XML definition for '%s'"), path); + ok = FALSE; + } + else + { +_set: + g_list_free(xml); + xml = fm_xml_file_item_get_children(item); + ok = FALSE; /* check if Include is already there */ + /* remove id */ + for (it = xml; it; it = it->next) + { + FmXmlFileTag tag = fm_xml_file_item_get_tag(it->data); + if (tag == menuTag_Exclude) + { + /* get Filename tag */ + child = fm_xml_file_item_find_child(it->data, menuTag_Filename); + if (child == NULL) + continue; + /* get contents of the tag */ + child = fm_xml_file_item_find_child(child, FM_XML_FILE_TEXT); + if (child == NULL) + continue; + if (strcmp(fm_xml_file_item_get_data(child, NULL), id) != 0) + continue; + fm_xml_file_item_destroy(it->data); + /* it was excluded before so removing exclude will add it */ + ok = TRUE; + } + else if (!ok && tag == menuTag_Include) + { + /* get Filename tag */ + child = fm_xml_file_item_find_child(it->data, menuTag_Filename); + if (child == NULL) + continue; + /* get contents of the tag */ + child = fm_xml_file_item_find_child(child, FM_XML_FILE_TEXT); + if (child == NULL) + continue; + if (strcmp(fm_xml_file_item_get_data(child, NULL), id) == 0) + ok = TRUE; /* found! */ + } + } + if (!ok) + { + /* add id */ + child = fm_xml_file_item_new(menuTag_Include); + fm_xml_file_item_set_comment(child, "added by LibFM"); + fm_xml_file_item_append_child(item, child); + item = fm_xml_file_item_new(menuTag_Filename); + fm_xml_file_item_append_text(item, id, -1, FALSE); + fm_xml_file_item_append_child(child, item); + ok = TRUE; + } + } + } + if (ok) + ok = _save_new_menu_file(gf, data.menu, cancellable, error); + G_UNLOCK(menuTree); + g_object_unref(gf); + g_object_unref(data.menu); + g_free(data.file_path); + g_list_free(xml); + g_free(dir); + return ok; +} + +/* changes .menu XML file */ +static gboolean _remove_application(const char *path, GCancellable *cancellable, + GError **error) +{ + const char *id; + char *dir; + GList *xml = NULL, *it; + GFile *gf; + FmXmlFileItem *apps, *item, *child; + FmMenuMenuTree data; + gboolean ok = TRUE; + + id = strrchr(path, '/'); + if (id == NULL) + { + dir = NULL; + id = path; + } + else + { + dir = g_strndup(path, id - path); + id++; + } + apps = _prepare_contents(&data, cancellable, error, &gf); + if (apps == NULL) + { + /* either failed to load contents or cancelled */ + ok = FALSE; + } + else if (dir == NULL) /* removing from root, use apps as target */ + { + item = apps; + goto _set; + } + else if ((xml = fm_xml_file_item_get_children(apps)) != NULL && + (item = _find_in_children(xml, dir)) != NULL) + { + /* already found that path */ + goto _set; + } + else + { + /* else create path and add content to it */ + item = _create_path_in_tree(apps, dir); + if (item == NULL) + { + g_set_error(error, G_IO_ERROR, G_IO_ERROR_EXISTS, + _("Cannot create XML definition for '%s'"), path); + ok = FALSE; + } + else + { +_set: + g_list_free(xml); + xml = fm_xml_file_item_get_children(item); + ok = FALSE; /* check if Include is already there */ + /* remove id */ + for (it = xml; it; it = it->next) + { + FmXmlFileTag tag = fm_xml_file_item_get_tag(it->data); + if (tag == menuTag_Include) + { + /* get Filename tag */ + child = fm_xml_file_item_find_child(it->data, menuTag_Filename); + if (child == NULL) + continue; + /* get contents of the tag */ + child = fm_xml_file_item_find_child(child, FM_XML_FILE_TEXT); + if (child == NULL) + continue; + if (strcmp(fm_xml_file_item_get_data(child, NULL), id) != 0) + continue; + fm_xml_file_item_destroy(it->data); + /* it was included before so removing include will remove it */ + ok = TRUE; + } + else if (!ok && tag == menuTag_Exclude) + { + /* get Filename tag */ + child = fm_xml_file_item_find_child(it->data, menuTag_Filename); + if (child == NULL) + continue; + /* get contents of the tag */ + child = fm_xml_file_item_find_child(child, FM_XML_FILE_TEXT); + if (child == NULL) + continue; + if (strcmp(fm_xml_file_item_get_data(child, NULL), id) == 0) + ok = TRUE; /* found! */ + } + } + if (!ok) + { + /* add id */ + child = fm_xml_file_item_new(menuTag_Exclude); + fm_xml_file_item_set_comment(child, "deleted by LibFM"); + fm_xml_file_item_append_child(item, child); + item = fm_xml_file_item_new(menuTag_Filename); + fm_xml_file_item_append_text(item, id, -1, FALSE); + fm_xml_file_item_append_child(child, item); + ok = TRUE; + } + } + } + if (ok) + ok = _save_new_menu_file(gf, data.menu, cancellable, error); + G_UNLOCK(menuTree); + g_object_unref(gf); + g_object_unref(data.menu); + g_free(data.file_path); + g_list_free(xml); + g_free(dir); + return ok; +} + + +/* ---- FmMenuVFile class ---- */ +#define FM_TYPE_MENU_VFILE (fm_vfs_menu_file_get_type()) +#define FM_MENU_VFILE(o) (G_TYPE_CHECK_INSTANCE_CAST((o), \ + FM_TYPE_MENU_VFILE, FmMenuVFile)) + +typedef struct _FmMenuVFile FmMenuVFile; +typedef struct _FmMenuVFileClass FmMenuVFileClass; + +static GType fm_vfs_menu_file_get_type (void); + +struct _FmMenuVFile +{ + GObject parent_object; + + char *path; +}; + +struct _FmMenuVFileClass +{ + GObjectClass parent_class; +}; + +static void fm_menu_g_file_init(GFileIface *iface); +static void fm_menu_fm_file_init(FmFileInterface *iface); + +G_DEFINE_TYPE_WITH_CODE(FmMenuVFile, fm_vfs_menu_file, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(G_TYPE_FILE, fm_menu_g_file_init) + G_IMPLEMENT_INTERFACE(FM_TYPE_FILE, fm_menu_fm_file_init)) + +static void fm_vfs_menu_file_finalize(GObject *object) +{ + FmMenuVFile *item = FM_MENU_VFILE(object); + + g_free(item->path); + + G_OBJECT_CLASS(fm_vfs_menu_file_parent_class)->finalize(object); +} + +static void fm_vfs_menu_file_class_init(FmMenuVFileClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = fm_vfs_menu_file_finalize; +} + +static void fm_vfs_menu_file_init(FmMenuVFile *item) +{ + /* nothing */ +} + +static FmMenuVFile *_fm_menu_vfile_new(void) +{ + return (FmMenuVFile*)g_object_new(FM_TYPE_MENU_VFILE, NULL); +} + + +/* ---- menu enumerator class ---- */ +#define FM_TYPE_VFS_MENU_ENUMERATOR (fm_vfs_menu_enumerator_get_type()) +#define FM_VFS_MENU_ENUMERATOR(o) (G_TYPE_CHECK_INSTANCE_CAST((o), \ + FM_TYPE_VFS_MENU_ENUMERATOR, FmVfsMenuEnumerator)) + +typedef struct _FmVfsMenuEnumerator FmVfsMenuEnumerator; +typedef struct _FmVfsMenuEnumeratorClass FmVfsMenuEnumeratorClass; + +struct _FmVfsMenuEnumerator +{ + GFileEnumerator parent; + + MenuCache *mc; + GSList *child; + guint32 de_flag; +}; + +struct _FmVfsMenuEnumeratorClass +{ + GFileEnumeratorClass parent_class; +}; + +static GType fm_vfs_menu_enumerator_get_type (void); + +G_DEFINE_TYPE(FmVfsMenuEnumerator, fm_vfs_menu_enumerator, G_TYPE_FILE_ENUMERATOR) + +static void _fm_vfs_menu_enumerator_dispose(GObject *object) +{ + FmVfsMenuEnumerator *enu = FM_VFS_MENU_ENUMERATOR(object); + + if(enu->mc) + { + menu_cache_unref(enu->mc); + enu->mc = NULL; + } + + G_OBJECT_CLASS(fm_vfs_menu_enumerator_parent_class)->dispose(object); +} + +GIcon* _fm_icon_from_name(const char* name); /* defined in core/iconinfo.cpp */ + +static GFileInfo *_g_file_info_from_menu_cache_item(MenuCacheItem *item, + /* GFileAttributeMatcher *attribute_matcher, */ + guint32 de_flag) +{ + GFileInfo *fileinfo = g_file_info_new(); + const char *icon_name; + GIcon* icon; + + /* FIXME: use g_uri_escape_string() for item name */ + g_file_info_set_name(fileinfo, menu_cache_item_get_id(item)); + if(menu_cache_item_get_name(item) != NULL) + g_file_info_set_display_name(fileinfo, menu_cache_item_get_name(item)); + + /* the setup below was in fm_file_info_set_from_menu_cache_item() + so this setup makes latter API deprecated */ + icon_name = menu_cache_item_get_icon(item); + if(icon_name) + { + icon = _fm_icon_from_name(icon_name); + if(G_LIKELY(icon)) + { + g_file_info_set_icon(fileinfo, icon); + g_object_unref(icon); + } + } + if(menu_cache_item_get_type(item) == MENU_CACHE_TYPE_DIR) + { + g_file_info_set_file_type(fileinfo, G_FILE_TYPE_DIRECTORY); +#if MENU_CACHE_CHECK_VERSION(0, 5, 0) + g_file_info_set_is_hidden(fileinfo, + !menu_cache_dir_is_visible(MENU_CACHE_DIR(item))); +#else + g_file_info_set_is_hidden(fileinfo, FALSE); +#endif + } + else /* MENU_CACHE_TYPE_APP */ + { + char *path = menu_cache_item_get_file_path(item); + g_file_info_set_file_type(fileinfo, G_FILE_TYPE_SHORTCUT); + g_file_info_set_attribute_string(fileinfo, + G_FILE_ATTRIBUTE_STANDARD_TARGET_URI, + path); + g_free(path); + g_file_info_set_content_type(fileinfo, "application/x-desktop"); + g_file_info_set_is_hidden(fileinfo, + !menu_cache_app_get_is_visible(MENU_CACHE_APP(item), + de_flag)); + } +// FIXME: use attribute_matcher and set G_FILE_ATTRIBUTE_ACCESS_CAN_{WRITE,READ,EXECUTE,DELETE} + g_file_info_set_attribute_string(fileinfo, G_FILE_ATTRIBUTE_ID_FILESYSTEM, + "menu-Applications"); + g_file_info_set_attribute_boolean(fileinfo, + G_FILE_ATTRIBUTE_ACCESS_CAN_RENAME, TRUE); + g_file_info_set_attribute_boolean(fileinfo, + G_FILE_ATTRIBUTE_ACCESS_CAN_TRASH, FALSE); + return fileinfo; +} + +typedef struct +{ + union + { + FmVfsMenuEnumerator *enumerator; + const char *path_str; + }; + union + { + FmMenuVFile *destination; +// const char *attributes; + const char *display_name; + GFileInfo *info; + }; +// GFileQueryInfoFlags flags; + union + { + GCancellable *cancellable; + GFile *file; + }; + GError **error; + gpointer result; +} FmVfsMenuMainThreadData; + +static gboolean _fm_vfs_menu_enumerator_next_file_real(gpointer data) +{ + FmVfsMenuMainThreadData *init = data; + FmVfsMenuEnumerator *enu = init->enumerator; + GSList *child = enu->child; + MenuCacheItem *item; + + init->result = NULL; + + if(child == NULL) + goto done; + + for(; child; child = child->next) + { + if(g_cancellable_set_error_if_cancelled(init->cancellable, init->error)) + break; + item = MENU_CACHE_ITEM(child->data); + if(!item || menu_cache_item_get_type(item) == MENU_CACHE_TYPE_SEP || + menu_cache_item_get_type(item) == MENU_CACHE_TYPE_NONE) + continue; +#if 0 + /* also hide menu items which should be hidden in current DE. */ + if(menu_cache_item_get_type(item) == MENU_CACHE_TYPE_APP + && !menu_cache_app_get_is_visible(MENU_CACHE_APP(item), enu->de_flag)) + continue; +#endif + + init->result = _g_file_info_from_menu_cache_item(item, enu->de_flag); + child = child->next; + break; + } +#if MENU_CACHE_CHECK_VERSION(0, 4, 0) + while(enu->child != child) /* free skipped/used elements */ + { + GSList *ch = enu->child; + enu->child = ch->next; + menu_cache_item_unref(ch->data); + g_slist_free_1(ch); + } +#else + enu->child = child; +#endif + +done: + return FALSE; +} + +static GFileInfo *_fm_vfs_menu_enumerator_next_file(GFileEnumerator *enumerator, + GCancellable *cancellable, + GError **error) +{ + FmVfsMenuMainThreadData init; + + init.enumerator = FM_VFS_MENU_ENUMERATOR(enumerator); + init.cancellable = cancellable; + init.error = error; + RUN_WITH_MENU_CACHE(_fm_vfs_menu_enumerator_next_file_real, &init); + return init.result; +} + +static gboolean _fm_vfs_menu_enumerator_close(GFileEnumerator *enumerator, + GCancellable *cancellable, + GError **error) +{ + FmVfsMenuEnumerator *enu = FM_VFS_MENU_ENUMERATOR(enumerator); + + if(enu->mc) + { + menu_cache_unref(enu->mc); + enu->mc = NULL; +#if MENU_CACHE_CHECK_VERSION(0, 4, 0) + g_slist_free_full(enu->child, (GDestroyNotify)menu_cache_item_unref); +#endif + enu->child = NULL; + } + return TRUE; +} + +static void fm_vfs_menu_enumerator_class_init(FmVfsMenuEnumeratorClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS(klass); + GFileEnumeratorClass *enumerator_class = G_FILE_ENUMERATOR_CLASS(klass); + + gobject_class->dispose = _fm_vfs_menu_enumerator_dispose; + + enumerator_class->next_file = _fm_vfs_menu_enumerator_next_file; + enumerator_class->close_fn = _fm_vfs_menu_enumerator_close; +} + +static void fm_vfs_menu_enumerator_init(FmVfsMenuEnumerator *enumerator) +{ + /* nothing */ +} + +static MenuCacheItem *_vfile_path_to_menu_cache_item(MenuCache* mc, const char *path) +{ + MenuCacheItem *dir; + char *unescaped, *tmp = NULL; + + unescaped = g_uri_unescape_string(path, NULL); +#if MENU_CACHE_CHECK_VERSION(0, 4, 0) + dir = MENU_CACHE_ITEM(menu_cache_dup_root_dir(mc)); +#else + dir = MENU_CACHE_ITEM(menu_cache_get_root_dir(mc)); +#endif + if(dir) + { +#if !MENU_CACHE_CHECK_VERSION(0, 5, 0) + char *id; +#if !MENU_CACHE_CHECK_VERSION(0, 4, 0) + GSList *child; +#endif +#endif + tmp = g_strconcat("/", menu_cache_item_get_id(dir), "/", unescaped, NULL); +#if MENU_CACHE_CHECK_VERSION(0, 4, 0) + menu_cache_item_unref(dir); + dir = menu_cache_item_from_path(mc, tmp); +#else + /* access not dir is a bit tricky */ + id = strrchr(tmp, '/'); + *id++ = '\0'; + dir = MENU_CACHE_ITEM(menu_cache_get_dir_from_path(mc, tmp)); + child = menu_cache_dir_get_children(MENU_CACHE_DIR(dir)); + dir = NULL; + while (child) + { + if (g_strcmp0(id, menu_cache_item_get_id(child->data)) == 0) + { + dir = child->data; + break; + } + child = child->next; + } +#endif +#if !MENU_CACHE_CHECK_VERSION(0, 5, 0) + /* The menu-cache is buggy and returns parent for invalid path + instead of failure so we check what we got here. + Unfortunately we cannot detect if requested name is the same + as its parent and menu-cache returned the parent. */ + id = strrchr(unescaped, '/'); + if(id) + id++; + else + id = unescaped; + if(dir != NULL && strcmp(id, menu_cache_item_get_id(dir)) != 0) + dir = NULL; +#endif + } + g_free(unescaped); + g_free(tmp); + /* NOTE: returned value is referenced for >= 0.4.0 only */ + return dir; +} + +static MenuCache *_get_menu_cache(GError **error) +{ + MenuCache *mc; + static gboolean environment_tested = FALSE; + static gboolean requires_prefix = FALSE; + + /* do it in compatibility with lxpanel */ + if(!environment_tested) + { + requires_prefix = (g_getenv("XDG_MENU_PREFIX") == NULL); + environment_tested = TRUE; + } +#if MENU_CACHE_CHECK_VERSION(0, 5, 0) + mc = menu_cache_lookup_sync(requires_prefix ? "lxde-applications.menu+hidden" : "applications.menu+hidden"); +#else + mc = menu_cache_lookup_sync(requires_prefix ? "lxde-applications.menu" : "applications.menu"); +#endif + /* FIXME: may be it is reasonable to set XDG_MENU_PREFIX ? */ + + if(mc == NULL) /* initialization failed */ + g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_FAILED, + _("Menu cache error")); + return mc; +} + +static gboolean _fm_vfs_menu_enumerator_new_real(gpointer data) +{ + FmVfsMenuMainThreadData *init = data; + FmVfsMenuEnumerator *enumerator; + MenuCache* mc; + const char *de_name; + MenuCacheItem *dir; + + mc = _get_menu_cache(init->error); + + if(mc == NULL) /* initialization failed */ + return FALSE; + + enumerator = g_object_new(FM_TYPE_VFS_MENU_ENUMERATOR, "container", + init->file, NULL); + enumerator->mc = mc; + de_name = g_getenv("XDG_CURRENT_DESKTOP"); + + if(de_name) + enumerator->de_flag = menu_cache_get_desktop_env_flag(mc, de_name); + else + enumerator->de_flag = (guint32)-1; + + /* the menu should be loaded now */ + if(init->path_str) + dir = _vfile_path_to_menu_cache_item(mc, init->path_str); + else +#if MENU_CACHE_CHECK_VERSION(0, 4, 0) + dir = MENU_CACHE_ITEM(menu_cache_dup_root_dir(mc)); +#else + dir = MENU_CACHE_ITEM(menu_cache_get_root_dir(mc)); +#endif + if(dir) + { +#if MENU_CACHE_CHECK_VERSION(0, 4, 0) + enumerator->child = menu_cache_dir_list_children(MENU_CACHE_DIR(dir)); + menu_cache_item_unref(dir); +#else + enumerator->child = menu_cache_dir_get_children(MENU_CACHE_DIR(dir)); +#endif + } + /* FIXME: do something with attributes and flags */ + + init->result = enumerator; + return FALSE; +} + +static GFileEnumerator *_fm_vfs_menu_enumerator_new(GFile *file, + const char *path_str, + const char *attributes, + GFileQueryInfoFlags flags, + GError **error) +{ + FmVfsMenuMainThreadData enu; + + enu.path_str = path_str; +// enu.attributes = attributes; +// enu.flags = flags; + enu.file = file; + enu.error = error; + enu.result = NULL; + RUN_WITH_MENU_CACHE(_fm_vfs_menu_enumerator_new_real, &enu); + return enu.result; +} + + +/* ---- GFile implementation ---- */ +static GFileAttributeInfoList *_fm_vfs_menu_settable_attributes = NULL; + +#define ERROR_UNSUPPORTED(err) g_set_error_literal(err, G_IO_ERROR, \ + G_IO_ERROR_NOT_SUPPORTED, _("Operation not supported")) + +static GFile *_fm_vfs_menu_dup(GFile *file) +{ + FmMenuVFile *item, *new_item; + + item = FM_MENU_VFILE(file); + new_item = _fm_menu_vfile_new(); + if(item->path) + new_item->path = g_strdup(item->path); + return (GFile*)new_item; +} + +static guint _fm_vfs_menu_hash(GFile *file) +{ + return g_str_hash(FM_MENU_VFILE(file)->path ? FM_MENU_VFILE(file)->path : "/"); +} + +static gboolean _fm_vfs_menu_equal(GFile *file1, GFile *file2) +{ + char *path1 = FM_MENU_VFILE(file1)->path; + char *path2 = FM_MENU_VFILE(file2)->path; + + return g_strcmp0(path1, path2) == 0; +} + +static gboolean _fm_vfs_menu_is_native(GFile *file) +{ + return FALSE; +} + +static gboolean _fm_vfs_menu_has_uri_scheme(GFile *file, const char *uri_scheme) +{ + return g_ascii_strcasecmp(uri_scheme, "menu") == 0; +} + +static char *_fm_vfs_menu_get_uri_scheme(GFile *file) +{ + return g_strdup("menu"); +} + +static char *_fm_vfs_menu_get_basename(GFile *file) +{ + /* g_debug("_fm_vfs_menu_get_basename %s", FM_MENU_VFILE(file)->path); */ + if(FM_MENU_VFILE(file)->path == NULL) + return g_strdup("/"); + return g_path_get_basename(FM_MENU_VFILE(file)->path); +} + +static char *_fm_vfs_menu_get_path(GFile *file) +{ + return NULL; +} + +static char *_fm_vfs_menu_get_uri(GFile *file) +{ + return g_strconcat("menu://applications/", FM_MENU_VFILE(file)->path, NULL); +} + +static char *_fm_vfs_menu_get_parse_name(GFile *file) +{ + char *unescaped, *path; + + /* g_debug("_fm_vfs_menu_get_parse_name %s", FM_MENU_VFILE(file)->path); */ + unescaped = g_uri_unescape_string(FM_MENU_VFILE(file)->path, NULL); + path = g_strconcat("menu://applications/", unescaped, NULL); + g_free(unescaped); + return path; +} + +static GFile *_fm_vfs_menu_get_parent(GFile *file) +{ + char *path = FM_MENU_VFILE(file)->path; + char *dirname; + GFile *parent; + + /* g_debug("_fm_vfs_menu_get_parent %s", path); */ + if(path) + { + dirname = g_path_get_dirname(path); + if(strcmp(dirname, ".") == 0) + { + g_free(dirname); + path = NULL; + } + else + path = dirname; + } + parent = _fm_vfs_menu_new_for_uri(path); + if(path) + g_free(path); + return parent; +} + +/* this function is taken from GLocalFile implementation */ +static const char *match_prefix (const char *path, const char *prefix) +{ + int prefix_len; + + prefix_len = strlen (prefix); + if (strncmp (path, prefix, prefix_len) != 0) + return NULL; + + if (prefix_len > 0 && (prefix[prefix_len-1]) == '/') + prefix_len--; + + return path + prefix_len; +} + +static gboolean _fm_vfs_menu_prefix_matches(GFile *prefix, GFile *file) +{ + const char *path = FM_MENU_VFILE(file)->path; + const char *pp = FM_MENU_VFILE(prefix)->path; + const char *remainder; + + if(pp == NULL) + return TRUE; + if(path == NULL) + return FALSE; + remainder = match_prefix(path, pp); + if(remainder != NULL && *remainder == '/') + return TRUE; + return FALSE; +} + +static char *_fm_vfs_menu_get_relative_path(GFile *parent, GFile *descendant) +{ + const char *path = FM_MENU_VFILE(descendant)->path; + const char *pp = FM_MENU_VFILE(parent)->path; + const char *remainder; + + if(pp == NULL) + return g_strdup(path); + if(path == NULL) + return NULL; + remainder = match_prefix(path, pp); + if(remainder != NULL && *remainder == '/') + return g_uri_unescape_string(&remainder[1], NULL); + return NULL; +} + +static GFile *_fm_vfs_menu_resolve_relative_path(GFile *file, const char *relative_path) +{ + const char *path = FM_MENU_VFILE(file)->path; + FmMenuVFile *new_item = _fm_menu_vfile_new(); + + /* g_debug("_fm_vfs_menu_resolve_relative_path %s %s", path, relative_path); */ + /* FIXME: handle if relative_path is invalid */ + if(relative_path == NULL || *relative_path == '\0') + new_item->path = g_strdup(path); + else if(path == NULL) + new_item->path = g_strdup(relative_path); + else + { + /* relative_path is the most probably unescaped string (at least GFVS + works such way) so we have to escape invalid chars here. */ + char *escaped = g_uri_escape_string(relative_path, + G_URI_RESERVED_CHARS_ALLOWED_IN_PATH, + TRUE); + new_item->path = g_strconcat(path, "/", relative_path, NULL); + g_free(escaped); + } + return (GFile*)new_item; +} + +static gboolean _fm_vfs_menu_get_child_for_display_name_real(gpointer data) +{ + FmVfsMenuMainThreadData *init = data; + MenuCache *mc; + MenuCacheItem *dir; + gboolean is_invalid = FALSE; + + init->result = NULL; + mc = _get_menu_cache(init->error); + if(mc == NULL) + goto _mc_failed; + + if(init->path_str) + { + dir = _vfile_path_to_menu_cache_item(mc, init->path_str); + if(dir == NULL || menu_cache_item_get_type(dir) != MENU_CACHE_TYPE_DIR) + is_invalid = TRUE; + } + else +#if MENU_CACHE_CHECK_VERSION(0, 4, 0) + dir = MENU_CACHE_ITEM(menu_cache_dup_root_dir(mc)); +#else + dir = MENU_CACHE_ITEM(menu_cache_get_root_dir(mc)); +#endif + if(is_invalid) + g_set_error_literal(init->error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + _("Invalid menu directory")); + else if(dir) + { +#if MENU_CACHE_CHECK_VERSION(0, 5, 0) + MenuCacheItem *item = menu_cache_find_child_by_name(MENU_CACHE_DIR(dir), + init->display_name); + g_debug("searched for child '%s' found '%s'", init->display_name, + item ? menu_cache_item_get_id(item) : "(nil)"); + if (item == NULL) + init->result = _fm_vfs_menu_resolve_relative_path(init->file, + init->display_name); + else + { + init->result = _fm_vfs_menu_resolve_relative_path(init->file, + menu_cache_item_get_id(item)); + menu_cache_item_unref(item); + } +#else /* < 0.5.0 */ + GSList *l; +#if MENU_CACHE_CHECK_VERSION(0, 4, 0) + GSList *children = menu_cache_dir_list_children(MENU_CACHE_DIR(dir)); +#else + GSList *children = menu_cache_dir_get_children(MENU_CACHE_DIR(dir)); +#endif + for (l = children; l; l = l->next) + if (g_strcmp0(init->display_name, menu_cache_item_get_name(l->data)) == 0) + break; + if (l == NULL) /* not found */ + init->result = _fm_vfs_menu_resolve_relative_path(init->file, + init->display_name); + else + init->result = _fm_vfs_menu_resolve_relative_path(init->file, + menu_cache_item_get_id(l->data)); +#if MENU_CACHE_CHECK_VERSION(0, 4, 0) + g_slist_free_full(children, (GDestroyNotify)menu_cache_item_unref); +#endif +#endif /* < 0.5.0 */ + } + else /* menu_cache_get_root_dir failed */ + g_set_error_literal(init->error, G_IO_ERROR, G_IO_ERROR_FAILED, + _("Menu cache error")); + +#if MENU_CACHE_CHECK_VERSION(0, 4, 0) + if(dir) + menu_cache_item_unref(dir); +#endif + menu_cache_unref(mc); + +_mc_failed: + return FALSE; +} + +static GFile *_fm_vfs_menu_get_child_for_display_name(GFile *file, + const char *display_name, + GError **error) +{ + FmMenuVFile *item = FM_MENU_VFILE(file); + FmVfsMenuMainThreadData enu; + + /* g_debug("_fm_vfs_menu_get_child_for_display_name: '%s' '%s'", item->path, display_name); */ + if (display_name == NULL || *display_name == '\0') + { + g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_FAILED, + _("Menu item name cannot be empty")); + return NULL; + } + /* NOTE: this violates GFile requirement that this API should not do I/O but + there is no way to do this without dirty tricks which may lead to failure */ + enu.path_str = item->path; + enu.error = error; + enu.display_name = display_name; + enu.file = file; + RUN_WITH_MENU_CACHE(_fm_vfs_menu_get_child_for_display_name_real, &enu); + return enu.result; +} + +static GFileEnumerator *_fm_vfs_menu_enumerate_children(GFile *file, + const char *attributes, + GFileQueryInfoFlags flags, + GCancellable *cancellable, + GError **error) +{ + const char *path = FM_MENU_VFILE(file)->path; + + return _fm_vfs_menu_enumerator_new(file, path, attributes, flags, error); +} + +static gboolean _fm_vfs_menu_query_info_real(gpointer data) +{ + FmVfsMenuMainThreadData *init = data; + MenuCache *mc; + MenuCacheItem *dir; + gboolean is_invalid = FALSE; + + init->result = NULL; + mc = _get_menu_cache(init->error); + if(mc == NULL) + goto _mc_failed; + + if(init->path_str) + { + dir = _vfile_path_to_menu_cache_item(mc, init->path_str); + if(dir == NULL) + is_invalid = TRUE; + } + else +#if MENU_CACHE_CHECK_VERSION(0, 4, 0) + dir = MENU_CACHE_ITEM(menu_cache_dup_root_dir(mc)); +#else + dir = MENU_CACHE_ITEM(menu_cache_get_root_dir(mc)); +#endif + if(is_invalid) + g_set_error(init->error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + _("Invalid menu directory '%s'"), init->path_str); + else if(dir) + { + const char *de_name = g_getenv("XDG_CURRENT_DESKTOP"); + + if(de_name) + init->result = _g_file_info_from_menu_cache_item(dir, + menu_cache_get_desktop_env_flag(mc, de_name)); + else + init->result = _g_file_info_from_menu_cache_item(dir, (guint32)-1); + } + else /* menu_cache_get_root_dir failed */ + g_set_error_literal(init->error, G_IO_ERROR, G_IO_ERROR_FAILED, + _("Menu cache error")); + +#if MENU_CACHE_CHECK_VERSION(0, 4, 0) + if(dir) + menu_cache_item_unref(dir); +#endif + menu_cache_unref(mc); + +_mc_failed: + return FALSE; +} + +static GFileInfo *_fm_vfs_menu_query_info(GFile *file, + const char *attributes, + GFileQueryInfoFlags flags, + GCancellable *cancellable, + GError **error) +{ + FmMenuVFile *item = FM_MENU_VFILE(file); + GFileInfo *info; + GFileAttributeMatcher *matcher; + char *basename, *id; + FmVfsMenuMainThreadData enu; + + matcher = g_file_attribute_matcher_new(attributes); + + if(item->path == NULL) /* menu root */ + { + info = g_file_info_new(); + if(g_file_attribute_matcher_matches(matcher, G_FILE_ATTRIBUTE_STANDARD_NAME)) + g_file_info_set_name(info, "/"); + if(g_file_attribute_matcher_matches(matcher, G_FILE_ATTRIBUTE_ID_FILESYSTEM)) + g_file_info_set_attribute_string(info, G_FILE_ATTRIBUTE_ID_FILESYSTEM, + "menu-Applications"); + if(g_file_attribute_matcher_matches(matcher, G_FILE_ATTRIBUTE_STANDARD_TYPE)) + g_file_info_set_file_type(info, G_FILE_TYPE_DIRECTORY); + if(g_file_attribute_matcher_matches(matcher, G_FILE_ATTRIBUTE_STANDARD_ICON)) + { + GIcon *icon = g_themed_icon_new("system-software-install"); + g_file_info_set_icon(info, icon); + g_object_unref(icon); + } + if(g_file_attribute_matcher_matches(matcher, G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN)) + g_file_info_set_is_hidden(info, FALSE); + if(g_file_attribute_matcher_matches(matcher, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME)) + g_file_info_set_display_name(info, _("Applications")); + if(g_file_attribute_matcher_matches(matcher, G_FILE_ATTRIBUTE_ACCESS_CAN_RENAME)) + g_file_info_set_attribute_boolean(info, G_FILE_ATTRIBUTE_ACCESS_CAN_RENAME, FALSE); + if(g_file_attribute_matcher_matches(matcher, G_FILE_ATTRIBUTE_ACCESS_CAN_TRASH)) + g_file_info_set_attribute_boolean(info, G_FILE_ATTRIBUTE_ACCESS_CAN_TRASH, FALSE); +#if 0 + /* FIXME: is this ever needed? */ + if(g_file_attribute_matcher_matches(matcher, G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE)) + g_file_info_set_attribute_boolean(info, G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE, TRUE); + if(g_file_attribute_matcher_matches(matcher, G_FILE_ATTRIBUTE_ACCESS_CAN_READ)) + g_file_info_set_attribute_boolean(info, G_FILE_ATTRIBUTE_ACCESS_CAN_READ, TRUE); + if(g_file_attribute_matcher_matches(matcher, G_FILE_ATTRIBUTE_ACCESS_CAN_EXECUTE)) + g_file_info_set_attribute_boolean(info, G_FILE_ATTRIBUTE_ACCESS_CAN_EXECUTE, FALSE); + if(g_file_attribute_matcher_matches(matcher, G_FILE_ATTRIBUTE_ACCESS_CAN_DELETE)) + g_file_info_set_attribute_boolean(info, G_FILE_ATTRIBUTE_ACCESS_CAN_DELETE, FALSE); +#endif + } + else if(g_file_attribute_matcher_matches(matcher, G_FILE_ATTRIBUTE_STANDARD_TYPE) || + g_file_attribute_matcher_matches(matcher, G_FILE_ATTRIBUTE_STANDARD_ICON) || + g_file_attribute_matcher_matches(matcher, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI) || + g_file_attribute_matcher_matches(matcher, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE) || + g_file_attribute_matcher_matches(matcher, G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN) || + g_file_attribute_matcher_matches(matcher, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME)) + { + /* retrieve matching attributes from menu-cache */ + enu.path_str = item->path; +// enu.attributes = attributes; +// enu.flags = flags; + enu.cancellable = cancellable; + enu.error = error; + RUN_WITH_MENU_CACHE(_fm_vfs_menu_query_info_real, &enu); + info = enu.result; + } + else + { + info = g_file_info_new(); + if(g_file_attribute_matcher_matches(matcher, G_FILE_ATTRIBUTE_STANDARD_NAME)) + { + basename = g_path_get_basename(item->path); + id = g_uri_unescape_string(basename, NULL); + g_free(basename); + g_file_info_set_name(info, id); + g_free(id); + } + if(g_file_attribute_matcher_matches(matcher, G_FILE_ATTRIBUTE_ID_FILESYSTEM)) + g_file_info_set_attribute_string(info, G_FILE_ATTRIBUTE_ID_FILESYSTEM, + "menu-Applications"); + } + + g_file_attribute_matcher_unref(matcher); + + return info; +} + +static GFileInfo *_fm_vfs_menu_query_filesystem_info(GFile *file, + const char *attributes, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return NULL; +} + +static GMount *_fm_vfs_menu_find_enclosing_mount(GFile *file, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return NULL; +} + +static gboolean _fm_vfs_menu_set_display_name_real(gpointer data) +{ + FmVfsMenuMainThreadData *init = data; + MenuCache *mc; + MenuCacheItem *dir; + gboolean ok = FALSE; + + mc = _get_menu_cache(init->error); + if(mc == NULL) + goto _mc_failed; + + dir = _vfile_path_to_menu_cache_item(mc, init->path_str); + if(dir == NULL) + g_set_error_literal(init->error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + _("Invalid menu item")); + else if (menu_cache_item_get_file_basename(dir) == NULL || + menu_cache_item_get_file_dirname(dir) == NULL) + g_set_error(init->error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + _("The menu item '%s' doesn't have appropriate entry file"), + menu_cache_item_get_id(dir)); + else if (!g_cancellable_set_error_if_cancelled(init->cancellable, init->error)) + { + char *path = menu_cache_item_get_file_path(dir); + GKeyFile *kf = g_key_file_new(); + + ok = g_key_file_load_from_file(kf, path, G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS, + init->error); + g_free(path); + if (ok) + { + /* get locale name */ + const gchar * const *langs = g_get_language_names(); + char *contents; + gsize length; + + if (strcmp(langs[0], "C") != 0) + { + char *lang; + /* remove encoding from locale name */ + char *sep = strchr(langs[0], '.'); + + if (sep) + lang = g_strndup(langs[0], sep - langs[0]); + else + lang = g_strdup(langs[0]); + g_key_file_set_locale_string(kf, G_KEY_FILE_DESKTOP_GROUP, + G_KEY_FILE_DESKTOP_KEY_NAME, lang, + init->display_name); + g_free(lang); + } + else + g_key_file_set_string(kf, G_KEY_FILE_DESKTOP_GROUP, + G_KEY_FILE_DESKTOP_KEY_NAME, init->display_name); + contents = g_key_file_to_data(kf, &length, init->error); + if (contents == NULL) + ok = FALSE; + else + { + path = g_build_filename(g_get_user_data_dir(), + (menu_cache_item_get_type(dir) == MENU_CACHE_TYPE_DIR) ? "desktop-directories" : "applications", + menu_cache_item_get_file_basename(dir), NULL); + ok = g_file_set_contents(path, contents, length, init->error); + /* FIXME: handle case if directory doesn't exist */ + g_free(contents); + g_free(path); + } + } + g_key_file_free(kf); + } + +#if MENU_CACHE_CHECK_VERSION(0, 4, 0) + if(dir) + menu_cache_item_unref(dir); +#endif + menu_cache_unref(mc); + +_mc_failed: + return ok; +} + +static GFile *_fm_vfs_menu_set_display_name(GFile *file, + const char *display_name, + GCancellable *cancellable, + GError **error) +{ + FmMenuVFile *item = FM_MENU_VFILE(file); + FmVfsMenuMainThreadData enu; + + /* g_debug("_fm_vfs_menu_set_display_name: %s -> %s", item->path, display_name); */ + if (item->path == NULL) + { + ERROR_UNSUPPORTED(error); + return NULL; + } + if (display_name == NULL || *display_name == '\0') + { + g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_FAILED, + _("Menu item name cannot be empty")); + return NULL; + } + enu.path_str = item->path; + enu.display_name = display_name; + enu.cancellable = cancellable; + enu.error = error; + if (RUN_WITH_MENU_CACHE(_fm_vfs_menu_set_display_name_real, &enu)) + return g_object_ref(file); + return NULL; +} + +static GFileAttributeInfoList *_fm_vfs_menu_query_settable_attributes(GFile *file, + GCancellable *cancellable, + GError **error) +{ + return g_file_attribute_info_list_ref(_fm_vfs_menu_settable_attributes); +} + +static GFileAttributeInfoList *_fm_vfs_menu_query_writable_namespaces(GFile *file, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return NULL; +} + +static gboolean _fm_vfs_menu_set_attributes_from_info_real(gpointer data) +{ + FmVfsMenuMainThreadData *init = data; + MenuCache *mc; + MenuCacheItem *item; + gpointer value; + const char *display_name = NULL; + GIcon *icon = NULL; + gint set_hidden = -1; + gboolean ok = FALSE; + + /* check attributes first */ + if (g_file_info_get_attribute_data(init->info, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME, + NULL, &value, NULL)) + display_name = value; + if (g_file_info_get_attribute_data(init->info, G_FILE_ATTRIBUTE_STANDARD_ICON, + NULL, &value, NULL)) + icon = value; + if (g_file_info_get_attribute_data(init->info, G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN, + NULL, &value, NULL)) + set_hidden = (*(gboolean *)value) ? 1 : 0; + if (display_name == NULL && icon == NULL && set_hidden < 0) + return TRUE; /* nothing to do */ + /* now try access item */ + mc = _get_menu_cache(init->error); + if(mc == NULL) + goto _mc_failed; + + item = _vfile_path_to_menu_cache_item(mc, init->path_str); + if(item == NULL) + g_set_error_literal(init->error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + _("Invalid menu item")); + else if (menu_cache_item_get_file_basename(item) == NULL || + menu_cache_item_get_file_dirname(item) == NULL) + g_set_error(init->error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + _("The menu item '%s' doesn't have appropriate entry file"), + menu_cache_item_get_id(item)); + else if (!g_cancellable_set_error_if_cancelled(init->cancellable, init->error)) + { + char *path; + GKeyFile *kf; + GError *err = NULL; + gboolean no_error = TRUE; + + /* for hidden on directory: use _add_directory() or _remove_directory() */ + if (set_hidden >= 0 && menu_cache_item_get_type(item) == MENU_CACHE_TYPE_DIR) + { +#if MENU_CACHE_CHECK_VERSION(0, 5, 0) + char *unescaped = g_uri_unescape_string(init->path_str, NULL); + if (set_hidden > 0) + no_error = _remove_directory(unescaped, init->cancellable, init->error); + else + no_error = _add_directory(unescaped, init->cancellable, init->error); + g_free(unescaped); + ok = no_error; +#else + g_set_error_literal(init->error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + _("Change hidden status isn't supported for menu directory")); + no_error = FALSE; +#endif + if (display_name == NULL && icon == NULL) /* nothing else to update */ + goto _done; + set_hidden = -1; /* don't set NoDisplay for a directory */ + } + /* in all other cases - update Name, Icon or NoDisplay and save keyfile */ + path = menu_cache_item_get_file_path(item); + kf = g_key_file_new(); + ok = g_key_file_load_from_file(kf, path, G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS, + &err); + g_free(path); + if (ok) /* otherwise there is no reason to continue */ + { + char *contents; + gsize length; + + if (display_name) + { + /* get locale name */ + const gchar * const *langs = g_get_language_names(); + + if (strcmp(langs[0], "C") != 0) + { + char *lang; + /* remove encoding from locale name */ + char *sep = strchr(langs[0], '.'); + + if (sep) + lang = g_strndup(langs[0], sep - langs[0]); + else + lang = g_strdup(langs[0]); + g_key_file_set_locale_string(kf, G_KEY_FILE_DESKTOP_GROUP, + G_KEY_FILE_DESKTOP_KEY_NAME, lang, + display_name); + g_free(lang); + } + else + g_key_file_set_string(kf, G_KEY_FILE_DESKTOP_GROUP, + G_KEY_FILE_DESKTOP_KEY_NAME, display_name); + } + if (icon) + { + char *icon_str = g_icon_to_string(icon); + /* FIXME: need to change encoding in some cases? */ + g_key_file_set_string(kf, G_KEY_FILE_DESKTOP_GROUP, + G_KEY_FILE_DESKTOP_KEY_ICON, icon_str); + g_free(icon_str); + } + if (set_hidden >= 0) + { + g_key_file_set_boolean(kf, G_KEY_FILE_DESKTOP_GROUP, + G_KEY_FILE_DESKTOP_KEY_NO_DISPLAY, + (set_hidden > 0)); + } + contents = g_key_file_to_data(kf, &length, &err); + if (contents == NULL) + ok = FALSE; + else + { + path = g_build_filename(g_get_user_data_dir(), + (menu_cache_item_get_type(item) == MENU_CACHE_TYPE_DIR) ? "desktop-directories" : "applications", + menu_cache_item_get_file_basename(item), NULL); + ok = g_file_set_contents(path, contents, length, &err); + /* FIXME: handle case if directory doesn't exist */ + g_free(contents); + g_free(path); + } + } + g_key_file_free(kf); + if (no_error && !ok) /* we got error in err */ + g_propagate_error(init->error, err); + else if (!ok) /* both init->error and err contain error */ + g_error_free(err); + else if (!no_error) /* we got error in init->error */ + ok = FALSE; + } + +_done: +#if MENU_CACHE_CHECK_VERSION(0, 4, 0) + if(item) + menu_cache_item_unref(item); +#endif + menu_cache_unref(mc); + +_mc_failed: + return ok; +} + +static gboolean _fm_vfs_menu_set_attributes_from_info(GFile *file, + GFileInfo *info, + GFileQueryInfoFlags flags, + GCancellable *cancellable, + GError **error) +{ + FmMenuVFile *item = FM_MENU_VFILE(file); + FmVfsMenuMainThreadData enu; + + if (item->path == NULL) + { + ERROR_UNSUPPORTED(error); + return FALSE; + } + enu.path_str = item->path; + enu.info = info; +// enu.flags = flags; + enu.cancellable = cancellable; + enu.error = error; + return (RUN_WITH_MENU_CACHE(_fm_vfs_menu_set_attributes_from_info_real, &enu)); +} + +static gboolean _fm_vfs_menu_set_attribute(GFile *file, + const char *attribute, + GFileAttributeType type, + gpointer value_p, + GFileQueryInfoFlags flags, + GCancellable *cancellable, + GError **error) +{ + FmMenuVFile *item = FM_MENU_VFILE(file); + GFileInfo *info; + gboolean result; + + g_debug("_fm_vfs_menu_set_attribute: %s on %s", attribute, item->path); + if (item->path == NULL) + { + ERROR_UNSUPPORTED(error); + return FALSE; + } + if (value_p == NULL) + goto _invalid_arg; + if (strcmp(attribute, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME) == 0) + { + if (type != G_FILE_ATTRIBUTE_TYPE_STRING) + goto _invalid_arg; + info = g_file_info_new(); + g_file_info_set_display_name(info, value_p); + } + else if (strcmp(attribute, G_FILE_ATTRIBUTE_STANDARD_ICON) == 0) + { + if (type != G_FILE_ATTRIBUTE_TYPE_OBJECT) + goto _invalid_arg; + if (!G_IS_ICON(value_p)) + goto _invalid_arg; + info = g_file_info_new(); + g_file_info_set_icon(info, value_p); + } + else if (strcmp(attribute, G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN) == 0) + { + if (type != G_FILE_ATTRIBUTE_TYPE_BOOLEAN) + goto _invalid_arg; + info = g_file_info_new(); + g_file_info_set_is_hidden(info, *(gboolean *)value_p); + } + else + { + g_set_error(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + _("Setting attribute '%s' not supported"), attribute); + return FALSE; + } + result = _fm_vfs_menu_set_attributes_from_info(file, info, flags, + cancellable, error); + g_object_unref(info); + return result; + +_invalid_arg: + g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + _("Invalid value for attribute '%s'"), attribute); + return FALSE; +} + +static inline GFile *_g_file_new_for_id(const char *id) +{ + char *file_path; + GFile *file; + + file_path = g_build_filename(g_get_user_data_dir(), "applications", id, NULL); + /* we can try to guess file path and make directories but it + hardly worth the efforts so it's easier to just make new file + by its ID since ID is unique thru all the menu */ + if (file_path == NULL) + return NULL; + file = g_file_new_for_path(file_path); + g_free(file_path); + return file; +} + +static gboolean _fm_vfs_menu_read_fn_real(gpointer data) +{ + FmVfsMenuMainThreadData *init = data; + MenuCache *mc; + MenuCacheItem *item = NULL; + + init->result = NULL; + mc = _get_menu_cache(init->error); + if(mc == NULL) + goto _mc_failed; + + if(init->path_str) + item = _vfile_path_to_menu_cache_item(mc, init->path_str); + + /* If item wasn't found or isn't a file then we cannot read it. */ + if(item != NULL && menu_cache_item_get_type(item) == MENU_CACHE_TYPE_DIR) + g_set_error(init->error, G_IO_ERROR, G_IO_ERROR_IS_DIRECTORY, + _("The '%s' is a menu directory"), init->path_str); + else if(item == NULL || menu_cache_item_get_type(item) != MENU_CACHE_TYPE_APP) + g_set_error(init->error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + _("The '%s' isn't a menu item"), + init->path_str ? init->path_str : "/"); + else + { + char *file_path; + GFile *gf; + GError *err = NULL; + + file_path = menu_cache_item_get_file_path(item); + if (file_path) + { + gf = g_file_new_for_path(file_path); + g_free(file_path); + if (gf) + { + init->result = g_file_read(gf, init->cancellable, &err); + if (init->result == NULL) + { + /* never return G_IO_ERROR_IS_DIRECTORY */ + if (err->domain == G_IO_ERROR && + err->code == G_IO_ERROR_IS_DIRECTORY) + { + g_error_free(err); + g_set_error(init->error, G_IO_ERROR, G_IO_ERROR_NOT_REGULAR_FILE, + _("The '%s' entry file is broken"), init->path_str); + } + else + g_propagate_error(init->error, err); + } + g_object_unref(gf); + } + } + } + +#if MENU_CACHE_CHECK_VERSION(0, 4, 0) + if(item) + menu_cache_item_unref(item); +#endif + menu_cache_unref(mc); + +_mc_failed: + return FALSE; +} + +static GFileInputStream *_fm_vfs_menu_read_fn(GFile *file, + GCancellable *cancellable, + GError **error) +{ + FmMenuVFile *item = FM_MENU_VFILE(file); + FmVfsMenuMainThreadData enu; + + /* g_debug("_fm_vfs_menu_read_fn %s", item->path); */ + enu.path_str = item->path; + enu.cancellable = cancellable; + enu.error = error; + RUN_WITH_MENU_CACHE(_fm_vfs_menu_read_fn_real, &enu); + return enu.result; +} + +static GFileOutputStream *_fm_vfs_menu_append_to(GFile *file, + GFileCreateFlags flags, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return NULL; +} + + +/* ---- FmMenuVFileOutputStream class ---- */ +#define FM_TYPE_MENU_VFILE_OUTPUT_STREAM (fm_vfs_menu_file_output_stream_get_type()) +#define FM_MENU_VFILE_OUTPUT_STREAM(o) (G_TYPE_CHECK_INSTANCE_CAST((o), \ + FM_TYPE_MENU_VFILE_OUTPUT_STREAM, \ + FmMenuVFileOutputStream)) + +typedef struct _FmMenuVFileOutputStream FmMenuVFileOutputStream; +typedef struct _FmMenuVFileOutputStreamClass FmMenuVFileOutputStreamClass; + +struct _FmMenuVFileOutputStream +{ + GFileOutputStream parent; + GOutputStream *real_stream; + gchar *path; /* "Dir/App.desktop" */ + GString *content; + gboolean do_close; +}; + +struct _FmMenuVFileOutputStreamClass +{ + GFileOutputStreamClass parent_class; +}; + +static GType fm_vfs_menu_file_output_stream_get_type (void); + +G_DEFINE_TYPE(FmMenuVFileOutputStream, fm_vfs_menu_file_output_stream, G_TYPE_FILE_OUTPUT_STREAM); + +static void fm_vfs_menu_file_output_stream_finalize(GObject *object) +{ + FmMenuVFileOutputStream *stream = FM_MENU_VFILE_OUTPUT_STREAM(object); + if(stream->real_stream) + g_object_unref(stream->real_stream); + g_free(stream->path); + g_string_free(stream->content, TRUE); + G_OBJECT_CLASS(fm_vfs_menu_file_output_stream_parent_class)->finalize(object); +} + +static gssize fm_vfs_menu_file_output_stream_write(GOutputStream *stream, + const void *buffer, gsize count, + GCancellable *cancellable, + GError **error) +{ + if (g_cancellable_set_error_if_cancelled(cancellable, error)) + return -1; + g_string_append_len(FM_MENU_VFILE_OUTPUT_STREAM(stream)->content, buffer, count); + return (gssize)count; +} + +static gboolean fm_vfs_menu_file_output_stream_close(GOutputStream *gos, + GCancellable *cancellable, + GError **error) +{ + FmMenuVFileOutputStream *stream = FM_MENU_VFILE_OUTPUT_STREAM(gos); + GKeyFile *kf; + gsize len = 0; + gchar *content; + gboolean ok; + + if (g_cancellable_set_error_if_cancelled(cancellable, error)) + return FALSE; + if (!stream->do_close) + return TRUE; + kf = g_key_file_new(); + /* parse entered file content first */ + if (stream->content->len > 0) + g_key_file_load_from_data(kf, stream->content->str, stream->content->len, + G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS, + NULL); /* FIXME: don't ignore some errors? */ + /* correct invalid data in desktop entry file: Name and Exec are mandatory, + Type must be Application, and Category should include requested one */ + if(!g_key_file_has_key(kf, G_KEY_FILE_DESKTOP_GROUP, + G_KEY_FILE_DESKTOP_KEY_NAME, NULL)) + g_key_file_set_string(kf, G_KEY_FILE_DESKTOP_GROUP, + G_KEY_FILE_DESKTOP_KEY_NAME, ""); + if(!g_key_file_has_key(kf, G_KEY_FILE_DESKTOP_GROUP, + G_KEY_FILE_DESKTOP_KEY_EXEC, NULL)) + g_key_file_set_string(kf, G_KEY_FILE_DESKTOP_GROUP, + G_KEY_FILE_DESKTOP_KEY_EXEC, ""); + g_key_file_set_string(kf, G_KEY_FILE_DESKTOP_GROUP, G_KEY_FILE_DESKTOP_KEY_TYPE, + G_KEY_FILE_DESKTOP_TYPE_APPLICATION); + content = g_key_file_to_data(kf, &len, error); + g_key_file_free(kf); + if (!content) + return FALSE; + ok = g_output_stream_write_all(stream->real_stream, content, len, &len, + cancellable, error); + g_free(content); + if (!ok || !g_output_stream_close(stream->real_stream, cancellable, error)) + return FALSE; + stream->do_close = FALSE; + if (stream->path && !_add_application(stream->path, cancellable, error)) + return FALSE; + return TRUE; +} + +static void fm_vfs_menu_file_output_stream_class_init(FmMenuVFileOutputStreamClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS(klass); + GOutputStreamClass *stream_class = G_OUTPUT_STREAM_CLASS(klass); + + gobject_class->finalize = fm_vfs_menu_file_output_stream_finalize; + stream_class->write_fn = fm_vfs_menu_file_output_stream_write; + stream_class->close_fn = fm_vfs_menu_file_output_stream_close; + /* we don't implement seek/truncate/etag/query so no GFileOutputStream funcs */ +} + +static void fm_vfs_menu_file_output_stream_init(FmMenuVFileOutputStream *stream) +{ + stream->content = g_string_sized_new(1024); + stream->do_close = TRUE; +} + +static FmMenuVFileOutputStream *_fm_vfs_menu_file_output_stream_new(const gchar *path) +{ + FmMenuVFileOutputStream *stream; + + stream = g_object_new(FM_TYPE_MENU_VFILE_OUTPUT_STREAM, NULL); + if (path) + stream->path = g_strdup(path); + return stream; +} + +static GFileOutputStream *_vfile_menu_create(GFile *file, + GFileCreateFlags flags, + GCancellable *cancellable, + GError **error, + const gchar *path) +{ + FmMenuVFileOutputStream *stream; + GFileOutputStream *ostream; + GError *err = NULL; + GFile *parent; + + if (g_cancellable_set_error_if_cancelled(cancellable, error)) + return NULL; +// g_file_delete(file, cancellable, NULL); /* remove old if there is any */ + ostream = g_file_create(file, flags, cancellable, &err); + if (ostream == NULL) + { + if (g_cancellable_is_cancelled(cancellable) || + err->domain != G_IO_ERROR || err->code != G_IO_ERROR_NOT_FOUND) + { + g_propagate_error(error, err); + return NULL; + } + /* .local/share/applications/ isn't found? make it! */ + g_clear_error(&err); + parent = g_file_get_parent(file); + if (!g_file_make_directory_with_parents(parent, cancellable, error)) + { + g_object_unref(parent); + return NULL; + } + g_object_unref(parent); + ostream = g_file_create(file, flags, cancellable, error); + if (ostream == NULL) + return ostream; + } + stream = _fm_vfs_menu_file_output_stream_new(path); + stream->real_stream = G_OUTPUT_STREAM(ostream); + return (GFileOutputStream*)stream; +} + +static GFileOutputStream *_vfile_menu_replace(GFile *file, + const char *etag, + gboolean make_backup, + GFileCreateFlags flags, + GCancellable *cancellable, + GError **error, + const gchar *path) +{ + FmMenuVFileOutputStream *stream; + GFileOutputStream *ostream; + + if (g_cancellable_set_error_if_cancelled(cancellable, error)) + return NULL; + stream = _fm_vfs_menu_file_output_stream_new(path); + ostream = g_file_replace(file, etag, make_backup, flags, cancellable, error); + if (ostream == NULL) + { + g_object_unref(stream); + return NULL; + } + stream->real_stream = G_OUTPUT_STREAM(ostream); + return (GFileOutputStream*)stream; +} + +static gboolean _fm_vfs_menu_create_real(gpointer data) +{ + FmVfsMenuMainThreadData *init = data; + MenuCache *mc; + char *unescaped = NULL, *id; + gboolean is_invalid = TRUE; + + init->result = NULL; + if(init->path_str) + { + MenuCacheItem *item; +#if !MENU_CACHE_CHECK_VERSION(0, 5, 0) + GSList *list, *l; +#endif + + mc = _get_menu_cache(init->error); + if(mc == NULL) + goto _mc_failed; + unescaped = g_uri_unescape_string(init->path_str, NULL); + /* ensure new menu item has suffix .desktop */ + if (!g_str_has_suffix(unescaped, ".desktop")) + { + id = unescaped; + unescaped = g_strconcat(unescaped, ".desktop", NULL); + g_free(id); + } + id = strrchr(unescaped, '/'); + if (id) + id++; + else + id = unescaped; +#if MENU_CACHE_CHECK_VERSION(0, 5, 0) + item = menu_cache_find_item_by_id(mc, id); + if (item) + menu_cache_item_unref(item); /* use item simply as marker */ +#else + list = menu_cache_list_all_apps(mc); + for (l = list; l; l = l->next) + if (strcmp(menu_cache_item_get_id(l->data), id) == 0) + break; + if (l) + item = l->data; + else + item = NULL; + g_slist_free_full(list, (GDestroyNotify)menu_cache_item_unref); +#endif + if(item == NULL) + is_invalid = FALSE; + /* g_debug("create id %s, category %s", id, category); */ + menu_cache_unref(mc); + } + + if(is_invalid) + g_set_error(init->error, G_IO_ERROR, G_IO_ERROR_EXISTS, + _("Cannot create menu item '%s'"), + init->path_str ? init->path_str : "/"); + else + { + GFile *gf = _g_file_new_for_id(id); + + if (gf) + { + init->result = _vfile_menu_create(gf, G_FILE_CREATE_NONE, + init->cancellable, init->error, + unescaped); + g_object_unref(gf); + } + } + g_free(unescaped); + +_mc_failed: + return FALSE; +} + +static GFileOutputStream *_fm_vfs_menu_create(GFile *file, + GFileCreateFlags flags, + GCancellable *cancellable, + GError **error) +{ + FmMenuVFile *item = FM_MENU_VFILE(file); + FmVfsMenuMainThreadData enu; + + /* g_debug("_fm_vfs_menu_create %s", item->path); */ + enu.path_str = item->path; + enu.cancellable = cancellable; + enu.error = error; + // enu.flags = flags; + RUN_WITH_MENU_CACHE(_fm_vfs_menu_create_real, &enu); + return enu.result; +} + +static gboolean _fm_vfs_menu_replace_real(gpointer data) +{ + FmVfsMenuMainThreadData *init = data; + MenuCache *mc; + char *unescaped = NULL, *id; + gboolean is_invalid = TRUE; + + init->result = NULL; + if(init->path_str) + { + MenuCacheItem *item, *item2; + + mc = _get_menu_cache(init->error); + if(mc == NULL) + goto _mc_failed; + /* prepare id first */ + unescaped = g_uri_unescape_string(init->path_str, NULL); + id = strrchr(unescaped, '/'); + if (id != NULL) + id++; + else + id = unescaped; + /* get existing item */ + item = _vfile_path_to_menu_cache_item(mc, init->path_str); + if (item != NULL) /* item is there, OK, we'll replace it then */ + is_invalid = FALSE; + /* if not found then check item by id to exclude conflicts */ + else + { +#if MENU_CACHE_CHECK_VERSION(0, 5, 0) + item2 = menu_cache_find_item_by_id(mc, id); +#else + GSList *list = menu_cache_list_all_apps(mc), *l; + for (l = list; l; l = l->next) + if (strcmp(menu_cache_item_get_id(l->data), id) == 0) + break; + if (l) + item2 = menu_cache_item_ref(l->data); + else + item2 = NULL; + g_slist_free_full(list, (GDestroyNotify)menu_cache_item_unref); +#endif + if(item2 == NULL) + is_invalid = FALSE; + else /* item was found in another category */ + menu_cache_item_unref(item2); + } + menu_cache_unref(mc); + } + + if(is_invalid) + g_set_error(init->error, G_IO_ERROR, G_IO_ERROR_EXISTS, + _("Cannot create menu item '%s'"), + init->path_str ? init->path_str : "/"); + else + { + GFile *gf = _g_file_new_for_id(id); + + if (gf) + { + /* FIXME: use flags and make_backup */ + init->result = _vfile_menu_replace(gf, NULL, FALSE, + G_FILE_CREATE_REPLACE_DESTINATION, + init->cancellable, init->error, + /* don't insert it into XML */ + NULL); + g_object_unref(gf); + } + } + g_free(unescaped); + +_mc_failed: + return FALSE; +} + +static GFileOutputStream *_fm_vfs_menu_replace(GFile *file, + const char *etag, + gboolean make_backup, + GFileCreateFlags flags, + GCancellable *cancellable, + GError **error) +{ + FmMenuVFile *item = FM_MENU_VFILE(file); + FmVfsMenuMainThreadData enu; + + /* g_debug("_fm_vfs_menu_replace %s", item->path); */ + enu.path_str = item->path; + enu.cancellable = cancellable; + enu.error = error; + // enu.flags = flags; + // enu.make_backup = make_backup; + RUN_WITH_MENU_CACHE(_fm_vfs_menu_replace_real, &enu); + return enu.result; +} + +/* not in main thread; returns NULL on failure */ +static GKeyFile *_g_key_file_from_item(GFile *file, GCancellable *cancellable, + GError **error) +{ + char *contents; + gsize length; + GKeyFile *kf; + + if (!g_file_load_contents(file, cancellable, &contents, &length, NULL, error)) + return NULL; + kf = g_key_file_new(); + if (!g_key_file_load_from_data(kf, contents, length, + G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS, + error)) + { + g_key_file_free(kf); + kf = NULL; + } + g_free(contents); + return kf; +} + +/* not in main thread; returns FALSE on failure, consumes key file */ +static gboolean _g_key_file_into_item(GFile *file, GKeyFile *kf, + GCancellable *cancellable, GError **error) +{ + char *contents; + gsize length; + gboolean result = FALSE; + + contents = g_key_file_to_data(kf, &length, error); + g_key_file_free(kf); + if (contents == NULL) + return FALSE; + result = g_file_replace_contents(file, contents, length, NULL, FALSE, + G_FILE_CREATE_REPLACE_DESTINATION, NULL, + cancellable, error); + g_free(contents); + return result; +} + +static gboolean _fm_vfs_menu_delete_file(GFile *file, + GCancellable *cancellable, + GError **error) +{ + FmMenuVFile *item = FM_MENU_VFILE(file); + GKeyFile *kf; + GError *err = NULL; + + g_debug("_fm_vfs_menu_delete_file %s", item->path); + /* load contents */ + kf = _g_key_file_from_item(file, cancellable, &err); + if (kf == NULL) + { +#if MENU_CACHE_CHECK_VERSION(0, 5, 0) + /* it might be just a directory */ + if (err->domain == G_IO_ERROR && err->code == G_IO_ERROR_IS_DIRECTORY) + { + char *unescaped = g_uri_unescape_string(item->path, NULL); + gboolean ok = _remove_directory(unescaped, cancellable, error); + g_error_free(err); + g_free(unescaped); + return ok; + } + /* else it just failed */ +#endif + g_propagate_error(error, err); + return FALSE; + } + /* set NoDisplay=true and save */ + g_key_file_set_boolean(kf, G_KEY_FILE_DESKTOP_GROUP, + G_KEY_FILE_DESKTOP_KEY_NO_DISPLAY, TRUE); + return _g_key_file_into_item(file, kf, cancellable, error); +} + +static gboolean _fm_vfs_menu_trash(GFile *file, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return FALSE; +} + +static gboolean _fm_vfs_menu_make_directory(GFile *file, + GCancellable *cancellable, + GError **error) +{ +#if !MENU_CACHE_CHECK_VERSION(0, 5, 0) + /* creating a directory with libmenu-cache < 0.5.0 will lead to invisible + directory; inexperienced user will be confused; therefore we disable + such operation in such conditions */ + ERROR_UNSUPPORTED(error); + return FALSE; +#else + FmMenuVFile *item = FM_MENU_VFILE(file); + char *unescaped; + gboolean ok; + + /* XDG desktop menu specification: desktop-entry-id should be *.desktop */ + if (g_str_has_suffix(item->path, ".desktop")) + { + g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_FILENAME, + _("Name of menu directory should not end with" + " \".desktop\"")); + return FALSE; + } + unescaped = g_uri_unescape_string(item->path, NULL); + ok = _add_directory(unescaped, cancellable, error); + g_free(unescaped); + return ok; +#endif +} + +static gboolean _fm_vfs_menu_make_symbolic_link(GFile *file, + const char *symlink_value, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return FALSE; +} + +static gboolean _fm_vfs_menu_copy(GFile *source, + GFile *destination, + GFileCopyFlags flags, + GCancellable *cancellable, + GFileProgressCallback progress_callback, + gpointer progress_callback_data, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return FALSE; +} + +static gboolean _fm_vfs_menu_move_real(gpointer data) +{ + FmVfsMenuMainThreadData *init = data; + MenuCache *mc = NULL; + MenuCacheItem *item = NULL, *item2; + char *src_path, *dst_path; + char *src_id, *dst_id; + gboolean result = FALSE; + + dst_path = init->destination->path; + if (init->path_str == NULL || dst_path == NULL) + { + g_set_error_literal(init->error, G_IO_ERROR, G_IO_ERROR_FAILED, + _("Invalid operation with menu root")); + return FALSE; + } + /* make path strings */ + src_path = g_uri_unescape_string(init->path_str, NULL); + dst_path = g_uri_unescape_string(dst_path, NULL); + src_id = strrchr(src_path, '/'); + if (src_id) + src_id++; + else + src_id = src_path; + dst_id = strrchr(dst_path, '/'); + if (dst_id) + dst_id++; + else + dst_id = dst_path; + if (strcmp(src_id, dst_id)) + { + /* ID change isn't supported now */ + ERROR_UNSUPPORTED(init->error); + goto _failed; + } + if (strcmp(src_path, dst_path) == 0) + { + g_warning("menu: tried to move '%s' into itself", src_path); + g_free(src_path); + g_free(dst_path); + return TRUE; /* nothing was changed */ + } + /* do actual move */ + mc = _get_menu_cache(init->error); + if(mc == NULL) + goto _failed; + item = _vfile_path_to_menu_cache_item(mc, init->path_str); + /* TODO: if id changed then check for ID conflicts */ + /* TODO: save updated desktop entry for old ID (if different) */ + if(item == NULL || menu_cache_item_get_type(item) != MENU_CACHE_TYPE_APP) + { + /* FIXME: implement directories movement */ + g_set_error(init->error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + _("The '%s' isn't a menu item"), init->path_str); + goto _failed; + } + item2 = _vfile_path_to_menu_cache_item(mc, init->destination->path); + if (item2) + { + g_set_error(init->error, G_IO_ERROR, G_IO_ERROR_EXISTS, + _("Menu path '%s' already exists"), dst_path); +#if MENU_CACHE_CHECK_VERSION(0, 4, 0) + menu_cache_item_unref(item2); +#endif + goto _failed; + } + /* do actual move */ + if (_add_application(dst_path, init->cancellable, init->error)) + { + if (_remove_application(src_path, init->cancellable, init->error)) + result = TRUE; + else /* failed, rollback */ + _remove_application(dst_path, init->cancellable, NULL); + } + +_failed: +#if MENU_CACHE_CHECK_VERSION(0, 4, 0) + if(item) + menu_cache_item_unref(item); +#endif + if(mc) + menu_cache_unref(mc); + g_free(src_path); + g_free(dst_path); + return result; +} + +static gboolean _fm_vfs_menu_move(GFile *source, + GFile *destination, + GFileCopyFlags flags, + GCancellable *cancellable, + GFileProgressCallback progress_callback, + gpointer progress_callback_data, + GError **error) +{ + FmMenuVFile *item = FM_MENU_VFILE(source); + FmVfsMenuMainThreadData enu; + + /* g_debug("_fm_vfs_menu_move"); */ + if(!FM_IS_FILE(destination)) + { + g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, + _("Invalid destination")); + return FALSE; + } + enu.path_str = item->path; + enu.cancellable = cancellable; + enu.error = error; + // enu.flags = flags; + enu.destination = FM_MENU_VFILE(destination); + /* FIXME: use progress_callback */ + return RUN_WITH_MENU_CACHE(_fm_vfs_menu_move_real, &enu); +} + +/* ---- FmMenuVFileMonitor class ---- */ +#define FM_TYPE_MENU_VFILE_MONITOR (fm_vfs_menu_file_monitor_get_type()) +#define FM_MENU_VFILE_MONITOR(o) (G_TYPE_CHECK_INSTANCE_CAST((o), \ + FM_TYPE_MENU_VFILE_MONITOR, FmMenuVFileMonitor)) + +typedef struct _FmMenuVFileMonitor FmMenuVFileMonitor; +typedef struct _FmMenuVFileMonitorClass FmMenuVFileMonitorClass; + +static GType fm_vfs_menu_file_monitor_get_type (void); + +struct _FmMenuVFileMonitor +{ + GFileMonitor parent_object; + + FmMenuVFile *file; + MenuCache *cache; +#if MENU_CACHE_CHECK_VERSION(0, 4, 0) + MenuCacheItem *item; + MenuCacheNotifyId notifier; +#else + GSList *items; + gboolean stopped; + gpointer notifier; +#endif +}; + +struct _FmMenuVFileMonitorClass +{ + GFileMonitorClass parent_class; +}; + +G_DEFINE_TYPE(FmMenuVFileMonitor, fm_vfs_menu_file_monitor, G_TYPE_FILE_MONITOR); + +static void fm_vfs_menu_file_monitor_finalize(GObject *object) +{ + FmMenuVFileMonitor *mon = FM_MENU_VFILE_MONITOR(object); + + if(mon->cache) + { + if(mon->notifier) + menu_cache_remove_reload_notify(mon->cache, mon->notifier); + menu_cache_unref(mon->cache); + } +#if MENU_CACHE_CHECK_VERSION(0, 4, 0) + if(mon->item) + menu_cache_item_unref(mon->item); +#else + g_slist_free_full(mon->items, (GDestroyNotify)menu_cache_item_unref); +#endif + g_object_unref(mon->file); + + G_OBJECT_CLASS(fm_vfs_menu_file_monitor_parent_class)->finalize(object); +} + +static gboolean fm_vfs_menu_file_monitor_cancel(GFileMonitor *monitor) +{ + FmMenuVFileMonitor *mon = FM_MENU_VFILE_MONITOR(monitor); + +#if MENU_CACHE_CHECK_VERSION(0, 4, 0) + if(mon->item) + menu_cache_item_unref(mon->item); /* rest will be done in finalizer */ + mon->item = NULL; +#else + mon->stopped = TRUE; +#endif + return TRUE; +} + +static void fm_vfs_menu_file_monitor_class_init(FmMenuVFileMonitorClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + GFileMonitorClass *gfilemon_class = G_FILE_MONITOR_CLASS (klass); + + gobject_class->finalize = fm_vfs_menu_file_monitor_finalize; + gfilemon_class->cancel = fm_vfs_menu_file_monitor_cancel; +} + +static void fm_vfs_menu_file_monitor_init(FmMenuVFileMonitor *item) +{ + /* nothing */ +} + +static FmMenuVFileMonitor *_fm_menu_vfile_monitor_new(void) +{ + return (FmMenuVFileMonitor*)g_object_new(FM_TYPE_MENU_VFILE_MONITOR, NULL); +} + +#if MENU_CACHE_CHECK_VERSION(0, 4, 0) +static void _reload_notify_handler(MenuCache* cache, gpointer user_data) +#else +static void _reload_notify_handler(gpointer cache, gpointer user_data) +#endif +{ + FmMenuVFileMonitor *mon = FM_MENU_VFILE_MONITOR(user_data); + GSList *items, *new_items, *ol, *nl; + MenuCacheItem *dir; + GFile *file; + const char *de_name; + guint32 de_flag; + +#if MENU_CACHE_CHECK_VERSION(0, 4, 0) + if(mon->item == NULL) /* menu folder was destroyed or monitor cancelled */ + return; + dir = mon->item; + if(mon->file->path) + mon->item = _vfile_path_to_menu_cache_item(cache, mon->file->path); + else + mon->item = MENU_CACHE_ITEM(menu_cache_dup_root_dir(cache)); + if(mon->item && menu_cache_item_get_type(mon->item) != MENU_CACHE_TYPE_DIR) + { + menu_cache_item_unref(mon->item); + mon->item = NULL; + } + if(mon->item == NULL) /* folder was destroyed - emit event and exit */ + { + menu_cache_item_unref(dir); + g_file_monitor_emit_event(G_FILE_MONITOR(mon), G_FILE(mon->file), NULL, + G_FILE_MONITOR_EVENT_DELETED); + return; + } + items = menu_cache_dir_list_children(MENU_CACHE_DIR(dir)); + menu_cache_item_unref(dir); + new_items = menu_cache_dir_list_children(MENU_CACHE_DIR(mon->item)); +#else + if(mon->stopped) /* menu folder was destroyed or monitor cancelled */ + return; + if(mon->file->path) + dir = _vfile_path_to_menu_cache_item(cache, mon->file->path); + else + dir = MENU_CACHE_ITEM(menu_cache_get_root_dir(cache)); + if(dir == NULL) /* folder was destroyed - emit event and exit */ + { + mon->stopped = TRUE; + g_file_monitor_emit_event(G_FILE_MONITOR(mon), G_FILE(mon->file), NULL, + G_FILE_MONITOR_EVENT_DELETED); + return; + } + /* emit change on the folder in any case */ + g_file_monitor_emit_event(G_FILE_MONITOR(mon), G_FILE(mon->file), NULL, + G_FILE_MONITOR_EVENT_CHANGED); + items = mon->items; + mon->items = g_slist_copy_deep(menu_cache_dir_get_children(MENU_CACHE_DIR(dir)), + (GCopyFunc)menu_cache_item_ref, NULL); + new_items = g_slist_copy_deep(mon->items, (GCopyFunc)menu_cache_item_ref, NULL); +#endif + for (ol = items; ol; ) /* remove all separatorts first */ + { + nl = ol->next; + if (menu_cache_item_get_id(ol->data) == NULL) + { + menu_cache_item_unref(ol->data); + items = g_slist_delete_link(items, ol); + } + ol = nl; + } + for (ol = new_items; ol; ) + { + nl = ol->next; + if (menu_cache_item_get_id(ol->data) == NULL) + { + menu_cache_item_unref(ol->data); + new_items = g_slist_delete_link(new_items, ol); + } + ol = nl; + } + /* we have two copies of lists now, compare them and emit events */ + ol = items; + de_name = g_getenv("XDG_CURRENT_DESKTOP"); + if(de_name) + de_flag = menu_cache_get_desktop_env_flag(cache, de_name); + else + de_flag = (guint32)-1; + while (ol) + { + for (nl = new_items; nl; nl = nl->next) + if (strcmp(menu_cache_item_get_id(ol->data), + menu_cache_item_get_id(nl->data)) == 0) + break; /* the same id found */ + if (nl) + { + /* check if any visible attribute of it was changed */ + if (g_strcmp0(menu_cache_item_get_name(ol->data), + menu_cache_item_get_name(nl->data)) == 0 || + g_strcmp0(menu_cache_item_get_icon(ol->data), + menu_cache_item_get_icon(nl->data)) == 0 || + menu_cache_app_get_is_visible(ol->data, de_flag) != + menu_cache_app_get_is_visible(nl->data, de_flag)) + { + file = _fm_vfs_menu_resolve_relative_path(G_FILE(mon->file), + menu_cache_item_get_id(nl->data)); + g_file_monitor_emit_event(G_FILE_MONITOR(mon), file, NULL, + G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED); + g_object_unref(file); + } + /* free both new and old from the list */ + menu_cache_item_unref(nl->data); + new_items = g_slist_delete_link(new_items, nl); + nl = ol->next; /* use 'nl' as storage */ + menu_cache_item_unref(ol->data); + items = g_slist_delete_link(items, ol); + ol = nl; + } + else /* id not found (removed), go to next */ + ol = ol->next; + } + /* emit events for removed files */ + while (items) + { + file = _fm_vfs_menu_resolve_relative_path(G_FILE(mon->file), + menu_cache_item_get_id(items->data)); + g_file_monitor_emit_event(G_FILE_MONITOR(mon), file, NULL, + G_FILE_MONITOR_EVENT_DELETED); + g_object_unref(file); + menu_cache_item_unref(items->data); + items = g_slist_delete_link(items, items); + } + /* emit events for added files */ + while (new_items) + { + file = _fm_vfs_menu_resolve_relative_path(G_FILE(mon->file), + menu_cache_item_get_id(new_items->data)); + g_file_monitor_emit_event(G_FILE_MONITOR(mon), file, NULL, + G_FILE_MONITOR_EVENT_CREATED); + g_object_unref(file); + menu_cache_item_unref(new_items->data); + new_items = g_slist_delete_link(new_items, new_items); + } +} + +static gboolean _fm_vfs_menu_monitor_dir_real(gpointer data) +{ + FmVfsMenuMainThreadData *init = data; + FmMenuVFileMonitor *mon; +#if !MENU_CACHE_CHECK_VERSION(0, 4, 0) + MenuCacheItem *dir; +#endif + + init->result = NULL; + if(g_cancellable_set_error_if_cancelled(init->cancellable, init->error)) + return FALSE; + /* open menu cache instance */ + mon = _fm_menu_vfile_monitor_new(); + if(mon == NULL) /* out of memory! */ + return FALSE; + mon->file = FM_MENU_VFILE(g_object_ref(init->destination)); + mon->cache = _get_menu_cache(init->error); + if(mon->cache == NULL) + goto _fail; + /* check if requested path exists within cache */ +#if MENU_CACHE_CHECK_VERSION(0, 4, 0) + if(mon->file->path) + mon->item = _vfile_path_to_menu_cache_item(mon->cache, mon->file->path); + else + mon->item = MENU_CACHE_ITEM(menu_cache_dup_root_dir(mon->cache)); + if(mon->item == NULL || menu_cache_item_get_type(mon->item) != MENU_CACHE_TYPE_DIR) +#else + if(mon->file->path) + dir = _vfile_path_to_menu_cache_item(mon->cache, mon->file->path); + else + dir = MENU_CACHE_ITEM(menu_cache_get_root_dir(mon->cache)); + if(dir == NULL) +#endif + { + g_set_error(init->error, G_IO_ERROR, G_IO_ERROR_FAILED, + _("FmMenuVFileMonitor: folder '%s' not found in menu cache"), + mon->file->path); + goto _fail; + } +#if !MENU_CACHE_CHECK_VERSION(0, 4, 0) + /* for old libmenu-cache we have no choice but copy all the data right now */ + mon->items = g_slist_copy_deep(menu_cache_dir_get_children(MENU_CACHE_DIR(dir)), + (GCopyFunc)menu_cache_item_ref, NULL); +#endif + if(g_cancellable_set_error_if_cancelled(init->cancellable, init->error)) + goto _fail; + /* current directory contents belong to mon->item now */ + /* attach reload notify handler */ + mon->notifier = menu_cache_add_reload_notify(mon->cache, + &_reload_notify_handler, mon); + init->result = mon; + return TRUE; + +_fail: + g_object_unref(mon); + return FALSE; +} + +static GFileMonitor *_fm_vfs_menu_monitor_dir(GFile *file, + GFileMonitorFlags flags, + GCancellable *cancellable, + GError **error) +{ + FmVfsMenuMainThreadData enu; + + /* g_debug("_fm_vfs_menu_monitor_dir %s", FM_MENU_VFILE(file)->path); */ + enu.cancellable = cancellable; + enu.error = error; + // enu.flags = flags; + enu.destination = FM_MENU_VFILE(file); + RUN_WITH_MENU_CACHE(_fm_vfs_menu_monitor_dir_real, &enu); + return (GFileMonitor*)enu.result; +} + +static GFileMonitor *_fm_vfs_menu_monitor_file(GFile *file, + GFileMonitorFlags flags, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return NULL; +} + +#if GLIB_CHECK_VERSION(2, 22, 0) +static GFileIOStream *_fm_vfs_menu_open_readwrite(GFile *file, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return NULL; +} + +static GFileIOStream *_fm_vfs_menu_create_readwrite(GFile *file, + GFileCreateFlags flags, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return NULL; +} + +static GFileIOStream *_fm_vfs_menu_replace_readwrite(GFile *file, + const char *etag, + gboolean make_backup, + GFileCreateFlags flags, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return NULL; +} +#endif /* Glib >= 2.22 */ + +static void fm_menu_g_file_init(GFileIface *iface) +{ + GFileAttributeInfoList *list; + + iface->dup = _fm_vfs_menu_dup; + iface->hash = _fm_vfs_menu_hash; + iface->equal = _fm_vfs_menu_equal; + iface->is_native = _fm_vfs_menu_is_native; + iface->has_uri_scheme = _fm_vfs_menu_has_uri_scheme; + iface->get_uri_scheme = _fm_vfs_menu_get_uri_scheme; + iface->get_basename = _fm_vfs_menu_get_basename; + iface->get_path = _fm_vfs_menu_get_path; + iface->get_uri = _fm_vfs_menu_get_uri; + iface->get_parse_name = _fm_vfs_menu_get_parse_name; + iface->get_parent = _fm_vfs_menu_get_parent; + iface->prefix_matches = _fm_vfs_menu_prefix_matches; + iface->get_relative_path = _fm_vfs_menu_get_relative_path; + iface->resolve_relative_path = _fm_vfs_menu_resolve_relative_path; + iface->get_child_for_display_name = _fm_vfs_menu_get_child_for_display_name; + iface->enumerate_children = _fm_vfs_menu_enumerate_children; + iface->query_info = _fm_vfs_menu_query_info; + iface->query_filesystem_info = _fm_vfs_menu_query_filesystem_info; + iface->find_enclosing_mount = _fm_vfs_menu_find_enclosing_mount; + iface->set_display_name = _fm_vfs_menu_set_display_name; + iface->query_settable_attributes = _fm_vfs_menu_query_settable_attributes; + iface->query_writable_namespaces = _fm_vfs_menu_query_writable_namespaces; + iface->set_attribute = _fm_vfs_menu_set_attribute; + iface->set_attributes_from_info = _fm_vfs_menu_set_attributes_from_info; + iface->read_fn = _fm_vfs_menu_read_fn; + iface->append_to = _fm_vfs_menu_append_to; + iface->create = _fm_vfs_menu_create; + iface->replace = _fm_vfs_menu_replace; + iface->delete_file = _fm_vfs_menu_delete_file; + iface->trash = _fm_vfs_menu_trash; + iface->make_directory = _fm_vfs_menu_make_directory; + iface->make_symbolic_link = _fm_vfs_menu_make_symbolic_link; + iface->copy = _fm_vfs_menu_copy; + iface->move = _fm_vfs_menu_move; + iface->monitor_dir = _fm_vfs_menu_monitor_dir; + iface->monitor_file = _fm_vfs_menu_monitor_file; +#if GLIB_CHECK_VERSION(2, 22, 0) + iface->open_readwrite = _fm_vfs_menu_open_readwrite; + iface->create_readwrite = _fm_vfs_menu_create_readwrite; + iface->replace_readwrite = _fm_vfs_menu_replace_readwrite; + iface->supports_thread_contexts = TRUE; +#endif /* Glib >= 2.22 */ + + list = g_file_attribute_info_list_new(); + g_file_attribute_info_list_add(list, G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN, + G_FILE_ATTRIBUTE_TYPE_BOOLEAN, + G_FILE_ATTRIBUTE_INFO_COPY_WHEN_MOVED); + g_file_attribute_info_list_add(list, G_FILE_ATTRIBUTE_STANDARD_ICON, + G_FILE_ATTRIBUTE_TYPE_OBJECT, + G_FILE_ATTRIBUTE_INFO_COPY_WHEN_MOVED); + _fm_vfs_menu_settable_attributes = list; +} + + +/* ---- FmFile implementation ---- */ +static gboolean _fm_vfs_menu_wants_incremental(GFile* file) +{ + return FALSE; +} + +static void fm_menu_fm_file_init(FmFileInterface *iface) +{ + iface->wants_incremental = _fm_vfs_menu_wants_incremental; +} + + +/* ---- interface for loading ---- */ +GFile *_fm_vfs_menu_new_for_uri(const char *uri) +{ + FmMenuVFile *item = _fm_menu_vfile_new(); + + if(uri == NULL) + uri = ""; + /* skip menu:/ */ + if(g_ascii_strncasecmp(uri, "menu:", 5) == 0) + uri += 5; + while(*uri == '/') + uri++; + /* skip "applications/" or "applications.menu/" */ + if(g_ascii_strncasecmp(uri, "applications", 12) == 0) + { + uri += 12; + if(g_ascii_strncasecmp(uri, ".menu", 5) == 0) + uri += 5; + } + while(*uri == '/') /* skip starting slashes */ + uri++; + /* save the rest of path, NULL means the root path */ + if(*uri) + { + char *end; + + item->path = g_strdup(uri); + for(end = item->path + strlen(item->path); end > item->path; end--) + if(end[-1] == '/') /* skip trailing slashes */ + end[-1] = '\0'; + else + break; + } + /* g_debug("_fm_vfs_menu_new_for_uri %s -> %s", uri, item->path); */ + return (GFile*)item; +} diff --git a/src/core/vfs/vfs-search.c b/src/core/vfs/vfs-search.c new file mode 100644 index 0000000..0a64fb6 --- /dev/null +++ b/src/core/vfs/vfs-search.c @@ -0,0 +1,1359 @@ +/* + * fm-vfs-search.c + * + * Copyright 2012 Hong Jen Yee (PCMan) + * Copyright 2010 Shae Smittle + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + * + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include "fm-file.h" + +#include + +#include +#include +#include +#include + +#define _GNU_SOURCE /* for FNM_CASEFOLD in fnmatch.h, a GNU extension */ +#include + +#if __GNUC__ >= 4 +#pragma GCC diagnostic ignored "-Wcomment" /* for comments below */ +#endif + +/* ---- Classes structures ---- */ +typedef struct _FmSearchIntIter FmSearchIntIter; + +struct _FmSearchIntIter +{ + FmSearchIntIter *parent; /* recursion path */ + GFile *folder_path; /* path to folder */ + GFileEnumerator *enu; /* children enumerator */ +}; + +#define FM_TYPE_VFS_SEACRH_ENUMERATOR (fm_vfs_search_enumerator_get_type()) +#define FM_VFS_SEACRH_ENUMERATOR(o) (G_TYPE_CHECK_INSTANCE_CAST((o),\ + FM_TYPE_VFS_SEACRH_ENUMERATOR, FmVfsSearchEnumerator)) + +typedef struct _FmVfsSearchEnumerator FmVfsSearchEnumerator; +typedef struct _FmVfsSearchEnumeratorClass FmVfsSearchEnumeratorClass; + +struct _FmVfsSearchEnumerator +{ + GFileEnumerator parent; + + FmSearchIntIter* iter; + char* attributes; + GFileQueryInfoFlags flags; + GSList* target_folders; /* GFile */ + char** name_patterns; + GRegex* name_regex; + char* content_pattern; + GRegex* content_regex; + char** mime_types; + guint64 min_mtime; + guint64 max_mtime; + guint64 min_size; + guint64 max_size; + gboolean name_case_insensitive : 1; + gboolean content_case_insensitive : 1; + gboolean recursive : 1; + gboolean show_hidden : 1; +}; + +struct _FmVfsSearchEnumeratorClass +{ + GFileEnumeratorClass parent_class; +}; + + +#define FM_TYPE_SEARCH_VFILE (fm_vfs_search_file_get_type()) +#define FM_SEARCH_VFILE(o) (G_TYPE_CHECK_INSTANCE_CAST((o), \ + FM_TYPE_SEARCH_VFILE, FmSearchVFile)) + +typedef struct _FmSearchVFile FmSearchVFile; +typedef struct _FmSearchVFileClass FmSearchVFileClass; + +static GType fm_vfs_search_file_get_type (void); + +struct _FmSearchVFile +{ + GObject parent_object; + + char *path; /* full search path */ + GFile *current; /* last scanned directory */ +}; + +struct _FmSearchVFileClass +{ + GObjectClass parent_class; +}; + + +/* beforehand declarations */ +static gboolean fm_search_job_match_file(FmVfsSearchEnumerator * priv, + GFileInfo * info, GFile * parent, + GCancellable *cancellable, + GError **error); +static void fm_search_job_match_folder(FmVfsSearchEnumerator * priv, + GFile * folder_path, + GCancellable *cancellable, + GError **error); +static void parse_search_uri(FmVfsSearchEnumerator* priv, const char* uri_str); + + +/* ---- Directory iterator ---- */ +/* caller should g_object_ref(folder_path) if success */ +static inline FmSearchIntIter *_search_iter_new(FmSearchIntIter *parent, + const char *attributes, + GFileQueryInfoFlags flags, + GFile *folder_path, + GCancellable *cancellable, + GError **error) +{ + GFileEnumerator *enu; + FmSearchIntIter *iter; + + enu = g_file_enumerate_children(folder_path, attributes, flags, + cancellable, error); + if(enu == NULL) + return NULL; + iter = g_slice_new(FmSearchIntIter); + iter->parent = parent; + iter->folder_path = folder_path; + iter->enu = enu; + return iter; +} + +static inline void _search_iter_free(FmSearchIntIter *iter, GCancellable *cancellable) +{ + g_file_enumerator_close(iter->enu, cancellable, NULL); + g_object_unref(iter->enu); + g_object_unref(iter->folder_path); + g_slice_free(FmSearchIntIter, iter); +} + + +/* ---- search enumerator class ---- */ +static GType fm_vfs_search_enumerator_get_type (void); + +G_DEFINE_TYPE(FmVfsSearchEnumerator, fm_vfs_search_enumerator, G_TYPE_FILE_ENUMERATOR) + +static void _fm_vfs_search_enumerator_dispose(GObject *object) +{ + FmVfsSearchEnumerator *priv = FM_VFS_SEACRH_ENUMERATOR(object); + FmSearchIntIter *iter; + + while((iter = priv->iter)) + { + priv->iter = iter->parent; + _search_iter_free(iter, NULL); + } + + if(priv->attributes) + { + g_free(priv->attributes); + priv->attributes = NULL; + } + + if(priv->target_folders) + { + g_slist_foreach(priv->target_folders, (GFunc)g_object_unref, NULL); + g_slist_free(priv->target_folders); + priv->target_folders = NULL; + } + + if(priv->name_patterns) + { + g_strfreev(priv->name_patterns); + priv->name_patterns = NULL; + } + + if(priv->name_regex) + { + g_regex_unref(priv->name_regex); + priv->name_regex = NULL; + } + + if(priv->content_pattern) + { + g_free(priv->content_pattern); + priv->content_pattern = NULL; + } + + if(priv->content_regex) + { + g_regex_unref(priv->content_regex); + priv->content_regex = NULL; + } + + if(priv->mime_types) + { + g_strfreev(priv->mime_types); + priv->mime_types = NULL; + } + + G_OBJECT_CLASS(fm_vfs_search_enumerator_parent_class)->dispose(object); +} + +/* The next directory that is a match with the recursive search: */ +static GFileInfo *recur_dir_match = NULL; + +static GFileInfo *_fm_vfs_search_enumerator_next_file(GFileEnumerator *enumerator, + GCancellable *cancellable, + GError **error) +{ + FmVfsSearchEnumerator *enu = FM_VFS_SEACRH_ENUMERATOR(enumerator); + FmSearchIntIter *iter; + GFileInfo * file_info; + GError *err = NULL; + FmSearchVFile *container; + + /* g_debug("_fm_vfs_search_enumerator_next_file"); */ + while(!g_cancellable_set_error_if_cancelled(cancellable, error)) + { + iter = enu->iter; + if(iter == NULL) /* ended with folder */ + { + if(enu->target_folders == NULL) + break; + iter = _search_iter_new(NULL, enu->attributes, enu->flags, + enu->target_folders->data, + cancellable, error); + if(iter == NULL) + break; + /* data is moved into iter now so free link itself */ + enu->target_folders = g_slist_delete_link(enu->target_folders, + enu->target_folders); + container = FM_SEARCH_VFILE(g_file_enumerator_get_container(enumerator)); + if(container->current) + g_object_unref(container->current); + container->current = g_object_ref(iter->folder_path); + enu->iter = iter; + } + + gboolean had_recur_dir_match = (recur_dir_match != NULL); + file_info = had_recur_dir_match ? recur_dir_match : g_file_enumerator_next_file(iter->enu, cancellable, &err); + if(file_info && g_file_info_get_name(file_info)) + { + gboolean is_recursive = (enu->recursive && + /* SF bug #969: very possibly we get multiple instances of the + same file if we follow symlink to a directory + FIXME: make it optional? */ + !g_file_info_get_is_symlink(file_info) && + g_file_info_get_file_type(file_info) == G_FILE_TYPE_DIRECTORY); + /* check if directory itself matches criteria */ + if(fm_search_job_match_file(enu, file_info, iter->folder_path, + cancellable, &err)) + { + g_debug("found matched: %s", g_file_info_get_name(file_info)); + if(err == NULL && is_recursive) + { + if(recur_dir_match) + { + /* directory match was found last time; + search inside it recursively below */ + recur_dir_match = NULL; + } + else + { + /* directory match is found; remember it for the + next recursive search before returning it */ + recur_dir_match = file_info; + return file_info; + } + } + else + return file_info; + } + + /* recurse upon each directory */ + if(err == NULL && is_recursive) + { + if(enu->show_hidden || !g_file_info_get_is_hidden(file_info)) + { + const char * name = g_file_info_get_name(file_info); + GFile * file = g_file_get_child(iter->folder_path, name); + /* go into directory and iterate it now */ + fm_search_job_match_folder(enu, file, cancellable, &err); + g_object_unref(file); + } + } + + if(!had_recur_dir_match) + g_object_unref(file_info); + if(err == NULL) + continue; + } + + if(err != NULL) /* file_info == NULL */ + { + if(err->domain == G_IO_ERROR && err->code == G_IO_ERROR_PERMISSION_DENIED) + { + g_error_free(err); /* ignore this error */ + err = NULL; + continue; + } + g_propagate_error(error, err); + break; + } + /* else end of file list - go up */ + enu->iter = iter->parent; + container = FM_SEARCH_VFILE(g_file_enumerator_get_container(enumerator)); + if(container->current) + g_object_unref(container->current); + if(enu->iter) + container->current = g_object_ref(enu->iter->folder_path); + else + container->current = NULL; + _search_iter_free(iter, cancellable); + } + return NULL; +} + +static gboolean _fm_vfs_search_enumerator_close(GFileEnumerator *enumerator, + GCancellable *cancellable, + GError **error) +{ + recur_dir_match = NULL; + FmVfsSearchEnumerator *enu = FM_VFS_SEACRH_ENUMERATOR(enumerator); + FmSearchIntIter *iter; + + while((iter = enu->iter)) + { + enu->iter = iter->parent; + _search_iter_free(iter, cancellable); + } + return TRUE; +} + +static void fm_vfs_search_enumerator_class_init(FmVfsSearchEnumeratorClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS(klass); + GFileEnumeratorClass *enumerator_class = G_FILE_ENUMERATOR_CLASS(klass); + + gobject_class->dispose = _fm_vfs_search_enumerator_dispose; + + enumerator_class->next_file = _fm_vfs_search_enumerator_next_file; + enumerator_class->close_fn = _fm_vfs_search_enumerator_close; + +} + +static void fm_vfs_search_enumerator_init(FmVfsSearchEnumerator *enumerator) +{ + /* nothing */ +} + +static GFileEnumerator *_fm_vfs_search_enumerator_new(GFile *file, + const char *path_str, + const char *attributes, + GFileQueryInfoFlags flags, + GError **error) +{ + FmVfsSearchEnumerator *enumerator; + + enumerator = g_object_new(FM_TYPE_VFS_SEACRH_ENUMERATOR, "container", file, NULL); + + enumerator->attributes = g_strdup(attributes); + enumerator->flags = flags; + parse_search_uri(enumerator, path_str); + /* FIXME: don't ignore flags */ + + return G_FILE_ENUMERATOR(enumerator); +} + + +/* ---- The search engine ---- */ + +/* + * name: parse_date_str + * @str: a string in YYYY-MM-DD format + * Return: a time_t value + */ +static time_t parse_date_str(const char* str) +{ + int len = strlen(str); + if(G_LIKELY(len >= 8)) + { + struct tm timeinfo = {0}; + if(sscanf(str, "%04d-%02d-%02d", &timeinfo.tm_year, &timeinfo.tm_mon, &timeinfo.tm_mday) == 3) + { + timeinfo.tm_year -= 1900; /* should be years since 1900 */ + --timeinfo.tm_mon; /* month should be 0-11 */ + return mktime(&timeinfo); + } + } + return 0; +} + +/* + * parse_search_uri + * @job + * @uri: a search uri + * + * Format of a search URI is similar to that of an http URI: + * + * search://,,?&&... + * The optional parameter key/value pairs are: + * show_hidden=<0 or 1>: whether to search for hidden files + * recursive=<0 or 1>: whether to search sub folders recursively + * name=: patterns of filenames, separated by comma + * name_regex=: regular expression + * name_case_sensitive=<0 or 1> + * content=: search for files containing the pattern + * content_regex=: regular expression + * content_case_sensitive=<0 or 1> + * mime_types=: mime-types to search for, can use /* (ex: image/*), separated by ';' + * min_size= + * max_size= + * min_mtime=YYYY-MM-DD + * max_mtime=YYYY-MM-DD + * + * An example to search all *.desktop files in /usr/share and /usr/local/share + * can be written like this: + * + * search:///usr/share,/usr/local/share?recursive=1&show_hidden=0&name=*.desktop&name_ci=0 + * + * If the folder paths and parameters contain invalid characters for a + * URI, they should be escaped. + * + */ +static void parse_search_uri(FmVfsSearchEnumerator* priv, const char* uri_str) +{ + const char scheme[] = "search://"; /* NOTE: sizeof(scheme) includes '\0' */ + if(g_ascii_strncasecmp(uri_str, scheme, sizeof(scheme)-1) == 0) + { + const char* p = uri_str + sizeof(scheme)-1; /* skip scheme part */ + char* params = strchr(p, '?'); + char* name_regex = NULL; + char* content_regex = NULL; + + /* add folder paths */ + while (p) + { + char* sep = strchr(p, ','); /* use , to separate multiple paths */ + char *path; + + if (sep && (params == NULL || sep < params)) + path = g_uri_unescape_segment(p, sep, NULL); + else if (params != NULL) + { + path = g_uri_unescape_segment(p, params, NULL); + sep = NULL; + } + else + path = g_uri_unescape_string(p, NULL); + /* g_debug("target folder path: %s", path); */ + /* add the path to target folders */ + priv->target_folders = g_slist_prepend(priv->target_folders, + g_file_new_for_commandline_arg(path)); + g_free(path); + + p = sep; + if (p) /* it's on ':' now */ + p++; + } + + /* priv->target_folders = g_slist_reverse(priv->target_folders); */ + + /* decode parameters */ + if(params) + { + params++; /* skip '?' */ + while(*params) + { + /* parameters are in name=value pairs */ + char *name; + char* value = strchr(params, '='); + char* sep = strchr(params, '&'); + + if (value && (sep == NULL || value < sep)) + { + name = g_strndup(params, value - params); + if (sep) + value = g_uri_unescape_segment(value+1, sep, NULL); + else + value = g_uri_unescape_string(value+1, NULL); + } + else if (sep) + { + name = g_strndup(params, sep - params); + value = NULL; + } + else /* value == NULL && sep == NULL */ + name = g_strdup(params); + + /* g_printf("parameter name/value: %s = %s\n", name, value); */ + + if(strcmp(name, "show_hidden") == 0) + priv->show_hidden = (value[0] == '1') ? TRUE : FALSE; + else if(strcmp(name, "recursive") == 0) + priv->recursive = (value[0] == '1') ? TRUE : FALSE; + else if(strcmp(name, "name") == 0) + priv->name_patterns = g_strsplit(value, ",", 0); + else if(strcmp(name, "name_regex") == 0) + { + g_free(name_regex); + name_regex = value; + value = NULL; + } + else if(strcmp(name, "name_ci") == 0) + priv->name_case_insensitive = (value[0] == '1') ? TRUE : FALSE; + else if(strcmp(name, "content") == 0) + { + g_free(priv->content_pattern); + priv->content_pattern = value; + value = NULL; + } + else if(strcmp(name, "content_regex") == 0) + { + g_free(content_regex); + content_regex = value; + value = NULL; + } + else if(strcmp(name, "content_ci") == 0) + priv->content_case_insensitive = (value[0] == '1') ? TRUE : FALSE; + else if(strcmp(name, "mime_types") == 0) + { + priv->mime_types = g_strsplit(value, ";", -1); + + /* For mime_type patterns such as image/* and audio/*, + * we move the trailing '*' to begining of the string + * as a measure of optimization. Later we can detect if it's a + * pattern or a full type name by checking the first char. */ + if(priv->mime_types) + { + char** pmime_type; + for(pmime_type = priv->mime_types; *pmime_type; ++pmime_type) + { + char* mime_type = *pmime_type; + int len = strlen(mime_type); + /* if the mime_type is end with "/*" */ + if(len > 2 && /*mime_type[len - 2] == '/' &&*/ mime_type[len - 1] == '*') + { + /* move the trailing * to first char */ + memmove(mime_type + 1, mime_type, len - 1); + mime_type[0] = '*'; + } + } + + if (!g_strstr_len(priv->attributes, -1, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE)) + { + gchar * attributes = g_strconcat(priv->attributes, ",", G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, NULL); + g_free(priv->attributes); + priv->attributes = attributes; + } + + } + } + else if(strcmp(name, "min_size") == 0) + priv->min_size = atoll(value); + else if(strcmp(name, "max_size") == 0) + priv->max_size = atoll(value); + else if(strcmp(name, "min_mtime") == 0) + priv->min_mtime = (guint64)parse_date_str(value); + else if(strcmp(name, "max_mtime") == 0) + priv->max_mtime = (guint64)parse_date_str(value); + + g_free(name); + g_free(value); + + /* continue with the next param=value pair */ + if(sep) + params = sep + 1; + else + break; + } + + if(name_regex) + { + /* we set G_REGEX_RAW because GLib might cause a crash + if a search is done in a non-utf8 string */ + GRegexCompileFlags flags = G_REGEX_RAW; + if(priv->name_case_insensitive) + flags |= G_REGEX_CASELESS; + priv->name_regex = g_regex_new(name_regex, flags, 0, NULL); + g_free(name_regex); + } + + if(content_regex) + { + GRegexCompileFlags flags = G_REGEX_RAW; /* like above */ + if(priv->content_case_insensitive) + flags |= G_REGEX_CASELESS; + priv->content_regex = g_regex_new(content_regex, flags, 0, NULL); + g_free(content_regex); + } + + if(priv->content_case_insensitive) /* case insensitive */ + { + if(priv->content_pattern) /* make sure the pattern is lower case */ + { + char* down = g_utf8_strdown(priv->content_pattern, -1); + g_free(priv->content_pattern); + priv->content_pattern = down; + } + } + } + } +} + +static void fm_search_job_match_folder(FmVfsSearchEnumerator * priv, + GFile * folder_path, + GCancellable *cancellable, + GError **error) +{ + FmSearchIntIter *iter; + FmSearchVFile *container; + + /* FIXME: make error if NULL */ + iter = _search_iter_new(priv->iter, priv->attributes, priv->flags, folder_path, + cancellable, error); + if(iter == NULL) /* error */ + return; + g_object_ref(folder_path); /* it's copied into iter */ + priv->iter = iter; + container = FM_SEARCH_VFILE(g_file_enumerator_get_container(G_FILE_ENUMERATOR(priv))); + if(container->current) + g_object_unref(container->current); + container->current = g_object_ref(folder_path); +} + +static gboolean fm_search_job_match_filename(FmVfsSearchEnumerator* priv, GFileInfo* info) +{ + gboolean ret; + + /* g_debug("fm_search_job_match_filename: %s", g_file_info_get_name(info)); */ + if(priv->name_regex) + { + const char* name = g_file_info_get_name(info); + ret = g_regex_match(priv->name_regex, name, 0, NULL); + } + else if(priv->name_patterns) + { + ret = FALSE; + const char* name = g_file_info_get_name(info); + char** ppattern; + for(ppattern = priv->name_patterns; *ppattern; ++ppattern) + { + const char* pattern = *ppattern; + /* FIXME: FNM_CASEFOLD is a GNU extension */ + int flags = FNM_PERIOD; + if(priv->name_case_insensitive) + flags |= FNM_CASEFOLD; + if(fnmatch(pattern, name, flags) == 0) + ret = TRUE; + } + } + else + ret = TRUE; + return ret; +} + +static gboolean fm_search_job_match_content_line_based(FmVfsSearchEnumerator* priv, + GFileInfo* info, + GInputStream* stream, + GCancellable* cancellable, + GError** error) +{ + gboolean ret = FALSE; + /* create a buffered data input stream for line-based I/O */ + GDataInputStream *input_stream = g_data_input_stream_new(stream); + do + { + gsize line_len; + char* line = g_data_input_stream_read_line(input_stream, &line_len, cancellable, error); + if(line == NULL) /* error or EOF */ + break; + if(priv->content_regex) + { + /* match using regexp */ + ret = g_regex_match(priv->content_regex, line, 0, NULL); + } + else if(priv->content_pattern && priv->content_case_insensitive) + { + /* case insensitive search is line-based because we need to + * do utf8 validation + case conversion and it's easier to + * do with lines than with raw streams. */ + if(g_utf8_validate(line, -1, NULL)) + { + /* this whole line contains valid UTF-8 */ + char* down = g_utf8_strdown(line, -1); + g_free(line); + line = down; + } + else /* non-UTF8, treat as ASCII */ + { + char* p; + for(p = line; *p; ++p) + *p = g_ascii_tolower(*p); + } + + if(strstr(line, priv->content_pattern)) + ret = TRUE; + } + g_free(line); + }while(ret == FALSE); + g_object_unref(input_stream); + return ret; +} + +static gboolean fm_search_job_match_content_exact(FmVfsSearchEnumerator* priv, + GFileInfo* info, + GInputStream* stream, + GCancellable* cancellable, + GError** error) +{ + gboolean ret = FALSE; + char *buf, *pbuf; + gssize size; + + /* Ensure that the allocated buffer is longer than the string being + * searched for. Otherwise it's not possible for the buffer to + * contain a string fully matching the pattern. */ + int pattern_len = strlen(priv->content_pattern); + int buf_size = pattern_len > 4095 ? pattern_len : 4095; + int bytes_to_read; + + buf = g_new(char, buf_size + 1); /* +1 for terminating null char. */ + bytes_to_read = buf_size; + pbuf = buf; + for(;;) + { + char* found; + size = g_input_stream_read(stream, pbuf, bytes_to_read, cancellable, error); + if(size <=0) /* EOF or error */ + break; + pbuf[size] = '\0'; /* make the string null terminated */ + + found = strstr(buf, priv->content_pattern); + if(found) /* the string is found in the buffer */ + { + ret = TRUE; + break; + } + else if(size == bytes_to_read) /* if size < bytes_to_read, we're at EOF and there are no further data. */ + { + /* Preserve the last bytes and move them to + * the beginning of the buffer. + * Append further data after this chunk of data at next read. */ + int preserve_len = pattern_len - 1; + char* buf_end = buf + buf_size; + memmove(buf, buf_end - preserve_len, preserve_len); + pbuf = buf + preserve_len; + bytes_to_read = buf_size - preserve_len; + } + } + g_free(buf); + return ret; +} + +static gboolean fm_search_job_match_content(FmVfsSearchEnumerator* priv, + GFileInfo* info, GFile* parent, + GCancellable* cancellable, + GError** error) +{ + gboolean ret; + if(priv->content_pattern || priv->content_regex) + { + ret = FALSE; + if(g_file_info_get_file_type(info) == G_FILE_TYPE_REGULAR && g_file_info_get_size(info) > 0) + { + GFile* file = g_file_get_child(parent, g_file_info_get_name(info)); + /* NOTE: I disabled mmap-based search since this could cause + * unexpected crashes sometimes if the mapped files are + * removed or changed during the search. */ + GFileInputStream * stream = g_file_read(file, cancellable, error); + g_object_unref(file); + + if(stream) + { + if(priv->content_pattern && !priv->content_case_insensitive) + { + /* stream based search optimized for case sensitive + * exact match. */ + ret = fm_search_job_match_content_exact(priv, info, + G_INPUT_STREAM(stream), + cancellable, error); + } + else + { + /* grep-like regexp search and case insensitive search + * are line-based. */ + ret = fm_search_job_match_content_line_based(priv, info, + G_INPUT_STREAM(stream), + cancellable, error); + } + + g_input_stream_close(G_INPUT_STREAM(stream), cancellable, NULL); + g_object_unref(stream); + } + } + } + else + ret = TRUE; + return ret; +} + +static gboolean fm_search_job_match_file_type(FmVfsSearchEnumerator* priv, GFileInfo* info) +{ + gboolean ret; + if(priv->mime_types) + { + const char* file_type = g_file_info_get_content_type(info); + char** pmime_type; + ret = FALSE; + for(pmime_type = priv->mime_types; *pmime_type; ++pmime_type) + { + const char* mime_type = *pmime_type; + /* For mime_type patterns such as image/* and audio/*, + * we move the trailing '*' to begining of the string + * as a measure of optimization. We can know it's a + * pattern not a full type name by checking the first char. */ + if(mime_type[0] == '*') + { + if(g_str_has_prefix(file_type, mime_type + 1)) + { + ret = TRUE; + break; + } + } + else if(g_content_type_is_a(file_type, mime_type)) + { + ret = TRUE; + break; + } + } + } + else + ret = TRUE; + return ret; +} + +static gboolean fm_search_job_match_size(FmVfsSearchEnumerator* priv, GFileInfo* info) +{ + guint64 size = g_file_info_get_size(info); + gboolean ret = TRUE; + if(priv->min_size > 0 && size < priv->min_size) + ret = FALSE; + else if(priv->max_size > 0 && size > priv->max_size) + ret = FALSE; + else if ((priv->min_size > 0 || priv->max_size > 0) && g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY) + ret = FALSE; + return ret; +} + +static gboolean fm_search_job_match_mtime(FmVfsSearchEnumerator* priv, GFileInfo* info) +{ + gboolean ret = TRUE; + if(priv->min_mtime || priv->max_mtime) + { + guint64 mtime = g_file_info_get_attribute_uint64(info, G_FILE_ATTRIBUTE_TIME_MODIFIED); + /* g_print("file mtime: %llu, min_mtime=%llu, max_mtime=%llu\n", mtime, priv->min_mtime, priv->max_mtime); */ + if(priv->min_mtime > 0 && mtime < priv->min_mtime) + ret = FALSE; /* earlier than min_mtime */ + else if(priv->max_mtime > 0 && mtime > priv->max_mtime) + ret = FALSE; /* later than max_mtime */ + } + return ret; +} + +static gboolean fm_search_job_match_file(FmVfsSearchEnumerator * priv, + GFileInfo * info, GFile * parent, + GCancellable *cancellable, + GError **error) +{ + //g_print("matching file %s\n", g_file_info_get_name(info)); + + if(!priv->show_hidden && g_file_info_get_is_hidden(info)) + return FALSE; + + if(!fm_search_job_match_filename(priv, info)) + return FALSE; + + if(!fm_search_job_match_file_type(priv, info)) + return FALSE; + + if(!fm_search_job_match_size(priv, info)) + return FALSE; + + if(!fm_search_job_match_mtime(priv, info)) + return FALSE; + + if(!fm_search_job_match_content(priv, info, parent, cancellable, error)) + return FALSE; + + return TRUE; +} + + +/* end of rule functions */ + +/* ---- FmSearchVFile class ---- */ +static void fm_search_g_file_init(GFileIface *iface); +static void fm_search_fm_file_init(FmFileInterface *iface); + +G_DEFINE_TYPE_WITH_CODE(FmSearchVFile, fm_vfs_search_file, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(G_TYPE_FILE, fm_search_g_file_init) + G_IMPLEMENT_INTERFACE(FM_TYPE_FILE, fm_search_fm_file_init)) + +static void fm_vfs_search_file_finalize(GObject *object) +{ + FmSearchVFile *item = FM_SEARCH_VFILE(object); + + g_free(item->path); + if(item->current) + g_object_unref(item->current); + + G_OBJECT_CLASS(fm_vfs_search_file_parent_class)->finalize(object); +} + +static void fm_vfs_search_file_class_init(FmSearchVFileClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = fm_vfs_search_file_finalize; +} + +static void fm_vfs_search_file_init(FmSearchVFile *item) +{ + /* nothing */ +} + +static FmSearchVFile *_fm_search_vfile_new(void) +{ + return (FmSearchVFile*)g_object_new(FM_TYPE_SEARCH_VFILE, NULL); +} + + +/* ---- GFile implementation ---- */ +#define ERROR_UNSUPPORTED(err) g_set_error_literal(err, G_IO_ERROR, \ + G_IO_ERROR_NOT_SUPPORTED, _("Operation not supported")) + +static GFile *_fm_vfs_search_dup(GFile *file) +{ + FmSearchVFile *item, *new_item; + + item = FM_SEARCH_VFILE(file); + new_item = _fm_search_vfile_new(); + if(item->path) + new_item->path = g_strdup(item->path); + return (GFile*)new_item; +} + +static guint _fm_vfs_search_hash(GFile *file) +{ + return g_str_hash(FM_SEARCH_VFILE(file)->path); +} + +static gboolean _fm_vfs_search_equal(GFile *file1, GFile *file2) +{ + char *path1 = FM_SEARCH_VFILE(file1)->path; + char *path2 = FM_SEARCH_VFILE(file2)->path; + + return g_str_equal(path1, path2); +} + +static gboolean _fm_vfs_search_is_native(GFile *file) +{ + return FALSE; +} + +static gboolean _fm_vfs_search_has_uri_scheme(GFile *file, const char *uri_scheme) +{ + return g_ascii_strcasecmp(uri_scheme, "search") == 0; +} + +static char *_fm_vfs_search_get_uri_scheme(GFile *file) +{ + return g_strdup("search"); +} + +static char *_fm_vfs_search_get_basename(GFile *file) +{ + return g_strdup("/"); +} + +static char *_fm_vfs_search_get_path(GFile *file) +{ + return NULL; +} + +static char *_fm_vfs_search_get_uri(GFile *file) +{ + FmSearchVFile *item = FM_SEARCH_VFILE(file); + + if(item->current) + return g_file_get_uri(item->current); + return g_strdup(item->path); +} + +static char *_fm_vfs_search_get_parse_name(GFile *file) +{ + /* FIXME: need name to be converted to UTF-8? */ + return g_strdup(_("Search")); +} + +static GFile *_fm_vfs_search_get_parent(GFile *file) +{ + return NULL; +} + +static gboolean _fm_vfs_search_prefix_matches(GFile *prefix, GFile *file) +{ + return FALSE; +} + +static char *_fm_vfs_search_get_relative_path(GFile *parent, GFile *descendant) +{ + return NULL; +} + +static GFile *_fm_vfs_search_resolve_relative_path(GFile *file, const char *relative_path) +{ + return NULL; +} + +static GFile *_fm_vfs_search_get_child_for_display_name(GFile *file, + const char *display_name, + GError **error) +{ + FmSearchVFile *new_item; + + g_return_val_if_fail(file != NULL, NULL); + + if (display_name == NULL || *display_name == '\0') + return g_object_ref(file); + /* just append "/"display_name to file->path */ + new_item = _fm_search_vfile_new(); + new_item->path = g_strdup_printf("%s/%s", FM_SEARCH_VFILE(file)->path, display_name); + return (GFile*)new_item; +} + +static GFileEnumerator *_fm_vfs_search_enumerate_children(GFile *file, + const char *attributes, + GFileQueryInfoFlags flags, + GCancellable *cancellable, + GError **error) +{ + const char *path = FM_SEARCH_VFILE(file)->path; + + return _fm_vfs_search_enumerator_new(file, path, attributes, flags, error); +} + +static GFileInfo *_fm_vfs_search_query_info(GFile *file, + const char *attributes, + GFileQueryInfoFlags flags, + GCancellable *cancellable, + GError **error) +{ + GFileInfo *fileinfo = g_file_info_new(); + GIcon* icon; + + /* g_debug("_fm_vfs_search_query_info on %s", FM_SEARCH_VFILE(file)->path); */ + /* FIXME: use matcher to set only requested data */ + g_file_info_set_name(fileinfo, FM_SEARCH_VFILE(file)->path); + g_file_info_set_display_name(fileinfo, _("Search Results")); + icon = g_themed_icon_new("search"); + g_file_info_set_icon(fileinfo, icon); + g_object_unref(icon); + g_file_info_set_file_type(fileinfo, G_FILE_TYPE_DIRECTORY); + return fileinfo; +} + +static GFileInfo *_fm_vfs_search_query_filesystem_info(GFile *file, + const char *attributes, + GCancellable *cancellable, + GError **error) +{ + /* FIXME: set some info on it */ + ERROR_UNSUPPORTED(error); + return NULL; +} + +static GMount *_fm_vfs_search_find_enclosing_mount(GFile *file, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return NULL; +} + +static GFile *_fm_vfs_search_set_display_name(GFile *file, + const char *display_name, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return NULL; +} + +static GFileAttributeInfoList *_fm_vfs_search_query_settable_attributes(GFile *file, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return NULL; +} + +static GFileAttributeInfoList *_fm_vfs_search_query_writable_namespaces(GFile *file, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return NULL; +} + +static gboolean _fm_vfs_search_set_attribute(GFile *file, + const char *attribute, + GFileAttributeType type, + gpointer value_p, + GFileQueryInfoFlags flags, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return FALSE; +} + +static gboolean _fm_vfs_search_set_attributes_from_info(GFile *file, + GFileInfo *info, + GFileQueryInfoFlags flags, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return FALSE; +} + +static GFileInputStream *_fm_vfs_search_read_fn(GFile *file, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return NULL; +} + +static GFileOutputStream *_fm_vfs_search_append_to(GFile *file, + GFileCreateFlags flags, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return NULL; +} + +static GFileOutputStream *_fm_vfs_search_create(GFile *file, + GFileCreateFlags flags, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return NULL; +} + +static GFileOutputStream *_fm_vfs_search_replace(GFile *file, + const char *etag, + gboolean make_backup, + GFileCreateFlags flags, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return NULL; +} + +static gboolean _fm_vfs_search_delete_file(GFile *file, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return FALSE; +} + +static gboolean _fm_vfs_search_trash(GFile *file, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return FALSE; +} + +static gboolean _fm_vfs_search_make_directory(GFile *file, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return FALSE; +} + +static gboolean _fm_vfs_search_make_symbolic_link(GFile *file, + const char *symlink_value, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return FALSE; +} + +static gboolean _fm_vfs_search_copy(GFile *source, + GFile *destination, + GFileCopyFlags flags, + GCancellable *cancellable, + GFileProgressCallback progress_callback, + gpointer progress_callback_data, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return FALSE; +} + +static gboolean _fm_vfs_search_move(GFile *source, + GFile *destination, + GFileCopyFlags flags, + GCancellable *cancellable, + GFileProgressCallback progress_callback, + gpointer progress_callback_data, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return FALSE; +} + +static GFileMonitor *_fm_vfs_search_monitor_dir(GFile *file, + GFileMonitorFlags flags, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return NULL; +} + +static GFileMonitor *_fm_vfs_search_monitor_file(GFile *file, + GFileMonitorFlags flags, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return NULL; +} + +#if GLIB_CHECK_VERSION(2, 22, 0) +static GFileIOStream *_fm_vfs_search_open_readwrite(GFile *file, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return NULL; +} + +static GFileIOStream *_fm_vfs_search_create_readwrite(GFile *file, + GFileCreateFlags flags, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return NULL; +} + +static GFileIOStream *_fm_vfs_search_replace_readwrite(GFile *file, + const char *etag, + gboolean make_backup, + GFileCreateFlags flags, + GCancellable *cancellable, + GError **error) +{ + ERROR_UNSUPPORTED(error); + return NULL; +} +#endif /* Glib >= 2.22 */ + +static void fm_search_g_file_init(GFileIface *iface) +{ + iface->dup = _fm_vfs_search_dup; + iface->hash = _fm_vfs_search_hash; + iface->equal = _fm_vfs_search_equal; + iface->is_native = _fm_vfs_search_is_native; + iface->has_uri_scheme = _fm_vfs_search_has_uri_scheme; + iface->get_uri_scheme = _fm_vfs_search_get_uri_scheme; + iface->get_basename = _fm_vfs_search_get_basename; + iface->get_path = _fm_vfs_search_get_path; + iface->get_uri = _fm_vfs_search_get_uri; + iface->get_parse_name = _fm_vfs_search_get_parse_name; + iface->get_parent = _fm_vfs_search_get_parent; + iface->prefix_matches = _fm_vfs_search_prefix_matches; + iface->get_relative_path = _fm_vfs_search_get_relative_path; + iface->resolve_relative_path = _fm_vfs_search_resolve_relative_path; + iface->get_child_for_display_name = _fm_vfs_search_get_child_for_display_name; + iface->enumerate_children = _fm_vfs_search_enumerate_children; + iface->query_info = _fm_vfs_search_query_info; + iface->query_filesystem_info = _fm_vfs_search_query_filesystem_info; + iface->find_enclosing_mount = _fm_vfs_search_find_enclosing_mount; + iface->set_display_name = _fm_vfs_search_set_display_name; + iface->query_settable_attributes = _fm_vfs_search_query_settable_attributes; + iface->query_writable_namespaces = _fm_vfs_search_query_writable_namespaces; + iface->set_attribute = _fm_vfs_search_set_attribute; + iface->set_attributes_from_info = _fm_vfs_search_set_attributes_from_info; + iface->read_fn = _fm_vfs_search_read_fn; + iface->append_to = _fm_vfs_search_append_to; + iface->create = _fm_vfs_search_create; + iface->replace = _fm_vfs_search_replace; + iface->delete_file = _fm_vfs_search_delete_file; + iface->trash = _fm_vfs_search_trash; + iface->make_directory = _fm_vfs_search_make_directory; + iface->make_symbolic_link = _fm_vfs_search_make_symbolic_link; + iface->copy = _fm_vfs_search_copy; + iface->move = _fm_vfs_search_move; + iface->monitor_dir = _fm_vfs_search_monitor_dir; + iface->monitor_file = _fm_vfs_search_monitor_file; +#if GLIB_CHECK_VERSION(2, 22, 0) + iface->open_readwrite = _fm_vfs_search_open_readwrite; + iface->create_readwrite = _fm_vfs_search_create_readwrite; + iface->replace_readwrite = _fm_vfs_search_replace_readwrite; + iface->supports_thread_contexts = TRUE; +#endif /* Glib >= 2.22 */ +} + + +/* ---- FmFile implementation ---- */ +static gboolean _fm_vfs_search_wants_incremental(GFile* file) +{ + return TRUE; +} + +static void fm_search_fm_file_init(FmFileInterface *iface) +{ + iface->wants_incremental = _fm_vfs_search_wants_incremental; +} + + +/* ---- interface for loading ---- */ +GFile *_fm_vfs_search_new_for_uri(const char *uri) +{ + FmSearchVFile *item; + g_return_val_if_fail(uri != NULL, NULL); + item = _fm_search_vfile_new(); + item->path = g_strdup(uri); + return (GFile*)item; +} diff --git a/src/core/volumemanager.cpp b/src/core/volumemanager.cpp new file mode 100644 index 0000000..522c38c --- /dev/null +++ b/src/core/volumemanager.cpp @@ -0,0 +1,111 @@ +#include "volumemanager.h" + +namespace Fm { + +std::mutex VolumeManager::mutex_; +std::weak_ptr VolumeManager::globalInstance_; + +VolumeManager::VolumeManager(): + QObject(), + monitor_{g_volume_monitor_get(), false} { + + // connect gobject signal handlers + g_signal_connect(monitor_.get(), "volume-added", G_CALLBACK(_onGVolumeAdded), this); + g_signal_connect(monitor_.get(), "volume-removed", G_CALLBACK(_onGVolumeRemoved), this); + g_signal_connect(monitor_.get(), "volume-changed", G_CALLBACK(_onGVolumeChanged), this); + + g_signal_connect(monitor_.get(), "mount-added", G_CALLBACK(_onGMountAdded), this); + g_signal_connect(monitor_.get(), "mount-removed", G_CALLBACK(_onGMountRemoved), this); + g_signal_connect(monitor_.get(), "mount-changed", G_CALLBACK(_onGMountChanged), this); + + // g_get_volume_monitor() is a slow blocking call, so call it in a low priority thread + auto job = new GetGVolumeMonitorJob(); + job->setAutoDelete(true); + connect(job, &GetGVolumeMonitorJob::finished, this, &VolumeManager::onGetGVolumeMonitorFinished, Qt::BlockingQueuedConnection); + job->runAsync(QThread::LowPriority); +} + +VolumeManager::~VolumeManager() { + if(monitor_) { + g_signal_handlers_disconnect_by_data(monitor_.get(), this); + } +} + +std::shared_ptr VolumeManager::globalInstance() { + std::lock_guard lock{mutex_}; + auto mon = globalInstance_.lock(); + if(mon == nullptr) { + mon = std::make_shared(); + globalInstance_ = mon; + } + return mon; +} + +void VolumeManager::onGetGVolumeMonitorFinished() { + auto job = static_cast(sender()); + monitor_ = std::move(job->monitor_); + GList* vols = g_volume_monitor_get_volumes(monitor_.get()); + for(GList* l = vols; l != nullptr; l = l->next) { + volumes_.push_back(Volume{G_VOLUME(l->data), false}); + Q_EMIT volumeAdded(volumes_.back()); + } + g_list_free(vols); + + GList* mnts = g_volume_monitor_get_mounts(monitor_.get()); + for(GList* l = mnts; l != nullptr; l = l->next) { + mounts_.push_back(Mount{G_MOUNT(l->data), false}); + Q_EMIT mountAdded(mounts_.back()); + } + g_list_free(mnts); +} + +void VolumeManager::onGVolumeAdded(GVolume* vol) { + if(std::find(volumes_.cbegin(), volumes_.cend(), vol) != volumes_.cend()) + return; + volumes_.push_back(Volume{vol, true}); + Q_EMIT volumeAdded(volumes_.back()); +} + +void VolumeManager::onGVolumeRemoved(GVolume* vol) { + auto it = std::find(volumes_.begin(), volumes_.end(), vol); + if(it == volumes_.end()) + return; + Q_EMIT volumeRemoved(*it); + volumes_.erase(it); +} + +void VolumeManager::onGVolumeChanged(GVolume* vol) { + auto it = std::find(volumes_.begin(), volumes_.end(), vol); + if(it == volumes_.end()) + return; + Q_EMIT volumeChanged(*it); +} + +void VolumeManager::onGMountAdded(GMount* mnt) { + if(std::find(mounts_.cbegin(), mounts_.cend(), mnt) != mounts_.cend()) + return; + mounts_.push_back(Mount{mnt, true}); + Q_EMIT mountAdded(mounts_.back()); +} + +void VolumeManager::onGMountRemoved(GMount* mnt) { + auto it = std::find(mounts_.begin(), mounts_.end(), mnt); + if(it == mounts_.end()) + return; + Q_EMIT mountRemoved(*it); + mounts_.erase(it); +} + +void VolumeManager::onGMountChanged(GMount* mnt) { + auto it = std::find(mounts_.begin(), mounts_.end(), mnt); + if(it == mounts_.end()) + return; + Q_EMIT mountChanged(*it); +} + +void VolumeManager::GetGVolumeMonitorJob::exec() { + monitor_ = GVolumeMonitorPtr{g_volume_monitor_get(), false}; +} + + +} // namespace Fm diff --git a/src/core/volumemanager.h b/src/core/volumemanager.h new file mode 100644 index 0000000..642adc8 --- /dev/null +++ b/src/core/volumemanager.h @@ -0,0 +1,237 @@ +#ifndef FM2_VOLUMEMANAGER_H +#define FM2_VOLUMEMANAGER_H + +#include "../libfmqtglobals.h" +#include +#include +#include "gioptrs.h" +#include "filepath.h" +#include "iconinfo.h" +#include "job.h" +#include +#include + +namespace Fm { + +class LIBFM_QT_API Volume: public GVolumePtr { +public: + + explicit Volume(GVolume* gvol, bool addRef): GVolumePtr{gvol, addRef} { + } + + explicit Volume(GVolumePtr gvol): GVolumePtr{std::move(gvol)} { + } + + CStrPtr name() const { + return CStrPtr{g_volume_get_name(get())}; + } + + CStrPtr uuid() const { + return CStrPtr{g_volume_get_uuid(get())}; + } + + std::shared_ptr icon() const { + return IconInfo::fromGIcon(GIconPtr{g_volume_get_icon(get()), false}); + } + + // GDrive * g_volume_get_drive(get()); + GMountPtr mount() const { + return GMountPtr{g_volume_get_mount(get()), false}; + } + + bool canMount() const { + return g_volume_can_mount(get()); + } + + bool shouldAutoMount() const { + return g_volume_should_automount(get()); + } + + FilePath activationRoot() const { + return FilePath{g_volume_get_activation_root(get()), false}; + } + + /* + void g_volume_mount(get()); + gboolean g_volume_mount_finish(get()); + */ + bool canEject() const { + return g_volume_can_eject(get()); + } + + /* + void g_volume_eject(get()); + gboolean g_volume_eject_finish(get()); + void g_volume_eject_with_operation(get()); + gboolean g_volume_eject_with_operation_finish(get()); + char ** g_volume_enumerate_identifiers(get()); + char * g_volume_get_identifier(get()); + const gchar * g_volume_get_sort_key(get()); + */ + + CStrPtr device() const { + return CStrPtr{g_volume_get_identifier(get(), G_VOLUME_IDENTIFIER_KIND_UNIX_DEVICE)}; + } + + CStrPtr label() const { + return CStrPtr{g_volume_get_identifier(get(), G_VOLUME_IDENTIFIER_KIND_LABEL)}; + } + +}; + + +class LIBFM_QT_API Mount: public GMountPtr { +public: + + explicit Mount(GMount* mnt, bool addRef): GMountPtr{mnt, addRef} { + } + + explicit Mount(GMountPtr gmnt): GMountPtr{std::move(gmnt)} { + } + + CStrPtr name() const { + return CStrPtr{g_mount_get_name(get())}; + } + + CStrPtr uuid() const { + return CStrPtr{g_mount_get_uuid(get())}; + } + + std::shared_ptr icon() const { + return IconInfo::fromGIcon(GIconPtr{g_mount_get_icon(get()), false}); + } + + // GIcon * g_mount_get_symbolic_icon(get()); + // GDrive * g_mount_get_drive(get()); + FilePath root() const { + return FilePath{g_mount_get_root(get()), false}; + } + + GVolumePtr volume() const { + return GVolumePtr{g_mount_get_volume(get()), false}; + } + + FilePath defaultLocation() const { + return FilePath{g_mount_get_default_location(get()), false}; + } + + bool canUnmount() const { + return g_mount_can_unmount(get()); + } + +/* + void g_mount_unmount(get()); + gboolean g_mount_unmount_finish(get()); + void g_mount_unmount_with_operation(get()); + gboolean g_mount_unmount_with_operation_finish(get()); + void g_mount_remount(get()); + gboolean g_mount_remount_finish(get()); +*/ + bool canEject() const { + return g_mount_can_eject(get()); + } + +/* + void g_mount_eject(get()); + gboolean g_mount_eject_finish(get()); + void g_mount_eject_with_operation(get()); + gboolean g_mount_eject_with_operation_finish(get()); +*/ + // void g_mount_guess_content_type(get()); + // gchar ** g_mount_guess_content_type_finish(get()); + // gchar ** g_mount_guess_content_type_sync(get()); + + bool isShadowed() const { + return g_mount_is_shadowed(get()); + } + + // void g_mount_shadow(get()); + // void g_mount_unshadow(get()); + // const gchar * g_mount_get_sort_key(get()); +}; + + + +class LIBFM_QT_API VolumeManager : public QObject { + Q_OBJECT +public: + explicit VolumeManager(); + + ~VolumeManager(); + + const std::vector& volumes() const { + return volumes_; + } + + const std::vector& mounts() const { + return mounts_; + } + + static std::shared_ptr globalInstance(); + +Q_SIGNALS: + void volumeAdded(const Volume& vol); + void volumeRemoved(const Volume& vol); + void volumeChanged(const Volume& vol); + + void mountAdded(const Mount& mnt); + void mountRemoved(const Mount& mnt); + void mountChanged(const Mount& mnt); + +public Q_SLOTS: + + void onGetGVolumeMonitorFinished(); + +private: + + class GetGVolumeMonitorJob: public Job { + public: + GetGVolumeMonitorJob() {} + GVolumeMonitorPtr monitor_; + protected: + void exec() override; + }; + + static void _onGVolumeAdded(GVolumeMonitor* /*mon*/, GVolume* vol, VolumeManager* _this) { + _this->onGVolumeAdded(vol); + } + void onGVolumeAdded(GVolume* vol); + + static void _onGVolumeRemoved(GVolumeMonitor* /*mon*/, GVolume* vol, VolumeManager* _this) { + _this->onGVolumeRemoved(vol); + } + void onGVolumeRemoved(GVolume* vol); + + static void _onGVolumeChanged(GVolumeMonitor* /*mon*/, GVolume* vol, VolumeManager* _this) { + _this->onGVolumeChanged(vol); + } + void onGVolumeChanged(GVolume* vol); + + static void _onGMountAdded(GVolumeMonitor* /*mon*/, GMount* mnt, VolumeManager* _this) { + _this->onGMountAdded(mnt); + } + void onGMountAdded(GMount* mnt); + + static void _onGMountRemoved(GVolumeMonitor* /*mon*/, GMount* mnt, VolumeManager* _this) { + _this->onGMountRemoved(mnt); + } + void onGMountRemoved(GMount* mnt); + + static void _onGMountChanged(GVolumeMonitor* /*mon*/, GMount* mnt, VolumeManager* _this) { + _this->onGMountChanged(mnt); + } + void onGMountChanged(GMount* mnt); + +private: + GVolumeMonitorPtr monitor_; + + std::vector volumes_; + std::vector mounts_; + + static std::mutex mutex_; + static std::weak_ptr globalInstance_; +}; + +} // namespace Fm + +#endif // FM2_VOLUMEMANAGER_H diff --git a/src/createnewmenu.cpp b/src/createnewmenu.cpp new file mode 100644 index 0000000..843cb03 --- /dev/null +++ b/src/createnewmenu.cpp @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "createnewmenu.h" +#include "folderview.h" +#include "utilities.h" +#include "core/iconinfo.h" +#include "core/templates.h" + +#include + +namespace Fm { + + +class TemplateAction: public QAction { +public: + TemplateAction(std::shared_ptr item, QObject* parent): + QAction(parent) { + setTemplateItem(std::move(item)); + } + + const std::shared_ptr templateItem() const { + return templateItem_; + } + + void setTemplateItem(std::shared_ptr item) { + templateItem_ = std::move(item); + auto mimeType = templateItem_->mimeType(); + setText(QString("%1 (%2)").arg(templateItem_->displayName()).arg(mimeType->desc())); + setIcon(templateItem_->icon()->qicon()); + } + +private: + std::shared_ptr templateItem_; +}; + + +CreateNewMenu::CreateNewMenu(QWidget* dialogParent, Fm::FilePath dirPath, QWidget* parent): + QMenu(parent), + dialogParent_(dialogParent), + dirPath_(std::move(dirPath)), + templateSeparator_{nullptr}, + templates_{Templates::globalInstance()} { + + 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 + connect(templates_.get(), &Templates::itemAdded, this, &CreateNewMenu::addTemplateItem); + connect(templates_.get(), &Templates::itemChanged, this, &CreateNewMenu::updateTemplateItem); + connect(templates_.get(), &Templates::itemRemoved, this, &CreateNewMenu::removeTemplateItem); + // when a template directory is already loaded + templates_->forEachItem([this](const std::shared_ptr& item) { + addTemplateItem(item); + }); +} + +CreateNewMenu::~CreateNewMenu() { +} + +void CreateNewMenu::onCreateNewFile() { + if(dirPath_) { + createFileOrFolder(CreateNewTextFile, dirPath_, nullptr, dialogParent_); + } +} + +void CreateNewMenu::onCreateNewFolder() { + if(dirPath_) { + createFileOrFolder(CreateNewFolder, dirPath_, nullptr, dialogParent_); + } +} + +void CreateNewMenu::onCreateNew() { + TemplateAction* action = static_cast(sender()); + if(dirPath_) { + createFileOrFolder(CreateWithTemplate, dirPath_, action->templateItem().get(), dialogParent_); + } +} + +void CreateNewMenu::addTemplateItem(const std::shared_ptr &item) { + if(!templateSeparator_) { + templateSeparator_= addSeparator(); + } + auto mimeType = item->mimeType(); + /* we support directories differently */ + if(mimeType->isDir()) { + return; + } + + QAction* action = new TemplateAction{item, this}; + connect(action, &QAction::triggered, this, &CreateNewMenu::onCreateNew); + + // sort actions alphabetically + const auto allActions = actions(); + auto separatorPos = allActions.indexOf(templateSeparator_); + if(allActions.size() == separatorPos + 1) { + addAction(action); + } + else { + // checking actions from end to start is usually faster + for(auto i = allActions.size() - 1; i > separatorPos; --i) { + if(QString::compare(action->text(), allActions[i]->text(), Qt::CaseInsensitive) > 0) { + if(i == allActions.size() - 1) { + addAction(action); + } + else { + insertAction(allActions[i + 1], action); + } + return; + } + } + insertAction(allActions[separatorPos + 1], action); + } +} + +void CreateNewMenu::updateTemplateItem(const std::shared_ptr &oldItem, const std::shared_ptr &newItem) { + auto allActions = actions(); + auto separatorPos = allActions.indexOf(templateSeparator_); + // all items after the separator are templates + for(auto i = separatorPos + 1; i < allActions.size(); ++i) { + auto action = static_cast(allActions[i]); + if(action->templateItem() == oldItem) { + // update the menu item + action->setTemplateItem(newItem); + break; + } + } +} + +void CreateNewMenu::removeTemplateItem(const std::shared_ptr &item) { + if(!templateSeparator_) { + return; + } + auto allActions = actions(); + auto separatorPos = allActions.indexOf(templateSeparator_); + // all items after the separator are templates + for(auto i = separatorPos + 1; i < allActions.size(); ++i) { + auto action = static_cast(allActions[i]); + if(action->templateItem() == item) { + // delete the action from the menu + removeAction(action); + allActions.removeAt(i); + break; + } + } + + // no more template items. remove the separator + if(separatorPos == allActions.size() - 1) { + removeAction(templateSeparator_); + templateSeparator_ = nullptr; + } +} + +} // namespace Fm diff --git a/src/createnewmenu.h b/src/createnewmenu.h new file mode 100644 index 0000000..078103a --- /dev/null +++ b/src/createnewmenu.h @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_CREATENEWMENU_H +#define FM_CREATENEWMENU_H + +#include "libfmqtglobals.h" +#include + +#include "core/filepath.h" + +namespace Fm { + +class FolderView; +class Templates; +class TemplateItem; + +class LIBFM_QT_API CreateNewMenu : public QMenu { + Q_OBJECT + +public: + explicit CreateNewMenu(QWidget* dialogParent, Fm::FilePath dirPath, QWidget* parent = 0); + virtual ~CreateNewMenu(); + +protected Q_SLOTS: + void onCreateNewFolder(); + + void onCreateNewFile(); + + void onCreateNew(); + +private Q_SLOTS: + void addTemplateItem(const std::shared_ptr& item); + + void updateTemplateItem(const std::shared_ptr& oldItem, const std::shared_ptr& newItem); + + void removeTemplateItem(const std::shared_ptr& item); + +private: + QWidget* dialogParent_; + Fm::FilePath dirPath_; + QAction* templateSeparator_; + std::shared_ptr templates_; +}; + +} + +#endif // FM_CREATENEWMENU_H diff --git a/src/customaction_p.h b/src/customaction_p.h new file mode 100644 index 0000000..7aba3f9 --- /dev/null +++ b/src/customaction_p.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_CUSTOMACTION_P_H +#define FM_CUSTOMACTION_P_H + +#include +#include "customactions/fileaction.h" + +namespace Fm { + +class CustomAction : public QAction { +public: + explicit CustomAction(std::shared_ptr item, QObject* parent = nullptr): + QAction{QString::fromStdString(item->get_name()), parent}, + item_{item} { + auto& icon_name = item->get_icon(); + if(!icon_name.empty()) { + setIcon(QIcon::fromTheme(icon_name.c_str())); + } + } + + virtual ~CustomAction() { + } + + const std::shared_ptr& item() const { + return item_; + } + +private: + std::shared_ptr item_; +}; + +} // namespace Fm + +#endif diff --git a/src/customactions/fileaction.cpp b/src/customactions/fileaction.cpp new file mode 100644 index 0000000..31d5e3a --- /dev/null +++ b/src/customactions/fileaction.cpp @@ -0,0 +1,615 @@ +#include "fileaction.h" +#include +#include + +using namespace std; + +namespace Fm { + +static const char* desktop_env = nullptr; // current desktop environment +static bool actions_loaded = false; // all actions are loaded? +static unordered_map, CStrHash, CStrEqual> all_actions; // cache all loaded actions + +FileActionObject::FileActionObject() { +} + +FileActionObject::FileActionObject(GKeyFile* kf) { + name = CStrPtr{g_key_file_get_locale_string(kf, "Desktop Entry", "Name", nullptr, nullptr)}; + tooltip = CStrPtr{g_key_file_get_locale_string(kf, "Desktop Entry", "Tooltip", nullptr, nullptr)}; + icon = CStrPtr{g_key_file_get_locale_string(kf, "Desktop Entry", "Icon", nullptr, nullptr)}; + desc = CStrPtr{g_key_file_get_locale_string(kf, "Desktop Entry", "Description", nullptr, nullptr)}; + GErrorPtr err; + enabled = g_key_file_get_boolean(kf, "Desktop Entry", "Enabled", &err); + if(err) { // key not found, default to true + err.reset(); + enabled = true; + } + hidden = g_key_file_get_boolean(kf, "Desktop Entry", "Hidden", nullptr); + suggested_shortcut = CStrPtr{g_key_file_get_string(kf, "Desktop Entry", "SuggestedShortcut", nullptr)}; + + condition = unique_ptr {new FileActionCondition(kf, "Desktop Entry")}; + + has_parent = false; +} + +FileActionObject::~FileActionObject() { +} + +//static +bool FileActionObject::is_plural_exec(const char* exec) { + if(!exec) { + return false; + } + // the first relevent code encountered in Exec parameter + // determines whether the command accepts singular or plural forms + for(int i = 0; exec[i]; ++i) { + char ch = exec[i]; + if(ch == '%') { + ++i; + ch = exec[i]; + switch(ch) { + case 'B': + case 'D': + case 'F': + case 'M': + case 'O': + case 'U': + case 'W': + case 'X': + return true; // plural + case 'b': + case 'd': + case 'f': + case 'm': + case 'o': + case 'u': + case 'w': + case 'x': + return false; // singular + default: + // irrelevent code, skip + break; + } + } + } + return false; // singular form by default +} + +std::string FileActionObject::expand_str(const char* templ, const FileInfoList& files, bool for_display, std::shared_ptr first_file) { + if(!templ) { + return string{}; + } + string result; + + if(!first_file) { + first_file = files.front(); + } + + for(int i = 0; templ[i]; ++i) { + char ch = templ[i]; + if(ch == '%') { + ++i; + ch = templ[i]; + switch(ch) { + case 'b': // (first) basename + if(for_display) { + result += first_file->name(); + } + else { + CStrPtr quoted{g_shell_quote(first_file->name().c_str())}; + result += quoted.get(); + } + break; + case 'B': // space-separated list of basenames + for(auto& fi : files) { + if(for_display) { + result += fi->name(); + } + else { + CStrPtr quoted{g_shell_quote(fi->name().c_str())}; + result += quoted.get(); + } + result += ' '; + } + if(result[result.length() - 1] == ' ') { // remove trailing space + result.erase(result.length() - 1); + } + break; + case 'c': // count of selected items + result += to_string(files.size()); + break; + case 'd': { // (first) base directory + // FIXME: should the base dir be a URI? + auto base_dir = first_file->dirPath(); + auto str = base_dir.toString(); + if(for_display) { + // FIXME: str = Filename.display_name(str); + } + CStrPtr quoted{g_shell_quote(str.get())}; + result += quoted.get(); + break; + } + case 'D': // space-separated list of base directory of each selected items + for(auto& fi : files) { + auto base_dir = fi->dirPath(); + auto str = base_dir.toString(); + if(for_display) { + // str = Filename.display_name(str); + } + CStrPtr quoted{g_shell_quote(str.get())}; + result += quoted.get(); + result += ' '; + } + if(result[result.length() - 1] == ' ') { // remove trailing space + result.erase(result.length() - 1); + } + break; + case 'f': { // (first) file name + auto filename = first_file->path().toString(); + if(for_display) { + // filename = Filename.display_name(filename); + } + CStrPtr quoted{g_shell_quote(filename.get())}; + result += quoted.get(); + break; + } + case 'F': // space-separated list of selected file names + for(auto& fi : files) { + auto filename = fi->path().toString(); + if(for_display) { + // filename = Filename.display_name(filename); + } + CStrPtr quoted{g_shell_quote(filename.get())}; + result += quoted.get(); + result += ' '; + } + if(result[result.length() - 1] == ' ') { // remove trailing space + result.erase(result.length() - 1); + } + break; + case 'h': // hostname of the (first) URI + // FIXME: how to support this correctly? + // FIXME: currently we pass g_get_host_name() + result += g_get_host_name(); + break; + case 'm': // mimetype of the (first) selected item + result += first_file->mimeType()->name(); + break; + case 'M': // space-separated list of the mimetypes of the selected items + for(auto& fi : files) { + result += fi->mimeType()->name(); + result += ' '; + } + break; + case 'n': // username of the (first) URI + // FIXME: how to support this correctly? + result += g_get_user_name(); + break; + case 'o': // no-op operator which forces a singular form of execution when specified as first parameter, + case 'O': // no-op operator which forces a plural form of execution when specified as first parameter, + break; + case 'p': // port number of the (first) URI + // FIXME: how to support this correctly? + // result.append("0"); + break; + case 's': // scheme of the (first) URI + result += first_file->path().uriScheme().get(); + break; + case 'u': // (first) URI + result += first_file->path().uri().get(); + break; + case 'U': // space-separated list of selected URIs + for(auto& fi : files) { + result += fi->path().uri().get(); + result += ' '; + } + if(result[result.length() - 1] == ' ') { // remove trailing space + result.erase(result.length() - 1); + } + break; + case 'w': { // (first) basename without the extension + auto basename = first_file->name(); + int pos = basename.rfind('.'); + // FIXME: handle non-UTF8 filenames + if(pos != -1) { + basename.erase(pos, string::npos); + } + CStrPtr quoted{g_shell_quote(basename.c_str())}; + result += quoted.get(); + break; + } + case 'W': // space-separated list of basenames without their extension + for(auto& fi : files) { + auto basename = fi->name(); + int pos = basename.rfind('.'); + // FIXME: for_display ? Shell.quote(str) : str); + if(pos != -1) { + basename.erase(pos, string::npos); + } + CStrPtr quoted{g_shell_quote(basename.c_str())}; + result += quoted.get(); + result += ' '; + } + if(result[result.length() - 1] == ' ') { // remove trailing space + result.erase(result.length() - 1); + } + break; + case 'x': { // (first) extension + auto basename = first_file->name(); + int pos = basename.rfind('.'); + const char* ext = ""; + if(pos >= 0) { + ext = basename.c_str() + pos + 1; + } + CStrPtr quoted{g_shell_quote(ext)}; + result += quoted.get(); + break; + } + case 'X': // space-separated list of extensions + for(auto& fi : files) { + auto basename = fi->name(); + int pos = basename.rfind('.'); + const char* ext = ""; + if(pos >= 0) { + ext = basename.c_str() + pos + 1; + } + CStrPtr quoted{g_shell_quote(ext)}; + result += quoted.get(); + result += ' '; + } + if(result[result.length() - 1] == ' ') { // remove trailing space + result.erase(result.length() - 1); + } + break; + case '%': // the % character + result += '%'; + break; + case '\0': + break; + } + } + else { + result += ch; + } + } + return result; +} + +FileAction::FileAction(GKeyFile* kf): FileActionObject{kf}, target{FILE_ACTION_TARGET_CONTEXT} { + type = FileActionType::ACTION; + + GErrorPtr err; + if(g_key_file_get_boolean(kf, "Desktop Entry", "TargetContext", &err)) { // default to true + target |= FILE_ACTION_TARGET_CONTEXT; + } + else if(!err) { // error means the key is abscent + target &= ~FILE_ACTION_TARGET_CONTEXT; + } + if(g_key_file_get_boolean(kf, "Desktop Entry", "TargetLocation", nullptr)) { + target |= FILE_ACTION_TARGET_LOCATION; + } + if(g_key_file_get_boolean(kf, "Desktop Entry", "TargetToolbar", nullptr)) { + target |= FILE_ACTION_TARGET_TOOLBAR; + } + toolbar_label = CStrPtr{g_key_file_get_locale_string(kf, "Desktop Entry", "ToolbarLabel", nullptr, nullptr)}; + + auto profile_names = CStrArrayPtr{g_key_file_get_string_list(kf, "Desktop Entry", "Profiles", nullptr, nullptr)}; + if(profile_names != nullptr) { + for(auto profile_name = profile_names.get(); *profile_name; ++profile_name) { + // stdout.printf("%s", profile); + profiles.push_back(make_shared(kf, *profile_name)); + } + } +} + +std::shared_ptr FileAction::match(const FileInfoList& files) const { + //qDebug() << "FileAction.match: " << id.get(); + if(hidden || !enabled) { + return nullptr; + } + + if(!condition->match(files)) { + return nullptr; + } + for(const auto& profile : profiles) { + if(profile->match(files)) { + //qDebug() << " profile matched!\n\n"; + return profile; + } + } + // stdout.printf("\n"); + return nullptr; +} + +FileActionMenu::FileActionMenu(GKeyFile* kf): FileActionObject{kf} { + type = FileActionType::MENU; + items_list = CStrArrayPtr{g_key_file_get_string_list(kf, "Desktop Entry", "ItemsList", nullptr, nullptr)}; +} + +bool FileActionMenu::match(const FileInfoList& files) const { + // stdout.printf("FileActionMenu.match: %s\n", id); + if(hidden || !enabled) { + return false; + } + if(!condition->match(files)) { + return false; + } + // stdout.printf("menu matched!: %s\n\n", id); + return true; +} + +void FileActionMenu::cache_children(const FileInfoList& files, const char** items_list) { + for(; *items_list; ++items_list) { + const char* item_id_prefix = *items_list; + size_t len = strlen(item_id_prefix); + if(item_id_prefix[0] == '[' && item_id_prefix[len - 1] == ']') { + // runtime dynamic item list + char* output; + int exit_status; + string prefix{item_id_prefix + 1, len - 2}; // skip [ and ] + auto command = expand_str(prefix.c_str(), files); + if(g_spawn_command_line_sync(command.c_str(), &output, nullptr, &exit_status, nullptr) && exit_status == 0) { + CStrArrayPtr item_ids{g_strsplit(output, ";", -1)}; + g_free(output); + cache_children(files, (const char**)item_ids.get()); + } + } + else if(strcmp(item_id_prefix, "SEPARATOR") == 0) { + // separator item + cached_children.push_back(nullptr); + } + else { + CStrPtr item_id{g_strconcat(item_id_prefix, ".desktop", nullptr)}; + auto it = all_actions.find(item_id.get()); + if(it != all_actions.end()) { + auto child_action = it->second; + child_action->has_parent = true; + cached_children.push_back(child_action); + // stdout.printf("add child: %s to menu: %s\n", item_id, id); + } + } + } +} + +std::shared_ptr FileActionItem::fromActionObject(std::shared_ptr action_obj, const FileInfoList& files) { + std::shared_ptr item; + if(action_obj->type == FileActionType::MENU) { + auto menu = static_pointer_cast(action_obj); + if(menu->match(files)) { + item = make_shared(menu, files); + // eliminate empty menus + if(item->children.empty()) { + item = nullptr; + } + } + } + else { + // handle profiles here + auto action = static_pointer_cast(action_obj); + auto profile = action->match(files); + if(profile) { + item = make_shared(action, profile, files); + } + } + return item; +} + +FileActionItem::FileActionItem(std::shared_ptr _action, std::shared_ptr _profile, const FileInfoList& files): + FileActionItem{static_pointer_cast(_action), files} { + profile = _profile; +} + +FileActionItem::FileActionItem(std::shared_ptr menu, const FileInfoList& files): + FileActionItem{static_pointer_cast(menu), files} { + for(auto& action_obj : menu->cached_children) { + if(action_obj == nullptr) { // separator + children.push_back(nullptr); + } + else { // action item or menu + auto subitem = fromActionObject(action_obj, files); + if(subitem != nullptr) { + children.push_back(subitem); + } + } + } +} + +FileActionItem::FileActionItem(std::shared_ptr _action, const FileInfoList& files) { + action = std::move(_action); + name = FileActionObject::expand_str(action->name.get(), files, true); + desc = FileActionObject::expand_str(action->desc.get(), files, true); + icon = FileActionObject::expand_str(action->icon.get(), files, false); +} + +bool FileActionItem::launch(GAppLaunchContext* ctx, const FileInfoList& files, CStrPtr& output) const { + if(action->type == FileActionType::ACTION) { + if(profile != nullptr) { + profile->launch(ctx, files, output); + } + return true; + } + return false; +} + +static void load_actions_from_dir(const char* dirname, const char* id_prefix) { + //qDebug() << "loading from: " << dirname << endl; + auto dir = g_dir_open(dirname, 0, nullptr); + if(dir != nullptr) { + for(;;) { + const char* name = g_dir_read_name(dir); + if(name == nullptr) { + break; + } + // found a file in file-manager/actions dir, get its full path + CStrPtr full_path{g_build_filename(dirname, name, nullptr)}; + // stdout.printf("\nfound %s\n", full_path); + + // see if it's a sub dir + if(g_file_test(full_path.get(), G_FILE_TEST_IS_DIR)) { + // load sub dirs recursively + CStrPtr new_id_prefix; + if(id_prefix) { + new_id_prefix = CStrPtr{g_strconcat(id_prefix, "-", name, nullptr)}; + } + load_actions_from_dir(full_path.get(), id_prefix ? new_id_prefix.get() : name); + } + else if(g_str_has_suffix(name, ".desktop")) { + CStrPtr new_id_prefix; + if(id_prefix) { + new_id_prefix = CStrPtr{g_strconcat(id_prefix, "-", name, nullptr)}; + } + const char* id = id_prefix ? new_id_prefix.get() : name; + // ensure that it's not already in the cache + if(all_actions.find(id) == all_actions.cend()) { + auto kf = g_key_file_new(); + if(g_key_file_load_from_file(kf, full_path.get(), G_KEY_FILE_NONE, nullptr)) { + auto type = CStrPtr{g_key_file_get_string(kf, "Desktop Entry", "Type", nullptr)}; + if(!type) { + continue; + } + std::shared_ptr action; + if(strcmp(type.get(), "Action") == 0) { + action = static_pointer_cast(make_shared(kf)); + // stdout.printf("load action: %s\n", id); + } + else if(strcmp(type.get(), "Menu") == 0) { + action = static_pointer_cast(make_shared(kf)); + // stdout.printf("load menu: %s\n", id); + } + else { + continue; + } + action->setId(id); + all_actions.insert(make_pair(action->id.get(), action)); // add the id/action pair to hash table + // stdout.printf("add to cache %s\n", id); + } + g_key_file_free(kf); + } + else { + // stdout.printf("cache found for action: %s\n", id); + } + } + } + g_dir_close(dir); + } +} + +void file_actions_set_desktop_env(const char* env) { + desktop_env = env; +} + +static void load_all_actions() { + all_actions.clear(); + auto dirs = g_get_system_data_dirs(); + for(auto dir = dirs; *dir; ++dir) { + CStrPtr dir_path{g_build_filename(*dir, "file-manager/actions", nullptr)}; + load_actions_from_dir(dir_path.get(), nullptr); + } + CStrPtr dir_path{g_build_filename(g_get_user_data_dir(), "file-manager/actions", nullptr)}; + load_actions_from_dir(dir_path.get(), nullptr); + actions_loaded = true; +} + +bool FileActionItem::compare_items(std::shared_ptr a, std::shared_ptr b) +{ + // first get the list of level-zero item names (http://www.nautilus-actions.org/?q=node/377) + static QStringList itemNamesList; + static bool level_zero_checked = false; + if(!level_zero_checked) { + level_zero_checked = true; + auto level_zero = CStrPtr{g_build_filename(g_get_user_data_dir(), + "file-manager/actions/level-zero.directory", nullptr)}; + if(g_file_test(level_zero.get(), G_FILE_TEST_IS_REGULAR)) { + GKeyFile* kf = g_key_file_new(); + if(g_key_file_load_from_file(kf, level_zero.get(), G_KEY_FILE_NONE, nullptr)) { + auto itemsList = CStrArrayPtr{g_key_file_get_string_list(kf, + "Desktop Entry", + "ItemsList", nullptr, nullptr)}; + if(itemsList) { + for(uint i = 0; i < g_strv_length(itemsList.get()); ++i) { + CStrPtr desktop_file_name{g_strconcat(itemsList.get()[i], ".desktop", nullptr)}; + auto desktop_file = CStrPtr{g_build_filename(g_get_user_data_dir(), + "file-manager/actions", + desktop_file_name.get(), nullptr)}; + GKeyFile* desktop_file_key = g_key_file_new(); + if(g_key_file_load_from_file(desktop_file_key, desktop_file.get(), G_KEY_FILE_NONE, nullptr)) { + auto actionName = CStrPtr{g_key_file_get_string(desktop_file_key, + "Desktop Entry", + "Name", NULL)}; + if(actionName) { + itemNamesList << QString::fromUtf8(actionName.get()); + } + } + g_key_file_free(desktop_file_key); + } + } + } + g_key_file_free(kf); + } + } + if(!itemNamesList.isEmpty()) { + int first = itemNamesList.indexOf(QString::fromStdString(a->get_name())); + int second = itemNamesList.indexOf(QString::fromStdString(b->get_name())); + if(first > -1) { + if(second > -1) { + return (first < second); + } + else { + return true; // list items have priority + } + } + else if(second > -1) { + return false; + } + } + return (a->get_name().compare(b->get_name()) < 0); +} + +FileActionItemList FileActionItem::get_actions_for_files(const FileInfoList& files) { + if(!actions_loaded) { + load_all_actions(); + } + + // Iterate over all actions to establish association between parent menu + // and children actions, and to find out toplevel ones which are not + // attached to any parent menu + for(auto& item : all_actions) { + auto& action_obj = item.second; + // stdout.printf("id = %s\n", action_obj.id); + if(action_obj->type == FileActionType::MENU) { // this is a menu + auto menu = static_pointer_cast(action_obj); + // stdout.printf("menu: %s\n", menu.name); + // associate child items with menus + menu->cache_children(files, (const char**)menu->items_list.get()); + } + } + + // Output the menus + FileActionItemList items; + + for(auto& item : all_actions) { + auto& action_obj = item.second; + // only output toplevel items here + if(action_obj->has_parent == false) { // this is a toplevel item + auto item = FileActionItem::fromActionObject(action_obj, files); + if(item != nullptr) { + items.push_back(item); + } + } + } + + // cleanup temporary data cached during menu generation + for(auto& item : all_actions) { + auto& action_obj = item.second; + action_obj->has_parent = false; + if(action_obj->type == FileActionType::MENU) { + auto menu = static_pointer_cast(action_obj); + menu->cached_children.clear(); + } + } + + std::sort(items.begin(), items.end(), compare_items); + return items; +} + +} // namespace Fm diff --git a/src/customactions/fileaction.h b/src/customactions/fileaction.h new file mode 100644 index 0000000..4c2a8bc --- /dev/null +++ b/src/customactions/fileaction.h @@ -0,0 +1,156 @@ +#ifndef FILEACTION_H +#define FILEACTION_H + +#include +#include + +#include "../core/fileinfo.h" +#include "fileactioncondition.h" +#include "fileactionprofile.h" + +namespace Fm { + +enum class FileActionType { + NONE, + ACTION, + MENU +}; + + +enum FileActionTarget { + FILE_ACTION_TARGET_NONE, + FILE_ACTION_TARGET_CONTEXT = 1, + FILE_ACTION_TARGET_LOCATION = 1 << 1, + FILE_ACTION_TARGET_TOOLBAR = 1 << 2 +}; + + +class FileActionObject { +public: + explicit FileActionObject(); + + explicit FileActionObject(GKeyFile* kf); + + virtual ~FileActionObject(); + + void setId(const char* _id) { + id = CStrPtr{g_strdup(_id)}; + } + + static bool is_plural_exec(const char* exec); + + static std::string expand_str(const char* templ, const FileInfoList& files, bool for_display = false, std::shared_ptr first_file = nullptr); + + FileActionType type; + CStrPtr id; + CStrPtr name; + CStrPtr tooltip; + CStrPtr icon; + CStrPtr desc; + bool enabled; + bool hidden; + CStrPtr suggested_shortcut; + std::unique_ptr condition; + + // values cached during menu generation + bool has_parent; +}; + + +class FileAction: public FileActionObject { +public: + + FileAction(GKeyFile* kf); + + std::shared_ptr match(const FileInfoList& files) const; + + int target; // bitwise or of FileActionTarget + CStrPtr toolbar_label; + + // FIXME: currently we don't support dynamic profiles + std::vector> profiles; +}; + + +class FileActionMenu : public FileActionObject { +public: + + FileActionMenu(GKeyFile* kf); + + bool match(const FileInfoList &files) const; + + // called during menu generation + void cache_children(const FileInfoList &files, const char** items_list); + + CStrArrayPtr items_list; + + // values cached during menu generation + std::vector> cached_children; +}; + + +class FileActionItem { +public: + + static std::shared_ptr fromActionObject(std::shared_ptr action_obj, const FileInfoList &files); + + FileActionItem(std::shared_ptr _action, std::shared_ptr _profile, const FileInfoList& files); + + FileActionItem(std::shared_ptr menu, const FileInfoList& files); + + FileActionItem(std::shared_ptr _action, const FileInfoList& files); + + const std::string& get_name() const { + return name; + } + + const std::string& get_desc() const { + return desc; + } + + const std::string& get_icon() const { + return icon; + } + + const char* get_id() const { + return action->id.get(); + } + + FileActionTarget get_target() const { + if(action->type == FileActionType::ACTION) { + return FileActionTarget(static_cast(action.get())->target); + } + return FILE_ACTION_TARGET_CONTEXT; + } + + bool is_menu() const { + return (action->type == FileActionType::MENU); + } + + bool is_action() const { + return (action->type == FileActionType::ACTION); + } + + bool launch(GAppLaunchContext *ctx, const FileInfoList &files, CStrPtr &output) const; + + const std::vector>& get_sub_items() const { + return children; + } + + static bool compare_items(std::shared_ptr a, std::shared_ptr b); + static std::vector> get_actions_for_files(const FileInfoList& files); + + std::string name; + std::string desc; + std::string icon; + std::shared_ptr action; + std::shared_ptr profile; // only used by action item + std::vector> children; // only used by menu +}; + +typedef std::vector> FileActionItemList; + +} // namespace Fm + + +#endif // FILEACTION_H diff --git a/src/customactions/fileactioncondition.cpp b/src/customactions/fileactioncondition.cpp new file mode 100644 index 0000000..22d5fa5 --- /dev/null +++ b/src/customactions/fileactioncondition.cpp @@ -0,0 +1,508 @@ +#include "fileactioncondition.h" +#include "fileaction.h" +#include + + +using namespace std; + +namespace Fm { + +FileActionCondition::FileActionCondition(GKeyFile *kf, const char* group) { + only_show_in = CStrArrayPtr{g_key_file_get_string_list(kf, group, "OnlyShowIn", nullptr, nullptr)}; + not_show_in = CStrArrayPtr{g_key_file_get_string_list(kf, group, "NotShowIn", nullptr, nullptr)}; + try_exec = CStrPtr{g_key_file_get_string(kf, group, "TryExec", nullptr)}; + show_if_registered = CStrPtr{g_key_file_get_string(kf, group, "ShowIfRegistered", nullptr)}; + show_if_true = CStrPtr{g_key_file_get_string(kf, group, "ShowIfTrue", nullptr)}; + show_if_running = CStrPtr{g_key_file_get_string(kf, group, "ShowIfRunning", nullptr)}; + mime_types = CStrArrayPtr{g_key_file_get_string_list(kf, group, "MimeTypes", nullptr, nullptr)}; + base_names = CStrArrayPtr{g_key_file_get_string_list(kf, group, "Basenames", nullptr, nullptr)}; + match_case = g_key_file_get_boolean(kf, group, "Matchcase", nullptr); + + CStrPtr selection_count_str{g_key_file_get_string(kf, group, "SelectionCount", nullptr)}; + if(selection_count_str != nullptr) { + switch(selection_count_str[0]) { + case '<': + case '>': + case '=': + selection_count_cmp = selection_count_str[0]; + selection_count = atoi(selection_count_str.get() + 1); + break; + default: + selection_count_cmp = '>'; + selection_count = 0; + break; + } + } + else { + selection_count_cmp = '>'; + selection_count = 0; + } + + schemes = CStrArrayPtr{g_key_file_get_string_list(kf, group, "Schemes", nullptr, nullptr)}; + folders = CStrArrayPtr{g_key_file_get_string_list(kf, group, "Folders", nullptr, nullptr)}; + auto caps = CStrArrayPtr{g_key_file_get_string_list(kf, group, "Capabilities", nullptr, nullptr)}; + + // FIXME: implement Capabilities support + +} + +bool FileActionCondition::match_try_exec(const FileInfoList& files) { + if(try_exec != nullptr) { + // stdout.printf(" TryExec: %s\n", try_exec); + CStrPtr exec_path{g_find_program_in_path(FileActionObject::expand_str(try_exec.get(), files).c_str())}; + if(!g_file_test(exec_path.get(), G_FILE_TEST_IS_EXECUTABLE)) { + return false; + } + } + return true; +} + +bool FileActionCondition::match_show_if_registered(const FileInfoList& files) { + if(show_if_registered != nullptr) { + // stdout.printf(" ShowIfRegistered: %s\n", show_if_registered); + auto service = FileActionObject::expand_str(show_if_registered.get(), files); + // References: + // http://people.freedesktop.org/~david/eggdbus-20091014/eggdbus-interface-org.freedesktop.DBus.html#eggdbus-method-org.freedesktop.DBus.NameHasOwner + // glib source code: gio/tests/gdbus-names.c + auto con = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr); + auto result = g_dbus_connection_call_sync(con, + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + "NameHasOwner", + g_variant_new("(s)", service.c_str()), + g_variant_type_new("(b)"), + G_DBUS_CALL_FLAGS_NONE, + -1, nullptr, nullptr); + bool name_has_owner; + g_variant_get(result, "(b)", &name_has_owner); + g_variant_unref(result); + // stdout.printf("check if service: %s is in use: %d\n", service, (int)name_has_owner); + if(!name_has_owner) { + return false; + } + } + return true; +} + +bool FileActionCondition::match_show_if_true(const FileInfoList& files) { + if(show_if_true != nullptr) { + auto cmd = FileActionObject::expand_str(show_if_true.get(), files); + int exit_status; + // FIXME: Process.spawn cannot handle shell commands. Use Posix.system() instead. + //if(!Process.spawn_command_line_sync(cmd, nullptr, nullptr, out exit_status) + // || exit_status != 0) + // return false; + exit_status = system(cmd.c_str()); + if(exit_status != 0) { + return false; + } + } + return true; +} + +bool FileActionCondition::match_show_if_running(const FileInfoList& files) { + if(show_if_running != nullptr) { + auto process_name = FileActionObject::expand_str(show_if_running.get(), files); + CStrPtr pgrep{g_find_program_in_path("pgrep")}; + bool running = false; + // pgrep is not fully portable, but we don't have better options here + if(pgrep != nullptr) { + int exit_status; + // cmd = "$pgrep -x '$process_name'" + string cmd = pgrep.get(); + cmd += " -x \'"; + cmd += process_name; + cmd += "\'"; + if(g_spawn_command_line_sync(cmd.c_str(), nullptr, nullptr, &exit_status, nullptr)) { + if(exit_status == 0) { + running = true; + } + } + } + if(!running) { + return false; + } + } + return true; +} + +bool FileActionCondition::match_mime_type(const FileInfoList& files, const char* type, bool negated) { + // stdout.printf("match_mime_type: %s, neg: %d\n", type, (int)negated); + + if(strcmp(type, "all/all") == 0 || strcmp(type, "*") == 0) { + return negated ? false : true; + } + else if(strcmp(type, "all/allfiles") == 0) { + // see if all fileinfos are files + if(negated) { // all fileinfos should not be files + for(auto& fi: files) { + if(!fi->isDir()) { // at least 1 of the fileinfos is a file. + return false; + } + } + } + else { // all fileinfos should be files + for(auto& fi: files) { + if(fi->isDir()) { // at least 1 of the fileinfos is a file. + return false; + } + } + } + } + else if(g_str_has_suffix(type, "/*")) { + // check if all are subtypes of allowed_type + string prefix{type}; + prefix.erase(prefix.length() - 1); // remove the last char + if(negated) { // all files should not have the prefix + for(auto& fi: files) { + if(g_str_has_prefix(fi->mimeType()->name(), prefix.c_str())) { + return false; + } + } + } + else { // all files should have the prefix + for(auto& fi: files) { + if(!g_str_has_prefix(fi->mimeType()->name(), prefix.c_str())) { + return false; + } + } + } + } + else { + if(negated) { // all files should not be of the type + for(auto& fi: files) { + if(strcmp(fi->mimeType()->name(),type) == 0) { + // if(ContentType.is_a(type, fi.get_mime_type().get_type())) { + return false; + } + } + } + else { // all files should be of the type + for(auto& fi: files) { + // stdout.printf("get_type: %s, type: %s\n", fi.get_mime_type().get_type(), type); + if(strcmp(fi->mimeType()->name(),type) != 0) { + // if(!ContentType.is_a(type, fi.get_mime_type().get_type())) { + return false; + } + } + } + } + return true; +} + +bool FileActionCondition::match_mime_types(const FileInfoList& files) { + if(mime_types != nullptr) { + bool allowed = false; + // FIXME: this is inefficient, but easier to implement + // check if all of the mime_types are allowed + for(auto mime_type = mime_types.get(); *mime_type; ++mime_type) { + const char* allowed_type = *mime_type; + const char* type; + bool negated; + if(allowed_type[0] == '!') { + type = allowed_type + 1; + negated = true; + } + else { + type = allowed_type; + negated = false; + } + + if(negated) { // negated mime_type rules are ANDed + bool type_is_allowed = match_mime_type(files, type, negated); + if(!type_is_allowed) { // so any mismatch is not allowed + return false; + } + } + else { // other mime_type rules are ORed + // matching any one of the mime_type is enough + if(!allowed) { // if no rule is matched yet + allowed = match_mime_type(files, type, false); + } + } + } + return allowed; + } + return true; +} + +bool FileActionCondition::match_base_name(const FileInfoList& files, const char* base_name, bool negated) const { + // see if all files has the base_name + // FIXME: this is inefficient, some optimization is needed later + GPatternSpec* pattern; + if(match_case) { + pattern = g_pattern_spec_new(base_name); + } + else { + CStrPtr case_fold{g_utf8_casefold(base_name, -1)}; + pattern = g_pattern_spec_new(case_fold.get()); // FIXME: is this correct? + } + for(auto& fi: files) { + const char* name = fi->name().c_str(); + if(match_case) { + if(g_pattern_match_string(pattern, name)) { + // at least 1 file has the base_name + if(negated) { + return false; + } + } + else { + // at least 1 file does not has the scheme + if(!negated) { + return false; + } + } + } + else { + CStrPtr case_fold{g_utf8_casefold(name, -1)}; + if(g_pattern_match_string(pattern, case_fold.get())) { + // at least 1 file has the base_name + if(negated) { + return false; + } + } + else { + // at least 1 file does not has the scheme + if(!negated) { + return false; + } + } + } + } + return true; +} + +bool FileActionCondition::match_base_names(const FileInfoList& files) { + if(base_names != nullptr) { + bool allowed = false; + // FIXME: this is inefficient, but easier to implement + // check if all of the base_names are allowed + for(auto it = base_names.get(); *it; ++it) { + auto allowed_name = *it; + const char* name; + bool negated; + if(allowed_name[0] == '!') { + name = allowed_name + 1; + negated = true; + } + else { + name = allowed_name; + negated = false; + } + + if(negated) { // negated base_name rules are ANDed + bool name_is_allowed = match_base_name(files, name, negated); + if(!name_is_allowed) { // so any mismatch is not allowed + return false; + } + } + else { // other base_name rules are ORed + // matching any one of the base_name is enough + if(!allowed) { // if no rule is matched yet + allowed = match_base_name(files, name, false); + } + } + } + return allowed; + } + return true; +} + +bool FileActionCondition::match_scheme(const FileInfoList& files, const char* scheme, bool negated) { + // FIXME: this is inefficient, some optimization is needed later + // see if all files has the scheme + for(auto& fi: files) { + if(fi->path().hasUriScheme(scheme)) { + // at least 1 file has the scheme + if(negated) { + return false; + } + } + else { + // at least 1 file does not has the scheme + if(!negated) { + return false; + } + } + } + return true; +} + +bool FileActionCondition::match_schemes(const FileInfoList& files) { + if(schemes != nullptr) { + bool allowed = false; + // FIXME: this is inefficient, but easier to implement + // check if all of the schemes are allowed + for(auto it = schemes.get(); *it; ++it) { + auto allowed_scheme = *it; + const char* scheme; + bool negated; + if(allowed_scheme[0] == '!') { + scheme = allowed_scheme + 1; + negated = true; + } + else { + scheme = allowed_scheme; + negated = false; + } + + if(negated) { // negated scheme rules are ANDed + bool scheme_is_allowed = match_scheme(files, scheme, negated); + if(!scheme_is_allowed) { // so any mismatch is not allowed + return false; + } + } + else { // other scheme rules are ORed + // matching any one of the scheme is enough + if(!allowed) { // if no rule is matched yet + allowed = match_scheme(files, scheme, false); + } + } + } + return allowed; + } + return true; +} + +bool FileActionCondition::match_folder(const FileInfoList& files, const char* folder, bool negated) { + // trailing /* should always be implied. + // FIXME: this is inefficient, some optimization is needed later + GPatternSpec* pattern; + if(g_str_has_suffix(folder, "/*")) { + pattern = g_pattern_spec_new(folder); + } + else { + auto pat_str = g_str_has_suffix(folder, "/") ? string(folder) + "*" // be tolerant + : string(folder) + "/*"; + pattern = g_pattern_spec_new(pat_str.c_str()); + } + for(auto& fi: files) { + auto dirname = fi->isDir() ? fi->path().toString() // also match "folder" itself + : fi->dirPath().toString(); + // Since the pattern ends with "/*", if the directory path is equal to "folder", + // it should end with "/" to be found as a match. Adding "/" is always harmless. + auto path_str = string(dirname.get()) + "/"; + if(g_pattern_match_string(pattern, path_str.c_str())) { // at least 1 file is in the folder + if(negated) { + return false; + } + } + else { + if(!negated) { + return false; + } + } + } + return true; +} + +bool FileActionCondition::match_folders(const FileInfoList& files) { + if(folders != nullptr) { + bool allowed = false; + // FIXME: this is inefficient, but easier to implement + // check if all of the schemes are allowed + for(auto it = folders.get(); *it; ++it) { + auto allowed_folder = *it; + const char* folder; + bool negated; + if(allowed_folder[0] == '!') { + folder = allowed_folder + 1; + negated = true; + } + else { + folder = allowed_folder; + negated = false; + } + + if(negated) { // negated folder rules are ANDed + bool folder_is_allowed = match_folder(files, folder, negated); + if(!folder_is_allowed) { // so any mismatch is not allowed + return false; + } + } + else { // other folder rules are ORed + // matching any one of the folder is enough + if(!allowed) { // if no rule is matched yet + allowed = match_folder(files, folder, false); + } + } + } + return allowed; + } + return true; +} + +bool FileActionCondition::match_selection_count(const FileInfoList& files) const { + const int n_files = files.size(); + switch(selection_count_cmp) { + case '<': + if(n_files >= selection_count) { + return false; + } + break; + case '=': + if(n_files != selection_count) { + return false; + } + break; + case '>': + if(n_files <= selection_count) { + return false; + } + break; + } + return true; +} + +bool FileActionCondition::match_capabilities(const FileInfoList& /*files*/) { + // TODO + return true; +} + +bool FileActionCondition::match(const FileInfoList& files) { + // all of the condition are combined with AND + // So, if any one of the conditions is not matched, we quit. + + // TODO: OnlyShowIn, NotShowIn + if(!match_try_exec(files)) { + return false; + } + + if(!match_mime_types(files)) { + return false; + } + if(!match_base_names(files)) { + return false; + } + if(!match_selection_count(files)) { + return false; + } + if(!match_schemes(files)) { + return false; + } + if(!match_folders(files)) { + return false; + } + // TODO: Capabilities + // currently, due to limitations of Fm.FileInfo, this cannot + // be implemanted correctly. + if(!match_capabilities(files)) { + return false; + } + + if(!match_show_if_registered(files)) { + return false; + } + if(!match_show_if_true(files)) { + return false; + } + if(!match_show_if_running(files)) { + return false; + } + + return true; +} + + +} // namespace Fm diff --git a/src/customactions/fileactioncondition.h b/src/customactions/fileactioncondition.h new file mode 100644 index 0000000..5a393f1 --- /dev/null +++ b/src/customactions/fileactioncondition.h @@ -0,0 +1,123 @@ +#ifndef FILEACTIONCONDITION_H +#define FILEACTIONCONDITION_H + +#include +#include "../core/gioptrs.h" +#include "../core/fileinfo.h" + +namespace Fm { + +// FIXME: we can use getgroups() to get groups of current process +// then, call stat() and stat.st_gid to handle capabilities +// in this way, we don't have to call euidaccess + +enum class FileActionCapability { + OWNER = 0, + READABLE = 1 << 1, + WRITABLE = 1 << 2, + EXECUTABLE = 1 << 3, + LOCAL = 1 << 4 +}; + + +class FileActionCondition { +public: + explicit FileActionCondition(GKeyFile* kf, const char* group); + +#if 0 + bool match_base_name_(const FileInfoList& files, const char* allowed_base_name) { + // all files should match the base_name pattern. + bool allowed = true; + if(allowed_base_name.index_of_char('*') >= 0) { + string allowed_base_name_ci; + if(!match_case) { + allowed_base_name_ci = allowed_base_name.casefold(); // FIXME: is this ok? + allowed_base_name = allowed_base_name_ci; + } + var pattern = new PatternSpec(allowed_base_name); + foreach(unowned FileInfo fi in files) { + unowned string name = fi.get_name(); + if(match_case) { + if(!pattern.match_string(name)) { + allowed = false; + break; + } + } + else { + if(!pattern.match_string(name.casefold())) { + allowed = false; + break; + } + } + } + } + else { + foreach(unowned FileInfo fi in files) { + unowned string name = fi.get_name(); + if(match_case) { + if(allowed_base_name != name) { + allowed = false; + break; + } + } + else { + if(allowed_base_name.collate(name) != 0) { + allowed = false; + break; + } + } + } + } + return allowed; + } +#endif + + bool match_try_exec(const FileInfoList& files); + + bool match_show_if_registered(const FileInfoList& files); + + bool match_show_if_true(const FileInfoList& files); + + bool match_show_if_running(const FileInfoList& files); + + static bool match_mime_type(const FileInfoList& files, const char* type, bool negated); + + bool match_mime_types(const FileInfoList& files); + + bool match_base_name(const FileInfoList& files, const char* base_name, bool negated) const; + + bool match_base_names(const FileInfoList& files); + + static bool match_scheme(const FileInfoList& files, const char* scheme, bool negated); + + bool match_schemes(const FileInfoList& files); + + static bool match_folder(const FileInfoList& files, const char* folder, bool negated); + + bool match_folders(const FileInfoList& files); + + bool match_selection_count(const FileInfoList &files) const; + + bool match_capabilities(const FileInfoList& files); + + bool match(const FileInfoList& files); + + CStrArrayPtr only_show_in; + CStrArrayPtr not_show_in; + CStrPtr try_exec; + CStrPtr show_if_registered; + CStrPtr show_if_true; + CStrPtr show_if_running; + CStrArrayPtr mime_types; + CStrArrayPtr base_names; + bool match_case; + char selection_count_cmp; + int selection_count; + CStrArrayPtr schemes; + CStrArrayPtr folders; + FileActionCapability capabilities; +}; + +} + +#endif // FILEACTIONCONDITION_H diff --git a/src/customactions/fileactionprofile.cpp b/src/customactions/fileactionprofile.cpp new file mode 100644 index 0000000..0150b1c --- /dev/null +++ b/src/customactions/fileactionprofile.cpp @@ -0,0 +1,124 @@ +#include "fileactionprofile.h" +#include "fileaction.h" +#include + +using namespace std; + +namespace Fm { + +FileActionProfile::FileActionProfile(GKeyFile *kf, const char* profile_name) { + id = profile_name; + std::string group_name = "X-Action-Profile " + id; + name = CStrPtr{g_key_file_get_string(kf, group_name.c_str(), "Name", nullptr)}; + exec = CStrPtr{g_key_file_get_string(kf, group_name.c_str(), "Exec", nullptr)}; + // stdout.printf("id: %s, Exec: %s\n", id, exec); + + path = CStrPtr{g_key_file_get_string(kf, group_name.c_str(), "Path", nullptr)}; + auto s = CStrPtr{g_key_file_get_string(kf, group_name.c_str(), "ExecutionMode", nullptr)}; + if(s) { + if(strcmp(s.get(), "Normal") == 0) { + exec_mode = FileActionExecMode::NORMAL; + } + else if(strcmp(s.get(), "Terminal") == 0) { + exec_mode = FileActionExecMode::TERMINAL; + } + else if(strcmp(s.get(), "Embedded") == 0) { + exec_mode = FileActionExecMode::EMBEDDED; + } + else if(strcmp(s.get(), "DisplayOutput") == 0) { + exec_mode = FileActionExecMode::DISPLAY_OUTPUT; + } + else { + exec_mode = FileActionExecMode::NORMAL; + } + } + else { + exec_mode = FileActionExecMode::NORMAL; + } + + startup_notify = g_key_file_get_boolean(kf, group_name.c_str(), "StartupNotify", nullptr); + startup_wm_class = CStrPtr{g_key_file_get_string(kf, group_name.c_str(), "StartupWMClass", nullptr)}; + exec_as = CStrPtr{g_key_file_get_string(kf, group_name.c_str(), "ExecuteAs", nullptr)}; + + condition = make_shared(kf, group_name.c_str()); +} + + +bool FileActionProfile::launch_once(GAppLaunchContext* /*ctx*/, std::shared_ptr first_file, const FileInfoList& files, CStrPtr& output) { + if(exec == nullptr) { + return false; + } + auto exec_cmd = FileActionObject::expand_str(exec.get(), files, false, first_file); + bool ret = false; + if(exec_mode == FileActionExecMode::DISPLAY_OUTPUT) { + int exit_status; + char* output_buf = nullptr; + ret = g_spawn_command_line_sync(exec_cmd.c_str(), &output_buf, nullptr, &exit_status, nullptr); + if(ret) { + ret = (exit_status == 0); + } + output = CStrPtr{output_buf}; + } + else { + /* + AppInfoCreateFlags flags = AppInfoCreateFlags.NONE; + if(startup_notify) + flags |= AppInfoCreateFlags.SUPPORTS_STARTUP_NOTIFICATION; + if(exec_mode == FileActionExecMode::TERMINAL || + exec_mode == FileActionExecMode::EMBEDDED) + flags |= AppInfoCreateFlags.NEEDS_TERMINAL; + GLib.AppInfo app = Fm.AppInfo.create_from_commandline(exec, nullptr, flags); + stdout.printf("Execute command line: %s\n\n", exec); + ret = app.launch(nullptr, ctx); + */ + + // NOTE: we cannot use GAppInfo here since GAppInfo does + // command line parsing which involving %u, %f, and other + // code defined in desktop entry spec. + // This may conflict with DES EMA parameters. + // FIXME: so how to handle this cleaner? + // Maybe we should leave all %% alone and don't translate + // them to %. Then GAppInfo will translate them to %, not + // codes specified in DES. + ret = g_spawn_command_line_async(exec_cmd.c_str(), nullptr); + } + return ret; +} + + +bool FileActionProfile::launch(GAppLaunchContext* ctx, const FileInfoList& files, CStrPtr& output) { + bool plural_form = FileActionObject::is_plural_exec(exec.get()); + bool ret; + if(plural_form) { // plural form command, handle all files at a time + ret = launch_once(ctx, files.front(), files, output); + } + else { // singular form command, run once for each file + GString* all_output = g_string_sized_new(1024); + bool show_output = false; + for(auto& fi: files) { + CStrPtr one_output; + launch_once(ctx, fi, files, one_output); + if(one_output) { + show_output = true; + // FIXME: how to handle multiple output std::strings properly? + g_string_append(all_output, one_output.get()); + g_string_append(all_output, "\n"); + } + } + if(show_output) { + output = CStrPtr{g_string_free(all_output, false)}; + } + else { + g_string_free(all_output, true); + } + ret = true; + } + return ret; +} + +bool FileActionProfile::match(FileInfoList files) { + // stdout.printf(" match profile: %s\n", id); + return condition->match(files); +} + +} diff --git a/src/customactions/fileactionprofile.h b/src/customactions/fileactionprofile.h new file mode 100644 index 0000000..18b959f --- /dev/null +++ b/src/customactions/fileactionprofile.h @@ -0,0 +1,45 @@ +#ifndef FILEACTIONPROFILE_H +#define FILEACTIONPROFILE_H + + +#include +#include +#include + +#include "../core/fileinfo.h" +#include "fileactioncondition.h" + +namespace Fm { + +enum class FileActionExecMode { + NORMAL, + TERMINAL, + EMBEDDED, + DISPLAY_OUTPUT +}; + +class FileActionProfile { +public: + explicit FileActionProfile(GKeyFile* kf, const char* profile_name); + + bool launch_once(GAppLaunchContext* ctx, std::shared_ptr first_file, const FileInfoList& files, CStrPtr& output); + + bool launch(GAppLaunchContext* ctx, const FileInfoList& files, CStrPtr& output); + + bool match(FileInfoList files); + + std::string id; + CStrPtr name; + CStrPtr exec; + CStrPtr path; + FileActionExecMode exec_mode; + bool startup_notify; + CStrPtr startup_wm_class; + CStrPtr exec_as; + + std::shared_ptr condition; +}; + +} // namespace Fm + +#endif // FILEACTIONPROFILE_H diff --git a/src/dirtreemodel.cpp b/src/dirtreemodel.cpp new file mode 100644 index 0000000..c6d030f --- /dev/null +++ b/src/dirtreemodel.cpp @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "dirtreemodel.h" +#include "dirtreemodelitem.h" +#include +#include "core/fileinfojob.h" + +namespace Fm { + +DirTreeModel::DirTreeModel(QObject* parent): + QAbstractItemModel(parent), + showHidden_(false) { +} + +DirTreeModel::~DirTreeModel() { +} + +void DirTreeModel::addRoots(Fm::FilePathList rootPaths) { + auto job = new Fm::FileInfoJob{std::move(rootPaths)}; + job->setAutoDelete(true); + connect(job, &Fm::FileInfoJob::finished, this, &DirTreeModel::onFileInfoJobFinished, Qt::BlockingQueuedConnection); + job->runAsync(); +} + +void DirTreeModel::onFileInfoJobFinished() { + auto job = static_cast(sender()); + for(auto file: job->files()) { + addRoot(std::move(file)); + } +} + +// QAbstractItemModel implementation + +Qt::ItemFlags DirTreeModel::flags(const QModelIndex& index) const { + DirTreeModelItem* item = itemFromIndex(index); + if(item && item->isPlaceHolder()) { + return Qt::ItemIsEnabled; + } + return QAbstractItemModel::flags(index); +} + +QVariant DirTreeModel::data(const QModelIndex& index, int role) const { + if(!index.isValid() || index.column() > 1) { + return QVariant(); + } + DirTreeModelItem* item = itemFromIndex(index); + if(item) { + auto info = item->fileInfo_; + switch(role) { + case Qt::ToolTipRole: + return QVariant(item->displayName_); + case Qt::DisplayRole: + return QVariant(item->displayName_); + case Qt::DecorationRole: + return QVariant(item->icon_); + case FileInfoRole: { + QVariant v; + v.setValue(info); + return v; + } + } + } + return QVariant(); +} + +int DirTreeModel::columnCount(const QModelIndex& /*parent*/) const { + return 1; +} + +int DirTreeModel::rowCount(const QModelIndex& parent) const { + if(!parent.isValid()) { + return rootItems_.size(); + } + DirTreeModelItem* item = itemFromIndex(parent); + if(item) { + return item->children_.size(); + } + return 0; +} + +QModelIndex DirTreeModel::parent(const QModelIndex& child) const { + DirTreeModelItem* item = itemFromIndex(child); + if(item && item->parent_) { + item = item->parent_; // go to parent item + if(item) { + const auto& items = item->parent_ ? item->parent_->children_ : rootItems_; + auto it = std::find(items.cbegin(), items.cend(), item); + if(it != items.cend()) { + int row = it - items.cbegin(); + return createIndex(row, 0, (void*)item); + } + } + } + return QModelIndex(); +} + +QModelIndex DirTreeModel::index(int row, int column, const QModelIndex& parent) const { + if(row >= 0 && column >= 0 && column == 0) { + if(!parent.isValid()) { // root items + if(static_cast(row) < rootItems_.size()) { + const DirTreeModelItem* item = rootItems_.at(row); + return createIndex(row, column, (void*)item); + } + } + else { // child items + DirTreeModelItem* parentItem = itemFromIndex(parent); + if(static_cast(row) < parentItem->children_.size()) { + const DirTreeModelItem* item = parentItem->children_.at(row); + return createIndex(row, column, (void*)item); + } + } + } + return QModelIndex(); // invalid index +} + +bool DirTreeModel::hasChildren(const QModelIndex& parent) const { + DirTreeModelItem* item = itemFromIndex(parent); + return item ? !item->isPlaceHolder() : true; +} + +QModelIndex DirTreeModel::indexFromItem(DirTreeModelItem* item) const { + Q_ASSERT(item); + const auto& items = item->parent_ ? item->parent_->children_ : rootItems_; + auto it = std::find(items.cbegin(), items.cend(), item); + if(it != items.cend()) { + int row = it - items.cbegin(); + return createIndex(row, 0, (void*)item); + } + return QModelIndex(); +} + +// public APIs +QModelIndex DirTreeModel::addRoot(std::shared_ptr root) { + DirTreeModelItem* item = new DirTreeModelItem(std::move(root), this); + int row = rootItems_.size(); + beginInsertRows(QModelIndex(), row, row); + rootItems_.push_back(item); + // add_place_holder_child_item(model, item_l, nullptr, FALSE); + endInsertRows(); + return createIndex(row, 0, (void*)item); +} + +DirTreeModelItem* DirTreeModel::itemFromIndex(const QModelIndex& index) const { + return reinterpret_cast(index.internalPointer()); +} + +QModelIndex DirTreeModel::indexFromPath(const Fm::FilePath &path) const { + DirTreeModelItem* item = itemFromPath(path); + return item ? item->index() : QModelIndex(); +} + +DirTreeModelItem* DirTreeModel::itemFromPath(const Fm::FilePath &path) const { + for(DirTreeModelItem* const item : qAsConst(rootItems_)) { + if(item->fileInfo_ && path == item->fileInfo_->path()) { + return item; + } + else { + DirTreeModelItem* child = item->childFromPath(path, true); + if(child) { + return child; + } + } + } + return nullptr; +} + + +void DirTreeModel::loadRow(const QModelIndex& index) { + DirTreeModelItem* item = itemFromIndex(index); + Q_ASSERT(item); + if(item && !item->isPlaceHolder()) { + item->loadFolder(); + } +} + +void DirTreeModel::unloadRow(const QModelIndex& index) { + DirTreeModelItem* item = itemFromIndex(index); + if(item && !item->isPlaceHolder()) { + item->unloadFolder(); + } +} + +bool DirTreeModel::isLoaded(const QModelIndex& index) { + DirTreeModelItem* item = itemFromIndex(index); + return item ? item->loaded_ : false; +} + +QIcon DirTreeModel::icon(const QModelIndex& index) { + DirTreeModelItem* item = itemFromIndex(index); + return item ? item->icon_ : QIcon(); +} + +std::shared_ptr DirTreeModel::fileInfo(const QModelIndex& index) { + DirTreeModelItem* item = itemFromIndex(index); + return item ? item->fileInfo_ : nullptr; +} + +Fm::FilePath DirTreeModel::filePath(const QModelIndex& index) { + DirTreeModelItem* item = itemFromIndex(index); + return (item && item->fileInfo_) ? item->fileInfo_->path() : Fm::FilePath{}; +} + +QString DirTreeModel::dispName(const QModelIndex& index) { + DirTreeModelItem* item = itemFromIndex(index); + return item ? item->displayName_ : QString(); +} + +void DirTreeModel::setShowHidden(bool show_hidden) { + showHidden_ = show_hidden; + for(DirTreeModelItem* const item : qAsConst(rootItems_)) { + item->setShowHidden(show_hidden); + } +} + + +} // namespace Fm diff --git a/src/dirtreemodel.h b/src/dirtreemodel.h new file mode 100644 index 0000000..5c851d0 --- /dev/null +++ b/src/dirtreemodel.h @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_DIRTREEMODEL_H +#define FM_DIRTREEMODEL_H + +#include "libfmqtglobals.h" +#include +#include +#include +#include +#include +#include + +#include "core/fileinfo.h" +#include "core/filepath.h" + +namespace Fm { + +class DirTreeModelItem; +class DirTreeView; + +class LIBFM_QT_API DirTreeModel : public QAbstractItemModel { + Q_OBJECT + +public: + friend class DirTreeModelItem; // allow direct access of private members in DirTreeModelItem + friend class DirTreeView; // allow direct access of private members in DirTreeView + + enum Role { + FileInfoRole = Qt::UserRole + }; + + explicit DirTreeModel(QObject* parent); + ~DirTreeModel(); + + void addRoots(Fm::FilePathList rootPaths); + + void loadRow(const QModelIndex& index); + void unloadRow(const QModelIndex& index); + + bool isLoaded(const QModelIndex& index); + QIcon icon(const QModelIndex& index); + std::shared_ptr fileInfo(const QModelIndex& index); + Fm::FilePath filePath(const QModelIndex& index); + QString dispName(const QModelIndex& index); + + void setShowHidden(bool show_hidden); + bool showHidden() const { + return showHidden_; + } + + QModelIndex indexFromPath(const Fm::FilePath& path) const; + + virtual Qt::ItemFlags flags(const QModelIndex& index) const; + virtual QVariant data(const QModelIndex& index, int role) const; + virtual int columnCount(const QModelIndex& parent) const; + virtual int rowCount(const QModelIndex& parent) const; + virtual QModelIndex parent(const QModelIndex& child) const; + virtual QModelIndex index(int row, int column, const QModelIndex& parent) const; + virtual bool hasChildren(const QModelIndex& parent = QModelIndex()) const; + +Q_SIGNALS: + void rowLoaded(const QModelIndex& index); + +private Q_SLOTS: + void onFileInfoJobFinished(); + +private: + QModelIndex addRoot(std::shared_ptr root); + + DirTreeModelItem* itemFromPath(const Fm::FilePath& path) const; + DirTreeModelItem* itemFromIndex(const QModelIndex& index) const; + QModelIndex indexFromItem(DirTreeModelItem* item) const; + +private: + bool showHidden_; + std::vector rootItems_; +}; + +} + +#endif // FM_DIRTREEMODEL_H diff --git a/src/dirtreemodelitem.cpp b/src/dirtreemodelitem.cpp new file mode 100644 index 0000000..57ecf41 --- /dev/null +++ b/src/dirtreemodelitem.cpp @@ -0,0 +1,416 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "dirtreemodelitem.h" +#include "dirtreemodel.h" +#include + +namespace Fm { + +DirTreeModelItem::DirTreeModelItem(): + fileInfo_(nullptr), + folder_(nullptr), + expanded_(false), + loaded_(false), + parent_(nullptr), + placeHolderChild_(nullptr), + model_(nullptr), + queuedForDeletion_(false) { +} + +DirTreeModelItem::DirTreeModelItem(std::shared_ptr info, DirTreeModel* model, DirTreeModelItem* parent): + fileInfo_{std::move(info)}, + expanded_(false), + loaded_(false), + parent_(parent), + placeHolderChild_(nullptr), + model_(model), + queuedForDeletion_(false) { + + if(fileInfo_) { + displayName_ = fileInfo_->displayName(); + icon_ = fileInfo_->icon()->qicon(); + addPlaceHolderChild(); + } +} + +DirTreeModelItem::~DirTreeModelItem() { + freeFolder(); + // delete child items if needed + if(!children_.empty()) { + for(DirTreeModelItem* const item : qAsConst(children_)) { + delete item; + } + } + if(!hiddenChildren_.empty()) { + for(DirTreeModelItem* const item : qAsConst(hiddenChildren_)) { + delete item; + } + } + /*if(queuedForDeletion_) + qDebug() << "queued deletion done";*/ +} + +void DirTreeModelItem::freeFolder() { + if(folder_) { + QObject::disconnect(onFolderFinishLoadingConn_); + QObject::disconnect(onFolderFilesAddedConn_); + QObject::disconnect(onFolderFilesRemovedConn_); + QObject::disconnect(onFolderFilesChangedConn_); + folder_.reset(); + } +} + +void DirTreeModelItem::addPlaceHolderChild() { + placeHolderChild_ = new DirTreeModelItem(); + placeHolderChild_->parent_ = this; + placeHolderChild_->model_ = model_; + placeHolderChild_->displayName_ = DirTreeModel::tr("Loading..."); + children_.push_back(placeHolderChild_); +} + +void DirTreeModelItem::loadFolder() { + if(!expanded_) { + /* dynamically load content of the folder. */ + folder_ = Fm::Folder::fromPath(fileInfo_->path()); + /* g_debug("fm_dir_tree_model_load_row()"); */ + /* associate the data with loaded handler */ + + onFolderFinishLoadingConn_ = QObject::connect(folder_.get(), &Fm::Folder::finishLoading, model_, [=]() { + onFolderFinishLoading(); + }); + onFolderFilesAddedConn_ = QObject::connect(folder_.get(), &Fm::Folder::filesAdded, model_, [=](Fm::FileInfoList files) { + onFolderFilesAdded(files); + }); + onFolderFilesRemovedConn_ = QObject::connect(folder_.get(), &Fm::Folder::filesRemoved, model_, [=](Fm::FileInfoList files) { + onFolderFilesRemoved(files); + }); + onFolderFilesChangedConn_ = QObject::connect(folder_.get(), &Fm::Folder::filesChanged, model_, [=](std::vector& changes) { + onFolderFilesChanged(changes); + }); + + /* set 'expanded' flag beforehand as callback may check it */ + expanded_ = true; + /* if the folder is already loaded, call "loaded" handler ourselves */ + if(folder_->isLoaded()) { // already loaded + insertFiles(folder_->files()); + onFolderFinishLoading(); + } + } +} + +void DirTreeModelItem::unloadFolder() { + if(expanded_) { /* do some cleanup */ + /* remove all children, and replace them with a dummy child + * item to keep expander in the tree view around. */ + + // delete all visible child items + model_->beginRemoveRows(index(), 0, children_.size() - 1); + if(!children_.empty()) { + for(DirTreeModelItem* const item : qAsConst(children_)) { + delete item; + } + children_.clear(); + } + model_->endRemoveRows(); + + // remove hidden children + if(!hiddenChildren_.empty()) { + for(DirTreeModelItem* const item : qAsConst(hiddenChildren_)) { + delete item; + } + hiddenChildren_.clear(); + } + + /* now, we have no child since all child items are removed. + * So we add a place holder child item to keep the expander around. */ + addPlaceHolderChild(); + /* deactivate folder since it will be reactivated on expand */ + freeFolder(); + expanded_ = false; + loaded_ = false; + } +} + +QModelIndex DirTreeModelItem::index() { + Q_ASSERT(model_); + return model_->indexFromItem(this); +} + +/* Add file info to parent node to proper position. */ +DirTreeModelItem* DirTreeModelItem::insertFile(std::shared_ptr fi) { + // qDebug() << "insertFileInfo: " << fm_file_info_get_disp_name(fi); + DirTreeModelItem* item = new DirTreeModelItem(std::move(fi), model_); + insertItem(item); + return item; +} + +/* Add file info to parent node to proper position. */ +void DirTreeModelItem::insertFiles(Fm::FileInfoList files) { + if(children_.size() == 1 && placeHolderChild_) { + // the list is empty, add them all at once and do sort + if(!model_->showHidden()) { // need to separate visible and hidden items + // insert hidden files into the "hiddenChildren_" list and remove them from "files" list + // WARNING: "std::remove_if" shouldn't be used to work on the "removed" items because, as + // docs say, the elements between the returned and the end iterators are in an unspecified + // state and, as far as I (@tsujan) have tested, some of them announce themselves as null. + for(auto it = files.begin(); it != files.end();) { + auto file = *it; + if(file->isHidden()) { + hiddenChildren_.push_back(new DirTreeModelItem{std::move(file), model_}); + it = files.erase(it); + } + else { + ++it; + } + } + + } + // sort the remaining visible files by name + std::sort(files.begin(), files.end(), [](const std::shared_ptr& a, const std::shared_ptr& b) { + return QString::localeAwareCompare(a->displayName(), b->displayName()) < 0; + }); + // insert the files into the visible children list at once + model_->beginInsertRows(index(), 1, files.size() + 1); // the first item is the placeholder item, so we start from row 1 + for(auto& file: files) { + if(file->isDir()) { + DirTreeModelItem* newItem = new DirTreeModelItem(std::move(file), model_); + newItem->parent_ = this; + children_.push_back(newItem); + } + } + model_->endInsertRows(); + + // remove the place holder if a folder is added + if(children_.size() > 1) { + auto it = std::find(children_.cbegin(), children_.cend(), placeHolderChild_); + if(it != children_.cend()) { + auto pos = it - children_.cbegin(); + model_->beginRemoveRows(index(), pos, pos); + children_.erase(it); + delete placeHolderChild_; + model_->endRemoveRows(); + placeHolderChild_ = nullptr; + } + } + } + else { + // the list already contain some items, insert new items one by one so they can be sorted. + for(auto& file: files) { + if(file->isDir()) { + insertFile(std::move(file)); + } + } + } +} + +// find a good position to insert the new item +// FIXME: insert one item at a time is slow. Insert multiple items at once and then sort is faster. +int DirTreeModelItem::insertItem(DirTreeModelItem* newItem) { + if(!newItem->fileInfo_ || !newItem->fileInfo_->isDir()) { + // don't insert placeholders or non-directory files + return -1; + } + if(model_->showHidden() || !newItem->fileInfo_ || !newItem->fileInfo_->isHidden()) { + auto it = std::lower_bound(children_.cbegin(), children_.cend(), newItem, [=](const DirTreeModelItem* a, const DirTreeModelItem* b) { + if(Q_UNLIKELY(!a->fileInfo_)) { + return true; // this is a placeholder item which will be removed so the order doesn't matter. + } + if(Q_UNLIKELY(!b->fileInfo_)) { + return false; + } + return QString::localeAwareCompare(a->fileInfo_->displayName(), b->fileInfo_->displayName()) < 0; + }); + // inform the world that we're about to insert the item + auto position = it - children_.begin(); + model_->beginInsertRows(index(), position, position); + newItem->parent_ = this; + children_.insert(it, newItem); + model_->endInsertRows(); + return position; + } + else { // hidden folder + hiddenChildren_.push_back(newItem); + } + return -1; +} + + +// FmFolder signal handlers + +void DirTreeModelItem::onFolderFinishLoading() { + DirTreeModel* model = model_; + /* set 'loaded' flag beforehand as callback may check it */ + loaded_ = true; + QModelIndex idx = index(); + //qDebug() << "folder loaded"; + // remove the placeholder child if needed + // (a check for its existence is necessary; see insertItem) + if(placeHolderChild_) { + if(children_.size() == 1) { // we have no other child other than the place holder item, leave it + placeHolderChild_->displayName_ = DirTreeModel::tr(""); + QModelIndex placeHolderIndex = placeHolderChild_->index(); + // qDebug() << "placeHolderIndex: "<dataChanged(placeHolderIndex, placeHolderIndex); + } + else { + auto it = std::find(children_.cbegin(), children_.cend(), placeHolderChild_); + if(it != children_.cend()) { + auto pos = it - children_.cbegin(); + model->beginRemoveRows(idx, pos, pos); + children_.erase(it); + delete placeHolderChild_; + model->endRemoveRows(); + placeHolderChild_ = nullptr; + } + } + } + + Q_EMIT model->rowLoaded(idx); +} + +void DirTreeModelItem::onFolderFilesAdded(Fm::FileInfoList& files) { + insertFiles(files); +} + +void DirTreeModelItem::onFolderFilesRemoved(Fm::FileInfoList& files) { + DirTreeModel* model = model_; + + for(auto& fi: files) { + int pos; + DirTreeModelItem* child = childFromName(fi->name().c_str(), &pos); + if(child) { + // The item shouldn't be deleted now but after its row is removed from QTreeView; + // otherwise a freeze will happen when it has a child item (its row is expanded). + child->queuedForDeletion_ = true; + model->beginRemoveRows(index(), pos, pos); + children_.erase(children_.cbegin() + pos); + model->endRemoveRows(); + + } + } + + if(children_.empty()) { // no visible children, add a placeholder item to keep the row expanded + addPlaceHolderChild(); + placeHolderChild_->displayName_ = DirTreeModel::tr(""); + } +} + +void DirTreeModelItem::onFolderFilesChanged(std::vector &changes) { + DirTreeModel* model = model_; + for(auto& changePair: changes) { + int pos; + auto& changedFile = changePair.first; + DirTreeModelItem* child = childFromName(changedFile->name().c_str(), &pos); + if(child) { + QModelIndex childIndex = child->index(); + Q_EMIT model->dataChanged(childIndex, childIndex); + } + } +} + +DirTreeModelItem* DirTreeModelItem::childFromName(const char* utf8_name, int* pos) { + int i = 0; + for(const auto item : children_) { + if(item->fileInfo_ && item->fileInfo_->name() == utf8_name) { + if(pos) { + *pos = i; + } + return item; + } + ++i; + } + return nullptr; +} + +DirTreeModelItem* DirTreeModelItem::childFromPath(Fm::FilePath path, bool recursive) const { + Q_ASSERT(path != nullptr); + + for(DirTreeModelItem* const item : qAsConst(children_)) { + // if(item->fileInfo_) + // qDebug() << "child: " << QString::fromUtf8(fm_file_info_get_disp_name(item->fileInfo_)); + if(item->fileInfo_ && item->fileInfo_->path() == path) { + return item; + } + else if(recursive) { + DirTreeModelItem* child = item->childFromPath(std::move(path), true); + if(child) { + return child; + } + } + } + return nullptr; +} + +void DirTreeModelItem::setShowHidden(bool show) { + if(show) { + // move all hidden children to visible list + for(auto item: hiddenChildren_) { + insertItem(item); + } + hiddenChildren_.clear(); + // remove the placeholder if needed + if(children_.size() > 1) { + auto it = std::find(children_.cbegin(), children_.cend(), placeHolderChild_); + if(it != children_.cend()) { + auto pos = it - children_.cbegin(); + model_->beginRemoveRows(index(), pos, pos); + children_.erase(it); + delete placeHolderChild_; + model_->endRemoveRows(); + placeHolderChild_ = nullptr; + } + } + // recursively show children of children, etc. + for(auto item: children_) { + item->setShowHidden(true); + } + } + else { // hide hidden folders + QModelIndex _index = index(); + int pos = 0; + for(auto it = children_.begin(); it != children_.end(); ++pos) { + DirTreeModelItem* item = *it; + if(item->fileInfo_) { + if(item->fileInfo_->isHidden()) { // hidden folder + // remove from the model and add to the hiddenChildren_ list + model_->beginRemoveRows(_index, pos, pos); + it = children_.erase(it); + hiddenChildren_.push_back(item); + model_->endRemoveRows(); + } + else { // visible folder, recursively filter its children + item->setShowHidden(show); + ++it; + } + } + else { + ++it; + } + } + if(children_.empty()) { // no visible children, add a placeholder item to keep the row expanded + addPlaceHolderChild(); + placeHolderChild_->displayName_ = DirTreeModel::tr(""); + } + } +} + + + +} // namespace Fm diff --git a/src/dirtreemodelitem.h b/src/dirtreemodelitem.h new file mode 100644 index 0000000..d0b562a --- /dev/null +++ b/src/dirtreemodelitem.h @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_DIRTREEMODELITEM_H +#define FM_DIRTREEMODELITEM_H + +#include "libfmqtglobals.h" +#include +#include +#include + +#include "core/fileinfo.h" +#include "core/folder.h" + +namespace Fm { + +class DirTreeModel; +class DirTreeView; + +class LIBFM_QT_API DirTreeModelItem { +public: + friend class DirTreeModel; // allow direct access of private members in DirTreeModel + friend class DirTreeView; // allow direct access of private members in DirTreeView + + explicit DirTreeModelItem(); + explicit DirTreeModelItem(std::shared_ptr info, DirTreeModel* model, DirTreeModelItem* parent = nullptr); + ~DirTreeModelItem(); + + void loadFolder(); + void unloadFolder(); + + inline bool isPlaceHolder() const { + return (fileInfo_ == nullptr); + } + + void setShowHidden(bool show); + + bool isQueuedForDeletion() const { + return queuedForDeletion_; + } + + +private: + void freeFolder(); + void addPlaceHolderChild(); + DirTreeModelItem* childFromName(const char* utf8_name, int* pos); + DirTreeModelItem* childFromPath(Fm::FilePath path, bool recursive) const; + + DirTreeModelItem* insertFile(std::shared_ptr fi); + void insertFiles(Fm::FileInfoList files); + int insertItem(Fm::DirTreeModelItem* newItem); + QModelIndex index(); + + void onFolderFinishLoading(); + void onFolderFilesAdded(Fm::FileInfoList &files); + void onFolderFilesRemoved(Fm::FileInfoList &files); + void onFolderFilesChanged(std::vector& changes); + +private: + std::shared_ptr fileInfo_; + std::shared_ptr folder_; + QString displayName_ ; + QIcon icon_; + bool expanded_; + bool loaded_; + DirTreeModelItem* parent_; + DirTreeModelItem* placeHolderChild_; + std::vector children_; + std::vector hiddenChildren_; + DirTreeModel* model_; + bool queuedForDeletion_; + // signal connections + QMetaObject::Connection onFolderFinishLoadingConn_; + QMetaObject::Connection onFolderFilesAddedConn_; + QMetaObject::Connection onFolderFilesRemovedConn_; + QMetaObject::Connection onFolderFilesChangedConn_; +}; + +} + +#endif // FM_DIRTREEMODELITEM_H diff --git a/src/dirtreeview.cpp b/src/dirtreeview.cpp new file mode 100644 index 0000000..7230559 --- /dev/null +++ b/src/dirtreeview.cpp @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "dirtreeview.h" +#include +#include +#include +#include +#include +#include +#include "dirtreemodel.h" +#include "dirtreemodelitem.h" +#include "filemenu.h" + +namespace Fm { + +DirTreeView::DirTreeView(QWidget* parent): + QTreeView(parent), + currentExpandingItem_(nullptr) { + + setSelectionMode(QAbstractItemView::SingleSelection); + setHeaderHidden(true); + setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); + header()->setStretchLastSection(false); + + connect(this, &DirTreeView::collapsed, this, &DirTreeView::onCollapsed); + connect(this, &DirTreeView::expanded, this, &DirTreeView::onExpanded); + + setContextMenuPolicy(Qt::CustomContextMenu); + connect(this, &DirTreeView::customContextMenuRequested, + this, &DirTreeView::onCustomContextMenuRequested); +} + +DirTreeView::~DirTreeView() { +} + +void DirTreeView::cancelPendingChdir() { + if(!pathsToExpand_.empty()) { + pathsToExpand_.clear(); + if(!currentExpandingItem_) { + return; + } + DirTreeModel* _model = static_cast(model()); + disconnect(_model, &DirTreeModel::rowLoaded, this, &DirTreeView::onRowLoaded); + currentExpandingItem_ = nullptr; + } +} + +void DirTreeView::expandPendingPath() { + if(pathsToExpand_.empty()) { + return; + } + + auto path = pathsToExpand_.front(); + // qDebug() << "expanding: " << Path(path).displayBasename(); + DirTreeModel* _model = static_cast(model()); + DirTreeModelItem* item = _model->itemFromPath(path); + // qDebug() << "findItem: " << item; + if(item) { + currentExpandingItem_ = item; + connect(_model, &DirTreeModel::rowLoaded, this, &DirTreeView::onRowLoaded); + if(item->loaded_) { // the node is already loaded + onRowLoaded(item->index()); + } + else { + // _model->loadRow(item->index()); + item->loadFolder(); + } + } + else { + selectionModel()->clear(); + /* since we never get it loaded we need to update cwd here */ + currentPath_ = path; + + cancelPendingChdir(); // FIXME: is this correct? this is not done in the gtk+ version of libfm. + } +} + +void DirTreeView::onRowLoaded(const QModelIndex& index) { + DirTreeModel* _model = static_cast(model()); + if(!currentExpandingItem_) { + return; + } + if(currentExpandingItem_ != _model->itemFromIndex(index)) { + return; + } + /* disconnect the handler since we only need it once */ + disconnect(_model, &DirTreeModel::rowLoaded, this, &DirTreeView::onRowLoaded); + + // DirTreeModelItem* item = _model->itemFromIndex(index); + // qDebug() << "row loaded: " << item->displayName_; + /* after the folder is loaded, the files should have been added to + * the tree model */ + expand(index); + + /* remove the expanded path from pending list */ + pathsToExpand_.erase(pathsToExpand_.begin()); + if(pathsToExpand_.empty()) { /* this is the last one and we're done, select the item */ + // qDebug() << "Done!"; + selectionModel()->select(index, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Clear); + scrollTo(index, QAbstractItemView::EnsureVisible); + } + else { /* continue expanding next pending path */ + expandPendingPath(); + } +} + + +void DirTreeView::setCurrentPath(Fm::FilePath path) { + DirTreeModel* _model = static_cast(model()); + if(!_model) { + return; + } + int rowCount = _model->rowCount(QModelIndex()); + if(rowCount <= 0 || currentPath_ == path) { + return; + } + + currentPath_ = std::move(path); + + // NOTE: The content of each node is loaded on demand dynamically. + // So, when we ask for a chdir operation, some nodes do not exists yet. + // We have to wait for the loading of child nodes and continue the + // pending chdir operation after the child nodes become available. + + // cancel previous pending tree expansion + cancelPendingChdir(); + + /* find a root item containing this path */ + Fm::FilePath root; + for(int row = 0; row < rowCount; ++row) { + QModelIndex index = _model->index(row, 0, QModelIndex()); + auto row_path = _model->filePath(index); + if(row_path.isPrefixOf(currentPath_)) { + root = row_path; + break; + } + } + + if(root) { /* root item is found */ + path = currentPath_; + do { /* add path elements one by one to a list */ + pathsToExpand_.insert(pathsToExpand_.cbegin(), path); + // qDebug() << "prepend path: " << Path(path).displayBasename(); + if(path == root) { + break; + } + path = path.parent(); + } + while(path); + + expandPendingPath(); + } +} + +void DirTreeView::setModel(QAbstractItemModel* model) { + Q_ASSERT(model->inherits("Fm::DirTreeModel")); + + if(!pathsToExpand_.empty()) { // if a chdir request is in progress, cancel it + cancelPendingChdir(); + } + + QTreeView::setModel(model); + header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &DirTreeView::onSelectionChanged); +} + +void DirTreeView::mousePressEvent(QMouseEvent* event) { + if(event && event->button() == Qt::RightButton && + event->type() == QEvent::MouseButtonPress) { + // Do not change the selection when the context menu is activated. + return; + } + QTreeView::mousePressEvent(event); +} + +void DirTreeView::onCustomContextMenuRequested(const QPoint& pos) { + QModelIndex index = indexAt(pos); + if(index.isValid()) { + QVariant data = index.data(DirTreeModel::FileInfoRole); + auto fileInfo = data.value>(); + if(fileInfo) { + auto path = fileInfo->path(); + Fm::FileInfoList files ; + files.push_back(fileInfo); + Fm::FileMenu* menu = new Fm::FileMenu(files, fileInfo, path); + // FIXME: apply some settings to the menu and set a proper file launcher to it + Q_EMIT prepareFileMenu(menu); + + QVariant pathData = qVariantFromValue(path); + QAction* action = menu->openAction(); + action->disconnect(); + action->setData(index); + connect(action, &QAction::triggered, this, &DirTreeView::onOpen); + action = new QAction(QIcon::fromTheme("window-new"), tr("Open in New T&ab"), menu); + action->setData(pathData); + connect(action, &QAction::triggered, this, &DirTreeView::onNewTab); + menu->insertAction(menu->separator1(), action); + action = new QAction(QIcon::fromTheme("window-new"), tr("Open in New Win&dow"), menu); + action->setData(pathData); + connect(action, &QAction::triggered, this, &DirTreeView::onNewWindow); + menu->insertAction(menu->separator1(), action); + if(fileInfo->isNative()) { + action = new QAction(QIcon::fromTheme("utilities-terminal"), tr("Open in Termina&l"), menu); + action->setData(pathData); + connect(action, &QAction::triggered, this, &DirTreeView::onOpenInTerminal); + menu->insertAction(menu->separator1(), action); + } + menu->exec(mapToGlobal(pos)); + delete menu; + } + } +} + +void DirTreeView::onOpen() { + if(QAction* action = qobject_cast(sender())) { + setCurrentIndex(action->data().toModelIndex()); + } +} + +void DirTreeView::onNewWindow() { + if(QAction* action = qobject_cast(sender())) { + auto path = action->data().value(); + Q_EMIT openFolderInNewWindowRequested(path); + } +} + +void DirTreeView::onNewTab() { + if(QAction* action = qobject_cast(sender())) { + auto path = action->data().value(); + Q_EMIT openFolderInNewTabRequested(path); + } +} + +void DirTreeView::onOpenInTerminal() { + if(QAction* action = qobject_cast(sender())) { + auto path = action->data().value(); + Q_EMIT openFolderInTerminalRequested(path); + } +} + +void DirTreeView::onNewFolder() { + if(QAction* action = qobject_cast(sender())) { + auto path = action->data().value(); + Q_EMIT createNewFolderRequested(path); + } +} + +void DirTreeView::onCollapsed(const QModelIndex& index) { + DirTreeModel* treeModel = static_cast(model()); + if(treeModel) { + treeModel->unloadRow(index); + } +} + +void DirTreeView::onExpanded(const QModelIndex& index) { + DirTreeModel* treeModel = static_cast(model()); + if(treeModel) { + treeModel->loadRow(index); + } +} +void DirTreeView::rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { + // see if to-be-removed items are queued for deletion + // and also clear selection if one of them is selected (otherwise a freeze will occur) + QModelIndex selIndex; + if(selectionModel()->selectedRows().size() == 1) { + selIndex = selectionModel()->selectedRows().at(0); + } + for (int i = start; i <= end; ++i) { + QModelIndex index = parent.child(i, 0); + if(index.isValid()) { + if(index == selIndex) { + selectionModel()->clear(); + } + DirTreeModelItem* item = reinterpret_cast(index.internalPointer()); + if (item->isQueuedForDeletion()) { + queuedForDeletion_.push_back(item); + } + } + } + + QTreeView::rowsAboutToBeRemoved (parent, start, end); +} + +void DirTreeView::rowsRemoved(const QModelIndex& parent, int start, int end) { + QTreeView::rowsRemoved (parent, start, end); + // do the queued deletions only after all rows are removed (otherwise a freeze might occur) + QTimer::singleShot(0, this, SLOT (doQueuedDeletions())); +} + +void DirTreeView::doQueuedDeletions() { + if(!queuedForDeletion_.empty()) { + for(DirTreeModelItem* const item : qAsConst(queuedForDeletion_)) { + delete item; + } + queuedForDeletion_.clear(); + } +} + +void DirTreeView::onSelectionChanged(const QItemSelection& selected, const QItemSelection& /*deselected*/) { + if(!selected.isEmpty()) { + QModelIndex index = selected.first().topLeft(); + DirTreeModel* _model = static_cast(model()); + auto path = _model->filePath(index); + if(path && currentPath_ && path == currentPath_) { + return; + } + cancelPendingChdir(); + if(!path) { + return; + } + currentPath_ = std::move(path); + + // FIXME: use enums for type rather than hard-coded values 0 or 1 + int type = 0; + if(QGuiApplication::mouseButtons() & Qt::MiddleButton) { + type = 1; + } + Q_EMIT chdirRequested(type, currentPath_); + } +} + + +} // namespace Fm diff --git a/src/dirtreeview.h b/src/dirtreeview.h new file mode 100644 index 0000000..1a9cc53 --- /dev/null +++ b/src/dirtreeview.h @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_DIRTREEVIEW_H +#define FM_DIRTREEVIEW_H + +#include "libfmqtglobals.h" +#include + +#include "core/filepath.h" + +class QItemSelection; + +namespace Fm { + +class FileMenu; +class DirTreeModelItem; + +class LIBFM_QT_API DirTreeView : public QTreeView { + Q_OBJECT + +public: + explicit DirTreeView(QWidget* parent); + ~DirTreeView(); + + const Fm::FilePath& currentPath() const { + return currentPath_; + } + + void setCurrentPath(Fm::FilePath path); + + void chdir(Fm::FilePath path) { + setCurrentPath(std::move(path)); + } + + virtual void setModel(QAbstractItemModel* model); + +protected: + virtual void mousePressEvent(QMouseEvent* event); + virtual void rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end); + +private: + void cancelPendingChdir(); + void expandPendingPath(); + +Q_SIGNALS: + void chdirRequested(int type, const Fm::FilePath& path); + void openFolderInNewWindowRequested(const Fm::FilePath& path); + void openFolderInNewTabRequested(const Fm::FilePath& path); + void openFolderInTerminalRequested(const Fm::FilePath& path); + void createNewFolderRequested(const Fm::FilePath& path); + void prepareFileMenu(Fm::FileMenu* menu); // emit before showing a Fm::FileMenu + +protected Q_SLOTS: + void onCollapsed(const QModelIndex& index); + void onExpanded(const QModelIndex& index); + void onRowLoaded(const QModelIndex& index); + void onSelectionChanged(const QItemSelection& selected, const QItemSelection& deselected); + void onCustomContextMenuRequested(const QPoint& pos); + void onOpen(); + void onNewWindow(); + void onNewTab(); + void onOpenInTerminal(); + void onNewFolder(); + void rowsRemoved(const QModelIndex& parent, int start, int end); + void doQueuedDeletions(); + +private: + Fm::FilePath currentPath_; + Fm::FilePathList pathsToExpand_; + DirTreeModelItem* currentExpandingItem_; + std::vector queuedForDeletion_; +}; + +} + +#endif // FM_DIRTREEVIEW_H diff --git a/src/dndactionmenu.cpp b/src/dndactionmenu.cpp new file mode 100644 index 0000000..2688376 --- /dev/null +++ b/src/dndactionmenu.cpp @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "dndactionmenu.h" + +namespace Fm { + +DndActionMenu::DndActionMenu(Qt::DropActions possibleActions, QWidget* parent) + : QMenu(parent) + , copyAction(nullptr) + , moveAction(nullptr) + , linkAction(nullptr) + , cancelAction(nullptr) { + if(possibleActions.testFlag(Qt::CopyAction)) { + copyAction = addAction(QIcon::fromTheme("edit-copy"), tr("Copy here")); + } + if(possibleActions.testFlag(Qt::MoveAction)) { + moveAction = addAction(tr("Move here")); + } + if(possibleActions.testFlag(Qt::LinkAction)) { + linkAction = addAction(tr("Create symlink here")); + } + addSeparator(); + cancelAction = addAction(tr("Cancel")); +} + +DndActionMenu::~DndActionMenu() { + +} + +Qt::DropAction DndActionMenu::askUser(Qt::DropActions possibleActions, QPoint pos) { + Qt::DropAction result = Qt::IgnoreAction; + DndActionMenu menu{possibleActions}; + QAction* action = menu.exec(pos); + if(nullptr != action) { + if(action == menu.copyAction) { + result = Qt::CopyAction; + } + else if(action == menu.moveAction) { + result = Qt::MoveAction; + } + else if(action == menu.linkAction) { + result = Qt::LinkAction; + } + } + return result; +} + + +} // namespace Fm diff --git a/src/dndactionmenu.h b/src/dndactionmenu.h new file mode 100644 index 0000000..d4dd1bd --- /dev/null +++ b/src/dndactionmenu.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_DNDACTIONMENU_H +#define FM_DNDACTIONMENU_H + +#include "libfmqtglobals.h" +#include +#include + +namespace Fm { + +class DndActionMenu : public QMenu { + Q_OBJECT +public: + explicit DndActionMenu(Qt::DropActions possibleActions, QWidget* parent = 0); + virtual ~DndActionMenu(); + + static Qt::DropAction askUser(Qt::DropActions possibleActions, QPoint pos); + +private: + QAction* copyAction; + QAction* moveAction; + QAction* linkAction; + QAction* cancelAction; +}; + +} + +#endif // FM_DNDACTIONMENU_H diff --git a/src/dnddest.cpp b/src/dnddest.cpp new file mode 100644 index 0000000..b0f35c9 --- /dev/null +++ b/src/dnddest.cpp @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "dnddest.h" +#include "fileoperation.h" +#include "utilities.h" + +namespace Fm { + +const char* supportedMimeTypes[] = { + "text/uri-list" + "XdndDirectSave0"/* X direct save */ + /* TODO: add more targets to support: text types, _NETSCAPE_URL, property/bgimage ... */ +}; + +DndDest::DndDest() { + +} + +DndDest::~DndDest() { + +} + +bool DndDest::dropMimeData(const QMimeData* data, Qt::DropAction action) { + // FIXME: should we put this in dropEvent handler of FolderView instead? + if(data->hasUrls()) { + qDebug("drop action: %d", action); + auto srcPaths = pathListFromQUrls(data->urls()); + switch(action) { + case Qt::CopyAction: + FileOperation::copyFiles(srcPaths, destPath_); + break; + case Qt::MoveAction: + FileOperation::moveFiles(srcPaths, destPath_); + break; + case Qt::LinkAction: + FileOperation::symlinkFiles(srcPaths, destPath_); + /* Falls through. */ + default: + return false; + } + return true; + } + return false; +} + +bool DndDest::isSupported(const QMimeData* /*data*/) { + return false; +} + +bool DndDest::isSupported(QString /*mimeType*/) { + return false; +} + + +} // namespace Fm diff --git a/src/dnddest.h b/src/dnddest.h new file mode 100644 index 0000000..d5769e8 --- /dev/null +++ b/src/dnddest.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_DNDDEST_H +#define FM_DNDDEST_H + +#include +#include "core/filepath.h" + +namespace Fm { + +class DndDest { +public: + explicit DndDest(); + ~DndDest(); + + void setDestPath(Fm::FilePath dest) { + destPath_ = std::move(dest); + } + + const Fm::FilePath& destPath() const { + return destPath_; + } + + static bool isSupported(const QMimeData* data); + static bool isSupported(QString mimeType); + + bool dropMimeData(const QMimeData* data, Qt::DropAction action); + +private: + Fm::FilePath destPath_; +}; + +} + +#endif // FM_DNDDEST_H diff --git a/src/edit-bookmarks.ui b/src/edit-bookmarks.ui new file mode 100644 index 0000000..8d989e6 --- /dev/null +++ b/src/edit-bookmarks.ui @@ -0,0 +1,143 @@ + + + EditBookmarksDialog + + + + 0 + 0 + 480 + 320 + + + + Edit Bookmarks + + + + + + true + + + true + + + QAbstractItemView::InternalMove + + + Qt::MoveAction + + + false + + + false + + + 100 + + + + Name + + + + + Location + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + &Add Item + + + + + + + + + + &Remove Item + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Use drag and drop to reorder the items + + + + + + + + + buttonBox + accepted() + EditBookmarksDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + EditBookmarksDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/editbookmarksdialog.cpp b/src/editbookmarksdialog.cpp new file mode 100644 index 0000000..a36a22f --- /dev/null +++ b/src/editbookmarksdialog.cpp @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "editbookmarksdialog.h" +#include "ui_edit-bookmarks.h" +#include +#include +#include +#include +#include + +namespace Fm { + +EditBookmarksDialog::EditBookmarksDialog(std::shared_ptr bookmarks, QWidget* parent, Qt::WindowFlags f): + QDialog(parent, f), + ui(new Ui::EditBookmarksDialog()), + bookmarks_(std::move(bookmarks)) { + + ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose); // auto delete on close + + // load bookmarks + for(const auto& bookmark: bookmarks_->items()) { + QTreeWidgetItem* item = new QTreeWidgetItem(); + item->setData(0, Qt::DisplayRole, bookmark->name()); + item->setData(1, Qt::DisplayRole, bookmark->path().toString().get()); + item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsDragEnabled | Qt::ItemIsEnabled); + ui->treeWidget->addTopLevelItem(item); + } + + connect(ui->addItem, &QPushButton::clicked, this, &EditBookmarksDialog::onAddItem); + connect(ui->removeItem, &QPushButton::clicked, this, &EditBookmarksDialog::onRemoveItem); +} + +EditBookmarksDialog::~EditBookmarksDialog() { + 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() { + const QList sels = ui->treeWidget->selectedItems(); + for(QTreeWidgetItem* const item : sels) { + delete item; + } +} + + +} // namespace Fm diff --git a/src/editbookmarksdialog.h b/src/editbookmarksdialog.h new file mode 100644 index 0000000..fb7b3cb --- /dev/null +++ b/src/editbookmarksdialog.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_EDITBOOKMARKSDIALOG_H +#define FM_EDITBOOKMARKSDIALOG_H + +#include "libfmqtglobals.h" +#include +#include "core/bookmarks.h" + +namespace Ui { +class EditBookmarksDialog; +} + +namespace Fm { + +class LIBFM_QT_API EditBookmarksDialog : public QDialog { + Q_OBJECT +public: + explicit EditBookmarksDialog(std::shared_ptr bookmarks, QWidget* parent = 0, Qt::WindowFlags f = 0); + virtual ~EditBookmarksDialog(); + + virtual void accept(); + +private Q_SLOTS: + void onAddItem(); + void onRemoveItem(); + +private: + Ui::EditBookmarksDialog* ui; + std::shared_ptr bookmarks_; +}; + +} + +#endif // FM_EDITBOOKMARKSDIALOG_H diff --git a/src/exec-file.ui b/src/exec-file.ui new file mode 100644 index 0000000..c5a9ea3 --- /dev/null +++ b/src/exec-file.ui @@ -0,0 +1,163 @@ + + + ExecFileDialog + + + + 0 + 0 + 487 + 58 + + + + Execute file + + + + + + + + + + + + + + true + + + + + + + + + + + &Open + + + + + + true + + + + + + + E&xecute + + + + + + + + + + Execute in &Terminal + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Cancel + + + + + + + + + + + + + + cancel + clicked() + ExecFileDialog + reject() + + + 341 + 39 + + + 196 + 28 + + + + + exec + clicked() + ExecFileDialog + accept() + + + 56 + 39 + + + 196 + 28 + + + + + execTerm + clicked() + ExecFileDialog + accept() + + + 201 + 39 + + + 196 + 28 + + + + + open + clicked() + ExecFileDialog + accept() + + + 346 + 39 + + + 250 + 28 + + + + + diff --git a/src/execfiledialog.cpp b/src/execfiledialog.cpp new file mode 100644 index 0000000..b0b2780 --- /dev/null +++ b/src/execfiledialog.cpp @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "execfiledialog_p.h" +#include "ui_exec-file.h" +#include "core/iconinfo.h" + +namespace Fm { + +ExecFileDialog::ExecFileDialog(const FileInfo &fileInfo, QWidget* parent, Qt::WindowFlags f): + QDialog(parent, f), + ui(new Ui::ExecFileDialog()), + result_(BasicFileLauncher::ExecAction::DIRECT_EXEC) { + + ui->setupUi(this); + // show file icon + auto gicon = fileInfo.icon(); + if(gicon) { + ui->icon->setPixmap(gicon->qicon().pixmap(QSize(48, 48))); + } + + QString msg; + if(fileInfo.isDesktopEntry()) { + msg = tr("This file '%1' seems to be a desktop entry.\nWhat do you want to do with it?") + .arg(fileInfo.displayName()); + ui->exec->setDefault(true); + ui->execTerm->hide(); + } + else if(fileInfo.isText()) { + msg = tr("This text file '%1' seems to be an executable script.\nWhat do you want to do with it?") + .arg(fileInfo.displayName()); + ui->execTerm->setDefault(true); + } + else { + msg = tr("This file '%1' is executable. Do you want to execute it?") + .arg(fileInfo.displayName()); + ui->exec->setDefault(true); + ui->open->hide(); + } + ui->msg->setText(msg); +} + +ExecFileDialog::~ExecFileDialog() { + delete ui; +} + +void ExecFileDialog::accept() { + QObject* _sender = sender(); + if(_sender == ui->exec) { + result_ = BasicFileLauncher::ExecAction::DIRECT_EXEC; + } + else if(_sender == ui->execTerm) { + result_ = BasicFileLauncher::ExecAction::EXEC_IN_TERMINAL; + } + else if(_sender == ui->open) { + result_ = BasicFileLauncher::ExecAction::OPEN_WITH_DEFAULT_APP; + } + else { + result_ = BasicFileLauncher::ExecAction::CANCEL; + } + QDialog::accept(); +} + +void ExecFileDialog::reject() { + result_ = BasicFileLauncher::ExecAction::CANCEL; + QDialog::reject(); +} + +} // namespace Fm diff --git a/src/execfiledialog_p.h b/src/execfiledialog_p.h new file mode 100644 index 0000000..8ceae1a --- /dev/null +++ b/src/execfiledialog_p.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_EXECFILEDIALOG_H +#define FM_EXECFILEDIALOG_H + +#include "core/basicfilelauncher.h" +#include "core/fileinfo.h" + +#include + +#include + +namespace Ui { + class ExecFileDialog; +} + +namespace Fm { + +class ExecFileDialog : public QDialog { + Q_OBJECT +public: + ~ExecFileDialog(); + ExecFileDialog(const FileInfo& fileInfo, QWidget* parent = 0, Qt::WindowFlags f = 0); + + BasicFileLauncher::ExecAction result() { + return result_; + } + +protected: + virtual void accept() override; + virtual void reject() override; + +private: + Ui::ExecFileDialog* ui; + BasicFileLauncher::ExecAction result_; +}; + +} + +#endif // FM_EXECFILEDIALOG_H diff --git a/src/file-operation-dialog.ui b/src/file-operation-dialog.ui new file mode 100644 index 0000000..47f8c67 --- /dev/null +++ b/src/file-operation-dialog.ui @@ -0,0 +1,192 @@ + + + FileOperationDialog + + + + 0 + 0 + 450 + 297 + + + + + + + + + + + + + + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + Destination: + + + + + + + + 0 + 0 + + + + + + + true + + + + + + + Processing: + + + + + + + + 0 + 0 + + + + Preparing... + + + + + + + Progress + + + + + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + Time remaining: + + + + + + + + 0 + 0 + + + + + + + + + + + + 0 + 0 + + + + + + + + Files processed: + + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel + + + + + + + + Fm::ElidedLabel + QLabel +
fileoperationdialog_p.h
+
+
+ + + + buttonBox + accepted() + FileOperationDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + FileOperationDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +
diff --git a/src/file-props.ui b/src/file-props.ui new file mode 100644 index 0000000..932e5f0 --- /dev/null +++ b/src/file-props.ui @@ -0,0 +1,800 @@ + + + FilePropsDialog + + + + 0 + 0 + 424 + 456 + + + + File Properties + + + + + + + + + 10 + + + 10 + + + 10 + + + 10 + + + + + 0 + + + + General + + + + 12 + + + 6 + + + + + + 0 + 0 + + + + + + + + + + + + + 32 + 32 + + + + + + + + + + + Location: + + + + + + + + 0 + 0 + + + + + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + File type: + + + + + + + + 0 + 0 + + + + + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + MIME type: + + + + + + + + 0 + 0 + + + + + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + File size: + + + + + + + + 0 + 0 + + + + + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + On-disk size: + + + + + + + + 0 + 0 + + + + + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Last modified: + + + + + + + + 0 + 0 + + + + + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Link target: + + + + + + + + 0 + 0 + + + + + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Open With: + + + + + + + + 0 + 0 + + + + + + + + Last accessed: + + + + + + + + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Contains: + + + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 5 + 5 + + + + + + + + + 0 + 0 + + + + Device Usage: + + + + + + + 5 + + + + + 0 + + + + + + + + + + true + + + + + + + + + + Permissions + + + + 6 + + + + + + 0 + 0 + + + + Ownership + + + + 12 + + + 6 + + + + + + + + + + + + 0 + 0 + + + + Group: + + + + + + + + 0 + 0 + + + + Owner: + + + + + + + + + + + 0 + 0 + + + + Access Control + + + + + + + 0 + 0 + + + + 0 + + + + + + + Owner: + + + + + + + + 0 + 0 + + + + + + + + Group: + + + + + + + + 0 + 0 + + + + + + + + Other: + + + + + + + + 0 + 0 + + + + + + + + Make the file executable + + + true + + + + + + + + + + + 0 + + + 6 + + + + + + 0 + 0 + + + + Owner: + + + + + + + + 0 + 0 + + + + Read + + + + + + + + 0 + 0 + + + + Write + + + + + + + + 0 + 0 + + + + Execute + + + + + + + + 0 + 0 + + + + Group: + + + + + + + + 0 + 0 + + + + Read + + + + + + + + 0 + 0 + + + + Write + + + + + + + + 0 + 0 + + + + Execute + + + + + + + + 0 + 0 + + + + Other: + + + + + + + + 0 + 0 + + + + Read + + + + + + + + 0 + 0 + + + + Write + + + + + + + + 0 + 0 + + + + Execute + + + + + + + + + Sticky + + + + + + + SetUID + + + + + + + SetGID + + + + + + + Qt::Vertical + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + Advanced Mode + + + true + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + Fm::AppChooserComboBox + QComboBox +
appchoosercombobox.h
+
+
+ + + + buttonBox + accepted() + FilePropsDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + FilePropsDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +
diff --git a/src/filedialog.cpp b/src/filedialog.cpp new file mode 100644 index 0000000..a3df58a --- /dev/null +++ b/src/filedialog.cpp @@ -0,0 +1,991 @@ +#include "filedialog.h" +#include "cachedfoldermodel.h" +#include "proxyfoldermodel.h" +#include "utilities.h" +#include "core/fileinfojob.h" +#include "ui_filedialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Fm { + + +FileDialog::FileDialog(QWidget* parent, FilePath path) : + QDialog(parent), + ui{new Ui::FileDialog()}, + folderModel_{nullptr}, + proxyModel_{nullptr}, + folder_{nullptr}, + options_{0}, + viewMode_{FolderView::DetailedListMode}, + fileMode_{QFileDialog::AnyFile}, + acceptMode_{QFileDialog::AcceptOpen}, + confirmOverwrite_{true}, + modelFilter_{this} { + + ui->setupUi(this); + + // path bar + connect(ui->location, &PathBar::chdir, [this](const FilePath &path) { + setDirectoryPath(path); + }); + + // side pane + ui->sidePane->setMode(Fm::SidePane::ModePlaces); + connect(ui->sidePane, &SidePane::chdirRequested, [this](int /*type*/, const FilePath &path) { + setDirectoryPath(path); + }); + + // folder view + proxyModel_ = new ProxyFolderModel(this); + proxyModel_->sort(FolderModel::ColumnFileName, Qt::AscendingOrder); + proxyModel_->setThumbnailSize(64); + proxyModel_->setShowThumbnails(true); + + proxyModel_->addFilter(&modelFilter_); + + connect(ui->folderView, &FolderView::clicked, this, &FileDialog::onFileClicked); + ui->folderView->setModel(proxyModel_); + ui->folderView->setAutoSelectionDelay(0); + // set the completer + QCompleter* completer = new QCompleter(this); + completer->setCaseSensitivity(Qt::CaseInsensitive); + completer->setFilterMode(Qt::MatchContains); + completer->setModel(proxyModel_); + ui->fileName->setCompleter(completer); + connect(completer, static_cast(&QCompleter::activated), [this](const QString &text) { + ui->folderView->selectionModel()->clearSelection(); + selectFilePath(directoryPath_.child(text.toLocal8Bit().constData())); + }); + // select typed paths if it they exist + connect(ui->fileName, &QLineEdit::textEdited, [this](const QString& /*text*/) { + disconnect(ui->folderView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &FileDialog::onSelectionChanged); + ui->folderView->selectionModel()->clearSelection(); + QStringList parsedNames = parseNames(); + for(auto& name: parsedNames) { + if(!defaultSuffix_.isEmpty() && name.lastIndexOf('.') == -1) { + name += '.'; + name += defaultSuffix_; + } + selectFilePath(directoryPath_.child(name.toLocal8Bit().constData())); + } + updateAcceptButtonState(); + updateSaveButtonText(false); + connect(ui->folderView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &FileDialog::onSelectionChanged); + }); + // update selection mode for the view + updateSelectionMode(); + + // file type + connect(ui->fileTypeCombo, &QComboBox::currentTextChanged, [this](const QString& text) { + selectNameFilter(text); + }); + ui->fileTypeCombo->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLength); + ui->fileTypeCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + ui->fileTypeCombo->setCurrentIndex(0); + + QShortcut* shortcut = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_H), this); + connect(shortcut, &QShortcut::activated, [this]() { + proxyModel_->setShowHidden(!proxyModel_->showHidden()); + }); + + // setup toolbar buttons + auto toolbar = new QToolBar(this); + // back button + backAction_ = toolbar->addAction(QIcon::fromTheme("go-previous"), tr("Go Back")); + backAction_->setShortcut(QKeySequence(tr("Alt+Left", "Go Back"))); + connect(backAction_, &QAction::triggered, [this]() { + history_.backward(); + setDirectoryPath(history_.currentPath(), FilePath(), false); + }); + // forward button + forwardAction_ = toolbar->addAction(QIcon::fromTheme("go-next"), tr("Go Forward")); + forwardAction_->setShortcut(QKeySequence(tr("Alt+Right", "Go Forward"))); + connect(forwardAction_, &QAction::triggered, [this]() { + history_.forward(); + setDirectoryPath(history_.currentPath(), FilePath(), false); + }); + toolbar->addSeparator(); + // reload button + auto reloadAction = toolbar->addAction(QIcon::fromTheme("view-refresh"), tr("Reload")); + reloadAction->setShortcut(QKeySequence(tr("F5", "Reload"))); + connect(reloadAction, &QAction::triggered, [this]() { + if(folder_ && folder_->isLoaded()) { + QObject::disconnect(lambdaConnection_); + auto selFiles = ui->folderView->selectedFiles(); + ui->folderView->selectionModel()->clear(); + // reselect files on reloading + if(!selFiles.empty() + && selFiles.size() <= 50) { // otherwise senseless and CPU-intensive + lambdaConnection_ = QObject::connect(folder_.get(), &Fm::Folder::finishLoading, this, [this, selFiles]() { + selectFilesOnReload(selFiles); + }); + } + folder_->reload(); + } + }); + // new folder button + auto newFolderAction = toolbar->addAction(QIcon::fromTheme("folder-new"), tr("Create Folder")); + connect(newFolderAction, &QAction::triggered, this, &FileDialog::onNewFolder); + toolbar->addSeparator(); + // view buttons + auto viewModeGroup = new QActionGroup(this); + + // use generic icons for view actions only if theme icons don't exist + iconViewAction_ = toolbar->addAction(QIcon::fromTheme(QLatin1String("view-list-icons"), style()->standardIcon(QStyle::SP_FileDialogContentsView)), tr("Icon View")); + iconViewAction_->setCheckable(true); + connect(iconViewAction_, &QAction::toggled, this, &FileDialog::onViewModeToggled); + viewModeGroup->addAction(iconViewAction_); + thumbnailViewAction_ = toolbar->addAction(QIcon::fromTheme(QLatin1String("dialog-information"), style()->standardIcon(QStyle::SP_FileDialogInfoView)), tr("Thumbnail View")); + thumbnailViewAction_->setCheckable(true); + connect(thumbnailViewAction_, &QAction::toggled, this, &FileDialog::onViewModeToggled); + viewModeGroup->addAction(thumbnailViewAction_); + compactViewAction_ = toolbar->addAction(QIcon::fromTheme(QLatin1String("view-list-text"), style()->standardIcon(QStyle::SP_FileDialogListView)), tr("Compact View")); + compactViewAction_->setCheckable(true); + connect(compactViewAction_, &QAction::toggled, this, &FileDialog::onViewModeToggled); + viewModeGroup->addAction(compactViewAction_); + detailedViewAction_ = toolbar->addAction(QIcon::fromTheme(QLatin1String("view-list-details"), style()->standardIcon(QStyle::SP_FileDialogDetailedView)), tr("Detailed List View")); + detailedViewAction_->setCheckable(true); + connect(detailedViewAction_, &QAction::toggled, this, &FileDialog::onViewModeToggled); + viewModeGroup->addAction(detailedViewAction_); + ui->toolbarLayout->addWidget(toolbar); + + setViewMode(viewMode_); + + // set the default splitter position + setSplitterPos(200); + + // browse to the directory + if(path.isValid()) { + setDirectoryPath(path); + } + else { + goHome(); + } + + // focus the text entry on showing the dialog + QTimer::singleShot(0, ui->fileName, SLOT(setFocus())); +} + +FileDialog::~FileDialog() { + freeFolder(); +} + +int FileDialog::splitterPos() const { + return ui->splitter->sizes().at(0); +} + +void FileDialog::setSplitterPos(int pos) { + QList sizes; + sizes.append(qMax(pos, 0)); + sizes.append(320); + ui->splitter->setSizes(sizes); +} + +// This should always be used instead of getting text directly from the entry. +QStringList FileDialog::parseNames() const { + // parse the file names from the text entry + QStringList parsedNames; + auto fileNames = ui->fileName->text(); + if(!fileNames.isEmpty()) { + /* check if there are multiple file names (containing "), + considering the fact that inside quotes were escaped by \ */ + auto firstQuote = fileNames.indexOf(QLatin1Char('\"')); + auto lastQuote = fileNames.lastIndexOf(QLatin1Char('\"')); + if(firstQuote != -1 && lastQuote != -1 + && firstQuote != lastQuote + && (firstQuote == 0 || fileNames.at(firstQuote - 1) != QLatin1Char('\\')) + && fileNames.at(lastQuote - 1) != QLatin1Char('\\')) { + // split the names +#if (QT_VERSION >= QT_VERSION_CHECK(5,12,0)) + QRegularExpression sep{"\"\\s+\""}; // separated with " " +#else + QRegExp sep{"\"\\s+\""}; // separated with " " +#endif + parsedNames = fileNames.mid(firstQuote + 1, lastQuote - firstQuote - 1).split(sep); + parsedNames.replaceInStrings(QLatin1String("\\\""), QLatin1String("\"")); + } + else { + parsedNames << fileNames.replace(QLatin1String("\\\""), QLatin1String("\"")); + } + } + return parsedNames; +} + +std::shared_ptr FileDialog::firstSelectedDir() const { + std::shared_ptr selectedFolder = nullptr; + auto list = ui->folderView->selectedFiles(); + for(auto it = list.cbegin(); it != list.cend(); ++it) { + auto& item = *it; + if(item->isDir()) { + selectedFolder = item; + break; + } + } + return selectedFolder; +} + +void FileDialog::accept() { + // handle selected filenames + selectedFiles_.clear(); + + // if a folder is selected in file mode, chdir into it (as QFileDialog does) + // by giving priority to the current index and, if it isn't a folder, + // to the first selected folder + if(fileMode_ != QFileDialog::Directory) { + std::shared_ptr selectedFolder = nullptr; + // check if the current index is a folder + QItemSelectionModel* selModel = ui->folderView->selectionModel(); + QModelIndex cur = selModel->currentIndex(); + if(cur.isValid() && selModel->isSelected(cur)) { + auto file = proxyModel_->fileInfoFromIndex(cur); + if(file && file->isDir()) { + selectedFolder = file; + } + } + if(!selectedFolder) { + selectedFolder = firstSelectedDir(); + } + if(selectedFolder) { + setDirectoryPath(selectedFolder->path()); + return; + } + } + + QStringList parsedNames = parseNames(); + if(parsedNames.isEmpty()) { + // when selecting a dir and the name is not provided, just select current dir in the view + if(fileMode_ == QFileDialog::Directory) { + auto localPath = directoryPath_.localPath(); + if(localPath) { + selectedFiles_.append(QUrl::fromLocalFile(localPath.get())); + } + else { + selectedFiles_.append(directory()); + } + } + else { + QMessageBox::critical(this, tr("Error"), tr("Please select a file")); + return; + } + } + else { + if(fileMode_ != QFileDialog::Directory) { + auto firstName = parsedNames.at(0); + if(!defaultSuffix_.isEmpty() && firstName.lastIndexOf('.') == -1) { + firstName += '.'; + firstName += defaultSuffix_; + } + auto childPath = directoryPath_.child(firstName.toLocal8Bit().constData()); + auto info = proxyModel_->fileInfoFromPath(childPath); + if(info) { + // if the typed name belongs to a (nonselected) directory, chdir into it + if(info->isDir()) { + setDirectoryPath(childPath); + return; + } + // overwrite prompt (as in QFileDialog::accept) + if(fileMode_ == QFileDialog::AnyFile + && acceptMode_ != QFileDialog::AcceptOpen + && confirmOverwrite_) { + if (QMessageBox::warning(this, windowTitle(), + tr("%1 already exists.\nDo you want to replace it?") + .arg(firstName), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + == QMessageBox::No) { + return; + } + } + } + } + + // get full paths for the filenames and convert them to URLs + for(auto& name: parsedNames) { + // add default filename extension as needed + if(!defaultSuffix_.isEmpty() && name.lastIndexOf('.') == -1) { + name += '.'; + name += defaultSuffix_; + } + auto fullPath = directoryPath_.child(name.toLocal8Bit().constData()); + auto localPath = fullPath.localPath(); + /* add the local path if it exists; otherwise, add the uri */ + if(localPath) { + selectedFiles_.append(QUrl::fromLocalFile(localPath.get())); + } + else { + selectedFiles_.append(QUrl::fromEncoded(fullPath.uri().get())); + } + } + } + + // check existence of the selected files and if their types are correct + // async operation, call doAccept() in the callback. + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + + auto pathList = pathListFromQUrls(selectedFiles_); + auto job = new FileInfoJob(pathList); + job->setAutoDelete(true); + connect(job, &Job::finished, this, &FileDialog::onFileInfoJobFinished); + job->runAsync(); +} + +void FileDialog::reject() { + QDialog::reject(); +} + +void FileDialog::setDirectory(const QUrl &directory) { + auto path = Fm::FilePath::fromUri(directory.toEncoded().constData()); + setDirectoryPath(path); +} + +// interface for QPlatformFileDialogHelper + +void FileDialog::freeFolder() { + if(folder_) { + QObject::disconnect(lambdaConnection_); // lambdaConnection_ can be invalid + disconnect(folder_.get(), nullptr, this, nullptr); + folder_ = nullptr; + } +} + +void FileDialog::goHome() { + setDirectoryPath(FilePath::homeDir()); +} + +void FileDialog::setDirectoryPath(FilePath directory, FilePath selectedPath, bool addHistory) { + if(!directory.isValid()) { + updateAcceptButtonState(); // FIXME: is this needed? + return; + } + + if(directoryPath_ != directory) { + if(folder_) { + if(folderModel_) { + proxyModel_->setSourceModel(nullptr); + folderModel_->unref(); // unref the cached model + folderModel_ = nullptr; + } + freeFolder(); + } + + directoryPath_ = std::move(directory); + + ui->location->setPath(directoryPath_); + ui->sidePane->chdir(directoryPath_); + if(addHistory) { + history_.add(directoryPath_); + } + backAction_->setEnabled(history_.canBackward()); + forwardAction_->setEnabled(history_.canForward()); + + folder_ = Fm::Folder::fromPath(directoryPath_); + folderModel_ = CachedFolderModel::modelFromFolder(folder_); + proxyModel_->setSourceModel(folderModel_); + + // no lambda in these connections for easy disconnection + connect(folder_.get(), &Fm::Folder::removed, this, &FileDialog::goHome); + connect(folder_.get(), &Fm::Folder::unmount, this, &FileDialog::goHome); + + QUrl uri = QUrl::fromEncoded(directory.uri().get()); + Q_EMIT directoryEntered(uri); + } + + // select the path if valid + if(selectedPath.isValid()) { + if(folder_->isLoaded()) { + selectFilePathWithDelay(selectedPath); + } + else { + lambdaConnection_ = QObject::connect(folder_.get(), &Fm::Folder::finishLoading, this, [this, selectedPath]() { + selectFilePathWithDelay(selectedPath); + }); + } + } + else { + updateAcceptButtonState(); + updateSaveButtonText(false); + } + +} + +void FileDialog::selectFilePath(const FilePath &path) { + auto idx = proxyModel_->indexFromPath(path); + if(!idx.isValid()) { + return; + } + + // FIXME: add a method to Fm::FolderView to select files + + // FIXME: need to add this for detailed list + QItemSelectionModel::SelectionFlags flags = QItemSelectionModel::Select; + if(viewMode_ == FolderView::DetailedListMode) { + flags |= QItemSelectionModel::Rows; + } + QItemSelectionModel* selModel = ui->folderView->selectionModel(); + selModel->select(idx, flags); + selModel->setCurrentIndex(idx, QItemSelectionModel::Current); + QTimer::singleShot(0, this, [this, idx]() { + ui->folderView->childView()->scrollTo(idx, QAbstractItemView::PositionAtCenter); + }); +} + +void FileDialog::selectFilePathWithDelay(const FilePath &path) { + QTimer::singleShot(0, this, [this, path]() { + if(acceptMode_ == QFileDialog::AcceptSave) { + // with a save dialog, always put the base name in line-edit, regardless of selection + ui->fileName->setText(path.baseName().get()); + } + // update "accept" button because there might be no selection later + updateAcceptButtonState(); + updateSaveButtonText(false); + // try to select path + selectFilePath(path); + }); +} + +void FileDialog::selectFilesOnReload(const Fm::FileInfoList& infos) { + QObject::disconnect(lambdaConnection_); + QTimer::singleShot(0, this, [this, infos]() { + for(auto& fileInfo: infos) { + selectFilePath(fileInfo->path()); + } + }); +} + +void FileDialog::onCurrentRowChanged(const QModelIndex ¤t, const QModelIndex& /*previous*/) { + // emit currentChanged signal + QUrl currentUrl; + if(current.isValid()) { + // emit changed siangl for newly selected items + auto fi = proxyModel_->fileInfoFromIndex(current); + if(fi) { + currentUrl = QUrl::fromEncoded(fi->path().uri().get()); + } + } + Q_EMIT currentChanged(currentUrl); +} + +void FileDialog::onSelectionChanged(const QItemSelection& /*selected*/, const QItemSelection& /*deselected*/) { + auto selFiles = ui->folderView->selectedFiles(); + if(selFiles.empty()) { + updateAcceptButtonState(); + updateSaveButtonText(false); + return; + } + bool multiple(selFiles.size() > 1); + bool hasDir(false); + QString fileNames; + for(auto& fileInfo: selFiles) { + if(fileMode_ == QFileDialog::Directory) { + // if we want to select dir, ignore selected files + if(!fileInfo->isDir()) { + continue; + } + } + else if(fileInfo->isDir()) { + // if we want to select files, ignore selected dirs + hasDir = true; + continue; + } + + auto baseName = fileInfo->path().baseName(); + if(multiple) { + // support multiple selection + if(!fileNames.isEmpty()) { + fileNames += ' '; + } + fileNames += QLatin1Char('\"'); + // escape inside quotes with \ to distinguish between them + // and the quotes used for separating file names from each other + QString name(baseName.get()); + fileNames += name.replace(QLatin1String("\""), QLatin1String("\\\"")); + fileNames += QLatin1Char('\"'); + } + else { + // support single selection only + QString name(baseName.get()); + fileNames = name.replace(QLatin1String("\""), QLatin1String("\\\"")); + break; + } + } + // put the selection list in the text entry + if(!fileNames.isEmpty()) { + ui->fileName->setText(fileNames); + } + updateSaveButtonText(hasDir); + updateAcceptButtonState(); +} + +void FileDialog::onFileClicked(int type, const std::shared_ptr &file) { + bool canAccept = false; + if(file && type == FolderView::ActivatedClick) { + if(file->isDir()) { + if(fileMode_ == QFileDialog::Directory) { + ui->fileName->clear(); + } + // chdir into the activated dir + setDirectoryPath(file->path()); + } + else if(fileMode_ != QFileDialog::Directory) { + // select file(s) and a file item is activated + canAccept = true; + } + } + + if(canAccept) { + selectFilePath(file->path()); + accept(); + } +} + +void FileDialog::onNewFolder() { + createFileOrFolder(CreateNewFolder, directoryPath_, nullptr, this); +} + +void FileDialog::onViewModeToggled(bool active) { + if(active) { + auto action = static_cast(sender()); + FolderView::ViewMode newMode; + if(action == iconViewAction_) { + newMode = FolderView::IconMode; + } + else if(action == thumbnailViewAction_) { + newMode = FolderView::ThumbnailMode; + } + else if(action == compactViewAction_) { + newMode = FolderView::CompactMode; + } + else if(action == detailedViewAction_) { + newMode = FolderView::DetailedListMode; + } + else { + return; + } + setViewMode(newMode); + } +} + +void FileDialog::updateSelectionMode() { + // enable multiple selection? + ui->folderView->childView()->setSelectionMode(fileMode_ == QFileDialog::ExistingFiles ? QAbstractItemView::ExtendedSelection : QAbstractItemView::SingleSelection); +} + +void FileDialog::doAccept() { + + Q_EMIT filesSelected(selectedFiles_); + + if(selectedFiles_.size() == 1) { + Q_EMIT fileSelected(selectedFiles_[0]); + } + + QDialog::accept(); +} + +void FileDialog::onFileInfoJobFinished() { + auto job = static_cast(sender()); + if(job->isCancelled()) { + selectedFiles_.clear(); + reject(); + } + else { + QString error; + // check if the files exist and their types are correct + auto paths = job->paths(); + auto files = job->files(); + for(size_t i = 0; i < paths.size(); ++i) { + const auto& path = paths[i]; + if(i >= files.size() || files[i]->path() != path) { + // the file path is not found and does not have file info + if(fileMode_ != QFileDialog::AnyFile) { + // if we do not allow non-existent file, this is an error. + error = tr("Path \"%1\" does not exist").arg(path.displayName().get()); + break; + } + ++i; // skip the file + continue; + } + + // FIXME: currently, if a path is not found, FmFileInfoJob does not return its file info object. + // This is bad API design. We may return nullptr for the failed file info query instead. + const auto& file = files[i]; + // check if the file type is correct + if(fileMode_ == QFileDialog::Directory) { + if(!file->isDir()) { + // we're selecting dirs, but the selected file path does not point to a dir + error = tr("\"%1\" is not a directory").arg(path.displayName().get()); + break; + } + } + else if(file->isDir() || file->isShortcut()) { + // we're selecting files, but the selected file path refers to a dir or shortcut (such as computer:///) + error = tr("\"%1\" is not a file").arg(path.displayName().get());; + break; + } + } + + if(error.isEmpty()) { + // no error! + doAccept(); + } + else { + QMessageBox::critical(this, tr("Error"), error); + selectedFiles_.clear(); + } + } + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); +} + +QUrl FileDialog::directory() const { + QUrl url{directoryPath_.uri().get()}; + return url; +} + +void FileDialog::selectFile(const QUrl& filename) { + auto urlStr = filename.toEncoded(); + auto path = FilePath::fromUri(urlStr.constData()); + auto parent = path.parent(); + // chdir into file's parent if needed and select the file + setDirectoryPath(parent, path); +} + +QList FileDialog::selectedFiles() { + return selectedFiles_; +} + +void FileDialog::selectNameFilter(const QString& filter) { + if(filter != currentNameFilter_) { + currentNameFilter_ = filter; + ui->fileTypeCombo->setCurrentText(filter); + + modelFilter_.update(); + proxyModel_->invalidate(); + Q_EMIT filterSelected(filter); + } +} + +void FileDialog::selectMimeTypeFilter(const QString &filter) { + auto idx = mimeTypeFilters_.indexOf(filter); + if(idx != -1) { + ui->fileTypeCombo->setCurrentIndex(idx); + } +} + +QString FileDialog::selectedMimeTypeFilter() const { + QString filter; + auto idx = mimeTypeFilters_.indexOf(filter); + if(idx >= 0 && idx < mimeTypeFilters_.size()) { + filter = mimeTypeFilters_[idx]; + } + return filter; +} + +bool FileDialog::isSupportedUrl(const QUrl& url) { + auto scheme = url.scheme().toLocal8Bit(); + // FIXME: this is not reliable due to the bug of gvfs. + return Fm::isUriSchemeSupported(scheme.constData()); +} + + +// options + +void FileDialog::setFilter(QDir::Filters filters) { + filters_ = filters; + // TODO: +} + +void FileDialog::setViewMode(FolderView::ViewMode mode) { + viewMode_ = mode; + + // Since setModel() is called by FolderView::setViewMode(), the selectionModel will be replaced by one + // created by the view. So, we need to deal with selection changes again after setting the view mode. + disconnect(ui->folderView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &FileDialog::onCurrentRowChanged); + disconnect(ui->folderView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &FileDialog::onSelectionChanged); + + ui->folderView->setViewMode(mode); + switch(mode) { + case FolderView::IconMode: + iconViewAction_->setChecked(true); + break; + case FolderView::ThumbnailMode: + thumbnailViewAction_->setChecked(true); + break; + case FolderView::CompactMode: + compactViewAction_->setChecked(true); + break; + case FolderView::DetailedListMode: + detailedViewAction_->setChecked(true); + break; + default: + break; + } + // selection changes + connect(ui->folderView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &FileDialog::onCurrentRowChanged); + connect(ui->folderView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &FileDialog::onSelectionChanged); + // update selection mode for the view + updateSelectionMode(); +} + + +void FileDialog::setFileMode(QFileDialog::FileMode mode) { + if(mode == QFileDialog::DirectoryOnly) { + // directly only is deprecated and not allowed. + mode = QFileDialog::Directory; + } + fileMode_ = mode; + + // enable multiple selection? + updateSelectionMode(); +} + + +void FileDialog::setAcceptMode(QFileDialog::AcceptMode mode) { + acceptMode_ = mode; + // set "open/save" label if it isn't set explicitly + if(isLabelExplicitlySet(QFileDialog::Accept)) { + return; + } + if(acceptMode_ == QFileDialog::AcceptOpen) { + setLabelTextControl(QFileDialog::Accept, tr("&Open")); + } + else if(acceptMode_ == QFileDialog::AcceptSave) { + setLabelTextControl(QFileDialog::Accept, tr("&Save")); + } +} + +void FileDialog::setNameFilters(const QStringList& filters) { + if(filters.isEmpty()) { + // default filename pattern + nameFilters_ = (QStringList() << tr("All Files (*)")); + } + else { + nameFilters_ = filters; + } + ui->fileTypeCombo->clear(); + ui->fileTypeCombo->addItems(nameFilters_); +} + +void FileDialog::setMimeTypeFilters(const QStringList& filters) { + mimeTypeFilters_ = filters; + + QStringList nameFilters; + QMimeDatabase db; + for(const auto& filter: filters) { + auto mimeType = db.mimeTypeForName(filter); + auto nameFilter = mimeType.comment(); + if(!mimeType.suffixes().empty()) { + nameFilter += " ("; + const auto suffixes = mimeType.suffixes(); + for(const auto& suffix: suffixes) { + nameFilter += "*."; + nameFilter += suffix; + nameFilter += ' '; + } + nameFilter[nameFilter.length() - 1] = ')'; + } + nameFilters << nameFilter; + } + setNameFilters(nameFilters); +} + +void FileDialog::setLabelTextControl(QFileDialog::DialogLabel label, const QString& text) { + switch(label) { + case QFileDialog::LookIn: + ui->lookInLabel->setText(text); + break; + case QFileDialog::FileName: + ui->fileNameLabel->setText(text); + break; + case QFileDialog::FileType: + ui->fileTypeLabel->setText(text); + break; + case QFileDialog::Accept: + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(text); + break; + case QFileDialog::Reject: + ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(text); + break; + default: + break; + } +} + +void FileDialog::setLabelText(QFileDialog::DialogLabel label, const QString& text) { + setLabelExplicitly(label, text); + setLabelTextControl(label, text); +} + +QString FileDialog::labelText(QFileDialog::DialogLabel label) const { + QString text; + switch(label) { + case QFileDialog::LookIn: + text = ui->lookInLabel->text(); + break; + case QFileDialog::FileName: + text = ui->fileNameLabel->text(); + break; + case QFileDialog::FileType: + text = ui->fileTypeLabel->text(); + break; + case QFileDialog::Accept: + ui->buttonBox->button(QDialogButtonBox::Ok)->text(); + break; + case QFileDialog::Reject: + ui->buttonBox->button(QDialogButtonBox::Cancel)->text(); + break; + default: + break; + } + return text; +} + +void FileDialog::updateSaveButtonText(bool saveOnFolder) { + if(fileMode_ != QFileDialog::Directory + && acceptMode_ == QFileDialog::AcceptSave) { + // change save button to open button when there is a dir with the save name, + // otherwise restore it to a save button again + if(!saveOnFolder) { + QStringList parsedNames = parseNames(); + if(!parsedNames.isEmpty()) { + auto info = proxyModel_->fileInfoFromPath(directoryPath_.child(parsedNames.at(0).toLocal8Bit().constData())); + if(info && info->isDir()) { + saveOnFolder = true; + } + } + } + if(saveOnFolder) { + setLabelTextControl(QFileDialog::Accept, tr("&Open")); + } + else { + // restore save button text appropriately + if(isLabelExplicitlySet(QFileDialog::Accept)) { + setLabelTextControl(QFileDialog::Accept, explicitLabels_[QFileDialog::Accept]); + } + else { + setLabelTextControl(QFileDialog::Accept, tr("&Save")); + } + } + } +} + +void FileDialog::updateAcceptButtonState() { + bool enable(false); + if(fileMode_ != QFileDialog::Directory) { + if(acceptMode_ == QFileDialog::AcceptOpen) + { + if(firstSelectedDir()) { + // enable "open" button if a dir is selected + enable = true; + } + else { + // enable "open" button when there is a file whose name is listed + QStringList parsedNames = parseNames(); + for(auto& name: parsedNames) { + if(proxyModel_->indexFromPath(directoryPath_.child(name.toLocal8Bit().constData())).isValid()) { + enable = true; + break; + } + } + } + } + else if(acceptMode_ == QFileDialog::AcceptSave) { + // enable "save" button when there is a name or a dir selection + if(!ui->fileName->text().isEmpty()) { + enable = true; + } + else if(firstSelectedDir()) { + enable = true; + } + } + } + else if(fileMode_ == QFileDialog::Directory + && acceptMode_ != QFileDialog::AcceptSave) { + QStringList parsedNames = parseNames(); + if(parsedNames.isEmpty()) { + // in the dir mode, the current dir will be opened + // if no dir is selected and the name list is empty + enable = true; + } + else { + for(auto& name: parsedNames) { + auto info = proxyModel_->fileInfoFromPath(directoryPath_.child(name.toLocal8Bit().constData())); + if(info && info->isDir()) { + // the name of a dir is listed + enable = true; + break; + } + } + } + } + else { + enable = true; + } + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(enable); +} + +bool FileDialog::FileDialogFilter::filterAcceptsRow(const ProxyFolderModel* /*model*/, const std::shared_ptr &info) const { + if(dlg_->fileMode_ == QFileDialog::Directory) { + // we only want to select directories + if(!info->isDir()) { // not a dir + // NOTE: here we ignore dlg_->options_& QFileDialog::ShowDirsOnly option. + return false; + } + } + else { + // we want to select files, so all directories can be shown regardless of their names + if(info->isDir()) { + return true; + } + } + + bool nameMatched = false; + auto& name = info->displayName(); + for(const auto& pattern: patterns_) { +#if (QT_VERSION >= QT_VERSION_CHECK(5,12,0)) + if(name.indexOf(pattern) == 0) { +#else + if(pattern.exactMatch(name)) { +#endif + nameMatched = true; + break; + } + } + return nameMatched; +} + +void FileDialog::FileDialogFilter::update() { + // update filename patterns + patterns_.clear(); + QString nameFilter = dlg_->currentNameFilter_; + // if the filter contains (...), only get the part between the parenthesis. + auto left = nameFilter.indexOf('('); + if(left != -1) { + ++left; + auto right = nameFilter.indexOf(')', left); + if(right == -1) { + right = nameFilter.length(); + } + nameFilter = nameFilter.mid(left, right - left); + } + // parse the "*.ext1 *.ext2 *.ext3 ..." list into QRegularExpression objects + auto globs = nameFilter.simplified().split(' '); + for(const auto& glob: globs) { +#if (QT_VERSION >= QT_VERSION_CHECK(5,12,0)) + patterns_.emplace_back(QRegularExpression("\\A(?:" + + QRegularExpression::wildcardToRegularExpression(glob) + + ")\\z", QRegularExpression::CaseInsensitiveOption)); +#else + patterns_.emplace_back(QRegExp(glob, Qt::CaseInsensitive, QRegExp::Wildcard)); +#endif + } +} + +} // namespace Fm diff --git a/src/filedialog.h b/src/filedialog.h new file mode 100644 index 0000000..772b3dd --- /dev/null +++ b/src/filedialog.h @@ -0,0 +1,221 @@ +#ifndef FM_FILEDIALOG_H +#define FM_FILEDIALOG_H + +#include "libfmqtglobals.h" +#include "core/filepath.h" + +#include +#if (QT_VERSION >= QT_VERSION_CHECK(5,12,0)) +#include +#else +#include +#endif +#include +#include +#include "folderview.h" +#include "browsehistory.h" + +namespace Ui { +class FileDialog; +} + +namespace Fm { + +class CachedFolderModel; +class ProxyFolderModel; + +class LIBFM_QT_API FileDialog : public QDialog { + Q_OBJECT +public: + explicit FileDialog(QWidget *parent = 0, FilePath path = FilePath::homeDir()); + + ~FileDialog(); + + // Some QFileDialog compatible interface + void accept() override; + + void reject() override; + + QFileDialog::Options options() const { + return options_; + } + + void setOptions(QFileDialog::Options options) { + options_ = options; + } + + // interface for QPlatformFileDialogHelper + + void setDirectory(const QUrl &directory); + + QUrl directory() const; + + void selectFile(const QUrl &filename); + + QList selectedFiles(); + + void selectNameFilter(const QString &filter); + + QString selectedNameFilter() const { + return currentNameFilter_; + } + + void selectMimeTypeFilter(const QString &filter); + + QString selectedMimeTypeFilter() const; + + bool isSupportedUrl(const QUrl &url); + + // options + + // not yet supported + QDir::Filters filter() const { + return filters_; + } + // not yet supported + void setFilter(QDir::Filters filters); + + void setViewMode(FolderView::ViewMode mode); + FolderView::ViewMode viewMode() const { + return viewMode_; + } + + void setFileMode(QFileDialog::FileMode mode); + QFileDialog::FileMode fileMode() const { + return fileMode_; + } + + void setAcceptMode(QFileDialog::AcceptMode mode); + QFileDialog::AcceptMode acceptMode() const { + return acceptMode_; + } + + void setNameFilters(const QStringList &filters); + QStringList nameFilters() const { + return nameFilters_; + } + + void setMimeTypeFilters(const QStringList &filters); + QStringList mimeTypeFilters() const { + return mimeTypeFilters_; + } + + void setDefaultSuffix(const QString &suffix) { + if(!suffix.isEmpty() && suffix[0] == '.') { + // if the first char is dot, remove it. + defaultSuffix_ = suffix.mid(1); + } + else { + defaultSuffix_ = suffix; + } + } + QString defaultSuffix() const { + return defaultSuffix_; + } + + void setConfirmOverwrite(bool enabled) { + confirmOverwrite_ = enabled; + } + bool confirmOverwrite() const { + return confirmOverwrite_; + } + + void setLabelText(QFileDialog::DialogLabel label, const QString &text); + QString labelText(QFileDialog::DialogLabel label) const; + + int splitterPos() const; + void setSplitterPos(int pos); + +private Q_SLOTS: + void onCurrentRowChanged(const QModelIndex ¤t, const QModelIndex& /*previous*/); + void onSelectionChanged(const QItemSelection& /*selected*/, const QItemSelection& /*deselected*/); + void onFileClicked(int type, const std::shared_ptr& file); + void onNewFolder(); + void onViewModeToggled(bool active); + void goHome(); + +Q_SIGNALS: + // emitted when the dialog is accepted and some files are selected + void fileSelected(const QUrl &file); + void filesSelected(const QList &files); + + // emitted whenever selection changes (including no selected files) + void currentChanged(const QUrl &path); + + void directoryEntered(const QUrl &directory); + void filterSelected(const QString &filter); + +private: + + class FileDialogFilter: public ProxyFolderModelFilter { + public: + FileDialogFilter(FileDialog* dlg): dlg_{dlg} {} + virtual bool filterAcceptsRow(const ProxyFolderModel* /*model*/, const std::shared_ptr& info) const override; + void update(); + + FileDialog* dlg_; +#if (QT_VERSION >= QT_VERSION_CHECK(5,12,0)) + std::vector patterns_; +#else + std::vector patterns_; +#endif + }; + + bool isLabelExplicitlySet(QFileDialog::DialogLabel label) const { + return !explicitLabels_[label].isEmpty(); + } + void setLabelExplicitly(QFileDialog::DialogLabel label, const QString& text) { + explicitLabels_[label] = text; + } + void setLabelTextControl(QFileDialog::DialogLabel label, const QString &text); + void updateSaveButtonText(bool saveOnFolder); + void updateAcceptButtonState(); + + std::shared_ptr firstSelectedDir() const; + void selectFilePath(const FilePath& path); + void selectFilePathWithDelay(const FilePath& path); + void selectFilesOnReload(const Fm::FileInfoList& infos); + void setDirectoryPath(FilePath directory, FilePath selectedPath = FilePath(), bool addHistory = true); + void updateSelectionMode(); + void doAccept(); + void onFileInfoJobFinished(); + void freeFolder(); + QStringList parseNames() const; + +private: + std::unique_ptr ui; + CachedFolderModel* folderModel_; + ProxyFolderModel* proxyModel_; + FilePath directoryPath_; + std::shared_ptr folder_; + Fm::BrowseHistory history_; + + QFileDialog::Options options_; + QDir::Filters filters_; + FolderView::ViewMode viewMode_; + QFileDialog::FileMode fileMode_; + QFileDialog::AcceptMode acceptMode_; + bool confirmOverwrite_; + QStringList nameFilters_; + QStringList mimeTypeFilters_; + QString defaultSuffix_; + FileDialogFilter modelFilter_; + QString currentNameFilter_; + QList selectedFiles_; + // view modes: + QAction* iconViewAction_; + QAction* thumbnailViewAction_; + QAction* compactViewAction_; + QAction* detailedViewAction_; + // back and forward buttons: + QAction* backAction_; + QAction* forwardAction_; + // dialog labels that can be set explicitly: + QString explicitLabels_[5]; + // needed for disconnecting Fm::Folder signal from lambda: + QMetaObject::Connection lambdaConnection_; +}; + + +} // namespace Fm +#endif // FM_FILEDIALOG_H diff --git a/src/filedialog.ui b/src/filedialog.ui new file mode 100644 index 0000000..cc7d65e --- /dev/null +++ b/src/filedialog.ui @@ -0,0 +1,151 @@ + + + FileDialog + + + + 0 + 0 + 700 + 500 + + + + + + + + + + + + Location: + + + + + + + + + + + + Qt::Horizontal + + + + + 0 + 0 + + + + + 120 + 0 + + + + + + + 1 + 0 + + + + + + + + + + + File name: + + + + + + + + + + File type: + + + + + + + + + + Qt::Vertical + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + Fm::SidePane + QWidget +
sidepane.h
+ 1 +
+ + Fm::FolderView + QWidget +
folderview.h
+ 1 +
+ + Fm::PathBar + QWidget +
pathbar.h
+ 1 +
+
+ + + + buttonBox + accepted() + FileDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + FileDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + +
diff --git a/src/filedialoghelper.cpp b/src/filedialoghelper.cpp new file mode 100644 index 0000000..310015f --- /dev/null +++ b/src/filedialoghelper.cpp @@ -0,0 +1,290 @@ +#include "filedialoghelper.h" + +#include "libfmqt.h" +#include "filedialog.h" + +#include +#include +#include +#include +#include + +#include + +namespace Fm { + +inline static const QString viewModeToString(Fm::FolderView::ViewMode value); +inline static Fm::FolderView::ViewMode viewModeFromString(const QString& str); + +FileDialogHelper::FileDialogHelper() { + // can only be used after libfm-qt initialization + dlg_ = std::unique_ptr(new Fm::FileDialog()); + connect(dlg_.get(), &Fm::FileDialog::accepted, [this]() { + saveSettings(); + accept(); + }); + connect(dlg_.get(), &Fm::FileDialog::rejected, [this]() { + saveSettings(); + reject(); + }); + + connect(dlg_.get(), &Fm::FileDialog::fileSelected, this, &FileDialogHelper::fileSelected); + connect(dlg_.get(), &Fm::FileDialog::filesSelected, this, &FileDialogHelper::filesSelected); + connect(dlg_.get(), &Fm::FileDialog::currentChanged, this, &FileDialogHelper::currentChanged); + connect(dlg_.get(), &Fm::FileDialog::directoryEntered, this, &FileDialogHelper::directoryEntered); + connect(dlg_.get(), &Fm::FileDialog::filterSelected, this, &FileDialogHelper::filterSelected); +} + +FileDialogHelper::~FileDialogHelper() { +} + +void FileDialogHelper::exec() { + dlg_->exec(); +} + +bool FileDialogHelper::show(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow* parent) { + dlg_->setAttribute(Qt::WA_NativeWindow, true); // without this, sometimes windowHandle() will return nullptr + + dlg_->setWindowFlags(windowFlags); + dlg_->setWindowModality(windowModality); + + // Reference: KDE implementation + // https://github.com/KDE/plasma-integration/blob/master/src/platformtheme/kdeplatformfiledialoghelper.cpp + dlg_->windowHandle()->setTransientParent(parent); + + applyOptions(); + + loadSettings(); + // central positioning with respect to the parent window + if(parent && parent->isVisible()) { + dlg_->move(parent->x() + (parent->width() - dlg_->width()) / 2, + parent->y() + (parent->height() - dlg_->height()) / 2); + } + + // NOTE: the timer here is required as a workaround borrowed from KDE. Without this, the dialog UI will be blocked. + // QFileDialog calls our platform plugin to show our own native file dialog instead of showing its widget. + // However, it still creates a hidden dialog internally, and then make it modal. + // So user input from all other windows that are not the children of the QFileDialog widget will be blocked. + // This includes our own dialog. After the return of this show() method, QFileDialog creates its own window and + // then make it modal, which blocks our UI. The timer schedule a delayed popup of our file dialog, so we can + // show again after QFileDialog and override the modal state. Then our UI can be unblocked. + QTimer::singleShot(0, dlg_.get(), &QDialog::show); + dlg_->setFocus(); + return true; +} + +void FileDialogHelper::hide() { + dlg_->hide(); +} + +bool FileDialogHelper::defaultNameFilterDisables() const { + return false; +} + +void FileDialogHelper::setDirectory(const QUrl& directory) { + dlg_->setDirectory(directory); +} + +QUrl FileDialogHelper::directory() const { + return dlg_->directory(); +} + +void FileDialogHelper::selectFile(const QUrl& filename) { + dlg_->selectFile(filename); +} + +QList FileDialogHelper::selectedFiles() const { + return dlg_->selectedFiles(); +} + +void FileDialogHelper::setFilter() { + // FIXME: what's this? + // The gtk+ 3 file dialog helper in Qt5 update options in this method. + applyOptions(); +} + +void FileDialogHelper::selectNameFilter(const QString& filter) { + dlg_->selectNameFilter(filter); +} + +#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0) +QString FileDialogHelper::selectedMimeTypeFilter() const { + return dlg_->selectedMimeTypeFilter(); +} + +void FileDialogHelper::selectMimeTypeFilter(const QString& filter) { + dlg_->selectMimeTypeFilter(filter); +} +#endif + +QString FileDialogHelper::selectedNameFilter() const { + return dlg_->selectedNameFilter(); +} + +bool FileDialogHelper::isSupportedUrl(const QUrl& url) const { + return dlg_->isSupportedUrl(url); +} + +void FileDialogHelper::applyOptions() { + auto& opt = options(); + + // set title + if(opt->windowTitle().isEmpty()) { + dlg_->setWindowTitle(opt->acceptMode() == QFileDialogOptions::AcceptOpen ? tr("Open File") + : tr("Save File")); + } + else { + dlg_->setWindowTitle(opt->windowTitle()); + } + + dlg_->setFilter(opt->filter()); + dlg_->setFileMode(QFileDialog::FileMode(opt->fileMode())); + dlg_->setAcceptMode(QFileDialog::AcceptMode(opt->acceptMode())); // also sets a default label for accept button + // bool useDefaultNameFilters() const; + dlg_->setNameFilters(opt->nameFilters()); + if(!opt->mimeTypeFilters().empty()) { + dlg_->setMimeTypeFilters(opt->mimeTypeFilters()); + } + + dlg_->setDefaultSuffix(opt->defaultSuffix()); + // QStringList history() const; + + // explicitly set labels + for(int i = 0; i < QFileDialogOptions::DialogLabelCount; ++i) { + auto label = static_cast(i); + if(opt->isLabelExplicitlySet(label)) { + dlg_->setLabelText(static_cast(label), opt->labelText(label)); + } + } + + auto url = opt->initialDirectory(); + if(url.isValid()) { + dlg_->setDirectory(url); + } + + +#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0) + auto filter = opt->initiallySelectedMimeTypeFilter(); + if(!filter.isEmpty()) { + selectMimeTypeFilter(filter); + } + else { + filter = opt->initiallySelectedNameFilter(); + if(!filter.isEmpty()) { + selectNameFilter(opt->initiallySelectedNameFilter()); + } + } +#else + auto filter = opt->initiallySelectedNameFilter(); + if(!filter.isEmpty()) { + selectNameFilter(filter); + } +#endif + + auto selectedFiles = opt->initiallySelectedFiles(); + for(const auto& selectedFile: selectedFiles) { + selectFile(selectedFile); + } + // QStringList supportedSchemes() const; +} + +static const QString viewModeToString(Fm::FolderView::ViewMode value) { + QString ret; + switch(value) { + case Fm::FolderView::DetailedListMode: + default: + ret = QLatin1String("Detailed"); + break; + case Fm::FolderView::CompactMode: + ret = QLatin1String("Compact"); + break; + case Fm::FolderView::IconMode: + ret = QLatin1String("Icon"); + break; + case Fm::FolderView::ThumbnailMode: + ret = QLatin1String("Thumbnail"); + break; + } + return ret; +} + +Fm::FolderView::ViewMode viewModeFromString(const QString& str) { + Fm::FolderView::ViewMode ret; + if(str == QLatin1String("Detailed")) { + ret = Fm::FolderView::DetailedListMode; + } + else if(str == QLatin1String("Compact")) { + ret = Fm::FolderView::CompactMode; + } + else if(str == QLatin1String("Icon")) { + ret = Fm::FolderView::IconMode; + } + else if(str == QLatin1String("Thumbnail")) { + ret = Fm::FolderView::ThumbnailMode; + } + else { + ret = Fm::FolderView::DetailedListMode; + } + return ret; +} + +void FileDialogHelper::loadSettings() { + QSettings settings(QSettings::UserScope, "lxqt", "filedialog"); + settings.beginGroup ("Sizes"); + dlg_->resize(settings.value("WindowSize", QSize(700, 500)).toSize()); + dlg_->setSplitterPos(settings.value("SplitterPos", 200).toInt()); + settings.endGroup(); + + settings.beginGroup ("View"); + dlg_->setViewMode(viewModeFromString(settings.value("Mode", "Detailed").toString())); + settings.endGroup(); +} + +void FileDialogHelper::saveSettings() { + QSettings settings(QSettings::UserScope, "lxqt", "filedialog"); + settings.beginGroup ("Sizes"); + QSize windowSize = dlg_->size(); + if(settings.value("WindowSize") != windowSize) { // no redundant write + settings.setValue("WindowSize", windowSize); + } + int splitterPos = dlg_->splitterPos(); + if(settings.value("SplitterPos") != splitterPos) { + settings.setValue("SplitterPos", splitterPos); + } + settings.endGroup(); + + settings.beginGroup ("View"); + QString mode = viewModeToString(dlg_->viewMode()); + if(settings.value("Mode") != mode) { + settings.setValue("Mode", mode); + } + settings.endGroup(); +} + +/* +FileDialogPlugin::FileDialogPlugin() { + +} + +QPlatformFileDialogHelper *FileDialogPlugin::createHelper() { + return new FileDialogHelper(); +} +*/ + +} // namespace Fm + + +QPlatformFileDialogHelper *createFileDialogHelper() { + // When a process has this environment set, that means glib event loop integration is disabled. + // In this case, libfm just won't work. So let's disable the file dialog helper and return nullptr. + if(qgetenv("QT_NO_GLIB") == "1") { + return nullptr; + } + + static std::unique_ptr libfmQtContext_; + if(!libfmQtContext_) { + // initialize libfm-qt only once + libfmQtContext_ = std::unique_ptr{new Fm::LibFmQt()}; + } + return new Fm::FileDialogHelper{}; +} diff --git a/src/filedialoghelper.h b/src/filedialoghelper.h new file mode 100644 index 0000000..af5c87b --- /dev/null +++ b/src/filedialoghelper.h @@ -0,0 +1,62 @@ +#ifndef FILEDIALOGHELPER_H +#define FILEDIALOGHELPER_H + +#include "libfmqtglobals.h" +#include // this private header is subject to changes +#include + +namespace Fm { + +class FileDialog; + +class LIBFM_QT_API FileDialogHelper : public QPlatformFileDialogHelper { + Q_OBJECT + +public: + FileDialogHelper(); + + virtual ~FileDialogHelper(); + + // QPlatformDialogHelper + void exec() override; + bool show(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent) override; + void hide() override; + + // QPlatformFileDialogHelper + bool defaultNameFilterDisables() const override; + void setDirectory(const QUrl &directory) override; + QUrl directory() const override; + void selectFile(const QUrl &filename) override; + QList selectedFiles() const override; + void setFilter() override; + void selectNameFilter(const QString &filter) override; +#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0) + QString selectedMimeTypeFilter() const override; + void selectMimeTypeFilter(const QString &filter) override; +#endif + QString selectedNameFilter() const override; + + bool isSupportedUrl(const QUrl &url) const override; + +private: + void applyOptions(); + void loadSettings(); + void saveSettings(); + +private: + std::unique_ptr dlg_; +}; + +} // namespace Fm + +// export a C API without C++ name mangling so others can dynamically load libfm-qt at runtime +// to call this API and get a new QPlatformFileDialogHelper object. + +extern "C" { + +// if the process calling this API fail to load libfm-qt, nullptr will be returned instead. +LIBFM_QT_API QPlatformFileDialogHelper* createFileDialogHelper(); + +} + +#endif // FILEDIALOGHELPER_H diff --git a/src/filelauncher.cpp b/src/filelauncher.cpp new file mode 100644 index 0000000..ff14533 --- /dev/null +++ b/src/filelauncher.cpp @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "filelauncher.h" +#include "applaunchcontext.h" +#include +#include +#include "execfiledialog_p.h" +#include "appchooserdialog.h" +#include "utilities.h" + +#include "core/fileinfojob.h" +#include "mountoperation.h" + +namespace Fm { + +FileLauncher::FileLauncher() { +} + +FileLauncher::~FileLauncher() { +} + + +bool FileLauncher::launchFiles(QWidget* parent, const FileInfoList &file_infos) { + GObjectPtr context{fm_app_launch_context_new_for_widget(parent), false}; + bool ret = BasicFileLauncher::launchFiles(file_infos, G_APP_LAUNCH_CONTEXT(context.get())); + return ret; +} + +bool FileLauncher::launchPaths(QWidget* parent, const FilePathList& paths) { + GObjectPtr context{fm_app_launch_context_new_for_widget(parent), false}; + bool ret = BasicFileLauncher::launchPaths(paths, G_APP_LAUNCH_CONTEXT(context.get())); + return ret; +} + +int FileLauncher::ask(const char* /*msg*/, char* const* /*btn_labels*/, int /*default_btn*/) { + /* FIXME: set default button properly */ + // return fm_askv(data->parent, nullptr, msg, btn_labels); + return -1; +} + +GAppInfoPtr FileLauncher::chooseApp(const FileInfoList& /*fileInfos*/, const char *mimeType, GErrorPtr& /*err*/) { + AppChooserDialog dlg(nullptr); + GAppInfoPtr app; + if(mimeType) { + dlg.setMimeType(Fm::MimeType::fromName(mimeType)); + } + else { + dlg.setCanSetDefault(false); + } + // FIXME: show error properly? + if(execModelessDialog(&dlg) == QDialog::Accepted) { + app = dlg.selectedApp(); + } + return app; +} + +bool FileLauncher::openFolder(GAppLaunchContext *ctx, const FileInfoList &folderInfos, GErrorPtr &err) { + return BasicFileLauncher::openFolder(ctx, folderInfos, err); +} + +bool FileLauncher::showError(GAppLaunchContext* /*ctx*/, const GErrorPtr &err, const FilePath &path, const FileInfoPtr &info) { + /* ask for mount if trying to launch unmounted path */ + if(err->domain == G_IO_ERROR) { + if(path && err->code == G_IO_ERROR_NOT_MOUNTED) { + MountOperation* op = new MountOperation(true); + op->setAutoDestroy(true); + if(info && info->isMountable()) { + // this is a mountable shortcut (such as computer:///xxxx.drive) + op->mountMountable(path); + } + else { + op->mountEnclosingVolume(path); + } + if(op->wait()) { + // if the mount operation succeeds, we can ignore the error and continue + return true; + } + } + 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 false; +} + +BasicFileLauncher::ExecAction FileLauncher::askExecFile(const FileInfoPtr &file) { + auto res = BasicFileLauncher::ExecAction::CANCEL; + ExecFileDialog dlg(*file); + if(execModelessDialog(&dlg) == QDialog::Accepted) { + res = dlg.result(); + } + return res; +} + + +} // namespace Fm diff --git a/src/filelauncher.h b/src/filelauncher.h new file mode 100644 index 0000000..991a00a --- /dev/null +++ b/src/filelauncher.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_FILELAUNCHER_H +#define FM_FILELAUNCHER_H + +#include "libfmqtglobals.h" +#include +#include "core/fileinfo.h" +#include "core/basicfilelauncher.h" + +namespace Fm { + +class LIBFM_QT_API FileLauncher: public BasicFileLauncher { +public: + explicit FileLauncher(); + virtual ~FileLauncher(); + + bool launchFiles(QWidget* parent, const FileInfoList& file_infos); + + bool launchPaths(QWidget* parent, const FilePathList &paths); + +protected: + + GAppInfoPtr chooseApp(const FileInfoList& fileInfos, const char* mimeType, GErrorPtr& err) override; + + bool openFolder(GAppLaunchContext* ctx, const FileInfoList& folderInfos, GErrorPtr& err) override; + + bool showError(GAppLaunchContext* ctx, const GErrorPtr &err, const FilePath& path = FilePath{}, const FileInfoPtr& info = FileInfoPtr{}) override; + + ExecAction askExecFile(const FileInfoPtr& file) override; + + int ask(const char* msg, char* const* btn_labels, int default_btn) override; +}; + +} + +#endif // FM_FILELAUNCHER_H diff --git a/src/filemenu.cpp b/src/filemenu.cpp new file mode 100644 index 0000000..383159d --- /dev/null +++ b/src/filemenu.cpp @@ -0,0 +1,434 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "filemenu.h" +#include "createnewmenu.h" +#include "filepropsdialog.h" +#include "utilities.h" +#include "fileoperation.h" +#include "filelauncher.h" +#include "appchooserdialog.h" + +#include "customactions/fileaction.h" +#include "customaction_p.h" + +#include +#include +#include +#include "filemenu_p.h" + +#include "core/archiver.h" + +#include "core/legacy/fm-app-info.h" + + +namespace Fm { + +FileMenu::FileMenu(Fm::FileInfoList files, std::shared_ptr info, Fm::FilePath cwd, bool isWritableDir, const QString& title, QWidget* parent): + QMenu(title, parent), + files_{std::move(files)}, + info_{std::move(info)}, + cwd_{std::move(cwd)}, + unTrashAction_(nullptr), + fileLauncher_(nullptr) { + + useTrash_ = true; + confirmDelete_ = true; + confirmTrash_ = false; // Confirm before moving files into "trash can" + + openAction_ = nullptr; + openWithMenuAction_ = nullptr; + openWithAction_ = nullptr; + separator1_ = nullptr; + cutAction_ = nullptr; + copyAction_ = nullptr; + pasteAction_ = nullptr; + deleteAction_ = nullptr; + unTrashAction_ = nullptr; + renameAction_ = nullptr; + separator2_ = nullptr; + propertiesAction_ = nullptr; + + auto mime_type = info_->mimeType(); + Fm::FilePath path = info_->path(); + + // check if the files are of the same type + sameType_ = files_.isSameType(); + // check if the files are on the same filesystem + sameFilesystem_ = files_.isSameFilesystem(); + // check if the files are all virtual + + // FIXME: allVirtual_ = sameFilesystem_ && fm_path_is_virtual(path); + allVirtual_ = false; + + // check if the files are all in the trash can + allTrash_ = sameFilesystem_ && path.hasUriScheme("trash"); + + openAction_ = new QAction(QIcon::fromTheme("document-open"), tr("Open"), this); + connect(openAction_, &QAction::triggered, this, &FileMenu::onOpenTriggered); + addAction(openAction_); + + openWithMenuAction_ = new QAction(tr("Open With..."), this); + addAction(openWithMenuAction_); + // create the "Open with..." sub menu + QMenu* menu = new QMenu(this); + openWithMenuAction_->setMenu(menu); + + if(sameType_) { /* add specific menu items for this mime type */ + if(mime_type && !allVirtual_) { /* the file has a valid mime-type and its not virtual */ + GList* apps = g_app_info_get_all_for_type(mime_type->name()); + GList* l; + for(l = apps; l; l = l->next) { + Fm::GAppInfoPtr app{G_APP_INFO(l->data), false}; + // check if the command really exists + gchar* program_path = g_find_program_in_path(g_app_info_get_executable(app.get())); + if(!program_path) { + continue; + } + g_free(program_path); + + // create a QAction for the application. + AppInfoAction* action = new AppInfoAction(std::move(app), menu); + connect(action, &QAction::triggered, this, &FileMenu::onApplicationTriggered); + menu->addAction(action); + } + g_list_free(apps); + } + } + menu->addSeparator(); + openWithAction_ = new QAction(tr("Other Applications"), this); + connect(openWithAction_, &QAction::triggered, this, &FileMenu::onOpenWithTriggered); + menu->addAction(openWithAction_); + + separator1_ = addSeparator(); + + createAction_ = new QAction(tr("Create &New"), this); + Fm::FilePath dirPath = files_.size() == 1 && info_->isDir() ? path : cwd_; + createAction_->setMenu(new CreateNewMenu(parent, dirPath, this)); + addAction(createAction_); + + separator2_ = addSeparator(); + + if(allTrash_) { // all selected files are in trash:/// + bool can_restore = true; + /* only immediate children of trash:/// can be restored. */ + auto trash_root = Fm::FilePath::fromUri("trash:///"); + for(auto& file: files_) { + Fm::FilePath trash_path = file->path(); + if(!trash_root.isParentOf(trash_path)) { + can_restore = false; + break; + } + } + if(can_restore) { + unTrashAction_ = new QAction(tr("&Restore"), this); + connect(unTrashAction_, &QAction::triggered, this, &FileMenu::onUnTrashTriggered); + addAction(unTrashAction_); + } + } + else { // ordinary files + cutAction_ = new QAction(QIcon::fromTheme("edit-cut"), tr("Cut"), this); + connect(cutAction_, &QAction::triggered, this, &FileMenu::onCutTriggered); + addAction(cutAction_); + + copyAction_ = new QAction(QIcon::fromTheme("edit-copy"), tr("Copy"), this); + connect(copyAction_, &QAction::triggered, this, &FileMenu::onCopyTriggered); + addAction(copyAction_); + + pasteAction_ = new QAction(QIcon::fromTheme("edit-paste"), tr("Paste"), this); + connect(pasteAction_, &QAction::triggered, this, &FileMenu::onPasteTriggered); + addAction(pasteAction_); + + deleteAction_ = new QAction(QIcon::fromTheme("user-trash"), tr("&Move to Trash"), this); + connect(deleteAction_, &QAction::triggered, this, &FileMenu::onDeleteTriggered); + addAction(deleteAction_); + + renameAction_ = new QAction(tr("Rename"), this); + connect(renameAction_, &QAction::triggered, this, &FileMenu::onRenameTriggered); + addAction(renameAction_); + + // disable actons that can't be used + bool hasAccessible(false); + bool hasDeletable(false); + bool hasRenamable(false); + for(auto& file: files_) { + if(file->isAccessible()) { + hasAccessible = true; + } + if(file->isDeletable()) { + hasDeletable = true; + } + if(file->canSetName()) { + hasRenamable = true; + } + if (hasAccessible && hasDeletable && hasRenamable) { + break; + } + } + copyAction_->setEnabled(hasAccessible); + cutAction_->setEnabled(hasDeletable); + deleteAction_->setEnabled(hasDeletable); + renameAction_->setEnabled(hasRenamable); + if(!(sameType_ && info_->isDir() + && (files_.size() > 1 ? isWritableDir : info_->isWritable()))) { + pasteAction_->setEnabled(false); + } + } + + // DES-EMA custom actions integration + // FIXME: port these parts to Fm API + auto custom_actions = FileActionItem::get_actions_for_files(files_); + for(auto& item: custom_actions) { + if(item && !(item->get_target() & FILE_ACTION_TARGET_CONTEXT)) { + continue; // this item is not for context menu + } + if(item == custom_actions.front() && !item->is_action()) { + addSeparator(); // before all custom actions + } + addCustomActionItem(this, item); + } + + // archiver integration + // FIXME: we need to modify upstream libfm to include some Qt-based archiver programs. + if(!allVirtual_) { + auto archiver = Archiver::defaultArchiver(); + if(archiver) { + if(sameType_ && archiver->isMimeTypeSupported(mime_type->name())) { + QAction* archiverSeparator = nullptr; + if(cwd_ && archiver->canExtractArchivesTo()) { + archiverSeparator = addSeparator(); + QAction* action = new QAction(tr("Extract to..."), this); + connect(action, &QAction::triggered, this, &FileMenu::onExtract); + addAction(action); + } + if(archiver->canExtractArchives()) { + if(!archiverSeparator) { + addSeparator(); + } + QAction* action = new QAction(tr("Extract Here"), this); + connect(action, &QAction::triggered, this, &FileMenu::onExtractHere); + addAction(action); + } + } + else if(archiver->canCreateArchive()){ + addSeparator(); + QAction* action = new QAction(tr("Compress"), this); + connect(action, &QAction::triggered, this, &FileMenu::onCompress); + addAction(action); + } + } + } + + separator3_ = addSeparator(); + + propertiesAction_ = new QAction(QIcon::fromTheme("document-properties"), tr("Properties"), this); + connect(propertiesAction_, &QAction::triggered, this, &FileMenu::onFilePropertiesTriggered); + addAction(propertiesAction_); +} + +FileMenu::~FileMenu() { +} + +void FileMenu::addTrustAction() { + if(info_->isExecutableType()) { + QAction* trustAction = new QAction(files_.size() > 1 + ? tr("Trust selected executables") + : tr("Trust this executable"), + this); + trustAction->setCheckable(true); + trustAction->setChecked(info_->isTrustable()); + connect(trustAction, &QAction::toggled, this, &FileMenu::onTrustToggled); + insertAction(propertiesAction_, trustAction); + } +} + +void FileMenu::addCustomActionItem(QMenu* menu, std::shared_ptr item) { + if(!item) { // separator + addSeparator(); + return; + } + + // this action is not for context menu + if(item->is_action() && !(item->get_target() & FILE_ACTION_TARGET_CONTEXT)) { + return; + } + + CustomAction* action = new CustomAction(item, menu); + menu->addAction(action); + if(item->is_menu()) { + auto& subitems = item->get_sub_items(); + if(!subitems.empty()) { + QMenu* submenu = new QMenu(menu); + for(auto& subitem: subitems) { + addCustomActionItem(submenu, subitem); + } + action->setMenu(submenu); + } + } + else if(item->is_action()) { + connect(action, &QAction::triggered, this, &FileMenu::onCustomActionTrigerred); + } +} + +void FileMenu::onOpenTriggered() { + if(fileLauncher_) { + fileLauncher_->launchFiles(nullptr, files_); + } + else { // use the default launcher + Fm::FileLauncher launcher; + launcher.launchFiles(nullptr, files_); + } +} + +void FileMenu::onOpenWithTriggered() { + AppChooserDialog dlg(nullptr); + if(sameType_) { + dlg.setMimeType(info_->mimeType()); + } + else { // we can only set the selected app as default if all files are of the same type + dlg.setCanSetDefault(false); + } + + if(execModelessDialog(&dlg) == QDialog::Accepted) { + auto app = dlg.selectedApp(); + if(app) { + openFilesWithApp(app.get()); + } + } +} + +void FileMenu::openFilesWithApp(GAppInfo* app) { + GList* uris = nullptr; + for(auto& file: files_) { + auto uri = file->path().uri(); + uris = g_list_prepend(uris, uri.release()); + } + uris = g_list_reverse(uris); // respect the original order + fm_app_info_launch_uris(app, uris, nullptr, nullptr); + g_list_foreach(uris, (GFunc)g_free, nullptr); + g_list_free(uris); +} + +void FileMenu::onApplicationTriggered() { + AppInfoAction* action = static_cast(sender()); + openFilesWithApp(action->appInfo().get()); +} + +void FileMenu::onCustomActionTrigerred() { + CustomAction* action = static_cast(sender()); + auto& item = action->item(); + /* g_debug("item: %s is activated, id:%s", fm_file_action_item_get_name(item), + fm_file_action_item_get_id(item)); */ + CStrPtr output; + item->launch(nullptr, files_, output); + if(output) { + QMessageBox::information(this, tr("Output"), output.get()); + } +} + +void FileMenu::onTrustToggled(bool checked) { + for(auto& file: files_) { + file->setTrustable(checked); + } +} + +void FileMenu::onFilePropertiesTriggered() { + FilePropsDialog::showForFiles(files_); +} + +void FileMenu::onCopyTriggered() { + Fm::copyFilesToClipboard(files_.paths()); +} + +void FileMenu::onCutTriggered() { + Fm::cutFilesToClipboard(files_.paths()); +} + +void FileMenu::onDeleteTriggered() { + auto paths = files_.paths(); + if(useTrash_) { + FileOperation::trashFiles(paths, confirmTrash_, parentWidget()); + } + else { + FileOperation::deleteFiles(paths, confirmDelete_, parentWidget()); + } +} + +void FileMenu::onUnTrashTriggered() { + FileOperation::unTrashFiles(files_.paths(), parentWidget()); +} + +void FileMenu::onPasteTriggered() { + Fm::pasteFilesFromClipboard(cwd_); +} + +void FileMenu::onRenameTriggered() { + // if there is a view and this is a single file, just edit the current index + if(files_.size() == 1) { + if (QAbstractItemView* view = qobject_cast(parentWidget())) { + QModelIndexList selIndexes = view->selectionModel()->selectedIndexes(); + if(selIndexes.size() > 1) { // in the detailed list mode, only the first index is editable + view->setCurrentIndex(selIndexes.at(0)); + } + if (view->currentIndex().isValid()) { + view->edit(view->currentIndex()); + return; + } + } + } + for(auto& info: files_) { + if(!Fm::renameFile(info, nullptr)) { + break; + } + } +} + +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() { + auto archiver = Archiver::defaultArchiver(); + if(archiver) { + archiver->createArchive(nullptr, files_.paths()); + } +} + +void FileMenu::onExtract() { + auto archiver = Archiver::defaultArchiver(); + if(archiver) { + archiver->extractArchives(nullptr, files_.paths()); + } +} + +void FileMenu::onExtractHere() { + auto archiver = Archiver::defaultArchiver(); + if(archiver) { + archiver->extractArchivesTo(nullptr, files_.paths(), cwd_); + } +} + +} // namespace Fm diff --git a/src/filemenu.h b/src/filemenu.h new file mode 100644 index 0000000..24c49ed --- /dev/null +++ b/src/filemenu.h @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_FILEMENU_H +#define FM_FILEMENU_H + +#include "libfmqtglobals.h" +#include +#include +#include "core/fileinfo.h" + +class QAction; + +namespace Fm { + +class FileLauncher; +class FileActionItem; + +class LIBFM_QT_API FileMenu : public QMenu { + Q_OBJECT + +public: + explicit FileMenu(Fm::FileInfoList files, std::shared_ptr info, Fm::FilePath cwd, bool isWritableDir = true, const QString& title = QString(), QWidget* parent = nullptr); + ~FileMenu(); + + void addTrustAction(); + + bool useTrash() { + return useTrash_; + } + + void setUseTrash(bool trash); + + bool confirmDelete() { + return confirmDelete_; + } + + void setConfirmDelete(bool confirm) { + confirmDelete_ = confirm; + } + + QAction* openAction() { + return openAction_; + } + + QAction* openWithMenuAction() { + return openWithMenuAction_; + } + + QAction* openWithAction() { + return openWithAction_; + } + + QAction* separator1() { + return separator1_; + } + + QAction* createAction() { + return createAction_; + } + + QAction* separator2() { + return separator2_; + } + + QAction* cutAction() { + return cutAction_; + } + + QAction* copyAction() { + return copyAction_; + } + + QAction* pasteAction() { + return pasteAction_; + } + + QAction* deleteAction() { + return deleteAction_; + } + + QAction* unTrashAction() { + return unTrashAction_; + } + + QAction* renameAction() { + return renameAction_; + } + + QAction* separator3() { + return separator3_; + } + + QAction* propertiesAction() { + return propertiesAction_; + } + + const Fm::FileInfoList& files() const { + return files_; + } + + const std::shared_ptr& firstFile() const { + return info_; + } + + const Fm::FilePath& cwd() const { + return cwd_; + } + + void setFileLauncher(FileLauncher* launcher) { + fileLauncher_ = launcher; + } + + FileLauncher* fileLauncher() { + return fileLauncher_; + } + + bool sameType() const { + return sameType_; + } + + bool sameFilesystem() const { + return sameFilesystem_; + } + + bool allVirtual() const { + return allVirtual_; + } + + bool allTrash() const { + return allTrash_; + } + + bool confirmTrash() const { + return confirmTrash_; + } + + void setConfirmTrash(bool value) { + confirmTrash_ = value; + } + +protected: + void addCustomActionItem(QMenu* menu, std::shared_ptr item); + void openFilesWithApp(GAppInfo* app); + +protected Q_SLOTS: + void onOpenTriggered(); + void onOpenWithTriggered(); + void onTrustToggled(bool checked); + void onFilePropertiesTriggered(); + void onApplicationTriggered(); + void onCustomActionTrigerred(); + void onCompress(); + void onExtract(); + void onExtractHere(); + + void onCutTriggered(); + void onCopyTriggered(); + void onPasteTriggered(); + void onRenameTriggered(); + void onDeleteTriggered(); + void onUnTrashTriggered(); + +private: + Fm::FileInfoList files_; + std::shared_ptr info_; + Fm::FilePath cwd_; + bool useTrash_; + bool confirmDelete_; + bool confirmTrash_; // Confirm before moving files into "trash can" + + bool sameType_; + bool sameFilesystem_; + bool allVirtual_; + bool allTrash_; + + QAction* openAction_; + QAction* openWithMenuAction_; + QAction* openWithAction_; + QAction* separator1_; + QAction* createAction_; + QAction* separator2_; + QAction* cutAction_; + QAction* copyAction_; + QAction* pasteAction_; + QAction* deleteAction_; + QAction* unTrashAction_; + QAction* renameAction_; + QAction* separator3_; + QAction* propertiesAction_; + + FileLauncher* fileLauncher_; +}; + +} + +#endif // FM_FILEMENU_H diff --git a/src/filemenu_p.h b/src/filemenu_p.h new file mode 100644 index 0000000..9cbd6ee --- /dev/null +++ b/src/filemenu_p.h @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_FILEMENU_P_H +#define FM_FILEMENU_P_H + +#include +#include "core/gioptrs.h" +#include "core/iconinfo.h" + +namespace Fm { + +class AppInfoAction : public QAction { + Q_OBJECT +public: + explicit AppInfoAction(Fm::GAppInfoPtr app, QObject* parent = 0): + QAction(QString::fromUtf8(g_app_info_get_name(app.get())), parent), + appInfo_{std::move(app)} { + setToolTip(QString::fromUtf8(g_app_info_get_description(appInfo_.get()))); + GIcon* gicon = g_app_info_get_icon(appInfo_.get()); + const auto icnInfo = Fm::IconInfo::fromGIcon(gicon); + if(icnInfo) { + setIcon(icnInfo->qicon()); + } + } + + virtual ~AppInfoAction() { + } + + const Fm::GAppInfoPtr& appInfo() const { + return appInfo_; + } + +private: + Fm::GAppInfoPtr appInfo_; +}; + +} // namespace Fm + +#endif diff --git a/src/fileoperation.cpp b/src/fileoperation.cpp new file mode 100644 index 0000000..5db818b --- /dev/null +++ b/src/fileoperation.cpp @@ -0,0 +1,425 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "fileoperation.h" +#include "fileoperationdialog.h" +#include +#include +#include +#include + +#include "core/deletejob.h" +#include "core/trashjob.h" +#include "core/untrashjob.h" +#include "core/filetransferjob.h" +#include "core/filechangeattrjob.h" +#include "utilities.h" + +namespace Fm { + +#define SHOW_DLG_DELAY 1000 + +FileOperation::FileOperation(Type type, Fm::FilePathList srcPaths, QObject* parent): + QObject(parent), + type_{type}, + job_{nullptr}, + dlg_{nullptr}, + srcPaths_{std::move(srcPaths)}, + uiTimer_(nullptr), + elapsedTimer_(nullptr), + lastElapsed_(0), + updateRemainingTime_(true), + autoDestroy_(true) { + + switch(type_) { + case Copy: + job_ = new FileTransferJob(srcPaths_, FileTransferJob::Mode::COPY); + break; + case Move: + job_ = new FileTransferJob(srcPaths_, FileTransferJob::Mode::MOVE); + break; + case Link: + job_ = new FileTransferJob(srcPaths_, FileTransferJob::Mode::LINK); + break; + case Delete: + job_ = new Fm::DeleteJob(srcPaths_); + break; + case Trash: + job_ = new Fm::TrashJob(srcPaths_); + break; + case UnTrash: + job_ = new Fm::UntrashJob(srcPaths_); + break; + case ChangeAttr: + job_ = new Fm::FileChangeAttrJob(srcPaths_); + break; + default: + break; + } + + if(job_) { + // automatically delete the job object when it's finished. + job_->setAutoDelete(true); + + // new C++ jobs + connect(job_, &Fm::Job::finished, this, &Fm::FileOperation::onJobFinish); + connect(job_, &Fm::Job::cancelled, this, &Fm::FileOperation::onJobCancalled); + connect(job_, &Fm::Job::error, this, &Fm::FileOperation::onJobError, Qt::BlockingQueuedConnection); + connect(job_, &Fm::FileOperationJob::fileExists, this, &Fm::FileOperation::onJobFileExists, Qt::BlockingQueuedConnection); + + // we block the job deliberately until we prepare to start (initiailize the timer) so we can calculate elapsed time correctly. + connect(job_, &Fm::FileOperationJob::preparedToRun, this, &Fm::FileOperation::onJobPrepared, Qt::BlockingQueuedConnection); + } +} + +void FileOperation::disconnectJob() { + if(job_) { + disconnect(job_, &Fm::Job::finished, this, &Fm::FileOperation::onJobFinish); + disconnect(job_, &Fm::Job::cancelled, this, &Fm::FileOperation::onJobCancalled); + disconnect(job_, &Fm::Job::error, this, &Fm::FileOperation::onJobError); + disconnect(job_, &Fm::FileOperationJob::fileExists, this, &Fm::FileOperation::onJobFileExists); + disconnect(job_, &Fm::FileOperationJob::preparedToRun, this, &Fm::FileOperation::onJobPrepared); + } +} + +FileOperation::~FileOperation() { + if(uiTimer_) { + uiTimer_->stop(); + delete uiTimer_; + uiTimer_ = nullptr; + } + if(elapsedTimer_) { + delete elapsedTimer_; + elapsedTimer_ = nullptr; + } +} + +void FileOperation::setDestination(Fm::FilePath dest) { + destPath_ = std::move(dest); + switch(type_) { + case Copy: + case Move: + case Link: + if(job_) { + static_cast(job_)->setDestDirPath(destPath_); + } + break; + default: + break; + } +} + +void FileOperation::setDestFiles(FilePathList destFiles) { + switch(type_) { + case Copy: + case Move: + case Link: + if(job_) { + static_cast(job_)->setDestPaths(std::move(destFiles)); + } + break; + default: + break; + } +} + +void FileOperation::setChmod(mode_t newMode, mode_t newModeMask) { + if(job_) { + auto job = static_cast(job_); + job->setFileModeEnabled(true); + job->setFileMode(newMode, newModeMask); + } +} + +void FileOperation::setChown(uid_t uid, gid_t gid) { + if(job_) { + auto job = static_cast(job_); + if(uid != INVALID_UID) { + job->setOwnerEnabled(true); + job->setOwner(uid); + } + if(gid != INVALID_GID) { + job->setGroupEnabled(true); + job->setGroup(gid); + } + } +} + +void FileOperation::setRecursiveChattr(bool recursive) { + if(job_) { + auto job = static_cast(job_); + job->setRecursive(recursive); + } +} + +bool FileOperation::run() { + delete uiTimer_; + // run the job + uiTimer_ = new QTimer(); + uiTimer_->start(SHOW_DLG_DELAY); + connect(uiTimer_, &QTimer::timeout, this, &FileOperation::onUiTimeout); + + if(job_) { + job_->runAsync(); + return true; + } + return false; +} + +void FileOperation::cancel() { + if(job_) { + job_->cancel(); + } +} + +void FileOperation::onUiTimeout() { + if(dlg_) { + // estimate remaining time based on past history + if(job_) { + Fm::FilePath curFilePath = job_->currentFile(); + // update progress bar + double finishedRatio = job_->progress(); + if(finishedRatio > 0.0 && updateRemainingTime_) { + dlg_->setPercent(int(finishedRatio * 100)); + + std::uint64_t totalSize, totalCount, finishedSize, finishedCount; + job_->totalAmount(totalSize, totalCount); + job_->finishedAmount(finishedSize, finishedCount); + + // only show data transferred if the job progress can be calculated by file size. + // for jobs not related to data transfer (for example: change attr, delete,...), hide the UI + if(job_->calcProgressUsingSize()) { + dlg_->setDataTransferred(finishedSize, totalSize); + } + else { + dlg_->setFilesProcessed(finishedCount, totalCount); + } + + double remainRatio = 1.0 - finishedRatio; + gint64 remaining = elapsedTime() * (remainRatio / finishedRatio) / 1000; + // qDebug("etime: %llu, finished: %lf, remain:%lf, remaining secs: %llu", + // elapsedTime(), finishedRatio, remainRatio, remaining); + dlg_->setRemainingTime(remaining); + } + // update currently processed file + if(curFilePath_ != curFilePath) { + curFilePath_ = std::move(curFilePath); + // FIXME: make this cleaner + curFile = QString::fromUtf8(curFilePath_.toString().get()); + dlg_->setCurFile(curFile); + } + } + // 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(); + } +} + +void FileOperation::onJobFileExists(const FileInfo& src, const FileInfo& dest, Fm::FileOperationJob::FileExistsAction& response, FilePath& newDest) { + pauseElapsedTimer(); + showDialog(); + response = dlg_->askRename(src, dest, newDest); + resumeElapsedTimer(); +} + +void FileOperation::onJobCancalled() { + qDebug("file operation is cancelled!"); +} + +void FileOperation::onJobError(const GErrorPtr& err, Fm::Job::ErrorSeverity severity, Fm::Job::ErrorAction& response) { + pauseElapsedTimer(); + showDialog(); + response = Fm::Job::ErrorAction(dlg_->error(err.get(), severity)); + resumeElapsedTimer(); +} + +void FileOperation::onJobPrepared() { + if(!elapsedTimer_) { + elapsedTimer_ = new QElapsedTimer(); + elapsedTimer_->start(); + } + if(dlg_) { + dlg_->setPrepared(); + } +} + +void FileOperation::onJobFinish() { + disconnectJob(); + + if(uiTimer_) { + uiTimer_->stop(); + delete uiTimer_; + uiTimer_ = nullptr; + } + + if(dlg_) { + dlg_->done(QDialog::Accepted); + delete dlg_; + dlg_ = nullptr; + } + Q_EMIT finished(); + + // special handling for trash job + if(type_ == Trash && !job_->isCancelled()) { + auto trashJob = static_cast(job_); + /* some files cannot be trashed because underlying filesystems don't support it. */ + auto unsupportedFiles = trashJob->unsupportedFiles(); + if(!unsupportedFiles.empty()) { /* delete them instead */ + /* parent object is not destroyed before this */ + QWidget* pWidget = qobject_cast(parent()); + if(QMessageBox::question(pWidget ? pWidget->window() : nullptr, + tr("Error"), + tr("Some files cannot be moved to trash can because " + "the underlying file systems don't support this operation.\n" + "Do you want to delete them instead?")) == QMessageBox::Yes) { + deleteFiles(std::move(unsupportedFiles), false); + } + } + } + + if(autoDestroy_) { + delete this; + } +} + +// static +FileOperation* FileOperation::copyFiles(Fm::FilePathList srcFiles, Fm::FilePath dest, QWidget* parent) { + FileOperation* op = new FileOperation(FileOperation::Copy, std::move(srcFiles), parent); + op->setDestination(dest); + op->run(); + return op; +} + +//static +FileOperation *FileOperation::copyFiles(FilePathList srcFiles, FilePathList destFiles, QWidget *parent) { + qDebug("copy: %s -> %s", srcFiles[0].toString().get(), destFiles[0].toString().get()); + FileOperation* op = new FileOperation(FileOperation::Copy, std::move(srcFiles), parent); + op->setDestFiles(std::move(destFiles)); + op->run(); + return op; +} + +// static +FileOperation* FileOperation::moveFiles(Fm::FilePathList srcFiles, Fm::FilePath dest, QWidget* parent) { + FileOperation* op = new FileOperation(FileOperation::Move, std::move(srcFiles), parent); + op->setDestination(dest); + op->run(); + return op; +} + +//static +FileOperation *FileOperation::moveFiles(FilePathList srcFiles, FilePathList destFiles, QWidget *parent) { + FileOperation* op = new FileOperation(FileOperation::Move, std::move(srcFiles), parent); + op->setDestFiles(std::move(destFiles)); + op->run(); + return op; +} + +//static +FileOperation* FileOperation::symlinkFiles(Fm::FilePathList srcFiles, Fm::FilePath dest, QWidget* parent) { + FileOperation* op = new FileOperation(FileOperation::Link, std::move(srcFiles), parent); + op->setDestination(dest); + op->run(); + return op; +} + +//static +FileOperation *FileOperation::symlinkFiles(FilePathList srcFiles, FilePathList destFiles, QWidget *parent) { + FileOperation* op = new FileOperation(FileOperation::Link, std::move(srcFiles), parent); + op->setDestFiles(std::move(destFiles)); + op->run(); + return op; +} + +//static +FileOperation* FileOperation::deleteFiles(Fm::FilePathList srcFiles, bool prompt, QWidget* parent) { + if(prompt) { + int result = QMessageBox::warning(parent ? parent->window() : nullptr, + tr("Confirm"), + tr("Do you want to delete the selected files?"), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No); + if(result != QMessageBox::Yes) { + return nullptr; + } + } + + FileOperation* op = new FileOperation(FileOperation::Delete, std::move(srcFiles), parent); + op->run(); + return op; +} + +//static +FileOperation* FileOperation::trashFiles(Fm::FilePathList srcFiles, bool prompt, QWidget* parent) { + if(prompt) { + int result = QMessageBox::warning(parent ? parent->window() : nullptr, + tr("Confirm"), + tr("Do you want to move the selected files to trash can?"), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No); + if(result != QMessageBox::Yes) { + return nullptr; + } + } + + FileOperation* op = new FileOperation(FileOperation::Trash, std::move(srcFiles), parent); + op->run(); + return op; +} + +//static +FileOperation* FileOperation::unTrashFiles(Fm::FilePathList srcFiles, QWidget* parent) { + FileOperation* op = new FileOperation(FileOperation::UnTrash, std::move(srcFiles), parent); + op->run(); + return op; +} + +// static +FileOperation* FileOperation::changeAttrFiles(Fm::FilePathList srcFiles, QWidget* parent) { + //TODO + FileOperation* op = new FileOperation(FileOperation::ChangeAttr, std::move(srcFiles), parent); + op->run(); + return op; +} + + +} // namespace Fm diff --git a/src/fileoperation.h b/src/fileoperation.h new file mode 100644 index 0000000..6b9b71a --- /dev/null +++ b/src/fileoperation.h @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_FILEOPERATION_H +#define FM_FILEOPERATION_H + +#include "libfmqtglobals.h" +#include +#include +#include "core/filepath.h" +#include "core/fileoperationjob.h" + +class QTimer; + +namespace Fm { + +class FileOperationDialog; + +class LIBFM_QT_API FileOperation : public QObject { + Q_OBJECT +public: + enum Type { + Copy, + Move, + Link, + Delete, + Trash, + UnTrash, + ChangeAttr + }; + +public: + explicit FileOperation(Type type, Fm::FilePathList srcFiles, QObject* parent = nullptr); + + virtual ~FileOperation(); + + void setDestination(Fm::FilePath dest); + + void setDestFiles(FilePathList destFiles); + + void setChmod(mode_t newMode, mode_t newModeMask); + + void setChown(uid_t uid, gid_t gid); + + // This only work for change attr jobs. + void setRecursiveChattr(bool recursive); + + bool run(); + + void cancel(); + + bool isRunning() const { + return job_ && !isCancelled(); + } + + bool isCancelled() const { + if(job_) { + return job_->isCancelled(); + } + return false; + } + + Fm::FileOperationJob* job() { + return job_; + } + + bool autoDestroy() { + return autoDestroy_; + } + void setAutoDestroy(bool destroy = true) { + autoDestroy_ = destroy; + } + + Type type() { + return type_; + } + + // convinient static functions + static FileOperation* copyFiles(Fm::FilePathList srcFiles, Fm::FilePath dest, QWidget* parent = nullptr); + + static FileOperation* copyFiles(Fm::FilePathList srcFiles, Fm::FilePathList destFiles, QWidget* parent = nullptr); + + static FileOperation* copyFile(Fm::FilePath srcFile, Fm::FilePath destFile, QWidget* parent = nullptr) { + return copyFiles(FilePathList{std::move(srcFile)}, FilePathList{std::move(destFile)}, parent); + } + + static FileOperation* moveFiles(Fm::FilePathList srcFiles, Fm::FilePath dest, QWidget* parent = nullptr); + + static FileOperation* moveFiles(Fm::FilePathList srcFiles, Fm::FilePathList destFiles, QWidget* parent = nullptr); + + static FileOperation* moveFile(Fm::FilePath srcFile, Fm::FilePath destFile, QWidget* parent = nullptr) { + return moveFiles(FilePathList{std::move(srcFile)}, FilePathList{std::move(destFile)}, parent); + } + + static FileOperation* symlinkFiles(Fm::FilePathList srcFiles, Fm::FilePath dest, QWidget* parent = nullptr); + + static FileOperation* symlinkFiles(Fm::FilePathList srcFiles, Fm::FilePathList destFiles, QWidget* parent = nullptr); + + static FileOperation* symlinkFile(Fm::FilePath srcFile, Fm::FilePath destFile, QWidget* parent = nullptr) { + return symlinkFiles(FilePathList{std::move(srcFile)}, FilePathList{std::move(destFile)}, parent); + } + + static FileOperation* deleteFiles(Fm::FilePathList srcFiles, bool promp = true, QWidget* parent = nullptr); + + static FileOperation* trashFiles(Fm::FilePathList srcFiles, bool promp = true, QWidget* parent = nullptr); + + static FileOperation* unTrashFiles(Fm::FilePathList srcFiles, QWidget* parent = nullptr); + + static FileOperation* changeAttrFiles(Fm::FilePathList srcFiles, QWidget* parent = nullptr); + +Q_SIGNALS: + void finished(); + +private Q_SLOTS: + void onJobPrepared(); + void onJobFinish(); + void onJobCancalled(); + void onJobError(const GErrorPtr& err, Fm::Job::ErrorSeverity severity, Fm::Job::ErrorAction& response); + void onJobFileExists(const FileInfo& src, const FileInfo& dest, Fm::FileOperationJob::FileExistsAction& response, FilePath& newDest); + +private: + + void disconnectJob(); + void showDialog(); + + void pauseElapsedTimer() { + if(Q_LIKELY(elapsedTimer_ != nullptr)) { + lastElapsed_ += elapsedTimer_->elapsed(); + elapsedTimer_->invalidate(); + } + } + + void resumeElapsedTimer() { + if(Q_LIKELY(elapsedTimer_ != nullptr)) { + elapsedTimer_->start(); + } + } + + qint64 elapsedTime() { + if(Q_LIKELY(elapsedTimer_ != nullptr)) { + return lastElapsed_ + elapsedTimer_->elapsed(); + } + return 0; + } + +private Q_SLOTS: + void onUiTimeout(); + +private: + Type type_; + FileOperationJob* job_; + FileOperationDialog* dlg_; + FilePath destPath_; + FilePath curFilePath_; + FilePathList srcPaths_; + QTimer* uiTimer_; + QElapsedTimer* elapsedTimer_; + qint64 lastElapsed_; + bool updateRemainingTime_; + QString curFile; + bool autoDestroy_; +}; + +} + +#endif // FM_FILEOPERATION_H diff --git a/src/fileoperationdialog.cpp b/src/fileoperationdialog.cpp new file mode 100644 index 0000000..18eff7e --- /dev/null +++ b/src/fileoperationdialog.cpp @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "fileoperationdialog.h" +#include "fileoperation.h" +#include "renamedialog.h" +#include +#include +#include "utilities.h" +#include "ui_file-operation-dialog.h" + +#include "core/legacy/fm-config.h" + +namespace Fm { + +FileOperationDialog::FileOperationDialog(FileOperation* _operation): + QDialog(nullptr), + operation(_operation), + defaultOption(-1), + ignoreNonCriticalErrors_(false) { + + ui = new Ui::FileOperationDialog(); + ui->setupUi(this); + + QString title; + QString message; + switch(_operation->type()) { + case FileOperation::Move: + title = tr("Move files"); + message = tr("Moving the following files to destination folder:"); + break; + case FileOperation::Copy: + title = tr("Copy Files"); + message = tr("Copying the following files to destination folder:"); + break; + case FileOperation::Trash: + title = tr("Trash Files"); + message = tr("Moving the following files to trash can:"); + break; + case FileOperation::Delete: + title = tr("Delete Files"); + message = tr("Deleting the following files:"); + ui->dest->hide(); + ui->destLabel->hide(); + break; + case FileOperation::Link: + title = tr("Create Symlinks"); + message = tr("Creating symlinks for the following files:"); + break; + case FileOperation::ChangeAttr: + title = tr("Change Attributes"); + message = tr("Changing attributes of the following files:"); + ui->dest->hide(); + ui->destLabel->hide(); + break; + case FileOperation::UnTrash: + title = tr("Restore Trashed Files"); + message = tr("Restoring the following files from trash can:"); + ui->dest->hide(); + ui->destLabel->hide(); + break; + } + ui->message->setText(message); + setWindowTitle(title); +} + + +FileOperationDialog::~FileOperationDialog() { + delete ui; +} + +void FileOperationDialog::setDestPath(const Fm::FilePath &dest) { + ui->dest->setText(dest.displayName().get()); +} + +void FileOperationDialog::setSourceFiles(const Fm::FilePathList &srcFiles) { + for(auto& srcFile : srcFiles) { + ui->sourceFiles->addItem(srcFile.displayName().get()); + } +} + +int FileOperationDialog::ask(QString /*question*/, char* const* /*options*/) { + // TODO: implement FileOperationDialog::ask() + return 0; +} + + +FileOperationJob::FileExistsAction FileOperationDialog::askRename(const FileInfo &src, const FileInfo &dest, FilePath &newDest) { + FileOperationJob::FileExistsAction 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 = FileOperationJob::OVERWRITE; + if(dlg.applyToAll()) { + defaultOption = ret; + } + break; + case RenameDialog::ActionRename: { + ret = FileOperationJob::RENAME; + auto newName = dlg.newName(); + if(!newName.isEmpty()) { + auto destDirPath = dest.path().parent(); + newDest = destDirPath.child(newName.toUtf8().constData()); + } + break; + } + case RenameDialog::ActionIgnore: + ret = FileOperationJob::SKIP; + if(dlg.applyToAll()) { + defaultOption = ret; + } + break; + default: + ret = FileOperationJob::CANCEL; + break; + } + } + else { + ret = (FileOperationJob::FileExistsAction)defaultOption; + } + return ret; +} + +Job::ErrorAction FileOperationDialog::error(GError* err, Job::ErrorSeverity severity) { + if(severity >= Job::ErrorSeverity::MODERATE) { + if(severity == Job::ErrorSeverity::CRITICAL) { + QMessageBox::critical(this, tr("Error"), QString::fromUtf8(err->message)); + return Job::ErrorAction::ABORT; + } + if (ignoreNonCriticalErrors_) { + return Job::ErrorAction::CONTINUE; + } + QMessageBox::StandardButton stb = QMessageBox::critical(this, tr("Error"), QString::fromUtf8(err->message), + QMessageBox::Ok | QMessageBox::Ignore); + if (stb == QMessageBox::Ignore) { + ignoreNonCriticalErrors_ = true; + } + } + return Job::ErrorAction::CONTINUE; +} + +void FileOperationDialog::setCurFile(QString cur_file) { + ui->curFile->setText(cur_file); +} + +void FileOperationDialog::setDataTransferred(uint64_t finishedSize, std::uint64_t totalSize) { + ui->filesProcessed->setText(QString("%1 / %2") + .arg(formatFileSize(finishedSize, fm_config->si_unit)) + .arg(formatFileSize(totalSize, fm_config->si_unit))); +} + +void FileOperationDialog::setFilesProcessed(uint64_t finishedCount, uint64_t totalCount) { + ui->filesProcessed->setText(QString("%1 / %2") + .arg(finishedCount) + .arg(totalCount)); +} + +void FileOperationDialog::setPercent(unsigned int percent) { + ui->progressBar->setValue(percent); +} + + +void FileOperationDialog::setRemainingTime(unsigned int sec) { + unsigned int min = 0; + unsigned int hr = 0; + if(sec > 60) { + min = sec / 60; + sec %= 60; + if(min > 60) { + hr = min / 60; + min %= 60; + } + } + ui->timeRemaining->setText(QString("%1:%2:%3") + .arg(hr, 2, 10, QChar('0')) + .arg(min, 2, 10, QChar('0')) + .arg(sec, 2, 10, QChar('0'))); +} + +void FileOperationDialog::setPrepared() { +} + +void FileOperationDialog::reject() { + operation->cancel(); + QDialog::reject(); +} + + +} // namespace Fm diff --git a/src/fileoperationdialog.h b/src/fileoperationdialog.h new file mode 100644 index 0000000..71f5af0 --- /dev/null +++ b/src/fileoperationdialog.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_FILEOPERATIONDIALOG_H +#define FM_FILEOPERATIONDIALOG_H + +#include "libfmqtglobals.h" +#include +#include +#include "core/filepath.h" +#include "core/fileinfo.h" +#include "core/fileoperationjob.h" + +namespace Ui { +class FileOperationDialog; +} + +namespace Fm { + +class FileOperation; + +class LIBFM_QT_API FileOperationDialog : public QDialog { + Q_OBJECT +public: + explicit FileOperationDialog(FileOperation* _operation); + virtual ~FileOperationDialog(); + + void setSourceFiles(const Fm::FilePathList& srcFiles); + void setDestPath(const Fm::FilePath& dest); + + int ask(QString question, char* const* options); + + FileOperationJob::FileExistsAction askRename(const FileInfo& src, const FileInfo& dest, FilePath& newDest); + + Job::ErrorAction error(GError* err, Job::ErrorSeverity severity); + void setPrepared(); + void setCurFile(QString cur_file); + void setPercent(unsigned int percent); + void setDataTransferred(std::uint64_t finishedSize, std::uint64_t totalSize); + void setFilesProcessed(std::uint64_t finishedCount, std::uint64_t totalCount); + void setRemainingTime(unsigned int sec); + + virtual void reject(); + +private: + Ui::FileOperationDialog* ui; + FileOperation* operation; + int defaultOption; + bool ignoreNonCriticalErrors_; +}; + +} + +#endif // FM_FILEOPERATIONDIALOG_H diff --git a/src/fileoperationdialog_p.h b/src/fileoperationdialog_p.h new file mode 100644 index 0000000..a73f3f4 --- /dev/null +++ b/src/fileoperationdialog_p.h @@ -0,0 +1,69 @@ +/* + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * 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_P_H +#define FM_FILEOPERATIONDIALOG_P_H + +#include +#include +#include + +namespace Fm { + +class ElidedLabel : public QLabel { +Q_OBJECT + +public: + explicit ElidedLabel(QWidget *parent = 0, Qt::WindowFlags f = Qt::WindowFlags()): + QLabel(parent, f), + lastWidth_(0) { + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); + // set a min width to prevent the window from widening with long texts + setMinimumWidth(fontMetrics().averageCharWidth() * 10); + } + +protected: + // A simplified version of QLabel::paintEvent() without pixmap or shortcut but with eliding. + void paintEvent(QPaintEvent* /*event*/) override { + QRect cr = contentsRect().adjusted(margin(), margin(), -margin(), -margin()); + QString txt = text(); + // if the text is changed or its rect is resized (due to window resizing), + // find whether it needs to be elided... + if (txt != lastText_ || cr.width() != lastWidth_) { + lastText_ = txt; + lastWidth_ = cr.width(); + elidedText_ = fontMetrics().elidedText(txt, Qt::ElideMiddle, cr.width()); + } + // ... then, draw the (elided) text + if(!elidedText_.isEmpty()) { + QPainter painter(this); + QStyleOption opt; + opt.initFrom(this); + style()->drawItemText(&painter, cr, alignment(), opt.palette, isEnabled(), elidedText_, foregroundRole()); + } + } + +private: + QString elidedText_; + QString lastText_; + int lastWidth_; +}; + +} + +#endif // FM_FILEOPERATIONDIALOG_P_H diff --git a/src/filepropsdialog.cpp b/src/filepropsdialog.cpp new file mode 100644 index 0000000..ecb84a7 --- /dev/null +++ b/src/filepropsdialog.cpp @@ -0,0 +1,600 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "filepropsdialog.h" +#include "ui_file-props.h" +#include "utilities.h" +#include "fileoperation.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "core/totalsizejob.h" +#include "core/folder.h" + +#include "core/legacy/fm-config.h" + +#define DIFFERENT_UIDS ((uid)-1) +#define DIFFERENT_GIDS ((gid)-1) +#define DIFFERENT_PERMS ((mode_t)-1) + +namespace Fm { + +enum { + ACCESS_NO_CHANGE = 0, + ACCESS_READ_ONLY, + ACCESS_READ_WRITE, + ACCESS_FORBID +}; + +FilePropsDialog::FilePropsDialog(Fm::FileInfoList files, QWidget* parent, Qt::WindowFlags f): + QDialog(parent, f), + fileInfos_{std::move(files)}, + fileInfo{fileInfos_.front()}, + singleType(fileInfos_.isSameType()), + singleFile(fileInfos_.size() == 1 ? true : false) { + + setAttribute(Qt::WA_DeleteOnClose); + + ui = new Ui::FilePropsDialog(); + ui->setupUi(this); + + if(singleType) { + mimeType = fileInfo->mimeType(); + } + + totalSizeJob = new Fm::TotalSizeJob(fileInfos_.paths(), Fm::TotalSizeJob::DEFAULT); + + initGeneralPage(); + initPermissionsPage(); + + if(!singleFile || !hasDir) { // not a single dir + ui->contentsLabel->hide(); + ui->fileNumber->hide(); + } +} + +FilePropsDialog::~FilePropsDialog() { + // Stop the timer if it's still running + if(fileSizeTimer) { + fileSizeTimer->stop(); + delete fileSizeTimer; + fileSizeTimer = nullptr; + } + + // Cancel the indexing job if it hasn't finished + if(totalSizeJob) { + totalSizeJob->cancel(); + totalSizeJob = nullptr; + } + + // And finally delete the dialog's UI + delete ui; +} + +void FilePropsDialog::initApplications() { + if(singleType && mimeType && !fileInfo->isDir()) { + ui->openWith->setMimeType(mimeType); + } + else { + ui->openWith->hide(); + ui->openWithLabel->hide(); + } +} + +void FilePropsDialog::initPermissionsPage() { + // ownership handling + // get owner/group and mode of the first file in the list + uid = fileInfo->uid(); + gid = fileInfo->gid(); + mode_t mode = fileInfo->mode(); + ownerPerm = (mode & (S_IRUSR | S_IWUSR | S_IXUSR)); + groupPerm = (mode & (S_IRGRP | S_IWGRP | S_IXGRP)); + otherPerm = (mode & (S_IROTH | S_IWOTH | S_IXOTH)); + execPerm = (mode & (S_IXUSR | S_IXGRP | S_IXOTH)); + allNative = fileInfo->isNative(); + hasDir = S_ISDIR(mode); + + // check if all selected files belongs to the same owner/group or have the same mode + // at the same time, check if all files are on native unix filesystems + for(auto& fi: fileInfos_) { + if(allNative && !fi->isNative()) { + allNative = false; // not all of the files are native + } + + mode_t fi_mode = fi->mode(); + if(S_ISDIR(fi_mode)) { + hasDir = true; // the files list contains dir(s) + } + + if(uid != DIFFERENT_UIDS && static_cast(uid) != fi->uid()) { + uid = DIFFERENT_UIDS; // not all files have the same owner + } + if(gid != DIFFERENT_GIDS && static_cast(gid) != fi->gid()) { + gid = DIFFERENT_GIDS; // not all files have the same owner group + } + + if(ownerPerm != DIFFERENT_PERMS && ownerPerm != (fi_mode & (S_IRUSR | S_IWUSR | S_IXUSR))) { + ownerPerm = DIFFERENT_PERMS; // not all files have the same permission for owner + } + if(groupPerm != DIFFERENT_PERMS && groupPerm != (fi_mode & (S_IRGRP | S_IWGRP | S_IXGRP))) { + groupPerm = DIFFERENT_PERMS; // not all files have the same permission for grop + } + if(otherPerm != DIFFERENT_PERMS && otherPerm != (fi_mode & (S_IROTH | S_IWOTH | S_IXOTH))) { + otherPerm = DIFFERENT_PERMS; // not all files have the same permission for other + } + if(execPerm != DIFFERENT_PERMS && execPerm != (fi_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) { + execPerm = DIFFERENT_PERMS; // not all files have the same executable permission + } + } + + // init owner/group + initOwner(); + + // if all files are of the same type, and some of them are dirs => all of the items are dirs + // rwx values have different meanings for dirs + // Let's make it clear for the users + // init combo boxes for file permissions here + QStringList comboItems; + comboItems.append("---"); // no change + if(singleType && hasDir) { // all files are dirs + comboItems.append(tr("View folder content")); + comboItems.append(tr("View and modify folder content")); + ui->executable->hide(); + } + else { //not all of the files are dirs + comboItems.append(tr("Read")); + comboItems.append(tr("Read and write")); + } + comboItems.append(tr("Forbidden")); + QStringListModel* comboModel = new QStringListModel(comboItems, this); + ui->ownerPerm->setModel(comboModel); + ui->groupPerm->setModel(comboModel); + ui->otherPerm->setModel(comboModel); + + // owner + ownerPermSel = ACCESS_NO_CHANGE; + if(ownerPerm != DIFFERENT_PERMS) { // permissions for owner are the same among all files + if(ownerPerm & S_IRUSR) { // can read + if(ownerPerm & S_IWUSR) { // can write + ownerPermSel = ACCESS_READ_WRITE; + } + else { + ownerPermSel = ACCESS_READ_ONLY; + } + } + else { + if((ownerPerm & S_IWUSR) == 0) { // cannot read or write + ownerPermSel = ACCESS_FORBID; + } + } + } + ui->ownerPerm->setCurrentIndex(ownerPermSel); + + // owner and group + groupPermSel = ACCESS_NO_CHANGE; + if(groupPerm != DIFFERENT_PERMS) { // permissions for owner are the same among all files + if(groupPerm & S_IRGRP) { // can read + if(groupPerm & S_IWGRP) { // can write + groupPermSel = ACCESS_READ_WRITE; + } + else { + groupPermSel = ACCESS_READ_ONLY; + } + } + else { + if((groupPerm & S_IWGRP) == 0) { // cannot read or write + groupPermSel = ACCESS_FORBID; + } + } + } + ui->groupPerm->setCurrentIndex(groupPermSel); + + // other + otherPermSel = ACCESS_NO_CHANGE; + if(otherPerm != DIFFERENT_PERMS) { // permissions for owner are the same among all files + if(otherPerm & S_IROTH) { // can read + if(otherPerm & S_IWOTH) { // can write + otherPermSel = ACCESS_READ_WRITE; + } + else { + otherPermSel = ACCESS_READ_ONLY; + } + } + else { + if((otherPerm & S_IWOTH) == 0) { // cannot read or write + otherPermSel = ACCESS_FORBID; + } + } + + } + ui->otherPerm->setCurrentIndex(otherPermSel); + + // set the checkbox to partially checked state + // when owner, group, and other have different executable flags set. + // some of them have exec, and others do not have. + execCheckState = Qt::PartiallyChecked; + if(execPerm != DIFFERENT_PERMS) { // if all files have the same executable permission + // check if the files are all executable + if((mode & (S_IXUSR | S_IXGRP | S_IXOTH)) == (S_IXUSR | S_IXGRP | S_IXOTH)) { + // owner, group, and other all have exec permission. + ui->executable->setTristate(false); + execCheckState = Qt::Checked; + } + else if((mode & (S_IXUSR | S_IXGRP | S_IXOTH)) == 0) { + // owner, group, and other all have no exec permission + ui->executable->setTristate(false); + execCheckState = Qt::Unchecked; + } + } + ui->executable->setCheckState(execCheckState); +} + +void FilePropsDialog::initGeneralPage() { + // update UI + if(singleType) { // all files are of the same mime-type + std::shared_ptr icon; + // FIXME: handle custom icons for some files + // FIXME: display special property pages for special files or + // some specified mime-types. + if(singleFile) { // only one file is selected. + icon = fileInfo->icon(); + } + if(mimeType) { + if(!icon) { // get an icon from mime type if needed + icon = mimeType->icon(); + } + ui->fileType->setText(mimeType->desc()); + ui->mimeType->setText(mimeType->name()); + } + if(icon) { + ui->iconButton->setIcon(icon->qicon()); + } + + if(singleFile && fileInfo->isSymlink()) { + ui->target->setText(QString::fromStdString(fileInfo->target())); + } + else { + ui->target->hide(); + ui->targetLabel->hide(); + } + if(fileInfo->isDir() && fileInfo->isNative()) { // all files are native dirs + connect(ui->iconButton, &QAbstractButton::clicked, this, &FilePropsDialog::onIconButtonclicked); + } + } // end if(singleType) + else { // not singleType, multiple files are selected at the same time + ui->fileType->setText(tr("Files of different types")); + ui->target->hide(); + ui->targetLabel->hide(); + } + + // FIXME: check if all files has the same parent dir, mtime, or atime + if(singleFile) { // only one file is selected + auto parent_path = fileInfo->path().parent(); + auto parent_str = parent_path ? parent_path.displayName(): nullptr; + + ui->fileName->setText(fileInfo->displayName()); + if(parent_str) { + ui->location->setText(parent_str.get()); + } + else { + ui->location->clear(); + } + auto mtime = QDateTime::fromMSecsSinceEpoch(fileInfo->mtime() * 1000); + ui->lastModified->setText(mtime.toString(Qt::SystemLocaleShortDate)); + auto atime = QDateTime::fromMSecsSinceEpoch(fileInfo->atime() * 1000); + ui->lastAccessed->setText(atime.toString(Qt::SystemLocaleShortDate)); + } + else { + ui->fileName->setText(tr("Multiple Files")); + ui->fileName->setEnabled(false); + } + + initApplications(); // init applications combo box + + // calculate total file sizes + fileSizeTimer = new QTimer(this); + connect(fileSizeTimer, &QTimer::timeout, this, &FilePropsDialog::onFileSizeTimerTimeout); + fileSizeTimer->start(600); + + connect(totalSizeJob, &Fm::TotalSizeJob::finished, this, &FilePropsDialog::onDeepCountJobFinished, Qt::BlockingQueuedConnection); + totalSizeJob->setAutoDelete(true); + totalSizeJob->runAsync(); + + // disk usage + bool canShowDeviceUsage = false; + if(fileInfo->dirPath()) { // skip directories like "search:///" + auto folder = Fm::Folder::fromPath(fileInfo->dirPath()); + if(!folder->isLoaded() && fileInfo->isDir()) { // an empty space is right clicked + folder = Fm::Folder::fromPath(fileInfo->path()); + } + guint64 free, total; + if(folder->getFilesystemInfo(&total, &free)) { + canShowDeviceUsage = true; + ui->progressBar->setValue(qRound(static_cast((total - free) * 100) / static_cast(total))); + ui->progressBar->setFormat(tr("%p% used")); + ui->spaceLabel->setText(tr("%1 Free of %2") + .arg(formatFileSize(free, false)) + .arg(formatFileSize(total, false))); + } + } + if(!canShowDeviceUsage) { + ui->deviceLabel->setVisible(false); + ui->spaceLabel->setVisible(false); + ui->progressBar->setVisible(false); + } +} + +void FilePropsDialog::onDeepCountJobFinished() { + onFileSizeTimerTimeout(); // update file size display + + totalSizeJob = nullptr; + + // stop the timer + if(fileSizeTimer) { + fileSizeTimer->stop(); + delete fileSizeTimer; + fileSizeTimer = nullptr; + } +} + +void FilePropsDialog::onFileSizeTimerTimeout() { + if(totalSizeJob && !totalSizeJob->isCancelled()) { + // FIXME: + // OMG! It's really unbelievable that Qt developers only implement + // QObject::tr(... int n). GNU gettext developers are smarter and + // they use unsigned long instead of int. + // We cannot use Qt here to handle plural forms. So sad. :-( + QString str = Fm::formatFileSize(totalSizeJob->totalSize(), fm_config->si_unit) % + QString(" (%1 B)").arg(totalSizeJob->totalSize()); + // tr(" (%n) byte(s)", "", deepCountJob->total_size); + ui->fileSize->setText(str); + + str = Fm::formatFileSize(totalSizeJob->totalOnDiskSize(), fm_config->si_unit) % + QString(" (%1 B)").arg(totalSizeJob->totalOnDiskSize()); + // tr(" (%n) byte(s)", "", deepCountJob->total_ondisk_size); + ui->onDiskSize->setText(str); + + if(ui->contentsLabel->isVisible()) { + unsigned int fc = totalSizeJob->fileCount(); // the directory is included + if (fc <= 1) + str = tr("no file"); + else if (fc == 2) + str = tr("one file"); + else + str = tr("%Ln files", "", fc - 1); + ui->fileNumber->setText(str); + } + } +} + +void FilePropsDialog::onIconButtonclicked() { + QString iconDir; + QString iconThemeName = QIcon::themeName(); + QStringList icons = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, + "icons", + QStandardPaths::LocateDirectory); + for (QStringList::ConstIterator it = icons.constBegin(); it != icons.constEnd(); ++it) { + QString iconThemeFolder = *it + '/' + iconThemeName; + if (QDir(iconThemeFolder).exists() && QFileInfo(iconThemeFolder).permission(QFileDevice::ReadUser)) { + // give priority to the "places" folder + const QString places = iconThemeFolder + QLatin1String("/places"); + if (QDir(places).exists() && QFileInfo(places).permission(QFileDevice::ReadUser)) { + iconDir = places; + } + else { + iconDir = iconThemeFolder; + } + break; + } + } + if(iconDir.isEmpty()) { + iconDir = QStandardPaths::locate(QStandardPaths::GenericDataLocation, + "icons", + QStandardPaths::LocateDirectory); + if(iconDir.isEmpty()) { + return; + } + } + const QString iconPath = QFileDialog::getOpenFileName(this, tr("Select an icon"), + iconDir, + tr("Images (*.png *.xpm *.svg *.svgz )")); + if(!iconPath.isEmpty()) { + QStringList parts = iconPath.split("/", QString::SkipEmptyParts); + if(!parts.isEmpty()) { + QString iconName = parts.at(parts.count() - 1); + int ln = iconName.lastIndexOf("."); + if(ln > -1) { + iconName.remove(ln, iconName.length() - ln); + customIcon = QIcon::fromTheme(iconName); + ui->iconButton->setIcon(customIcon); + } + } + } +} + +void FilePropsDialog::accept() { + + // applications + if(mimeType && ui->openWith->isChanged()) { + auto currentApp = ui->openWith->selectedApp(); + g_app_info_set_as_default_for_type(currentApp.get(), mimeType->name(), nullptr); + } + + // check if chown or chmod is needed + uid_t newUid = uidFromName(ui->owner->text()); + gid_t newGid = gidFromName(ui->ownerGroup->text()); + bool needChown = (newUid != INVALID_UID && newUid != uid) || (newGid != INVALID_GID && newGid != gid); + + int newOwnerPermSel = ui->ownerPerm->currentIndex(); + int newGroupPermSel = ui->groupPerm->currentIndex(); + int newOtherPermSel = ui->otherPerm->currentIndex(); + Qt::CheckState newExecCheckState = ui->executable->checkState(); + bool needChmod = ((newOwnerPermSel != ownerPermSel) || + (newGroupPermSel != groupPermSel) || + (newOtherPermSel != otherPermSel) || + (newExecCheckState != execCheckState)); + + if(needChmod || needChown) { + FileOperation* op = new FileOperation(FileOperation::ChangeAttr, fileInfos_.paths()); + if(needChown) { + // don't do chown if new uid/gid and the original ones are actually the same. + if(newUid == uid) { + newUid = INVALID_UID; + } + if(newGid == gid) { + newGid = INVALID_GID; + } + op->setChown(newUid, newGid); + } + if(needChmod) { + mode_t newMode = 0; + mode_t newModeMask = 0; + // FIXME: we need to make sure that folders with "r" permission also have "x" + // at the same time. Otherwise, it's not able to browse the folder later. + if(newOwnerPermSel != ownerPermSel && newOwnerPermSel != ACCESS_NO_CHANGE) { + // owner permission changed + newModeMask |= (S_IRUSR | S_IWUSR); // affect user bits + if(newOwnerPermSel == ACCESS_READ_ONLY) { + newMode |= S_IRUSR; + } + else if(newOwnerPermSel == ACCESS_READ_WRITE) { + newMode |= (S_IRUSR | S_IWUSR); + } + } + if(newGroupPermSel != groupPermSel && newGroupPermSel != ACCESS_NO_CHANGE) { + qDebug("newGroupPermSel: %d", newGroupPermSel); + // group permission changed + newModeMask |= (S_IRGRP | S_IWGRP); // affect group bits + if(newGroupPermSel == ACCESS_READ_ONLY) { + newMode |= S_IRGRP; + } + else if(newGroupPermSel == ACCESS_READ_WRITE) { + newMode |= (S_IRGRP | S_IWGRP); + } + } + if(newOtherPermSel != otherPermSel && newOtherPermSel != ACCESS_NO_CHANGE) { + // other permission changed + newModeMask |= (S_IROTH | S_IWOTH); // affect other bits + if(newOtherPermSel == ACCESS_READ_ONLY) { + newMode |= S_IROTH; + } + else if(newOtherPermSel == ACCESS_READ_WRITE) { + newMode |= (S_IROTH | S_IWOTH); + } + } + if(newExecCheckState != execCheckState && newExecCheckState != Qt::PartiallyChecked) { + // executable state changed + newModeMask |= (S_IXUSR | S_IXGRP | S_IXOTH); + if(newExecCheckState == Qt::Checked) { + newMode |= (S_IXUSR | S_IXGRP | S_IXOTH); + } + } + op->setChmod(newMode, newModeMask); + + if(hasDir) { // if there are some dirs in our selected files + QMessageBox::StandardButton r = QMessageBox::question(this, + tr("Apply changes"), + tr("Do you want to recursively apply these changes to all files and sub-folders?"), + QMessageBox::Yes | QMessageBox::No); + if(r == QMessageBox::Yes) { + op->setRecursiveChattr(true); + } + } + } + op->setAutoDestroy(true); + op->run(); + } + + // Renaming + if(singleFile) { + QString new_name = ui->fileName->text(); + if(fileInfo->displayName() != new_name) { + auto path = fileInfo->path(); + auto parent_path = path.parent(); + auto dest = parent_path.child(new_name.toLocal8Bit().constData()); + Fm::GErrorPtr err; + if(!g_file_move(path.gfile().get(), dest.gfile().get(), + GFileCopyFlags(G_FILE_COPY_ALL_METADATA | + G_FILE_COPY_NO_FALLBACK_FOR_MOVE | + G_FILE_COPY_NOFOLLOW_SYMLINKS), + nullptr, nullptr, nullptr, &err)) { + QMessageBox::critical(this, QObject::tr("Error"), err.message()); + } + } + } + + // Custom (folder) icon + if(!customIcon.isNull()) { + bool reloadNeeded(false); + QString iconNamne = customIcon.name(); + for(auto& fi: fileInfos_) { + std::shared_ptr icon = fi->icon(); + if (!fi->icon() || fi->icon()->qicon().name() != iconNamne) { + auto dot_dir = CStrPtr{g_build_filename(fi->path().localPath().get(), ".directory", nullptr)}; + GKeyFile* kf = g_key_file_new(); + g_key_file_set_string(kf, "Desktop Entry", "Icon", iconNamne.toLocal8Bit().constData()); + Fm::GErrorPtr err; + if (!g_key_file_save_to_file(kf, dot_dir.get(), &err)) { + QMessageBox::critical(this, QObject::tr("Custom Icon Error"), err.message()); + } + else { + reloadNeeded = true; + } + g_key_file_free(kf); + } + } + if(reloadNeeded) { + // since there can be only one parent dir, only one reload is needed + auto parent = fileInfo->path().parent(); + if(parent.isValid()) { + auto folder = Fm::Folder::fromPath(parent); + if(folder->isLoaded()) { + folder->reload(); + } + } + } + } + + QDialog::accept(); +} + +void FilePropsDialog::initOwner() { + if(allNative) { + if(uid != DIFFERENT_UIDS) { + ui->owner->setText(uidToName(uid)); + } + if(gid != DIFFERENT_GIDS) { + ui->ownerGroup->setText(gidToName(gid)); + } + + if(geteuid() != 0) { // on local filesystems, only root can do chown. + ui->owner->setEnabled(false); + ui->ownerGroup->setEnabled(false); + } + } +} + + +} // namespace Fm diff --git a/src/filepropsdialog.h b/src/filepropsdialog.h new file mode 100644 index 0000000..67a68d4 --- /dev/null +++ b/src/filepropsdialog.h @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_FILEPROPSDIALOG_H +#define FM_FILEPROPSDIALOG_H + +#include "libfmqtglobals.h" +#include +#include + +#include "core/fileinfo.h" +#include "core/totalsizejob.h" + +namespace Ui { +class FilePropsDialog; +} + +namespace Fm { + +class LIBFM_QT_API FilePropsDialog : public QDialog { + Q_OBJECT + +public: + explicit FilePropsDialog(Fm::FileInfoList files, QWidget* parent = 0, Qt::WindowFlags f = 0); + virtual ~FilePropsDialog(); + + virtual void accept(); + + static FilePropsDialog* showForFile(std::shared_ptr file, QWidget* parent = 0) { + Fm::FileInfoList files; + files.push_back(std::move(file)); + FilePropsDialog* dlg = showForFiles(files, parent); + return dlg; + } + + static FilePropsDialog* showForFiles(Fm::FileInfoList files, QWidget* parent = 0) { + FilePropsDialog* dlg = new FilePropsDialog(std::move(files), parent); + dlg->show(); + return dlg; + } + +private: + void initGeneralPage(); + void initApplications(); + void initPermissionsPage(); + void initOwner(); + +private Q_SLOTS: + void onDeepCountJobFinished(); + void onFileSizeTimerTimeout(); + void onIconButtonclicked(); + +private: + Ui::FilePropsDialog* ui; + Fm::FileInfoList fileInfos_; // list of all file infos + std::shared_ptr fileInfo; // file info of the first file in the list + bool singleType; // all files are of the same type? + bool singleFile; // only one file is selected? + bool hasDir; // is there any dir in the files? + bool allNative; // all files are on native UNIX filesystems (not virtual or remote) + QIcon customIcon; // custom (folder) icon (wiil be checked for its nullity) + + std::shared_ptr mimeType; // mime type of the files + + uid_t uid; // owner uid of the files, INVALID_UID means all files do not have the same uid + gid_t gid; // owner gid of the files, INVALID_GID means all files do not have the same uid + mode_t ownerPerm; // read permission of the files, -1 means not all files have the same value + int ownerPermSel; + mode_t groupPerm; // read permission of the files, -1 means not all files have the same value + int groupPermSel; + mode_t otherPerm; // read permission of the files, -1 means not all files have the same value + int otherPermSel; + mode_t execPerm; // exec permission of the files + Qt::CheckState execCheckState; + + Fm::TotalSizeJob* totalSizeJob; // job used to count total size + QTimer* fileSizeTimer; +}; + +} + +#endif // FM_FILEPROPSDIALOG_H diff --git a/src/filesearch.ui b/src/filesearch.ui new file mode 100644 index 0000000..d8c0e1a --- /dev/null +++ b/src/filesearch.ui @@ -0,0 +1,574 @@ + + + SearchDialog + + + + 0 + 0 + 512 + 420 + + + + Search Files + + + + + + + + + + + 0 + + + + Name/Location + + + + + + File Name Patterns: + + + + + + * + + + + + + + Case insensitive + + + + + + + Use regular expression + + + true + + + + + + + + + + Places to Search: + + + + + + + + + + + + + &Add + + + + + + + + + + + + &Remove + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + Search in sub directories + + + + + + + Search for hidden files + + + + + + + + + + + File Type + + + + + + Only search for files of following types: + + + + + + Text files + + + + + + + Image files + + + + + + + Audio files + + + + + + + Video files + + + + + + + Documents + + + + + + + Folders + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Content + + + + + + File contains: + + + + + + + + + Case insensiti&ve + + + + + + + &Use regular expression + + + true + + + + + + + + + + Qt::Vertical + + + + 20 + 186 + + + + + + + + + Properties + + + + + + File Size: + + + + + + + + Smaller than: + + + + + + + false + + + + + + + false + + + + + + + Larger than: + + + + + + + false + + + 2 + + + + Bytes + + + + + KiB + + + + + MiB + + + + + GiB + + + + + + + + false + + + 2 + + + + Bytes + + + + + KiB + + + + + MiB + + + + + GiB + + + + + + + + + + + + + Qt::LeftToRight + + + Last Modified Time: + + + + + + + + Earlier than: + + + + + + + Later than: + + + + + + + false + + + true + + + + + + + false + + + true + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + SearchDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + SearchDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + largerThan + toggled(bool) + minSizeUnit + setEnabled(bool) + + + 93 + 84 + + + 403 + 88 + + + + + smallerThan + toggled(bool) + maxSize + setEnabled(bool) + + + 96 + 119 + + + 241 + 123 + + + + + largerThan + toggled(bool) + minSize + setEnabled(bool) + + + 93 + 84 + + + 241 + 88 + + + + + smallerThan + toggled(bool) + maxSizeUnit + setEnabled(bool) + + + 96 + 119 + + + 403 + 123 + + + + + laterThan + toggled(bool) + minTime + setEnabled(bool) + + + 88 + 223 + + + 319 + 226 + + + + + earlierThan + toggled(bool) + maxTime + setEnabled(bool) + + + 93 + 190 + + + 319 + 193 + + + + + diff --git a/src/filesearchdialog.cpp b/src/filesearchdialog.cpp new file mode 100644 index 0000000..2ee1af7 --- /dev/null +++ b/src/filesearchdialog.cpp @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "filesearchdialog.h" +#include +#include "fm-search.h" +#include "ui_filesearch.h" +#include +#include +#include + +namespace Fm { + +FileSearchDialog::FileSearchDialog(QStringList paths, QWidget* parent, Qt::WindowFlags f): + QDialog(parent, f), + ui(new Ui::SearchDialog()) { + ui->setupUi(this); + ui->minSize->setMaximum(std::numeric_limits().max()); + ui->maxSize->setMaximum(std::numeric_limits().max()); + for(const QString& path : qAsConst(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_ = FilePath{fm_search_to_gfile(search), false}; + + 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 + const auto itemList = ui->listView->selectedItems(); + for(QListWidgetItem* const item : itemList) { + delete item; + } +} + +void FileSearchDialog::setNameCaseInsensitive(bool caseInsensitive) { + ui->nameCaseInsensitive->setChecked(caseInsensitive); +} + +void FileSearchDialog::setContentCaseInsensitive(bool caseInsensitive) { + ui->contentCaseInsensitive->setChecked(caseInsensitive); +} + +void FileSearchDialog::setNameRegexp(bool reg) { + ui->nameRegExp->setChecked(reg); +} + +void FileSearchDialog::setContentRegexp(bool reg) { + ui->contentRegExp->setChecked(reg); +} + +void FileSearchDialog::setRecursive(bool rec) { + ui->recursiveSearch->setChecked(rec); +} + +void FileSearchDialog::setSearchhHidden(bool hidden) { + ui->searchHidden->setChecked(hidden); +} + +bool FileSearchDialog::nameCaseInsensitive() const { + return ui->nameCaseInsensitive->isChecked(); +} + +bool FileSearchDialog::contentCaseInsensitive() const { + return ui->contentCaseInsensitive->isChecked(); +} + +bool FileSearchDialog::nameRegexp() const { + return ui->nameRegExp->isChecked(); +} + +bool FileSearchDialog::contentRegexp() const { + return ui->contentRegExp->isChecked(); +} + +bool FileSearchDialog::recursive() const { + return ui->recursiveSearch->isChecked(); +} + +bool FileSearchDialog::searchhHidden() const { + return ui->searchHidden->isChecked(); +} + +} diff --git a/src/filesearchdialog.h b/src/filesearchdialog.h new file mode 100644 index 0000000..5f6126b --- /dev/null +++ b/src/filesearchdialog.h @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_FILESEARCHDIALOG_H +#define FM_FILESEARCHDIALOG_H + +#include "libfmqtglobals.h" +#include +#include "core/filepath.h" + +namespace Ui { +class SearchDialog; +} + +namespace Fm { + +class LIBFM_QT_API FileSearchDialog : public QDialog { +public: + explicit FileSearchDialog(QStringList paths = QStringList(), QWidget* parent = 0, Qt::WindowFlags f = 0); + ~FileSearchDialog(); + + const FilePath& searchUri() const { + return searchUri_; + } + + virtual void accept(); + + bool nameCaseInsensitive() const; + void setNameCaseInsensitive(bool caseInsensitive); + + bool contentCaseInsensitive() const; + void setContentCaseInsensitive(bool caseInsensitive); + + bool nameRegexp() const; + void setNameRegexp(bool reg); + + bool contentRegexp() const; + void setContentRegexp(bool reg); + + bool recursive() const; + void setRecursive(bool rec); + + bool searchhHidden() const; + void setSearchhHidden(bool hidden); + +private Q_SLOTS: + void onAddPath(); + void onRemovePath(); + +private: + Ui::SearchDialog* ui; + FilePath searchUri_; +}; + +} + +#endif // FM_FILESEARCHDIALOG_H diff --git a/src/fm-search.c b/src/fm-search.c new file mode 100644 index 0000000..4fa20d7 --- /dev/null +++ b/src/fm-search.c @@ -0,0 +1,317 @@ +/* + * fm-search-uri.c + * + * Copyright 2015 Hong Jen Yee (PCMan) + * Copyright 2012-2014 Andriy Grytsenko (LStranger) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "fm-search.h" +#include + +struct _FmSearch +{ + gboolean recursive; + gboolean show_hidden; + char* name_patterns; + gboolean name_ci; + gboolean name_regex; + char* content_pattern; + gboolean content_ci; + gboolean content_regex; + GList* mime_types; + GList* search_path_list; + guint64 max_size; + guint64 min_size; + char* max_mtime; + char* min_mtime; +}; + +FmSearch* fm_search_new (void) +{ + FmSearch* search = (FmSearch*)g_slice_new0(FmSearch); + return search; +} + +void fm_search_free(FmSearch* search) +{ + g_list_free_full(search->mime_types, (GDestroyNotify)g_free); + g_list_free_full(search->search_path_list, (GDestroyNotify)g_free); + g_free(search->name_patterns); + g_free(search->content_pattern); + g_free(search->max_mtime); + g_free(search->min_mtime); + g_slice_free(FmSearch, search); +} + +gboolean fm_search_get_recursive(FmSearch* search) +{ + return search->recursive; +} + +void fm_search_set_recursive(FmSearch* search, gboolean recursive) +{ + search->recursive = recursive; +} + +gboolean fm_search_get_show_hidden(FmSearch* search) +{ + return search->show_hidden; +} + +void fm_search_set_show_hidden(FmSearch* search, gboolean show_hidden) +{ + search->show_hidden = show_hidden; +} + +const char* fm_search_get_name_patterns(FmSearch* search) +{ + return search->name_patterns; +} + +void fm_search_set_name_patterns(FmSearch* search, const char* name_patterns) +{ + g_free(search->name_patterns); + search->name_patterns = g_strdup(name_patterns); +} + +gboolean fm_search_get_name_ci(FmSearch* search) +{ + return search->name_ci; +} + +void fm_search_set_name_ci(FmSearch* search, gboolean name_ci) +{ + search->name_ci = name_ci; +} + +gboolean fm_search_get_name_regex(FmSearch* search) +{ + return search->name_regex; +} + +void fm_search_set_name_regex(FmSearch* search, gboolean name_regex) +{ + search->name_regex = name_regex; +} + +const char* fm_search_get_content_pattern(FmSearch* search) +{ + return search->content_pattern; +} + +void fm_search_set_content_pattern(FmSearch* search, const char* content_pattern) +{ + g_free(search->content_pattern); + search->content_pattern = g_strdup(content_pattern); +} + +gboolean fm_search_get_content_ci(FmSearch* search) +{ + return search->content_ci; +} + +void fm_search_set_content_ci(FmSearch* search, gboolean content_ci) +{ + search->content_ci = content_ci; +} + +gboolean fm_search_get_content_regex(FmSearch* search) +{ + return search->content_regex; +} + +void fm_search_set_content_regex(FmSearch* search, gboolean content_regex) +{ + search->content_regex = content_regex; +} + +void fm_search_add_dir(FmSearch* search, const char* dir) +{ + GList* l = g_list_find_custom(search->search_path_list, dir, (GCompareFunc)strcmp); + if(!l) + search->search_path_list = g_list_prepend(search->search_path_list, g_strdup(dir)); +} + +void fm_search_remove_dir(FmSearch* search, const char* dir) +{ + GList* l = g_list_find_custom(search->search_path_list, dir, (GCompareFunc)strcmp); + if(G_LIKELY(l)) + { + g_free(l->data); + search->search_path_list = g_list_delete_link(search->search_path_list, l); + } +} + +GList* fm_search_get_dirs(FmSearch* search) +{ + return search->search_path_list; +} + +void fm_search_add_mime_type(FmSearch* search, const char* mime_type) +{ + GList* l = g_list_find_custom(search->mime_types, mime_type, (GCompareFunc)strcmp); + if(!l) + search->mime_types = g_list_prepend(search->mime_types, g_strdup(mime_type)); +} + +void fm_search_remove_mime_type(FmSearch* search, const char* mime_type) +{ + GList* l = g_list_find_custom(search->mime_types, mime_type, (GCompareFunc)strcmp); + if(G_LIKELY(l)) + { + g_free(l->data); + search->mime_types = g_list_delete_link(search->mime_types, l); + } +} + +GList* fm_search_get_mime_types(FmSearch* search) +{ + return search->mime_types; +} + +guint64 fm_search_get_max_size(FmSearch* search) +{ + return search->max_size; +} + +void fm_search_set_max_size(FmSearch* search, guint64 size) +{ + search->max_size = size; +} + +guint64 fm_search_get_min_size(FmSearch* search) +{ + return search->min_size; +} + +void fm_search_set_min_size(FmSearch* search, guint64 size) +{ + search->min_size = size; +} + +/* format of mtime: YYYY-MM-DD */ +const char* fm_search_get_max_mtime(FmSearch* search) +{ + return search->max_mtime; +} + +void fm_search_set_max_mtime(FmSearch* search, const char* mtime) +{ + g_free(search->max_mtime); + search->max_mtime = g_strdup(mtime); +} + +/* format of mtime: YYYY-MM-DD */ +const char* fm_search_get_min_mtime(FmSearch* search) +{ + return search->min_mtime; +} + +void fm_search_set_min_mtime(FmSearch* search, const char* mtime) +{ + g_free(search->min_mtime); + search->min_mtime = g_strdup(mtime); +} + +/* really build the path */ +GFile* fm_search_to_gfile(FmSearch* search) +{ + GFile* 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 = g_file_new_for_uri(search_str->str); + g_string_free(search_str, TRUE); + } + return search_path; +} diff --git a/src/fm-search.h b/src/fm-search.h new file mode 100644 index 0000000..f85b152 --- /dev/null +++ b/src/fm-search.h @@ -0,0 +1,89 @@ +/* + * fm-search-uri.h + * + * Copyright 2015 Hong Jen Yee (PCMan) + * Copyright 2012-2014 Andriy Grytsenko (LStranger) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +/* FmSearch implements a tool used to generate a search:// URI used by libfm to search for files. + * This API might become part of libfm in the future. + */ + +#ifndef _FM_SEARCH_H_ +#define _FM_SEARCH_H_ + +#include + +G_BEGIN_DECLS + +typedef struct _FmSearch FmSearch; + +FmSearch* fm_search_new(void); +void fm_search_free(FmSearch* search); + +GFile* fm_search_to_gfile(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/folderitemdelegate.cpp b/src/folderitemdelegate.cpp new file mode 100644 index 0000000..4af7f75 --- /dev/null +++ b/src/folderitemdelegate.cpp @@ -0,0 +1,517 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "folderitemdelegate.h" +#include "foldermodel.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Fm { + +FolderItemDelegate::FolderItemDelegate(QAbstractItemView* view, QObject* parent): + QStyledItemDelegate(parent ? parent : view), + symlinkIcon_(QIcon::fromTheme("emblem-symbolic-link")), + untrustedIcon_(QIcon::fromTheme("emblem-important")), + addIcon_(QIcon::fromTheme("list-add")), + removeIcon_(QIcon::fromTheme("list-remove")), + fileInfoRole_(Fm::FolderModel::FileInfoRole), + iconInfoRole_(-1), + margins_(QSize(3, 3)), + shadowHidden_(false), + hasEditor_(false) { + connect(this, &QAbstractItemDelegate::closeEditor, [=]{hasEditor_ = false;}); +} + +FolderItemDelegate::~FolderItemDelegate() { + +} + +QSize FolderItemDelegate::iconViewTextSize(const QModelIndex& index) const { + QStyleOptionViewItem opt; + initStyleOption(&opt, index); + opt.decorationSize = iconSize_.isValid() ? iconSize_ : QSize(0, 0); + opt.decorationAlignment = Qt::AlignHCenter | Qt::AlignTop; + opt.displayAlignment = Qt::AlignTop | Qt::AlignHCenter; + QRectF textRect(0, 0, + itemSize_.width() - 2 * margins_.width(), + itemSize_.height() - 2 * margins_.height() - opt.decorationSize.height()); + drawText(nullptr, opt, textRect); // passing nullptr for painter will calculate the bounding rect only + return textRect.toRect().size(); +} + +QSize FolderItemDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const { + QVariant value = index.data(Qt::SizeHintRole); + if(value.isValid()) { + // no further processing if the size is specified by the data model + return qvariant_cast(value); + } + + if(option.decorationPosition == QStyleOptionViewItem::Top || + option.decorationPosition == QStyleOptionViewItem::Bottom) { + // we handle vertical layout just by returning our item size + return itemSize_; + } + + // The default size hint of the horizontal layout isn't reliable + // because Qt calculates the row size based on the real icon size, + // which may not be equal to the requested icon size on various occasions. + // So, we do as in QStyledItemDelegate::sizeHint() but use the requested size. + QStyleOptionViewItem opt = option; + initStyleOption(&opt, index); + opt.decorationSize = option.decorationSize; // requested by the view + const QWidget* widget = option.widget; + QStyle* style = widget ? widget->style() : QApplication::style(); + return style->sizeFromContents(QStyle::CT_ItemViewItem, &opt, QSize(), widget); +} + +QIcon::Mode FolderItemDelegate::iconModeFromState(const QStyle::State state) { + + if(state & QStyle::State_Enabled) { + return (state & QStyle::State_Selected) ? QIcon::Selected : QIcon::Normal; + } + + return QIcon::Disabled; +} + +void FolderItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { + if(!index.isValid()) + return; + + // get emblems for this icon + std::forward_list> icon_emblems; + auto fmicon = index.data(iconInfoRole_).value>(); + if(fmicon) { + icon_emblems = fmicon->emblems(); + } + // get file info for the item + auto file = index.data(fileInfoRole_).value>(); + const auto& emblems = file ? file->emblems() : icon_emblems; + + QStyleOptionViewItem opt = option; + initStyleOption(&opt, index); + + // distinguish the hidden items visually by making their texts italic + bool shadowIcon(false); + if(file && file->isHidden()) { + QFont f(opt.font); + f.setItalic(true); + opt.font = f; + shadowIcon = shadowHidden_; + } + + bool isSymlink = file && file->isSymlink(); + bool isCut = index.data(FolderModel::FileIsCutRole).toBool(); + // for practical reasons, an emblem is added only to an untrusted, deletable desktop file + bool untrusted = file && !file->isTrustable() && file->isDesktopEntry() && file->isDeletable(); + // vertical layout (icon mode, thumbnail mode) + if(option.decorationPosition == QStyleOptionViewItem::Top || + option.decorationPosition == QStyleOptionViewItem::Bottom) { + painter->save(); + painter->setClipRect(option.rect); + + opt.decorationAlignment = Qt::AlignHCenter | Qt::AlignTop; + opt.displayAlignment = Qt::AlignTop | Qt::AlignHCenter; + + // draw the icon + QIcon::Mode iconMode = shadowIcon ? QIcon::Disabled : iconModeFromState(opt.state); + QPoint iconPos(opt.rect.x() + (opt.rect.width() - option.decorationSize.width()) / 2, opt.rect.y() + margins_.height()); + QPixmap pixmap = opt.icon.pixmap(option.decorationSize, iconMode); + // in case the pixmap is smaller than the requested size + QSize margin = ((option.decorationSize - pixmap.size()) / 2).expandedTo(QSize(0, 0)); + if(isCut) { + painter->save(); + painter->setOpacity(0.45); + } + painter->drawPixmap(iconPos + QPoint(margin.width(), margin.height()), pixmap); + if(isCut) { + painter->restore(); + } + + // draw some emblems for the item if needed + if(isSymlink) { + // draw the emblem for symlinks + painter->drawPixmap(iconPos, symlinkIcon_.pixmap(option.decorationSize / 2, iconMode)); + } + + if(untrusted) { + // emblem for untrusted, deletable desktop files + painter->drawPixmap(iconPos.x(), opt.rect.y() + option.decorationSize.height() / 2, untrustedIcon_.pixmap(option.decorationSize / 2, iconMode)); + } + + // draw other emblems if there's any + if(!emblems.empty()) { + // FIXME: we only support one emblem now + QPoint emblemPos(opt.rect.x() + opt.rect.width() / 2, opt.rect.y() + option.decorationSize.height() / 2); + QIcon emblem = emblems.front()->qicon(); + painter->drawPixmap(emblemPos, emblem.pixmap(option.decorationSize / 2, iconMode)); + } + + // Draw select/deselect icons outside the main icon but near its top left corner, + // with its 1/3 size and only if the icon size isn't smaller than 48 px + // (otherwise, the user could not click on them easily). + if(option.decorationSize.width() >= 48 && opt.state & QStyle::State_MouseOver) { + int s = option.decorationSize.width() / 3; + bool cursorOnSelectionCorner = false; + iconPos = QPoint(qMax(opt.rect.x(), iconPos.x() - s), + qMax(opt.rect.y(), iconPos.y() - s)); + if(const QAbstractItemView* iv = qobject_cast(opt.widget)) { + QPoint curPos = iv->viewport()->mapFromGlobal(QCursor::pos()); + if(curPos.x() >= iconPos.x() && curPos.x() <= iconPos.x() + s + && curPos.y() >= iconPos.y() && curPos.y() <= iconPos.y() + s) { + cursorOnSelectionCorner = true; + } + } + if(!cursorOnSelectionCorner) { // make it translucent when not under the cursor + painter->save(); + painter->setOpacity(0.6); + } + if(opt.state & QStyle::State_Selected) { + painter->drawPixmap(iconPos, removeIcon_.pixmap(QSize(s, s), QIcon::Normal)); + } + else { + painter->drawPixmap(iconPos, addIcon_.pixmap(QSize(s, s), QIcon::Normal)); + } + if(!cursorOnSelectionCorner) { + painter->restore(); + } + } + + // draw the text + QSize drawAreaSize = itemSize_ - 2 * margins_; + // The text rect dimensions should be exactly as they were in sizeHint() + QRectF textRect(opt.rect.x() + (opt.rect.width() - drawAreaSize.width()) / 2, + opt.rect.y() + margins_.height() + option.decorationSize.height(), + drawAreaSize.width(), + drawAreaSize.height() - option.decorationSize.height()); + drawText(painter, opt, textRect); + painter->restore(); + } + else { // horizontal layout (list view) + + // Let the style engine do the painting but take care of shadowed and cut icons. + // NOTE: The shadowing can also be done directly. + // WARNING: QStyledItemDelegate shouldn't be used for painting because it resets the icon. + // FIXME: For better text alignment, here we could have increased the icon width + // when it's smaller than the requested size. + + QIcon::Mode iconMode = shadowIcon ? QIcon::Disabled : iconModeFromState(opt.state); + + if(!opt.icon.isNull()) { + if(shadowIcon) { + QPixmap pixmap = opt.icon.pixmap(option.decorationSize, iconMode); + opt.icon = QIcon(pixmap); + } + + if(isCut) { + QPixmap pixmap = opt.icon.pixmap(option.decorationSize, iconMode); + QImage img = pixmap.toImage().convertToFormat(QImage::Format_ARGB32_Premultiplied); + img.fill(Qt::transparent); + QPainter p(&img); + p.setOpacity(0.45); + p.drawPixmap(0, 0, pixmap); + p.end(); + opt.icon = QIcon(QPixmap::fromImage(img)); + } + } + + const QWidget* widget = opt.widget; + QStyle* style = widget ? widget->style() : QApplication::style(); + style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, widget); + + if(isCut && !opt.icon.isNull()) { + // restore the original icon to not make it translucent more than once + opt.icon = option.icon; + } + + // 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)); + } + if(untrusted) { + QPoint iconPos(opt.rect.x(), opt.rect.y() + opt.rect.height() / 2); + painter->drawPixmap(iconPos, untrustedIcon_.pixmap(option.decorationSize / 2, iconMode)); + } + if(!emblems.empty()) { + // FIXME: we only support one emblem now + QPoint iconPos(opt.rect.x() + option.decorationSize.width() / 2, opt.rect.y() + opt.rect.height() / 2); + QIcon emblem = emblems.front()->qicon(); + painter->drawPixmap(iconPos, emblem.pixmap(option.decorationSize / 2, iconMode)); + } + } +} + +// if painter is nullptr, the method calculate the bounding rectangle of the text and save it to textRect +void FolderItemDelegate::drawText(QPainter* painter, QStyleOptionViewItem& opt, QRectF& textRect) const { + QTextLayout layout(opt.text, opt.font); + QTextOption textOption; + textOption.setAlignment(opt.displayAlignment); + textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); + // FIXME: textOption.setTextDirection(opt.direction); ? + if(opt.text.isRightToLeft()) { + textOption.setTextDirection(Qt::RightToLeft); + } + else { + textOption.setTextDirection(Qt::LeftToRight); + } + layout.setTextOption(textOption); + qreal height = 0; + qreal width = 0; + int visibleLines = 0; + layout.beginLayout(); + QString elidedText; + textRect.adjust(2, 2, -2, -2); // a 2-px margin is considered at FolderView::updateGridSize() + for(;;) { + QTextLine line = layout.createLine(); + if(!line.isValid()) { + break; + } + line.setLineWidth(textRect.width()); + height += opt.fontMetrics.leading(); + line.setPosition(QPointF(0, height)); + if((height + line.height() + textRect.y()) > textRect.bottom()) { + // if part of this line falls outside the textRect, ignore it and quit. + QTextLine lastLine = layout.lineAt(visibleLines - 1); + elidedText = opt.text.mid(lastLine.textStart()); + elidedText = opt.fontMetrics.elidedText(elidedText, opt.textElideMode, textRect.width()); + if(visibleLines == 1) { // this is the only visible line + width = textRect.width(); + } + break; + } + height += line.height(); + width = qMax(width, line.naturalTextWidth()); + ++ visibleLines; + } + layout.endLayout(); + width = qMax(width, (qreal)opt.fontMetrics.width(elidedText)); + + // draw background for selected item + QRectF boundRect = layout.boundingRect(); + //qDebug() << "bound rect: " << boundRect << "width: " << width; + boundRect.setWidth(width); + boundRect.setHeight(height); + boundRect.moveTo(textRect.x() + (textRect.width() - width) / 2, textRect.y()); + + QRectF selRect = boundRect.adjusted(-2, -2, 2, 2); + + if(!painter) { // no painter, calculate the bounding rect only + textRect = selRect; + return; + } + + // Respect the active and inactive palettes (some styles can use different colors for them). + // Also, take into account a probable disabled palette. + QPalette::ColorGroup cg = (opt.state & QStyle::State_Enabled) + ? (opt.state & QStyle::State_Active) + ? QPalette::Active + : QPalette::Inactive + : QPalette::Disabled; + if(opt.state & QStyle::State_Selected) { + if(!opt.widget) { + painter->fillRect(selRect, opt.palette.highlight()); + } + painter->setPen(opt.palette.color(cg, QPalette::HighlightedText)); + } + else { + painter->setPen(opt.palette.color(cg, QPalette::Text)); + } + + if(opt.state & QStyle::State_Selected || opt.state & QStyle::State_MouseOver) { + if(const QWidget* widget = opt.widget) { // let the style engine do it + QStyle* style = widget->style() ? widget->style() : qApp->style(); + QStyleOptionViewItem o(opt); + o.text = QString(); + o.rect = selRect.toAlignedRect().intersected(opt.rect); // due to clipping and rounding, we might lose 1px + o.showDecorationSelected = true; + style->drawPrimitive(QStyle::PE_PanelItemViewItem, &o, painter, widget); + } + } + + // draw shadow for text if the item is not selected and a shadow color is set + if(!(opt.state & QStyle::State_Selected) && shadowColor_.isValid()) { + QPen prevPen = painter->pen(); + painter->setPen(QPen(shadowColor_)); + for(int i = 0; i < visibleLines; ++i) { + QTextLine line = layout.lineAt(i); + if(i == (visibleLines - 1) && !elidedText.isEmpty()) { // the last line, draw elided text + QPointF pos(boundRect.x() + line.position().x() + 1, boundRect.y() + line.y() + line.ascent() + 1); + painter->drawText(pos, elidedText); + } + else { + line.draw(painter, textRect.topLeft() + QPointF(1, 1)); + } + } + painter->setPen(prevPen); + } + + // draw text + for(int i = 0; i < visibleLines; ++i) { + QTextLine line = layout.lineAt(i); + if(i == (visibleLines - 1) && !elidedText.isEmpty()) { // the last line, draw elided text + QPointF pos(boundRect.x() + line.position().x(), boundRect.y() + line.y() + line.ascent()); + painter->drawText(pos, elidedText); + } + else { + line.draw(painter, textRect.topLeft()); + } + } + + if(opt.state & QStyle::State_HasFocus) { + // draw focus rect + QStyleOptionFocusRect o; + o.QStyleOption::operator=(opt); + o.rect = selRect.toRect(); // subElementRect(SE_ItemViewItemFocusRect, vopt, widget); + o.state |= QStyle::State_KeyboardFocusChange; + o.state |= QStyle::State_Item; + QPalette::ColorGroup cg = (opt.state & QStyle::State_Enabled) + ? QPalette::Normal : QPalette::Disabled; + o.backgroundColor = opt.palette.color(cg, (opt.state & QStyle::State_Selected) + ? QPalette::Highlight : QPalette::Window); + if(const QWidget* widget = opt.widget) { + QStyle* style = widget->style() ? widget->style() : qApp->style(); + style->drawPrimitive(QStyle::PE_FrameFocusRect, &o, painter, widget); + } + } +} + +/* + * The following methods are for inline renaming. + */ + +QWidget* FolderItemDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const { + hasEditor_ = true; + if (option.decorationPosition == QStyleOptionViewItem::Top + || option.decorationPosition == QStyleOptionViewItem::Bottom) + { + // in icon view, we use QTextEdit as the editor (and not QPlainTextEdit + // because the latter always shows an empty space at the bottom) + QTextEdit *textEdit = new QTextEdit(parent); + textEdit->setAcceptRichText(false); + + // Since the text color on desktop is inherited from desktop foreground color, + // it may not be suitable. So, we reset it by using the app palette. + QPalette p = textEdit->palette(); + p.setColor(QPalette::Text, qApp->palette().text().color()); + textEdit->setPalette(p); + + textEdit->ensureCursorVisible(); + textEdit->setFocusPolicy(Qt::StrongFocus); + textEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + textEdit->setContentsMargins(0, 0, 0, 0); + return textEdit; + } + else { + // return the default line-edit in other views and + // ensure that its background isn't transparent (on the side-pane) + QWidget* editor = QStyledItemDelegate::createEditor(parent, option, index); + QPalette p = editor->palette(); + p.setColor(QPalette::Text, qApp->palette().text().color()); + p.setColor(QPalette::Base, qApp->palette().color(QPalette::Base)); + editor->setPalette(p); + return editor; + } +} + +void FolderItemDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const { + if (!index.isValid()) { + return; + } + const QString currentName = index.data(Qt::EditRole).toString(); + + if (QTextEdit* textEdit = qobject_cast(editor)) { + textEdit->setPlainText(currentName); + textEdit->setUndoRedoEnabled(false); + textEdit->setAlignment(Qt::AlignCenter); + textEdit->setUndoRedoEnabled(true); + // select text appropriately + QTextCursor cur = textEdit->textCursor(); + int end; + if (index.data(Fm::FolderModel::FileIsDirRole).toBool() || !currentName.contains(".")) { + end = currentName.size(); + } + else { + end = currentName.lastIndexOf("."); + } + cur.setPosition(end, QTextCursor::KeepAnchor); + textEdit->setTextCursor(cur); + } + else if (QLineEdit* lineEdit = qobject_cast(editor)) { + lineEdit->setText(currentName); + if (!index.data(Fm::FolderModel::FileIsDirRole).toBool() && currentName.contains(".")) + { + /* Qt will call QLineEdit::selectAll() after calling setEditorData() in + qabstractitemview.cpp -> QAbstractItemViewPrivate::editor(). Therefore, + we cannot select a part of the text in the usual way here. */ + QTimer::singleShot(0, [lineEdit]() { + int length = lineEdit->text().lastIndexOf("."); + lineEdit->setSelection(0, length); + }); + } + } +} + +bool FolderItemDelegate::eventFilter(QObject* object, QEvent* event) { + QWidget *editor = qobject_cast(object); + if (editor && event->type() == QEvent::KeyPress) { + int k = static_cast(event)->key(); + if (k == Qt::Key_Return || k == Qt::Key_Enter) { + Q_EMIT QAbstractItemDelegate::commitData(editor); + Q_EMIT QAbstractItemDelegate::closeEditor(editor, QAbstractItemDelegate::NoHint); + return true; + } + } + return QStyledItemDelegate::eventFilter(object, event); +} + +void FolderItemDelegate::updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const { + if (option.decorationPosition == QStyleOptionViewItem::Top + || option.decorationPosition == QStyleOptionViewItem::Bottom) { + // give all of the available space to the editor + QStyleOptionViewItem opt = option; + initStyleOption(&opt, index); + opt.decorationAlignment = Qt::AlignHCenter|Qt::AlignTop; + opt.displayAlignment = Qt::AlignTop|Qt::AlignHCenter; + QRect textRect(opt.rect.x(), + opt.rect.y() + margins_.height() + option.decorationSize.height(), + itemSize_.width(), + itemSize_.height() - margins_.height() - option.decorationSize.height()); + int frame = editor->style()->pixelMetric(QStyle::PM_DefaultFrameWidth, &option, editor); + editor->setGeometry(textRect.adjusted(-frame, -frame, frame, frame)); + } + else { + // use the default editor geometry in compact view + QStyledItemDelegate::updateEditorGeometry(editor, option, index); + } +} + + +} // namespace Fm diff --git a/src/folderitemdelegate.h b/src/folderitemdelegate.h new file mode 100644 index 0000000..90756ed --- /dev/null +++ b/src/folderitemdelegate.h @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_FOLDERITEMDELEGATE_H +#define FM_FOLDERITEMDELEGATE_H + +#include "libfmqtglobals.h" +#include +class QAbstractItemView; + +namespace Fm { + +class LIBFM_QT_API FolderItemDelegate : public QStyledItemDelegate { + Q_OBJECT +public: + explicit FolderItemDelegate(QAbstractItemView* view, QObject* parent = nullptr); + + virtual ~FolderItemDelegate(); + + inline void setItemSize(QSize size) { + itemSize_ = size; + } + + inline QSize itemSize() const { + return itemSize_; + } + + inline void setIconSize(QSize size) { + iconSize_ = size; + } + + inline QSize iconSize() const { + return iconSize_; + } + + int fileInfoRole() { + return fileInfoRole_; + } + + void setFileInfoRole(int role) { + fileInfoRole_ = role; + } + + int iconInfoRole() { + return iconInfoRole_; + } + + void setIconInfoRole(int role) { + iconInfoRole_ = role; + } + + // only support vertical layout (icon view mode: text below icon) + void setShadowColor(const QColor& shadowColor) { + shadowColor_ = shadowColor; + } + + // only support vertical layout (icon view mode: text below icon) + const QColor& shadowColor() const { + return shadowColor_; + } + + // only support vertical layout (icon view mode: text below icon) + void setMargins(QSize margins) { + margins_ = margins.expandedTo(QSize(0, 0)); + } + + QSize getMargins() const { + return margins_; + } + + bool hasEditor() const { + return hasEditor_; + } + + void setShadowHidden(bool value) { + shadowHidden_ = value; + } + + virtual QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const; + + virtual void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const; + + virtual QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const; + + virtual void setEditorData(QWidget* editor, const QModelIndex& index) const; + + virtual bool eventFilter(QObject* object, QEvent* event); + + virtual void updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const; + + QSize iconViewTextSize(const QModelIndex& index) const; + +private: + void drawText(QPainter* painter, QStyleOptionViewItem& opt, QRectF& textRect) const; + + static QIcon::Mode iconModeFromState(QStyle::State state); + +private: + QIcon symlinkIcon_; + QIcon untrustedIcon_; + QIcon addIcon_; + QIcon removeIcon_; + QSize iconSize_; + QSize itemSize_; + int fileInfoRole_; + int iconInfoRole_; + QColor shadowColor_; + QSize margins_; + bool shadowHidden_; + mutable bool hasEditor_; +}; + +} + +#endif // FM_FOLDERITEMDELEGATE_H diff --git a/src/foldermenu.cpp b/src/foldermenu.cpp new file mode 100644 index 0000000..86675d5 --- /dev/null +++ b/src/foldermenu.cpp @@ -0,0 +1,300 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * Copyright (C) 2012 - 2014 Andriy Grytsenko (LStranger) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "foldermenu.h" +#include "createnewmenu.h" +#include "filepropsdialog.h" +#include "folderview.h" +#include "utilities.h" +#include // for memset +#include +#include "customaction_p.h" +#include "customactions/fileaction.h" +#include + +namespace Fm { + +FolderMenu::FolderMenu(FolderView* view, QWidget* parent): + QMenu(parent), + view_(view) { + + ProxyFolderModel* model = view_->model(); + + createAction_ = new QAction(tr("Create &New"), this); + addAction(createAction_); + + createAction_->setMenu(new CreateNewMenu(view_, view_->path(), this)); + + separator1_ = addSeparator(); + + pasteAction_ = new QAction(QIcon::fromTheme("edit-paste"), tr("&Paste"), this); + addAction(pasteAction_); + connect(pasteAction_, &QAction::triggered, this, &FolderMenu::onPasteActionTriggered); + + separator2_ = addSeparator(); + + selectAllAction_ = new QAction(tr("Select &All"), this); + addAction(selectAllAction_); + connect(selectAllAction_, &QAction::triggered, this, &FolderMenu::onSelectAllActionTriggered); + + invertSelectionAction_ = new QAction(tr("Invert Selection"), this); + addAction(invertSelectionAction_); + connect(invertSelectionAction_, &QAction::triggered, this, &FolderMenu::onInvertSelectionActionTriggered); + + separator3_ = addSeparator(); + + sortAction_ = new QAction(tr("Sorting"), this); + addAction(sortAction_); + createSortMenu(); + sortAction_->setMenu(sortMenu_); + + showHiddenAction_ = new QAction(tr("Show Hidden"), this); + addAction(showHiddenAction_); + showHiddenAction_->setCheckable(true); + showHiddenAction_->setChecked(model->showHidden()); + connect(showHiddenAction_, &QAction::triggered, this, &FolderMenu::onShowHiddenActionTriggered); + + auto folderInfo = view_->folderInfo(); + if(folderInfo) { // should never be null (see FolderView::onFileClicked) + // DES-EMA custom actions integration + FileInfoList files; + files.push_back(folderInfo); + auto custom_actions = FileActionItem::get_actions_for_files(files); + for(auto& item: custom_actions) { + if(item && !(item->get_target() & FILE_ACTION_TARGET_CONTEXT)) { + continue; // this item is not for context menu + } + if(item == custom_actions.front() && item && !item->is_action()) { + addSeparator(); // before all custom actions + } + addCustomActionItem(this, item); + } + + // disable paste acton if it can't be used + pasteAction_->setEnabled(folderInfo->isWritable()); + } + + separator4_ = addSeparator(); + + propertiesAction_ = new QAction(tr("Folder Pr&operties"), this); + addAction(propertiesAction_); + connect(propertiesAction_, &QAction::triggered, this, &FolderMenu::onPropertiesActionTriggered); +} + +FolderMenu::~FolderMenu() { +} + +void FolderMenu::addCustomActionItem(QMenu* menu, std::shared_ptr item) { + if(!item) { + return; + } + if(item->is_action() && !(item->get_target() & FILE_ACTION_TARGET_CONTEXT)) { + return; + } + + CustomAction* action = new CustomAction(item, menu); + menu->addAction(action); + if(item->is_menu()) { + auto& subitems = item->get_sub_items(); + if(!subitems.empty()) { + QMenu* submenu = new QMenu(menu); + for(auto& subitem: subitems) { + addCustomActionItem(submenu, subitem); + } + action->setMenu(submenu); + } + } + else if(item->is_action()) { + connect(action, &QAction::triggered, this, &FolderMenu::onCustomActionTrigerred); + } +} + +void FolderMenu::onCustomActionTrigerred() { + CustomAction* action = static_cast(sender()); + auto& item = action->item(); + auto folderInfo = view_->folderInfo(); + if(folderInfo) { + CStrPtr output; + FileInfoList file_list; + file_list.push_back(folderInfo); + item->launch(nullptr, file_list, output); + if(output) { + QMessageBox::information(this, tr("Output"), output.get()); + } + } +} + +void FolderMenu::addSortMenuItem(const QString &title, int id) { + QAction* action = new QAction(title, this); + action->setData(QVariant(id)); + sortMenu_->addAction(action); + action->setCheckable(true); + action->setChecked(id == view_->model()->sortColumn()); + sortActionGroup_->addAction(action); + connect(action, &QAction::triggered, this, &FolderMenu::onSortActionTriggered); +} + +void FolderMenu::createSortMenu() { + ProxyFolderModel* model = view_->model(); + + sortMenu_ = new QMenu(this); + sortActionGroup_ = new QActionGroup(sortMenu_); + sortActionGroup_->setExclusive(true); + + 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); + addSortMenuItem(tr("By File Group"), FolderModel::ColumnFileGroup); + + sortMenu_->addSeparator(); + + QActionGroup* group = new QActionGroup(this); + group->setExclusive(true); + actionAscending_ = new QAction(tr("Ascending"), this); + actionAscending_->setCheckable(true); + sortMenu_->addAction(actionAscending_); + group->addAction(actionAscending_); + + actionDescending_ = new QAction(tr("Descending"), this); + actionDescending_->setCheckable(true); + sortMenu_->addAction(actionDescending_); + group->addAction(actionDescending_); + + if(model->sortOrder() == Qt::AscendingOrder) { + actionAscending_->setChecked(true); + } + else { + actionDescending_->setChecked(true); + } + + connect(actionAscending_, &QAction::triggered, this, &FolderMenu::onSortOrderActionTriggered); + connect(actionDescending_, &QAction::triggered, this, &FolderMenu::onSortOrderActionTriggered); + + sortMenu_->addSeparator(); + + QAction* actionFolderFirst = new QAction(tr("Folder First"), this); + sortMenu_->addAction(actionFolderFirst); + actionFolderFirst->setCheckable(true); + + if(model->folderFirst()) { + actionFolderFirst->setChecked(true); + } + + connect(actionFolderFirst, &QAction::triggered, this, &FolderMenu::onFolderFirstActionTriggered); + + QAction* actionCaseSensitive = new QAction(tr("Case Sensitive"), this); + sortMenu_->addAction(actionCaseSensitive); + actionCaseSensitive->setCheckable(true); + + if(model->sortCaseSensitivity() == Qt::CaseSensitive) { + actionCaseSensitive->setChecked(true); + } + + connect(actionCaseSensitive, &QAction::triggered, this, &FolderMenu::onCaseSensitiveActionTriggered); +} + +void FolderMenu::onPasteActionTriggered() { + auto folderPath = view_->path(); + if(folderPath) { + pasteFilesFromClipboard(folderPath); + } +} + +void FolderMenu::onSelectAllActionTriggered() { + view_->selectAll(); +} + +void FolderMenu::onInvertSelectionActionTriggered() { + view_->invertSelection(); +} + +void FolderMenu::onSortActionTriggered(bool /*checked*/) { + ProxyFolderModel* model = view_->model(); + + if(model && sortActionGroup_) { + QAction* action = static_cast(sender()); + + const auto actions = sortActionGroup_->actions(); + for(auto a : actions) { + if(a == action) { + int col = a->data().toInt(); + if(col >= 0 && col < FolderModel::NumOfColumns) { + model->sort(col, model->sortOrder()); + } + break; + } + } + } +} + +void FolderMenu::onSortOrderActionTriggered(bool /*checked*/) { + ProxyFolderModel* model = view_->model(); + + if(model) { + QAction* action = static_cast(sender()); + Qt::SortOrder order; + + if(action == actionAscending_) { + order = Qt::AscendingOrder; + } + else { + order = Qt::DescendingOrder; + } + + model->sort(model->sortColumn(), order); + } +} + +void FolderMenu::onShowHiddenActionTriggered(bool checked) { + ProxyFolderModel* model = view_->model(); + + if(model) { + qDebug("show hidden: %d", checked); + model->setShowHidden(checked); + } +} + +void FolderMenu::onCaseSensitiveActionTriggered(bool checked) { + ProxyFolderModel* model = view_->model(); + + if(model) { + model->setSortCaseSensitivity(checked ? Qt::CaseSensitive : Qt::CaseInsensitive); + } +} + +void FolderMenu::onFolderFirstActionTriggered(bool checked) { + ProxyFolderModel* model = view_->model(); + + if(model) { + model->setFolderFirst(checked); + } +} + +void FolderMenu::onPropertiesActionTriggered() { + auto folderInfo = view_->folderInfo(); + if(folderInfo) { + FilePropsDialog::showForFile(folderInfo); + } +} + +} // namespace Fm diff --git a/src/foldermenu.h b/src/foldermenu.h new file mode 100644 index 0000000..eb7ec4c --- /dev/null +++ b/src/foldermenu.h @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_FOLDERMENU_H +#define FM_FOLDERMENU_H + +#include "libfmqtglobals.h" +#include +#include "foldermodel.h" + +class QAction; + +namespace Fm { + +class FolderView; +class FileActionItem; + +class LIBFM_QT_API FolderMenu : public QMenu { + Q_OBJECT + +public: + explicit FolderMenu(FolderView* view, QWidget* parent = 0); + virtual ~FolderMenu(); + + QAction* createAction() { + return createAction_; + } + + QAction* separator1() { + return separator1_; + } + + QAction* pasteAction() { + return pasteAction_; + } + + QAction* separator2() { + return separator2_; + } + + QAction* selectAllAction() { + return selectAllAction_; + } + + QAction* invertSelectionAction() { + return invertSelectionAction_; + } + + QAction* separator3() { + return separator3_; + } + + QAction* sortAction() { + return sortAction_; + } + + QAction* showHiddenAction() { + return showHiddenAction_; + } + + QAction* separator4() { + return separator4_; + } + + QAction* propertiesAction() { + return propertiesAction_; + } + + FolderView* view() { + return view_; + } + +protected: + void addCustomActionItem(QMenu* menu, std::shared_ptr item); + +protected Q_SLOTS: + void onPasteActionTriggered(); + void onSelectAllActionTriggered(); + void onInvertSelectionActionTriggered(); + void onSortActionTriggered(bool checked); + void onSortOrderActionTriggered(bool checked); + void onShowHiddenActionTriggered(bool checked); + void onCaseSensitiveActionTriggered(bool checked); + void onFolderFirstActionTriggered(bool checked); + void onPropertiesActionTriggered(); + void onCustomActionTrigerred(); + +private: + void createSortMenu(); + void addSortMenuItem(const 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* actionAscending_; + QAction* actionDescending_; + QAction* showHiddenAction_; + QAction* separator4_; + QAction* propertiesAction_; +}; + +} + +#endif // FM_FOLDERMENU_H diff --git a/src/foldermodel.cpp b/src/foldermodel.cpp new file mode 100644 index 0000000..c453b5b --- /dev/null +++ b/src/foldermodel.cpp @@ -0,0 +1,566 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "foldermodel.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "utilities.h" +#include "fileoperation.h" + +namespace Fm { + +FolderModel::FolderModel(): + hasPendingThumbnailHandler_{false}, + showFullNames_{false}, + isLoaded_{false} { +} + +FolderModel::~FolderModel() { + // if the thumbnail requests list is not empty, cancel them + for(auto job: pendingThumbnailJobs_) { + job->cancel(); + } +} + +void FolderModel::setFolder(const std::shared_ptr& new_folder) { + if(folder_) { + removeAll(); // remove old items + } + if(new_folder) { + folder_ = new_folder; + connect(folder_.get(), &Fm::Folder::startLoading, this, &FolderModel::onStartLoading); + connect(folder_.get(), &Fm::Folder::finishLoading, this, &FolderModel::onFinishLoading); + connect(folder_.get(), &Fm::Folder::filesAdded, this, &FolderModel::onFilesAdded); + connect(folder_.get(), &Fm::Folder::filesChanged, this, &FolderModel::onFilesChanged); + connect(folder_.get(), &Fm::Folder::filesRemoved, this, &FolderModel::onFilesRemoved); + // handle the case if the folder is already loaded + if(folder_->isLoaded()) { + isLoaded_ = true; + insertFiles(0, folder_->files()); + } + } +} + +void FolderModel::onStartLoading() { + isLoaded_ = false; + // remove all items + removeAll(); +} + +void FolderModel::onFinishLoading() { + isLoaded_ = true; +} + +void FolderModel::onFilesAdded(const Fm::FileInfoList& files) { + int n_files = files.size(); + beginInsertRows(QModelIndex(), items.count(), items.count() + n_files - 1); + for(auto& info : files) { + FolderModelItem item(info); + /* + if(fm_file_info_is_hidden(info)) { + model->hiddenItems.append(item); + continue; + } + */ + items.append(item); + } + endInsertRows(); + + if(isLoaded_) { + Q_EMIT filesAdded(files); + } +} + +void FolderModel::onFilesChanged(std::vector& files) { + for(auto& change : files) { + int row; + auto& oldInfo = change.first; + auto& newInfo = change.second; + QList::iterator it = findItemByFileInfo(oldInfo.get(), &row); + if(it != items.end()) { + FolderModelItem& item = *it; + // try to update the item + item.info = newInfo; + item.thumbnails.clear(); + QModelIndex index = createIndex(row, 0, &item); + Q_EMIT dataChanged(index, index); + if(oldInfo->size() != newInfo->size()) { + Q_EMIT fileSizeChanged(index); + } + } + } +} + +void FolderModel::onFilesRemoved(const Fm::FileInfoList& files) { + for(auto& info : files) { + int row; + QList::iterator it = findItemByName(info->name().c_str(), &row); + if(it != items.end()) { + beginRemoveRows(QModelIndex(), row, row); + items.erase(it); + endRemoveRows(); + } + } +} + +void FolderModel::loadPendingThumbnails() { + hasPendingThumbnailHandler_ = false; + for(auto& item: thumbnailData_) { + if(!item.pendingThumbnails_.empty()) { + auto job = new Fm::ThumbnailJob(std::move(item.pendingThumbnails_), item.size_); + pendingThumbnailJobs_.push_back(job); + job->setAutoDelete(true); + connect(job, &Fm::ThumbnailJob::thumbnailLoaded, this, &FolderModel::onThumbnailLoaded, Qt::BlockingQueuedConnection); + connect(job, &Fm::ThumbnailJob::finished, this, &FolderModel::onThumbnailJobFinished, Qt::BlockingQueuedConnection); + Fm::ThumbnailJob::threadPool()->start(job); + } + } +} + +void FolderModel::queueLoadThumbnail(const std::shared_ptr& file, int size) { + auto it = std::find_if(thumbnailData_.begin(), thumbnailData_.end(), [size](ThumbnailData& item){return item.size_ == size;}); + if(it != thumbnailData_.end()) { + it->pendingThumbnails_.push_back(file); + if(!hasPendingThumbnailHandler_) { + QTimer::singleShot(0, this, &FolderModel::loadPendingThumbnails); + hasPendingThumbnailHandler_ = true; + } + } +} + +void FolderModel::insertFiles(int row, const Fm::FileInfoList& files) { + int n_files = files.size(); + beginInsertRows(QModelIndex(), row, row + n_files - 1); + for(auto& info : files) { + FolderModelItem item(info); + items.append(item); + } + endInsertRows(); +} + +void FolderModel::setCutFiles(const QItemSelection& selection) { + if(folder_) { + if(!selection.isEmpty()) { + auto cutFilesHashSet = std::make_shared(); + folder_->setCutFiles(cutFilesHashSet); + const auto indexes = selection.indexes(); + for(const auto& index : indexes) { + auto item = itemFromIndex(index); + item->bindCutFiles(cutFilesHashSet); + cutFilesHashSet->insert(item->info->path().hash()); + } + } + Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0)); + } +} + +void FolderModel::removeAll() { + if(items.empty()) { + return; + } + beginRemoveRows(QModelIndex(), 0, items.size() - 1); + items.clear(); + endRemoveRows(); +} + +int FolderModel::rowCount(const QModelIndex& parent) const { + if(parent.isValid()) { + return 0; + } + return items.size(); +} + +int FolderModel::columnCount(const QModelIndex& parent = QModelIndex()) const { + if(parent.isValid()) { + return 0; + } + return NumOfColumns; +} + +FolderModelItem* FolderModel::itemFromIndex(const QModelIndex& index) const { + return reinterpret_cast(index.internalPointer()); +} + +std::shared_ptr FolderModel::fileInfoFromIndex(const QModelIndex& index) const { + FolderModelItem* item = itemFromIndex(index); + return item ? item->info : nullptr; +} + +QVariant FolderModel::data(const QModelIndex& index, int role/* = Qt::DisplayRole*/) const { + if(!index.isValid() || index.row() > items.size() || index.column() >= NumOfColumns) { + return QVariant(); + } + FolderModelItem* item = itemFromIndex(index); + auto info = item->info; + + bool isCut = false; + if(folder_ && Q_UNLIKELY(folder_->hasCutFiles())) { + isCut = item->isCut(); + } + + switch(role) { + case Qt::ToolTipRole: + return QVariant(item->displayName()); + case Qt::DisplayRole: { + switch(index.column()) { + case ColumnFileName: + return (showFullNames_ && !item->name().empty() ? QString::fromStdString(item->name()) + : item->displayName()); + case ColumnFileType: + return QString(info->mimeType()->desc()); + case ColumnFileMTime: + return item->displayMtime(); + case ColumnFileSize: + return item->displaySize(); + case ColumnFileOwner: + return item->ownerName(); + case ColumnFileGroup: + return item->ownerGroup(); + } + break; + } + case Qt::DecorationRole: { + if(index.column() == 0) { + return QVariant(item->icon(isCut)); + } + break; + } + case Qt::EditRole: { + if(index.column() == 0) { + return QString::fromStdString(info->name()); + } + break; + } + case FileInfoRole: + return QVariant::fromValue(info); + case FileIsDirRole: + return QVariant(info->isDir()); + case FileIsCutRole: + return isCut; + } + return QVariant(); +} + +QVariant FolderModel::headerData(int section, Qt::Orientation orientation, int role/* = Qt::DisplayRole*/) const { + if(role == Qt::DisplayRole) { + if(orientation == Qt::Horizontal) { + QString title; + switch(section) { + case ColumnFileName: + title = tr("Name"); + break; + case ColumnFileType: + title = tr("Type"); + break; + case ColumnFileSize: + title = tr("Size"); + break; + case ColumnFileMTime: + title = tr("Modified"); + break; + case ColumnFileOwner: + title = tr("Owner"); + break; + case ColumnFileGroup: + title = tr("Group"); + break; + } + return QVariant(title); + } + } + return QVariant(); +} + +QModelIndex FolderModel::index(int row, int column, const QModelIndex& /*parent*/) const { + if(row < 0 || row >= items.size() || column < 0 || column >= NumOfColumns) { + return QModelIndex(); + } + const FolderModelItem& item = items.at(row); + return createIndex(row, column, (void*)&item); +} + +QModelIndex FolderModel::parent(const QModelIndex& /*index*/) const { + return QModelIndex(); +} + +Qt::ItemFlags FolderModel::flags(const QModelIndex& index) const { + // FIXME: should not return same flags unconditionally for all columns + Qt::ItemFlags flags; + if(index.isValid()) { + flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable; + if(index.column() == ColumnFileName) { + flags |= (Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled + | Qt::ItemIsEditable); // inline renaming); + } + } + else { + flags = Qt::ItemIsDropEnabled; + } + return flags; +} + +// FIXME: this is very inefficient and should be replaced with a +// more reasonable implementation later. +QList::iterator FolderModel::findItemByPath(const Fm::FilePath& path, int* row) { + QList::iterator it = items.begin(); + int i = 0; + while(it != items.end()) { + FolderModelItem& item = *it; + auto item_path = item.info->path(); + if(item_path == path) { + *row = i; + return it; + } + ++it; + ++i; + } + return items.end(); +} + +// FIXME: this is very inefficient and should be replaced with a +// more reasonable implementation later. +QList::iterator FolderModel::findItemByName(const char* name, int* row) { + QList::iterator it = items.begin(); + int i = 0; + while(it != items.end()) { + FolderModelItem& item = *it; + if(item.info->name() == name) { + *row = i; + return it; + } + ++it; + ++i; + } + return items.end(); +} + +QList< FolderModelItem >::iterator FolderModel::findItemByFileInfo(const Fm::FileInfo* info, int* row) { + QList::iterator it = items.begin(); + int i = 0; + while(it != items.end()) { + FolderModelItem& item = *it; + if(item.info.get() == info) { + *row = i; + return it; + } + ++it; + ++i; + } + return items.end(); +} + +QStringList FolderModel::mimeTypes() const { + //qDebug("FolderModel::mimeTypes"); + QStringList types = QAbstractItemModel::mimeTypes(); + // now types contains "application/x-qabstractitemmodeldatalist" + + // add support for freedesktop Xdnd direct save (XDS) protocol. + // https://www.freedesktop.org/wiki/Specifications/XDS/#index4h2 + // the real implementation is in FolderView::childDropEvent(). + types << "XdndDirectSave0"; + types << "text/uri-list"; + // types << "x-special/gnome-copied-files"; + return types; +} + +QMimeData* FolderModel::mimeData(const QModelIndexList& indexes) const { + QMimeData* data = QAbstractItemModel::mimeData(indexes); + //qDebug("FolderModel::mimeData"); + // build a uri list + QByteArray urilist; + urilist.reserve(4096); + + for(const auto& index : indexes) { + FolderModelItem* item = itemFromIndex(index); + if(item && item->info) { + auto path = item->info->path(); + if(path.isValid()) { + auto uri = path.uri(); + urilist.append(uri.get()); + urilist.append('\n'); + } + } + } + data->setData("text/uri-list", urilist); + + return data; +} + +bool FolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) { + //qDebug("FolderModel::dropMimeData"); + if(!folder_ || !data) { + return false; + } + Fm::FilePath destPath; + if(parent.isValid()) { // drop on an item + std::shared_ptr info; + if(row == -1 && column == -1) { + info = fileInfoFromIndex(parent); + } + else { + QModelIndex itemIndex = parent.child(row, column); + info = fileInfoFromIndex(itemIndex); + } + if(info) { + if (info->isDir()) { + destPath = info->path(); + } + else { + destPath = path(); // don't drop on file + } + } + else { + return false; + } + } + else { // drop on blank area of the folder + destPath = path(); + } + + // FIXME: should we put this in dropEvent handler of FolderView instead? + if(data->hasUrls()) { + //qDebug("drop action: %d", action); + auto srcPaths = pathListFromQUrls(data->urls()); + switch(action) { + case Qt::CopyAction: + FileOperation::copyFiles(srcPaths, destPath); + break; + case Qt::MoveAction: + FileOperation::moveFiles(srcPaths, destPath); + break; + case Qt::LinkAction: + FileOperation::symlinkFiles(srcPaths, destPath); + /* Falls through. */ + default: + return false; + } + return true; + } + else if(data->hasFormat("application/x-qabstractitemmodeldatalist")) { + return true; + } + return QAbstractListModel::dropMimeData(data, action, row, column, parent); +} + +Qt::DropActions FolderModel::supportedDropActions() const { + //qDebug("FolderModel::supportedDropActions"); + return Qt::CopyAction | Qt::MoveAction | Qt::LinkAction; +} + +// ask the model to load thumbnails of the specified size +void FolderModel::cacheThumbnails(const int size) { + auto it = std::find_if(thumbnailData_.begin(), thumbnailData_.end(), [size](ThumbnailData& item){return item.size_ == size;}); + if(it != thumbnailData_.cend()) { + ++it->refCount_; + } + else { + thumbnailData_.push_front(ThumbnailData(size)); + } +} + +// ask the model to free cached thumbnails of the specified size +void FolderModel::releaseThumbnails(int size) { + auto prev = thumbnailData_.before_begin(); + for(auto it = thumbnailData_.begin(); it != thumbnailData_.end(); ++it) { + if(it->size_ == size) { + --it->refCount_; + if(it->refCount_ == 0) { + thumbnailData_.erase_after(prev); + } + + // remove all cached thumbnails of the specified size + QList::iterator itemIt; + for(itemIt = items.begin(); itemIt != items.end(); ++itemIt) { + FolderModelItem& item = *itemIt; + item.removeThumbnail(size); + } + break; + } + prev = it; + } +} + +void FolderModel::onThumbnailJobFinished() { + Fm::ThumbnailJob* job = static_cast(sender()); + auto it = std::find(pendingThumbnailJobs_.cbegin(), pendingThumbnailJobs_.cend(), job); + if(it != pendingThumbnailJobs_.end()) { + pendingThumbnailJobs_.erase(it); + } +} + +void FolderModel::onThumbnailLoaded(const std::shared_ptr& file, int size, const QImage& image) { + // find the model item this thumbnail belongs to + int row; + QList::iterator it = findItemByFileInfo(file.get(), &row); + if(it != items.end()) { + // the file is found in our model + FolderModelItem& item = *it; + QModelIndex index = createIndex(row, 0, (void*)&item); + // store the image in the folder model item. + FolderModelItem::Thumbnail* thumbnail = item.findThumbnail(size, false); + thumbnail->image = image; + thumbnail->transparent = false; + // qDebug("thumbnail loaded for: %s, size: %d", item.displayName.toUtf8().constData(), size); + if(image.isNull()) { + thumbnail->status = FolderModelItem::ThumbnailFailed; + } + else { + thumbnail->status = FolderModelItem::ThumbnailLoaded; + thumbnail->image = image; + + // tell the world that we have the thumbnail loaded + Q_EMIT thumbnailLoaded(index, size); + } + } +} + +// get a thumbnail of size at the index +// if a thumbnail is not yet loaded, this will initiate loading of the thumbnail. +QImage FolderModel::thumbnailFromIndex(const QModelIndex& index, int size) { + FolderModelItem* item = itemFromIndex(index); + if(item) { + FolderModelItem::Thumbnail* thumbnail = item->findThumbnail(size, item->isCut()); + // qDebug("FolderModel::thumbnailFromIndex: %d, %s", thumbnail->status, item->displayName.toUtf8().data()); + switch(thumbnail->status) { + case FolderModelItem::ThumbnailNotChecked: { + // load the thumbnail + queueLoadThumbnail(item->info, size); + thumbnail->status = FolderModelItem::ThumbnailLoading; + break; + } + case FolderModelItem::ThumbnailLoaded: + return thumbnail->image; + default: + ; + } + } + return QImage(); +} + + +} // namespace Fm diff --git a/src/foldermodel.h b/src/foldermodel.h new file mode 100644 index 0000000..b8fc4a3 --- /dev/null +++ b/src/foldermodel.h @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_FOLDERMODEL_H +#define FM_FOLDERMODEL_H + +#include "libfmqtglobals.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "foldermodelitem.h" + +#include "core/folder.h" +#include "core/thumbnailjob.h" + +namespace Fm { + +class LIBFM_QT_API FolderModel : public QAbstractListModel { + Q_OBJECT +public: + + enum Role { + FileInfoRole = Qt::UserRole, + FileIsDirRole, + FileIsCutRole + }; + + enum ColumnId { + ColumnFileName, + ColumnFileType, + ColumnFileSize, + ColumnFileMTime, + ColumnFileOwner, + ColumnFileGroup, + NumOfColumns + }; + +public: + explicit FolderModel(); + virtual ~FolderModel(); + + const std::shared_ptr& folder() const { + return folder_; + } + + void setFolder(const std::shared_ptr& new_folder); + + Fm::FilePath path() { + return folder_ ? folder_->path() : Fm::FilePath(); + } + + int rowCount(const QModelIndex& parent = QModelIndex()) const; + int columnCount(const QModelIndex& parent) const; + QVariant data(const QModelIndex& index, int role) const; + QVariant headerData(int section, Qt::Orientation orientation, int role) const; + QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const; + QModelIndex parent(const QModelIndex& index) const; + // void sort(int column, Qt::SortOrder order = Qt::AscendingOrder); + + Qt::ItemFlags flags(const QModelIndex& index) const; + + virtual QStringList mimeTypes() const; + virtual QMimeData* mimeData(const QModelIndexList& indexes) const; + virtual Qt::DropActions supportedDropActions() const; + virtual bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent); + + std::shared_ptr fileInfoFromIndex(const QModelIndex& index) const; + FolderModelItem* itemFromIndex(const QModelIndex& index) const; + QImage thumbnailFromIndex(const QModelIndex& index, int size); + + void cacheThumbnails(int size); + void releaseThumbnails(int size); + + void setCutFiles(const QItemSelection& selection); + + void setShowFullName(bool fullName) { + showFullNames_ = fullName; + } + +Q_SIGNALS: + void thumbnailLoaded(const QModelIndex& index, int size); + void fileSizeChanged(const QModelIndex& index); + void filesAdded(FileInfoList infoList); + +protected Q_SLOTS: + + void onStartLoading(); + void onFinishLoading(); + void onFilesAdded(const Fm::FileInfoList& files); + void onFilesChanged(std::vector& files); + void onFilesRemoved(const Fm::FileInfoList& files); + + void onThumbnailLoaded(const std::shared_ptr& file, int size, const QImage& image); + void onThumbnailJobFinished(); + void loadPendingThumbnails(); + +protected: + void queueLoadThumbnail(const std::shared_ptr& file, int size); + void insertFiles(int row, const Fm::FileInfoList& files); + void removeAll(); + QList::iterator findItemByPath(const Fm::FilePath& path, int* row); + QList::iterator findItemByName(const char* name, int* row); + QList::iterator findItemByFileInfo(const Fm::FileInfo* info, int* row); + +private: + + struct ThumbnailData { + ThumbnailData(int size): + size_{size}, + refCount_{1} { + } + + int size_; + int refCount_; + Fm::FileInfoList pendingThumbnails_; + }; + + std::shared_ptr folder_; + QList items; + + bool hasPendingThumbnailHandler_; + std::vector pendingThumbnailJobs_; + std::forward_list thumbnailData_; + + bool showFullNames_; + + bool isLoaded_; +}; + +} + +#endif // FM_FOLDERMODEL_H diff --git a/src/foldermodelitem.cpp b/src/foldermodelitem.cpp new file mode 100644 index 0000000..44922ce --- /dev/null +++ b/src/foldermodelitem.cpp @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "foldermodelitem.h" +#include +#include +#include "utilities.h" +#include "core/userinfocache.h" + +namespace Fm { + +FolderModelItem::FolderModelItem(const std::shared_ptr& _info): + info{_info} { + thumbnails.reserve(2); +} + +FolderModelItem::FolderModelItem(const FolderModelItem& other): + info{other.info}, + thumbnails{other.thumbnails} { +} + +FolderModelItem::~FolderModelItem() { +} + +QString FolderModelItem::ownerName() const { + QString name; + auto user = Fm::UserInfoCache::globalInstance()->userFromId(info->uid()); + if(user) { + name = user->name(); + } + return name; +} + +QString FolderModelItem::ownerGroup() const { + auto group = Fm::UserInfoCache::globalInstance()->groupFromId(info->gid()); + return group ? group->name() : QString(); +} + +const QString &FolderModelItem::displayMtime() const { + if(dispMtime_.isEmpty()) { + auto mtime = QDateTime::fromMSecsSinceEpoch(info->mtime() * 1000); + dispMtime_ = mtime.toString(Qt::SystemLocaleShortDate); + } + return dispMtime_; +} + +const QString& FolderModelItem::displaySize() const { + if(!info->isDir()) { + // FIXME: choose IEC or SI units + dispSize_ = Fm::formatFileSize(info->size(), false); + } + return dispSize_; +} + +bool FolderModelItem::isCut() const { + return !cutFilesHashSet_.expired() || info->isCut(); +} + +void FolderModelItem::bindCutFiles(const std::shared_ptr& cutFilesHashSet) { + cutFilesHashSet_ = cutFilesHashSet; +} + +// find thumbnail of the specified size +// The returned thumbnail item is temporary and short-lived +// If you need to use the struct later, copy it to your own struct to keep it. +FolderModelItem::Thumbnail* FolderModelItem::findThumbnail(int size, bool transparent) { + QVector::iterator it; + Thumbnail* transThumb = nullptr; + for(it = thumbnails.begin(); it != thumbnails.end(); ++it) { + if(it->size == size) { + if(it->status != ThumbnailLoaded) { + return it; + } + else { // it->status == ThumbnailLoaded + if(it->transparent == false && transparent == true + && size < 48 /* (dirty) needed only for 'compact' and 'details list' view */ ) { + transThumb = it; // save thumb to add transparency later + } + else { + return it; // an image of the same size and transparency is found + } + } + } + } + if(transThumb) { + QImage image(transThumb->image); + + if(!image.hasAlphaChannel()) { + image = image.convertToFormat(QImage::Format_ARGB32); + } + + // add transparency to image + QPainter p; + p.begin(&image); + p.setCompositionMode(QPainter::CompositionMode_DestinationIn); + p.fillRect(image.rect(), QColor(0, 0, 0, 115 /* alpha 45% */)); + p.end(); + + // add image to thumbnails + Thumbnail thumbnail; + thumbnail.status = ThumbnailLoaded; + thumbnail.image = image; + thumbnail.size = size; + thumbnail.transparent = true; + thumbnails.append(thumbnail); + } + else if(it == thumbnails.end()) { + Thumbnail thumbnail; + thumbnail.status = ThumbnailNotChecked; + thumbnail.size = size; + thumbnail.transparent = false; + thumbnails.append(thumbnail); + } + return &thumbnails.back(); +} + +// remove cached thumbnail of the specified size +void FolderModelItem::removeThumbnail(int size) { + QVector::iterator it; + for(it = thumbnails.begin(); it != thumbnails.end(); ++it) { + if(it->size == size) { // an image of the same size is found + thumbnails.erase(it); + break; + } + } +} + + +} // namespace Fm diff --git a/src/foldermodelitem.h b/src/foldermodelitem.h new file mode 100644 index 0000000..82eeb9a --- /dev/null +++ b/src/foldermodelitem.h @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_FOLDERMODELITEM_H +#define FM_FOLDERMODELITEM_H + +#include "libfmqtglobals.h" +#include +#include +#include +#include + +#include "core/folder.h" + +namespace Fm { + +class LIBFM_QT_API FolderModelItem { +public: + + enum ThumbnailStatus { + ThumbnailNotChecked, + ThumbnailLoading, + ThumbnailLoaded, + ThumbnailFailed + }; + + struct Thumbnail { + int size; + bool transparent; + ThumbnailStatus status; + QImage image; + }; + +public: + explicit FolderModelItem(const std::shared_ptr& _info); + FolderModelItem(const FolderModelItem& other); + virtual ~FolderModelItem(); + + const QString& displayName() const { + return info->displayName(); + } + + const std::string& name() const { + return info->name(); + } + + QIcon icon(bool transparent = false) const { + const auto i = info->icon(); + return i ? i->qicon(transparent) : QIcon{}; + } + + QString ownerName() const; + + QString ownerGroup() const; + + const QString& displayMtime() const; + + const QString &displaySize() const; + + bool isCut() const; + + void bindCutFiles(const std::shared_ptr& cutFilesHashSet); + + Thumbnail* findThumbnail(int size, bool transparent); + + void removeThumbnail(int size); + + std::shared_ptr info; + mutable QString dispMtime_; + mutable QString dispSize_; + std::weak_ptr cutFilesHashSet_; + QVector thumbnails; +}; + +} + +#endif // FM_FOLDERMODELITEM_H diff --git a/src/folderview.cpp b/src/folderview.cpp new file mode 100644 index 0000000..1506a0b --- /dev/null +++ b/src/folderview.cpp @@ -0,0 +1,1747 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "folderview.h" +#include "foldermodel.h" +#include +#include +#include +#include "proxyfoldermodel.h" +#include "folderitemdelegate.h" +#include "dndactionmenu.h" +#include "filemenu.h" +#include "foldermenu.h" +#include "filelauncher.h" +#include "utilities.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // for detailed list header context menu +#include // for detailed list header context menu +#include // for XDS support +#include // for XDS support +#include "xdndworkaround.h" // for XDS support +#include "folderview_p.h" +#include "utilities.h" + +#define SCROLL_FRAMES_PER_SEC 50 +#define SCROLL_DURATION 300 // in ms + +static const int scrollAnimFrames = SCROLL_FRAMES_PER_SEC * SCROLL_DURATION / 1000; + +using namespace Fm; + +FolderViewListView::FolderViewListView(QWidget* parent): + QListView(parent), + activationAllowed_(true), + cursorOnSelectionCorner_(false) { + connect(this, &QListView::activated, this, &FolderViewListView::activation); + // inline renaming + setEditTriggers(QAbstractItemView::NoEditTriggers); + setMouseTracking(true); // needed with selection corner icon +} + +FolderViewListView::~FolderViewListView() { +} + +void FolderViewListView::startDrag(Qt::DropActions supportedActions) { + if(movement() != Static) { + QListView::startDrag(supportedActions); + } + else { + QAbstractItemView::startDrag(supportedActions); + } +} + +void FolderViewListView::mousePressEvent(QMouseEvent* event) { + setSelectionMode(cursorOnSelectionCorner_ && event->button() == Qt::LeftButton + ? QAbstractItemView::MultiSelection + : QAbstractItemView::ExtendedSelection); + QListView::mousePressEvent(event); + static_cast(parent())->childMousePressEvent(event); +} + +void FolderViewListView::mouseMoveEvent(QMouseEvent* event) { + // NOTE: Filter the BACK & FORWARD buttons to not Drag & Drop with them. + // (by default Qt views drag with any button) + if (event->buttons() == Qt::NoButton || (event->buttons() & ~(Qt::BackButton | Qt::ForwardButton))) { + bool cursorOnSelectionCorner = cursorOnSelectionCorner_; + QListView::mouseMoveEvent(event); + // update the index if the cursor enters/leaves the selection corner icon + if(cursorOnSelectionCorner != cursorOnSelectionCorner_ && event->buttons() == Qt::NoButton) { + update(indexAt(event->pos())); + } + } +} + +QModelIndex FolderViewListView::indexAt(const QPoint& point) const { + QModelIndex index = QListView::indexAt(point); + bool isCursorPos(point == viewport()->mapFromGlobal(QCursor::pos())); + if(isCursorPos) { + cursorOnSelectionCorner_ = false; + } + // NOTE: QListView has a severe design flaw here. It does hit-testing based on the + // total bound rect of the item. The width of an item is determined by max(icon_width, text_width). + // So if the text label is much wider than the icon, when you click outside the icon but + // the point is still within the outer bound rect, the item is still selected. + // This results in very poor usability. Let's do precise hit-testing here. + // An item is hit only when the point is in the icon or text label. + // If the point is in the bound rectangle but outside the icon or text, it should not be selected. + if(viewMode() == QListView::IconMode && index.isValid()) { + QRect visRect = visualRect(index); // visible area on the screen + FolderItemDelegate* delegate = static_cast(itemDelegateForColumn(FolderModel::ColumnFileName)); + QSize margins = delegate->getMargins(); + QSize _iconSize = iconSize(); + int iconXMargin = (visRect.width() - _iconSize.width()) / 2; + int iconLeft = visRect.left() + iconXMargin; + int iconTop = visRect.top() + margins.height(); + // the selection (hover) corner is a rectangle near the top left corner of + // the icon and outside it as far as possible, so that its width and height + // are 1/3 of the icon size >= 48 px (see FolderItemDelegate::paint) + if(isCursorPos && _iconSize.width() >= 48) { + int s = _iconSize.width() / 3; + iconLeft = qMax(visRect.left(), iconLeft - s); + iconTop = qMax(visRect.top(), iconTop - s); + if(point.x() >= iconLeft && point.x() <= iconLeft + s + && point.y() >= iconTop && point.y() <= iconTop + s) { + cursorOnSelectionCorner_ = true; + return index; + } + } + if(point.y() < iconTop) { // above icon + return QModelIndex(); + } + else if(point.y() < visRect.top() + margins.height() + _iconSize.height()) { // on the icon area + if(point.x() < iconLeft || point.x() > (visRect.right() + 1 - iconXMargin)) { + // to the left or right of the icon + return QModelIndex(); + } + } + else { + QSize _textSize = delegate->iconViewTextSize(index); + int textHMargin = (visRect.width() - _textSize.width()) / 2; + if(point.y() > visRect.top() + margins.height() + _iconSize.height() + _textSize.height() // below text + // on the text area but to the left or right of the text + || point.x() < visRect.left() + textHMargin || point.x() > visRect.right() + 1 - textHMargin) { + return QModelIndex(); + } + } + // qDebug() << "visualRect: " << visRect << "point:" << point; + } + return index; +} + + +// NOTE: +// QListView has a problem which I consider a bug or a design flaw. +// When you set movement property to Static, theoratically the icons +// should not be movable. However, if you turned on icon mode, +// the icons becomes freely movable despite the value of movement is Static. +// To overcome this bug, we override all drag handling methods, and +// call QAbstractItemView directly, bypassing QListView. +// In this way, we can workaround the buggy behavior. +// The drag handlers of QListView basically does the same things +// as its parent QAbstractItemView, but it also stores the currently +// dragged item and paint them in the view as needed. +// TODO: I really should file a bug report to Qt developers. + +void FolderViewListView::dragEnterEvent(QDragEnterEvent* event) { + if(movement() != Static) { + QListView::dragEnterEvent(event); + } + else { + QAbstractItemView::dragEnterEvent(event); + } + //qDebug("dragEnterEvent"); + //static_cast(parent())->childDragEnterEvent(event); +} + +void FolderViewListView::dragLeaveEvent(QDragLeaveEvent* e) { + if(movement() != Static) { + QListView::dragLeaveEvent(e); + } + else { + QAbstractItemView::dragLeaveEvent(e); + } + static_cast(parent())->childDragLeaveEvent(e); +} + +void FolderViewListView::dragMoveEvent(QDragMoveEvent* e) { + if(movement() != Static) { + QListView::dragMoveEvent(e); + } + else { + QAbstractItemView::dragMoveEvent(e); + } + static_cast(parent())->childDragMoveEvent(e); +} + +void FolderViewListView::dropEvent(QDropEvent* e) { + + static_cast(parent())->childDropEvent(e); + + if(movement() != Static) { + QListView::dropEvent(e); + } + else { + QAbstractItemView::dropEvent(e); + } +} + +void FolderViewListView::mouseReleaseEvent(QMouseEvent* event) { + bool activationWasAllowed = activationAllowed_; + if(!style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, this) + || event->button() != Qt::LeftButton + // no activation with mouse when the cursor is on the selection corner + || cursorOnSelectionCorner_) { + 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 + // no activation with mouse when the cursor is on the selection corner + || cursorOnSelectionCorner_) { + activationAllowed_ = false; + } + + QListView::mouseDoubleClickEvent(event); + + activationAllowed_ = activationWasAllowed; +} + +QModelIndex FolderViewListView::moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers) { + QAbstractItemModel* model_ = model(); + + if(model_ && currentIndex().isValid()) { + FolderView::ViewMode viewMode = static_cast(parent())->viewMode(); + if((viewMode == FolderView::IconMode) || (viewMode == FolderView::ThumbnailMode)) { + int next = (layoutDirection() == Qt::RightToLeft) ? - 1 : 1; + + if(cursorAction == QAbstractItemView::MoveRight) { + return model_->index(currentIndex().row() + next, 0); + } + else if(cursorAction == QAbstractItemView::MoveLeft) { + return model_->index(currentIndex().row() - next, 0); + } + } + } + + return QListView::moveCursor(cursorAction, modifiers); +} + +void FolderViewListView::activation(const QModelIndex& index) { + if(activationAllowed_) { + Q_EMIT activatedFiltered(index); + } +} + +//----------------------------------------------------------------------------- + +FolderViewTreeView::FolderViewTreeView(QWidget* parent): + QTreeView(parent), + doingLayout_(false), + layoutTimer_(nullptr), + activationAllowed_(true), + ctrlDragSelectionFlag_(QItemSelectionModel::NoUpdate) { + + header()->setSectionResizeMode(QHeaderView::Interactive); + header()->setStretchLastSection(true); + + // get the new width if the section is resized by user + connect(header(), &QHeaderView::sectionResized, [this](int logicalIndex, int/* oldSize*/, int newSize) { + if(doingLayout_ || customColumnWidths_.isEmpty()) { + return; + } + int vIndx = header()->visualIndex(logicalIndex); + if(vIndx >= 0 && vIndx < customColumnWidths_.size()) { + customColumnWidths_[vIndx] = newSize; + Q_EMIT columnResizedByUser(vIndx, newSize); + queueLayoutColumns(); + } + }); + + // header context menu for configuring its resizing and hidden sections + header()->setContextMenuPolicy(Qt::CustomContextMenu); + connect(header(), &QWidget::customContextMenuRequested, this, &FolderViewTreeView::headerContextMenu); + + setIndentation(0); + // the default true value may cause a crash on entering a folder by double clicking (a Qt bug?) + setExpandsOnDoubleClick(false); + + connect(this, &QTreeView::activated, this, &FolderViewTreeView::activation); + // don't open editor on double clicking + setEditTriggers(QAbstractItemView::NoEditTriggers); +} + +FolderViewTreeView::~FolderViewTreeView() { + if(layoutTimer_) { + delete layoutTimer_; + } +} + +void FolderViewTreeView::setCustomColumnWidths(const QList &widths) { + // enables cutomizable widths if "widths" is not empty; otherwise, enables auto-resizing + if(customColumnWidths_ == widths) { + return; + } + customColumnWidths_.clear(); + customColumnWidths_ = widths; + header()->setStretchLastSection(widths.isEmpty()); + queueLayoutColumns(); + if(widths.isEmpty()) { + Q_EMIT autoResizeEnabled(); + } +} + +void FolderViewTreeView::setHiddenColumns(const QSet &columns) { + if(hiddenColumns_ == columns) { + return; + } + hiddenColumns_.clear(); + hiddenColumns_ = columns; + queueLayoutColumns(); +} + +void FolderViewTreeView::headerContextMenu(const QPoint &p) { + QMenu menu; + QAction *action = menu.addAction (tr("Auto-resize columns")); + action->setCheckable(true); + action->setChecked(customColumnWidths_.isEmpty()); + connect(action, &QAction::triggered, action, [this] (bool checked) { + QList widths; + if(!checked) { + for(int column = 0; column < FolderModel::NumOfColumns; ++column) { + widths << 0; + } + // one signal is enough to make a raw FolderView::customColumnWidths_ + Q_EMIT columnResizedByUser(0, 0); + } + setCustomColumnWidths(widths); + }); + if(model()) { + menu.addSeparator(); + QWidgetAction *labelAction = new QWidgetAction(&menu); + QLabel *label = new QLabel("
" + tr("Visible Columns") + "
"); + labelAction->setDefaultWidget(label); + menu.addAction (labelAction); + + int filenameColumn = header()->visualIndex(FolderModel::ColumnFileName); + int numCols = header()->count(); + for(int column = 0; column < numCols; ++column) { + int columnId = header()->logicalIndex(column); + if(columnId >= 0 && columnId < FolderModel::NumOfColumns) { + action = menu.addAction (model()->headerData(columnId, Qt::Horizontal, Qt::DisplayRole).toString()); + action->setCheckable(true); + if(columnId == filenameColumn) { // never hide the name column + action->setChecked(true); + action->setDisabled(true); + } + else { + action->setChecked(!header()->isSectionHidden(columnId)); + connect(action, &QAction::triggered, action, [this, column, columnId] (bool checked) { + if(checked) { + hiddenColumns_.remove(column); + } + else { + hiddenColumns_ << column; + } + Q_EMIT columnHiddenByUser(column, !checked); + queueLayoutColumns(); + }); + } + } + } + } + menu.exec(header()->mapToGlobal(p)); +} + +void FolderViewTreeView::setModel(QAbstractItemModel* model) { + QTreeView::setModel(model); + layoutColumns(); + if(ProxyFolderModel* proxyModel = qobject_cast(model)) { + connect(proxyModel, &ProxyFolderModel::sortFilterChanged, this, &FolderViewTreeView::onSortFilterChanged, + Qt::UniqueConnection); + onSortFilterChanged(); + } +} + +void FolderViewTreeView::paintEvent(QPaintEvent * event) { + QTreeView::paintEvent(event); + if(rubberBandRect_.isValid()) { // draw rubberband + QPainter p(viewport()); + QStyleOptionRubberBand opt; + opt.initFrom(this); + opt.shape = QRubberBand::Rectangle; + opt.opaque = false; + QRect r = rubberBandRect_.adjusted(-horizontalOffset(), -verticalOffset(), + -horizontalOffset(), -verticalOffset()) + .intersected(viewport()->rect() + .adjusted(-16, -16, 16, 16)); + opt.rect = r; + style()->drawControl(QStyle::CE_RubberBand, &opt, &p); + } +} + +void FolderViewTreeView::mousePressEvent(QMouseEvent* event) { + QAbstractItemView::mousePressEvent(event); + + // remember mouse press position and determine whether selections should be kept + // or removed later, when the cursor moves + mousePressPoint_ = event->pos() + QPoint(horizontalOffset(), verticalOffset()); + QModelIndex index = indexAt(event->pos()); + if(index.isValid()) { + QItemSelectionModel::SelectionFlags command; + Qt::KeyboardModifiers modifiers = QApplication::keyboardModifiers(); + const Qt::MouseButton button = static_cast(event)->button(); + const bool shiftKeyPressed = modifiers & Qt::ShiftModifier; + const bool controlKeyPressed = modifiers & Qt::ControlModifier; + const bool rightButtonPressed = button & Qt::RightButton; + const bool indexIsSelected = selectionModel()->isSelected(index); + if(controlKeyPressed && !shiftKeyPressed && !rightButtonPressed) { + ctrlDragSelectionFlag_ = indexIsSelected ? QItemSelectionModel::Deselect : QItemSelectionModel::Select; + } + } + + static_cast(parent())->childMousePressEvent(event); +} + +void FolderViewTreeView::mouseMoveEvent(QMouseEvent* event) { + // NOTE: Filter the BACK & FORWARD buttons to not Drag & Drop with them. + // (by default Qt views drag with any button) + if(event->buttons() == Qt::NoButton || (event->buttons() & ~(Qt::BackButton | Qt::ForwardButton))) { + // handle rubberband + if((event->buttons() & Qt::LeftButton) + && (rubberBandRect_.isValid() + || !indexAt(mousePressPoint_ - QPoint(horizontalOffset(), verticalOffset())).isValid())) { + QAbstractItemView::mouseMoveEvent(event); + + // set rubberband rectangle + QRect rect(mousePressPoint_, event->pos() + QPoint(horizontalOffset(), verticalOffset())); + rect = rect.normalized(); + QRect r = rect.united(rubberBandRect_); + viewport()->update(r.adjusted(-horizontalOffset(), -verticalOffset(), + -horizontalOffset(), -verticalOffset())); + rubberBandRect_ = rect; + + // set state and selection + setState(QAbstractItemView::DragSelectingState); + Qt::KeyboardModifiers modifiers = QApplication::keyboardModifiers(); + QItemSelectionModel::SelectionFlags command; + if(modifiers & Qt::ControlModifier) { + command = QItemSelectionModel::ToggleCurrent; + } + else if(modifiers & Qt::ShiftModifier) { + command = QItemSelectionModel::SelectCurrent; + } + else { + command = QItemSelectionModel::Clear|QItemSelectionModel::SelectCurrent; + } + command |= QItemSelectionModel::Rows; + if(ctrlDragSelectionFlag_ != QItemSelectionModel::NoUpdate && command.testFlag(QItemSelectionModel::Toggle)) { + command &= ~QItemSelectionModel::Toggle; + command |= ctrlDragSelectionFlag_; + } + QRect selectionRect = QRect(rubberBandRect_.topLeft(), rubberBandRect_.bottomRight()); + setSelection(selectionRect, command); + } + else { + QTreeView::mouseMoveEvent(event); + } + } +} + +void FolderViewTreeView::setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags command) { + if(model() && state() == QAbstractItemView::DragSelectingState) { // rubberband selection + QRect r = rubberBandRect_.adjusted(-horizontalOffset(), -verticalOffset(), + -horizontalOffset(), -verticalOffset()); + r.setLeft(qMax(0, r.left())); + r.setTop(qMax(-verticalOffset(), r.top())); + QModelIndex top = indexAt(r.topLeft()); + QItemSelection selection; + if(top.isValid()) { + top = top.sibling(top.row(), 0); + if(top.isValid()) { + QModelIndex bottom = indexAt(r.bottomLeft()); + if(!bottom.isValid()) { + bottom = model()->index(model()->rowCount() - 1, 0); + } + if(bottom.isValid()) { + selection = QItemSelection(top, bottom); + } + } + } + selectionModel()->select(selection, command | QItemSelectionModel::Rows); + } + else { + QTreeView::setSelection(rect, command); + } +} + +void FolderViewTreeView::dragEnterEvent(QDragEnterEvent* event) { + QTreeView::dragEnterEvent(event); + //static_cast(parent())->childDragEnterEvent(event); +} + +void FolderViewTreeView::dragLeaveEvent(QDragLeaveEvent* e) { + QTreeView::dragLeaveEvent(e); + static_cast(parent())->childDragLeaveEvent(e); +} + +void FolderViewTreeView::dragMoveEvent(QDragMoveEvent* e) { + QTreeView::dragMoveEvent(e); + static_cast(parent())->childDragMoveEvent(e); +} + +void FolderViewTreeView::dropEvent(QDropEvent* e) { + static_cast(parent())->childDropEvent(e); + QTreeView::dropEvent(e); +} + +// the default list mode of QListView handles column widths +// very badly (worse than gtk+) and it's not very flexible. +// so, let's handle column widths outselves. +void FolderViewTreeView::layoutColumns() { + // qDebug("layoutColumns"); + if(!model()) { + return; + } + doingLayout_ = true; + QHeaderView* headerView = header(); + // the width that's available for showing the columns. + int availWidth = viewport()->contentsRect().width(); + + // get the width that every column want + int numCols = headerView->count(); + if(numCols > 0) { + int desiredWidth = 0; + int* widths = new int[numCols]; // array to store the widths every column needs + QStyleOptionHeader opt; + opt.initFrom(headerView); + opt.fontMetrics = QFontMetrics(font()); + if (headerView->isSortIndicatorShown()) { + opt.sortIndicator = QStyleOptionHeader::SortDown; + } + QAbstractItemModel* model_ = model(); + int column; + for(column = 0; column < numCols; ++column) { + int columnId = headerView->logicalIndex(column); + + // handle hidden columns + bool wasHidden = false; + if(headerView->isSectionHidden(columnId)) { + if(!hiddenColumns_.contains(columnId)) { + headerView->setSectionHidden(columnId, false); + wasHidden = true; + } + else { + continue; + } + } + else if(hiddenColumns_.contains(columnId)) { + headerView->setSectionHidden(columnId, true); + continue; + } + + if(customColumnWidths_.size() > column) { + // see FolderView::setCustomColumnWidths for the meaning of custom width <= 0 + if(customColumnWidths_.at(column) > 0) { + widths[column] = qMax(customColumnWidths_.at(column), headerView->minimumSectionSize()); + } + else { + if(wasHidden) { + // WARNING: When a section is shown in the interactive mode, Qt gives + // a huge width to it. As a workaround, the width is set to the minimum here. + customColumnWidths_[column] = widths[column] = headerView->minimumSectionSize(); + } + else { + customColumnWidths_[column] = widths[column] = headerView->sectionSize(columnId); + } + Q_EMIT columnResizedByUser(column, customColumnWidths_.at(column)); + } + } + else { + // get the size that the column needs + if(model_) { + QVariant data = model_->headerData(columnId, Qt::Horizontal, Qt::DisplayRole); + if(data.isValid()) { + opt.text = data.isValid() ? data.toString() : QString(); + } + } + opt.section = columnId; + widths[column] = qMax(sizeHintForColumn(columnId), + style()->sizeFromContents(QStyle::CT_HeaderSection, &opt, QSize(), + headerView).width()); + } + // compute the total width needed + desiredWidth += widths[column]; + } + + int filenameColumn = headerView->visualIndex(FolderModel::ColumnFileName); + if(customColumnWidths_.size() <= filenameColumn) { // practically means no custom width + // if the total witdh we want exceeds the available space + if(desiredWidth > availWidth) { + // Compute the width available for the filename column + int filenameAvailWidth = availWidth - desiredWidth + widths[filenameColumn]; + + // Compute the minimum acceptable width for the filename column + int filenameMinWidth = qMin(200, sizeHintForColumn(filenameColumn)); + + if(filenameAvailWidth > filenameMinWidth) { + // Shrink the filename column to the available width + widths[filenameColumn] = filenameAvailWidth; + } + else { + // Set the filename column to its minimum width + widths[filenameColumn] = filenameMinWidth; + } + } + else { + // Fill the extra available space with the filename column + widths[filenameColumn] += availWidth - desiredWidth; + } + } + + // really do the resizing for every column + for(int column = 0; column < numCols; ++column) { + headerView->resizeSection(headerView->logicalIndex(column), widths[column]); + } + delete []widths; + } + doingLayout_ = false; + + if(layoutTimer_) { + delete layoutTimer_; + layoutTimer_ = nullptr; + } + setUpdatesEnabled(true); +} + +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) { + setUpdatesEnabled(false); // prevent header text flickering + queueLayoutColumns(); + QTreeView::rowsInserted(parent, start, end); +} + +void FolderViewTreeView::rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { + QTreeView::rowsAboutToBeRemoved(parent, start, end); + queueLayoutColumns(); +} + +void FolderViewTreeView::dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector& roles /*= QVector{}*/) { + QTreeView::dataChanged(topLeft, bottomRight, roles); + // FIXME: this will be very inefficient + // queueLayoutColumns(); +} + +void FolderViewTreeView::reset() { + // Sometimes when the content of the model is radically changed, Qt does reset() + // on the model rather than doing large amount of insertion and deletion. + // This is for performance reason so in this case rowsInserted() and rowsAboutToBeRemoved() + // might not be called. Hence we also have to re-layout the columns when the model is reset. + // This fixes bug #190 + // https://github.com/lxqt/pcmanfm-qt/issues/190 + setUpdatesEnabled(false); // prevent header text flickering + queueLayoutColumns(); + QTreeView::reset(); +} + +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; + } + + QAbstractItemView::mouseReleaseEvent(event); + viewport()->update(rubberBandRect_.adjusted(-horizontalOffset(), -verticalOffset(), + -horizontalOffset(), -verticalOffset())); + rubberBandRect_ = QRect(); + ctrlDragSelectionFlag_ = QItemSelectionModel::NoUpdate; + + activationAllowed_ = activationWasAllowed; + +} + +void FolderViewTreeView::mouseDoubleClickEvent(QMouseEvent* event) { + bool activationWasAllowed = activationAllowed_; + if((style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, this)) || (event->button() != Qt::LeftButton)) { + activationAllowed_ = false; + } + + QTreeView::mouseDoubleClickEvent(event); + + activationAllowed_ = activationWasAllowed; +} + +void FolderViewTreeView::activation(const QModelIndex& index) { + if(activationAllowed_) { + Q_EMIT activatedFiltered(index); + } +} + +void FolderViewTreeView::onSortFilterChanged() { + if(QSortFilterProxyModel* proxyModel = qobject_cast(model())) { + header()->setSortIndicatorShown(true); + header()->setSortIndicator(proxyModel->sortColumn(), proxyModel->sortOrder()); + if(!isSortingEnabled()) { + setSortingEnabled(true); + } + } +} + + +//----------------------------------------------------------------------------- + +FolderView::FolderView(FolderView::ViewMode _mode, QWidget *parent): + QWidget(parent), + view(nullptr), + model_(nullptr), + mode((ViewMode)0), + fileLauncher_(nullptr), + autoSelectionDelay_(600), + autoSelectionTimer_(nullptr), + selChangedTimer_(nullptr), + itemDelegateMargins_(QSize(3, 3)), + shadowHidden_(false), + smoothScrollTimer_(nullptr), + wheelEvent_(nullptr) { + + iconSize_[IconMode - FirstViewMode] = QSize(48, 48); + iconSize_[CompactMode - FirstViewMode] = QSize(24, 24); + iconSize_[ThumbnailMode - FirstViewMode] = QSize(128, 128); + iconSize_[DetailedListMode - FirstViewMode] = QSize(24, 24); + + QVBoxLayout* layout = new QVBoxLayout(); + layout->setMargin(0); + setLayout(layout); + + setViewMode(_mode); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + connect(this, &FolderView::clicked, this, &FolderView::onFileClicked); + connect(QApplication::clipboard(), &QClipboard::dataChanged, this, &FolderView::onClipboardDataChange); +} + +FolderView::~FolderView() { + if(smoothScrollTimer_) { + disconnect(smoothScrollTimer_, &QTimer::timeout, this, &FolderView::scrollSmoothly); + smoothScrollTimer_->stop(); + delete smoothScrollTimer_; + } +} + +void FolderView::setCustomColumnWidths(const QList &widths) { + customColumnWidths_.clear(); + customColumnWidths_ = widths; + // Complete the widths list with zeros if needed. A value of <= 0 means that + // the initial custom width of the column should be set to its current width. + if(!customColumnWidths_.isEmpty()) { + for(int i = customColumnWidths_.size(); i < FolderModel::NumOfColumns; ++i) { + customColumnWidths_ << 0; + } + } + // resize header sections to custom widths if the tree view exists + if(mode == DetailedListMode) { + if(FolderViewTreeView* treeView = static_cast(view)) { + treeView->setCustomColumnWidths(customColumnWidths_); + } + } +} + +void FolderView::setHiddenColumns(const QList &columns) { + hiddenColumns_.clear(); + hiddenColumns_ = columns.toSet(); + if(mode == DetailedListMode) { + if(FolderViewTreeView* treeView = static_cast(view)) { + treeView->setHiddenColumns(hiddenColumns_); + } + } +} + +void FolderView::onItemActivated(QModelIndex index) { + QItemSelectionModel* selModel = selectionModel(); + if(index.isValid() && index.model() + && selModel && selModel->isSelected(index)) { // do nothing when the item is not selected + QVariant data = index.model()->data(index, FolderModel::FileInfoRole); + auto info = data.value>(); + if(info) { + if(!(QApplication::keyboardModifiers() & (Qt::ShiftModifier | Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier))) { + Q_EMIT clicked(ActivatedClick, info); + } + } + } +} + +void FolderView::onSelChangedTimeout() { + selChangedTimer_->deleteLater(); + selChangedTimer_ = nullptr; + // qDebug()<<"selected:" << nSel; + Q_EMIT selChanged(); +} + +void FolderView::onSelectionChanged(const QItemSelection& /*selected*/, const QItemSelection& /*deselected*/) { + // It's possible that the selected items change too often and this slot gets called for thousands of times. + // For example, when you select thousands of files and delete them, we will get one selectionChanged() event + // for every deleted file. So, we use a timer to delay the handling to avoid too frequent updates of the UI. + if(!selChangedTimer_) { + selChangedTimer_ = new QTimer(this); + selChangedTimer_->setSingleShot(true); + connect(selChangedTimer_, &QTimer::timeout, this, &FolderView::onSelChangedTimeout); + selChangedTimer_->start(200); + } +} + +void FolderView::onClosingEditor(QWidget* editor, QAbstractItemDelegate::EndEditHint hint) { + if (hint != QAbstractItemDelegate::NoHint) { + // we set the hint to NoHint in FolderItemDelegate::eventFilter() + return; + } + QString newName; + if (qobject_cast(editor)) { // icon and thumbnail view + newName = qobject_cast(editor)->toPlainText(); + } + else if (qobject_cast(editor)) { // compact view + newName = qobject_cast(editor)->text(); + } + if (newName.isEmpty()) { + return; + } + // the editor will be deleted by QAbstractItemDelegate::destroyEditor() when no longer needed + + QModelIndex index = view->selectionModel()->currentIndex(); + if(index.isValid() && index.model()) { + QVariant data = index.model()->data(index, FolderModel::FileInfoRole); + auto info = data.value>(); + if (info) { + auto oldName = QString::fromStdString(info->name()); + if(newName == oldName) { + return; + } + QWidget* parent = window(); + if (window() == this) { // supposedly desktop, in case it uses this + parent = nullptr; + } + changeFileName(info->path(), newName, parent); + } + } +} + +void FolderView::setViewMode(ViewMode _mode) { + if(_mode == mode) { // if it's the same more, ignore + return; + } + // smooth scrolling is only for icon and thumbnail modes + if(smoothScrollTimer_ && (_mode == DetailedListMode || _mode == CompactMode)) { + disconnect(smoothScrollTimer_, &QTimer::timeout, this, &FolderView::scrollSmoothly); + smoothScrollTimer_->stop(); + delete smoothScrollTimer_; + smoothScrollTimer_ = nullptr; + } + // FIXME: retain old selection + + // since only detailed list mode uses QTreeView, and others + // all use QListView, it's wise to preserve QListView when possible. + bool recreateView = false; + if(view && (mode == DetailedListMode || _mode == DetailedListMode)) { + delete view; // FIXME: no virtual dtor? + view = nullptr; + recreateView = true; + } + mode = _mode; + QSize iconSize = iconSize_[mode - FirstViewMode]; + + FolderItemDelegate* delegate = nullptr; + if(mode == DetailedListMode) { + FolderViewTreeView* treeView = new FolderViewTreeView(this); + treeView->setCustomColumnWidths(customColumnWidths_); + treeView->setHiddenColumns(hiddenColumns_); + connect(treeView, &FolderViewTreeView::activatedFiltered, this, &FolderView::onItemActivated); + // update the list of custom widhts when the user changes it + connect(treeView, &FolderViewTreeView::columnResizedByUser, [this](int visualIndex, int newWidth) { + if(visualIndex >= 0) { + if(visualIndex < customColumnWidths_.size()){ + customColumnWidths_[visualIndex] = newWidth; + } + else { + customColumnWidths_ << newWidth; + } + // complete the widths list with zeros if needed + for(int i = customColumnWidths_.size(); i < FolderModel::NumOfColumns; ++i) { + customColumnWidths_ << 0; + } + Q_EMIT columnResizedByUser(); + } + }); + connect(treeView, &FolderViewTreeView::autoResizeEnabled, [this]() { + customColumnWidths_.clear(); + Q_EMIT columnResizedByUser(); + }); + // update the list of hidden columns when the user changes it + connect(treeView, &FolderViewTreeView::columnHiddenByUser, [this](int visibleIndex, bool hidden) { + if(hidden) { + hiddenColumns_ << visibleIndex; + } + else { + hiddenColumns_.remove(visibleIndex); + } + Q_EMIT columnHiddenByUser(); + }); + setFocusProxy(treeView); + + view = treeView; + treeView->setItemsExpandable(false); + treeView->setRootIsDecorated(false); + treeView->setAllColumnsShowFocus(false); + + // set our own custom delegate + delegate = new FolderItemDelegate(treeView); + delegate->setShadowHidden(shadowHidden_); + treeView->setItemDelegateForColumn(FolderModel::ColumnFileName, delegate); + } + else { + FolderViewListView* listView; + if(view) { + listView = static_cast(view); + } + else { + listView = new FolderViewListView(this); + connect(listView, &FolderViewListView::activatedFiltered, this, &FolderView::onItemActivated); + view = listView; + } + setFocusProxy(listView); + + // set our own custom delegate + delegate = new FolderItemDelegate(listView); + delegate->setShadowHidden(shadowHidden_); + 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 (the drop indicator is set at "FolderView::childDragMoveEvent()") + view->setDragEnabled(true); + view->setAcceptDrops(true); + view->setDragDropMode(QAbstractItemView::DragDrop); + + // inline renaming + if(delegate) { + connect(delegate, &QAbstractItemDelegate::closeEditor, this, &FolderView::onClosingEditor); + } + + if(model_) { + // FIXME: preserve selections + model_->setThumbnailSize(iconSize.width()); + view->setModel(model_); + if(recreateView) { + connect(view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &FolderView::onSelectionChanged); + } + } + } +} + +// set proper grid size for the QListView based on current view mode, icon size, and font size. +void FolderView::updateGridSize() { + if(mode == DetailedListMode || !view) { + return; + } + FolderViewListView* listView = static_cast(view); + QSize icon = iconSize(mode); // size of the icon + QFontMetrics fm = fontMetrics(); // size of current font + QSize grid; // the final grid size + switch(mode) { + case IconMode: + case ThumbnailMode: { + // NOTE by PCMan about finding the optimal text label size: + // The average filename length on my root filesystem is roughly 18-20 chars. + // So, a reasonable size for the text label is about 10 chars each line since string of this length + // can be shown in two lines. If you consider word wrap, then the result is around 10 chars per word. + // In average, 10 char per line should be enough to display a "word" in the filename without breaking. + // The values can be estimated with this command: + // > find / | xargs basename -a | sed -e s'/[_-]/ /g' | wc -mcw + // However, this average only applies to English. For some Asian characters, such as Chinese chars, + // each char actually takes doubled space. To be safe, we use 13 chars per line x average char width + // to get a nearly optimal width for the text label. As most of the filenames have less than 40 chars + // 13 chars x 3 lines should be enough to show the full filenames for most files. + int textWidth = fm.averageCharWidth() * 13; + int textHeight = fm.lineSpacing() * 3; + grid.setWidth(qMax(icon.width(), textWidth) + 4); // a margin of 2 px for selection rects + grid.setHeight(icon.height() + textHeight + 4); // a margin of 2 px for selection rects + // grow to include margins + grid += 2*itemDelegateMargins_; + // let horizontal and vertical spacings be set only by itemDelegateMargins_ + listView->setSpacing(0); + + break; + } + default: + // FIXME: set proper item size + listView->setSpacing(2); + ; // do not use grid size + } + + FolderItemDelegate* delegate = static_cast(listView->itemDelegateForColumn(FolderModel::ColumnFileName)); + delegate->setItemSize(grid); + delegate->setIconSize(icon); + delegate->setMargins(itemDelegateMargins_); +} + +void FolderView::setIconSize(ViewMode mode, QSize size) { + Q_ASSERT(mode >= FirstViewMode && mode <= LastViewMode); + iconSize_[mode - FirstViewMode] = size; + if(viewMode() == mode) { + view->setIconSize(size); + if(model_) { + model_->setThumbnailSize(size.width()); + } + updateGridSize(); + } +} + +QSize FolderView::iconSize(ViewMode mode) const { + Q_ASSERT(mode >= FirstViewMode && mode <= LastViewMode); + return iconSize_[mode - FirstViewMode]; +} + +void FolderView::setMargins(QSize size) { + if(itemDelegateMargins_ != size.expandedTo(QSize(0, 0))) { + itemDelegateMargins_ = size.expandedTo(QSize(0, 0)); + updateGridSize(); + } +} + +void FolderView::setShadowHidden(bool shadowHidden) { + if(view && shadowHidden != shadowHidden_) { + shadowHidden_ = shadowHidden; + FolderItemDelegate* delegate = nullptr; + if(mode == DetailedListMode) { + FolderViewTreeView* treeView = static_cast(view); + delegate = static_cast(treeView->itemDelegateForColumn(FolderModel::ColumnFileName)); + } + else { + FolderViewListView* listView = static_cast(view); + delegate = static_cast(listView->itemDelegateForColumn(FolderModel::ColumnFileName)); + } + if(delegate) { + delegate->setShadowHidden(shadowHidden); + } + } +} + +FolderView::ViewMode FolderView::viewMode() const { + return mode; +} + +void FolderView::setAutoSelectionDelay(int delay) { + autoSelectionDelay_ = delay; +} + +QAbstractItemView* FolderView::childView() const { + return view; +} + +ProxyFolderModel* FolderView::model() const { + return model_; +} + +void FolderView::setModel(ProxyFolderModel* model) { + if(view) { + view->setModel(model); + QSize iconSize = iconSize_[mode - FirstViewMode]; + model->setThumbnailSize(iconSize.width()); + if(view->selectionModel()) { + connect(view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &FolderView::onSelectionChanged); + } + } + if(model_) { + delete model_; + } + model_ = model; +} + +bool FolderView::event(QEvent* event) { + switch(event->type()) { + case QEvent::StyleChange: + break; + case QEvent::FontChange: + updateGridSize(); + break; + default: + break; + } + return QWidget::event(event); +} + +void FolderView::contextMenuEvent(QContextMenuEvent* event) { + QWidget::contextMenuEvent(event); + QPoint pos = event->pos(); + QPoint view_pos = view->mapFromParent(pos); + QPoint viewport_pos = view->viewport()->mapFromParent(view_pos); + emitClickedAt(ContextMenuClick, viewport_pos); +} + +void FolderView::childMousePressEvent(QMouseEvent* event) { + // called from mousePressEvent() of child view + Qt::MouseButton button = event->button(); + if(button == Qt::MiddleButton) { + emitClickedAt(MiddleClick, event->pos()); + } + else if(button == Qt::BackButton) { + Q_EMIT clickedBack(); + } + else if(button == Qt::ForwardButton) { + Q_EMIT clickedForward(); + } +} + +void FolderView::emitClickedAt(ClickType type, const QPoint& pos) { + // indexAt() needs a point in "viewport" coordinates. + QModelIndex index = view->indexAt(pos); + if(index.isValid()) { + QVariant data = index.data(FolderModel::FileInfoRole); + auto info = data.value>(); + Q_EMIT clicked(type, info); + } + else { + // FIXME: should we show popup menu for the selected files instead + // if there are selected files? + if(type == ContextMenuClick) { + // clear current selection if clicked outside selected files + view->clearSelection(); + Q_EMIT clicked(type, nullptr); + } + } +} + +QModelIndexList FolderView::selectedRows(int column) const { + QItemSelectionModel* selModel = selectionModel(); + if(selModel) { + return selModel->selectedRows(column); + } + return QModelIndexList(); +} + +// This returns all selected "cells", which means all cells of the same row are returned. +QModelIndexList FolderView::selectedIndexes() const { + QItemSelectionModel* selModel = selectionModel(); + if(selModel) { + return selModel->selectedIndexes(); + } + return QModelIndexList(); +} + +QItemSelectionModel* FolderView::selectionModel() const { + return view ? view->selectionModel() : nullptr; +} + +Fm::FilePathList FolderView::selectedFilePaths() const { + if(model_) { + QModelIndexList selIndexes = mode == DetailedListMode ? selectedRows() : selectedIndexes(); + if(!selIndexes.isEmpty()) { + Fm::FilePathList paths; + QModelIndexList::const_iterator it; + for(it = selIndexes.constBegin(); it != selIndexes.constEnd(); ++it) { + auto file = model_->fileInfoFromIndex(*it); + paths.push_back(file->path()); + } + return paths; + } + } + return Fm::FilePathList(); +} + +bool FolderView::hasSelection() const { + QItemSelectionModel* selModel = selectionModel(); + return selModel ? selModel->hasSelection() : false; +} + +QModelIndex FolderView::indexFromFolderPath(const Fm::FilePath& folderPath) const { + if(!model_ || !folderPath.isValid()) { + return QModelIndex(); + } + QModelIndex index; + int count = model_->rowCount(); + for(int row = 0; row < count; ++row) { + index = model_->index(row, 0); + auto info = model_->fileInfoFromIndex(index); + if(info && info->isDir() && folderPath == info->path()) { + return index; + } + } + return QModelIndex(); +} + +void FolderView::selectFiles(const Fm::FileInfoList& files, bool add) { + if(!model_ || files.empty()) { + return; + } + if(!add) { + selectionModel()->clear(); + } + QModelIndex index, firstIndex; + int count = model_->rowCount(); + Fm::FileInfoList list = files; + bool singleFile(files.size() == 1); + QItemSelectionModel::SelectionFlags flags = QItemSelectionModel::Select; + if(mode == DetailedListMode) { + flags |= QItemSelectionModel::Rows; + } + for(int row = 0; row < count; ++row) { + if (list.empty()) { + break; + } + index = model_->index(row, 0); + auto info = model_->fileInfoFromIndex(index); + for(auto it = list.cbegin(); it != list.cend(); ++it) { + auto& item = *it; + if(item == info) { + selectionModel()->select(index, flags); + if (!firstIndex.isValid()) { + firstIndex = index; + } + list.erase(it); + break; + } + } + } + if (firstIndex.isValid()) { + view->scrollTo(firstIndex, QAbstractItemView::EnsureVisible); + if (singleFile) { // give focus to the single file + selectionModel()->setCurrentIndex(firstIndex, QItemSelectionModel::Current); + } + } +} + +Fm::FileInfoList FolderView::selectedFiles() const { + if(model_) { + QModelIndexList selIndexes = mode == DetailedListMode ? selectedRows() : selectedIndexes(); + if(!selIndexes.isEmpty()) { + Fm::FileInfoList files; + QModelIndexList::const_iterator it; + for(it = selIndexes.constBegin(); it != selIndexes.constEnd(); ++it) { + auto file = model_->fileInfoFromIndex(*it); + files.push_back(file); + } + return files; + } + } + return Fm::FileInfoList(); +} + +void FolderView::selectAll() { + if(mode == DetailedListMode) { + view->selectAll(); + } + else { + // NOTE: By default QListView::selectAll() selects all columns in the model. + // However, QListView only show the first column. Normal selection by mouse + // can only select the first column of every row. I consider this discripancy yet + // another design flaw of Qt. To make them consistent, we do it ourselves by only + // selecting the first column of every row and do not select all columns as Qt does. + // I'll report a Qt bug for this later. + if(model_) { + const QItemSelection sel{model_->index(0, 0), model_->index(model_->rowCount() - 1, 0)}; + selectionModel()->select(sel, QItemSelectionModel::Select); + } + } +} + +void FolderView::invertSelection() { + if(model_) { + QItemSelectionModel* selModel = view->selectionModel(); + QItemSelectionModel::SelectionFlags flags; + if(mode == DetailedListMode) { + flags |= QItemSelectionModel::Rows; + } + // we don't use a "for" loop on rows because it would be slow + const QItemSelection _all{model_->index(0, 0), model_->index(model_->rowCount() - 1, 0)}; + const QItemSelection _old{selModel->selection()}; + + selModel->select(_all, QItemSelectionModel::Select); + selModel->select(_old, QItemSelectionModel::Deselect); + } +} + +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) { + // Since it isn't possible to drop on a file (see "FolderModel::dropMimeData()"), + // we enable the drop indicator only when the cursor is on a folder. + QModelIndex index = view->indexAt(e->pos()); + if(index.isValid() && index.model()) { + QVariant data = index.model()->data(index, FolderModel::FileInfoRole); + auto info = data.value>(); + if(info && !info->isDir()) { + view->setDropIndicatorShown(false); + return; + } + } + view->setDropIndicatorShown(true); +} + +void FolderView::childDropEvent(QDropEvent* e) { + // qDebug("drop"); + // Try to support XDS + // NOTE: in theory, it's not possible to implement XDS with pure Qt. + // We achieved this with some dirty XCB/XDND workarounds. + // Please refer to XdndWorkaround::clientMessage() in xdndworkaround.cpp for details. + if(QX11Info::isPlatformX11() && e->mimeData()->hasFormat("XdndDirectSave0")) { + e->setDropAction(Qt::CopyAction); + const QWidget* targetWidget = childView()->viewport(); + // these are dynamic QObject property set by our XDND workarounds in xdndworkaround.cpp. + xcb_window_t dndSource = xcb_window_t(targetWidget->property("xdnd::lastDragSource").toUInt()); + //xcb_timestamp_t dropTimestamp = (xcb_timestamp_t)targetWidget->property("xdnd::lastDropTime").toUInt(); + // qDebug() << "XDS: source window" << dndSource << dropTimestamp; + if(dndSource != 0) { + xcb_atom_t XdndDirectSaveAtom = XdndWorkaround::internAtom("XdndDirectSave0", 15); + xcb_atom_t textAtom = XdndWorkaround::internAtom("text/plain", 10); + + // 1. get the filename from XdndDirectSave property of the source window + QByteArray basename = XdndWorkaround::windowProperty(dndSource, XdndDirectSaveAtom, textAtom, 1024); + + // 2. construct the fill URI for the file, and update the source window property. + Fm::FilePath filePath; + if(model_) { + QModelIndex index = view->indexAt(e->pos()); + auto info = model_->fileInfoFromIndex(index); + if(info && info->isDir()) { + filePath = info->path().child(basename); + } + } + if(!filePath.isValid()) { + filePath = path().child(basename); + } + QByteArray fileUri = filePath.uri().get(); + XdndWorkaround::setWindowProperty(dndSource, XdndDirectSaveAtom, textAtom, (void*)fileUri.constData(), fileUri.length()); + + // 3. send to XDS selection data request with type "XdndDirectSave" to the source window and + // receive result from the source window. (S: success, E: error, or F: failure) + QByteArray result = e->mimeData()->data("XdndDirectSave0"); + // NOTE: there seems to be some bugs in file-roller so it always replies with "E" even if the + // file extraction is finished successfully. Anyways, we ignore any error at the moment. + } + e->accept(); // yeah! we've done with XDS so stop Qt from further event propagation. + return; + } + + if(e->keyboardModifiers() == Qt::NoModifier) { + // if no key modifiers are used, popup a menu + // to ask the user for the action he/she wants to perform. + Qt::DropAction action = DndActionMenu::askUser(e->possibleActions(), QCursor::pos()); + e->setDropAction(action); + } +} + +bool FolderView::eventFilter(QObject* watched, QEvent* event) { + // NOTE: Instead of simply filtering the drag and drop events of the child view in + // the event filter, we overrided each event handler virtual methods in + // both QListView and QTreeView and added some childXXXEvent() callbacks. + // We did this because of a design flaw of Qt. + // All QAbstractScrollArea derived widgets, including QAbstractItemView + // contains an internal child widget, which is called a viewport. + // The events actually comes from the child viewport, not the parent view itself. + // Qt redirects the events of viewport to the viewportEvent() method of + // QAbstractScrollArea and let the parent widget handle the events. + // Qt implemented this using a event filter installed on the child viewport widget. + // That means, when we try to install an event filter on the viewport, + // there is already a filter installed by Qt which will be called before ours. + // So we can never intercept the event handling of QAbstractItemView by using a filter. + // That's why we override respective virtual methods for different events. + if(view && watched == view->viewport()) { + switch(event->type()) { + case QEvent::HoverMove: + // activate items on single click + if(style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick)) { + QHoverEvent* hoverEvent = static_cast(event); + QModelIndex index = view->indexAt(hoverEvent->pos()); // find out the hovered item + if(index.isValid()) { // change the cursor to a hand when hovering on an item + setCursor(Qt::PointingHandCursor); + } + else { + setCursor(Qt::ArrowCursor); + } + // turn on auto-selection for hovered item when single click mode is used. + if(autoSelectionDelay_ > 0 && model_) { + if(!autoSelectionTimer_) { + autoSelectionTimer_ = new QTimer(this); + connect(autoSelectionTimer_, &QTimer::timeout, this, &FolderView::onAutoSelectionTimeout); + lastAutoSelectionIndex_ = QModelIndex(); + } + autoSelectionTimer_->start(autoSelectionDelay_); + } + } + break; + case QEvent::HoverLeave: + if(style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick)) { + setCursor(Qt::ArrowCursor); + } + break; + case QEvent::Wheel: + // don't let the view scroll during an inline renaming + if (view) { + FolderItemDelegate* delegate = nullptr; + if(mode == DetailedListMode) { + FolderViewTreeView* treeView = static_cast(view); + delegate = static_cast(treeView->itemDelegateForColumn(FolderModel::ColumnFileName)); + } + else { + FolderViewListView* listView = static_cast(view); + delegate = static_cast(listView->itemDelegateForColumn(FolderModel::ColumnFileName)); + } + if (delegate && delegate->hasEditor()) { + return true; + } + } + // This is to fix #85: Scrolling doesn't work in compact view + // Actually, I think it's the bug of Qt, not ours. + // When in compact mode, only the horizontal scroll bar is used and the vertical one is hidden. + // So, when a user scroll his mouse wheel, it's reasonable to scroll the horizontal scollbar. + // Qt does not implement such a simple feature, unfortunately. + // We do it by forwarding the scroll event in the viewport to the horizontal scrollbar. + // FIXME: if someday Qt supports this, we have to disable the workaround. + if(mode == CompactMode) { + QScrollBar* scroll = view->horizontalScrollBar(); + if(scroll) { + QApplication::sendEvent(scroll, event); + return true; + } + } + // Smooth Scrolling + // Some tricks are adapted from . + else if(mode != DetailedListMode + && event->spontaneous() + && !(QApplication::keyboardModifiers() & (Qt::ShiftModifier | Qt::AltModifier))) { + if(QScrollBar* vbar = view->verticalScrollBar()) { + // keep track of the wheel event for smooth scrolling + wheelEvent_ = static_cast(event); + int delta = wheelEvent_->angleDelta().y(); + if((delta > 0 && vbar->value() == vbar->minimum()) || (delta < 0 && vbar->value() == vbar->maximum())) { + break; // the scrollbar can't move + } + // get a rough estimation of the wheel speed and disable animation if it's too high + static QList wheelEvents; + wheelEvents << QDateTime::currentMSecsSinceEpoch(); + while(wheelEvents.last() - wheelEvents.first() > 500) { + wheelEvents.removeFirst(); + } + if(wheelEvents.size() > 10) { + break; + } + + if(!smoothScrollTimer_) { + smoothScrollTimer_ = new QTimer(); + connect(smoothScrollTimer_, &QTimer::timeout, this, &FolderView::scrollSmoothly); + } + + // set the data for smooth scrolling + scollData data; + data.delta = delta; + data.leftFrames = scrollAnimFrames; + queuedScrollSteps_.append(data); + smoothScrollTimer_->start(1000 / SCROLL_FRAMES_PER_SEC); + return true; + } + } + break; + default: + break; + } + } + return QObject::eventFilter(watched, event); +} + +void FolderView::scrollSmoothly() { + if(!wheelEvent_ || !view->verticalScrollBar()) { + return; + } + + int totalDelta = 0; + QList::iterator it = queuedScrollSteps_.begin(); + while(it != queuedScrollSteps_.end()) { + if(it->leftFrames == 1) { // find the exact delta for the last frame + totalDelta += it->delta - (scrollAnimFrames - 1) * qRound((qreal)it->delta / (qreal)scrollAnimFrames); + it = queuedScrollSteps_.erase(it); + } + else { + totalDelta += qRound((qreal)it->delta / (qreal)scrollAnimFrames); + -- it->leftFrames; + ++it; + } + } + if(totalDelta != 0) { + // as in qevent.cpp -> QWheelEvent::QWheelEvent() + QWheelEvent e(wheelEvent_->pos(), wheelEvent_->globalPos(), + totalDelta, + wheelEvent_->buttons(), Qt::NoModifier, Qt::Vertical); + QApplication::sendEvent(view->verticalScrollBar(), &e); + } + if(queuedScrollSteps_.empty()) { + smoothScrollTimer_->stop(); + } +} + +// this slot handles auto-selection of items. +void FolderView::onAutoSelectionTimeout() { + if(QApplication::mouseButtons() != Qt::NoButton) { + return; + } + + // don't do anything if the cursor is on selection corner icon + if(mode != DetailedListMode) { + FolderViewListView* listView = static_cast(view); + if(listView->cursorOnSelectionCorner()) { + return; + } + } + + Qt::KeyboardModifiers mods = QApplication::keyboardModifiers(); + QPoint pos = view->viewport()->mapFromGlobal(QCursor::pos()); // convert to viewport coordinates + QModelIndex index = view->indexAt(pos); // find out the hovered item + QItemSelectionModel::SelectionFlags flags = (mode == DetailedListMode ? QItemSelectionModel::Rows : QItemSelectionModel::NoUpdate); + QItemSelectionModel* selModel = view->selectionModel(); + + if(mods & Qt::ControlModifier) { // Ctrl key is pressed + if(selModel->isSelected(index) && index != lastAutoSelectionIndex_) { + // unselect a previously selected item + selModel->select(index, flags | QItemSelectionModel::Deselect); + lastAutoSelectionIndex_ = QModelIndex(); + } + else { + // select an unselected item + selModel->select(index, flags | QItemSelectionModel::Select); + lastAutoSelectionIndex_ = index; + } + selModel->setCurrentIndex(index, QItemSelectionModel::NoUpdate); // move the cursor + } + else if(mods & Qt::ShiftModifier) { // Shift key is pressed + // select all items between current index and the hovered index. + QModelIndex current = selModel->currentIndex(); + if(selModel->hasSelection() && current.isValid()) { + selModel->clear(); // clear old selection + selModel->setCurrentIndex(current, QItemSelectionModel::NoUpdate); + int begin = current.row(); + int end = index.row(); + if(begin > end) { + qSwap(begin, end); + } + for(int row = begin; row <= end; ++row) { + QModelIndex sel = model_->index(row, 0); + selModel->select(sel, flags | QItemSelectionModel::Select); + } + } + else { // no items are selected, select the hovered item. + if(index.isValid()) { + selModel->select(index, flags | QItemSelectionModel::SelectCurrent); + selModel->setCurrentIndex(index, QItemSelectionModel::NoUpdate); + } + } + lastAutoSelectionIndex_ = index; + } + else if(mods == Qt::NoModifier) { // no modifier keys are pressed. + if(index.isValid()) { + // select the hovered item + view->clearSelection(); + selModel->select(index, flags | QItemSelectionModel::SelectCurrent); + selModel->setCurrentIndex(index, QItemSelectionModel::NoUpdate); + } + lastAutoSelectionIndex_ = index; + } + + autoSelectionTimer_->deleteLater(); + autoSelectionTimer_ = nullptr; +} + +void FolderView::onFileClicked(int type, const std::shared_ptr &fileInfo) { + if(type == ActivatedClick) { + if(fileLauncher_) { + Fm::FileInfoList files; + files.push_back(fileInfo); + fileLauncher_->launchFiles(nullptr, std::move(files)); + } + } + else if(type == ContextMenuClick) { + Fm::FilePath folderPath; + bool isWritableDir(true); + auto files = selectedFiles(); + if(!files.empty()) { + auto& first = files.front(); + if(files.size() == 1 && first->isDir()) { + folderPath = first->path(); + isWritableDir = first->isWritable(); + } + } + if(!folderPath.isValid()) { + folderPath = path(); + if(auto info = folderInfo()) { + isWritableDir = info->isWritable(); + } + } + QMenu* menu = nullptr; + if(fileInfo) { + // show context menu + auto files = selectedFiles(); + if(!files.empty()) { + Fm::FileMenu* fileMenu = new Fm::FileMenu(files, fileInfo, folderPath, isWritableDir, QString(), view); + fileMenu->setFileLauncher(fileLauncher_); + fileMenu->addTrustAction(); + prepareFileMenu(fileMenu); + menu = fileMenu; + } + } + else if (folderInfo()) { + Fm::FolderMenu* folderMenu = new Fm::FolderMenu(this); + prepareFolderMenu(folderMenu); + menu = folderMenu; + } + if(menu) { + menu->exec(QCursor::pos()); + delete menu; + } + } +} + +void FolderView::onClipboardDataChange() { + if(model_) { + const QClipboard* clipboard = QApplication::clipboard(); + const QMimeData* data = clipboard->mimeData(); + Fm::FilePathList paths; + bool isCutSelection; + std::tie(paths, isCutSelection) = Fm::parseClipboardData(*data); + if(!folder()->path().hasUriScheme("search") // skip for search results + && isCutSelection + && Fm::isCurrentPidClipboardData(*data)) { // set cut files only with this app + auto cutDirPath = paths.size() > 0 ? paths[0].parent() : FilePath(); + // set the cut file(s) only if the cutting is done here + if(folder()->path() == cutDirPath + && selectedFilePaths() == paths) { + model_->setCutFiles(selectionModel()->selection()); + } + else if(folder()->hadCutFilesUnset() || folder()->hasCutFiles()) { + model_->setCutFiles(QItemSelection()); + } + return; + } + + folder()->setCutFiles(std::make_shared()); // clean Folder::cutFilesHashSet_ + if(folder()->hadCutFilesUnset()) { + model_->setCutFiles(QItemSelection()); // update indexes if there were cut files here + } + } +} + +void FolderView::prepareFileMenu(FileMenu* /*menu*/) { +} + +void FolderView::prepareFolderMenu(FolderMenu* /*menu*/) { +} diff --git a/src/folderview.h b/src/folderview.h new file mode 100644 index 0000000..be7c573 --- /dev/null +++ b/src/folderview.h @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_FOLDERVIEW_H +#define FM_FOLDERVIEW_H + +#include "libfmqtglobals.h" +#include +#include +#include +#include +#include "foldermodel.h" +#include "proxyfoldermodel.h" + +#include "core/folder.h" + +class QTimer; + +namespace Fm { + +class FileMenu; +class FolderMenu; +class FileLauncher; +class FolderViewStyle; + +class LIBFM_QT_API FolderView : public QWidget { + Q_OBJECT + +public: + + enum ViewMode { + FirstViewMode = 1, + IconMode = FirstViewMode, + CompactMode, + DetailedListMode, + ThumbnailMode, + LastViewMode = ThumbnailMode, + NumViewModes = (LastViewMode - FirstViewMode + 1) + }; + + enum ClickType { + ActivatedClick, + MiddleClick, + ContextMenuClick + }; + + friend class FolderViewTreeView; + friend class FolderViewListView; + + explicit FolderView(ViewMode _mode = IconMode, QWidget* parent = nullptr); + + explicit FolderView(QWidget* parent): FolderView{IconMode, parent} {} + + virtual ~FolderView(); + + void setViewMode(ViewMode _mode); + ViewMode viewMode() const; + + void setIconSize(ViewMode mode, QSize size); + QSize iconSize(ViewMode mode) const; + + QAbstractItemView* childView() const; + + ProxyFolderModel* model() const; + void setModel(ProxyFolderModel* _model); + + std::shared_ptr folder() const { + return model_ ? static_cast(model_->sourceModel())->folder() : nullptr; + } + + std::shared_ptr folderInfo() const { + auto _folder = folder(); + return _folder ? _folder->info() : nullptr; + } + + Fm::FilePath path() { + auto _folder = folder(); + return _folder ? _folder->path() : Fm::FilePath(); + } + + QItemSelectionModel* selectionModel() const; + Fm::FileInfoList selectedFiles() const; + Fm::FilePathList selectedFilePaths() const; + bool hasSelection() const; + QModelIndex indexFromFolderPath(const Fm::FilePath& folderPath) const; + void selectFiles(const Fm::FileInfoList& files, bool add = false); + + void selectAll(); + + void invertSelection(); + + void setFileLauncher(FileLauncher* launcher) { + fileLauncher_ = launcher; + } + + FileLauncher* fileLauncher() { + return fileLauncher_; + } + + int autoSelectionDelay() const { + return autoSelectionDelay_; + } + + void setAutoSelectionDelay(int delay); + + void setShadowHidden(bool shadowHidden); + + QList getCustomColumnWidths() const { + return customColumnWidths_; + } + void setCustomColumnWidths(const QList &widths); + + QList getHiddenColumns() const { + return hiddenColumns_.toList(); + } + void setHiddenColumns(const QList &columns); + +protected: + virtual bool event(QEvent* event); + virtual void contextMenuEvent(QContextMenuEvent* event); + virtual void childMousePressEvent(QMouseEvent* event); + virtual void childDragEnterEvent(QDragEnterEvent* event); + virtual void childDragMoveEvent(QDragMoveEvent* e); + virtual void childDragLeaveEvent(QDragLeaveEvent* e); + virtual void childDropEvent(QDropEvent* e); + + void emitClickedAt(ClickType type, const QPoint& pos); + + QModelIndexList selectedRows(int column = 0) const; + QModelIndexList selectedIndexes() const; + + virtual void prepareFileMenu(Fm::FileMenu* menu); + virtual void prepareFolderMenu(Fm::FolderMenu* menu); + + virtual bool eventFilter(QObject* watched, QEvent* event); + + void updateGridSize(); // called when view mode, icon size, font size or cell margin is changed + + QSize getMargins() const { + return itemDelegateMargins_; + } + + // sets the cell margins in the icon and thumbnail modes + // and calls updateGridSize() when needed + void setMargins(QSize size); + +public Q_SLOTS: + void onItemActivated(QModelIndex index); + void onSelectionChanged(const QItemSelection& selected, const QItemSelection& deselected); + virtual void onFileClicked(int type, const std::shared_ptr& fileInfo); + void onClipboardDataChange(); + +private Q_SLOTS: + void onAutoSelectionTimeout(); + void onSelChangedTimeout(); + void onClosingEditor(QWidget* editor, QAbstractItemDelegate::EndEditHint hint); + void scrollSmoothly(); + +Q_SIGNALS: + void clicked(int type, const std::shared_ptr& file); + void clickedBack(); + void clickedForward(); + void selChanged(); + void sortChanged(); + + void columnResizedByUser(); + void columnHiddenByUser(); + +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_; + bool shadowHidden_; + // smooth scrolling: + struct scollData { + int delta; + int leftFrames; + }; + QTimer *smoothScrollTimer_; + QWheelEvent *wheelEvent_; + QList queuedScrollSteps_; + QList customColumnWidths_; + QSet hiddenColumns_; +}; + +} + +#endif // FM_FOLDERVIEW_H diff --git a/src/folderview_p.h b/src/folderview_p.h new file mode 100644 index 0000000..4587029 --- /dev/null +++ b/src/folderview_p.h @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_FOLDERVIEW_P_H +#define FM_FOLDERVIEW_P_H + +#include +#include +#include +#include "folderview.h" + +class QTimer; + +namespace Fm { + +// override these classes for implementing FolderView +class FolderViewListView : public QListView { + Q_OBJECT +public: + friend class FolderView; + FolderViewListView(QWidget* parent = nullptr); + virtual ~FolderViewListView(); + virtual void startDrag(Qt::DropActions supportedActions); + virtual void mousePressEvent(QMouseEvent* event); + virtual void mouseMoveEvent(QMouseEvent* event); + virtual void mouseReleaseEvent(QMouseEvent* event); + virtual void mouseDoubleClickEvent(QMouseEvent* event); + virtual void dragEnterEvent(QDragEnterEvent* event); + virtual void dragMoveEvent(QDragMoveEvent* e); + virtual void dragLeaveEvent(QDragLeaveEvent* e); + virtual void dropEvent(QDropEvent* e); + + virtual QModelIndex indexAt(const QPoint & point) const; + + inline void setPositionForIndex(const QPoint & position, const QModelIndex & index) { + QListView::setPositionForIndex(position, index); + } + + inline QRect rectForIndex(const QModelIndex & index) const { + return QListView::rectForIndex(index); + } + + inline QStyleOptionViewItem getViewOptions() { + return viewOptions(); + } + + inline bool cursorOnSelectionCorner() const { + return cursorOnSelectionCorner_; + } + +Q_SIGNALS: + void activatedFiltered(const QModelIndex &index); + +protected: + virtual QModelIndex moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers); + +private Q_SLOTS: + void activation(const QModelIndex &index); + +private: + bool activationAllowed_; + mutable bool cursorOnSelectionCorner_; +}; + +class FolderViewTreeView : public QTreeView { + Q_OBJECT +public: + friend class FolderView; + FolderViewTreeView(QWidget* parent = nullptr); + virtual ~FolderViewTreeView(); + virtual void setModel(QAbstractItemModel* model); + virtual void mousePressEvent(QMouseEvent* event); + virtual void mouseMoveEvent(QMouseEvent* event); + virtual void mouseReleaseEvent(QMouseEvent* event); + virtual void mouseDoubleClickEvent(QMouseEvent* event); + virtual void dragEnterEvent(QDragEnterEvent* event); + virtual void dragMoveEvent(QDragMoveEvent* e); + virtual void dragLeaveEvent(QDragLeaveEvent* e); + virtual void dropEvent(QDropEvent* e); + + // for rubberband + virtual void paintEvent(QPaintEvent * event); + virtual void setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags command); + + virtual void rowsInserted(const QModelIndex& parent,int start, int end); + virtual void rowsAboutToBeRemoved(const QModelIndex& parent,int start, int end); + virtual void dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector& roles = QVector{}); + virtual void reset(); + + virtual void resizeEvent(QResizeEvent* event); + void queueLayoutColumns(); + + virtual void keyboardSearch(const QString &search) { + QAbstractItemView::keyboardSearch(search); // let items be selected by typing + } + + void setCustomColumnWidths(const QList &widths); + + void setHiddenColumns(const QSet &columns); + +Q_SIGNALS: + void activatedFiltered(const QModelIndex &index); + void columnResizedByUser(int visualIndex, int newWidth); + void autoResizeEnabled(); + void columnHiddenByUser(int visibleIndex, bool hidden); + +private Q_SLOTS: + void layoutColumns(); + void activation(const QModelIndex &index); + void onSortFilterChanged(); + void headerContextMenu(const QPoint &p); + +private: + bool doingLayout_; + QTimer* layoutTimer_; + bool activationAllowed_; + QList customColumnWidths_; + QSet hiddenColumns_; + QPoint mousePressPoint_; + QRect rubberBandRect_; + QItemSelectionModel::SelectionFlag ctrlDragSelectionFlag_; +}; + + +} // namespace Fm + +#endif // FM_FOLDERVIEW_P_H diff --git a/src/fontbutton.cpp b/src/fontbutton.cpp new file mode 100644 index 0000000..f3b21a6 --- /dev/null +++ b/src/fontbutton.cpp @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "fontbutton.h" +#include +#include + +namespace Fm { + +FontButton::FontButton(QWidget* parent): QPushButton(parent) { + connect(this, &QPushButton::clicked, this, &FontButton::onClicked); +} + +FontButton::~FontButton() { +} + +void FontButton::onClicked() { + QFontDialog dlg(font_); + if(dlg.exec() == QDialog::Accepted) { + setFont(dlg.selectedFont()); + } +} + +void FontButton::setFont(QFont font) { + font_ = font; + QString text = font.family(); + if(font.bold()) { + text += " "; + text += tr("Bold"); + } + if(font.italic()) { + text += " "; + text += tr("Italic"); + } + text += QString(" %1").arg(font.pointSize()); + setText(text); + Q_EMIT changed(); +} + + +} // namespace Fm diff --git a/src/fontbutton.h b/src/fontbutton.h new file mode 100644 index 0000000..4dee7b2 --- /dev/null +++ b/src/fontbutton.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_FONTBUTTON_H +#define FM_FONTBUTTON_H + +#include "libfmqtglobals.h" +#include + + +namespace Fm { + +class LIBFM_QT_API FontButton : public QPushButton { + Q_OBJECT +public: + explicit FontButton(QWidget* parent = 0); + virtual ~FontButton(); + + QFont font() { + return font_; + } + + void setFont(QFont font); + +Q_SIGNALS: + void changed(); + +private Q_SLOTS: + void onClicked(); + +private: + QFont font_; +}; + +} + +#endif // FM_FONTBUTTON_H diff --git a/src/libfm-qt.pc.in b/src/libfm-qt.pc.in new file mode 100644 index 0000000..793fb13 --- /dev/null +++ b/src/libfm-qt.pc.in @@ -0,0 +1,12 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +exec_prefix=${prefix} +libdir=${exec_prefix}/lib +includedir=${prefix}/include + +Name: libfm-qt +Description: A Qt/glib/gio-based lib used to develop file managers providing some file management utilities. +URL: https://github.com/lxqt/libfm-qt +Requires: @REQUIRED_QT@ +Version: @LIBFM_QT_API_VERSION@ +Libs: -L${libdir} -l@LIBFM_QT_LIBRARY_NAME@ +Cflags: -I${includedir} diff --git a/src/libfmqt.cpp b/src/libfmqt.cpp new file mode 100644 index 0000000..83905ee --- /dev/null +++ b/src/libfmqt.cpp @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "libfmqt.h" +#include +#include +#include "core/thumbnailer.h" +#include "xdndworkaround.h" +#include "core/vfs/fm-file.h" +#include "core/legacy/fm-config.h" + +namespace Fm { + +struct LibFmQtData { + LibFmQtData(); + ~LibFmQtData(); + + QTranslator translator; + XdndWorkaround workaround; + int refCount; + Q_DISABLE_COPY(LibFmQtData) +}; + +static LibFmQtData* theLibFmData = nullptr; + +extern "C" { + +GFile *_fm_vfs_search_new_for_uri(const char *uri); // defined in vfs-search.c +GFile *_fm_vfs_menu_new_for_uri(const char *uri); // defined in vfs-menu.c + +} + +static GFile* lookupSearchUri(GVfs * /*vfs*/, const char *identifier, gpointer /*user_data*/) { + return _fm_vfs_search_new_for_uri(identifier); +} + +static GFile* lookupMenuUri(GVfs * /*vfs*/, const char *identifier, gpointer /*user_data*/) { + return _fm_vfs_menu_new_for_uri(identifier); +} + +LibFmQtData::LibFmQtData(): refCount(1) { +#if !GLIB_CHECK_VERSION(2, 36, 0) + g_type_init(); +#endif + // turn on glib debug message + // g_setenv("G_MESSAGES_DEBUG", "all", true); + Fm::Thumbnailer::loadAll(); + translator.load("libfm-qt_" + QLocale::system().name(), LIBFM_QT_DATA_DIR "/translations"); + + // FIXME: we keep the FmConfig data structure here to keep compatibility with legacy libfm API. + fm_config_init(); + + // register some URI schemes implemented by libfm + GVfs* vfs = g_vfs_get_default(); + g_vfs_register_uri_scheme(vfs, "menu", lookupMenuUri, nullptr, nullptr, lookupMenuUri, nullptr, nullptr); + g_vfs_register_uri_scheme(vfs, "search", lookupSearchUri, nullptr, nullptr, lookupSearchUri, nullptr, nullptr); +} + +LibFmQtData::~LibFmQtData() { + // _fm_file_finalize(); + + GVfs* vfs = g_vfs_get_default(); + g_vfs_unregister_uri_scheme(vfs, "menu"); + g_vfs_unregister_uri_scheme(vfs, "search"); +} + +LibFmQt::LibFmQt() { + if(!theLibFmData) { + theLibFmData = new LibFmQtData(); + } + else { + ++theLibFmData->refCount; + } + d = theLibFmData; +} + +LibFmQt::~LibFmQt() { + if(--d->refCount == 0) { + delete d; + theLibFmData = nullptr; + } +} + +QTranslator* LibFmQt::translator() { + return &d->translator; +} + +} // namespace Fm diff --git a/src/libfmqt.h b/src/libfmqt.h new file mode 100644 index 0000000..a186064 --- /dev/null +++ b/src/libfmqt.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_APPLICATION_H +#define FM_APPLICATION_H + +#include "libfmqtglobals.h" +#include +#include + +namespace Fm { + +struct LibFmQtData; + +class LIBFM_QT_API LibFmQt { +public: + explicit LibFmQt(); + ~LibFmQt(); + + QTranslator* translator(); + +private: + LibFmQt(LibFmQt& other); // disable copy + LibFmQtData* d; +}; + +} + +#endif // FM_APPLICATION_H diff --git a/src/libfmqtglobals.h b/src/libfmqtglobals.h new file mode 100644 index 0000000..1a6d4fe --- /dev/null +++ b/src/libfmqtglobals.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef _LIBFM_QT_GLOBALS_ +#define _LIBFM_QT_GLOBALS_ + +#include "fm-qt_export.h" + +#endif diff --git a/src/mount-operation-password.ui b/src/mount-operation-password.ui new file mode 100644 index 0000000..72f1b30 --- /dev/null +++ b/src/mount-operation-password.ui @@ -0,0 +1,215 @@ + + + MountOperationPasswordDialog + + + + 0 + 0 + 244 + 302 + + + + + 0 + 0 + + + + Mount + + + + + + false + + + false + + + + + + + 0 + 0 + + + + + + + + + + + Connect &anonymously + + + usernameGroup + + + + + + + Connect as u&ser: + + + usernameGroup + + + + + + + + + + + + + 0 + 0 + + + + &Username: + + + username + + + + + + + QLineEdit::Password + + + + + + + + 0 + 0 + + + + &Password: + + + password + + + + + + + &Domain: + + + domain + + + + + + + + + + + + Forget password &immediately + + + passwordGroup + + + + + + + Remember password until you &logout + + + passwordGroup + + + + + + + Remember &forever + + + passwordGroup + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + Anonymous + asUser + username + domain + password + forgetPassword + sessionPassword + storePassword + + + + + buttonBox + accepted() + MountOperationPasswordDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + MountOperationPasswordDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + + + + + diff --git a/src/mountoperation.cpp b/src/mountoperation.cpp new file mode 100644 index 0000000..09a8df2 --- /dev/null +++ b/src/mountoperation.cpp @@ -0,0 +1,253 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "mountoperation.h" +#include // for _() +#include // for g_chdir() +#include +#include +#include +#include "mountoperationpassworddialog_p.h" +#include "mountoperationquestiondialog_p.h" +#include "ui_mount-operation-password.h" +#include "core/gioptrs.h" + +namespace Fm { + +MountOperation::MountOperation(bool interactive, QWidget* parent): + QObject(parent), + op(g_mount_operation_new()), + cancellable_(g_cancellable_new()), + running(false), + interactive_(interactive), + eventLoop(nullptr), + autoDestroy_(true) { + + g_signal_connect(op, "ask-password", G_CALLBACK(onAskPassword), this); + g_signal_connect(op, "ask-question", G_CALLBACK(onAskQuestion), this); + // g_signal_connect(op, "reply", G_CALLBACK(onReply), this); + +#if GLIB_CHECK_VERSION(2, 20, 0) + g_signal_connect(op, "aborted", G_CALLBACK(onAbort), this); +#endif +#if GLIB_CHECK_VERSION(2, 22, 0) + g_signal_connect(op, "show-processes", G_CALLBACK(onShowProcesses), this); +#endif +#if GLIB_CHECK_VERSION(2, 34, 0) + g_signal_connect(op, "show-unmount-progress", G_CALLBACK(onShowUnmountProgress), this); +#endif + +} + +MountOperation::~MountOperation() { + qDebug("delete MountOperation"); + if(cancellable_) { + cancel(); + g_object_unref(cancellable_); + } + + if(eventLoop) { // if wait() is called to block the main loop, but the event loop is still running + // NOTE: is this possible? + eventLoop->exit(1); + } + + if(op) { + g_signal_handlers_disconnect_by_func(op, (gpointer)G_CALLBACK(onAskPassword), this); + g_signal_handlers_disconnect_by_func(op, (gpointer)G_CALLBACK(onAskQuestion), this); +#if GLIB_CHECK_VERSION(2, 20, 0) + g_signal_handlers_disconnect_by_func(op, (gpointer)G_CALLBACK(onAbort), this); +#endif +#if GLIB_CHECK_VERSION(2, 22, 0) + g_signal_handlers_disconnect_by_func(op, (gpointer)G_CALLBACK(onShowProcesses), this); +#endif +#if GLIB_CHECK_VERSION(2, 34, 0) + g_signal_handlers_disconnect_by_func(op, (gpointer)G_CALLBACK(onShowUnmountProgress), this); +#endif + g_object_unref(op); + } + // qDebug("MountOperation deleted"); +} + +void MountOperation::mountEnclosingVolume(const FilePath &path) { + g_file_mount_enclosing_volume(path.gfile().get(), G_MOUNT_MOUNT_NONE, op, cancellable_, + (GAsyncReadyCallback)onMountFileFinished, new QPointer(this)); +} + +void MountOperation::mountMountable(const FilePath &mountable) { + g_file_mount_mountable(mountable.gfile().get(), G_MOUNT_MOUNT_NONE, op, cancellable_, + (GAsyncReadyCallback)onMountMountableFinished, new QPointer(this)); +} + +void MountOperation::onAbort(GMountOperation* /*_op*/, MountOperation* /*pThis*/) { + +} + +void MountOperation::onAskPassword(GMountOperation* /*_op*/, gchar* message, gchar* default_user, gchar* default_domain, GAskPasswordFlags flags, MountOperation* pThis) { + qDebug("ask password"); + MountOperationPasswordDialog dlg(pThis, flags); + dlg.setMessage(QString::fromUtf8(message)); + dlg.setDefaultUser(QString::fromUtf8(default_user)); + dlg.setDefaultDomain(QString::fromUtf8(default_domain)); + dlg.exec(); +} + +void MountOperation::onAskQuestion(GMountOperation* /*_op*/, gchar* message, GStrv choices, MountOperation* pThis) { + qDebug("ask question"); + MountOperationQuestionDialog dialog(pThis, message, choices); + dialog.exec(); +} + +/* +void MountOperation::onReply(GMountOperation* _op, GMountOperationResult result, MountOperation* pThis) { + qDebug("reply"); +} +*/ + +void MountOperation::onShowProcesses(GMountOperation* /*_op*/, gchar* /*message*/, GArray* /*processes*/, GStrv /*choices*/, MountOperation* /*pThis*/) { + qDebug("show processes"); +} + +void MountOperation::onShowUnmountProgress(GMountOperation* /*_op*/, gchar* /*message*/, gint64 /*time_left*/, gint64 /*bytes_left*/, MountOperation* /*pThis*/) { + qDebug("show unmount progress"); +} + +void MountOperation::onEjectMountFinished(GMount* mount, GAsyncResult* res, QPointer< MountOperation >* pThis) { + if(*pThis) { + GError* error = nullptr; + g_mount_eject_with_operation_finish(mount, res, &error); + (*pThis)->handleFinish(error); + } + delete pThis; +} + +void MountOperation::onEjectVolumeFinished(GVolume* volume, GAsyncResult* res, QPointer< MountOperation >* pThis) { + if(*pThis) { + GError* error = nullptr; + g_volume_eject_with_operation_finish(volume, res, &error); + (*pThis)->handleFinish(error); + } + delete pThis; +} + +void MountOperation::onMountFileFinished(GFile* file, GAsyncResult* res, QPointer< MountOperation >* pThis) { + if(*pThis) { + GError* error = nullptr; + g_file_mount_enclosing_volume_finish(file, res, &error); + (*pThis)->handleFinish(error); + } + delete pThis; +} + +void MountOperation::onMountMountableFinished(GFile* file, GAsyncResult* res, QPointer* pThis) { + if(*pThis) { + GError* error = nullptr; + g_file_mount_mountable_finish(file, res, &error); + (*pThis)->handleFinish(error); + } + delete pThis; +} + +void MountOperation::onMountVolumeFinished(GVolume* volume, GAsyncResult* res, QPointer< MountOperation >* pThis) { + if(*pThis) { + GError* error = nullptr; + g_volume_mount_finish(volume, res, &error); + (*pThis)->handleFinish(error); + } + delete pThis; +} + +void MountOperation::onUnmountMountFinished(GMount* mount, GAsyncResult* res, QPointer< MountOperation >* pThis) { + if(*pThis) { + GError* error = nullptr; + g_mount_unmount_with_operation_finish(mount, res, &error); + (*pThis)->handleFinish(error); + } + delete pThis; +} + +void MountOperation::handleFinish(GError* error) { + qDebug("operation finished: %p", static_cast(error)); + if(error) { + bool showError = interactive_; + if(error->domain == G_IO_ERROR) { + if(error->code == G_IO_ERROR_FAILED) { + // Generate a more human-readable error message instead of using a gvfs one. + // The original error message is something like: + // Error unmounting: umount exited with exit code 1: + // helper failed with: umount: only root can unmount + // UUID=18cbf00c-e65f-445a-bccc-11964bdea05d from /media/sda4 */ + // Why they pass this back to us? This is not human-readable for the users at all. + if(strstr(error->message, "only root can ")) { + g_free(error->message); + error->message = g_strdup(_("Only system administrators have the permission to do this.")); + } + } + else if(error->code == G_IO_ERROR_FAILED_HANDLED) { + showError = false; + } + } + if(showError) { + QMessageBox::critical(nullptr, QObject::tr("Error"), QString::fromUtf8(error->message)); + } + } + + Q_EMIT finished(error); + + if(eventLoop) { // if wait() is called to block the main loop + eventLoop->exit(error != nullptr ? 1 : 0); + eventLoop = nullptr; + } + + if(error) { + g_error_free(error); + } + + // free ourself here!! + if(autoDestroy_) { + deleteLater(); + } +} + +void MountOperation::prepareUnmount(GMount* mount) { + /* ensure that CWD is not on the mounted filesystem. */ + char* cwd_str = g_get_current_dir(); + GFile* cwd = g_file_new_for_path(cwd_str); + GFile* root = g_mount_get_root(mount); + g_free(cwd_str); + /* FIXME: This cannot cover 100% cases since symlinks are not checked. + * There may be other cases that cwd is actually under mount root + * but checking prefix is not enough. We already did our best, though. */ + if(g_file_has_prefix(cwd, root)) { + g_chdir("/"); + } + g_object_unref(cwd); + g_object_unref(root); +} + +// block the operation used an internal QEventLoop and returns +// only after the whole operation is finished. +bool MountOperation::wait() { + QEventLoop loop; + eventLoop = &loop; + int exitCode = loop.exec(); + return exitCode == 0 ? true : false; +} + +} // namespace Fm diff --git a/src/mountoperation.h b/src/mountoperation.h new file mode 100644 index 0000000..67219b9 --- /dev/null +++ b/src/mountoperation.h @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_MOUNTOPERATION_H +#define FM_MOUNTOPERATION_H + +#include "libfmqtglobals.h" +#include +#include +#include +#include + +#include "core/filepath.h" + +class QEventLoop; + +namespace Fm { + +// FIXME: the original APIs in gtk+ version of libfm for mounting devices is poor. +// Need to find a better API design which make things fully async and cancellable. + +// FIXME: parent_ does not work. All dialogs shown by the mount operation has no parent window assigned. +// FIXME: Need to reconsider the propery way of API design. Blocking sync calls are handy, but +// indeed causes some problems. :-( + +class LIBFM_QT_API MountOperation: public QObject { + Q_OBJECT + +public: + explicit MountOperation(bool interactive = true, QWidget* parent = 0); + ~MountOperation(); + + FM_QT_DEPRECATED + void mount(const Fm::FilePath& path) { + mountEnclosingVolume(path); + } + + void mountEnclosingVolume(const Fm::FilePath& path); + + void mountMountable(const Fm::FilePath& mountable); + + void mount(GVolume* volume) { + g_volume_mount(volume, G_MOUNT_MOUNT_NONE, op, cancellable_, (GAsyncReadyCallback)onMountVolumeFinished, new QPointer(this)); + } + + void unmount(GMount* mount) { + prepareUnmount(mount); + g_mount_unmount_with_operation(mount, G_MOUNT_UNMOUNT_NONE, op, cancellable_, (GAsyncReadyCallback)onUnmountMountFinished, new QPointer(this)); + } + + void unmount(GVolume* volume) { + GMount* mount = g_volume_get_mount(volume); + if(!mount) { + return; + } + unmount(mount); + g_object_unref(mount); + } + + void eject(GMount* mount) { + prepareUnmount(mount); + g_mount_eject_with_operation(mount, G_MOUNT_UNMOUNT_NONE, op, cancellable_, (GAsyncReadyCallback)onEjectMountFinished, new QPointer(this)); + } + + void eject(GVolume* volume) { + GMount* mnt = g_volume_get_mount(volume); + prepareUnmount(mnt); + g_object_unref(mnt); + g_volume_eject_with_operation(volume, G_MOUNT_UNMOUNT_NONE, op, cancellable_, (GAsyncReadyCallback)onEjectVolumeFinished, new QPointer(this)); + } + + QWidget* parent() const { + return parent_; + } + + void setParent(QWidget* parent) { + parent_ = parent; + } + + GCancellable* cancellable() const { + return cancellable_; + } + + GMountOperation* mountOperation() { + return op; + } + + void cancel() { + g_cancellable_cancel(cancellable_); + } + + bool isRunning() const { + return running; + } + + // block the operation used an internal QEventLoop and returns + // only after the whole operation is finished. + bool wait(); + + bool autoDestroy() { + return autoDestroy_; + } + + void setAutoDestroy(bool destroy = true) { + autoDestroy_ = destroy; + } + +Q_SIGNALS: + void finished(GError* error = nullptr); + +private: + void prepareUnmount(GMount* mount); + + static void onAskPassword(GMountOperation* _op, gchar* message, gchar* default_user, gchar* default_domain, GAskPasswordFlags flags, MountOperation* pThis); + static void onAskQuestion(GMountOperation* _op, gchar* message, GStrv choices, MountOperation* pThis); + // static void onReply(GMountOperation *_op, GMountOperationResult result, MountOperation* pThis); + + static void onAbort(GMountOperation* _op, MountOperation* pThis); + static void onShowProcesses(GMountOperation* _op, gchar* message, GArray* processes, GStrv choices, MountOperation* pThis); + static void onShowUnmountProgress(GMountOperation* _op, gchar* message, gint64 time_left, gint64 bytes_left, MountOperation* pThis); + + // it's possible that this object is freed when the callback is called by gio, so guarding with QPointer is needed here. + static void onMountFileFinished(GFile* file, GAsyncResult* res, QPointer* pThis); + static void onMountMountableFinished(GFile* file, GAsyncResult* res, QPointer* pThis); + static void onMountVolumeFinished(GVolume* volume, GAsyncResult* res, QPointer* pThis); + static void onUnmountMountFinished(GMount* mount, GAsyncResult* res, QPointer* pThis); + static void onEjectMountFinished(GMount* mount, GAsyncResult* res, QPointer* pThis); + static void onEjectVolumeFinished(GVolume* volume, GAsyncResult* res, QPointer* pThis); + + void handleFinish(GError* error); + +private: + GMountOperation* op; + GCancellable* cancellable_; + QWidget* parent_; + bool running; + bool interactive_; + QEventLoop* eventLoop; + bool autoDestroy_; +}; + +} + +#endif // FM_MOUNTOPERATION_H diff --git a/src/mountoperationpassworddialog.cpp b/src/mountoperationpassworddialog.cpp new file mode 100644 index 0000000..4e7d850 --- /dev/null +++ b/src/mountoperationpassworddialog.cpp @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "mountoperationpassworddialog_p.h" +#include "ui_mount-operation-password.h" +#include "mountoperation.h" + +namespace Fm { + +MountOperationPasswordDialog::MountOperationPasswordDialog(MountOperation* op, GAskPasswordFlags flags): + QDialog(), + mountOperation(op), + needPassword(flags & G_ASK_PASSWORD_NEED_PASSWORD ? true : false), + needUserName(flags & G_ASK_PASSWORD_NEED_USERNAME ? true : false), + needDomain(flags & G_ASK_PASSWORD_NEED_DOMAIN ? true : false), + canSavePassword(flags & G_ASK_PASSWORD_SAVING_SUPPORTED ? true : false), + canAnonymous(flags & G_ASK_PASSWORD_ANONYMOUS_SUPPORTED ? true : false) { + + ui = new Ui::MountOperationPasswordDialog(); + ui->setupUi(this); + + // change the text of Ok button to Connect + ui->buttonBox->buttons().constFirst()->setText(tr("&Connect")); + connect(ui->Anonymous, &QAbstractButton::toggled, this, &MountOperationPasswordDialog::onAnonymousToggled); + + if(canAnonymous) { + // select ananymous by default if applicable. + ui->Anonymous->setChecked(true); + } + else { + ui->Anonymous->setEnabled(false); + ui->asUser->setChecked(true); + } + if(!needUserName) { + ui->username->setEnabled(false); + } + if(needPassword) { + if(!needUserName) { + ui->password->setFocus(); + } + } + else { + ui->password->setEnabled(false); + } + if(!needDomain) { + ui->domain->hide(); + ui->domainLabel->hide(); + } + if(canSavePassword) { + ui->sessionPassword->setChecked(true); + } + else { + ui->storePassword->setEnabled(false); + ui->sessionPassword->setEnabled(false); + ui->forgetPassword->setChecked(true); + } +} + +MountOperationPasswordDialog::~MountOperationPasswordDialog() { + delete ui; +} + +void MountOperationPasswordDialog::onAnonymousToggled(bool checked) { + // disable username/password entries if anonymous mode is used + bool useUserPassword = !checked; + if(needUserName) { + ui->username->setEnabled(useUserPassword); + } + if(needPassword) { + ui->password->setEnabled(useUserPassword); + } + if(needDomain) { + ui->domain->setEnabled(useUserPassword); + } + + if(canSavePassword) { + ui->forgetPassword->setEnabled(useUserPassword); + ui->sessionPassword->setEnabled(useUserPassword); + ui->storePassword->setEnabled(useUserPassword); + } +} + +void MountOperationPasswordDialog::setMessage(QString message) { + ui->message->setText(message); +} + +void MountOperationPasswordDialog::setDefaultDomain(QString domain) { + ui->domain->setText(domain); +} + +void MountOperationPasswordDialog::setDefaultUser(QString user) { + ui->username->setText(user); +} + +void MountOperationPasswordDialog::done(int r) { + GMountOperation* gmop = mountOperation->mountOperation(); + + if(r == QDialog::Accepted) { + + if(needUserName) { + g_mount_operation_set_username(gmop, ui->username->text().toUtf8()); + } + if(needDomain) { + g_mount_operation_set_domain(gmop, ui->domain->text().toUtf8()); + } + if(needPassword) { + g_mount_operation_set_password(gmop, ui->password->text().toUtf8()); + } + if(canAnonymous) { + g_mount_operation_set_anonymous(gmop, ui->Anonymous->isChecked()); + } + + g_mount_operation_reply(gmop, G_MOUNT_OPERATION_HANDLED); + } + else { + g_mount_operation_reply(gmop, G_MOUNT_OPERATION_ABORTED); + } + QDialog::done(r); +} + +} // namespace Fm diff --git a/src/mountoperationpassworddialog_p.h b/src/mountoperationpassworddialog_p.h new file mode 100644 index 0000000..1fd6cd1 --- /dev/null +++ b/src/mountoperationpassworddialog_p.h @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_MOUNTOPERATIONPASSWORDDIALOG_H +#define FM_MOUNTOPERATIONPASSWORDDIALOG_H + +#include "libfmqtglobals.h" +#include +#include + +namespace Ui { +class MountOperationPasswordDialog; +} + +namespace Fm { + +class MountOperation; + +class MountOperationPasswordDialog : public QDialog { + Q_OBJECT + +public: + explicit MountOperationPasswordDialog(MountOperation* op, GAskPasswordFlags flags); + virtual ~MountOperationPasswordDialog(); + + void setMessage(QString message); + void setDefaultUser(QString user); + void setDefaultDomain(QString domain); + + virtual void done(int r); + +private Q_SLOTS: + void onAnonymousToggled(bool checked); + +private: + Ui::MountOperationPasswordDialog* ui; + MountOperation* mountOperation; + bool needPassword; + bool needUserName; + bool needDomain; + bool canSavePassword; + bool canAnonymous; +}; + +} + +#endif // FM_MOUNTOPERATIONPASSWORDDIALOG_H diff --git a/src/mountoperationquestiondialog.cpp b/src/mountoperationquestiondialog.cpp new file mode 100644 index 0000000..3679740 --- /dev/null +++ b/src/mountoperationquestiondialog.cpp @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "mountoperationquestiondialog_p.h" +#include "mountoperation.h" +#include + +namespace Fm { + +MountOperationQuestionDialog::MountOperationQuestionDialog(MountOperation* op, gchar* message, GStrv choices): + QMessageBox(), + mountOperation(op) { + + setIcon(QMessageBox::Question); + setText(QString::fromUtf8(message)); + + choiceCount = g_strv_length(choices); + choiceButtons = new QAbstractButton*[choiceCount]; + for(int i = 0; i < choiceCount; ++i) { + // It's not allowed to add custom buttons without standard roles + // to QMessageBox. So we set role of all buttons to AcceptRole. + // When any of the set buttons is clicked, exec() always returns "accept". + QPushButton* button = new QPushButton(QString::fromUtf8(choices[i])); + addButton(button, QMessageBox::AcceptRole); + choiceButtons[i] = button; + } +} + +MountOperationQuestionDialog::~MountOperationQuestionDialog() { + delete []choiceButtons; +} + +void MountOperationQuestionDialog::done(int r) { + GMountOperation* op = mountOperation->mountOperation(); + + g_mount_operation_set_choice(op, r); + g_mount_operation_reply(op, G_MOUNT_OPERATION_HANDLED); + + QDialog::done(r); +} + +void MountOperationQuestionDialog::closeEvent(QCloseEvent *event) +{ + GMountOperation* op = mountOperation->mountOperation(); + + g_mount_operation_reply(op, G_MOUNT_OPERATION_ABORTED); + + event->accept(); +} + +} // namespace Fm diff --git a/src/mountoperationquestiondialog_p.h b/src/mountoperationquestiondialog_p.h new file mode 100644 index 0000000..8c6a810 --- /dev/null +++ b/src/mountoperationquestiondialog_p.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_MOUNTOPERATIONQUESTIONDIALOG_H +#define FM_MOUNTOPERATIONQUESTIONDIALOG_H + +#include "libfmqtglobals.h" +#include +#include +#include + +namespace Fm { + +class MountOperation; + +class MountOperationQuestionDialog : public QMessageBox { + Q_OBJECT +public: + MountOperationQuestionDialog(MountOperation* op, gchar* message, GStrv choices); + virtual ~MountOperationQuestionDialog(); + + virtual void done(int r); + virtual void closeEvent(QCloseEvent *event); + +private: + MountOperation* mountOperation; + QAbstractButton** choiceButtons; + int choiceCount; +}; + +} + +#endif // FM_MOUNTOPERATIONQUESTIONDIALOG_H diff --git a/src/pathbar.cpp b/src/pathbar.cpp new file mode 100644 index 0000000..c9230f7 --- /dev/null +++ b/src/pathbar.cpp @@ -0,0 +1,352 @@ +/* + * Copyright (C) 2016 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "pathbar.h" +#include "pathbar_p.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "pathedit.h" + + +namespace Fm { + +PathBar::PathBar(QWidget* parent): + QWidget(parent), + tempPathEdit_(nullptr), + toggledBtn_(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(); + QTimer::singleShot(0, this, SLOT(ensureToggledVisible())); +} + +void PathBar::wheelEvent(QWheelEvent* event) { + QWidget::wheelEvent(event); + QAbstractSlider::SliderAction action = QAbstractSlider::SliderNoAction; + int vDelta = event->angleDelta().y(); + if(vDelta > 0) { + if(scrollToStart_->isEnabled()) { + action = QAbstractSlider::SliderSingleStepSub; + } + } + else if(vDelta < 0) { + if(scrollToEnd_->isEnabled()) { + action = QAbstractSlider::SliderSingleStepAdd; + } + } + scrollArea_->horizontalScrollBar()->triggerAction(action); +} + +void PathBar::mousePressEvent(QMouseEvent* event) { + QWidget::mousePressEvent(event); + if(event->button() == Qt::LeftButton) { + openEditor(); + } + else if(event->button() == Qt::MiddleButton) { + PathButton* btn = qobject_cast(childAt(event->x(), event->y())); + if(btn != nullptr) { + scrollArea_->ensureWidgetVisible(btn, + 1); // a harmless compensation for a miscalculation in Qt + Q_EMIT middleClickChdir(pathForButton(btn)); + } + } +} + +void PathBar::contextMenuEvent(QContextMenuEvent* event) { + QMenu* menu = new QMenu(this); + connect(menu, &QMenu::aboutToHide, menu, &QMenu::deleteLater); + + QAction* action = menu->addAction(tr("&Edit Path")); + connect(action, &QAction::triggered, this, &PathBar::openEditor); + + action = menu->addAction(tr("&Copy Path")); + connect(action, &QAction::triggered, this, &PathBar::copyPath); + + menu->popup(mapToGlobal(event->pos())); +} + +void PathBar::updateScrollButtonVisibility() { + // Wait for the horizontal scrollbar to be completely shaped. + // Without this, the enabled state of arrow buttons might be + // wrong when the pathbar is created for the first time. + QTimer::singleShot(0, this, SLOT(setScrollButtonVisibility())); +} + +void PathBar::setScrollButtonVisibility() { + bool showScrollers; + if(tempPathEdit_ != nullptr) { + showScrollers = false; + } + else { + showScrollers = (buttonsLayout_->sizeHint().width() > width()); + } + scrollToStart_->setVisible(showScrollers); + scrollToEnd_->setVisible(showScrollers); + if(showScrollers) { + QScrollBar* sb = scrollArea_->horizontalScrollBar(); + int value = sb->value(); + scrollToStart_->setEnabled(value != sb->minimum()); + scrollToEnd_->setEnabled(value != sb->maximum()); + } +} + +Fm::FilePath PathBar::pathForButton(PathButton* btn) { + std::string fullPath; + int buttonCount = buttonsLayout_->count() - 1; // the last item is a spacer + for(int i = 0; i < buttonCount; ++i) { + if(!fullPath.empty() && fullPath.back() != '/') { + fullPath += '/'; + } + PathButton* elem = static_cast(buttonsLayout_->itemAt(i)->widget()); + fullPath += elem->name(); + if(elem == btn) + break; + } + return Fm::FilePath::fromPathStr(fullPath.c_str()); +} + +void PathBar::onButtonToggled(bool checked) { + if(checked) { + PathButton* btn = static_cast(sender()); + toggledBtn_ = btn; + currentPath_ = pathForButton(btn); + Q_EMIT chdir(currentPath_); + + // since scrolling to the toggled buton will happen correctly only when the + // layout is updated and because the update is disabled on creating buttons + // in setPath(), the update status can be used as a sign to know when to wait + if(updatesEnabled()) { + scrollArea_->ensureWidgetVisible(btn, 1); + } + else { + QTimer::singleShot(0, this, SLOT(ensureToggledVisible())); + } + } +} + +void PathBar::ensureToggledVisible() { + if(toggledBtn_ != nullptr && tempPathEdit_ == nullptr) { + scrollArea_->ensureWidgetVisible(toggledBtn_, 1); + } +} + +void PathBar::onScrollButtonClicked() { + QToolButton* btn = static_cast(sender()); + QAbstractSlider::SliderAction action = QAbstractSlider::SliderNoAction; + if(btn == scrollToEnd_) { + action = QAbstractSlider::SliderSingleStepAdd; + } + else if(btn == scrollToStart_) { + action = QAbstractSlider::SliderSingleStepSub; + } + scrollArea_->horizontalScrollBar()->triggerAction(action); +} + +void PathBar::setPath(Fm::FilePath path) { + if(currentPath_ == path) { // same path, do nothing + return; + } + + auto oldPath = std::move(currentPath_); + currentPath_ = std::move(path); + // check if we already have a button for this path + int buttonCount = buttonsLayout_->count() - 1; // the last item is a spacer + if(oldPath && currentPath_.isPrefixOf(oldPath)) { + for(int i = buttonCount - 1; i >= 0; --i) { + auto btn = static_cast(buttonsLayout_->itemAt(i)->widget()); + if(pathForButton(btn) == currentPath_) { + btn->setChecked(true); // toggle the button + /* we don't need to emit chdir signal here since later + * toggled signal will be triggered on the button, which + * in turns emit chdir. */ + return; + } + } + } + + /* FIXME: if the new path is the subdir of our full path, actually + * we can append several new buttons rather than re-create + * all of the buttons. This can reduce flickers. */ + + setUpdatesEnabled(false); + toggledBtn_ = nullptr; + // we do not have the path in the buttons list + // destroy existing path element buttons and the spacer + QLayoutItem* item; + while((item = buttonsLayout_->takeAt(0)) != nullptr) { + delete item->widget(); + delete item; + } + + // create new buttons for the new path + auto btnPath = currentPath_; + while(btnPath) { + Fm::CStrPtr name; + Fm::CStrPtr displayName; + auto parent = btnPath.parent(); + // FIXME: some buggy uri types, such as menu://, fail to return NULL when there is no parent path. + // Instead, the path itself is returned. So we check if the parent path is the same as current path. + auto isRoot = !parent.isValid() || parent == btnPath; + if(isRoot) { + displayName = btnPath.displayName(); + name = btnPath.toString(); + } + else { + name = btnPath.baseName(); + } + auto btn = new PathButton(name.get(), displayName ? displayName.get() : name.get(), isRoot, buttonsWidget_); + btn->show(); + connect(btn, &QAbstractButton::toggled, this, &PathBar::onButtonToggled); + buttonsLayout_->insertWidget(0, btn); + if(isRoot) { // this is the root element of the path + break; + } + btnPath = parent; + } + buttonsLayout_->addStretch(1); // add a spacer at the tail of the buttons + + // we don't want to scroll vertically. make the scroll area fit the height of the buttons + // FIXME: this is a little bit hackish :-( + scrollArea_->setFixedHeight(buttonsLayout_->sizeHint().height()); + updateScrollButtonVisibility(); + + // to guarantee that the button will be scrolled to correctly, + // it should be toggled only after the layout update starts above + buttonCount = buttonsLayout_->count() - 1; + if(buttonCount > 0) { + PathButton* lastBtn = static_cast(buttonsLayout_->itemAt(buttonCount - 1)->widget()); + // we don't have to emit the chdir signal since the "onButtonToggled()" slot will be triggered by this. + lastBtn->setChecked(true); + } + + setUpdatesEnabled(true); +} + +void PathBar::openEditor() { + if(tempPathEdit_ == nullptr) { + tempPathEdit_ = new PathEdit(this); + delete layout()->replaceWidget(scrollArea_, tempPathEdit_, Qt::FindDirectChildrenOnly); + scrollArea_->hide(); + scrollToStart_->setVisible(false); + scrollToEnd_->setVisible(false); + tempPathEdit_->setText(currentPath_.toString().get()); + + connect(tempPathEdit_, &PathEdit::returnPressed, this, &PathBar::onReturnPressed); + connect(tempPathEdit_, &PathEdit::editingFinished, this, &PathBar::closeEditor); + } + tempPathEdit_->selectAll(); + QApplication::clipboard()->setText(tempPathEdit_->text(), QClipboard::Selection); + QTimer::singleShot(0, tempPathEdit_, SLOT(setFocus())); +} + +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. + // However, since hiding the path-edit makes it lose focus and emit editingFinished(), + // we should first disconnect from it to avoid recursive calling of the current function. + tempPathEdit_->disconnect(); + tempPathEdit_->setVisible(false); + delete layout()->replaceWidget(tempPathEdit_, scrollArea_, Qt::FindDirectChildrenOnly); + scrollArea_->show(); + if(buttonsLayout_->sizeHint().width() > width()) { + scrollToStart_->setVisible(true); + scrollToEnd_->setVisible(true); + } + + tempPathEdit_->deleteLater(); + tempPathEdit_ = nullptr; + updateScrollButtonVisibility(); + + Q_EMIT editingFinished(); +} + +void PathBar::copyPath() { + QApplication::clipboard()->setText(currentPath_.toString().get()); +} + +void PathBar::onReturnPressed() { + QByteArray pathStr = tempPathEdit_->text().toLocal8Bit(); + setPath(Fm::FilePath::fromPathStr(pathStr.constData())); +} + +void PathBar::setArrowEnabledState(int value) { + if(buttonsLayout_->sizeHint().width() > width()) { + QScrollBar* sb = scrollArea_->horizontalScrollBar(); + scrollToStart_->setEnabled(value != sb->minimum()); + scrollToEnd_->setEnabled(value != sb->maximum()); + } +} + +} // namespace Fm diff --git a/src/pathbar.h b/src/pathbar.h new file mode 100644 index 0000000..c0dd464 --- /dev/null +++ b/src/pathbar.h @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2016 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_PATHBAR_H +#define FM_PATHBAR_H + +#include "libfmqtglobals.h" +#include +#include "core/filepath.h" + +class QToolButton; +class QScrollArea; +class QPushButton; +class QHBoxLayout; + +namespace Fm { + +class PathEdit; +class PathButton; + +class LIBFM_QT_API PathBar: public QWidget { + Q_OBJECT +public: + explicit PathBar(QWidget* parent = 0); + + const Fm::FilePath& path() { + return currentPath_; + } + + void setPath(Fm::FilePath path); + +Q_SIGNALS: + void chdir(const Fm::FilePath& path); + void middleClickChdir(const Fm::FilePath& path); + void editingFinished(); + +public Q_SLOTS: + void openEditor(); + void closeEditor(); + void copyPath(); + +private Q_SLOTS: + void onButtonToggled(bool checked); + void onScrollButtonClicked(); + void onReturnPressed(); + void setArrowEnabledState(int value); + void setScrollButtonVisibility(); + void ensureToggledVisible(); + +protected: + void resizeEvent(QResizeEvent* event); + void wheelEvent(QWheelEvent* event); + void mousePressEvent(QMouseEvent* event); + void contextMenuEvent(QContextMenuEvent* event); + +private: + void updateScrollButtonVisibility(); + Fm::FilePath pathForButton(PathButton* btn); + +private: + QToolButton* scrollToStart_; + QToolButton* scrollToEnd_; + QScrollArea* scrollArea_; + QWidget* buttonsWidget_; + QHBoxLayout* buttonsLayout_; + PathEdit* tempPathEdit_; + + Fm::FilePath currentPath_; // currently active path + PathButton* toggledBtn_; +}; + +} // namespace Fm + +#endif // FM_PATHBAR_H diff --git a/src/pathbar_p.h b/src/pathbar_p.h new file mode 100644 index 0000000..6a64b98 --- /dev/null +++ b/src/pathbar_p.h @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2016 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_PATHBAR_P_H +#define FM_PATHBAR_P_H + +#include +#include +#include +#include +#include +#include + +namespace Fm { + +class PathButton: public QToolButton { + Q_OBJECT +public: + PathButton(std::string name, QString displayName, bool isRoot = false, QWidget* parent = nullptr): + QToolButton(parent), + name_{name} { + + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::MinimumExpanding); + setCheckable(true); + setAutoExclusive(true); + setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + /* respect the toolbar icon size (can be set with some styles) */ + int icnSize = style()->pixelMetric(QStyle::PM_ToolBarIconSize); + setIconSize(QSize(icnSize, icnSize)); + + setText(displayName); + + if(isRoot) { /* this element is root */ + QIcon icon = QIcon::fromTheme("drive-harddisk"); + setIcon(icon); + } + } + + void changeEvent(QEvent* event) override { + QToolButton::changeEvent(event); + if(event->type() == QEvent::StyleChange) { + int icnSize = style()->pixelMetric(QStyle::PM_ToolBarIconSize); + setIconSize(QSize(icnSize, icnSize)); + } + } + + std::string name() const { + return name_; + } + + void setName(const std::string& name) { + name_ = name; + } + +private: + QString displayName_; + std::string name_; +}; + +} // namespace Fm + +#endif // FM_PATHBAR_P_H diff --git a/src/pathedit.cpp b/src/pathedit.cpp new file mode 100644 index 0000000..b8495ee --- /dev/null +++ b/src/pathedit.cpp @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "pathedit.h" +#include "pathedit_p.h" +#include +#include +#include +#include +#include +#include +#include + +namespace Fm { + +void PathEditJob::runJob() { + GError* err = nullptr; + GFileEnumerator* enu = g_file_enumerate_children(dirName, + // G_FILE_ATTRIBUTE_STANDARD_NAME"," + G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME"," + G_FILE_ATTRIBUTE_STANDARD_TYPE, + G_FILE_QUERY_INFO_NONE, cancellable, + &err); + if(enu) { + while(!g_cancellable_is_cancelled(cancellable)) { + GFileInfo* inf = g_file_enumerator_next_file(enu, cancellable, &err); + if(inf) { + GFileType type = g_file_info_get_file_type(inf); + if(type == G_FILE_TYPE_DIRECTORY) { + const char* name = g_file_info_get_display_name(inf); + // FIXME: encoding conversion here? + subDirs.append(QString::fromUtf8(name)); + } + g_object_unref(inf); + } + else { + if(err) { + g_error_free(err); + err = nullptr; + } + else { /* EOF */ + break; + } + } + } + g_file_enumerator_close(enu, cancellable, nullptr); + g_object_unref(enu); + } + // finished! let's update the UI in the main thread + Q_EMIT finished(); + QThread::currentThread()->quit(); +} + + +PathEdit::PathEdit(QWidget* parent): + QLineEdit(parent), + completer_(new QCompleter()), + model_(new QStringListModel()), + cancellable_(nullptr) { + completer_->setCaseSensitivity(Qt::CaseInsensitive); + setCompleter(completer_); + completer_->setModel(model_); + connect(this, &PathEdit::textChanged, this, &PathEdit::onTextChanged); + connect(this, &PathEdit::textEdited, this, &PathEdit::onTextEdited); +} + +PathEdit::~PathEdit() { + delete completer_; + if(model_) { + delete model_; + } + if(cancellable_) { + g_cancellable_cancel(cancellable_); + g_object_unref(cancellable_); + } +} + +void PathEdit::focusInEvent(QFocusEvent* e) { + QLineEdit::focusInEvent(e); + // build the completion list only when we have the keyboard focus + reloadCompleter(true); +} + +void PathEdit::focusOutEvent(QFocusEvent* e) { + QLineEdit::focusOutEvent(e); + // free the completion list since we don't need it anymore + freeCompleter(); +} + +bool PathEdit::event(QEvent* e) { + // Stop Qt from moving the keyboard focus to the next widget when "Tab" is pressed. + // Instead, we need to do auto-completion in this case. + if(e->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast(e); + if(keyEvent->key() == Qt::Key_Tab && keyEvent->modifiers() == Qt::NoModifier) { // Tab key is pressed + e->accept(); + // do auto-completion when the user press the Tab key. + // This fixes #201: https://github.com/lxqt/pcmanfm-qt/issues/201 + autoComplete(); + return true; + } + } + return QLineEdit::event(e); +} + +void PathEdit::onTextEdited(const QString& text) { + // just replace start tilde with home path if text is changed by user + if(text == QLatin1String("~") || text.startsWith(QLatin1String("~/"))) { + QString txt(text); + txt.replace(0, 1, QDir::homePath()); + setText(txt); // emits textChanged() + return; + } +} + +void PathEdit::onTextChanged(const QString& text) { + if(text == QLatin1String("~") || text.startsWith(QLatin1String("~/"))) { + // do nothing with a start tilde because neither Fm::FilePath nor autocompletion + // understands it; instead, wait until textChanged() is emitted again without it + // WARNING: replacing tilde may not be safe here + return; + } + int pos = text.lastIndexOf('/'); + if(pos >= 0) { + ++pos; + } + else { + pos = text.length(); + } + QString newPrefix = text.left(pos); + if(currentPrefix_ != newPrefix) { + currentPrefix_ = newPrefix; + // only build the completion list if we have the keyboard focus + // if we don't have the focus now, then we'll rebuild the completion list + // when focusInEvent happens. this avoid unnecessary dir loading. + if(hasFocus()) { + reloadCompleter(false); + } + } +} + +void PathEdit::autoComplete() { + // find longest common prefix of the strings currently shown in the candidate list + QAbstractItemModel* model = completer_->completionModel(); + if(model->rowCount() > 0) { + int minLen = text().length(); + QString commonPrefix = model->data(model->index(0, 0)).toString(); + for(int row = 1; row < model->rowCount() && commonPrefix.length() > minLen; ++row) { + QModelIndex index = model->index(row, 0); + QString rowText = model->data(index).toString(); + int prefixLen = 0; + while(prefixLen < rowText.length() && prefixLen < commonPrefix.length() && rowText[prefixLen] == commonPrefix[prefixLen]) { + ++prefixLen; + } + commonPrefix.truncate(prefixLen); + } + if(commonPrefix.length() > minLen) { + setText(commonPrefix); + } + } +} + +void PathEdit::reloadCompleter(bool triggeredByFocusInEvent) { + // parent dir has been changed, reload dir list + // if(currentPrefix_[0] == "~") { // special case for home dir + // cancel running dir-listing jobs, if there's any + if(cancellable_) { + g_cancellable_cancel(cancellable_); + g_object_unref(cancellable_); + } + + // create a new job to do dir listing + PathEditJob* job = new PathEditJob(); + job->edit = this; + job->triggeredByFocusInEvent = triggeredByFocusInEvent; + // need to use fm_file_new_for_commandline_arg() rather than g_file_new_for_commandline_arg(). + // otherwise, our own vfs, such as menu://, won't be loaded. + job->dirName = g_file_new_for_commandline_arg(currentPrefix_.toLocal8Bit().constData()); + // qDebug("load: %s", g_file_get_uri(data->dirName)); + cancellable_ = g_cancellable_new(); + job->cancellable = (GCancellable*)g_object_ref(cancellable_); + + // launch a new worker thread to handle the job + QThread* thread = new QThread(); + job->moveToThread(thread); + connect(job, &PathEditJob::finished, this, &PathEdit::onJobFinished, Qt::BlockingQueuedConnection); + // connect(job, &PathEditJob::finished, thread, &QThread::quit); + connect(thread, &QThread::started, job, &PathEditJob::runJob); + connect(thread, &QThread::finished, thread, &QObject::deleteLater); + connect(thread, &QThread::finished, job, &QObject::deleteLater); + thread->start(QThread::LowPriority); +} + +void PathEdit::freeCompleter() { + if(cancellable_) { + g_cancellable_cancel(cancellable_); + g_object_unref(cancellable_); + cancellable_ = nullptr; + } + model_->setStringList(QStringList()); +} + +// This slot is called from main thread so it's safe to access the GUI +void PathEdit::onJobFinished() { + PathEditJob* data = static_cast(sender()); + if(!g_cancellable_is_cancelled(data->cancellable)) { + // update the completer only if the job is not cancelled + QStringList::iterator it; + for(it = data->subDirs.begin(); it != data->subDirs.end(); ++it) { + // qDebug("%s", it->toUtf8().constData()); + *it = (currentPrefix_ % *it); + } + model_->setStringList(data->subDirs); + // trigger completion manually + if(hasFocus() && !data->triggeredByFocusInEvent) { + completer_->complete(); + } + } + else { + model_->setStringList(QStringList()); + } + if(cancellable_) { + g_object_unref(cancellable_); + cancellable_ = nullptr; + } +} + +} // namespace Fm diff --git a/src/pathedit.h b/src/pathedit.h new file mode 100644 index 0000000..6385ddf --- /dev/null +++ b/src/pathedit.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_PATHEDIT_H +#define FM_PATHEDIT_H + +#include "libfmqtglobals.h" +#include +#include + +class QCompleter; +class QStringListModel; + +namespace Fm { + +class PathEditJob; + +class LIBFM_QT_API PathEdit : public QLineEdit { + Q_OBJECT +public: + explicit PathEdit(QWidget* parent = 0); + virtual ~PathEdit(); + +protected: + virtual void focusInEvent(QFocusEvent* e); + virtual void focusOutEvent(QFocusEvent* e); + virtual bool event(QEvent* e); + +private Q_SLOTS: + void onTextChanged(const QString& text); + void onTextEdited(const QString& text); + +private: + void autoComplete(); + void reloadCompleter(bool triggeredByFocusInEvent = false); + void freeCompleter(); + void onJobFinished(); + +private: + QCompleter* completer_; + QStringListModel* model_; + QString currentPrefix_; + GCancellable* cancellable_; +}; + +} + +#endif // FM_PATHEDIT_H diff --git a/src/pathedit_p.h b/src/pathedit_p.h new file mode 100644 index 0000000..190f0bc --- /dev/null +++ b/src/pathedit_p.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_PATHEDIT_P_H +#define FM_PATHEDIT_P_H + +#include + +namespace Fm { + +class PathEdit; + +class PathEditJob : public QObject { + Q_OBJECT +public: + GCancellable* cancellable; + GFile* dirName; + QStringList subDirs; + PathEdit* edit; + bool triggeredByFocusInEvent; + + ~PathEditJob() { + g_object_unref(dirName); + g_object_unref(cancellable); + } + +Q_SIGNALS: + void finished(); + +public Q_SLOTS: + void runJob(); + +}; + +} + +#endif // FM_PATHEDIT_P_H diff --git a/src/placesmodel.cpp b/src/placesmodel.cpp new file mode 100644 index 0000000..7494c99 --- /dev/null +++ b/src/placesmodel.cpp @@ -0,0 +1,633 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "placesmodel.h" +#include +#include +#include +#include +#include +#include +#include "utilities.h" +#include "placesmodelitem.h" + +namespace Fm { + +std::weak_ptr PlacesModel::globalInstance_; + +PlacesModel::PlacesModel(QObject* parent): + QStandardItemModel(parent), + showApplications_(true), + showDesktop_(true), + trashMonitor_(nullptr), + trashUpdateTimer_(nullptr), + // FIXME: this seems to be broken when porting to new API. + ejectIcon_(QIcon::fromTheme("media-eject")) { + setColumnCount(2); + + placesRoot = new QStandardItem(tr("Places")); + placesRoot->setSelectable(false); + placesRoot->setColumnCount(2); + appendRow(placesRoot); + + homeItem = new PlacesModelItem("user-home", g_get_user_name(), Fm::FilePath::homeDir()); + placesRoot->appendRow(homeItem); + + desktopItem = new PlacesModelItem("user-desktop", tr("Desktop"), + Fm::FilePath::fromLocalPath(QStandardPaths::writableLocation(QStandardPaths::DesktopLocation).toLocal8Bit().constData())); + placesRoot->appendRow(desktopItem); + + createTrashItem(); + + computerItem = new PlacesModelItem("computer", tr("Computer"), Fm::FilePath::fromUri("computer:///")); + placesRoot->appendRow(computerItem); + + { // Applications + const char* applicaion_icon_names[] = {"system-software-install", "applications-accessories", "application-x-executable"}; + // NOTE: g_themed_icon_new_from_names() accepts char**, but actually const char** is OK. + Fm::GIconPtr gicon{g_themed_icon_new_from_names((char**)applicaion_icon_names, G_N_ELEMENTS(applicaion_icon_names)), false}; + auto fmicon = Fm::IconInfo::fromGIcon(std::move(gicon)); + applicationsItem = new PlacesModelItem(fmicon, tr("Applications"), Fm::FilePath::fromUri("menu:///applications/")); + placesRoot->appendRow(applicationsItem); + } + + { // Network + const char* network_icon_names[] = {"network", "folder-network", "folder"}; + // NOTE: g_themed_icon_new_from_names() accepts char**, but actually const char** is OK. + Fm::GIconPtr gicon{g_themed_icon_new_from_names((char**)network_icon_names, G_N_ELEMENTS(network_icon_names)), false}; + auto fmicon = Fm::IconInfo::fromGIcon(std::move(gicon)); + networkItem = new PlacesModelItem(fmicon, tr("Network"), Fm::FilePath::fromUri("network:///")); + placesRoot->appendRow(networkItem); + } + + devicesRoot = new QStandardItem(tr("Devices")); + devicesRoot->setSelectable(false); + devicesRoot->setColumnCount(2); + appendRow(devicesRoot); + + // volumes + volumeMonitor = g_volume_monitor_get(); + if(volumeMonitor) { + g_signal_connect(volumeMonitor, "volume-added", G_CALLBACK(onVolumeAdded), this); + g_signal_connect(volumeMonitor, "volume-removed", G_CALLBACK(onVolumeRemoved), this); + g_signal_connect(volumeMonitor, "volume-changed", G_CALLBACK(onVolumeChanged), this); + g_signal_connect(volumeMonitor, "mount-added", G_CALLBACK(onMountAdded), this); + g_signal_connect(volumeMonitor, "mount-changed", G_CALLBACK(onMountChanged), this); + g_signal_connect(volumeMonitor, "mount-removed", G_CALLBACK(onMountRemoved), this); + + // add volumes to side-pane + GList* vols = g_volume_monitor_get_volumes(volumeMonitor); + GList* l; + for(l = vols; l; l = l->next) { + GVolume* volume = G_VOLUME(l->data); + onVolumeAdded(volumeMonitor, volume, this); + g_object_unref(volume); + } + g_list_free(vols); + + /* add mounts to side-pane */ + vols = g_volume_monitor_get_mounts(volumeMonitor); + for(l = vols; l; l = l->next) { + GMount* mount = G_MOUNT(l->data); + GVolume* volume = g_mount_get_volume(mount); + if(volume) { + g_object_unref(volume); + } + else { /* network mounts or others */ + gboolean shadowed = FALSE; +#if GLIB_CHECK_VERSION(2, 20, 0) + shadowed = g_mount_is_shadowed(mount); +#endif + // according to gio API doc, a shadowed mount should not be visible to the user + if(shadowed) { + shadowedMounts_.push_back(mount); + continue; + } + else { + PlacesModelItem* item = new PlacesModelMountItem(mount); + devicesRoot->appendRow(item); + } + } + g_object_unref(mount); + } + g_list_free(vols); + } + + // bookmarks + bookmarksRoot = new QStandardItem(tr("Bookmarks")); + bookmarksRoot->setSelectable(false); + bookmarksRoot->setColumnCount(2); + appendRow(bookmarksRoot); + + bookmarks = Fm::Bookmarks::globalInstance(); + loadBookmarks(); + connect(bookmarks.get(), &Fm::Bookmarks::changed, this, &PlacesModel::onBookmarksChanged); +} + +void PlacesModel::loadBookmarks() { + for(auto& bm_item: bookmarks->items()) { + PlacesModelBookmarkItem* item = new PlacesModelBookmarkItem(bm_item); + bookmarksRoot->appendRow(item); + } +} + +PlacesModel::~PlacesModel() { + if(volumeMonitor) { + g_signal_handlers_disconnect_by_func(volumeMonitor, (gpointer)G_CALLBACK(onVolumeAdded), this); + g_signal_handlers_disconnect_by_func(volumeMonitor, (gpointer)G_CALLBACK(onVolumeRemoved), this); + g_signal_handlers_disconnect_by_func(volumeMonitor, (gpointer)G_CALLBACK(onVolumeChanged), this); + g_signal_handlers_disconnect_by_func(volumeMonitor, (gpointer)G_CALLBACK(onMountAdded), this); + g_signal_handlers_disconnect_by_func(volumeMonitor, (gpointer)G_CALLBACK(onMountChanged), this); + g_signal_handlers_disconnect_by_func(volumeMonitor, (gpointer)G_CALLBACK(onMountRemoved), this); + g_object_unref(volumeMonitor); + } + if(trashMonitor_) { + g_signal_handlers_disconnect_by_func(trashMonitor_, (gpointer)G_CALLBACK(onTrashChanged), this); + g_object_unref(trashMonitor_); + } + + for(GMount* const mount : qAsConst(shadowedMounts_)) { + g_object_unref(mount); + } +} + +// static +void PlacesModel::onTrashChanged(GFileMonitor* /*monitor*/, GFile* /*gf*/, GFile* /*other*/, GFileMonitorEvent /*evt*/, PlacesModel* pThis) { + if(pThis->trashUpdateTimer_ != nullptr && !pThis->trashUpdateTimer_->isActive()) { + pThis->trashUpdateTimer_->start(250); // don't update trash very fast + } +} + +void PlacesModel::updateTrash() { + + struct UpdateTrashData { + QPointer model; + GFile* gf; + UpdateTrashData(PlacesModel* _model) : model(_model) { + gf = g_file_new_for_uri("trash:///"); + } + ~UpdateTrashData() { + g_object_unref(gf); + } + }; + + if(trashItem_) { + UpdateTrashData* data = new UpdateTrashData(this); + g_file_query_info_async(data->gf, G_FILE_ATTRIBUTE_TRASH_ITEM_COUNT, G_FILE_QUERY_INFO_NONE, G_PRIORITY_LOW, nullptr, + [](GObject * /*source_object*/, GAsyncResult * res, gpointer user_data) { + // the callback lambda function is called when the asyn query operation is finished + UpdateTrashData* data = reinterpret_cast(user_data); + PlacesModel* _this = data->model.data(); + if(_this != nullptr) { // ensure that our model object is not deleted yet + Fm::GFileInfoPtr inf{g_file_query_info_finish(data->gf, res, nullptr), false}; + if(inf) { + if(_this->trashItem_ != nullptr) { // it's possible that when we finish, the trash item is removed + guint32 n = g_file_info_get_attribute_uint32(inf.get(), G_FILE_ATTRIBUTE_TRASH_ITEM_COUNT); + const char* icon_name = n > 0 ? "user-trash-full" : "user-trash"; + auto icon = Fm::IconInfo::fromName(icon_name); + _this->trashItem_->setIcon(std::move(icon)); + } + } + } + delete data; // free the data used for this async operation. + }, data); + } +} + +void PlacesModel::createTrashItem() { + GFile* gf; + gf = g_file_new_for_uri("trash:///"); + // check if trash is supported by the current vfs + // if gvfs is not installed, this can be unavailable. + if(!g_file_query_exists(gf, nullptr)) { + g_object_unref(gf); + trashItem_ = nullptr; + trashMonitor_ = nullptr; + return; + } + trashItem_ = new PlacesModelItem("user-trash", tr("Trash"), Fm::FilePath::fromUri("trash:///")); + + trashMonitor_ = g_file_monitor_directory(gf, G_FILE_MONITOR_NONE, nullptr, nullptr); + if(trashMonitor_) { + if(trashUpdateTimer_ == nullptr) { + trashUpdateTimer_ = new QTimer(this); + trashUpdateTimer_->setSingleShot(true); + connect(trashUpdateTimer_, &QTimer::timeout, this, &PlacesModel::updateTrash); + } + 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(trashUpdateTimer_) { + trashUpdateTimer_->stop(); + delete trashUpdateTimer_; + trashUpdateTimer_ = nullptr; + } + if(trashMonitor_) { + g_signal_handlers_disconnect_by_func(trashMonitor_, (gpointer)G_CALLBACK(onTrashChanged), this); + g_object_unref(trashMonitor_); + trashMonitor_ = nullptr; + } + placesRoot->removeRow(trashItem_->row()); // delete trashItem_; + trashItem_ = nullptr; + } + } +} + +PlacesModelItem* PlacesModel::itemFromPath(const Fm::FilePath &path) { + PlacesModelItem* item = itemFromPath(placesRoot, path); + if(!item) { + item = itemFromPath(devicesRoot, path); + } + if(!item) { + item = itemFromPath(bookmarksRoot, path); + } + return item; +} + +PlacesModelItem* PlacesModel::itemFromPath(QStandardItem* rootItem, const Fm::FilePath &path) { + int rowCount = rootItem->rowCount(); + for(int i = 0; i < rowCount; ++i) { + PlacesModelItem* item = static_cast(rootItem->child(i, 0)); + if(item->path() == path) { + return item; + } + } + return nullptr; +} + +PlacesModelVolumeItem* PlacesModel::itemFromVolume(GVolume* volume) { + int rowCount = devicesRoot->rowCount(); + for(int i = 0; i < rowCount; ++i) { + PlacesModelItem* item = static_cast(devicesRoot->child(i, 0)); + if(item->type() == PlacesModelItem::Volume) { + PlacesModelVolumeItem* volumeItem = static_cast(item); + if(volumeItem->volume() == volume) { + return volumeItem; + } + } + } + return nullptr; +} + +PlacesModelMountItem* PlacesModel::itemFromMount(GMount* mount) { + int rowCount = devicesRoot->rowCount(); + for(int i = 0; i < rowCount; ++i) { + PlacesModelItem* item = static_cast(devicesRoot->child(i, 0)); + if(item->type() == PlacesModelItem::Mount) { + PlacesModelMountItem* mountItem = static_cast(item); + if(mountItem->mount() == mount) { + return mountItem; + } + } + } + return nullptr; +} + +PlacesModelBookmarkItem* PlacesModel::itemFromBookmark(std::shared_ptr bkitem) { + int rowCount = bookmarksRoot->rowCount(); + for(int i = 0; i < rowCount; ++i) { + PlacesModelBookmarkItem* item = static_cast(bookmarksRoot->child(i, 0)); + if(item->bookmark() == bkitem) { + return item; + } + } + return nullptr; +} + +void PlacesModel::onMountAdded(GVolumeMonitor* /*monitor*/, GMount* mount, PlacesModel* pThis) { + // according to gio API doc, a shadowed mount should not be visible to the user +#if GLIB_CHECK_VERSION(2, 20, 0) + if(g_mount_is_shadowed(mount)) { + if(pThis->shadowedMounts_.indexOf(mount) == -1) { + pThis->shadowedMounts_.push_back(G_MOUNT(g_object_ref(mount))); + } + return; + } +#endif + GVolume* vol = g_mount_get_volume(mount); + if(vol) { // mount-added is also emitted when a volume is newly mounted. + PlacesModelVolumeItem* item = pThis->itemFromVolume(vol); + if(item && !item->path()) { + // update the mounted volume and show a button for eject. + Fm::FilePath path{g_mount_get_root(mount), false}; + item->setPath(path); + // update the mount indicator (eject button) + QStandardItem* ejectBtn = item->parent()->child(item->row(), 1); + Q_ASSERT(ejectBtn); + ejectBtn->setIcon(pThis->ejectIcon_); + } + g_object_unref(vol); + } + else { // network mounts and others + PlacesModelMountItem* item = pThis->itemFromMount(mount); + /* for some unknown reasons, sometimes we get repeated mount-added + * signals and added a device more than one. So, make a sanity check here. */ + if(!item) { + item = new PlacesModelMountItem(mount); + QStandardItem* eject_btn = new QStandardItem(pThis->ejectIcon_, QString()); + pThis->devicesRoot->appendRow(QList() << item << eject_btn); + } + } +} + +void PlacesModel::onMountChanged(GVolumeMonitor* monitor, GMount* mount, PlacesModel* pThis) { + gboolean shadowed = FALSE; + // according to gio API doc, a shadowed mount should not be visible to the user +#if GLIB_CHECK_VERSION(2, 20, 0) + shadowed = g_mount_is_shadowed(mount); + // qDebug() << "changed:" << mount << shadowed; +#endif + PlacesModelMountItem* item = pThis->itemFromMount(mount); + if(item) { + if(shadowed) { // if a visible item becomes shadowed, remove it from the model + pThis->shadowedMounts_.push_back(G_MOUNT(g_object_ref(mount))); // remember the shadowed mount + pThis->devicesRoot->removeRow(item->row()); + } + else { // otherwise, update its status + item->update(); + } + } + else { +#if GLIB_CHECK_VERSION(2, 20, 0) + if(!shadowed) { // if a mount is unshadowed + int i = pThis->shadowedMounts_.indexOf(mount); + if(i != -1) { // a previously shadowed mount is unshadowed + pThis->shadowedMounts_.removeAt(i); + onMountAdded(monitor, mount, pThis); // add it to our model again + } + } +#endif + } +} + +void PlacesModel::onMountRemoved(GVolumeMonitor* monitor, GMount* mount, PlacesModel* pThis) { + GVolume* vol = g_mount_get_volume(mount); + // qDebug() << "mount removed" << mount << "volume umounted: " << vol; + if(vol) { + // a volume is being unmounted + // NOTE: Due to some problems of gvfs, sometimes the volume does not receive + // "change" signal so it does not update the eject button. Let's workaround + // this by calling onVolumeChanged() handler manually. (This is needed for mtp://) + onVolumeChanged(monitor, vol, pThis); + g_object_unref(vol); + } + else { // network mounts and others + PlacesModelMountItem* item = pThis->itemFromMount(mount); + if(item) { + pThis->devicesRoot->removeRow(item->row()); + } + } + +#if GLIB_CHECK_VERSION(2, 20, 0) + // NOTE: g_mount_is_shadowed() sometimes returns FALSE here even if the mount is shadowed. + // I don't know whether this is a bug in gvfs or not. + // So let's check if its in our list instead. + if(pThis->shadowedMounts_.removeOne(mount)) { + // if this is a shadowed mount + // qDebug() << "remove shadow mount"; + g_object_unref(mount); + } +#endif +} + +void PlacesModel::onVolumeAdded(GVolumeMonitor* /*monitor*/, GVolume* volume, PlacesModel* pThis) { + // the item may have been added with "mount-added" (as in loopback mounting) + bool itemExists = false; + GMount* mount = g_volume_get_mount(volume); + if(mount) { + if(pThis->itemFromMount(mount)) { + itemExists = true; + } + g_object_unref(mount); + } + if(itemExists) { + return; + } + // for some unknown reasons, sometimes we get repeated volume-added + // signals and added a device more than one. So, make a sanity check here. + PlacesModelVolumeItem* volumeItem = pThis->itemFromVolume(volume); + if(!volumeItem) { + volumeItem = new PlacesModelVolumeItem(volume); + QStandardItem* ejectBtn = new QStandardItem(); + if(volumeItem->isMounted()) { + ejectBtn->setIcon(pThis->ejectIcon_); + } + pThis->devicesRoot->appendRow(QList() << volumeItem << ejectBtn); + } +} + +void PlacesModel::onVolumeChanged(GVolumeMonitor* /*monitor*/, GVolume* volume, PlacesModel* pThis) { + PlacesModelVolumeItem* item = pThis->itemFromVolume(volume); + if(item) { + item->update(); + QStandardItem* ejectBtn = item->parent()->child(item->row(), 1); + Q_ASSERT(ejectBtn); + 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) + ejectBtn->setIcon(QIcon()); + } + else if(ejectBtn->icon().isNull()) { + // this function may be called before onMountAdded(), + // so that the path is set but the eject icon isn't added yet + ejectBtn->setIcon(pThis->ejectIcon_); + } + } +} + +void PlacesModel::onVolumeRemoved(GVolumeMonitor* /*monitor*/, GVolume* volume, PlacesModel* pThis) { + PlacesModelVolumeItem* item = pThis->itemFromVolume(volume); + if(item) { + pThis->devicesRoot->removeRow(item->row()); + } +} + +void PlacesModel::onBookmarksChanged() { + // remove all items + bookmarksRoot->removeRows(0, bookmarksRoot->rowCount()); + loadBookmarks(); +} + +Qt::ItemFlags PlacesModel::flags(const QModelIndex& index) const { + if(index.column() == 1) { // make 2nd column of every row selectable. + return Qt::ItemIsSelectable | Qt::ItemIsEnabled; + } + if(!index.parent().isValid()) { // root items + if(index.row() == 2) { // bookmarks root + return Qt::ItemIsEnabled | Qt::ItemIsDropEnabled; + } + else { + return Qt::ItemIsEnabled; + } + } + return QStandardItemModel::flags(index); +} + + +QVariant PlacesModel::data(const QModelIndex& index, int role) const { + if(index.column() == 0 && index.parent().isValid()) { + PlacesModelItem* item = static_cast(QStandardItemModel::itemFromIndex(index)); + if(item != nullptr) { + switch(role) { + case FileInfoRole: + return QVariant::fromValue(item->fileInfo()); + case FmIconRole: + return QVariant::fromValue(item->icon()); + } + } + } + return QStandardItemModel::data(index, role); +} + +std::shared_ptr PlacesModel::globalInstance() { + auto model = globalInstance_.lock(); + if(!model) { + model = std::make_shared(); + globalInstance_ = model; + } + return model; +} + + +bool PlacesModel::dropMimeData(const QMimeData* data, Qt::DropAction /*action*/, int row, int column, const QModelIndex& parent) { + QStandardItem* item = itemFromIndex(parent); + if(data->hasFormat("application/x-bookmark-row")) { // the data being dopped is a bookmark row + // decode it and do bookmark reordering + QByteArray buf = data->data("application/x-bookmark-row"); + QDataStream stream(&buf, QIODevice::ReadOnly); + int oldPos = -1; + char* pathStr = nullptr; + stream >> oldPos >> pathStr; + // find the source bookmark item being dragged + auto allBookmarks = bookmarks->items(); + auto& draggedItem = allBookmarks[oldPos]; + // If we cannot find the dragged bookmark item at position , or we find an item, + // but the path of the item is not the same as what we expected, than it's the wrong item. + // This means that the bookmarks are changed during our dnd processing, which is an extremely rare case. + auto draggedPath = Fm::FilePath::fromPathStr(pathStr); + if(!draggedItem || draggedItem->path() != draggedPath) { + delete []pathStr; + return false; + } + delete []pathStr; + + int newPos = -1; + if(row == -1 && column == -1) { // drop on an item + // we only allow dropping on an bookmark item + if(item && item->parent() == bookmarksRoot) { + newPos = parent.row(); + } + } + else { // drop on a position between items + if(item == bookmarksRoot) { // we only allow dropping on a bookmark item + newPos = row; + } + } + if(newPos != -1 && newPos != oldPos) { // reorder the bookmark item + bookmarks->reorder(draggedItem, newPos); + } + } + else if(data->hasUrls()) { // files uris are dropped + if(row == -1 && column == -1) { // drop uris on an item + if(item && item->parent()) { // need to be a child item + PlacesModelItem* placesItem = static_cast(item); + if(placesItem->path()) { + qDebug() << "dropped dest:" << placesItem->text(); + // TODO: copy or move the dragged files to the dir pointed by the item. + qDebug() << "drop on" << item->text(); + } + } + } + else { // drop uris on a position between items + if(item == bookmarksRoot) { // we only allow dropping on blank row of bookmarks section + auto paths = pathListFromQUrls(data->urls()); + for(auto& path: paths) { + // FIXME: this is a blocking call + if(g_file_query_file_type(path.gfile().get(), G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + nullptr) == G_FILE_TYPE_DIRECTORY) { + auto disp_name = path.baseName(); + bookmarks->insert(path, disp_name.get(), row); + } + return true; + } + } + } + } + return false; +} + +// we only support dragging bookmark items and use our own +// custom pseudo-mime-type: application/x-bookmark-row +QMimeData* PlacesModel::mimeData(const QModelIndexList& indexes) const { + if(!indexes.isEmpty()) { + // we only allow dragging one bookmark item at a time, so handle the first index only. + QModelIndex index = indexes.first(); + QStandardItem* item = itemFromIndex(index); + // ensure that it's really a bookmark item + if(item && item->parent() == bookmarksRoot) { + PlacesModelBookmarkItem* bookmarkItem = static_cast(item); + QMimeData* mime = new QMimeData(); + QByteArray data; + QDataStream stream(&data, QIODevice::WriteOnly); + // There is no safe and cross-process way to store a reference of a row. + // Let's store the pos, name, and path of the bookmark item instead. + auto pathStr = bookmarkItem->path().toString(); + stream << index.row() << pathStr.get(); + mime->setData("application/x-bookmark-row", data); + return mime; + } + } + return nullptr; +} + +QStringList PlacesModel::mimeTypes() const { + return QStringList() << "application/x-bookmark-row" << "text/uri-list"; +} + +Qt::DropActions PlacesModel::supportedDropActions() const { + return QStandardItemModel::supportedDropActions(); +} + + +} // namespace Fm diff --git a/src/placesmodel.h b/src/placesmodel.h new file mode 100644 index 0000000..a18f804 --- /dev/null +++ b/src/placesmodel.h @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_PLACESMODEL_H +#define FM_PLACESMODEL_H + +#include "libfmqtglobals.h" +#include +#include +#include +#include + +#include + +#include "core/filepath.h" +#include "core/bookmarks.h" + +namespace Fm { + +class PlacesModelItem; +class PlacesModelVolumeItem; +class PlacesModelMountItem; +class PlacesModelBookmarkItem; + +class LIBFM_QT_API PlacesModel : public QStandardItemModel { + Q_OBJECT + friend class PlacesView; +public: + + enum { + FileInfoRole = Qt::UserRole, + FmIconRole + }; + + // QAction used for popup menus + class ItemAction : public QAction { + public: + explicit ItemAction(const QModelIndex& index, QString text, QObject* parent = 0): + QAction(text, parent), + index_(index) { + } + + QPersistentModelIndex& index() { + return index_; + } + private: + QPersistentModelIndex index_; + }; + +public: + explicit PlacesModel(QObject* parent = 0); + virtual ~PlacesModel(); + + bool showTrash() { + return trashItem_ != nullptr; + } + void setShowTrash(bool show); + + bool showApplications() { + return showApplications_; + } + void setShowApplications(bool show); + + bool showDesktop() { + return showDesktop_; + } + void setShowDesktop(bool show); + + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + + static std::shared_ptr globalInstance(); + +public Q_SLOTS: + void updateTrash(); + void onBookmarksChanged(); + +protected: + + PlacesModelItem* itemFromPath(const Fm::FilePath& path); + PlacesModelItem* itemFromPath(QStandardItem* rootItem, const Fm::FilePath & path); + PlacesModelVolumeItem* itemFromVolume(GVolume* volume); + PlacesModelMountItem* itemFromMount(GMount* mount); + PlacesModelBookmarkItem* itemFromBookmark(std::shared_ptr bkitem); + + virtual Qt::ItemFlags flags(const QModelIndex& index) const; + virtual QStringList mimeTypes() const; + virtual QMimeData* mimeData(const QModelIndexList& indexes) const; + virtual bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent); + Qt::DropActions supportedDropActions() const; + + void createTrashItem(); + +private: + void loadBookmarks(); + + static void onVolumeAdded(GVolumeMonitor* monitor, GVolume* volume, PlacesModel* pThis); + static void onVolumeRemoved(GVolumeMonitor* monitor, GVolume* volume, PlacesModel* pThis); + static void onVolumeChanged(GVolumeMonitor* monitor, GVolume* volume, PlacesModel* pThis); + static void onMountAdded(GVolumeMonitor* monitor, GMount* mount, PlacesModel* pThis); + static void onMountRemoved(GVolumeMonitor* monitor, GMount* mount, PlacesModel* pThis); + static void onMountChanged(GVolumeMonitor* monitor, GMount* mount, PlacesModel* pThis); + + static void onTrashChanged(GFileMonitor* monitor, GFile* gf, GFile* other, GFileMonitorEvent evt, PlacesModel* pThis); + +private: + std::shared_ptr bookmarks; + GVolumeMonitor* volumeMonitor; + bool showApplications_; + bool showDesktop_; + QStandardItem* placesRoot; + QStandardItem* devicesRoot; + QStandardItem* bookmarksRoot; + PlacesModelItem* trashItem_; + GFileMonitor* trashMonitor_; + QTimer* trashUpdateTimer_; + PlacesModelItem* desktopItem; + PlacesModelItem* homeItem; + PlacesModelItem* computerItem; + PlacesModelItem* networkItem; + PlacesModelItem* applicationsItem; + QIcon ejectIcon_; + QList shadowedMounts_; + + static std::weak_ptr globalInstance_; +}; + +} + +#endif // FM_PLACESMODEL_H diff --git a/src/placesmodelitem.cpp b/src/placesmodelitem.cpp new file mode 100644 index 0000000..9f178a8 --- /dev/null +++ b/src/placesmodelitem.cpp @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "placesmodelitem.h" +#include +#include + +namespace Fm { + +PlacesModelItem::PlacesModelItem(): + QStandardItem(), + fileInfo_(nullptr), + icon_(nullptr) { +} + +PlacesModelItem::PlacesModelItem(const char* iconName, QString title, Fm::FilePath path): + QStandardItem(title), + path_{std::move(path)}, + icon_(Fm::IconInfo::fromName(iconName)) { + if(icon_) { + QStandardItem::setIcon(icon_->qicon()); + } + setEditable(false); +} + +PlacesModelItem::PlacesModelItem(std::shared_ptr icon, QString title, Fm::FilePath path): + QStandardItem(title), + path_{std::move(path)}, + icon_{std::move(icon)} { + if(icon_) { + QStandardItem::setIcon(icon_->qicon()); + } + setEditable(false); +} + +PlacesModelItem::PlacesModelItem(QIcon icon, QString title, Fm::FilePath path): + QStandardItem(icon, title), + path_{std::move(path)} { + setEditable(false); +} + +PlacesModelItem::~PlacesModelItem() { +} + + +void PlacesModelItem::setIcon(std::shared_ptr icon) { + icon_= std::move(icon); + if(icon_) { + QStandardItem::setIcon(icon_->qicon()); + } + else { + QStandardItem::setIcon(QIcon()); + } +} + +void PlacesModelItem::setIcon(GIcon* gicon) { + setIcon(Fm::IconInfo::fromGIcon(Fm::GIconPtr{gicon, true})); +} + +void PlacesModelItem::updateIcon() { + if(icon_) { + QStandardItem::setIcon(icon_->qicon()); + } +} + +QVariant PlacesModelItem::data(int role) const { + // we use a QPixmap from FmIcon cache rather than QIcon object for decoration role. + return QStandardItem::data(role); +} + +PlacesModelBookmarkItem::PlacesModelBookmarkItem(std::shared_ptr bm_item): + PlacesModelItem{bm_item->icon(), bm_item->name(), bm_item->path()}, + bookmarkItem_{std::move(bm_item)} { + setEditable(true); +} + +PlacesModelVolumeItem::PlacesModelVolumeItem(GVolume* volume): + PlacesModelItem(), + volume_(reinterpret_cast(g_object_ref(volume))) { + update(); + setEditable(false); +} + +void PlacesModelVolumeItem::update() { + // set title + char* volumeName = g_volume_get_name(volume_); + setText(QString::fromUtf8(volumeName)); + g_free(volumeName); + + // set icon + Fm::GIconPtr gicon{g_volume_get_icon(volume_), false}; + setIcon(gicon.get()); + + QString toolTip; + + // set dir path and tooltip + Fm::GMountPtr mount{g_volume_get_mount(volume_), false}; + if(mount) { + Fm::FilePath mount_root{g_mount_get_root(mount.get()), false}; + setPath(mount_root); + toolTip = mount_root.toString().get(); + } + else { + setPath(Fm::FilePath{}); + if(CStrPtr identifier{g_volume_get_identifier(volume_, G_VOLUME_IDENTIFIER_KIND_UNIX_DEVICE)}) { + toolTip = QObject::tr("Identifier: "); + toolTip += identifier.get(); + } + if(CStrPtr uuid{g_volume_get_uuid(volume_)}) { + if(toolTip.isEmpty()) { + toolTip = "UUID: "; + } + else { + toolTip += "\nUUID: "; + } + toolTip += uuid.get(); + } + } + + setToolTip(toolTip); +} + + +bool PlacesModelVolumeItem::isMounted() { + GMount* mount = g_volume_get_mount(volume_); + if(mount) { + g_object_unref(mount); + } + return mount != nullptr ? true : false; +} + + +PlacesModelMountItem::PlacesModelMountItem(GMount* mount): + PlacesModelItem(), + mount_(reinterpret_cast(mount)) { + update(); + setEditable(false); +} + +void PlacesModelMountItem::update() { + // set title + setText(QString::fromUtf8(g_mount_get_name(mount_))); + + // set path + Fm::FilePath mount_root{g_mount_get_root(mount_), false}; + setPath(mount_root); + setToolTip(mount_root.toString().get()); + + // set icon + Fm::GIconPtr gicon{g_mount_get_icon(mount_), false}; + setIcon(gicon.get()); +} + +} diff --git a/src/placesmodelitem.h b/src/placesmodelitem.h new file mode 100644 index 0000000..3ecd9bc --- /dev/null +++ b/src/placesmodelitem.h @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_PLACESMODELITEM_H +#define FM_PLACESMODELITEM_H + +#include "libfmqtglobals.h" +#include +#include +#include +#include + +#include "core/fileinfo.h" +#include "core/filepath.h" +#include "core/bookmarks.h" + +namespace Fm { + +// model item +class LIBFM_QT_API PlacesModelItem : public QStandardItem { +public: + enum Type { + Places = QStandardItem::UserType + 1, + Volume, + Mount, + Bookmark + }; + +public: + explicit PlacesModelItem(); + explicit PlacesModelItem(QIcon icon, QString title, Fm::FilePath path = Fm::FilePath{}); + explicit PlacesModelItem(const char* iconName, QString title, Fm::FilePath path = Fm::FilePath{}); + explicit PlacesModelItem(std::shared_ptr icon, QString title, Fm::FilePath path = Fm::FilePath{}); + ~PlacesModelItem(); + + const std::shared_ptr& fileInfo() const { + return fileInfo_; + } + void setFileInfo(std::shared_ptr fileInfo) { + fileInfo_ = std::move(fileInfo); + } + + const Fm::FilePath& path() const { + return path_; + } + void setPath(Fm::FilePath path) { + path_ = std::move(path); + } + + const std::shared_ptr& icon() const { + return icon_; + } + void setIcon(std::shared_ptr icon); + void setIcon(GIcon* gicon); + void updateIcon(); + + QVariant data(int role = Qt::UserRole + 1) const; + + virtual int type() const { + return Places; + } + +private: + Fm::FilePath path_; + std::shared_ptr fileInfo_; + std::shared_ptr icon_; +}; + +class LIBFM_QT_API PlacesModelVolumeItem : public PlacesModelItem { +public: + PlacesModelVolumeItem(GVolume* volume); + bool isMounted(); + bool canEject() { + return g_volume_can_eject(volume_); + } + virtual int type() const { + return Volume; + } + GVolume* volume() { + return volume_; + } + void update(); +private: + GVolume* volume_; +}; + +class LIBFM_QT_API PlacesModelMountItem : public PlacesModelItem { +public: + PlacesModelMountItem(GMount* mount); + virtual int type() const { + return Mount; + } + GMount* mount() const { + return mount_; + } + void update(); +private: + GMount* mount_; +}; + +class LIBFM_QT_API PlacesModelBookmarkItem : public PlacesModelItem { +public: + virtual int type() const { + return Bookmark; + } + PlacesModelBookmarkItem(std::shared_ptr bm_item); + const std::shared_ptr& bookmark() const { + return bookmarkItem_; + } +private: + std::shared_ptr bookmarkItem_; +}; + +} + +#endif // FM_PLACESMODELITEM_H diff --git a/src/placesview.cpp b/src/placesview.cpp new file mode 100644 index 0000000..4718d2c --- /dev/null +++ b/src/placesview.cpp @@ -0,0 +1,635 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "placesview.h" +#include "placesmodel.h" +#include "placesmodelitem.h" +#include "mountoperation.h" +#include "fileoperation.h" +#include +#include +#include +#include +#include +#include "folderitemdelegate.h" + +namespace Fm { + +std::shared_ptr PlacesView::proxyModel_; + +PlacesProxyModel::PlacesProxyModel(QObject* parent) : + QSortFilterProxyModel(parent), + showAll_(false), + hiddenItemsRestored_(false) { +} + +PlacesProxyModel::~PlacesProxyModel() { +} + +void PlacesProxyModel::restoreHiddenItems(const QSet& items) { + // hidden items should be restored only once + if(!hiddenItemsRestored_ && !items.isEmpty()) { + hidden_.clear(); + QSet::const_iterator i = items.constBegin(); + while (i != items.constEnd()) { + if(!(*i).isEmpty()) { + hidden_ << *i; + } + ++i; + } + hiddenItemsRestored_ = true; + invalidateFilter(); + } +} + +void PlacesProxyModel::setHidden(const QString& str, bool hide) { + if(hide) { + if(!str.isEmpty()) { + hidden_ << str; + } + } + else { + hidden_.remove(str); + } + invalidateFilter(); +} + +void PlacesProxyModel::showAll(bool show) { + showAll_ = show; + invalidateFilter(); +} + +bool PlacesProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { + if(showAll_ || hidden_.isEmpty()) { + return true; + } + if(PlacesModel* srcModel = static_cast(sourceModel())) { + QModelIndex index = srcModel->index(source_row, 0, source_parent); + if(PlacesModelItem* item = static_cast(srcModel->itemFromIndex(index))) { + if(item->type() == PlacesModelItem::Places) { + if(auto path = item->path()) { + if(hidden_.contains(path.toString().get())) { + return false; + } + } + } + else if(item->type() == PlacesModelItem::Volume) { + CStrPtr uuid{g_volume_get_uuid(static_cast(item)->volume())}; + if(uuid && hidden_.contains(uuid.get())) { + return false; + } + } + // show a root items only if, at least, one of its children is shown + else if((source_row == 0 || source_row == 1) && !source_parent.isValid()) { + QModelIndex indx = index.child(0, 0); + while(PlacesModelItem* childItem = static_cast(srcModel->itemFromIndex(indx))) { + if(childItem->type() == PlacesModelItem::Places) { + if(auto path = childItem->path()) { + if(!hidden_.contains(path.toString().get())) { + return true; + } + } + } + else if(childItem->type() == PlacesModelItem::Volume) { + CStrPtr uuid{g_volume_get_uuid(static_cast(childItem)->volume())}; + if(uuid == nullptr || !hidden_.contains(uuid.get())) { + return true; + } + } + else { + return true; + } + indx = indx.sibling(indx.row() + 1, 0); + } + return false; + } + } + } + return true; +} + +PlacesView::PlacesView(QWidget* parent): + QTreeView(parent) { + setRootIsDecorated(false); + setHeaderHidden(true); + setIndentation(12); + + connect(this, &QTreeView::clicked, this, &PlacesView::onClicked); + connect(this, &QTreeView::pressed, this, &PlacesView::onPressed); + + setIconSize(QSize(24, 24)); + + FolderItemDelegate* delegate = new FolderItemDelegate(this, this); + delegate->setFileInfoRole(PlacesModel::FileInfoRole); + delegate->setIconInfoRole(PlacesModel::FmIconRole); + setItemDelegateForColumn(0, delegate); + + model_ = PlacesModel::globalInstance(); + if(!proxyModel_) { + proxyModel_ = std::make_shared(); + } + if(!proxyModel_->sourceModel()) { // all places-views may have been closed + proxyModel_->setSourceModel(model_.get()); + } + setModel(proxyModel_.get()); + + // these 2 connections are needed to update filtering + connect(model_.get(), &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex&, int, int) { + proxyModel_->setHidden(QString()); // just invalidates filter + expandAll(); + // for some reason (a Qt bug?), spanning is reset + spanFirstColumn(); + }); + connect(model_.get(), &QAbstractItemModel::rowsRemoved, this, [this](const QModelIndex&, int, int) { + proxyModel_->setHidden(QString()); + }); + + QHeaderView* headerView = header(); + headerView->setSectionResizeMode(0, QHeaderView::Stretch); + headerView->setSectionResizeMode(1, QHeaderView::Fixed); + headerView->setStretchLastSection(false); + expandAll(); + + spanFirstColumn(); + + // the 2nd column is for the eject buttons + setSelectionBehavior(QAbstractItemView::SelectRows); // FIXME: why this does not work? + setAllColumnsShowFocus(false); + + setAcceptDrops(true); + setDragEnabled(true); + + // update the umount button's column width based on icon size + onIconSizeChanged(iconSize()); +#if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0) // this signal requires Qt >= 5.5 + connect(this, &QAbstractItemView::iconSizeChanged, this, &PlacesView::onIconSizeChanged); +#endif +} + +PlacesView::~PlacesView() { + // qDebug("delete PlacesView"); +} + +void PlacesView::spanFirstColumn() { + // 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 + // NOTE: The first column of the devices children shouldn't be spanned + // because the second column contains eject buttons. + QModelIndex indx = proxyModel_->mapFromSource(model_->placesRoot->index()); + if(indx.isValid()) { + for(int i = 0; i < indx.model()->rowCount(indx); ++i) { + setFirstColumnSpanned(i, indx, true); + } + } + indx = proxyModel_->mapFromSource(model_->bookmarksRoot->index()); + if(indx.isValid()) { + for(int i = 0; i < indx.model()->rowCount(indx); ++i) { + setFirstColumnSpanned(i, indx, true); + } + } +} + +void PlacesView::activateRow(int type, const QModelIndex& index) { + if(!index.parent().isValid()) { // ignore root items + return; + } + PlacesModelItem* item = static_cast(model_->itemFromIndex(proxyModel_->mapToSource(index))); + if(item) { + auto path = item->path(); + if(!path) { + // check if mounting volumes is needed + if(item->type() == PlacesModelItem::Volume) { + PlacesModelVolumeItem* volumeItem = static_cast(item); + if(!volumeItem->isMounted()) { + // Mount the volume + GVolume* volume = volumeItem->volume(); + MountOperation* op = new MountOperation(true, this); + op->mount(volume); + // connect(op, SIGNAL(finished(GError*)), SLOT(onMountOperationFinished(GError*))); + // blocking here until the mount operation is finished? + + // FIXME: update status of the volume after mount is finished!! + if(!op->wait()) { + return; + } + path = item->path(); + } + } + } + if(path) { + Q_EMIT chdirRequested(type, path); + } + } +} + +// mouse button pressed +void PlacesView::onPressed(const QModelIndex& index) { + // if middle button is pressed + if(QGuiApplication::mouseButtons() & Qt::MiddleButton) { + // the real item is at column 0 + activateRow(1, 0 == index.column() ? index : index.sibling(index.row(), 0)); + } +} + +void PlacesView::onIconSizeChanged(const QSize& size) { + setColumnWidth(1, size.width() + 5); +} + +void PlacesView::onEjectButtonClicked(PlacesModelItem* item) { + // The eject button is clicked for a device item (volume or mount) + if(item->type() == PlacesModelItem::Volume) { + PlacesModelVolumeItem* volumeItem = static_cast(item); + MountOperation* op = new MountOperation(true, this); + if(volumeItem->canEject()) { // do eject if applicable + op->eject(volumeItem->volume()); + } + else { // otherwise, do unmount instead + op->unmount(volumeItem->volume()); + } + } + else if(item->type() == PlacesModelItem::Mount) { + PlacesModelMountItem* mountItem = static_cast(item); + MountOperation* op = new MountOperation(true, this); + op->unmount(mountItem->mount()); + } + qDebug("PlacesView::onEjectButtonClicked"); +} + +void PlacesView::onClicked(const QModelIndex& index) { + if(!index.parent().isValid()) { // ignore root items + return; + } + + if(index.column() == 0) { + activateRow(0, index); + } + else if(index.column() == 1) { // column 1 contains eject buttons of the mounted devices + if(index.parent() == proxyModel_->mapFromSource(model_->devicesRoot->index())) { // this is a mounted device + // the eject button is clicked + QModelIndex itemIndex = index.sibling(index.row(), 0); // the real item is at column 0 + PlacesModelItem* item = static_cast(model_->itemFromIndex(proxyModel_->mapToSource(itemIndex))); + if(item) { + // eject the volume or the mount + onEjectButtonClicked(item); + } + } + else { + activateRow(0, index.sibling(index.row(), 0)); + } + } +} + +void PlacesView::setCurrentPath(Fm::FilePath path) { + currentPath_ = std::move(path); + if(currentPath_) { + // TODO: search for item with the path in model_ and select it. + PlacesModelItem* item = model_->itemFromPath(currentPath_); + if(item) { + selectionModel()->select(proxyModel_->mapFromSource(item->index()), QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); + } + else { + clearSelection(); + } + } + else { + clearSelection(); + } +} + + +void PlacesView::dragMoveEvent(QDragMoveEvent* event) { + QTreeView::dragMoveEvent(event); + /* + QModelIndex index = indexAt(event->pos()); + if(event->isAccepted() && index.isValid() && index.parent() == model_->bookmarksRoot->index()) { + if(dropIndicatorPosition() != OnItem) { + event->setDropAction(Qt::LinkAction); + event->accept(); + } + } + */ +} + +void PlacesView::dropEvent(QDropEvent* event) { + QTreeView::dropEvent(event); +} + +void PlacesView::onEmptyTrash() { + Fm::FilePathList files; + files.push_back(Fm::FilePath::fromUri("trash:///")); + Fm::FileOperation::deleteFiles(std::move(files), true, this); +} + +void PlacesView::onMoveBookmarkUp() { + PlacesModel::ItemAction* action = static_cast(sender()); + if(!action->index().isValid()) { + return; + } + PlacesModelBookmarkItem* item = static_cast(model_->itemFromIndex(action->index())); + + int row = item->row(); + if(row > 0) { + auto bookmarkItem = item->bookmark(); + Fm::Bookmarks::globalInstance()->reorder(bookmarkItem, row - 1); + } +} + +void PlacesView::onMoveBookmarkDown() { + PlacesModel::ItemAction* action = static_cast(sender()); + if(!action->index().isValid()) { + return; + } + PlacesModelBookmarkItem* item = static_cast(model_->itemFromIndex(action->index())); + + int row = item->row(); + if(row < model_->rowCount()) { + auto bookmarkItem = item->bookmark(); + Fm::Bookmarks::globalInstance()->reorder(bookmarkItem, row + 1); + } +} + +void PlacesView::onDeleteBookmark() { + PlacesModel::ItemAction* action = static_cast(sender()); + if(!action->index().isValid()) { + return; + } + PlacesModelBookmarkItem* item = static_cast(model_->itemFromIndex(action->index())); + auto bookmarkItem = item->bookmark(); + Fm::Bookmarks::globalInstance()->remove(bookmarkItem); +} + +// virtual +void PlacesView::commitData(QWidget* editor) { + QTreeView::commitData(editor); + PlacesModelBookmarkItem* item = static_cast(model_->itemFromIndex(proxyModel_->mapToSource(currentIndex()))); + auto bookmarkItem = item->bookmark(); + // rename bookmark + Fm::Bookmarks::globalInstance()->rename(bookmarkItem, item->text()); +} + +void PlacesView::onOpenNewTab() { + PlacesModel::ItemAction* action = static_cast(sender()); + if(!action->index().isValid()) { + return; + } + PlacesModelItem* item = static_cast(model_->itemFromIndex(action->index())); + if(item) { + Q_EMIT chdirRequested(1, item->path()); + } +} + +void PlacesView::onOpenNewWindow() { + PlacesModel::ItemAction* action = static_cast(sender()); + if(!action->index().isValid()) { + return; + } + PlacesModelItem* item = static_cast(model_->itemFromIndex(action->index())); + if(item) { + Q_EMIT chdirRequested(2, item->path()); + } +} + +void PlacesView::onRenameBookmark() { + PlacesModel::ItemAction* action = static_cast(sender()); + if(!action->index().isValid()) { + return; + } + PlacesModelBookmarkItem* item = static_cast(model_->itemFromIndex(action->index())); + setFocus(); + setCurrentIndex(proxyModel_->mapFromSource(item->index())); + edit(proxyModel_->mapFromSource(item->index())); +} + +void PlacesView::onMountVolume() { + PlacesModel::ItemAction* action = static_cast(sender()); + if(!action->index().isValid()) { + return; + } + PlacesModelVolumeItem* item = static_cast(model_->itemFromIndex(action->index())); + MountOperation* op = new MountOperation(true, this); + op->mount(item->volume()); + op->wait(); +} + +void PlacesView::onUnmountVolume() { + PlacesModel::ItemAction* action = static_cast(sender()); + if(!action->index().isValid()) { + return; + } + PlacesModelVolumeItem* item = static_cast(model_->itemFromIndex(action->index())); + MountOperation* op = new MountOperation(true, this); + op->unmount(item->volume()); + op->wait(); +} + +void PlacesView::onUnmountMount() { + PlacesModel::ItemAction* action = static_cast(sender()); + if(!action->index().isValid()) { + return; + } + PlacesModelMountItem* item = static_cast(model_->itemFromIndex(action->index())); + GMount* mount = item->mount(); + MountOperation* op = new MountOperation(true, this); + op->unmount(mount); + op->wait(); +} + +void PlacesView::onEjectVolume() { + PlacesModel::ItemAction* action = static_cast(sender()); + if(!action->index().isValid()) { + return; + } + PlacesModelVolumeItem* item = static_cast(model_->itemFromIndex(action->index())); + MountOperation* op = new MountOperation(true, this); + op->eject(item->volume()); + op->wait(); +} + +void PlacesView::contextMenuEvent(QContextMenuEvent* event) { + QModelIndex index = indexAt(event->pos()); + if(index.isValid()) { + 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/lxqt/pcmanfm-qt/issues/145 + QMenu* menu = new QMenu(); + QAction* action = nullptr; + PlacesModelItem* item = static_cast(model_->itemFromIndex(proxyModel_->mapToSource(index))); + + if(index.parent().isValid() + && item->type() != PlacesModelItem::Mount + && (item->type() != PlacesModelItem::Volume + || static_cast(item)->isMounted())) { + action = new PlacesModel::ItemAction(item->index(), tr("Open in New Tab"), menu); + connect(action, &QAction::triggered, this, &PlacesView::onOpenNewTab); + menu->addAction(action); + action = new PlacesModel::ItemAction(item->index(), tr("Open in New Window"), menu); + connect(action, &QAction::triggered, this, &PlacesView::onOpenNewWindow); + menu->addAction(action); + } + + switch(item->type()) { + case PlacesModelItem::Places: { + auto path = item->path(); + // FIXME: inefficient + if(path) { + auto path_str = path.toString(); + if(strcmp(path_str.get(), "trash:///") == 0) { + action = new PlacesModel::ItemAction(item->index(), tr("Empty Trash"), menu); + auto icn = item->icon(); + if(icn && icn->qicon().name() == QLatin1String("user-trash")) { // surely an empty trash + action->setEnabled(false); + } + else { + connect(action, &QAction::triggered, this, &PlacesView::onEmptyTrash); + } + // add the "Empty Trash" item on the top + QList actions = menu->actions(); + if(!actions.isEmpty()) { + menu->insertAction(actions.at(0), action); + menu->insertSeparator(actions.at(0)); + } + else { // impossible + menu->addAction(action); + } + } + // add a "Hide" action to the end + menu->addSeparator(); + action = new PlacesModel::ItemAction(item->index(), tr("Hide"), menu); + QString pathStr(path_str.get()); + action->setCheckable(true); + if(proxyModel_->isShowingAll()) { + action->setChecked(proxyModel_->isHidden(pathStr)); + } + connect(action, &QAction::triggered, [this, pathStr](bool checked) { + proxyModel_->setHidden(pathStr, checked); + Q_EMIT hiddenItemSet(pathStr, checked); + }); + menu->addAction(action); + } + break; + } + case PlacesModelItem::Bookmark: { + // create context menu for bookmark item + if(item->index().row() > 0) { + action = new PlacesModel::ItemAction(item->index(), tr("Move Bookmark Up"), menu); + connect(action, &QAction::triggered, this, &PlacesView::onMoveBookmarkUp); + menu->addAction(action); + } + if(item->index().row() < model_->rowCount()) { + action = new PlacesModel::ItemAction(item->index(), tr("Move Bookmark Down"), menu); + connect(action, &QAction::triggered, this, &PlacesView::onMoveBookmarkDown); + menu->addAction(action); + } + action = new PlacesModel::ItemAction(item->index(), tr("Rename Bookmark"), menu); + connect(action, &QAction::triggered, this, &PlacesView::onRenameBookmark); + menu->addAction(action); + action = new PlacesModel::ItemAction(item->index(), tr("Remove Bookmark"), menu); + connect(action, &QAction::triggered, this, &PlacesView::onDeleteBookmark); + menu->addAction(action); + break; + } + case PlacesModelItem::Volume: { + PlacesModelVolumeItem* volumeItem = static_cast(item); + + if(volumeItem->isMounted()) { + action = new PlacesModel::ItemAction(item->index(), tr("Unmount"), menu); + connect(action, &QAction::triggered, this, &PlacesView::onUnmountVolume); + } + else { + action = new PlacesModel::ItemAction(item->index(), tr("Mount"), menu); + connect(action, &QAction::triggered, this, &PlacesView::onMountVolume); + } + menu->addAction(action); + + if(volumeItem->canEject()) { + action = new PlacesModel::ItemAction(item->index(), tr("Eject"), menu); + connect(action, &QAction::triggered, this, &PlacesView::onEjectVolume); + menu->addAction(action); + } + // add a "Hide" action to the end + CStrPtr uuid{g_volume_get_uuid(static_cast(item)->volume())}; + if(uuid) { + QString str = uuid.get(); + menu->addSeparator(); + action = new PlacesModel::ItemAction(item->index(), tr("Hide"), menu); + action->setCheckable(true); + if(proxyModel_->isShowingAll()) { + action->setChecked(proxyModel_->isHidden(str)); + } + connect(action, &QAction::triggered, [this, str](bool checked) { + proxyModel_->setHidden(str, checked); + Q_EMIT hiddenItemSet(str, checked); + }); + 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; + } + } + + // also add an acton for showing all hidden items + if(proxyModel_->hasHidden()) { + if(item->type() == PlacesModelItem::Bookmark) { + menu->addSeparator(); + } + action = new PlacesModel::ItemAction(item->index(), tr("Show All Entries"), menu); + action->setCheckable(true); + action->setChecked(proxyModel_->isShowingAll()); + connect(action, &QAction::triggered, [this](bool checked) { + showAll(checked); + }); + menu->addAction(action); + } + + if(menu->actions().size()) { + menu->popup(mapToGlobal(event->pos())); + connect(menu, &QMenu::aboutToHide, menu, &QMenu::deleteLater); + } + else { + menu->deleteLater(); + } + } +} + +void PlacesView::restoreHiddenItems(const QSet& items) { + proxyModel_->restoreHiddenItems(items); +} + +void PlacesView::showAll(bool show) { + proxyModel_->showAll(show); + if(show) { + expandAll(); + // for some reason (a Qt bug?), spanning is reset + spanFirstColumn(); + } +} + +} // namespace Fm diff --git a/src/placesview.h b/src/placesview.h new file mode 100644 index 0000000..9dba4ed --- /dev/null +++ b/src/placesview.h @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2012 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_PLACESVIEW_H +#define FM_PLACESVIEW_H + +#include "libfmqtglobals.h" +#include +#include + +#include +#include "core/filepath.h" + +namespace Fm { + +class PlacesModel; +class PlacesModelItem; +class PlacesView; + +class LIBFM_QT_API PlacesProxyModel : public QSortFilterProxyModel { + Q_OBJECT +public: + explicit PlacesProxyModel(QObject* parent = 0); + virtual ~PlacesProxyModel(); + + void setHidden(const QString& str, bool hide = true); + + void restoreHiddenItems(const QSet& items); + + void showAll(bool show); + + bool isShowingAll() const { + return showAll_; + } + + bool isHidden(const QString& str) const { + return hidden_.contains(str); + } + + bool hasHidden() const { + return !hidden_.isEmpty(); + } + +protected: + bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const; + +private: + QSet hidden_; + bool showAll_; + bool hiddenItemsRestored_; +}; + +class LIBFM_QT_API PlacesView : public QTreeView { + Q_OBJECT + +public: + explicit PlacesView(QWidget* parent = 0); + virtual ~PlacesView(); + + void setCurrentPath(Fm::FilePath path); + + const Fm::FilePath& currentPath() const { + return currentPath_; + } + + void chdir(Fm::FilePath path) { + setCurrentPath(std::move(path)); + } + +#if QT_VERSION < QT_VERSION_CHECK(5, 5, 0) + void setIconSize(const QSize& size) { + // The signal QAbstractItemView::iconSizeChanged is only available after Qt 5.5. + // To simulate the effect for older Qt versions, we override setIconSize(). + QAbstractItemView::setIconSize(size); + onIconSizeChanged(size); + } +#endif + + void restoreHiddenItems(const QSet& items); + +Q_SIGNALS: + void chdirRequested(int type, const Fm::FilePath& path); + void hiddenItemSet(const QString& str, bool hide); + +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); + void showAll(bool show); + void spanFirstColumn(); + +private: + std::shared_ptr model_; + Fm::FilePath currentPath_; + + static std::shared_ptr proxyModel_; // used to hide items in all views +}; + +} + +#endif // FM_PLACESVIEW_H diff --git a/src/proxyfoldermodel.cpp b/src/proxyfoldermodel.cpp new file mode 100644 index 0000000..f66b3a8 --- /dev/null +++ b/src/proxyfoldermodel.cpp @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "proxyfoldermodel.h" +#include "foldermodel.h" +#include + +namespace Fm { + +ProxyFolderModel::ProxyFolderModel(QObject* parent): + QSortFilterProxyModel(parent), + showHidden_(false), + backupAsHidden_(true), + folderFirst_(true), + showThumbnails_(false), + thumbnailSize_(0) { + + setDynamicSortFilter(true); + setSortCaseSensitivity(Qt::CaseInsensitive); + + collator_.setNumericMode(true); +} + +ProxyFolderModel::~ProxyFolderModel() { + if(showThumbnails_ && thumbnailSize_ != 0) { + FolderModel* srcModel = static_cast(sourceModel()); + // tell the source model that we don't need the thumnails anymore + if(srcModel) { + srcModel->releaseThumbnails(thumbnailSize_); + disconnect(srcModel, SIGNAL(thumbnailLoaded(QModelIndex, int))); + } + } +} + +void ProxyFolderModel::setSourceModel(QAbstractItemModel* model) { + if(model == sourceModel()) // avoid setting the same model twice + return; + FolderModel* oldSrcModel = static_cast(sourceModel()); +#if (QT_VERSION == QT_VERSION_CHECK(5,11,0)) + // workaround for Qt-5.11 bug https://bugreports.qt.io/browse/QTBUG-68581 + if(oldSrcModel) { + disconnect(oldSrcModel, SIGNAL(destroyed()), this, SLOT(_q_sourceModelDestroyed())); + } +#endif + if(model) { + // we only support Fm::FolderModel + Q_ASSERT(model->inherits("Fm::FolderModel")); + + if(showThumbnails_ && thumbnailSize_ != 0) { // if we're showing thumbnails + if(oldSrcModel) { // we need to release cached thumbnails for the old source model + oldSrcModel->releaseThumbnails(thumbnailSize_); + disconnect(oldSrcModel, SIGNAL(thumbnailLoaded(QModelIndex, int))); + } + FolderModel* newSrcModel = static_cast(model); + 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(); + } +} + +void ProxyFolderModel::setBackupAsHidden(bool backupAsHidden) { + if(backupAsHidden != backupAsHidden_) { + backupAsHidden_ = backupAsHidden; + invalidateFilter(); + Q_EMIT sortFilterChanged(); + } +} + +// need to call invalidateFilter() manually. +void ProxyFolderModel::setFolderFirst(bool folderFirst) { + if(folderFirst != folderFirst_) { + folderFirst_ = folderFirst; + invalidate(); + Q_EMIT sortFilterChanged(); + } +} + +void ProxyFolderModel::setSortCaseSensitivity(Qt::CaseSensitivity cs) { + collator_.setCaseSensitivity(cs); + QSortFilterProxyModel::setSortCaseSensitivity(cs); + invalidate(); + Q_EMIT sortFilterChanged(); +} + +bool ProxyFolderModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { + if(!showHidden_) { + if(FolderModel* srcModel = static_cast(sourceModel())) { + auto info = srcModel->fileInfoFromIndex(srcModel->index(source_row, 0, source_parent)); + if(info && (info->isHidden() || (backupAsHidden_ && info->isBackup()))) { + return false; + } + } + } + // apply additional filters if there're any + for(ProxyFolderModelFilter* const filter : qAsConst(filters_)) { + if(FolderModel* srcModel = static_cast(sourceModel())){ + auto fileInfo = srcModel->fileInfoFromIndex(srcModel->index(source_row, 0, source_parent)); + if(!filter->filterAcceptsRow(this, fileInfo)) { + return false; + } + } + } + return true; +} + +bool ProxyFolderModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { + FolderModel* srcModel = static_cast(sourceModel()); + // left and right are indexes of source model, not the proxy model. + if(srcModel) { + auto leftInfo = srcModel->fileInfoFromIndex(left); + auto rightInfo = srcModel->fileInfoFromIndex(right); + + if(folderFirst_) { + bool leftIsFolder = leftInfo->isDir(); + bool rightIsFolder = rightInfo->isDir(); + if(leftIsFolder != rightIsFolder) { + return sortOrder() == Qt::AscendingOrder ? leftIsFolder : rightIsFolder; + } + } + + int comp; + switch(sortColumn()) { + case FolderModel::ColumnFileMTime: + comp = leftInfo->mtime() - rightInfo->mtime(); + break; + case FolderModel::ColumnFileSize: + comp = leftInfo->size() - rightInfo->size(); + break; + default: { + QString leftText = left.data(Qt::DisplayRole).toString(); + QString rightText = right.data(Qt::DisplayRole).toString(); + comp = collator_.compare(leftText, rightText); + break; + } + } + // always sort files by their display names when they have the same property + if(comp == 0) { + return collator_.compare(leftInfo->displayName(), rightInfo->displayName()) < 0; + } + return comp < 0; + } + return QSortFilterProxyModel::lessThan(left, right); +} + +std::shared_ptr ProxyFolderModel::fileInfoFromIndex(const QModelIndex& index) const { + if(index.isValid()) { + FolderModel* srcModel = static_cast(sourceModel()); + if(srcModel) { + QModelIndex srcIndex = mapToSource(index); + return srcModel->fileInfoFromIndex(srcIndex); + } + } + return nullptr; +} + +QModelIndex ProxyFolderModel::indexFromPath(const FilePath &path) const { + QModelIndex ret; + int n_rows = rowCount(); + for(int row = 0; row < n_rows; ++row) { + auto idx = index(row, FolderModel::ColumnFileName, QModelIndex()); + auto fi = fileInfoFromIndex(idx); + if(fi && fi->path() == path) { // found the item + ret = idx; + break; + } + } + return ret; +} + +std::shared_ptr ProxyFolderModel::fileInfoFromPath(const FilePath &path) const { + return fileInfoFromIndex(indexFromPath(path)); +} + +void ProxyFolderModel::setCutFiles(const QItemSelection& selection) { + FolderModel* srcModel = static_cast(sourceModel()); + if(srcModel) { + srcModel->setCutFiles(mapSelectionToSource(selection)); + } +} + +void ProxyFolderModel::setShowThumbnails(bool show) { + if(show != showThumbnails_) { + showThumbnails_ = show; + FolderModel* srcModel = static_cast(sourceModel()); + if(srcModel && thumbnailSize_ != 0) { + if(show) { + // ask for cache of thumbnails of the new size in source model + srcModel->cacheThumbnails(thumbnailSize_); + // connect to the srcModel so we can be notified when a thumbnail is loaded. + connect(srcModel, &FolderModel::thumbnailLoaded, this, &ProxyFolderModel::onThumbnailLoaded); + } + else { // turn off thumbnails + // free cached old thumbnails in souce model + srcModel->releaseThumbnails(thumbnailSize_); + disconnect(srcModel, SIGNAL(thumbnailLoaded(QModelIndex, int))); + } + // reload all items, FIXME: can we only update items previously having thumbnails + Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0)); + } + } +} + +void ProxyFolderModel::setThumbnailSize(int size) { + if(size != thumbnailSize_) { + FolderModel* srcModel = static_cast(sourceModel()); + if(showThumbnails_ && srcModel) { + // free cached thumbnails of the old size + if(thumbnailSize_ != 0) { + srcModel->releaseThumbnails(thumbnailSize_); + } + else { + // if the old thumbnail size is 0, we did not turn on thumbnail initially + connect(srcModel, &FolderModel::thumbnailLoaded, this, &ProxyFolderModel::onThumbnailLoaded); + } + // ask for cache of thumbnails of the new size in source model + srcModel->cacheThumbnails(size); + // reload all items, FIXME: can we only update items previously having thumbnails + Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0)); + } + + thumbnailSize_ = size; + } +} + +QVariant ProxyFolderModel::data(const QModelIndex& index, int role) const { + if(index.column() == 0) { // only show the decoration role for the first column + if(role == Qt::DecorationRole && showThumbnails_ && thumbnailSize_) { + // we need to show thumbnails instead of icons + FolderModel* srcModel = static_cast(sourceModel()); + QModelIndex srcIndex = mapToSource(index); + QImage image = srcModel->thumbnailFromIndex(srcIndex, thumbnailSize_); + if(!image.isNull()) { // if we got a thumbnail of the desired size, use it + return QVariant(image); + } + } + } + // fallback to icons if thumbnails are not available + return QSortFilterProxyModel::data(index, role); +} + +void ProxyFolderModel::onThumbnailLoaded(const QModelIndex& srcIndex, int size) { + // FolderModel* srcModel = static_cast(sourceModel()); + // FolderModelItem* item = srcModel->itemFromIndex(srcIndex); + // qDebug("ProxyFolderModel::onThumbnailLoaded: %d, %s", size, item->displayName.toUtf8().data()); + + if(size == thumbnailSize_ // if a thumbnail of the size we want is loaded + && srcIndex.model() == sourceModel()) { // check if the sourse model contains the index item + QModelIndex index = mapFromSource(srcIndex); + Q_EMIT dataChanged(index, index); + } +} + +void ProxyFolderModel::addFilter(ProxyFolderModelFilter* filter) { + filters_.append(filter); + invalidateFilter(); + Q_EMIT sortFilterChanged(); +} + +void ProxyFolderModel::removeFilter(ProxyFolderModelFilter* filter) { + filters_.removeOne(filter); + invalidateFilter(); + Q_EMIT sortFilterChanged(); +} + +void ProxyFolderModel::updateFilters() { + invalidate(); + Q_EMIT sortFilterChanged(); +} + +#if 0 +void ProxyFolderModel::reloadAllThumbnails() { + // reload all thumbnails and update UI + FolderModel* srcModel = static_cast(sourceModel()); + if(srcModel) { + int rows = rowCount(); + for(int row = 0; row < rows; ++row) { + QModelIndex index = this->index(row, 0); + QModelIndex srcIndex = mapToSource(index); + QImage image = srcModel->thumbnailFromIndex(srcIndex, size); + // tell the world that the item is changed to trigger a UI update + if(!image.isNull()) { + Q_EMIT dataChanged(index, index); + } + } + } +} +#endif + + +} // namespace Fm diff --git a/src/proxyfoldermodel.h b/src/proxyfoldermodel.h new file mode 100644 index 0000000..b8ff4dc --- /dev/null +++ b/src/proxyfoldermodel.h @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_PROXYFOLDERMODEL_H +#define FM_PROXYFOLDERMODEL_H + +#include "libfmqtglobals.h" +#include +#include +#include + +#include "core/fileinfo.h" + +namespace Fm { + +// a proxy model used to sort and filter FolderModel + +class FolderModelItem; +class ProxyFolderModel; + +class LIBFM_QT_API ProxyFolderModelFilter { +public: + virtual bool filterAcceptsRow(const ProxyFolderModel* model, const std::shared_ptr& info) const = 0; + virtual ~ProxyFolderModelFilter() {} +}; + + +class LIBFM_QT_API ProxyFolderModel : public QSortFilterProxyModel { + Q_OBJECT +public: + explicit ProxyFolderModel(QObject* parent = 0); + virtual ~ProxyFolderModel(); + + // only Fm::FolderModel is allowed for being sourceModel + virtual void setSourceModel(QAbstractItemModel* model); + + void setShowHidden(bool show); + bool showHidden() const { + return showHidden_; + } + + void setBackupAsHidden(bool backupAsHidden); + bool backupAsHidden() const { + return backupAsHidden_; + } + + void setFolderFirst(bool folderFirst); + bool folderFirst() { + return folderFirst_; + } + + void setSortCaseSensitivity(Qt::CaseSensitivity cs); + + void setCutFiles(const QItemSelection& selection); + + bool showThumbnails() { + return showThumbnails_; + } + void setShowThumbnails(bool show); + + int thumbnailSize() { + return thumbnailSize_; + } + void setThumbnailSize(int size); + + std::shared_ptr fileInfoFromIndex(const QModelIndex& index) const; + + std::shared_ptr fileInfoFromPath(const FilePath& path) const; + + QModelIndex indexFromPath(const FilePath& path) const; + + virtual void sort(int column, Qt::SortOrder order = Qt::AscendingOrder); + virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + + void addFilter(ProxyFolderModelFilter* filter); + void removeFilter(ProxyFolderModelFilter* filter); + void updateFilters(); + +Q_SIGNALS: + void sortFilterChanged(); + +protected Q_SLOTS: + void onThumbnailLoaded(const QModelIndex& srcIndex, int size); + +protected: + bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const; + bool lessThan(const QModelIndex& left, const QModelIndex& right) const; + // void reloadAllThumbnails(); + +private: + QCollator collator_; + bool showHidden_; + bool backupAsHidden_; + bool folderFirst_; + bool showThumbnails_; + int thumbnailSize_; + QList filters_; +}; + +} + +#endif // FM_PROXYFOLDERMODEL_H diff --git a/src/rename-dialog.ui b/src/rename-dialog.ui new file mode 100644 index 0000000..2b0c123 --- /dev/null +++ b/src/rename-dialog.ui @@ -0,0 +1,204 @@ + + + RenameDialog + + + + 0 + 0 + 398 + 220 + + + + Confirm to replace files + + + false + + + + 6 + + + 10 + + + + + + 0 + 0 + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + + + + + + + 12 + + + 6 + + + + + + 0 + 0 + + + + dest + + + + + + + with the following file? + + + + + + + + 0 + 0 + + + + src file info + + + + + + + + 0 + 0 + + + + dest file info + + + + + + + + 0 + 0 + + + + src + + + + + + + + + 12 + + + + + + 0 + 0 + + + + &File name: + + + fileName + + + + + + + + + + + + Apply this option to all existing files + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 0 + 0 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ignore|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + RenameDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + RenameDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/renamedialog.cpp b/src/renamedialog.cpp new file mode 100644 index 0000000..cf70670 --- /dev/null +++ b/src/renamedialog.cpp @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "renamedialog.h" +#include "ui_rename-dialog.h" +#include +#include +#include + +#include "core/iconinfo.h" +#include "utilities.h" + +#include "core/legacy/fm-config.h" + +namespace Fm { + +RenameDialog::RenameDialog(const FileInfo &src, const FileInfo &dest, QWidget* parent, Qt::WindowFlags f): + QDialog(parent, f), + action_(ActionIgnore), + applyToAll_(false) { + + ui = new Ui::RenameDialog(); + ui->setupUi(this); + + auto path = dest.path(); + auto srcIcon = src.icon(); + auto destIcon = dest.icon(); + + // show info for the source file + QIcon icon = srcIcon->qicon(); + // FIXME: deprecate fm_config + QSize iconSize(fm_config->big_icon_size, fm_config->big_icon_size); + QPixmap pixmap = icon.pixmap(iconSize); + ui->srcIcon->setPixmap(pixmap); + + QString infoStr; + // FIXME: deprecate fm_config + auto disp_size = Fm::formatFileSize(src.size(), fm_config->si_unit); + auto srcMtime = QDateTime::fromMSecsSinceEpoch(src.mtime() * 1000).toString(Qt::SystemLocaleShortDate); + if(!disp_size.isEmpty()) { + infoStr = QString(tr("Type: %1\nSize: %2\nModified: %3")) + .arg(src.description()) + .arg(disp_size) + .arg(srcMtime); + } + else { + infoStr = QString(tr("Type: %1\nModified: %2")) + .arg(src.description()) + .arg(srcMtime); + } + ui->srcInfo->setText(infoStr); + + // show info for the dest file + icon = destIcon->qicon(); + pixmap = icon.pixmap(iconSize); + ui->destIcon->setPixmap(pixmap); + + disp_size = Fm::formatFileSize(dest.size(), fm_config->si_unit); + auto destMtime = QDateTime::fromMSecsSinceEpoch(dest.mtime() * 1000).toString(Qt::SystemLocaleShortDate); + if(!disp_size.isEmpty()) { + infoStr = QString(tr("Type: %1\nSize: %2\nModified: %3")) + .arg(dest.description()) + .arg(disp_size) + .arg(destMtime); + } + else { + infoStr = QString(tr("Type: %1\nModified: %2")) + .arg(dest.description()) + .arg(destMtime); + } + ui->destInfo->setText(infoStr); + + auto basename = path.baseName(); + ui->fileName->setText(QString::fromUtf8(basename.get())); + oldName_ = basename.get(); + connect(ui->fileName, &QLineEdit::textChanged, this, &RenameDialog::onFileNameChanged); + + // add "Rename" button + QAbstractButton* button = ui->buttonBox->button(QDialogButtonBox::Ok); + button->setText(tr("&Overwrite")); + // FIXME: there seems to be no way to place the Rename button next to the overwrite one. + renameButton_ = ui->buttonBox->addButton(tr("&Rename"), QDialogButtonBox::ActionRole); + connect(renameButton_, &QPushButton::clicked, this, &RenameDialog::onRenameClicked); + renameButton_->setEnabled(false); // disabled by default + + button = ui->buttonBox->button(QDialogButtonBox::Ignore); + connect(button, &QPushButton::clicked, this, &RenameDialog::onIgnoreClicked); +} + +RenameDialog::~RenameDialog() { + delete ui; +} + +void RenameDialog::onRenameClicked() { + action_ = ActionRename; + QDialog::done(QDialog::Accepted); +} + +void RenameDialog::onIgnoreClicked() { + action_ = ActionIgnore; +} + +// the overwrite button +void RenameDialog::accept() { + action_ = ActionOverwrite; + applyToAll_ = ui->applyToAll->isChecked(); + QDialog::accept(); +} + +// cancel, or close the dialog +void RenameDialog::reject() { + action_ = ActionCancel; + QDialog::reject(); +} + +void RenameDialog::onFileNameChanged(QString newName) { + newName_ = newName; + // FIXME: check if the name already exists in the current dir + bool hasNewName = (newName_ != oldName_); + renameButton_->setEnabled(hasNewName); + renameButton_->setDefault(hasNewName); + + // change default button to rename rather than overwrire + // if the user typed a new filename + QPushButton* overwriteButton = static_cast(ui->buttonBox->button(QDialogButtonBox::Ok)); + overwriteButton->setEnabled(!hasNewName); + overwriteButton->setDefault(!hasNewName); +} + + +} // namespace Fm diff --git a/src/renamedialog.h b/src/renamedialog.h new file mode 100644 index 0000000..c068ac0 --- /dev/null +++ b/src/renamedialog.h @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_RENAMEDIALOG_H +#define FM_RENAMEDIALOG_H + +#include "libfmqtglobals.h" +#include + +#include "core/fileinfo.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(const FileInfo &src, const FileInfo &dest, QWidget* parent = 0, Qt::WindowFlags f = 0); + virtual ~RenameDialog(); + + Action action() { + return action_; + } + + bool applyToAll() { + return applyToAll_; + } + + QString newName() { + return newName_; + } + +protected Q_SLOTS: + void onRenameClicked(); + void onIgnoreClicked(); + void onFileNameChanged(QString newName); + +protected: + void accept(); + void reject(); + +private: + Ui::RenameDialog* ui; + QPushButton* renameButton_; + Action action_; + bool applyToAll_; + QString oldName_; + QString newName_; +}; + +} + +#endif // FM_RENAMEDIALOG_H diff --git a/src/sidepane.cpp b/src/sidepane.cpp new file mode 100644 index 0000000..7325d48 --- /dev/null +++ b/src/sidepane.cpp @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "sidepane.h" +#include +#include +#include +#include "placesview.h" +#include "dirtreeview.h" +#include "dirtreemodel.h" +#include "filemenu.h" + +namespace Fm { + +SidePane::SidePane(QWidget* parent): + QWidget(parent), + view_(nullptr), + combo_(nullptr), + iconSize_(24, 24), + mode_(ModeNone), + showHidden_(false) { + + verticalLayout = new QVBoxLayout(this); + verticalLayout->setContentsMargins(0, 0, 0, 0); + + combo_ = new QComboBox(this); + combo_->setFrame(false); + combo_->addItem(tr("Places")); + combo_->addItem(tr("Directory Tree")); + connect(combo_, static_cast(&QComboBox::currentIndexChanged), this, &SidePane::onComboCurrentIndexChanged); + verticalLayout->addWidget(combo_); +} + +SidePane::~SidePane() { + // qDebug("delete SidePane"); +} + +void SidePane::onComboCurrentIndexChanged(int current) { + if(current != mode_) { + setMode(Mode(current)); + } +} + +void SidePane::setIconSize(QSize size) { + iconSize_ = size; + switch(mode_) { + case ModePlaces: + static_cast(view_)->setIconSize(size); + /* Falls through. */ + case ModeDirTree: + static_cast(view_)->setIconSize(size); + break; + default: + ; + } +} + +void SidePane::setCurrentPath(Fm::FilePath path) { + Q_ASSERT(path); + currentPath_ = std::move(path); + switch(mode_) { + case ModePlaces: + static_cast(view_)->setCurrentPath(currentPath_); + break; + case ModeDirTree: + static_cast(view_)->setCurrentPath(currentPath_); + break; + default: + ; + } +} + +SidePane::Mode SidePane::modeByName(const char* str) { + if(str == nullptr) { + return ModeNone; + } + if(strcmp(str, "places") == 0) { + return ModePlaces; + } + if(strcmp(str, "dirtree") == 0) { + return ModeDirTree; + } + return ModeNone; +} + +const char* SidePane::modeName(SidePane::Mode mode) { + switch(mode) { + case ModePlaces: + return "places"; + case ModeDirTree: + return "dirtree"; + default: + return nullptr; + } +} + +bool SidePane::setHomeDir(const char* /*home_dir*/) { + if(view_ == nullptr) { + return false; + } + // TODO: SidePane::setHomeDir + + switch(mode_) { + case ModePlaces: + // static_cast(view_); + return true; + case ModeDirTree: + // static_cast(view_); + return true; + default: + ; + } + return true; +} + +void SidePane::initDirTree() { + DirTreeModel* model = new DirTreeModel(view_); + model->setShowHidden(showHidden_); + + Fm::FilePathList rootPaths; + rootPaths.emplace_back(Fm::FilePath::homeDir()); + rootPaths.emplace_back(Fm::FilePath::fromLocalPath("/")); + model->addRoots(std::move(rootPaths)); + static_cast(view_)->setModel(model); +} + +void SidePane::setMode(Mode mode) { + if(mode == mode_) { + return; + } + + if(view_) { + delete view_; + view_ = nullptr; + //if(sp->update_popup) + // g_signal_handlers_disconnect_by_func(sp->view, on_item_popup, sp); + } + mode_ = mode; + + combo_->setCurrentIndex(mode); + switch(mode) { + case ModePlaces: { + PlacesView* placesView = new Fm::PlacesView(this); + + // visually merge it with its surroundings + placesView->setFrameShape(QFrame::NoFrame); + QPalette p = placesView->palette(); + p.setColor(QPalette::Base, QColor(Qt::transparent)); + p.setColor(QPalette::Text, p.color(QPalette::WindowText)); + placesView->setPalette(p); + placesView->viewport()->setAutoFillBackground(false); + + view_ = placesView; + placesView->restoreHiddenItems(restorableHiddenPlaces_); + placesView->setIconSize(iconSize_); + placesView->setCurrentPath(currentPath_); + connect(placesView, &PlacesView::chdirRequested, this, &SidePane::chdirRequested); + connect(placesView, &PlacesView::hiddenItemSet, this, &SidePane::hiddenPlaceSet); + break; + } + case ModeDirTree: { + DirTreeView* dirTreeView = new Fm::DirTreeView(this); + view_ = dirTreeView; + initDirTree(); + dirTreeView->setIconSize(iconSize_); + dirTreeView->setCurrentPath(currentPath_); + connect(dirTreeView, &DirTreeView::chdirRequested, this, &SidePane::chdirRequested); + connect(dirTreeView, &DirTreeView::openFolderInNewWindowRequested, + this, &SidePane::openFolderInNewWindowRequested); + connect(dirTreeView, &DirTreeView::openFolderInNewTabRequested, + this, &SidePane::openFolderInNewTabRequested); + connect(dirTreeView, &DirTreeView::openFolderInTerminalRequested, + this, &SidePane::openFolderInTerminalRequested); + connect(dirTreeView, &DirTreeView::createNewFolderRequested, + this, &SidePane::createNewFolderRequested); + connect(dirTreeView, &DirTreeView::prepareFileMenu, + this, &SidePane::prepareFileMenu); + break; + } + default: + ; + } + if(view_) { + // if(sp->update_popup) + // g_signal_connect(sp->view, "item-popup", G_CALLBACK(on_item_popup), sp); + verticalLayout->addWidget(view_); + } + Q_EMIT modeChanged(mode); +} + +void SidePane::setShowHidden(bool show_hidden) { + if(view_ == nullptr || show_hidden == showHidden_) { + return; + } + showHidden_ = show_hidden; + if(mode_ == ModeDirTree) { + DirTreeView* dirTreeView = static_cast(view_); + DirTreeModel* model = static_cast(dirTreeView->model()); + if(model) { + model->setShowHidden(showHidden_); + } + } +} + +void SidePane::restoreHiddenPlaces(const QSet& items) { + if(mode_ == ModePlaces) { + static_cast(view_)->restoreHiddenItems(items); + } + else { + restorableHiddenPlaces_.unite(items); + } +} + +} // namespace Fm diff --git a/src/sidepane.h b/src/sidepane.h new file mode 100644 index 0000000..9ae7935 --- /dev/null +++ b/src/sidepane.h @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#ifndef FM_SIDEPANE_H +#define FM_SIDEPANE_H + +#include "libfmqtglobals.h" +#include + +#include "core/filepath.h" + +class QComboBox; +class QVBoxLayout; +class QWidget; + +namespace Fm { + +class FileMenu; + +class LIBFM_QT_API SidePane : public QWidget { + Q_OBJECT + +public: + enum Mode { + ModeNone = -1, + ModePlaces = 0, + ModeDirTree, + NumModes + }; + +public: + explicit SidePane(QWidget* parent = 0); + virtual ~SidePane(); + + QSize iconSize() const { + return iconSize_; + } + + void setIconSize(QSize size); + + const Fm::FilePath& currentPath() const { + return currentPath_; + } + + void setCurrentPath(Fm::FilePath path); + + void setMode(Mode mode); + + Mode mode() const { + return mode_; + } + + QWidget* view() const { + return view_; + } + + static const char* modeName(Mode mode); + + static Mode modeByName(const char* str); + +#if 0 // FIXME: are these APIs from libfm-qt needed? + int modeCount(void) { + return NumModes; + } + + QString modeLabel(Mode mode); + + QString modeTooltip(Mode mode); +#endif + + void setShowHidden(bool show_hidden); + + bool showHidden() const { + return showHidden_; + } + + bool setHomeDir(const char* home_dir); + + void chdir(Fm::FilePath path) { + setCurrentPath(std::move(path)); + } + + void restoreHiddenPlaces(const QSet& items); + +Q_SIGNALS: + void chdirRequested(int type, const Fm::FilePath& path); + void openFolderInNewWindowRequested(const Fm::FilePath& path); + void openFolderInNewTabRequested(const Fm::FilePath& path); + void openFolderInTerminalRequested(const Fm::FilePath& path); + void createNewFolderRequested(const Fm::FilePath& path); + void modeChanged(Fm::SidePane::Mode mode); + + void prepareFileMenu(Fm::FileMenu* menu); // emit before showing a Fm::FileMenu + + void hiddenPlaceSet(const QString& str, bool hide); + +protected Q_SLOTS: + void onComboCurrentIndexChanged(int current); + +private: + void initDirTree(); + +private: + Fm::FilePath currentPath_; + QWidget* view_; + QComboBox* combo_; + QVBoxLayout* verticalLayout; + QSize iconSize_; + Mode mode_; + bool showHidden_; + QSet restorableHiddenPlaces_; +}; + +} + +#endif // FM_SIDEPANE_H diff --git a/src/tests/test-filedialog.cpp b/src/tests/test-filedialog.cpp new file mode 100644 index 0000000..ccd1809 --- /dev/null +++ b/src/tests/test-filedialog.cpp @@ -0,0 +1,48 @@ +#include +#include +#include +#include +#include "../core/folder.h" +#include "../foldermodel.h" +#include "../folderview.h" +#include "../cachedfoldermodel.h" +#include "../proxyfoldermodel.h" +#include "../pathedit.h" +#include "../filedialog.h" +#include "libfmqt.h" + + +int main(int argc, char** argv) { + QApplication app(argc, argv); + + Fm::LibFmQt contex; + + /* + QFileDialog dlg0; + dlg0.setFileMode(QFileDialog::ExistingFiles); + + dlg0.setNameFilters(QStringList() << "Txt (*.txt)"); + QObject::connect(&dlg0, &QFileDialog::currentChanged, [](const QString& path) { + qDebug() << "currentChanged:" << path; + }); + QObject::connect(&dlg0, &QFileDialog::fileSelected, [](const QString& path) { + qDebug() << "fileSelected:" << path; + }); + QObject::connect(&dlg0, &QFileDialog::filesSelected, [](const QStringList& paths) { + qDebug() << "filesSelected:" << paths; + }); + + dlg0.exec(); + */ + + Fm::FileDialog dlg; + // dlg.setFileMode(QFileDialog::ExistingFile); + dlg.setFileMode(QFileDialog::ExistingFiles); + // dlg.setFileMode(QFileDialog::Directory); + dlg.setNameFilters(QStringList() << "All (*)" << "Text (*.txt)" << "Images (*.gif *.jpeg *.jpg)"); + + dlg.exec(); + qDebug() << "selected files:" << dlg.selectedFiles(); + + return 0; +} diff --git a/src/tests/test-folder.cpp b/src/tests/test-folder.cpp new file mode 100644 index 0000000..45af775 --- /dev/null +++ b/src/tests/test-folder.cpp @@ -0,0 +1,44 @@ +#include +#include +#include "../core/folder.h" + +int main(int argc, char** argv) { + QApplication app(argc, argv); + + auto home = Fm::FilePath::homeDir(); + auto folder = Fm::Folder::fromPath(home); + + QObject::connect(folder.get(), &Fm::Folder::startLoading, [=]() { + qDebug("start loading"); + }); + QObject::connect(folder.get(), &Fm::Folder::finishLoading, [=]() { + qDebug("finish loading"); + }); + + QObject::connect(folder.get(), &Fm::Folder::filesAdded, [=](Fm::FileInfoList& files) { + qDebug("files added"); + for(auto& item: files) { + qDebug() << item->displayName(); + } + }); + QObject::connect(folder.get(), &Fm::Folder::filesRemoved, [=](Fm::FileInfoList& files) { + qDebug("files removed"); + for(auto& item: files) { + qDebug() << item->displayName(); + } + }); + QObject::connect(folder.get(), &Fm::Folder::filesChanged, [=](std::vector& file_pairs) { + qDebug("files changed"); + for(auto& pair: file_pairs) { + auto& item = pair.second; + qDebug() << item->displayName(); + } + }); + + for(auto& item: folder->files()) { + qDebug() << item->displayName(); + } + qDebug() << "here"; + + return app.exec(); +} diff --git a/src/tests/test-folderview.cpp b/src/tests/test-folderview.cpp new file mode 100644 index 0000000..427f1e5 --- /dev/null +++ b/src/tests/test-folderview.cpp @@ -0,0 +1,48 @@ +#include +#include +#include +#include +#include "../core/folder.h" +#include "../foldermodel.h" +#include "../folderview.h" +#include "../cachedfoldermodel.h" +#include "../proxyfoldermodel.h" +#include "../pathedit.h" +#include "libfmqt.h" + +int main(int argc, char** argv) { + QApplication app(argc, argv); + + Fm::LibFmQt contex; + QMainWindow win; + + Fm::FolderView folder_view; + win.setCentralWidget(&folder_view); + + auto home = Fm::FilePath::homeDir(); + Fm::CachedFolderModel* model = Fm::CachedFolderModel::modelFromPath(home); + auto proxy_model = new Fm::ProxyFolderModel(); + proxy_model->sort(Fm::FolderModel::ColumnFileName, Qt::AscendingOrder); + proxy_model->setSourceModel(model); + + proxy_model->setThumbnailSize(64); + proxy_model->setShowThumbnails(true); + + folder_view.setModel(proxy_model); + + QToolBar toolbar; + win.addToolBar(Qt::TopToolBarArea, &toolbar); + Fm::PathEdit edit; + edit.setText(home.toString().get()); + toolbar.addWidget(&edit); + auto action = new QAction("Go", nullptr); + toolbar.addAction(action); + QObject::connect(action, &QAction::triggered, [&]() { + auto path = Fm::FilePath::fromPathStr(edit.text().toLocal8Bit().constData()); + auto new_model = Fm::CachedFolderModel::modelFromPath(path); + proxy_model->setSourceModel(new_model); + }); + + win.show(); + return app.exec(); +} diff --git a/src/tests/test-placesview.cpp b/src/tests/test-placesview.cpp new file mode 100644 index 0000000..ddfe213 --- /dev/null +++ b/src/tests/test-placesview.cpp @@ -0,0 +1,20 @@ +#include +#include +#include +#include +#include +#include "../placesview.h" +#include "libfmqt.h" + +int main(int argc, char** argv) { + QApplication app(argc, argv); + + Fm::LibFmQt contex; + QMainWindow win; + + Fm::PlacesView view; + win.setCentralWidget(&view); + + win.show(); + return app.exec(); +} diff --git a/src/tests/test-volumemanager.cpp b/src/tests/test-volumemanager.cpp new file mode 100644 index 0000000..7b6438a --- /dev/null +++ b/src/tests/test-volumemanager.cpp @@ -0,0 +1,24 @@ +#include +#include +#include +#include "../core/volumemanager.h" + +int main(int argc, char** argv) { + QApplication app(argc, argv); + + auto vm = Fm::VolumeManager::globalInstance(); + + QObject::connect(vm.get(), &Fm::VolumeManager::volumeAdded, [=](const Fm::Volume& vol) { + qDebug() << "volume added:" << vol.name().get(); + }); + QObject::connect(vm.get(), &Fm::VolumeManager::volumeRemoved, [=](const Fm::Volume& vol) { + qDebug() << "volume removed:" << vol.name().get(); + }); + + for(auto& item: vm->volumes()) { + auto name = item.name(); + qDebug() << "list volume:" << name.get(); + } + + return app.exec(); +} diff --git a/src/translations/CMakeLists.txt b/src/translations/CMakeLists.txt new file mode 100644 index 0000000..fea1753 --- /dev/null +++ b/src/translations/CMakeLists.txt @@ -0,0 +1,3 @@ +project(libfm-qt) + +build_component("." "${CMAKE_INSTALL_FULL_DATADIR}/libfm-qt/translations") diff --git a/src/translations/libfm-qt.ts b/src/translations/libfm-qt.ts new file mode 100644 index 0000000..f079181 --- /dev/null +++ b/src/translations/libfm-qt.ts @@ -0,0 +1,1547 @@ + + + + + AppChooserDialog + + + Choose an Application + + + + + Installed Applications + + + + + Custom Command + + + + + Command line to execute: + + + + + Application name: + + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + + + + + Keep terminal window open after command execution + + + + + Execute in terminal emulator + + + + + Set selected application as default action of this file type + + + + + EditBookmarksDialog + + + Edit Bookmarks + + + + + Name + + + + + Location + + + + + &Add Item + + + + + &Remove Item + + + + + Use drag and drop to reorder the items + + + + + ExecFileDialog + + + Execute file + + + + + &Open + + + + + E&xecute + + + + + Execute in &Terminal + + + + + Cancel + + + + + FileDialog + + + Location: + + + + + File name: + + + + + File type: + + + + + FileOperationDialog + + + Destination: + + + + + Processing: + + + + + Preparing... + + + + + Progress + + + + + Time remaining: + + + + + Files processed: + + + + + FilePropsDialog + + + File Properties + + + + + General + + + + + Location: + + + + + File type: + + + + + MIME type: + + + + + File size: + + + + + On-disk size: + + + + + Last modified: + + + + + Link target: + + + + + Open With: + + + + + Last accessed: + + + + + Contains: + + + + + Device Usage: + + + + + Permissions + + + + + Ownership + + + + + + + Group: + + + + + + + Owner: + + + + + Access Control + + + + + + Other: + + + + + Make the file executable + + + + + + + Read + + + + + + + Write + + + + + + + Execute + + + + + Sticky + + + + + SetUID + + + + + SetGID + + + + + Advanced Mode + + + + + Fm::AppChooserComboBox + + + Customize + + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + + + + + Fm::CreateNewMenu + + + Folder + + + + + Blank File + + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + + + + + Fm::DirTreeModel + + + Loading... + + + + + + + <No sub folders> + + + + + Fm::DirTreeView + + + Open in New T&ab + + + + + Open in New Win&dow + + + + + Open in Termina&l + + + + + Fm::DndActionMenu + + + Copy here + + + + + Move here + + + + + Create symlink here + + + + + Cancel + + + + + Fm::EditBookmarksDialog + + + New bookmark + + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + + + + + This file '%1' is executable. Do you want to execute it? + + + + + Fm::FileDialog + + + Go Back + + + + + Alt+Left + Go Back + + + + + Go Forward + + + + + Alt+Right + Go Forward + + + + + Reload + + + + + F5 + Reload + + + + + Create Folder + + + + + Icon View + + + + + Thumbnail View + + + + + Compact View + + + + + Detailed List View + + + + + + Error + + + + + Please select a file + + + + + %1 already exists. +Do you want to replace it? + + + + + Path "%1" does not exist + + + + + "%1" is not a directory + + + + + "%1" is not a file + + + + + + &Open + + + + + + &Save + + + + + All Files (*) + + + + + Fm::FileDialogHelper + + + Open File + + + + + Save File + + + + + Fm::FileMenu + + + Open + + + + + Open With... + + + + + Other Applications + + + + + Create &New + + + + + &Restore + + + + + Cut + + + + + Copy + + + + + Paste + + + + + + &Move to Trash + + + + + Rename + + + + + Extract to... + + + + + Extract Here + + + + + Compress + + + + + Properties + + + + + Trust selected executables + + + + + Trust this executable + + + + + Output + + + + + &Delete + + + + + Fm::FileOperation + + + Error + + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + + + + + + Confirm + + + + + Do you want to delete the selected files? + + + + + Do you want to move the selected files to trash can? + + + + + Fm::FileOperationDialog + + + Move files + + + + + Moving the following files to destination folder: + + + + + Copy Files + + + + + Copying the following files to destination folder: + + + + + Trash Files + + + + + Moving the following files to trash can: + + + + + Delete Files + + + + + Deleting the following files: + + + + + Create Symlinks + + + + + Creating symlinks for the following files: + + + + + Change Attributes + + + + + Changing attributes of the following files: + + + + + Restore Trashed Files + + + + + Restoring the following files from trash can: + + + + + + Error + + + + + Fm::FilePropsDialog + + + View folder content + + + + + View and modify folder content + + + + + Read + + + + + Read and write + + + + + Forbidden + + + + + Files of different types + + + + + Multiple Files + + + + + %p% used + + + + + %1 Free of %2 + + + + + no file + + + + + one file + + + + + %1 files + + + + + Select an icon + + + + + Images (*.png *.xpm *.svg *.svgz ) + + + + + Apply changes + + + + + Do you want to recursively apply these changes to all files and sub-folders? + + + + + Fm::FileSearchDialog + + + Error + + + + + You should add at least one directory to search. + + + + + Select a folder + + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + + + + + Fm::FolderMenu + + + Create &New + + + + + &Paste + + + + + Select &All + + + + + Invert Selection + + + + + Sorting + + + + + Show Hidden + + + + + Folder Pr&operties + + + + + Output + + + + + By File Name + + + + + By Modification Time + + + + + By File Size + + + + + By File Type + + + + + By File Owner + + + + + Ascending + + + + + Descending + + + + + Folder First + + + + + Case Sensitive + + + + + Fm::FolderModel + + + Name + + + + + Type + + + + + Size + + + + + Modified + + + + + Owner + + + + + Group + + + + + Fm::FontButton + + + Bold + + + + + Italic + + + + + Fm::MountOperationPasswordDialog + + + &Connect + + + + + Fm::PathBar + + + &Edit Path + + + + + &Copy Path + + + + + Fm::PlacesModel + + + Places + + + + + Desktop + + + + + Computer + + + + + Applications + + + + + Network + + + + + Devices + + + + + Bookmarks + + + + + Trash + + + + + Fm::PlacesView + + + Open in New Tab + + + + + Open in New Window + + + + + Empty Trash + + + + + + Hide + + + + + Move Bookmark Up + + + + + Move Bookmark Down + + + + + Rename Bookmark + + + + + Remove Bookmark + + + + + + Unmount + + + + + Mount + + + + + Eject + + + + + Show All Entries + + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + + + + + + Type: %1 +Modified: %2 + + + + + &Overwrite + + + + + &Rename + + + + + Fm::SidePane + + + Places + + + + + Directory Tree + + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + + + + + MountOperationPasswordDialog + + + Mount + + + + + Connect &anonymously + + + + + Connect as u&ser: + + + + + &Username: + + + + + &Password: + + + + + &Domain: + + + + + Forget password &immediately + + + + + Remember password until you &logout + + + + + Remember &forever + + + + + QObject + + + Rename File + + + + + Please enter a new name: + + + + + + + + + Error + + + + + Create Folder + + + + + Create File + + + + + Please enter a new file name: + + + + + New text file + + + + + Please enter a new folder name: + + + + + New folder + + + + + Enter a name for the new %1: + + + + + Custom Icon Error + + + + + The path is not mounted. + + + + + Invalid desktop entry file: '%1' + + + + + No default application is set to launch '%1' + + + + + Cannot set working directory to '%1': %2 + + + + + Identifier: + + + + + RenameDialog + + + Confirm to replace files + + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + + + + + dest + + + + + with the following file? + + + + + src file info + + + + + dest file info + + + + + src + + + + + &File name: + + + + + Apply this option to all existing files + + + + + SearchDialog + + + Search Files + + + + + Name/Location + + + + + File Name Patterns: + + + + + * + + + + + Case insensitive + + + + + Use regular expression + + + + + Places to Search: + + + + + &Add + + + + + &Remove + + + + + Search in sub directories + + + + + Search for hidden files + + + + + File Type + + + + + Only search for files of following types: + + + + + Text files + + + + + Image files + + + + + Audio files + + + + + Video files + + + + + Documents + + + + + Folders + + + + + Content + + + + + File contains: + + + + + Case insensiti&ve + + + + + &Use regular expression + + + + + Properties + + + + + File Size: + + + + + Larger than: + + + + + + Bytes + + + + + + KiB + + + + + + MiB + + + + + + GiB + + + + + Smaller than: + + + + + Last Modified Time: + + + + + Earlier than: + + + + + Later than: + + + + diff --git a/src/translations/libfm-qt_ar.ts b/src/translations/libfm-qt_ar.ts new file mode 100644 index 0000000..b93b84c --- /dev/null +++ b/src/translations/libfm-qt_ar.ts @@ -0,0 +1,1560 @@ + + + + + AppChooserDialog + + + Choose an Application + اختر تطبيقا + + + + Installed Applications + التطبيقات المثبّتة + + + + Custom Command + أمر مخصّص + + + + Command line to execute: + سطر الأوامر لتنفيذه: + + + + Application name: + اسم التطبيق: + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + <b>يمكن استخدام هذه العلامات الخاصّة في سطر الأوامر:</b> +<ul> +<li><b>%f</b>: يشير إلى اسم ملف واحد</li> +<li><b>%F</b>: يشير إلى أسماء عدّة ملفات</li> +<li><b>%u</b>: يشير إلى مسار واحد للملف</li> +<li><b>%U</b>: يشير إلى عدّة مسارات</li> +</ul> + + + + Keep terminal window open after command execution + أبقِ نافذة الطرفية مفتوحة بعد تنفيذ الأمر + + + + Execute in terminal emulator + نفّذ في محاكي طرفية + + + + Set selected application as default action of this file type + اضبط التطبيق المنتقى ليكون الإجراء المبدئي لنوع الملفات هذا + + + + EditBookmarksDialog + + + Edit Bookmarks + حرّر العلامات + + + + Name + الاسم + + + + Location + المكان + + + + &Add Item + Ø£&ضِف عنصرا + + + + &Remove Item + Ø£&زِل العنصر + + + + Use drag and drop to reorder the items + استخدم السحب والإفلات لإعادة ترتيب العناصر + + + + ExecFileDialog + + + Execute file + نفّذ الملف + + + + &Open + ا&فتح + + + + E&xecute + &نفّذ + + + + Execute in &Terminal + نفّذ في ال&طرفية + + + + Cancel + ألغِ + + + + FileDialog + + + Location: + المكان: + + + + File name: + اسم الملف: + + + + File type: + نوع الملف: + + + + FileOperationDialog + + + Destination: + المقصد: + + + + Processing: + يعالج: + + + + Preparing... + يحضّر… + + + + Progress + التقدّم + + + + Time remaining: + الوقت المتبقي: + + + + Files processed: + الملفات المعالجة: + + + + FilePropsDialog + + + File Properties + خصائص الملف + + + + General + عام + + + + Location: + المكان: + + + + File type: + نوع الملف: + + + + MIME type: + نوع MIME: + + + + File size: + حجم الملف: + + + + On-disk size: + الحجم على القرص: + + + + Last modified: + آخر تعديل: + + + + Link target: + هدف الرابط: + + + + Open With: + افتح في: + + + + Last accessed: + آخر نفاذ: + + + + Contains: + يحتوي على: + + + + Device Usage: + استخدام الجهاز: + + + + Permissions + التصاريح + + + + Ownership + الملكية + + + + + + Group: + المجموعة: + + + + + + Owner: + المالك: + + + + Access Control + التحكم بالنفاذ + + + + + Other: + الغير: + + + + Make the file executable + اجعل الملف تنفيذيا + + + + + + Read + القراءة + + + + + + Write + الكتابة + + + + + + Execute + التنفيذ + + + + Sticky + ملتصق + + + + SetUID + ضبط معرّف المستخدم + + + + SetGID + ضبط معرّف المجموعة + + + + Advanced Mode + الوضع المتقدم + + + + Fm::AppChooserComboBox + + + Customize + خصّص + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + اختر تطبيقا لفتح ملفات ”%1“ + + + + Fm::CreateNewMenu + + + Folder + مجلد + + + + Blank File + ملف فارغ + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + الدليل المحدد ”%1“ ليس صالحا + + + + Fm::DirTreeModel + + + Loading... + يحمّل… + + + + + + <No sub folders> + <لا مجلدات فرعية> + + + + Fm::DirTreeView + + + Open in New T&ab + افتح في ل&سان جديد + + + + Open in New Win&dow + افتح في &نافذة جديدة + + + + Open in Termina&l + افتح في ال&طرفية + + + + Fm::DndActionMenu + + + Copy here + انسخ هنا + + + + Move here + انقل هنا + + + + Create symlink here + أنشِئ رابطا رمزيا هنا + + + + Cancel + ألغِ + + + + Fm::EditBookmarksDialog + + + New bookmark + علامة جديدة + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + يبدو الملف ”%1“ مدخلة سطح مكتب. +ما الذي تريد فعله معه؟ + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + يبدو الملف النصي ”%1“ سكربتا تنفيذيا. +ما الذي تريد فعله معه؟ + + + + This file '%1' is executable. Do you want to execute it? + الملف ”%1“ تنفيذي. أتريد تنفيذه؟ + + + + Fm::FileDialog + + + Go Back + عُد + + + + Alt+Left + Go Back + Alt+Left + + + + Go Forward + تقدّم + + + + Alt+Right + Go Forward + Alt+Right + + + + Reload + أعِد التحميل + + + + F5 + Reload + F5 + + + + Create Folder + أنشِئ مجلدا + + + + Icon View + منظور أيقوناتي + + + + Thumbnail View + منظور مصغّراتي + + + + Compact View + منظور متضام + + + + Detailed List View + منظور قائمة مفصّلة + + + + + Error + خطأ + + + + Please select a file + رجاءً اختر ملفا + + + + %1 already exists. +Do you want to replace it? + ‏⁨%1⁩ موجود بالفعل. +أتريد استبداله؟ + + + + Path "%1" does not exist + المسار ”%1“ غير موجود + + + + "%1" is not a directory + ‏”⁨%1⁩“ ليس دليلا + + + + "%1" is not a file + ‏”⁨%1⁩“ ليس ملفا + + + + + &Open + ا&فتح + + + + + &Save + ا&حفظ + + + + All Files (*) + كل الملفات (*) + + + + Fm::FileDialogHelper + + + Open File + فتح الملف + + + + Save File + حفظ الملف + + + + Fm::FileMenu + + + Open + افتح + + + + Cut + قصّ + + + + Copy + انسخ + + + + Paste + ألصِق + + + + + &Move to Trash + ا&نقل إلى المهملات + + + + Trust selected executables + الوثوق بالملفات التنفيذية المحددة + + + + Trust this executable + الوثوق بهذا الملف التنفيذي المحدد + + + + Output + الخرج + + + + &Delete + ا&حذف + + + + Rename + غيّر الاسم + + + + Open With... + افتح في… + + + + Other Applications + تطبيق آخر + + + + Create &New + أنشِئ &جديدا + + + + &Restore + ا&ستعد + + + + Extract to... + استخرج إلى… + + + + Extract Here + استخرج هنا + + + + Compress + اضغط + + + + Properties + خصائص + + + + Fm::FileOperation + + + Error + خطأ + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + تعذر نقل بعض الملفات إلى المهملات لأن أنظمة الملفات التحتية لا تدعم ذلك. +أتريد حذفها بدل نقلها؟ + + + + + Confirm + تأكيد + + + + Do you want to delete the selected files? + أتريد حذف الملفات المحددة؟ + + + + Do you want to move the selected files to trash can? + أتريد نقل الملفات المحددة إلى سلة المهملات؟ + + + + Fm::FileOperationDialog + + + Move files + نقل الملفات + + + + Moving the following files to destination folder: + ينقل الملفات الآتية إلى المجلد المقصد: + + + + Copy Files + نسخ الملفات + + + + Copying the following files to destination folder: + ينسخ الملفات الآتية إلى المجلد المقصد: + + + + Trash Files + رمي الملفات + + + + Moving the following files to trash can: + ينقل الملفات الآتية إلى سلة المهملات: + + + + Delete Files + حذف الملفات + + + + Deleting the following files: + يحذف الملفات الآتية: + + + + Create Symlinks + إنشاء الروابط الرمزية + + + + Creating symlinks for the following files: + ينشئ روابط رمزية للملفات الآتية: + + + + Change Attributes + تغيير الصفات + + + + Changing attributes of the following files: + يغيّر صفات الملفات الآتية: + + + + Restore Trashed Files + استعادة الملفات المرمية + + + + Restoring the following files from trash can: + يستعيد الملفات الآتية من سلة المهملات: + + + + + Error + خطأ + + + + Fm::FilePropsDialog + + + View folder content + عرض محتوى المجلد + + + + View and modify folder content + عرض محتوى المجلد وتعديله + + + + Read + القراءة + + + + Read and write + القراءة والكتابة + + + + Forbidden + ممنوع + + + + Files of different types + ملفات أنواعها مختلفة + + + + Multiple Files + عدد من الملفات + + + + %p% used + %p% مستخدم + + + + %1 Free of %2 + %1% متاح من %2 + + + + no file + لا ملف + + + + one file + ملف واحد + + + + %1 files + %1 ملفات + + + + Select an icon + اختر أيقونة + + + + Images (*.png *.xpm *.svg *.svgz ) + الصور (*.png *.xpm *.svg *.svgz ) + + + + Apply changes + تطبيق التعديلات + + + + Do you want to recursively apply these changes to all files and sub-folders? + أتريد تطبيق هذه التعديلات تكراريا لكل الملفات والمجلدات الفرعية؟ + + + + Fm::FileSearchDialog + + + Error + خطأ + + + + You should add at least one directory to search. + عليك إضافة دليل واحد على الأقل للبحث فيه. + + + + Select a folder + اختر مجلدا + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + لا يمكن إنشاء رابط في نظام ملفات غير أصيل + + + + Fm::FolderMenu + + + Create &New + أنشِئ &جديدا + + + + &Paste + أل&صِق + + + + Select &All + حدّد ال&كل + + + + Invert Selection + اعكس التحديد + + + + Sorting + الفرز + + + + Show Hidden + اعرض المخفي + + + + Folder Pr&operties + &خصائص المجلد + + + + Output + الخرج + + + + By File Name + باسم الملف + + + + By Modification Time + بوقت التعديل + + + + By File Size + بحجم الملف + + + + By File Type + بنوع الملف + + + + By File Owner + بمالك الملف + + + + Ascending + تصاعديا + + + + Descending + تنازليا + + + + Folder First + المجلدات أولا + + + + Case Sensitive + حساس للحالة + + + + Fm::FolderModel + + + Name + الاسم + + + + Type + النوع + + + + Size + الحجم + + + + Modified + التعديل + + + + Owner + المالك + + + + Group + المجموعة + + + + Fm::FontButton + + + Bold + ثخين + + + + Italic + مائل + + + + Fm::MountOperationPasswordDialog + + + &Connect + اتّ&صل + + + + Fm::PathBar + + + &Edit Path + &حرّر المسار + + + + &Copy Path + ا&نسخ المسار + + + + Fm::PlacesModel + + + Places + الأماكن + + + + Desktop + سطح المكتب + + + + Trash + المهملات + + + + Computer + الحاسوب + + + + Applications + التطبيقات + + + + Network + الشبكة + + + + Devices + الأجهزة + + + + Bookmarks + العلامات + + + + Fm::PlacesView + + + Empty Trash + أفرِغ المهملات + + + + Open in New Tab + افتح في لسان جديد + + + + Open in New Window + افتح في نافذة جديدة + + + + + Hide + أخفِ + + + + Move Bookmark Up + انقل العلامة لأعلى + + + + Move Bookmark Down + انقل العلامة لأسفل + + + + Rename Bookmark + غيّر اسم العلامة + + + + Remove Bookmark + أزِل العلامة + + + + + Unmount + ألغِ الضمّ + + + + Mount + ضُمّ + + + + Eject + أخرِج + + + + Show All Entries + اعرض كل المدخلات + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + النوع: %1 +الحجم: %2 +التعديل: %3 + + + + + Type: %1 +Modified: %2 + النوع: %1 +التعديل: %2 + + + + &Overwrite + ا&كتب فوقه + + + + &Rename + &غيّر اسمه + + + + Fm::SidePane + + + Places + الأماكن + + + + Directory Tree + شجرة الأدلة + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + تعذر إلغاء رمي الملف ”%s“: المسار الأصلي مجهول + + + + MountOperationPasswordDialog + + + Mount + ضُمّ + + + + Connect &anonymously + اتّصل كم&جهول + + + + Connect as u&ser: + اتّصل كالمست&خدم: + + + + &Username: + ا&سم المستخدم: + + + + &Password: + &كلمة السر: + + + + &Domain: + الن&طاق: + + + + Forget password &immediately + انسَ كلمة السر مبا&شرة + + + + Remember password until you &logout + تذكّر كلمة السر حتى Ø£&خرج + + + + Remember &forever + تذكّر للأب&د + + + + QObject + + + + + + + Error + خطأ + + + + Rename File + تغيير اسم الملف + + + + Please enter a new name: + رجاءً أدخِل الاسم الجديد: + + + + Create Folder + إنشاء مجلد + + + + Please enter a new file name: + رجاءً أدخِل اسم الملف الجديد: + + + + New text file + ملف نصي جديد + + + + Please enter a new folder name: + رجاءً أدخِل اسم المجلد الجديد: + + + + New folder + مجلد جديد + + + + Enter a name for the new %1: + رجاءً أدخِل اسما لِ‍ ”%1“ الجديد: + + + + Create File + إنشاء ملف + + + + Custom Icon Error + خطأ في الأيقونة المخصصة + + + + The path is not mounted. + المسار ليس مضموما. + + + + Invalid desktop entry file: '%1' + ملف مدخلة سطح المكتب غير صالح: ”%1“ + + + + No default application is set to launch '%1' + لم يُضبط أي تطبيق مبدئي ليُطلق ”%1“ + + + + Cannot set working directory to '%1': %2 + تعذر ضبط دليل العمل ليكون ”%1“: ‏”%2“ + + + + Identifier: + معرف: + + + + RenameDialog + + + Confirm to replace files + تأكيد استبدال الملفات + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + <html><head/><body><p><span style=" font-weight:600;">ثمة ملف بنفس الاسم في هذا المكان بالفعل.</span></p><p>أتريد استبدال الملف الموجود؟</p></body></html> + + + + dest + المقصد + + + + with the following file? + بالملف الآتي؟ + + + + src file info + معلومات الملف المصدر + + + + dest file info + معلومات الملف المقصد + + + + src + المصدر + + + + &File name: + اسم المل&ف: + + + + Apply this option to all existing files + طبّق هذا الخيار على كل الملفات الموجودة + + + + SearchDialog + + + Search Files + بحث عن ملفات + + + + Name/Location + الاسم/المكان + + + + File Name Patterns: + أنماط لأسماء الملفات: + + + + * + * + + + + Case insensitive + غير حسّاس للحالة + + + + Use regular expression + استخدم التعابير النمطية + + + + Places to Search: + أماكن البحث: + + + + &Add + Ø£&ضِف + + + + &Remove + Ø£&زِل + + + + Search in sub directories + ابحث في المجلدات الفرعية + + + + Search for hidden files + ابحث عن الملفات المخفية + + + + File Type + نوع الملف + + + + Only search for files of following types: + ابحث عن الملفات بالأنواع الآتية فقط: + + + + Text files + الملفات النصية + + + + Image files + ملفات الصور + + + + Audio files + ملفات الصوت + + + + Video files + ملفات الڤديو + + + + Documents + المستندات + + + + Folders + المجلدات + + + + Content + المحتوى + + + + File contains: + في داخل الملف: + + + + Case insensiti&ve + &غير حسّاس للحالة + + + + &Use regular expression + ا&ستخدم التعابير النمطية + + + + Properties + الخصائص + + + + File Size: + حجم الملف: + + + + Larger than: + أكبر من: + + + + + Bytes + بايت + + + + + KiB + ك.بايت + + + + + MiB + م.بايت + + + + + GiB + غ.بايت + + + + Smaller than: + أصغر من: + + + + Last Modified Time: + آخر وقت تعديل: + + + + Earlier than: + كان قبل: + + + + Later than: + كان بعد: + + + diff --git a/src/translations/libfm-qt_ca.ts b/src/translations/libfm-qt_ca.ts new file mode 100644 index 0000000..8415977 --- /dev/null +++ b/src/translations/libfm-qt_ca.ts @@ -0,0 +1,1561 @@ + + + + + AppChooserDialog + + + Choose an Application + Trieu una aplicació + + + + Installed Applications + Aplicacions instal·lades + + + + Custom Command + Ordre personalitzada + + + + Command line to execute: + Línia d'ordres a executar: + + + + Application name: + Nom de l'aplicació: + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + <b>En la línia d'ordres es poden utilitzar aquests codis especials:</b> +<ul> +<li><b>%f</b>: Representa un sol nom de fitxer</li> +<li><b>%F</b>: Representa múltiples noms de fitxers</li> +<li><b>%u</b>: Representa una sola URI del fitxer</li> +<li><b>%U</b>: Representa múltiples URI</li> +</ul> + + + + Keep terminal window open after command execution + Mantén oberta la finestra del terminal després de l'execució de l'ordre + + + + Execute in terminal emulator + Executa a l'emulador del terminal + + + + Set selected application as default action of this file type + Estableix l'aplicació seleccionada com a acció predeterminada d'aquest tipus de fitxer + + + + EditBookmarksDialog + + + Edit Bookmarks + Edició dels marcadors + + + + Name + Nom + + + + Location + Ubicació + + + + &Add Item + &Afegeix un ítem + + + + &Remove Item + Sup&rimeix l'ítem + + + + Use drag and drop to reorder the items + Utilitzeu arrossega i deixa anar per a reordenar els ítems + + + + ExecFileDialog + + + Execute file + Executa el fitxer + + + + &Open + &Obre + + + + E&xecute + E&xecuta + + + + Execute in &Terminal + Executa al &terminal + + + + Cancel + Cancel·la + + + + FileDialog + + + Location: + Ubicació: + + + + File name: + Nom de fitxer: + + + + File type: + Tipus de fitxer: + + + + FileOperationDialog + + + Destination: + Destinació: + + + + Processing: + Processament: + + + + Preparing... + S'està preparant... + + + + Progress + Progrés + + + + Time remaining: + Temps restant: + + + + Files processed: + Fitxers processats: + + + + FilePropsDialog + + + File Properties + Propietats del fitxer + + + + General + General + + + + Location: + Ubicació: + + + + File type: + Tipus de fitxer: + + + + MIME type: + Tipus MIME: + + + + File size: + Mida del fitxer: + + + + On-disk size: + Mida al disc: + + + + Last modified: + Última modificació: + + + + Link target: + Objectiu de l'enllaç: + + + + Open With: + Obre amb: + + + + Last accessed: + Últim accés: + + + + Contains: + Conté: + + + + Device Usage: + Ús del dispositiu: + + + + Permissions + Permisos + + + + Ownership + Propietat + + + + + + Group: + Grup: + + + + + + Owner: + Propietari: + + + + Access Control + Control d'accés + + + + + Other: + Altres: + + + + Make the file executable + Fes que el fitxer sigui executable + + + + + + Read + Lectura + + + + + + Write + Escriptura + + + + + + Execute + Execució + + + + Sticky + Fix + + + + SetUID + SetUID + + + + SetGID + SetGID + + + + Advanced Mode + Mode avançat + + + + Fm::AppChooserComboBox + + + Customize + Personalitza + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + Seleccioneu una aplicació per obrir els fitxers "%1" + + + + Fm::CreateNewMenu + + + Folder + Carpeta + + + + Blank File + Fitxer en blanc + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + El directori especificat «%1» no és vàlid + + + + Fm::DirTreeModel + + + Loading... + S'està carregant... + + + + + + <No sub folders> + <Sense subcarpetes> + + + + Fm::DirTreeView + + + Open in New T&ab + Obre en una pes&tanya nova + + + + Open in New Win&dow + Obre en una &finestra nova + + + + Open in Termina&l + Obre al termina&l + + + + Fm::DndActionMenu + + + Copy here + Copia aquí + + + + Move here + Mou aquí + + + + Create symlink here + Copia aquí l'enllaç simbòlic + + + + Cancel + Cancel·la + + + + Fm::EditBookmarksDialog + + + New bookmark + Nou marcador + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + El fitxer «%1» sembla que sigui una entrada d'escriptori. +Què en voleu fer? + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + Aquest fitxer de text «%1» pel que sembla és un script executable. +Què voleu fer amb ell? + + + + This file '%1' is executable. Do you want to execute it? + Aquest fitxer «%1» és executable. Voleu executar-lo? + + + + Fm::FileDialog + + + Go Back + Vés enrere + + + + Alt+Left + Go Back + Alt+Esquerra + + + + Go Forward + Vés endavant + + + + Alt+Right + Go Forward + Alt+Dreta + + + + Reload + Torna a carregar + + + + F5 + Reload + F5 + + + + Create Folder + Crea una carpeta + + + + Icon View + Vista d'icones + + + + Thumbnail View + Vista de miniatures + + + + Compact View + Vista compacta + + + + Detailed List View + Vista de llista detallada + + + + + Error + Error + + + + Please select a file + Seleccioneu un fitxer + + + + %1 already exists. +Do you want to replace it? + %1 ja existeix. +Voleu substituir-ho? + + + + Path "%1" does not exist + No existeix el camí «%1» + + + + "%1" is not a directory + «%1» no és un directori + + + + "%1" is not a file + «%1» no és un fitxer + + + + + &Open + &Obre + + + + + &Save + De&sa + + + + All Files (*) + Tots els fitxers (*) + + + + Fm::FileDialogHelper + + + Open File + Obre un fitxer + + + + Save File + Desa el fitxer + + + + Fm::FileMenu + + + Open + Obre + + + + Open With... + Obre amb... + + + + Other Applications + Altres aplicacions + + + + Create &New + Crea &nou + + + + &Restore + &Restaura + + + + Cut + Retalla + + + + Copy + Copia + + + + Paste + Enganxa + + + + + &Move to Trash + &Mou a la paperera + + + + Rename + Reanomena + + + + Extract to... + Extreu a... + + + + Extract Here + Extreu aquí + + + + Compress + Comprimeix + + + + Properties + Propietats + + + + Trust selected executables + Confia amb els executables seleccionats + + + + Trust this executable + Confia amb aquest executable + + + + Output + Sortida + + + + &Delete + &Suprimeix + + + + Fm::FileOperation + + + Error + Error + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + Alguns fitxers no es poden moure a la paperera perquè els sistemes de fitxers subjacents no són compatibles amb aquesta operació. +En lloc seu, voleu suprimir-los? + + + + + Confirm + Confirma + + + + Do you want to delete the selected files? + Voleu suprimir els fitxers seleccionats? + + + + Do you want to move the selected files to trash can? + Voleu moure els fitxers seleccionats a la paperera? + + + + Fm::FileOperationDialog + + + Move files + Mou els fitxers + + + + Moving the following files to destination folder: + S'estan movent els següents fitxers a la carpeta de destinació: + + + + Copy Files + Copia els fitxers + + + + Copying the following files to destination folder: + S'estan copiant els següents fitxers a la carpeta de destinació: + + + + Trash Files + Mou els fitxers a la paperera + + + + Moving the following files to trash can: + S'estan movent els següents fitxers a la paperera: + + + + Delete Files + Suprimeix els fitxers + + + + Deleting the following files: + S'estan suprimint els següents fitxers: + + + + Create Symlinks + Crea enllaços simbòlics + + + + Creating symlinks for the following files: + S'estan creant els enllaços simbòlics per als següents fitxers: + + + + Change Attributes + Canvia els atributs + + + + Changing attributes of the following files: + S'estan canviant els atributs per als següents fitxers: + + + + Restore Trashed Files + Restaura els fitxers de la paperera + + + + Restoring the following files from trash can: + S'estan restaurant els següents fitxers de la paperera: + + + + + Error + Error + + + + Fm::FilePropsDialog + + + View folder content + Visualització del contingut de la carpeta + + + + View and modify folder content + Visualització i modificació del contingut de la carpeta + + + + Read + Lectura + + + + Read and write + Lectura i escriptura + + + + Forbidden + Prohibit + + + + Files of different types + Fitxers de diferents tipus + + + + Multiple Files + Múltiples fitxers + + + + %p% used + %p% utilitzat + + + + %1 Free of %2 + %1 lliure de %2 + + + + no file + cap fitxer + + + + one file + un fitxer + + + + %1 files + %1 fitxers + + + + Select an icon + Seleccioneu una icona + + + + Images (*.png *.xpm *.svg *.svgz ) + Imatges (*.png *.xpm *.svg *.svgz ) + + + + Apply changes + Aplica els canvis + + + + Do you want to recursively apply these changes to all files and sub-folders? + Voleu aplicar recursivament aquests canvis a tots els fitxers i subcarpetes? + + + + Fm::FileSearchDialog + + + Error + Error + + + + You should add at least one directory to search. + Com a mínim heu de seleccionar una carpeta per cercar. + + + + Select a folder + Seleccioneu una carpeta + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + No es pot crear un enllaç en un sistema de fitxers no natiu + + + + Fm::FolderMenu + + + Create &New + Crea &nou + + + + &Paste + En&ganxa + + + + Select &All + Seleccion&a-ho tot + + + + Invert Selection + Inverteix la selecció + + + + Sorting + Ordenació + + + + Show Hidden + Mostra els ocults + + + + Folder Pr&operties + Pr&opietats de la carpeta + + + + Output + Sortida + + + + By File Name + Pel nom del fitxer + + + + By Modification Time + Per la data de modificació + + + + By File Size + Per la mida del fitxer + + + + By File Type + Pel tipus de fitxer + + + + By File Owner + Pel propietari del fitxer + + + + Ascending + Ascendent + + + + Descending + Descendent + + + + Folder First + Primer les carpetes + + + + Case Sensitive + Distinció entre majúscules i minúscules + + + + Fm::FolderModel + + + Name + Nom + + + + Type + Tipus + + + + Size + Mida + + + + Modified + Modificat + + + + Owner + Propietari + + + + Group + Grup + + + + Fm::FontButton + + + Bold + Negreta + + + + Italic + Cursiva + + + + Fm::MountOperationPasswordDialog + + + &Connect + &Connecta + + + + Fm::PathBar + + + &Edit Path + &Edita el camí + + + + &Copy Path + &Copia el camí + + + + Fm::PlacesModel + + + Places + Llocs + + + + Desktop + Escriptori + + + + Computer + Ordinador + + + + Applications + Aplicacions + + + + Network + Xarxa + + + + Devices + Dispositius + + + + Bookmarks + Marcadors + + + + Trash + Paperera + + + + Fm::PlacesView + + + Open in New Tab + Obre en una pestanya nova + + + + Open in New Window + Obre en una finestra nova + + + + Empty Trash + Buida la paperera + + + + + Hide + Oculta + + + + Move Bookmark Up + Mou amunt el marcador + + + + Move Bookmark Down + Mou avall el marcador + + + + Rename Bookmark + Reanomena el marcador + + + + Remove Bookmark + Suprimeix el marcador + + + + + Unmount + Desmunta + + + + Mount + Munta + + + + Eject + Expulsa + + + + Show All Entries + Mostra totes les entrades + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + Tipus: %1 +Mida: %2 +Modificat: %3 + + + + + Type: %1 +Modified: %2 + Tipus: %1 +Modificat: %2 + + + + &Overwrite + S&obreescriu + + + + &Rename + &Reanomena + + + + Fm::SidePane + + + Places + Llocs + + + + Directory Tree + Arbre de directoris + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + No es pot treure de la paperera el fitxer «%s»: camí original no conegut + + + + MountOperationPasswordDialog + + + Mount + Munta + + + + Connect &anonymously + Connecta &anònimament + + + + Connect as u&ser: + Connecta com a l'&usuari: + + + + &Username: + Nom d'&usuari: + + + + &Password: + &Contrasenya: + + + + &Domain: + &Domini: + &Domini: + + + + Forget password &immediately + Oblida &immediatament la contrasenya + + + + Remember password until you &logout + Recorda la contrasenya fins que no es tanqui &la sessió + + + + Remember &forever + Recorda-la sem&pre + + + + QObject + + + Rename File + Reanomena el fitxer + + + + Please enter a new name: + Introduïu un nom nou: + + + + + + + + Error + Error + + + + Create Folder + Crea una carpeta + + + + Create File + Crea un fitxer + + + + Please enter a new file name: + Introduïu el nom del fitxer nou: + + + + New text file + Fitxer de text nou + + + + Please enter a new folder name: + Introduïu el nom de la carpeta nova: + + + + New folder + Carpeta nova + + + + Enter a name for the new %1: + Introduïu el nom per al nou %1: + + + + Custom Icon Error + Error de la icona personalitzada + + + + The path is not mounted. + El camí no està muntat. + + + + Invalid desktop entry file: '%1' + Fitxer d'entrada d'escriptori no vàlid: «%1» + + + + No default application is set to launch '%1' + No s'ha establert cap aplicació predeterminada per obrir «%1» + + + + Cannot set working directory to '%1': %2 + No es pot establir el directori de treball a «%1»: %2 + + + + Identifier: + Identificador: + + + + RenameDialog + + + Confirm to replace files + Confirmació per substituir els fitxers + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + <html><head/><body><p><span style=" font-weight:600;">Ja hi ha un fitxer amb el mateix nom en aquesta ubicació.</span></p><p>Voleu substituir el fitxer existent?</p></body></html> + + + + dest + dest + + + + with the following file? + amb el següent fitxer? + + + + src file info + info del fitxer ori + + + + dest file info + info del fitxer dest + + + + src + ori + + + + &File name: + Nom del &fitxer: + + + + Apply this option to all existing files + Aplica aquesta opció a tots els fitxers existents + + + + SearchDialog + + + Search Files + Cerca fitxers + + + + Name/Location + Nom/Ubicació + + + + File Name Patterns: + Patrons dels noms dels fitxers: + + + + * + * + + + + Case insensitive + Distinció entre majúscules i minúscules + + + + Use regular expression + Utilitza l'expressió regular + + + + Places to Search: + Llocs a cercar: + + + + &Add + &Afegeix + + + + &Remove + Sup&rimeix + + + + Search in sub directories + Cerca als subdirectoris + + + + Search for hidden files + Cerca els fitxers ocults + + + + File Type + Tipus de fitxer + + + + Only search for files of following types: + Cerca únicament els següents tipus de fitxers: + + + + Text files + Fitxers de text + + + + Image files + Fitxers d'imatges + + + + Audio files + Fitxers d'àudio + + + + Video files + Fitxers de vídeo + + + + Documents + Documents + + + + Folders + Carpetes + + + + Content + Contingut + + + + File contains: + El fitxer conté: + + + + Case insensiti&ve + Distinció entre ma&júscules i minúscules + + + + &Use regular expression + &Utilitza l'expressió regular + + + + Properties + Propietats + + + + File Size: + Mida del fitxer: + + + + Larger than: + Més gran que: + + + + + Bytes + Bytes + + + + + KiB + KiB + + + + + MiB + MiB + + + + + GiB + GiB + + + + Smaller than: + Més petit que: + + + + Last Modified Time: + Data de l'última modificació: + + + + Earlier than: + Abans de: + + + + Later than: + Després de: + + + diff --git a/src/translations/libfm-qt_cs.ts b/src/translations/libfm-qt_cs.ts new file mode 100644 index 0000000..4a661d5 --- /dev/null +++ b/src/translations/libfm-qt_cs.ts @@ -0,0 +1,1560 @@ + + + + + AppChooserDialog + + + Choose an Application + Zvolte aplikaci + + + + Installed Applications + Nainstalované aplikace + + + + Custom Command + Uživatelem určený příkaz + + + + Command line to execute: + Příkaz k vykonání: + + + + Application name: + Název aplikace: + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + <b>V příkazovém řádku je možné využít tato zástupná vyjádření:</b> +<ul> +<li><b>%f</b>: Představuje jeden soubor</li> +<li><b>%F</b>: Představuje vícero souborů</li> +<li><b>%u</b>: Představuje URI identifikátor jednoho souboru</li> +<li><b>%U</b>: Představuje vícero URI identifikátorů</li> +</ul> + + + + Keep terminal window open after command execution + Po dokončení příkazu nezavírat okno terminálu + + + + Execute in terminal emulator + Spustit v emulátoru terminálu + + + + Set selected application as default action of this file type + Použít zvolenou aplikaci jako výchozí akci pro soubory tohoto typu + + + + EditBookmarksDialog + + + Edit Bookmarks + Upravit záložky + + + + Name + Název + + + + Location + Umístění + + + + &Add Item + Přid&at položku + + + + &Remove Item + Odeb&rat položku + + + + Use drag and drop to reorder the items + Pořadí položek upravíte přetažením + + + + ExecFileDialog + + + Execute file + Spustit soubor + + + + &Open + &Otevřít + + + + E&xecute + &Spustit + + + + Execute in &Terminal + Spustit v &terminálu + + + + Cancel + Storno + + + + FileDialog + + + Location: + Umístění: + + + + File name: + Název souboru: + + + + File type: + Typ souboru: + + + + FileOperationDialog + + + Destination: + Cílové umístění: + + + + Processing: + Zpracování: + + + + Preparing... + Připravuje se… + + + + Progress + Ukazatel průběhu + + + + Time remaining: + Bude trvat jeÅ¡tě: + + + + Files processed: + Zpracovaných souborů: + + + + FilePropsDialog + + + File Properties + Vlastnosti souboru + + + + General + Obecné + + + + Location: + Umístění: + + + + File type: + Typ souboru: + + + + MIME type: + MIME typ: + + + + File size: + Velikost souboru: + + + + On-disk size: + Zabrané místo na disku: + + + + Last modified: + Naposledy změněno: + + + + Link target: + Cíl odkazu: + + + + Open With: + Otevřít pomocí: + + + + Last accessed: + Naposledy přistupováno: + + + + Contains: + Obsahuje: + + + + Device Usage: + Využití zařízení: + + + + Permissions + Oprávnění + + + + Ownership + Vlastnictví + + + + + + Group: + Skupina: + + + + + + Owner: + Vlastník: + + + + Access Control + Řízení přístupu + + + + + Other: + Ostatní: + + + + Make the file executable + Označit soubor jako spustitelný + + + + + + Read + Čtení + + + + + + Write + Zápis + + + + + + Execute + SpuÅ¡tění + + + + Sticky + Mazat/přejmenovávat pouze vlastník (sticky) + + + + SetUID + SpouÅ¡tět s právy vlastníka souboru (SetUID) + + + + SetGID + Přebírat skupinu u nových podsložek a souborů (SetGID) + + + + Advanced Mode + Pokročilý režim + + + + Fm::AppChooserComboBox + + + Customize + Přizpůsobit + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + Vyberte aplikace, ve které otevírat soubory „%1“ + + + + Fm::CreateNewMenu + + + Folder + Složka + + + + Blank File + Prázdný soubor + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + Zadaná složka „%1“ není platná + + + + Fm::DirTreeModel + + + Loading... + Načítání… + + + + + + <No sub folders> + <Žádné podsložky> + + + + Fm::DirTreeView + + + Open in New T&ab + Otevřít v nové k&artě + + + + Open in New Win&dow + &Otevřít v novém okně + + + + Open in Termina&l + Otevřít v terminá&lu + + + + Fm::DndActionMenu + + + Copy here + Zkopírovat sem + + + + Move here + Přesunout sem + + + + Create symlink here + Vytvořit zde symbolický odkaz + + + + Cancel + Storno + + + + Fm::EditBookmarksDialog + + + New bookmark + Nová záložka + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + Zdá se, že soubor „%1“ je spouÅ¡těč. +Co s ním chcete dělat? + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + Zdá se, že textový soubor „%1“ je spustitelný skript. +Co s ním chcete dělat? + + + + This file '%1' is executable. Do you want to execute it? + Tento soubor „%1“ je spustitelný. Chcete ho spustit? + + + + Fm::FileDialog + + + Go Back + Zpět + + + + Alt+Left + Go Back + Alt + Å¡ipka doleva + + + + Go Forward + Vpřed + + + + Alt+Right + Go Forward + Alt + Å¡ipka doprava + + + + Reload + Načíst znovu + + + + F5 + Reload + F5 + + + + Create Folder + Vytvořit složku + + + + Icon View + Zobrazení s ikonami + + + + Thumbnail View + Zobrazení s náhledy + + + + Compact View + Kompaktní zobrazení + + + + Detailed List View + Podrobný seznam + + + + + Error + Chyba + + + + Please select a file + Vyberte soubor + + + + %1 already exists. +Do you want to replace it? + %1 už existuje. +Chcete ho nahradit? + + + + Path "%1" does not exist + Umístění „%“ neexistuje + + + + "%1" is not a directory + „%1“ není složka + + + + "%1" is not a file + „%1“ není soubor + + + + + &Open + &Otevřít + + + + + &Save + &Uložit + + + + All Files (*) + VÅ¡echny soubory (*) + + + + Fm::FileDialogHelper + + + Open File + Otevřít soubor + + + + Save File + Uložit soubor + + + + Fm::FileMenu + + + Open + Otevřít + + + + Create &New + Vytvořit &nový + + + + &Restore + O&bnovit + + + + Cut + Vyjmout + + + + Copy + Kopírovat + + + + Paste + Vložit + + + + + &Move to Trash + Přesunout do &koÅ¡e + + + + Trust selected executables + Důvěřovat označeným spustitelným + + + + Trust this executable + Důvěřovat tomuto spustitelnému souboru + + + + Output + Výstup + + + + &Delete + &Smazat + + + + Rename + Přejmenovat + + + + Open With... + Otevřít s… + + + + Other Applications + Ostatní aplikace + + + + Extract to... + Rozbalit do… + + + + Extract Here + Rozbalit sem + + + + Compress + Komprimovat + + + + Properties + Vlastnosti + + + + Fm::FileOperation + + + Error + Chyba + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + Některé soubory nemohou být přesunuty do koÅ¡e – souborové systémy, na kterých leží, tuto operaci nepodporují. +Chcete je namísto toho přímo odstranit? + + + + + Confirm + Potvrdit + + + + Do you want to delete the selected files? + Chcete vybrané soubory smazat? + + + + Do you want to move the selected files to trash can? + Chcete vybrané soubory přesunout do koÅ¡e? + + + + Fm::FileOperationDialog + + + Move files + Přesunout soubory + + + + Moving the following files to destination folder: + Přesouvání následujících souborů do cílové složky: + + + + Copy Files + Kopírovat soubory + + + + Copying the following files to destination folder: + Kopírování následujících souborů do cílové složky: + + + + Trash Files + Soubory v KoÅ¡i + + + + Moving the following files to trash can: + Přesouvání následujících souborů do koÅ¡e: + + + + Delete Files + Smazat soubory + + + + Deleting the following files: + Mazání následujících souborů: + + + + Create Symlinks + Vytvořit symbolické odkazy + + + + Creating symlinks for the following files: + Vytváření symbolických odkazů pro následující soubory: + + + + Change Attributes + Změnit atributy + + + + Changing attributes of the following files: + Měnění atributů u následujících souborů: + + + + Restore Trashed Files + Obnovit soubory z koÅ¡e + + + + Restoring the following files from trash can: + Obnovování následujících souborů z koÅ¡e: + + + + + Error + Chyba + + + + Fm::FilePropsDialog + + + View folder content + Zobrazit obsah složky + + + + View and modify folder content + Zobrazit a změnit obsah složky + + + + Read + Čtení + + + + Read and write + Čtení a zápis + + + + Forbidden + Odepřen + + + + Files of different types + Soubory různých typů + + + + Multiple Files + Vícero souborů + + + + %p% used + %p% použito + + + + %1 Free of %2 + volných %1 z %2 + + + + no file + žádný soubor + + + + one file + jeden soubor + + + + %1 files + %1 souborů + + + + Select an icon + Vybrat ikonu + + + + Images (*.png *.xpm *.svg *.svgz ) + Obrázky (*.png *.xpm *.svg *.svgz ) + + + + Apply changes + Použít změny + + + + Do you want to recursively apply these changes to all files and sub-folders? + Chcete tyto změny použít také na vÅ¡echny obsažené soubory a podsložky? + + + + Fm::FileSearchDialog + + + Error + Chyba + + + + You should add at least one directory to search. + Přidejte alespoň jednu složku, ve které vyhledávat. + + + + Select a folder + Vybrat složku + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + Nelze vytvořit odkaz na souborovém systému, který to nepodporuje + + + + Fm::FolderMenu + + + Create &New + Vytvořit &nový + + + + &Paste + &Vložit + + + + Select &All + Vybr&at vÅ¡e + + + + Invert Selection + Prohodit výběr + + + + Sorting + Řazení + + + + Show Hidden + Zobrazit skryté + + + + Folder Pr&operties + Vlastnosti sl&ožky + + + + Output + Výstup + + + + By File Name + Podle názvu + + + + By Modification Time + Podle okamžiku změny + + + + By File Size + Podle velikosti + + + + By File Type + Podle typu + + + + By File Owner + Podle vlastníka + + + + Ascending + Vzestupně + + + + Descending + Sestupně + + + + Folder First + Složky jako první + + + + Case Sensitive + RozliÅ¡ovat malá/VELKÁ písmena + + + + Fm::FolderModel + + + Name + Název + + + + Type + Typ + + + + Size + Velikost + + + + Modified + Změněno + + + + Owner + Vlastník + + + + Group + Skupina + + + + Fm::FontButton + + + Bold + Tučné + + + + Italic + Kurzíva + + + + Fm::MountOperationPasswordDialog + + + &Connect + &Připojit + + + + Fm::PathBar + + + &Edit Path + &Upravit popis umístění + + + + &Copy Path + &Zkopírovat popis umístění + + + + Fm::PlacesModel + + + Places + Místa + + + + Desktop + Plocha + + + + Trash + KoÅ¡ + + + + Computer + Počítač + + + + Applications + Aplikace + + + + Network + Síť + + + + Devices + Zařízení + + + + Bookmarks + Záložky + + + + Fm::PlacesView + + + Empty Trash + Vysypat koÅ¡ + + + + Open in New Tab + Otevřít v novém panelu + + + + Open in New Window + Otevřít v novém okně + + + + + Hide + Skrýt + + + + Move Bookmark Up + Posunout záložku nahoru + + + + Move Bookmark Down + Posunout záložku dolů + + + + Rename Bookmark + Přejmenovat záložku + + + + Remove Bookmark + Odstranit záložku + + + + + Unmount + Odpojit + + + + Mount + Připojit + + + + Eject + Vysunout + + + + Show All Entries + Zobrazit vÅ¡echny položky + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + Typ: %1 +Velikost: %2 +Změněno: %3 + + + + + Type: %1 +Modified: %2 + Typ: %1 +Změněno: %2 + + + + &Overwrite + &Přepsat + + + + &Rename + Pře&jmenovat + + + + Fm::SidePane + + + Places + Místa + + + + Directory Tree + Strom složek + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + Nelze obnovit soubor „%s“: původní umístění není známo + + + + MountOperationPasswordDialog + + + Mount + Připojit + + + + Connect &anonymously + Připojit &anonymně + + + + Connect as u&ser: + Připojit jako &uživatel: + + + + &Username: + &Uživatelské jméno: + + + + &Password: + &Heslo: + + + + &Domain: + &Doména: + + + + Forget password &immediately + Heslo okamž&itě zapomenout + + + + Remember password until you &logout + Zapamatovat si heslo do odh&lášení + + + + Remember &forever + Zapamatovat si heslo &trvale + + + + QObject + + + + + + + Error + Chyba + + + + Rename File + Přejmenovat soubor + + + + Please enter a new name: + Zadejte nový název: + + + + Create Folder + Vytvořit složku + + + + Please enter a new file name: + Zadejte název pro nový soubor: + + + + New text file + Nový textový soubor + + + + Please enter a new folder name: + Zadejte název pro novou složku: + + + + New folder + Nová složka + + + + Enter a name for the new %1: + Zadejte název pro nový %1: + + + + Create File + Vytvořit soubor + + + + Custom Icon Error + Chyba uživatelem určené ikony + + + + The path is not mounted. + Umístění není připojené (mount). + + + + Invalid desktop entry file: '%1' + Neplatný soubor spouÅ¡těče: „%1“ + + + + No default application is set to launch '%1' + Pro spouÅ¡tění „%1“ není nastavená žádná výchozí aplikace + + + + Cannot set working directory to '%1': %2 + Nedaří se nastavit pracovní složku na „%1“: %2 + + + + Identifier: + Identifikátor: + + + + RenameDialog + + + Confirm to replace files + Potvrdit nahrazení souborů + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + <html><head/><body><p><span style=" font-weight:600;">V tomto umístění se už nachází soubor se stejným názvem.</span></p><p>Chcete existující soubor nahradit?</p></body></html> + + + + dest + Cíl + + + + with the following file? + následujícím souborem? + + + + src file info + Informace o zdrojovém souboru + + + + dest file info + Informace o cílovém souboru + + + + src + Zdroj + + + + &File name: + Název &souboru: + + + + Apply this option to all existing files + Použít tuto volbu pro vÅ¡echny soubory + + + + SearchDialog + + + Search Files + Hledat soubory + + + + Name/Location + Název/umístění + + + + File Name Patterns: + Název souboru obsahuje: + + + + * + + + + + Case insensitive + NerozliÅ¡ovat malá/VELKÁ písmena + + + + Use regular expression + Použít regulární výrazy + + + + Places to Search: + Místa k prohledání: + + + + &Add + Přid&at + + + + &Remove + Odst&ranit + + + + Search in sub directories + Hledat v podsložkách + + + + Search for hidden files + Hledat skryté soubory + + + + File Type + Typ souboru + + + + Only search for files of following types: + Hledat pouze soubory těchto typů: + + + + Text files + Textové soubory + + + + Image files + Obrázky + + + + Audio files + Zvuky + + + + Video files + Videa + + + + Documents + Dokumenty + + + + Folders + Složky + + + + Content + Obsah + + + + File contains: + Soubor obsahuje: + + + + Case insensiti&ve + NerozliÅ¡ovat malá/&VELKÁ písmena + + + + &Use regular expression + Po&užít regulární výrazy + + + + Properties + Vlastnosti + + + + File Size: + Velikost souboru: + + + + Larger than: + Větší než: + + + + + Bytes + Bajtů + + + + + KiB + + + + + + MiB + + + + + + GiB + + + + + Smaller than: + Menší než: + + + + Last Modified Time: + Okamžik poslední změny: + + + + Earlier than: + Dříve než: + + + + Later than: + Později než: + + + diff --git a/src/translations/libfm-qt_cy.ts b/src/translations/libfm-qt_cy.ts new file mode 100644 index 0000000..dbfd5b9 --- /dev/null +++ b/src/translations/libfm-qt_cy.ts @@ -0,0 +1,1547 @@ + + + + + AppChooserDialog + + + Choose an Application + + + + + Installed Applications + + + + + Custom Command + + + + + Command line to execute: + + + + + Application name: + + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + + + + + Keep terminal window open after command execution + + + + + Execute in terminal emulator + + + + + Set selected application as default action of this file type + + + + + EditBookmarksDialog + + + Edit Bookmarks + + + + + Name + + + + + Location + + + + + &Add Item + + + + + &Remove Item + + + + + Use drag and drop to reorder the items + + + + + ExecFileDialog + + + Execute file + + + + + &Open + + + + + E&xecute + + + + + Execute in &Terminal + + + + + Cancel + + + + + FileDialog + + + Location: + + + + + File name: + + + + + File type: + + + + + FileOperationDialog + + + Destination: + + + + + Processing: + + + + + Preparing... + + + + + Progress + + + + + Time remaining: + + + + + Files processed: + + + + + FilePropsDialog + + + File Properties + + + + + General + + + + + Location: + + + + + File type: + + + + + MIME type: + + + + + File size: + + + + + On-disk size: + + + + + Last modified: + + + + + Link target: + + + + + Open With: + + + + + Last accessed: + + + + + Contains: + + + + + Device Usage: + + + + + Permissions + + + + + Ownership + + + + + + + Group: + + + + + + + Owner: + + + + + Access Control + + + + + + Other: + + + + + Make the file executable + + + + + + + Read + + + + + + + Write + + + + + + + Execute + + + + + Sticky + + + + + SetUID + + + + + SetGID + + + + + Advanced Mode + + + + + Fm::AppChooserComboBox + + + Customize + + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + + + + + Fm::CreateNewMenu + + + Folder + + + + + Blank File + + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + + + + + Fm::DirTreeModel + + + Loading... + + + + + + + <No sub folders> + + + + + Fm::DirTreeView + + + Open in New T&ab + + + + + Open in New Win&dow + + + + + Open in Termina&l + + + + + Fm::DndActionMenu + + + Copy here + + + + + Move here + + + + + Create symlink here + + + + + Cancel + + + + + Fm::EditBookmarksDialog + + + New bookmark + + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + + + + + This file '%1' is executable. Do you want to execute it? + + + + + Fm::FileDialog + + + Go Back + + + + + Alt+Left + Go Back + + + + + Go Forward + + + + + Alt+Right + Go Forward + + + + + Reload + + + + + F5 + Reload + + + + + Create Folder + + + + + Icon View + + + + + Thumbnail View + + + + + Compact View + + + + + Detailed List View + + + + + + Error + + + + + Please select a file + + + + + %1 already exists. +Do you want to replace it? + + + + + Path "%1" does not exist + + + + + "%1" is not a directory + + + + + "%1" is not a file + + + + + + &Open + + + + + + &Save + + + + + All Files (*) + + + + + Fm::FileDialogHelper + + + Open File + + + + + Save File + + + + + Fm::FileMenu + + + Open + + + + + Open With... + + + + + Other Applications + + + + + Create &New + + + + + &Restore + + + + + Cut + + + + + Copy + + + + + Paste + + + + + + &Move to Trash + + + + + Rename + + + + + Extract to... + + + + + Extract Here + + + + + Compress + + + + + Properties + + + + + Trust selected executables + + + + + Trust this executable + + + + + Output + + + + + &Delete + + + + + Fm::FileOperation + + + Error + + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + + + + + + Confirm + + + + + Do you want to delete the selected files? + + + + + Do you want to move the selected files to trash can? + + + + + Fm::FileOperationDialog + + + Move files + + + + + Moving the following files to destination folder: + + + + + Copy Files + + + + + Copying the following files to destination folder: + + + + + Trash Files + + + + + Moving the following files to trash can: + + + + + Delete Files + + + + + Deleting the following files: + + + + + Create Symlinks + + + + + Creating symlinks for the following files: + + + + + Change Attributes + + + + + Changing attributes of the following files: + + + + + Restore Trashed Files + + + + + Restoring the following files from trash can: + + + + + + Error + + + + + Fm::FilePropsDialog + + + View folder content + + + + + View and modify folder content + + + + + Read + + + + + Read and write + + + + + Forbidden + + + + + Files of different types + + + + + Multiple Files + + + + + %p% used + + + + + %1 Free of %2 + + + + + no file + + + + + one file + + + + + %1 files + + + + + Select an icon + + + + + Images (*.png *.xpm *.svg *.svgz ) + + + + + Apply changes + + + + + Do you want to recursively apply these changes to all files and sub-folders? + + + + + Fm::FileSearchDialog + + + Error + + + + + You should add at least one directory to search. + + + + + Select a folder + + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + + + + + Fm::FolderMenu + + + Create &New + + + + + &Paste + + + + + Select &All + + + + + Invert Selection + + + + + Sorting + + + + + Show Hidden + + + + + Folder Pr&operties + + + + + Output + + + + + By File Name + + + + + By Modification Time + + + + + By File Size + + + + + By File Type + + + + + By File Owner + + + + + Ascending + + + + + Descending + + + + + Folder First + + + + + Case Sensitive + + + + + Fm::FolderModel + + + Name + + + + + Type + + + + + Size + + + + + Modified + + + + + Owner + + + + + Group + + + + + Fm::FontButton + + + Bold + + + + + Italic + + + + + Fm::MountOperationPasswordDialog + + + &Connect + + + + + Fm::PathBar + + + &Edit Path + + + + + &Copy Path + + + + + Fm::PlacesModel + + + Places + + + + + Desktop + + + + + Computer + + + + + Applications + + + + + Network + + + + + Devices + + + + + Bookmarks + + + + + Trash + + + + + Fm::PlacesView + + + Open in New Tab + + + + + Open in New Window + + + + + Empty Trash + + + + + + Hide + + + + + Move Bookmark Up + + + + + Move Bookmark Down + + + + + Rename Bookmark + + + + + Remove Bookmark + + + + + + Unmount + + + + + Mount + + + + + Eject + + + + + Show All Entries + + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + + + + + + Type: %1 +Modified: %2 + + + + + &Overwrite + + + + + &Rename + + + + + Fm::SidePane + + + Places + + + + + Directory Tree + + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + + + + + MountOperationPasswordDialog + + + Mount + + + + + Connect &anonymously + + + + + Connect as u&ser: + + + + + &Username: + + + + + &Password: + + + + + &Domain: + + + + + Forget password &immediately + + + + + Remember password until you &logout + + + + + Remember &forever + + + + + QObject + + + Rename File + + + + + Please enter a new name: + + + + + + + + + Error + + + + + Create Folder + + + + + Create File + + + + + Please enter a new file name: + + + + + New text file + + + + + Please enter a new folder name: + + + + + New folder + + + + + Enter a name for the new %1: + + + + + Custom Icon Error + + + + + The path is not mounted. + + + + + Invalid desktop entry file: '%1' + + + + + No default application is set to launch '%1' + + + + + Cannot set working directory to '%1': %2 + + + + + Identifier: + + + + + RenameDialog + + + Confirm to replace files + + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + + + + + dest + + + + + with the following file? + + + + + src file info + + + + + dest file info + + + + + src + + + + + &File name: + + + + + Apply this option to all existing files + + + + + SearchDialog + + + Search Files + + + + + Name/Location + + + + + File Name Patterns: + + + + + * + + + + + Case insensitive + + + + + Use regular expression + + + + + Places to Search: + + + + + &Add + + + + + &Remove + + + + + Search in sub directories + + + + + Search for hidden files + + + + + File Type + + + + + Only search for files of following types: + + + + + Text files + + + + + Image files + + + + + Audio files + + + + + Video files + + + + + Documents + + + + + Folders + + + + + Content + + + + + File contains: + + + + + Case insensiti&ve + + + + + &Use regular expression + + + + + Properties + + + + + File Size: + + + + + Larger than: + + + + + + Bytes + + + + + + KiB + + + + + + MiB + + + + + + GiB + + + + + Smaller than: + + + + + Last Modified Time: + + + + + Earlier than: + + + + + Later than: + + + + diff --git a/src/translations/libfm-qt_da.ts b/src/translations/libfm-qt_da.ts new file mode 100644 index 0000000..73fbc8a --- /dev/null +++ b/src/translations/libfm-qt_da.ts @@ -0,0 +1,1560 @@ + + + + + AppChooserDialog + + + Choose an Application + Vælg et program + + + + Installed Applications + Installerede programmer + + + + Custom Command + Valgfri kommando + + + + Command line to execute: + Udfør følgende kommando: + + + + Application name: + Programnavn: + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + <b>Disse specielle koder kan bruges i kommandoen:</b> +<ul> +<li><b>%f</b>: Repræsenterer ét filnavn</li> +<li><b>%F</b>: Repræsenterer flere filnavne</li> +<li><b>%u</b>: Repræsenterer én URI af filen</li> +<li><b>%U</b>: Repræsenterer flere URI's</li> +</ul> + + + + Keep terminal window open after command execution + Luk ikke terminalen efter kommandoen er udført + + + + Execute in terminal emulator + Udfør i terminalemulator + + + + Set selected application as default action of this file type + Brug altid dette program til at Ã¥bne denne filtype + + + + EditBookmarksDialog + + + Edit Bookmarks + Rediger bogmærker + + + + Name + Navn + + + + Location + Sted + + + + &Add Item + &Tilføj bogmærke + + + + &Remove Item + &Fjern bogmærke + + + + Use drag and drop to reorder the items + Brug træk- og-slip for at arrangere bogmærkerne + + + + ExecFileDialog + + + Execute file + Kør fil + + + + &Open + &Åbn + + + + E&xecute + &Kør + + + + Execute in &Terminal + Kør i &terminal + + + + Cancel + Annuller + + + + FileDialog + + + Location: + Placering: + + + + File name: + Filnavn: + + + + File type: + Filtype: + + + + FileOperationDialog + + + Destination: + Destination: + + + + Processing: + Arbejder: + + + + Preparing... + Forbereder... + + + + Progress + Fremskridt + + + + Time remaining: + Tid tilbage: + + + + Files processed: + Filer behandlet: + + + + FilePropsDialog + + + File Properties + Filegenskaber + + + + General + Generelt + + + + Location: + Placering: + + + + File type: + Filtype: + + + + MIME type: + MIME-type: + + + + File size: + Filstørrelse: + + + + On-disk size: + Størrelse pÃ¥ disk: + + + + Last modified: + Sidst ændret: + + + + Link target: + Henviser til: + + + + Open With: + Åbn med: + + + + Last accessed: + Senest tilgÃ¥et: + + + + Contains: + Indeholder: + + + + Device Usage: + Enhedsforbrug: + + + + Permissions + Tilladelser + + + + Ownership + Ejerskab + + + + + + Group: + Gruppe: + + + + + + Owner: + Ejer: + + + + Access Control + Filrettigheder + + + + + Other: + Andre: + + + + Make the file executable + Gør filen eksekverbar + + + + + + Read + Læs + + + + + + Write + Skriv + + + + + + Execute + Eksekver + + + + Sticky + Sticky + + + + SetUID + SetUID + + + + SetGID + SetGID + + + + Advanced Mode + Avanceret + + + + Fm::AppChooserComboBox + + + Customize + Brugerdefineret + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + Vælg et program til at Ã¥bne filer af typen "%1" + + + + Fm::CreateNewMenu + + + Folder + Mappe + + + + Blank File + Tom fil + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + Den specifcerede mappe '%1' er ikke gyldig + + + + Fm::DirTreeModel + + + Loading... + Åbner... + + + + + + <No sub folders> + <Ingen undermapper> + + + + Fm::DirTreeView + + + Open in New T&ab + Åbn i nyt &faneblad + + + + Open in New Win&dow + Åbn i nyt &vindue + + + + Open in Termina&l + Åbn i termina&l + + + + Fm::DndActionMenu + + + Copy here + Kopier her + + + + Move here + Flyt her + + + + Create symlink here + Opret symbolsk henvisning her + + + + Cancel + Annuller + + + + Fm::EditBookmarksDialog + + + New bookmark + Nyt bogmærke + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + Filen '%1' ser ud til at være en skrivebordspost. +Hvad vil du gøre med den? + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + Denne tekstfil "%1" er tilsyneladende et eksekverbart script. +Hvad vil du foretage med det? + + + + This file '%1' is executable. Do you want to execute it? + Filen "%1" er eksekverbar. Vil du eksekvere den? + + + + Fm::FileDialog + + + Go Back + GÃ¥ tilbage + + + + Alt+Left + Go Back + Alt+Venstre + + + + Go Forward + GÃ¥ fremad + + + + Alt+Right + Go Forward + Alt+Højre + + + + Reload + Genindlæs + + + + F5 + Reload + F5 + + + + Create Folder + Opret mappe + + + + Icon View + Ikonvisning + + + + Thumbnail View + Miniaturevisning + + + + Compact View + Kompakt visning + + + + Detailed List View + Detaljeret liste visning + + + + + Error + Fejl + + + + Please select a file + Vælg venligst en fil + + + + %1 already exists. +Do you want to replace it? + %1 findes allerede. +Vil du erstatte den? + + + + Path "%1" does not exist + Stien "%1" findes ikke + + + + "%1" is not a directory + "%1" er ikke en mappe + + + + "%1" is not a file + "%1" er ikke en fil + + + + + &Open + &Åbn + + + + + &Save + &Gem + + + + All Files (*) + Alle filer (*) + + + + Fm::FileDialogHelper + + + Open File + Åbn fil + + + + Save File + Gem fil + + + + Fm::FileMenu + + + Open + Åbn + + + + Open With... + Åbn med... + + + + Other Applications + Andre programmer + + + + Create &New + Opret &ny + + + + &Restore + &Gendan + + + + Cut + Klip + + + + Copy + Kopiér + + + + Paste + Indsæt + + + + + &Move to Trash + &Smid i papirkurven + + + + Rename + Omdøb + + + + Extract to... + Udpak til... + + + + Extract Here + Udpak her + + + + Compress + Komprimer + + + + Properties + Egenskaber + + + + Trust selected executables + Hav tillid til valgte eksekverbare + + + + Trust this executable + Hav tillid til denne eksekverbar + + + + Output + Resultat + + + + &Delete + &Slet + + + + Fm::FileOperation + + + Error + Fejl + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + Nogle filer kan ikke smides i papirkurven, fordi det underliggende filsystem ikke understøtter denne funktion. +Vil du slette dem istedet? + + + + + Confirm + Bekræft + + + + Do you want to delete the selected files? + Vil du slette de valgte filer? + + + + Do you want to move the selected files to trash can? + Vil du smide de valgte filer i papirkurven? + + + + Fm::FileOperationDialog + + + Move files + Flyt filer + + + + Moving the following files to destination folder: + Flytter følgende filer til destinationsmappen: + + + + Copy Files + Kopier filer + + + + Copying the following files to destination folder: + Kopier følgende filer til destinationsmappen: + + + + Trash Files + Smid filer i papirkurven + + + + Moving the following files to trash can: + Flytter følgende filer til papirkurven: + + + + Delete Files + Slet filer + + + + Deleting the following files: + Sletter følgende filer: + + + + Create Symlinks + Opret symbolske henvisninger + + + + Creating symlinks for the following files: + Opretter symbolske henvisninger for følgende filer: + + + + Change Attributes + Skift filegenskaber + + + + Changing attributes of the following files: + Skifter filegenskaber for følgende filer: + + + + Restore Trashed Files + Gendan filer fra papirkurven + + + + Restoring the following files from trash can: + Gendanner følgende filer fra papirkurven: + + + + + Error + Fejl + + + + Fm::FilePropsDialog + + + View folder content + Vis mappens indhold + + + + View and modify folder content + Vis og ændre mappens indhold + + + + Read + Læs + + + + Read and write + Læs og skriv + + + + Forbidden + Forbudt + + + + Files of different types + Filer af forskellig type + + + + Multiple Files + Flere filer + + + + %p% used + %p% anvendt + + + + %1 Free of %2 + %1 ledig af %2 + + + + no file + ingen fil + + + + one file + en fil + + + + %1 files + %1 filer + + + + Select an icon + Vælg et ikon + + + + Images (*.png *.xpm *.svg *.svgz ) + Billeder (*.png *.xpm *.svg *.svgz ) + + + + Apply changes + Anvend ændringer + + + + Do you want to recursively apply these changes to all files and sub-folders? + Vil du anvende disse ændringer rekursivt pÃ¥ alle filer og undermapper? + + + + Fm::FileSearchDialog + + + Error + Fejl + + + + You should add at least one directory to search. + Du skal mindst vælge en mappe at søge i. + + + + Select a folder + Vælg en mappe + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + Kan ikke oprette et link pÃ¥ filsystem som ikke er indbygget + + + + Fm::FolderMenu + + + Create &New + Opret &ny + + + + &Paste + &Indsæt + + + + Select &All + Vælg &alle + + + + Invert Selection + Inventer markering + + + + Sorting + Sortering + + + + Show Hidden + Vis skjulte + + + + Folder Pr&operties + Mappe&egenskaber + + + + Output + Resultat + + + + By File Name + Efter filnavn + + + + By Modification Time + Efter ændringstidspunkt + + + + By File Size + Efter filstørrelse + + + + By File Type + Efter filtype + + + + By File Owner + Efter filejer + + + + Ascending + Stigende + + + + Descending + Faldende + + + + Folder First + Mapper først + + + + Case Sensitive + Forskel pÃ¥ store og smÃ¥ bogstaver + + + + Fm::FolderModel + + + Name + Navn + + + + Type + Type + + + + Size + Størrelse + + + + Modified + Ændringstidspunkt + + + + Owner + Ejer + + + + Group + Gruppe + + + + Fm::FontButton + + + Bold + Fed + + + + Italic + Kursiv + + + + Fm::MountOperationPasswordDialog + + + &Connect + &Forbind + + + + Fm::PathBar + + + &Edit Path + &Rediger sti + + + + &Copy Path + &Kopiér sti + + + + Fm::PlacesModel + + + Places + Steder + + + + Desktop + Skrivebord + + + + Computer + Computer + + + + Applications + Programmer + + + + Network + Netværk + + + + Devices + Enheder + + + + Bookmarks + Bogmærker + + + + Trash + Papirkurv + + + + Fm::PlacesView + + + Open in New Tab + Åbn i nyt faneblad + + + + Open in New Window + Åbn i nyt vindue + + + + Empty Trash + Tøm papirkurven + + + + + Hide + Skjul + + + + Move Bookmark Up + Flyt bogmærke op + + + + Move Bookmark Down + Flyt bogmærke ned + + + + Rename Bookmark + Omdøb bogmærke + + + + Remove Bookmark + Fjern bogmærke + + + + + Unmount + Afmonter + + + + Mount + Monter + + + + Eject + Skub ud + + + + Show All Entries + Vis alle poster + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + Type: %1 +Størrelse: %2 +Ændringstidspunkt: %3 + + + + + Type: %1 +Modified: %2 + Type: %1 +Senset ændret: %2 + + + + &Overwrite + Over&skriv + + + + &Rename + &Omdøb + + + + Fm::SidePane + + + Places + Steder + + + + Directory Tree + Mappetræ + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + Kan ikke fortryde at filen '%s' blev flyttet til papirkurven: oprindelig sti er ukendt + + + + MountOperationPasswordDialog + + + Mount + Monter + + + + Connect &anonymously + Fobind &anonymt + + + + Connect as u&ser: + Forbind som &bruger: + + + + &Username: + &Brugernavn: + + + + &Password: + &Kodeord: + + + + &Domain: + &Domæne: + + + + Forget password &immediately + Glem kodeordet ø&jeblikkeligt + + + + Remember password until you &logout + Husk kodeordet til du &logger ud + + + + Remember &forever + Husk &for evigt + + + + QObject + + + Rename File + Omdøb fil + + + + Please enter a new name: + Indtast venligst et nyt navn: + + + + + + + + Error + Fejl + + + + Create Folder + Opret mappe + + + + Create File + Opret fil + + + + Please enter a new file name: + Indtast venligst et nyt filnavn: + + + + New text file + Ny tekstfil + + + + Please enter a new folder name: + Indtast venligst et nyt mappenavn: + + + + New folder + Ny mappe + + + + Enter a name for the new %1: + Indtast et navn til det nye %1: + + + + Custom Icon Error + Fejl ved brugerdefineret ikon + + + + The path is not mounted. + Stien er ikke monteret. + + + + Invalid desktop entry file: '%1' + Ugyldig skrivebordspost-fil: '%1' + + + + No default application is set to launch '%1' + Der er ikke sat noget standardprogram til at starte '%1' + + + + Cannot set working directory to '%1': %2 + Kan ikke sætte arbejdsmappe til '%1': %2 + + + + Identifier: + Identifikator: + + + + RenameDialog + + + Confirm to replace files + Bekræft for at erstatte filer + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + <html><head/><body><p><span style=" font-weight:600;">Der er allerede en fil med dette navn i denne mappe.</span></p><p>Vil du erstatte den eksisterende fil?</p></body></html> + + + + dest + Destination + + + + with the following file? + med den følgende fil? + + + + src file info + Kildefilinfo + + + + dest file info + Dest. filinfo + + + + src + kilde + + + + &File name: + &Filnavn: + + + + Apply this option to all existing files + Gør dette for alle eksisterende filer + + + + SearchDialog + + + Search Files + Søg filer + + + + Name/Location + Navn/Sted + + + + File Name Patterns: + Filnavnsmønster: + + + + * + * + + + + Case insensitive + Ingen forskel pÃ¥ store og smÃ¥ bogstaver + + + + Use regular expression + Brug regulære udtryk + + + + Places to Search: + Mapper at søge i: + + + + &Add + &Tilføj + + + + &Remove + &Fjern + + + + Search in sub directories + Søg i undermapper + + + + Search for hidden files + Søg efter skjulte filer + + + + File Type + Filtype + + + + Only search for files of following types: + Søg kun efter følgende typer: + + + + Text files + Tekstfiler + + + + Image files + Billeder + + + + Audio files + Lydfiler + + + + Video files + Videofiler + + + + Documents + Dokumenter + + + + Folders + Mapper + + + + Content + Indhold + + + + File contains: + Filen indeholder: + + + + Case insensiti&ve + Ingen forskel pÃ¥ store og smÃ¥ &bogstaver + + + + &Use regular expression + Bruger &regulære udtryk + + + + Properties + Egenskaber + + + + File Size: + Filstørrelse: + + + + Larger than: + Større end: + + + + + Bytes + Bytes + + + + + KiB + KiB + + + + + MiB + MiB + + + + + GiB + GiB + + + + Smaller than: + Mindre end: + + + + Last Modified Time: + Sidst ændret: + + + + Earlier than: + Tidligere end: + + + + Later than: + Senere end: + + + diff --git a/src/translations/libfm-qt_de.ts b/src/translations/libfm-qt_de.ts new file mode 100644 index 0000000..b13fa9d --- /dev/null +++ b/src/translations/libfm-qt_de.ts @@ -0,0 +1,1560 @@ + + + + + AppChooserDialog + + + Choose an Application + Wählen Sie eine Anwendung + + + + Installed Applications + Installierte Anwendungen + + + + Custom Command + Benutzerdefinierter Befehl + + + + Command line to execute: + Auszuführende Befehlszeile: + + + + Application name: + Anwendungsname: + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + <b>Diese speziellen Kürzel können im Befehl verwendet werden</b> +<ul> +<li><b>%f</b>: Repräsentiert einen einzelnen Dateinamen</li> +<li><b>%F</b>: Repräsentiert mehrere Dateiennamen</li> +<li><b>%u</b>: Repräsentiert eine einzelne URI einer Datei</li> +<li><b>%U</b>: Repräsentiert mehrere URIs</li> +</ul> + + + + Keep terminal window open after command execution + Terminalfenster nach der Ausführung des Befehls offen lassen + + + + Execute in terminal emulator + In einem Terminalemulator ausführen + + + + Set selected application as default action of this file type + Ausgewählte Anwendung als Standardaktion für diesen Dateityp festlegen + + + + EditBookmarksDialog + + + Edit Bookmarks + Lesezeichen bearbeiten + + + + Name + Name + + + + Location + Ort + + + + &Add Item + Element &hinzufügen + + + + &Remove Item + Element &entfernen + + + + Use drag and drop to reorder the items + Benutzen Sie Drag&Drop, um Elemente zu sortieren + + + + ExecFileDialog + + + Execute file + Datei ausführen + + + + &Open + Ö&ffnen + + + + E&xecute + &Ausführen + + + + Execute in &Terminal + Im &Terminal ausführen + + + + Cancel + Abbrechen + + + + FileDialog + + + Location: + Ort: + + + + File name: + Dateiname: + + + + File type: + Dateityp: + + + + FileOperationDialog + + + Destination: + Ziel: + + + + Processing: + Verarbeite: + + + + Preparing... + Vorbereiten... + + + + Progress + Fortschritt + + + + Time remaining: + Verbleibende Zeit: + + + + Files processed: + Dateien verarbeitet: + + + + FilePropsDialog + + + File Properties + Dateieigenschaften + + + + General + Allgemein + + + + Location: + Ort: + + + + File type: + Dateityp: + + + + MIME type: + MIME-Typ: + + + + File size: + Dateigröße: + + + + On-disk size: + Größe auf dem Datenträger: + + + + Last modified: + Zuletzt geändert: + + + + Link target: + Verknüpfungsziel: + + + + Open With: + Öffnen mit: + + + + Last accessed: + Letzter Zugriff: + + + + Contains: + Enthält: + + + + Device Usage: + Gerätenutzung: + + + + Permissions + Berechtigungen + + + + Ownership + Besitz + + + + + + Group: + Gruppe: + + + + + + Owner: + Besitzer: + + + + Access Control + Zugriffskontrolle + + + + + Other: + Andere: + + + + Make the file executable + Datei ausführbar machen + + + + + + Read + Lesen + + + + + + Write + Schreiben + + + + + + Execute + Ausführen + + + + Sticky + "Sticky bit" + + + + SetUID + SetUID + + + + SetGID + SetGID + + + + Advanced Mode + Erweiterter Modus + + + + Fm::AppChooserComboBox + + + Customize + Anpassen + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + Wählen Sie eine Anwendung zum Öffnen von Dateien des Typs "%1" aus + + + + Fm::CreateNewMenu + + + Folder + Ordner + + + + Blank File + Leere Datei + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + Das angegebene Verzeichnis '%1' ist nicht gültig + + + + Fm::DirTreeModel + + + Loading... + Laden... + + + + + + <No sub folders> + <Keine Unterverzeichnisse> + + + + Fm::DirTreeView + + + Open in New T&ab + Öffnen in neuem &Reiter + + + + Open in New Win&dow + In neuem &Fenster öffnen + + + + Open in Termina&l + Im Termina&l öffnen + + + + Fm::DndActionMenu + + + Copy here + Hierhin kopieren + + + + Move here + Hierhin verschieben + + + + Create symlink here + Hier eine symbolische Verknüpfung erstellen + + + + Cancel + Abbrechen + + + + Fm::EditBookmarksDialog + + + New bookmark + Neues Lesezeichen + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + Diese Datei '%1' scheint ein Schreibtischeintrag zu sein. +Was möchten Sie damit tun? + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + Diese Textdatei '%1' scheint ein ausführbares Skript zu sein. +Was möchten Sie damit tun? + + + + This file '%1' is executable. Do you want to execute it? + Diese Datei '%1' ist ausführbar. Möchten Sie sie ausführen? + + + + Fm::FileDialog + + + Go Back + Zurück + + + + Alt+Left + Go Back + Alt+Links + + + + Go Forward + Vorwärts + + + + Alt+Right + Go Forward + Alt+Rechts + + + + Reload + Neu laden + + + + F5 + Reload + F5 + + + + Create Folder + Ordner erstellen + + + + Icon View + Symbolansicht + + + + Thumbnail View + Miniaturansicht + + + + Compact View + Kompaktansicht + + + + Detailed List View + Detaillierte Listenansicht + + + + + Error + Fehler + + + + Please select a file + Bitte wählen Sie eine Datei aus + + + + %1 already exists. +Do you want to replace it? + %1 existiert bereits. +Möchten Sie sie ersetzen? + + + + Path "%1" does not exist + Pfad "%1" existiert nicht + + + + "%1" is not a directory + "%1" ist kein Verzeichnis + + + + "%1" is not a file + "%1" ist keine Datei + + + + + &Open + &Öffnen + + + + + &Save + &Speichern + + + + All Files (*) + Alle Dateien (*) + + + + Fm::FileDialogHelper + + + Open File + Datei öffnen + + + + Save File + Datei speichern + + + + Fm::FileMenu + + + Open + Öffnen + + + + Create &New + &Neu erstellen + + + + &Restore + Wiede&rherstellen + + + + Cut + Ausschneiden + + + + Copy + Kopieren + + + + Paste + Einfügen + + + + + &Move to Trash + In den Papierkorb &verschieben + + + + Trust selected executables + Ausgewählten ausführbaren Dateien vertrauen + + + + Trust this executable + Dieser ausführbaren Datei vertrauen + + + + Output + Ausgabe + + + + &Delete + &Löschen + + + + Rename + Umbenennen + + + + Open With... + Öffnen mit... + + + + Other Applications + Andere Anwendungen + + + + Extract to... + Entpacken nach... + + + + Extract Here + Hier entpacken + + + + Compress + Komprimieren + + + + Properties + Eigenschaften + + + + Fm::FileOperation + + + Error + Fehler + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + Einige Dateien können nicht in den Papierkorb verschoben werden, da die zugrundeliegenden Dateisysteme den Vorgang nicht unterstützen. +Möchtest Sie sie stattdessen löschen? + + + + + Confirm + Bestätigen + + + + Do you want to delete the selected files? + Möchten Sie die ausgewählten Dateien löschen? + + + + Do you want to move the selected files to trash can? + Möchten Sie die ausgewählten Dateien in den Papierkorb verschieben? + + + + Fm::FileOperationDialog + + + Move files + Dateien verschieben + + + + Moving the following files to destination folder: + Verschiebe die folgenden Dateien in den Zielordner: + + + + Copy Files + Dateien kopieren + + + + Copying the following files to destination folder: + Kopiere die folgenden Dateien in den Zielordner: + + + + Trash Files + Dateien für den Papierkorb + + + + Moving the following files to trash can: + Verschiebe die folgenden Dateien in den Papierkorb: + + + + Delete Files + Dateien löschen + + + + Deleting the following files: + Lösche die folgenden Dateien: + + + + Create Symlinks + Symbolische Verknüpfungen erstellen + + + + Creating symlinks for the following files: + Erstelle symbolische Verknüpfungen für die folgenden Dateien: + + + + Change Attributes + Eigenschaften ändern + + + + Changing attributes of the following files: + Ändere die Eigenschaften der folgenden Dateien: + + + + Restore Trashed Files + Dateien aus dem Papierkorb wiederherstellen + + + + Restoring the following files from trash can: + Stelle folgende Dateien aus dem Papierkorb wieder her: + + + + + Error + Fehler + + + + Fm::FilePropsDialog + + + View folder content + Ordnerinhalt ansehen + + + + View and modify folder content + Ordnerinhalt ansehen und verändern + + + + Read + Lesen + + + + Read and write + Lesen und Schreiben + + + + Forbidden + Unzulässig + + + + Files of different types + Dateien unterschiedlicher Typen + + + + Multiple Files + Mehrere Dateien + + + + %p% used + %p% benutzt + + + + %1 Free of %2 + %1 frei von %2 + + + + no file + Keine Datei + + + + one file + eine Datei + + + + %1 files + %1 Dateien + + + + Select an icon + Wählen Sie ein Symbol aus + + + + Images (*.png *.xpm *.svg *.svgz ) + Bilder (*.png *.xpm *.svg *.svgz ) + + + + Apply changes + Änderungen anwenden + + + + Do you want to recursively apply these changes to all files and sub-folders? + Möchten Sie diese Änderungen rekursiv auf alle Dateien und Unterordner anwenden? + + + + Fm::FileSearchDialog + + + Error + Fehler + + + + You should add at least one directory to search. + Sie sollten mindestens ein Verzeichnis zur Suche hinzufügen. + + + + Select a folder + Wählen Sie einen Ordner aus + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + Kann keine Verknüpfung auf einem nicht-nativen Dateisystem erstellen + + + + Fm::FolderMenu + + + Create &New + &Neu erstellen + + + + &Paste + &Einfügen + + + + Select &All + &Alles auswählen + + + + Invert Selection + Auswahl umkehren + + + + Sorting + Sortierung + + + + Show Hidden + Versteckte anzeigen + + + + Folder Pr&operties + &Ordnereigenschaften + + + + Output + Ausgabe + + + + By File Name + Nach Dateiname + + + + By Modification Time + Nach Änderungszeit + + + + By File Size + Nach Dateigröße + + + + By File Type + Nach Dateityp + + + + By File Owner + Nach Dateibesitzer + + + + Ascending + Aufsteigend + + + + Descending + Absteigend + + + + Folder First + Ordner zuerst + + + + Case Sensitive + Groß-/ Kleinschreibung beachten + + + + Fm::FolderModel + + + Name + Name + + + + Type + Typ + + + + Size + Größe + + + + Modified + Geändert + + + + Owner + Besitzer + + + + Group + Gruppe + + + + Fm::FontButton + + + Bold + Fett + + + + Italic + Kursiv + + + + Fm::MountOperationPasswordDialog + + + &Connect + &Verbinden + + + + Fm::PathBar + + + &Edit Path + Pfad &bearbeiten + + + + &Copy Path + Pfad &kopieren + + + + Fm::PlacesModel + + + Places + Orte + + + + Desktop + Schreibtisch + + + + Trash + Papierkorb + + + + Computer + Rechner + + + + Applications + Anwendungen + + + + Network + Netzwerk + + + + Devices + Geräte + + + + Bookmarks + Lesezeichen + + + + Fm::PlacesView + + + Empty Trash + Papierkorb leeren + + + + Open in New Tab + Öffne in neuer Registerkarte + + + + Open in New Window + In neuem Fenster öffnen + + + + + Hide + Ausblenden + + + + Move Bookmark Up + Lesezeichen nach oben verschieben + + + + Move Bookmark Down + Lesezeichen nach unten verschieben + + + + Rename Bookmark + Lesezeichen umbenennen + + + + Remove Bookmark + Lesezeichen entfernen + + + + + Unmount + Aushängen + + + + Mount + Einhängen + + + + Eject + Auswerfen + + + + Show All Entries + Alle Einträge anzeigen + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + Typ: %1 +Größe: %2 +Geändert: %3 + + + + + Type: %1 +Modified: %2 + Typ: %1 +Geändert: %2 + + + + &Overwrite + &Überschreiben + + + + &Rename + &Umbenennen + + + + Fm::SidePane + + + Places + Orte + + + + Directory Tree + Verzeichnisbaum + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + Datei '1%' ist aus dem Papierkorb nicht wieder herstellbar: ursprünglicher Pfad nicht bekannt + + + + MountOperationPasswordDialog + + + Mount + Einhängen + + + + Connect &anonymously + &Anonym verbinden + + + + Connect as u&ser: + Als &Benutzer verbinden: + + + + &Username: + Ben&utzername: + + + + &Password: + &Passwort: + + + + &Domain: + &Domäne: + + + + Forget password &immediately + Passwort &sofort vergessen + + + + Remember password until you &logout + Passwort bis zum Abme&lden merken + + + + Remember &forever + Passwort &für immer merken + + + + QObject + + + + + + + Error + Fehler + + + + Rename File + Datei umbenennen + + + + Please enter a new name: + Bitte geben Sie einen neuen Namen ein: + + + + Create Folder + Ordner erstellen + + + + Please enter a new file name: + Bitte geben Sie einen neuen Dateinamen ein: + + + + New text file + Neue Textdatei + + + + Please enter a new folder name: + Bitte geben Sie einen neuen Ordnernamen ein: + + + + New folder + Neuer Ordner + + + + Enter a name for the new %1: + Geben Sie einen Namen für %1 ein: + + + + Create File + Datei erstellen + + + + Custom Icon Error + Benutzerdefiniertes-Symbol Fehler + + + + The path is not mounted. + Der Pfad ist nicht eingehängt. + + + + Invalid desktop entry file: '%1' + Ungültige Schreibtischeintragsdatei: '1%' + + + + No default application is set to launch '%1' + Es ist keine Standardanwendung zum Starten von '%1' festgelegt + + + + Cannot set working directory to '%1': %2 + Arbeitsverzeichnis kann nicht auf '%1' festgelegt werden: %2 + + + + Identifier: + Bezeichner: + + + + RenameDialog + + + Confirm to replace files + Überschreiben von Dateien bestätigen + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + <html><head/><body><p><span style=" font-weight:600;">Es gibt bereits eine gleichnamige Datei an diesem Ort.</span></p><p>Möchten Sie die vorhandene Datei ersetzen?</p></body></html> + + + + dest + Ziel + + + + with the following file? + mit der folgenden Datei? + + + + src file info + Info über die Quelldatei + + + + dest file info + Info über die Zieldatei + + + + src + Quelle + + + + &File name: + &Dateiname: + + + + Apply this option to all existing files + Diese Option auf alle existierenden Dateien anwenden + + + + SearchDialog + + + Search Files + Dateien suchen + + + + Name/Location + Name/Ort + + + + File Name Patterns: + Muster für Dateinamen: + + + + * + * + + + + Case insensitive + Groß-/Kleinschreibung ignorieren + + + + Use regular expression + Regulären Ausdruck verwenden + + + + Places to Search: + Zu durchsuchende Orte: + + + + &Add + &Hinzufügen + + + + &Remove + Entfe&rnen + + + + Search in sub directories + In Unterverzeichnissen suchen + + + + Search for hidden files + Nach versteckten Dateien suchen + + + + File Type + Dateityp + + + + Only search for files of following types: + Nur nach Dateien der folgenden Typen suchen: + + + + Text files + Textdateien + + + + Image files + Bilddateien + + + + Audio files + Audiodateien + + + + Video files + Videodateien + + + + Documents + Dokumente + + + + Folders + Ordner + + + + Content + Inhalt + + + + File contains: + Datei enthält: + + + + Case insensiti&ve + Groß- /Kleinschreibung ig&norieren + + + + &Use regular expression + Reg&ulären Ausdruck verwenden + + + + Properties + Eigenschaften + + + + File Size: + Dateigröße: + + + + Larger than: + Größer als: + + + + + Bytes + Bytes + + + + + KiB + KiB + + + + + MiB + MiB + + + + + GiB + GiB + + + + Smaller than: + Kleiner als: + + + + Last Modified Time: + Letzte Änderungszeit: + + + + Earlier than: + Früher als: + + + + Later than: + Später als: + + + diff --git a/src/translations/libfm-qt_el.ts b/src/translations/libfm-qt_el.ts new file mode 100644 index 0000000..f42e579 --- /dev/null +++ b/src/translations/libfm-qt_el.ts @@ -0,0 +1,1560 @@ + + + + + AppChooserDialog + + + Choose an Application + Επιλέξτε μια εφαρμογή + + + + Installed Applications + Εγκατεστημένες εφαρμογές + + + + Custom Command + Προσαρμοσμένη εντολή + + + + Command line to execute: + Γραμμή εντολών προς εκτέλεση: + + + + Application name: + Όνομα της εφαρμογής: + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + <b>Αυτοί οι ειδικοί κωδικοί μπορούν να χρησιμοποιηθούν στη γραμμή εντολών:</b> +<ul> +<li><b>%f</b>: Αναπαριστά ένα όνομα αρχείου</li> +<li><b>%F</b>: Αναπαριστά πολλαπλά ονόματα αρχείων</li> +<li><b>%u</b>: Αναπαριστά ένα URI του αρχείου</li> +<li><b>%U</b>: Αναπαριστά πολλαπλά URI</li> +</ul> + + + + Keep terminal window open after command execution + Διατήρηση του παραθύρου του τερματικού ανοιχτό μετά την εκτέλεση της εντολής + + + + Execute in terminal emulator + Εκτέλεση στον προσομοιωτή τερματικού + + + + Set selected application as default action of this file type + Ορίστε την επιλεγμένη εφαρμογή ως την εξ ορισμού ενέργεια για αυτού του τύπου αρχεία + + + + EditBookmarksDialog + + + Edit Bookmarks + Επεξεργασία σελιδοδεικτών + + + + Name + Όνομα + + + + Location + Τοποθεσία + + + + &Add Item + &Προσθήκη αντικειμένου + + + + &Remove Item + &Αφαίρεση αντικειμένου + + + + Use drag and drop to reorder the items + Χρησιμοποιήστε τη μεταφορά και απόθεση για αναδιάταξη των αντικειμένων + + + + ExecFileDialog + + + Execute file + Εκτέλεση του αρχείου + + + + &Open + Ά&νοιγμα + + + + E&xecute + &Εκτέλεση + + + + Execute in &Terminal + Εκτέλεση στο &τερματικό + + + + Cancel + Ακύρωση + + + + FileDialog + + + Location: + Τοποθεσία: + + + + File name: + Όνομα αρχείου: + + + + File type: + Τύπος αρχείου: + + + + FileOperationDialog + + + Destination: + Προορισμός: + + + + Processing: + Επεξεργασία: + + + + Preparing... + Προετοιμασία... + + + + Progress + Πρόοδος + + + + Time remaining: + Υπολειπόμενος χρόνος: + + + + Files processed: + Επεξεργασμένα αρχεία: + + + + FilePropsDialog + + + File Properties + Ιδιότητες του αρχείου + + + + General + Γενικά + + + + Location: + Τοποθεσία: + + + + File type: + Τύπος αρχείου: + + + + MIME type: + Τύπος Mime: + + + + File size: + Μέγεθος αρχείου: + + + + On-disk size: + Μέγεθος στον δίσκο: + + + + Last modified: + Τελευταία τροποποίηση: + + + + Link target: + Προορισμός συνδέσμου: + + + + Open With: + Άνοιγμα με: + + + + Last accessed: + Τελευταία προσπέλαση: + + + + Contains: + Περιέχει: + + + + Device Usage: + Χρήση συσκευής: + + + + Permissions + Άδειες + + + + Ownership + Ιδιοκτησία + + + + + + Group: + Ομάδα: + + + + + + Owner: + Ιδιοκτήτης: + + + + Access Control + Έλεγχος πρόσβασης + + + + + Other: + Άλλο: + + + + Make the file executable + Ορισμός ως εκτελέσιμο + + + + + + Read + Ανάγνωση + + + + + + Write + Εγγραφή + + + + + + Execute + Εκτέλεση + + + + Sticky + Κολλημένο + + + + SetUID + SetUID + + + + SetGID + SetGID + + + + Advanced Mode + Προηγμένη λειτουργία + + + + Fm::AppChooserComboBox + + + Customize + Προσαρμοσμένο + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + Επιλέξτε μια εφαρμογή για το άνοιγμα των αρχείων «%1» + + + + Fm::CreateNewMenu + + + Folder + Φάκελος + + + + Blank File + Κενό αρχείο + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + Ο κατάλογος '%1' δεν είναι έγκυρος + + + + Fm::DirTreeModel + + + Loading... + Φόρτωση... + + + + + + <No sub folders> + <Κανένας υποφάκελος> + + + + Fm::DirTreeView + + + Open in New T&ab + Άνοιγμα σε νέα &καρτέλα + + + + Open in New Win&dow + Άνοιγμα σε νέο &παράθυρο + + + + Open in Termina&l + Άνοιγμα στο &τερματικό + + + + Fm::DndActionMenu + + + Copy here + Αντιγραφή εδώ + + + + Move here + Μετακίνηση εδώ + + + + Create symlink here + Δημιουργία συμβολικού δεσμού εδώ + + + + Cancel + Ακύρωση + + + + Fm::EditBookmarksDialog + + + New bookmark + Νέος σελιδοδείκτης + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + Το αρχείο «%1» φαίνεται να είναι ένα αρχείο επιφάνειας εργασίας. +Ποια ενέργεια θέλετε να πραγματοποιήσετε; + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + Το αρχείο κειμένου «%1» φαίνεται ότι είναι ένα εκτελέσιμο σενάριο. +Τι θέλετε να κάνετε με αυτό; + + + + This file '%1' is executable. Do you want to execute it? + Αυτό το αρχείο «%1» είναι εκτελέσιμο. Θέλετε να το εκτελέσετε; + + + + Fm::FileDialog + + + Go Back + Μετάβαση πίσω + + + + Alt+Left + Go Back + Alt+Αριστερά + + + + Go Forward + Μετάβαση εμπρός + + + + Alt+Right + Go Forward + Alt+Δεξιά + + + + Reload + Επαναφόρτωση + + + + F5 + Reload + F5 + + + + Create Folder + Δημιουργία φακέλου + + + + Icon View + Προβολή εικονιδίων + + + + Thumbnail View + Προβολή εικόνων επισκόπησης + + + + Compact View + Συμπαγής προβολή + + + + Detailed List View + Προβολή αναλυτικού κατάστιχου + + + + + Error + Σφάλμα + + + + Please select a file + Παρακαλώ επιλέξτε ένα αρχείο + + + + %1 already exists. +Do you want to replace it? + Το αρχείο %1 υπάρχει ήδη. +Θέλετε να το αντικαταστήσετε; + + + + Path "%1" does not exist + Η διαδρομή «%1» δεν υπάρχει + + + + "%1" is not a directory + Το «%1» δεν είναι κατάλογος + + + + "%1" is not a file + Το «%1» δεν είναι αρχείο + + + + + &Open + Ά&νοιγμα + + + + + &Save + Απο&θήκευση + + + + All Files (*) + Όλα τα αρχεία (*) + + + + Fm::FileDialogHelper + + + Open File + Άνοιγμα αρχείου + + + + Save File + Αποθήκευση αρχείου + + + + Fm::FileMenu + + + Open + Άνοιγμα + + + + Open With... + Άνοιγμα με... + + + + Other Applications + Άλλες εφαρμογές + + + + Create &New + Δημιουργία &νέου + + + + &Restore + &Επαναφορά + + + + Cut + Αποκοπή + + + + Copy + Αντιγραφή + + + + Paste + Επικόλληση + + + + + &Move to Trash + &Μετακίνηση στα απορρίμματα + + + + Rename + Μετονομασία + + + + Extract to... + Εξαγωγή σε... + + + + Extract Here + Εξαγωγή εδώ + + + + Compress + Συμπίεση + + + + Properties + Ιδιότητες + + + + Trust selected executables + Τα επιλεγμένα εκτελέσιμα είναι έμπιστα + + + + Trust this executable + Το εκτελέσιμο είναι έμπιστο + + + + Output + Έξοδος + + + + &Delete + &Διαγραφή + + + + Fm::FileOperation + + + Error + Σφάλμα + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + Μερικά αρχεία δεν μπορούν να μετακινηθούν στον κάδο απορριμμάτων διότι το υποκείμενο αρχείο συστήματος δεν υποστηρίζει αυτήν την ενέργεια. +Θέλετε αντί αυτού να τα διαγράψετε; + + + + + Confirm + Επιβεβαίωση + + + + Do you want to delete the selected files? + Επιθυμείτε την διαγραφή των επιλεγμένων αρχείων; + + + + Do you want to move the selected files to trash can? + Επιθυμείτε την μετακίνηση των επιλεγμένων αρχείων στον κάδο απορριμμάτων; + + + + Fm::FileOperationDialog + + + Move files + Μετακίνηση των αρχείων + + + + Moving the following files to destination folder: + Μετακίνηση των ακολούθων αρχείων στον φάκελο προορισμού: + + + + Copy Files + Αντιγραφή των αρχείων + + + + Copying the following files to destination folder: + Αντιγραφή των ακολούθων αρχείων στον φάκελο προορισμού: + + + + Trash Files + Ρίψη των αρχείων στα απορρίμματα + + + + Moving the following files to trash can: + Μετακίνηση των ακολούθων αρχείων στον κάδο απορριμμάτων: + + + + Delete Files + Διαγραφή των αρχείων + + + + Deleting the following files: + Διαγραφή των ακολούθων αρχείων: + + + + Create Symlinks + Δημιουργία συμβολικών δεσμών + + + + Creating symlinks for the following files: + Δημιουργία συμβολικών δεσμών για τα ακόλουθα αρχεία: + + + + Change Attributes + Αλλαγή ιδιοχαρακτηριστικών + + + + Changing attributes of the following files: + Αλλαγή των ιδιοχαρακτηριστικών των ακολούθων αρχείων: + + + + Restore Trashed Files + Επαναφορά των αρχείων από τον κάδο απορριμμάτων + + + + Restoring the following files from trash can: + Επαναφέρονται τα παρακάτω αρχεία από τον κάδο απορριμμάτων: + + + + + Error + Σφάλμα + + + + Fm::FilePropsDialog + + + View folder content + Προβολή των περιεχομένων του φακέλου + + + + View and modify folder content + Προβολή και τροποποίηση των περιεχομένων του φακέλου + + + + Read + Ανάγνωση + + + + Read and write + Ανάγνωση και εγγραφή + + + + Forbidden + Απαγορευμένο + + + + Files of different types + Αρχεία διαφορετικού τύπου + + + + Multiple Files + Πολλαπλά αρχεία + + + + %p% used + %p% σε χρήση + + + + %1 Free of %2 + %1 Ελεύθερα από %2 + + + + no file + κανένα αρχείο + + + + one file + ένα αρχείο + + + + %1 files + %1 αρχεία + + + + Select an icon + Επιλογή εικονιδίου + + + + Images (*.png *.xpm *.svg *.svgz ) + Εικόνες (*.png *.xpm *.svg *.svgz ) + + + + Apply changes + Εφαρμογή των αλλαγών + + + + Do you want to recursively apply these changes to all files and sub-folders? + Θέλετε να εφαρμόσετε αναδρομικά αυτές τις αλλαγές σε όλα τα αρχεία και υποφακέλους; + + + + Fm::FileSearchDialog + + + Error + Σφάλμα + + + + You should add at least one directory to search. + Για την αναζήτηση θα πρέπει να προσθέσετε τουλάχιστον έναν κατάλογο. + + + + Select a folder + Επιλογή φακέλου + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + Αδύνατη η δημιουργία δεσμού σε μη εγγενές σύστημα αρχείων + + + + Fm::FolderMenu + + + Create &New + Δημιουργία &νέου + + + + &Paste + Επι&κόλληση + + + + Select &All + Επιλογή ό&λων + + + + Invert Selection + Αντιστροφή επιλογής + + + + Sorting + Ταξινόμηση + + + + Show Hidden + Εμφάνιση των κρυφών + + + + Folder Pr&operties + Ι&διότητες του φακέλου + + + + Output + Έξοδος + + + + By File Name + Ανά όνομα αρχείου + + + + By Modification Time + Ανά χρόνο τροποποίησης + + + + By File Size + Ανά μέγεθος αρχείου + + + + By File Type + Ανά τύπο αρχείου + + + + By File Owner + Ανά ιδιοκτήτη αρχείου + + + + Ascending + Αύξουσα + + + + Descending + Φθίνουσα + + + + Folder First + Οι φάκελοι πρώτα + + + + Case Sensitive + Διάκριση πεζών/κεφαλαίων + + + + Fm::FolderModel + + + Name + Όνομα + + + + Type + Τύπος + + + + Size + Μέγεθος + + + + Modified + Τροποποιήθηκε + + + + Owner + Ιδιοκτήτης + + + + Group + Ομάδα + + + + Fm::FontButton + + + Bold + Έντονα + + + + Italic + Πλάγια + + + + Fm::MountOperationPasswordDialog + + + &Connect + &Σύνδεση + + + + Fm::PathBar + + + &Edit Path + &Επεξεργασία της διαδρομής + + + + &Copy Path + &Αντιγραφή της διαδρομής + + + + Fm::PlacesModel + + + Places + Τοποθεσίες + + + + Desktop + Επιφάνεια εργασίας + + + + Computer + Υπολογιστής + + + + Applications + Εφαρμογές + + + + Network + Δίκτυο + + + + Devices + Συσκευές + + + + Bookmarks + Σελιδοδείκτες + + + + Trash + Απορρίμματα + + + + Fm::PlacesView + + + Open in New Tab + Άνοιγμα σε νέα καρτέλα + + + + Open in New Window + Άνοιγμα σε νέο παράθυρο + + + + Empty Trash + Άδειασμα των απορριμμάτων + + + + + Hide + Απόκρυψη + + + + Move Bookmark Up + Μετακίνηση του σελιδοδείκτη προς τα πάνω + + + + Move Bookmark Down + Μετακίνηση του σελιδοδείκτη προς τα κάτω + + + + Rename Bookmark + Μετονομασία σελιδοδείκτη + + + + Remove Bookmark + Αφαίρεση σελιδοδείκτη + + + + + Unmount + Αποπροσάρτηση + + + + Mount + Προσάρτηση + + + + Eject + Εξαγωγή + + + + Show All Entries + Εμφάνιση όλων των καταχωρήσεων + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + Τύπος: %1 +Μέγεθος: %2 +Τροποποιήθηκε: %3 + + + + + Type: %1 +Modified: %2 + Τύπος: %1 +Τροποποιήθηκε: %2 + + + + &Overwrite + &Αντικατάσταση + + + + &Rename + &Μετονομασία + + + + Fm::SidePane + + + Places + Τοποθεσίες + + + + Directory Tree + Δέντρο καταλόγων + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + Αδύνατη η επαναφορά του αρχείου «%s» από τα απορρίμματα: άγνωστη αρχική διαδρομή + + + + MountOperationPasswordDialog + + + Mount + Προσάρτηση + + + + Connect &anonymously + &Ανώνυμη σύνδεση + + + + Connect as u&ser: + Σύνδεση ως &χρήστης: + + + + &Username: + Όνομα χ&ρήστη: + + + + &Password: + &Κωδικός πρόσβασης: + + + + &Domain: + &Τομέας: + + + + Forget password &immediately + &Λήθη του κωδικού πρόσβασης άμεσα + + + + Remember password until you &logout + Απομνημόνευση του κωδικού πρόσβασης &μέχρι να αποσυνδεθείτε + + + + Remember &forever + Απομνημόνευση ε&σαεί + + + + QObject + + + Rename File + Μετονομασία αρχείου + + + + Please enter a new name: + Παρακαλώ εισαγάγετε ένα νέο όνομα: + + + + + + + + Error + Σφάλμα + + + + Create Folder + Δημιουργία φακέλου + + + + Create File + Δημιουργία αρχείου + + + + Please enter a new file name: + Παρακαλώ εισαγάγετε ένα νέο όνομα αρχείου: + + + + New text file + Νέο αρχείο κειμένου + + + + Please enter a new folder name: + Παρακαλώ εισαγάγετε ένα νέο όνομα φακέλου: + + + + New folder + Νέος φάκελος + + + + Enter a name for the new %1: + Εισαγάγετε ένα όνομα για το νέο %1: + + + + Custom Icon Error + Σφάλμα προσαρμοσμένου εικονιδίου + + + + The path is not mounted. + Η διαδρομή δεν έχει προσαρτηθεί. + + + + Invalid desktop entry file: '%1' + Μη έγκυρο αρχείο επιφάνειας εργασίας: «%1» + + + + No default application is set to launch '%1' + Καμία εξ ορισμού εφαρμογή για την εκτέλεση του '%1' + + + + Cannot set working directory to '%1': %2 + Αδύνατος ο ορισμός του καταλόγου εργασίας «%1»: %2 + + + + Identifier: + Αναγνωριστικό: + + + + RenameDialog + + + Confirm to replace files + Επιβεβαίωση αντικατάστασης των αρχείων + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + <html><head/><body><p><span style=" font-weight:600;">Υπάρχει ήδη ένα αρχείο με το ίδιο όνομα στην συγκεκριμένη τοποθεσία.</span></p><p>Θέλετε να αντικαταστήσετε το υπάρχον αρχείο;</p></body></html> + + + + dest + προορισμός + + + + with the following file? + με το παρακάτω αρχείο; + + + + src file info + πληροφορίες αρχείου πηγής + + + + dest file info + πληροφορίες αρχείου προορισμού + + + + src + πηγή + + + + &File name: + &Όνομα αρχείου: + + + + Apply this option to all existing files + Εφαρμογή της επιλογής σε όλα τα υπάρχοντα αρχεία + + + + SearchDialog + + + Search Files + Αναζήτηση αρχείων + + + + Name/Location + Όνομα/Τοποθεσία + + + + File Name Patterns: + Σχηματομορφές ονομάτων αρχείων: + + + + * + * + + + + Case insensitive + Δίχως διάκριση πεζών/κεφαλαίων + + + + Use regular expression + Χρήση κανονικής έκφρασης + + + + Places to Search: + Τοποθεσίες προς αναζήτηση: + + + + &Add + &Προσθήκη + + + + &Remove + Α&φαίρεση + + + + Search in sub directories + Αναζήτηση σε υποφακέλους + + + + Search for hidden files + Αναζήτηση για κρυφά αρχεία + + + + File Type + Τύπος αρχείου + + + + Only search for files of following types: + Αναζήτηση αρχείων μόνο για τους ακόλουθους τύπους: + + + + Text files + Αρχεία κειμένου + + + + Image files + Αρχεία εικόνων + + + + Audio files + Αρχεία ήχου + + + + Video files + Αρχεία βίντεο + + + + Documents + Έγγραφα + + + + Folders + Φάκελοι + + + + Content + Περιεχόμενο + + + + File contains: + Αρχεία που περιέχουν: + + + + Case insensiti&ve + Δίχως διάκριση &πεζών/κεφαλαίων + + + + &Use regular expression + &Χρήση κανονικής έκφρασης + + + + Properties + Ιδιότητες + + + + File Size: + Μέγεθος αρχείου: + + + + Larger than: + Μεγαλύτερο από: + + + + + Bytes + Byte + + + + + KiB + KiB + + + + + MiB + MiB + + + + + GiB + GiB + + + + Smaller than: + Μικρότερο από: + + + + Last Modified Time: + Χρόνος της τελευταίας τροποποίησης: + + + + Earlier than: + Προγενέστερα από: + + + + Later than: + Αργότερα από: + + + diff --git a/src/translations/libfm-qt_en_GB.ts b/src/translations/libfm-qt_en_GB.ts new file mode 100644 index 0000000..9f2821d --- /dev/null +++ b/src/translations/libfm-qt_en_GB.ts @@ -0,0 +1,1548 @@ + + + + + AppChooserDialog + + + Choose an Application + + + + + Installed Applications + + + + + Custom Command + + + + + Command line to execute: + + + + + Application name: + + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + + + + + Keep terminal window open after command execution + + + + + Execute in terminal emulator + + + + + Set selected application as default action of this file type + + + + + EditBookmarksDialog + + + Edit Bookmarks + + + + + Name + + + + + Location + + + + + &Add Item + + + + + &Remove Item + + + + + Use drag and drop to reorder the items + + + + + ExecFileDialog + + + Execute file + + + + + &Open + + + + + E&xecute + + + + + Execute in &Terminal + + + + + Cancel + + + + + FileDialog + + + Location: + + + + + File name: + + + + + File type: + + + + + FileOperationDialog + + + Destination: + + + + + Processing: + + + + + Preparing... + + + + + Progress + + + + + Time remaining: + + + + + Files processed: + + + + + FilePropsDialog + + + File Properties + + + + + General + + + + + Location: + + + + + File type: + + + + + MIME type: + + + + + File size: + + + + + On-disk size: + + + + + Last modified: + + + + + Link target: + + + + + Open With: + + + + + Last accessed: + + + + + Contains: + + + + + Device Usage: + + + + + Permissions + + + + + Ownership + + + + + + + Group: + + + + + + + Owner: + + + + + Access Control + + + + + + Other: + + + + + Make the file executable + + + + + + + Read + + + + + + + Write + + + + + + + Execute + + + + + Sticky + + + + + SetUID + + + + + SetGID + + + + + Advanced Mode + + + + + Fm::AppChooserComboBox + + + Customize + Customise + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + + + + + Fm::CreateNewMenu + + + Folder + + + + + Blank File + + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + + + + + Fm::DirTreeModel + + + Loading... + + + + + + + <No sub folders> + + + + + Fm::DirTreeView + + + Open in New T&ab + + + + + Open in New Win&dow + + + + + Open in Termina&l + + + + + Fm::DndActionMenu + + + Copy here + + + + + Move here + + + + + Create symlink here + + + + + Cancel + + + + + Fm::EditBookmarksDialog + + + New bookmark + + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + + + + + This file '%1' is executable. Do you want to execute it? + + + + + Fm::FileDialog + + + Go Back + + + + + Alt+Left + Go Back + + + + + Go Forward + + + + + Alt+Right + Go Forward + + + + + Reload + + + + + F5 + Reload + + + + + Create Folder + + + + + Icon View + + + + + Thumbnail View + + + + + Compact View + + + + + Detailed List View + + + + + + Error + + + + + Please select a file + + + + + %1 already exists. +Do you want to replace it? + + + + + Path "%1" does not exist + + + + + "%1" is not a directory + + + + + "%1" is not a file + + + + + + &Open + + + + + + &Save + + + + + All Files (*) + + + + + Fm::FileDialogHelper + + + Open File + + + + + Save File + + + + + Fm::FileMenu + + + Open + + + + + Open With... + + + + + Other Applications + + + + + Create &New + + + + + &Restore + + + + + Cut + + + + + Copy + + + + + Paste + + + + + + &Move to Trash + + + + + Rename + + + + + Extract to... + + + + + Extract Here + + + + + Compress + + + + + Properties + + + + + Trust selected executables + Trust selected executable(s) + + + + Trust this executable + + + + + Output + + + + + &Delete + + + + + Fm::FileOperation + + + Error + + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + Some files cannot be moved to trash can because the underlying file-systems don't support this operation. +Do you want to delete them instead? + + + + + Confirm + + + + + Do you want to delete the selected files? + + + + + Do you want to move the selected files to trash can? + + + + + Fm::FileOperationDialog + + + Move files + + + + + Moving the following files to destination folder: + + + + + Copy Files + + + + + Copying the following files to destination folder: + + + + + Trash Files + + + + + Moving the following files to trash can: + + + + + Delete Files + + + + + Deleting the following files: + + + + + Create Symlinks + + + + + Creating symlinks for the following files: + + + + + Change Attributes + + + + + Changing attributes of the following files: + + + + + Restore Trashed Files + + + + + Restoring the following files from trash can: + + + + + + Error + + + + + Fm::FilePropsDialog + + + View folder content + + + + + View and modify folder content + + + + + Read + + + + + Read and write + + + + + Forbidden + + + + + Files of different types + + + + + Multiple Files + + + + + %p% used + + + + + %1 Free of %2 + + + + + no file + + + + + one file + + + + + %1 files + + + + + Select an icon + + + + + Images (*.png *.xpm *.svg *.svgz ) + + + + + Apply changes + + + + + Do you want to recursively apply these changes to all files and sub-folders? + + + + + Fm::FileSearchDialog + + + Error + + + + + You should add at least one directory to search. + + + + + Select a folder + + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + Cannot create a link on non-native file-system + + + + Fm::FolderMenu + + + Create &New + + + + + &Paste + + + + + Select &All + + + + + Invert Selection + + + + + Sorting + + + + + Show Hidden + + + + + Folder Pr&operties + + + + + Output + + + + + By File Name + + + + + By Modification Time + + + + + By File Size + + + + + By File Type + + + + + By File Owner + + + + + Ascending + + + + + Descending + + + + + Folder First + + + + + Case Sensitive + + + + + Fm::FolderModel + + + Name + + + + + Type + + + + + Size + + + + + Modified + + + + + Owner + + + + + Group + + + + + Fm::FontButton + + + Bold + + + + + Italic + + + + + Fm::MountOperationPasswordDialog + + + &Connect + + + + + Fm::PathBar + + + &Edit Path + + + + + &Copy Path + + + + + Fm::PlacesModel + + + Places + + + + + Desktop + + + + + Computer + + + + + Applications + + + + + Network + + + + + Devices + + + + + Bookmarks + + + + + Trash + + + + + Fm::PlacesView + + + Open in New Tab + + + + + Open in New Window + + + + + Empty Trash + + + + + + Hide + + + + + Move Bookmark Up + + + + + Move Bookmark Down + + + + + Rename Bookmark + + + + + Remove Bookmark + + + + + + Unmount + + + + + Mount + + + + + Eject + + + + + Show All Entries + + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + + + + + + Type: %1 +Modified: %2 + + + + + &Overwrite + + + + + &Rename + + + + + Fm::SidePane + + + Places + + + + + Directory Tree + + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + Cannot restore file '%s': original path not known + + + + MountOperationPasswordDialog + + + Mount + + + + + Connect &anonymously + + + + + Connect as u&ser: + + + + + &Username: + + + + + &Password: + + + + + &Domain: + + + + + Forget password &immediately + + + + + Remember password until you &logout + + + + + Remember &forever + + + + + QObject + + + Rename File + + + + + Please enter a new name: + + + + + + + + + Error + + + + + Create Folder + + + + + Create File + + + + + Please enter a new file name: + + + + + New text file + + + + + Please enter a new folder name: + + + + + New folder + + + + + Enter a name for the new %1: + + + + + Custom Icon Error + + + + + The path is not mounted. + + + + + Invalid desktop entry file: '%1' + + + + + No default application is set to launch '%1' + + + + + Cannot set working directory to '%1': %2 + + + + + Identifier: + + + + + RenameDialog + + + Confirm to replace files + + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + + + + + dest + + + + + with the following file? + + + + + src file info + + + + + dest file info + + + + + src + + + + + &File name: + + + + + Apply this option to all existing files + + + + + SearchDialog + + + Search Files + + + + + Name/Location + + + + + File Name Patterns: + + + + + * + + + + + Case insensitive + + + + + Use regular expression + + + + + Places to Search: + + + + + &Add + + + + + &Remove + + + + + Search in sub directories + + + + + Search for hidden files + + + + + File Type + + + + + Only search for files of following types: + + + + + Text files + + + + + Image files + + + + + Audio files + + + + + Video files + + + + + Documents + + + + + Folders + + + + + Content + + + + + File contains: + + + + + Case insensiti&ve + + + + + &Use regular expression + + + + + Properties + + + + + File Size: + + + + + Larger than: + + + + + + Bytes + + + + + + KiB + + + + + + MiB + + + + + + GiB + + + + + Smaller than: + + + + + Last Modified Time: + + + + + Earlier than: + + + + + Later than: + + + + diff --git a/src/translations/libfm-qt_es.ts b/src/translations/libfm-qt_es.ts new file mode 100644 index 0000000..4bc7288 --- /dev/null +++ b/src/translations/libfm-qt_es.ts @@ -0,0 +1,1560 @@ + + + + + AppChooserDialog + + + Choose an Application + Elija una aplicación + + + + Installed Applications + Aplicaciones instaladas + + + + Custom Command + Comando personalizado + + + + Command line to execute: + Línea de comandos a ejecutar: + + + + Application name: + Nombre de la aplicación: + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + <b>Estos códigos especiales pueden ser usados en la línea de comandos:</b> +<ul> +<li><b>%f</b>: Representa un solo nombre de archivo</li> +<li><b>%F</b>: Representa múltiples nombres de archivos</li> +<li><b>%u</b>: Representa un solo URI del archivo</li> +<li><b>%U</b>: Representa múltiples URI</li> +</ul> + + + + Keep terminal window open after command execution + Mantener la terminal abierta tras la ejecución del comando + + + + Execute in terminal emulator + Ejecutar en un emulador de terminal + + + + Set selected application as default action of this file type + Establecer la aplicación seleccionada como predefinida para este tipo de archivo + + + + EditBookmarksDialog + + + Edit Bookmarks + Editar marcadores + + + + Name + Nombre + + + + Location + Ubicación + + + + &Add Item + &Añadir elemento + + + + &Remove Item + &Eliminar elemento + + + + Use drag and drop to reorder the items + Arrastre y suelte para reordenar los elementos + + + + ExecFileDialog + + + Execute file + Ejecutar archivo + + + + &Open + &Abrir + + + + E&xecute + &Ejecutar + + + + Execute in &Terminal + Ejecutar en una &terminal + + + + Cancel + Cancelar + + + + FileDialog + + + Location: + Ubicación: + + + + File name: + Nombre del archivo: + + + + File type: + Tipo de archivo: + + + + FileOperationDialog + + + Destination: + Destino: + + + + Processing: + Procesando: + + + + Preparing... + Preparando... + + + + Progress + Progreso + + + + Time remaining: + Tiempo restante: + + + + Files processed: + Archivos procesados: + + + + FilePropsDialog + + + File Properties + Propiedades del archivo + + + + General + General + + + + Location: + Ubicación: + + + + File type: + Tipo de archivo: + + + + MIME type: + Tipo MIME: + + + + File size: + Tamaño del archivo: + + + + On-disk size: + Tamaño en disco: + + + + Last modified: + Última modificación: + + + + Link target: + Destino del enlace: + + + + Open With: + Abrir con: + + + + Last accessed: + Último acceso: + + + + Contains: + Contiene: + + + + Device Usage: + Uso del dispositivo: + + + + Permissions + Permisos + + + + Ownership + Propiedad + + + + + + Group: + Grupo: + + + + + + Owner: + Propietario: + + + + Access Control + Control de acceso + + + + + Other: + Otros: + + + + Make the file executable + Hacer el archivo ejecutable + + + + + + Read + Lectura + + + + + + Write + Escritura + + + + + + Execute + Ejecución + + + + Sticky + Pegajoso + + + + SetUID + SetUID + + + + SetGID + SetGID + + + + Advanced Mode + Modo avanzado + + + + Fm::AppChooserComboBox + + + Customize + Personalizar + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + Seleccione una aplicación para abrir archivos «%1» + + + + Fm::CreateNewMenu + + + Folder + Carpeta + + + + Blank File + Archivo vacío + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + El directorio dado «%1» no es válido + + + + Fm::DirTreeModel + + + Loading... + Cargando... + + + + + + <No sub folders> + <No hay subdirectorios> + + + + Fm::DirTreeView + + + Open in New T&ab + Abrir en una pes&taña nueva + + + + Open in New Win&dow + Abrir en una ventana nueva + + + + Open in Termina&l + Abrir en una termina&l + + + + Fm::DndActionMenu + + + Copy here + Copiar aquí + + + + Move here + Mover aquí + + + + Create symlink here + Crear enlace simbólico aquí + + + + Cancel + Cancelar + + + + Fm::EditBookmarksDialog + + + New bookmark + Nuevo marcador + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + El archivo «%1» parece ser un lanzador de programa («desktop entry»). +¿Qué quiere hacer con él? + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + El archivo de texto «%1» parece ser un guion ejecutable. +¿Qué desea hacer con él? + + + + This file '%1' is executable. Do you want to execute it? + El archivo «%1» es ejecutable. ¿Quiere ejecutarlo? + + + + Fm::FileDialog + + + Go Back + Retroceder + + + + Alt+Left + Go Back + Alt+Izquierda + + + + Go Forward + Avanzar + + + + Alt+Right + Go Forward + Alt+Derecha + + + + Reload + Recargar + + + + F5 + Reload + F5 + + + + Create Folder + Crear carpeta + + + + Icon View + Vista de iconos + + + + Thumbnail View + Vista de miniaturas + + + + Compact View + Vista compacta + + + + Detailed List View + Vista de lista detallada + + + + + Error + Error + + + + Please select a file + Seleccione un archivo + + + + %1 already exists. +Do you want to replace it? + Ya existe %1. +¿Quiere sobrescribirlo? + + + + Path "%1" does not exist + No existe la ruta «%1» + + + + "%1" is not a directory + «%1» no es un directorio + + + + "%1" is not a file + «%1» no es un archivo + + + + + &Open + &Abrir + + + + + &Save + Guardar + + + + All Files (*) + Todos los archivos (*) + + + + Fm::FileDialogHelper + + + Open File + Abrir un archivo + + + + Save File + Guardar el archivo + + + + Fm::FileMenu + + + Open + Abrir + + + + Open With... + Abrir con... + + + + Other Applications + Otras aplicaciones + + + + Create &New + Crear &nuevo + + + + &Restore + &Restaurar + + + + Cut + Cortar + + + + Copy + Copiar + + + + Paste + Pegar + + + + + &Move to Trash + &Mover a la papelera + + + + Rename + Renombrar + + + + Extract to... + Extraer en... + + + + Extract Here + Extraer aquí + + + + Compress + Comprimir + + + + Properties + Propiedades + + + + Trust selected executables + Confiar en los ejecutables seleccionados + + + + Trust this executable + Confiar en este ejecutable + + + + Output + Salida + + + + &Delete + &Borrar + + + + Fm::FileOperation + + + Error + Error + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + Algunos archivos no pueden moverse a la papelera porque los sistemas de archivos subyacentes no permiten esta operación. +¿Quiere eliminarlos en vez de moverlos? + + + + + Confirm + Confirmación + + + + Do you want to delete the selected files? + ¿Quiere borrar los archivos seleccionados? + + + + Do you want to move the selected files to trash can? + ¿Quiere mover los archivos seleccionados a la papelera? + + + + Fm::FileOperationDialog + + + Move files + Mover archivos + + + + Moving the following files to destination folder: + Moviendo los siguientes archivos al directorio de destino: + + + + Copy Files + Copiar archivos + + + + Copying the following files to destination folder: + Copiando los siguientes archivos al directorio de destino: + + + + Trash Files + Enviar los archivos a la papelera + + + + Moving the following files to trash can: + Moviendo los siguientes archivos a la papelera: + + + + Delete Files + Borrar los archivos + + + + Deleting the following files: + Borrando los siguientes archivos: + + + + Create Symlinks + Crear enlaces simbólicos + + + + Creating symlinks for the following files: + Creando enlaces simbólicos para los siguientes archivos: + + + + Change Attributes + Cambiar atributos + + + + Changing attributes of the following files: + Cambiando atributos para los siguientes archivos: + + + + Restore Trashed Files + Restaurar archivos de la papelera + + + + Restoring the following files from trash can: + Restaurando los siguientes archivos desde la papelera: + + + + + Error + Error + + + + Fm::FilePropsDialog + + + View folder content + Ver el contenido de la carpeta + + + + View and modify folder content + Ver y modificar el contenido de la carpeta + + + + Read + Lectura + + + + Read and write + Lectura y escritura + + + + Forbidden + Prohibido + + + + Files of different types + Archivos de diferentes tipos + + + + Multiple Files + Múltiples archivos + + + + %p% used + %p% usado + + + + %1 Free of %2 + %1 libre(s) de %2 + + + + no file + ningún archivo + + + + one file + un archivo + + + + %1 files + %1 archivos + + + + Select an icon + Seleccione un icono + + + + Images (*.png *.xpm *.svg *.svgz ) + Imágenes (*.png *.xpm *.svg *.svgz) + + + + Apply changes + Aplicar los cambios + + + + Do you want to recursively apply these changes to all files and sub-folders? + ¿Quiere aplicar los cambios a todos los archivos y subdirectorios? + + + + Fm::FileSearchDialog + + + Error + Error + + + + You should add at least one directory to search. + Debe añadir al menos un directorio donde buscar. + + + + Select a folder + Seleccione una carpeta + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + No se pueden crear enlaces en sistemas de archivos no nativos + + + + Fm::FolderMenu + + + Create &New + Crear &nuevo + + + + &Paste + &Pegar + + + + Select &All + Seleccion&ar todo + + + + Invert Selection + Invertir la selección + + + + Sorting + Ordenar + + + + Show Hidden + Mostrar ocultos + + + + Folder Pr&operties + Pr&opiedades del directorio + + + + Output + Salida + + + + By File Name + Por nombre de archivo + + + + By Modification Time + Por fecha de modificación + + + + By File Size + Por tamaño de archivo + + + + By File Type + Por tipo de archivo + + + + By File Owner + Por propietario del archivo + + + + Ascending + Ascendente + + + + Descending + Descendente + + + + Folder First + Primero las carpetas + + + + Case Sensitive + Distinguir mayúsculas de minúsculas + + + + Fm::FolderModel + + + Name + Nombre + + + + Type + Tipo + + + + Size + Tamaño + + + + Modified + Modificado + + + + Owner + Propietario + + + + Group + Grupo + + + + Fm::FontButton + + + Bold + Negrita + + + + Italic + Cursiva + + + + Fm::MountOperationPasswordDialog + + + &Connect + &Conectar + + + + Fm::PathBar + + + &Edit Path + &Editar la ruta + + + + &Copy Path + &Copiar la ruta + + + + Fm::PlacesModel + + + Places + Lugares + + + + Desktop + Escritorio + + + + Computer + Sistema + + + + Applications + Aplicaciones + + + + Network + Red + + + + Devices + Dispositivos + + + + Bookmarks + Marcadores + + + + Trash + Papelera + + + + Fm::PlacesView + + + Empty Trash + Vaciar la papelera + + + + Open in New Tab + Abrir en una pestaña nueva + + + + Open in New Window + Abrir en una ventana nueva + + + + + Hide + Ocultar + + + + Move Bookmark Up + Mover el marcador hacia arriba + + + + Move Bookmark Down + Mover el marcador hacia abajo + + + + Rename Bookmark + Renombrar el marcador + + + + Remove Bookmark + Eliminar el marcador + + + + + Unmount + Desmontar + + + + Mount + Montar + + + + Eject + Expulsar + + + + Show All Entries + Mostrar todas las entradas + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + Tipo: %1 +Tamaño: %2 +Modificado: %3 + + + + + Type: %1 +Modified: %2 + Tipo: %1 +Modificado: %2 + + + + &Overwrite + &Sobrescribir + + + + &Rename + &Renombrar + + + + Fm::SidePane + + + Places + Lugares + + + + Directory Tree + Árbol de directorios + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + No se puede restaurar el archivo «%s» desde la papelera: la ruta original es desconocida + + + + MountOperationPasswordDialog + + + Mount + Montar + + + + Connect &anonymously + Conectar &anónimamente + + + + Connect as u&ser: + Conectar como el u&suario: + + + + &Username: + &Usuario: + + + + &Password: + &Contraseña: + + + + &Domain: + &Dominio: + + + + Forget password &immediately + Olvidar la contraseña &inmediatamente + + + + Remember password until you &logout + &Recordar la contraseña hasta cerrar la sesión + + + + Remember &forever + Recordar &para siempre + + + + QObject + + + + + + + Error + Error + + + + Rename File + Renombrar archivo + + + + Please enter a new name: + Introduzca un nuevo nombre: + + + + Create Folder + Crear carpeta + + + + Please enter a new file name: + Introduzca un nuevo nombre de archivo: + + + + New text file + Nuevo archivo de texto + + + + Please enter a new folder name: + Introduzca el nuevo nombre de la carpeta: + + + + New folder + Nueva carpeta + + + + Enter a name for the new %1: + Introduzca un nombre para el nuevo %1: + + + + Create File + Crear archivo + + + + Custom Icon Error + Icono de error personalizado + + + + The path is not mounted. + La ruta no está montada. + + + + Invalid desktop entry file: '%1' + Archivo lanzador (desktop entry) no válido: «%1» + + + + No default application is set to launch '%1' + No hay ninguna aplicación por defecto configurada para lanzar «%1» + + + + Cannot set working directory to '%1': %2 + No se puede cambiar el directorio de trabajo a «%1»: %2 + + + + Identifier: + Identificador: + + + + RenameDialog + + + Confirm to replace files + Pedir confirmación al sustituir archivos + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + <html><head/><body><p><span style=" font-weight:600;">Ya existe un archivo con el mismo nombre en este lugar.</span></p><p>¿Quiere reemplazar el archivo existente?</p></body></html> + + + + dest + destino + + + + with the following file? + con el siguiente archivo? + + + + src file info + información del archivo de origen + + + + dest file info + información del archivo de destino + + + + src + origen + + + + &File name: + Nombre del &archivo: + + + + Apply this option to all existing files + Aplicar esta opción a todos los archivos existentes + + + + SearchDialog + + + Search Files + Buscar archivos + + + + Name/Location + Nombre/Ubicación + + + + File Name Patterns: + Patrones de nombre de archivo: + + + + * + * + + + + Case insensitive + No distinguir mayúsculas de minúsculas + + + + Use regular expression + Usar expresiones regulares + + + + Places to Search: + Lugares donde buscar: + + + + &Add + &Añadir + + + + &Remove + Elimina&r + + + + Search in sub directories + Buscar en subdirectorios + + + + Search for hidden files + Buscar archivos ocultos + + + + File Type + Tipo de archivo + + + + Only search for files of following types: + Buscar solo archivos de los siguientes tipos: + + + + Text files + Archivos de texto + + + + Image files + Archivos de imagen + + + + Audio files + Archivos de sonido + + + + Video files + Archivos de vídeo + + + + Documents + Documentos + + + + Folders + Carpetas + + + + Content + Contenido + + + + File contains: + El archivo contiene: + + + + Case insensiti&ve + No &distinguir mayúsculas de minúsculas + + + + &Use regular expression + &Usar expresiones regulares + + + + Properties + Propiedades + + + + File Size: + Tamaño del archivo: + + + + Larger than: + Mayor que: + + + + + Bytes + Bytes + + + + + KiB + KiB + + + + + MiB + MiB + + + + + GiB + GiB + + + + Smaller than: + Menor que: + + + + Last Modified Time: + Fecha de la última modificación: + + + + Earlier than: + Anterior al: + + + + Later than: + Posterior al: + + + diff --git a/src/translations/libfm-qt_fr.ts b/src/translations/libfm-qt_fr.ts new file mode 100644 index 0000000..9299f80 --- /dev/null +++ b/src/translations/libfm-qt_fr.ts @@ -0,0 +1,1560 @@ + + + + + AppChooserDialog + + + Choose an Application + Sélectionnez une application + + + + Installed Applications + Applications installées + + + + Custom Command + Commandes personnalisées + + + + Command line to execute: + Ligne de commande à exécuter: + + + + Application name: + Nom de l’application: + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + <b>Ces codes spéciaux peuvent être utilisés dans la ligne de commande :</b> +<ul> +<li><b>%f</b>: Représente un seul nom de fichier</li> +<li><b>%F</b>: Représente plusieurs noms de fichiers</li> +<li><b>%u</b>: Représente l’URI d'un seul fichier</li> +<li><b>%U</b>: Représente plusieurs URIs</li> +</ul> + + + + Keep terminal window open after command execution + Garder la fenêtre de terminal ouverte après exécution de la commande + + + + Execute in terminal emulator + Exécuter dans un émulateur de terminal + + + + Set selected application as default action of this file type + Définir l’application sélectionnée comme action par défaut pour les fichiers de ce type + + + + EditBookmarksDialog + + + Edit Bookmarks + Modifier les signets + + + + Name + Nom + + + + Location + Emplacement + + + + &Add Item + &Ajouter un élément + + + + &Remove Item + &Supprimer un élément + + + + Use drag and drop to reorder the items + Utiliser le glisser-déposer pour trier à nouveau les éléments + + + + ExecFileDialog + + + Execute file + Exécuter le fichier + + + + &Open + &Ouvrir + + + + E&xecute + E&xécuter + + + + Execute in &Terminal + Exécuter dans un &terminal + + + + Cancel + Annuler + + + + FileDialog + + + Location: + Emplacement: + + + + File name: + Nom du fichier: + + + + File type: + Type de fichier: + + + + FileOperationDialog + + + Destination: + Destination : + + + + Processing: + En Traitement: + + + + Preparing... + En Préparation… + + + + Progress + En Progression + + + + Time remaining: + Temps restant: + + + + Files processed: + Fichiers traités: + + + + FilePropsDialog + + + File Properties + Propriétés du fichier + + + + General + Général + + + + Location: + Emplacement: + + + + File type: + Type de fichier: + + + + MIME type: + Type MIME: + + + + File size: + Taille du fichier: + + + + On-disk size: + Taille sur le disque: + + + + Last modified: + Date de dernière modification: + + + + Link target: + Cible du lien: + + + + Open With: + Ouvrir avec: + + + + Last accessed: + Date de dernier accès: + + + + Contains: + Contient : + + + + Device Usage: + Utilisation périphérique : + + + + Permissions + Droits d'accès + + + + Ownership + Propriété + + + + + + Group: + Groupe: + + + + + + Owner: + Propriétaire: + + + + Access Control + Contrôle d’accès + + + + + Other: + Autre: + + + + Make the file executable + Rendre le fichier exécutable + + + + + + Read + Lecture + + + + + + Write + Écriture + + + + + + Execute + Exécution + + + + Sticky + Permanent + + + + SetUID + Définir l'UID + + + + SetGID + Définir le GID + + + + Advanced Mode + Mode avancé + + + + Fm::AppChooserComboBox + + + Customize + Personnaliser + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + Sélectionnez une application pour ouvrir les fichiers de type "%1" + + + + Fm::CreateNewMenu + + + Folder + Dossier + + + + Blank File + Fichier vide + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + Le répertoire spécifié '%1' n'est pas valide + + + + Fm::DirTreeModel + + + Loading... + Chargement… + + + + + + <No sub folders> + <Aucun sous-dossiers> + + + + Fm::DirTreeView + + + Open in New T&ab + Ouvrir dans un nouvel &onglet + + + + Open in New Win&dow + Ouvrir dans une nouvelle &fenêtre + + + + Open in Termina&l + Ouvrir dans un &terminal + + + + Fm::DndActionMenu + + + Copy here + Copier ici + + + + Move here + Déplacer ici + + + + Create symlink here + Créer un lien symbolique ici + + + + Cancel + Annuler + + + + Fm::EditBookmarksDialog + + + New bookmark + Nouveau signet + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + Ce fichier '%1' semble être une entrée du bureau. +Que voulez-vous faire avec ? + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + Le fichier texte '%1' semble être un script exécutable. +Qu voulez-voulez vous en faire ? + + + + This file '%1' is executable. Do you want to execute it? + Le fichier '%1' est exécutable. Souhaitez-vous le lancer ? + + + + Fm::FileDialog + + + Go Back + Précédent + + + + Alt+Left + Go Back + Alt+flèche gauche + + + + Go Forward + Suite + + + + Alt+Right + Go Forward + Alt+flèche droite + + + + Reload + Recharger + + + + F5 + Reload + F5 + + + + Create Folder + Créer un dossier + + + + Icon View + Vue par icônes + + + + Thumbnail View + Vue par miniatures + + + + Compact View + Vue compacte de la liste + + + + Detailed List View + Vue détaillée de la liste + + + + + Error + Erreur + + + + Please select a file + Veuillez sélectionner un fichier + + + + %1 already exists. +Do you want to replace it? + %1 existe déjà. +Voulez-vous le remplacer ? + + + + Path "%1" does not exist + Le chemin "%1" n'existe pas + + + + "%1" is not a directory + "%1" n'est pas un répertoire + + + + "%1" is not a file + "%1" n'est pas un fichier + + + + + &Open + &Ouvrir + + + + + &Save + &Enregistrer + + + + All Files (*) + Tous les fichiers (*) + + + + Fm::FileDialogHelper + + + Open File + Ouvrir un fichier + + + + Save File + Enregistrer le fichier + + + + Fm::FileMenu + + + Open + Ouvrir + + + + Open With... + Ouvrir avec… + + + + Other Applications + Autres applications + + + + Create &New + Créer un &nouveau + + + + &Restore + &Restaurer + + + + Cut + Couper + + + + Copy + Copier + + + + Paste + Coller + + + + + &Move to Trash + &Mettre à la corbeille + + + + Rename + Renommer + + + + Extract to... + Extraire vers… + + + + Extract Here + Extraire ici + + + + Compress + Compresser + + + + Properties + Propriétés + + + + Trust selected executables + Faire confiance aux exécutables sélectionnés + + + + Trust this executable + Faites confiance à cet exécutable + + + + Output + Sortie + + + + &Delete + &Supprimer + + + + Fm::FileOperation + + + Error + Erreur + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + Certains fichiers ne peuvent pas être déplacés vers la corbeille car les systèmes de fichiers sous-jacents ne prennent pas en charge cette opération. +Voulez-vous les supprimer à la place ? + + + + + Confirm + Confirmer + + + + Do you want to delete the selected files? + Voulez-vous supprimer les fichiers sélectionnés ? + + + + Do you want to move the selected files to trash can? + Voulez-vous mettre les fichiers sélectionnés à la corbeille ? + + + + Fm::FileOperationDialog + + + Move files + Déplacer les fichiers + + + + Moving the following files to destination folder: + Déplacement des fichiers suivants vers le dossier de destination en cours: + + + + Copy Files + Copier les fichiers + + + + Copying the following files to destination folder: + Copie des fichiers suivants vers le dossier de destination en cours: + + + + Trash Files + Fichiers de la corbeille + + + + Moving the following files to trash can: + Déplacement des fichiers suivants vers la corbeille en cours: + + + + Delete Files + Supprimer les fichiers + + + + Deleting the following files: + Suppression des fichiers suivants: + + + + Create Symlinks + Créer des liens symboliques + + + + Creating symlinks for the following files: + Création de liens symboliques avec les fichiers suivants en cours: + + + + Change Attributes + Modifier les attributs + + + + Changing attributes of the following files: + Modification des attributs des fichiers suivants en cours: + + + + Restore Trashed Files + Restaurer les fichiers de la corbeille + + + + Restoring the following files from trash can: + Restauration des fichiers suivants depuis la corbeille en cours: + + + + + Error + Erreur + + + + Fm::FilePropsDialog + + + View folder content + Voir le contenu du dossier + + + + View and modify folder content + Voir et modifier le contenu du dossier + + + + Read + Lecture + + + + Read and write + Lecture et écriture + + + + Forbidden + Interdit + + + + Files of different types + Fichiers de différents types + + + + Multiple Files + Fichiers multiples + + + + %p% used + %p% utilisé + + + + %1 Free of %2 + %1 Libre sur %2 + + + + no file + aucun fichier + + + + one file + un fichier + + + + %1 files + %1 fichiers + + + + Select an icon + Sélectionner une icône + + + + Images (*.png *.xpm *.svg *.svgz ) + Images (*.png *.xpm *.svg *.svgz) + + + + Apply changes + Appliquer les modifications + + + + Do you want to recursively apply these changes to all files and sub-folders? + Voulez-vous appliquer ces changements récursivement à tous les fichiers et sous-dossiers ? + + + + Fm::FileSearchDialog + + + Error + Erreur + + + + You should add at least one directory to search. + Vous devez ajouter au moins un répertoire à rechercher. + + + + Select a folder + Sélectionnez un dossier + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + Ne peut pas créer un lien vers un système de fichiers non natif + + + + Fm::FolderMenu + + + Create &New + Créer un &nouveau + + + + &Paste + &Coller + + + + Select &All + Tout sélect&ionner + + + + Invert Selection + Inverser la sélection + + + + Sorting + Tri en cours + + + + Show Hidden + Afficher les éléments cachés + + + + Folder Pr&operties + Pr&opriétés du dossier + + + + Output + Sortie + + + + By File Name + Par nom de fichier + + + + By Modification Time + Par date de modification + + + + By File Size + Par taille de fichier + + + + By File Type + Par type de fichier + + + + By File Owner + Par propriétaire de fichier + + + + Ascending + Ascendant + + + + Descending + Descendant + + + + Folder First + Dossier en premier + + + + Case Sensitive + Sensible à la casse + + + + Fm::FolderModel + + + Name + Nom + + + + Type + Type + + + + Size + Taille + + + + Modified + Modifié + + + + Owner + Propriétaire + + + + Group + Groupe + + + + Fm::FontButton + + + Bold + Gras + + + + Italic + Italique + + + + Fm::MountOperationPasswordDialog + + + &Connect + &Connecter + + + + Fm::PathBar + + + &Edit Path + &Éditer le chemin + + + + &Copy Path + &Copier le chemin + + + + Fm::PlacesModel + + + Places + Emplacements + + + + Desktop + Bureau + + + + Trash + Corbeille + + + + Computer + Ordinateur + + + + Applications + Application(s) + + + + Network + Réseau + + + + Devices + Appareils + + + + Bookmarks + Signets + + + + Fm::PlacesView + + + Empty Trash + Corbeille vide + + + + Open in New Tab + Ouvrir dans un nouvel onglet + + + + Open in New Window + Ouvrir dans une nouvelle fenêtre + + + + + Hide + Cacher + + + + Move Bookmark Up + Déplacer le signet vers le haut + + + + Move Bookmark Down + Déplacer le signet vers le bas + + + + Rename Bookmark + Renommer le signet + + + + Remove Bookmark + Supprimer le signet + + + + + Unmount + Démonter + + + + Mount + Monter + + + + Eject + Éjecter + + + + Show All Entries + Afficher toutes les entrées + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + Type: %1 +Taille: %2 +Modification: %3 + + + + + Type: %1 +Modified: %2 + Type: %1 +Modification: %2 + + + + &Overwrite + &Écraser + + + + &Rename + &Renommer + + + + Fm::SidePane + + + Places + Emplacements + + + + Directory Tree + Arborescence des répertoires + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + Ne peut pas sortir le fichier '%s' de la corbeille: le chemin d'origine est inconnu + + + + MountOperationPasswordDialog + + + Mount + Monter + + + + Connect &anonymously + Connecter &anonymement + + + + Connect as u&ser: + Connecter comme utilis&sateur: + + + + &Username: + Nom d'&utilisateur: + + + + &Password: + Mot de &passe: + + + + &Domain: + &Domaine: + + + + Forget password &immediately + Oublier le mot de passe &immédiatement + + + + Remember password until you &logout + Se souvenir du mot de passe jusqu'à &la déconnexion + + + + Remember &forever + &Toujours s'en souvenir + + + + QObject + + + + + + + Error + Erreur + + + + Rename File + Renommer le fichier + + + + Please enter a new name: + Veuillez entrer un nouveau nom: + + + + Create Folder + Créer un dossier + + + + Please enter a new file name: + Veuillez entrer un nouveau nom de fichier: + + + + New text file + Nouveau fichier texte + + + + Please enter a new folder name: + Veuillez entrer un nouveau nom de répertoire: + + + + New folder + Nouveau répertoire + + + + Enter a name for the new %1: + Entrez un nom pour le nouveau %1: + + + + Create File + Créer un fichier + + + + Custom Icon Error + Erreur avec l'icône personnelle + + + + The path is not mounted. + Le chemin n'est pas monté. + + + + Invalid desktop entry file: '%1' + Fichier d'entrée de bureau non valide: '%1' + + + + No default application is set to launch '%1' + Aucune application par défaut n'est définie pour lancer '%1' + + + + Cannot set working directory to '%1': %2 + Impossible de définir le répertoire de travail à '%1': '%2' + + + + Identifier: + Identifiant : + + + + RenameDialog + + + Confirm to replace files + Confirmer le remplacement des fichiers + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + <html><head/><body><p><span style=" font-weight:600;">Il existe déjà un fichier avec le même nom à cet emplacement.</span></p><p>Voulez-vous remplacer le fichier existant ?</p></body></html> + + + + dest + destination + + + + with the following file? + avec le fichier suivant ? + + + + src file info + Informations sur le fichier source + + + + dest file info + Informations sur le fichier de destination + + + + src + source + + + + &File name: + Nom du &fichier : + + + + Apply this option to all existing files + Appliquer cette option à tous les fichiers existants + + + + SearchDialog + + + Search Files + Recherche de fichiers + + + + Name/Location + Nom/Emplacement + + + + File Name Patterns: + Motifs dans le nom des fichiers: + + + + * + * + + + + Case insensitive + Insensible à la casse + + + + Use regular expression + Utiliser une expression régulière + + + + Places to Search: + Emplacements de la recherche: + + + + &Add + &Ajouter + + + + &Remove + &Supprimer + + + + Search in sub directories + Recherche dans les sous-répertoires + + + + Search for hidden files + Rechercher les fichiers cachés + + + + File Type + Type de fichier + + + + Only search for files of following types: + Rechercher uniquement les types de fichiers suivants: + + + + Text files + Fichiers texte + + + + Image files + Fichiers image + + + + Audio files + Fichiers audio + + + + Video files + Fichiers vidéo + + + + Documents + Documents + + + + Folders + Dossiers + + + + Content + Contenu + + + + File contains: + Le fichier contient: + + + + Case insensiti&ve + &Insensible à la casse + + + + &Use regular expression + &Utiliser une expression régulière + + + + Properties + Propriétés + + + + File Size: + Taille du fichier: + + + + Larger than: + Plus grands que: + + + + + Bytes + Octets + + + + + KiB + Ko + + + + + MiB + Mo + + + + + GiB + Go + + + + Smaller than: + Plus petits que: + + + + Last Modified Time: + Date de dernière modification: + + + + Earlier than: + Plus récent que: + + + + Later than: + Plus tard que: + + + diff --git a/src/translations/libfm-qt_gl.ts b/src/translations/libfm-qt_gl.ts new file mode 100644 index 0000000..6ceafce --- /dev/null +++ b/src/translations/libfm-qt_gl.ts @@ -0,0 +1,1560 @@ + + + + + AppChooserDialog + + + Choose an Application + Escolla un aplicativo + + + + Installed Applications + Aplicativos instalados + + + + Custom Command + Orde personalizada + + + + Command line to execute: + Liña de ordes a executar: + + + + Application name: + Nome do aplicativo: + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + <b>Na liña de ordes pódense empregar estes códigos especiais:</b> +<ul> +<li><b>%f</b>: Representa un só nome de ficheiro</li> +<li><b>%F</b>: Representa múltiples nomes de ficheiros</li> +<li><b>%u</b>: Representa un só URI do ficheiro</li> +<li><b>%U</b>: Representa múltiples URI</li> +</ul> + + + + Keep terminal window open after command execution + Manter a terminal aberta após a execución da orde + + + + Execute in terminal emulator + Executar nun emulador de terminal + + + + Set selected application as default action of this file type + Estabelecer o aplicativo seleccionado como predeterminado para este tipo de ficheiro + + + + EditBookmarksDialog + + + Edit Bookmarks + Editar marcadores + + + + Name + Nome + + + + Location + Localización + + + + &Add Item + &Engadir un elemento + + + + &Remove Item + &Retirar o elemento + + + + Use drag and drop to reorder the items + Usar arrastrar e soltar para ordenar elementos + + + + ExecFileDialog + + + Execute file + Executar o ficheiro + + + + &Open + &Abrir + + + + E&xecute + E&xecutar + + + + Execute in &Terminal + Executar nunha &terminal + + + + Cancel + Cancelar + + + + FileDialog + + + Location: + Localización: + + + + File name: + Nome do ficheiro: + + + + File type: + Tipo de ficheiro: + + + + FileOperationDialog + + + Destination: + Destino: + + + + Processing: + Procesando: + + + + Preparing... + Preparando... + + + + Progress + En progreso + + + + Time remaining: + Tempo restante: + + + + Files processed: + Ficheiros procesados: + + + + FilePropsDialog + + + File Properties + Propiedades do ficheiro + + + + General + Xeral + + + + Location: + Localización: + + + + File type: + Tipo de ficheiro: + + + + MIME type: + Tipo MIME: + + + + File size: + Tamaño do ficheiro: + + + + On-disk size: + Tamaño no disco: + + + + Last modified: + Última modificación: + + + + Link target: + Destino da ligazón: + + + + Open With: + Abrir con: + + + + Last accessed: + Último acceso: + + + + Contains: + Contén: + + + + Device Usage: + Uso do dispositivo: + + + + Permissions + Permisos + + + + Ownership + Pertenza + + + + + + Group: + Grupo: + + + + + + Owner: + Propietario: + + + + Access Control + Control de acceso + + + + + Other: + Outros: + + + + Make the file executable + Facer que o ficheiro sexa executábel + + + + + + Read + Lectura + + + + + + Write + Escritura + + + + + + Execute + Execución + + + + Sticky + Persistente + + + + SetUID + Estabelecer o UID + + + + SetGID + Estabelecer o GID + + + + Advanced Mode + Modo avanzado + + + + Fm::AppChooserComboBox + + + Customize + Personalizar + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + Seleccione un aplicativo para abrir os ficheiros «%1» + + + + Fm::CreateNewMenu + + + Folder + Cartafol + + + + Blank File + Ficheiro en branco + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + O directorio especificado «%1» non é válido + + + + Fm::DirTreeModel + + + Loading... + Cargando... + + + + + + <No sub folders> + <non hai subcartafoles> + + + + Fm::DirTreeView + + + Open in New T&ab + Abrir nunha nova l&apela + + + + Open in New Win&dow + Abrir nunha nova xa&nela + + + + Open in Termina&l + Abrir nun termina&l + + + + Fm::DndActionMenu + + + Copy here + Copiar para aquí + + + + Move here + Mover para aquí + + + + Create symlink here + Crear aquí unha ligazón simbólica + + + + Cancel + Cancelar + + + + Fm::EditBookmarksDialog + + + New bookmark + Novo marcador + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + O ficheiro «%1» semella ser unha entrada de escritorio. +Que quere facer con el? + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + Este ficheiro de texto «%1» semella ser un script executábel. +Que quere facer con el? + + + + This file '%1' is executable. Do you want to execute it? + Este ficheiro «%1» é executábel. Quere executalo? + + + + Fm::FileDialog + + + Go Back + Ir cara atrás + + + + Alt+Left + Go Back + Alt+Esquerda + + + + Go Forward + Ir cara adiante + + + + Alt+Right + Go Forward + Alt+Dereita + + + + Reload + Cargar de novo + + + + F5 + Reload + F5 + + + + Create Folder + Crear un cartafol + + + + Icon View + Vista en Iconas + + + + Thumbnail View + Vista de miniaturas + + + + Compact View + Vista compacta + + + + Detailed List View + Ver como lista detallada + + + + + Error + Erro + + + + Please select a file + Seleccione un ficheiro + + + + %1 already exists. +Do you want to replace it? + Xa existe %1. +Quere sobrescribilo? + + + + Path "%1" does not exist + Non existe a ruta «%1» + + + + "%1" is not a directory + «%1» non é un directorio + + + + "%1" is not a file + «%1» non é un ficheiro + + + + + &Open + &Abrir + + + + + &Save + &Gardar + + + + All Files (*) + Todos os ficheiros (*) + + + + Fm::FileDialogHelper + + + Open File + Abrir ficheiro + + + + Save File + Gardar o ficheiro + + + + Fm::FileMenu + + + Open + Abrir + + + + Cut + Cortar + + + + Copy + Copiar + + + + Paste + Pegar + + + + + &Move to Trash + Deitar no &lixo + + + + Trust selected executables + Confiar nos executábeis seleccionados + + + + Trust this executable + Confiar neste executábel + + + + Output + Saída + + + + &Delete + &Eliminar + + + + Rename + Renomear + + + + Open With... + Abrir con... + + + + Other Applications + Outros aplicativos + + + + Create &New + Crear &novo + + + + &Restore + &Restaurar + + + + Extract to... + Extraer en… + + + + Extract Here + Extraer aquí + + + + Compress + Comprimir + + + + Properties + Propiedades + + + + Fm::FileOperation + + + Error + Erro + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + Algúns ficheiros non poden enviarse ao cesto do lixo porque o subsistema de ficheiros non permite esta operación. +Desexa eliminalos no seu canto? + + + + + Confirm + Confirmar + + + + Do you want to delete the selected files? + Quere eliminar os ficheiros seleccionados? + + + + Do you want to move the selected files to trash can? + Quere mover os ficheiros seleccionados ao cesto do lixo? + + + + Fm::FileOperationDialog + + + Move files + Mover os ficheiros + + + + Moving the following files to destination folder: + Movendo os seguintes ficheiros ao cartafol de destino: + + + + Copy Files + Copiar os ficheiros + + + + Copying the following files to destination folder: + Copiando os seguintes ficheiros ao cartafol de destino: + + + + Trash Files + Deitar os ficheiros no lixo + + + + Moving the following files to trash can: + Movendo os seguintes ficheiros ao lixo: + + + + Delete Files + Eliminar os ficheiros + + + + Deleting the following files: + Eliminando os seguintes ficheiros: + + + + Create Symlinks + Crear ligazóns simbólicas + + + + Creating symlinks for the following files: + Creando ligazóns simbólicas para os seguintes ficheiros: + + + + Change Attributes + Cambiar os atributos + + + + Changing attributes of the following files: + Cambiando os atributos dos seguintes ficheiros: + + + + Restore Trashed Files + Restaurar os ficheiro do lixo + + + + Restoring the following files from trash can: + Restaurando os seguintes ficheiros do lixo: + + + + + Error + Erro + + + + Fm::FilePropsDialog + + + View folder content + Ver o contido do cartafol + + + + View and modify folder content + Ver e modificar o contido do cartafol + + + + Read + Lectura + + + + Read and write + Lectura e escritura + + + + Forbidden + Prohibido + + + + Files of different types + Ficheiros de tipos diferentes + + + + Multiple Files + Múltiplos ficheiros + + + + %p% used + %p% utilizado + + + + %1 Free of %2 + %1 libre de %2 + + + + no file + ningún ficheiro + + + + one file + un ficheiro + + + + %1 files + %1 ficheiros + + + + Select an icon + Seleccione unha icona + + + + Images (*.png *.xpm *.svg *.svgz ) + Imaxes (*.png *.xpm *.svg *.svgz ) + + + + Apply changes + Aplicar os cambios + + + + Do you want to recursively apply these changes to all files and sub-folders? + Quere aplicar recursivamente estes cambios a todos os ficheiros e subcartafoles? + + + + Fm::FileSearchDialog + + + Error + Erro + + + + You should add at least one directory to search. + Debe engadir a lo menos un directorio onde buscar. + + + + Select a folder + Seleccione un cartafol + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + Non é posíbel crear ligazóns en sistemas de ficheiros non nativos + + + + Fm::FolderMenu + + + Create &New + Crear &novo + + + + &Paste + &Pegar + + + + Select &All + Seleccionar &todo + + + + Invert Selection + Inverter a selección + + + + Sorting + Ordenación + + + + Show Hidden + Amosar agachados + + + + Folder Pr&operties + Pr&opiedades do cartafol + + + + Output + Saída + + + + By File Name + Polo nome do ficheiro + + + + By Modification Time + Pola data de modificación + + + + By File Size + Polo tamaño do ficheiro + + + + By File Type + Polo tipo do ficheiro + + + + By File Owner + Polo propietario do ficheiro + + + + Ascending + Ascendente + + + + Descending + Descendente + + + + Folder First + Primeiro os cartafoles + + + + Case Sensitive + Distinguindo maiúsculas de minúsculas + + + + Fm::FolderModel + + + Name + Nome + + + + Type + Tipo + + + + Size + Tamaño + + + + Modified + Modificado + + + + Owner + Propietario + + + + Group + Grupo + + + + Fm::FontButton + + + Bold + Grosa + + + + Italic + Itálica + + + + Fm::MountOperationPasswordDialog + + + &Connect + &Conectar + + + + Fm::PathBar + + + &Edit Path + &Editar a ruta + + + + &Copy Path + &Copiar a ruta + + + + Fm::PlacesModel + + + Places + Lugares + + + + Desktop + Escritorio + + + + Trash + Lixo + + + + Computer + Computador + + + + Applications + Aplicativos + + + + Network + Rede + + + + Devices + Dispositivos + + + + Bookmarks + Marcadores + + + + Fm::PlacesView + + + Empty Trash + Baleirar o lixo + + + + Open in New Tab + Abrir nunha lapela nova + + + + Open in New Window + Abrir nunha xanela nova + + + + + Hide + Agachar + + + + Move Bookmark Up + Mover o marcador cara arriba + + + + Move Bookmark Down + Mover o marcador cara abaixo + + + + Rename Bookmark + Renomear o marcador + + + + Remove Bookmark + Retirar o marcador + + + + + Unmount + Desmontar + + + + Mount + Montar + + + + Eject + Expulsar + + + + Show All Entries + Amosar todas as entradas + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + Tipo: %1 +Tamaño: %2 +Modificado: %3 + + + + + Type: %1 +Modified: %2 + Tipo: %1 +Modificado: %2 + + + + &Overwrite + &Sobrescribir + + + + &Rename + &Renomear + + + + Fm::SidePane + + + Places + Lugares + + + + Directory Tree + Árbore de directorios + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + Non é posíbel restaurar o ficheiro «%s» dende o lixo: descoñecese a ruta orixinal + + + + MountOperationPasswordDialog + + + Mount + Montar + + + + Connect &anonymously + Conectar &anonimamente + + + + Connect as u&ser: + Conectar como u&suario: + + + + &Username: + Nome de &usuario: + + + + &Password: + &Contrasinal: + + + + &Domain: + &Dominio: + + + + Forget password &immediately + Esquecer o contrasinal &inmediatamente + + + + Remember password until you &logout + Lembrar o contrasinal ata &pechar a sesión + + + + Remember &forever + &Lembrar para sempre + + + + QObject + + + + + + + Error + Erro + + + + Rename File + Renomear o ficheiro + + + + Please enter a new name: + Introduza un nome novo: + + + + Create Folder + Crear un cartafol + + + + Please enter a new file name: + Introduza un novo nome de ficheiro: + + + + New text file + Novo ficheiro de texto + + + + Please enter a new folder name: + Introduza un nome novo para o cartafol: + + + + New folder + Novo cartafol + + + + Enter a name for the new %1: + Introduza un nome para o novo %1: + + + + Create File + Crear un ficheiro + + + + Custom Icon Error + Erro da icona personalizada + + + + The path is not mounted. + A ruta non está montada. + + + + Invalid desktop entry file: '%1' + Ficheiro de entrada de escritorio non valido: «%1» + + + + No default application is set to launch '%1' + Non foi estabelecido ningún aplicativo predeterminado para iniciar «%1» + + + + Cannot set working directory to '%1': %2 + Non é posíbel cambiar o directorio de traballo a «%1»: %2 + + + + Identifier: + Identificador: + + + + RenameDialog + + + Confirm to replace files + Confirmar a substitución de ficheiros + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + <html><head/><body><p><span style=" font-weight:600;">Xa existe un ficheiro con este mesmo nome neste lugar.</span></p><p>Quere substituír o ficheiro existente?</p></body></html> + + + + dest + destino + + + + with the following file? + co seguinte ficheiro? + + + + src file info + Información do ficheiro «orixe» + + + + dest file info + Información do ficheiro «destino» + + + + src + orixe + + + + &File name: + Nome do &ficheiro: + + + + Apply this option to all existing files + Aplicar esta opción a todos os ficheiros existentes + + + + SearchDialog + + + Search Files + Buscar ficheiros + + + + Name/Location + Nome/Localización + + + + File Name Patterns: + Patróns de nomes de ficheiro: + + + + * + * + + + + Case insensitive + Non distinguir maiúsculas e minúsculas + + + + Use regular expression + Usar expresións regulares + + + + Places to Search: + Lugares onde buscar: + + + + &Add + &Engadir + + + + &Remove + &Retirar + + + + Search in sub directories + Buscar en subdirectorios + + + + Search for hidden files + Buscar ficheiros agachados + + + + File Type + Tipo de ficheiro + + + + Only search for files of following types: + Buscar só os seguintes tipos de ficheiro: + + + + Text files + Ficheiros de texto + + + + Image files + Ficheiros de imaxe + + + + Audio files + Ficheiros de son + + + + Video files + Ficheiros de vídeo + + + + Documents + Documentos + + + + Folders + Cartafoles + + + + Content + Contido + + + + File contains: + O ficheiro contén: + + + + Case insensiti&ve + &Non distinguir maiúsculas e minúsculas + + + + &Use regular expression + &Usar expresións regulares + + + + Properties + Propiedades + + + + File Size: + Tamaño do ficheiro: + + + + Larger than: + Maior que: + + + + + Bytes + Bytes + + + + + KiB + KiB + + + + + MiB + MiB + + + + + GiB + GiB + + + + Smaller than: + Menor que: + + + + Last Modified Time: + Data da última modificación: + + + + Earlier than: + Anterior a: + + + + Later than: + Posterior a: + + + diff --git a/src/translations/libfm-qt_he.ts b/src/translations/libfm-qt_he.ts new file mode 100644 index 0000000..ffc5e84 --- /dev/null +++ b/src/translations/libfm-qt_he.ts @@ -0,0 +1,1560 @@ + + + + + AppChooserDialog + + + Choose an Application + בחירת יישום + + + + Installed Applications + יישומים מותקנים + + + + Custom Command + פקודה בהתאמה אישית + + + + Command line to execute: + שורת פקודה להפעלה: + + + + Application name: + שם יישום: + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + <b>ניתן להשתמש בקודים המיוחדים האלה בשורת הפקודה:</b> +<ul> +<li><b>%f</b>: מייצג שם של קובץ אחד</li> +<li><b>%F</b>: מייצג שמות של מספר קבצים</li> +<li><b>%u</b>: מייצג כתובת מדויקת של הקובץ</li> +<li><b>%U</b>: מייצג מספר כתובות</li> +</ul> + + + + Keep terminal window open after command execution + להשאיר את חלון המסוף פתוח לאחר הפעלת פקודה + + + + Execute in terminal emulator + הפעלה במדמה מסוף + + + + Set selected application as default action of this file type + הגדרת היישום הנבחר כפעולת בררת המחדל לסוג קובץ זה + + + + EditBookmarksDialog + + + Edit Bookmarks + עריכת סימניות + + + + Name + שם + + + + Location + מיקום + + + + &Add Item + הוספת &פריט + + + + &Remove Item + ה&סרת פריט + + + + Use drag and drop to reorder the items + ניתן להשתמש בגרירה והשלכה כדי לסדר את הפריטים מחדש + + + + ExecFileDialog + + + Execute file + הפעלת קובץ + + + + &Open + &פתיחה + + + + E&xecute + הפ&עלה + + + + Execute in &Terminal + הפעלה ב&מסוף + + + + Cancel + ביטול + + + + FileDialog + + + Location: + מיקום: + + + + File name: + שם קובץ: + + + + File type: + סוג קובץ: + + + + FileOperationDialog + + + Destination: + יעד: + + + + Processing: + עיבוד: + + + + Preparing... + בהכנה… + + + + Progress + תהליך + + + + Time remaining: + הזמן שנותר: + + + + Files processed: + קבצים שעברו עיבוד: + + + + FilePropsDialog + + + File Properties + מאפייני קובץ + + + + General + כללי + + + + Location: + מיקום: + + + + File type: + סוג קובץ: + + + + MIME type: + סוג MIME: + + + + File size: + גודל קובץ: + + + + On-disk size: + גודל בכונן: + + + + Last modified: + שינוי אחרון: + + + + Link target: + יעד קישור: + + + + Open With: + פתיחה עם: + + + + Last accessed: + גישה אחרונה: + + + + Contains: + + + + + Device Usage: + + + + + Permissions + הרשאות + + + + Ownership + בעלות + + + + + + Group: + קבוצה: + + + + + + Owner: + בעלים: + + + + Access Control + בקרת גישה + + + + + Other: + אחר: + + + + Make the file executable + להפוך את הקובץ למורשה הרצה + + + + + + Read + קריאה + + + + + + Write + כתיבה + + + + + + Execute + הרצה + + + + Sticky + דביק + + + + SetUID + + + + + SetGID + + + + + Advanced Mode + מצב מתקדם + + + + Fm::AppChooserComboBox + + + Customize + התאמה אישית + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + נא לבחור יישום לפתיחת קובצי „%1” + + + + Fm::CreateNewMenu + + + Folder + תיקייה + + + + Blank File + קובץ ריק + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + התיקייה שצוינה ‚%1’ אינה תקנית + + + + Fm::DirTreeModel + + + Loading... + בטעינה… + + + + + + <No sub folders> + <אין תת תיקיות> + + + + Fm::DirTreeView + + + Open in New T&ab + פתיחה ב&לשונית חדשה + + + + Open in New Win&dow + פתיחה ב&חלון חדש + + + + Open in Termina&l + פתיחה במ&סוף + + + + Fm::DndActionMenu + + + Copy here + להעתיק לכאן + + + + Move here + להעביר לכאן + + + + Create symlink here + יצירת קישור סמלי כאן + + + + Cancel + ביטול + + + + Fm::EditBookmarksDialog + + + New bookmark + סימנייה חדשה + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + נראה כי הקובץ ‚%1’ הוא למעשה רשומת שולחן עבודה. +מה ברצונך לעשות אתו? + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + נראה כי קובץ הטקסט ‚%1’ הוא למעשה סקריפט שניתן להריץ. +מה ברצונך לעשות אתו? + + + + This file '%1' is executable. Do you want to execute it? + ניתן להריץ את הקובץ ‚%1’. להריץ אותו? + + + + Fm::FileDialog + + + Go Back + חזרה + + + + Alt+Left + Go Back + Alt+שמאלה + + + + Go Forward + קדימה + + + + Alt+Right + Go Forward + Alt+ימינה + + + + Reload + רענון + + + + F5 + Reload + + + + + Create Folder + יצירת תיקייה + + + + Icon View + תצוגת סמלים + + + + Thumbnail View + תצוגת תמונות ממוזערות + + + + Compact View + תצוגה חסכונית + + + + Detailed List View + תצוגת פירוט + + + + + Error + שגיאה + + + + Please select a file + נא לבחור בקובץ + + + + %1 already exists. +Do you want to replace it? + %1 כבר קיים. +להחליף אותו? + + + + Path "%1" does not exist + הנתיב „%1” לא קיים + + + + "%1" is not a directory + „%1” אינה תיקייה + + + + "%1" is not a file + „%1” אינו קובץ + + + + + &Open + &פתיחה + + + + + &Save + &שמירה + + + + All Files (*) + כל הקבצים (*) + + + + Fm::FileDialogHelper + + + Open File + פתיחת קובץ + + + + Save File + שמירת קובץ + + + + Fm::FileMenu + + + Open + פתיחה + + + + Open With... + פתיחה עם… + + + + Other Applications + יישומים אחרים + + + + Create &New + יצירת &חדש + + + + &Restore + &שחזור + + + + Cut + גזירה + + + + Copy + העתקה + + + + Paste + הדבקה + + + + + &Move to Trash + העברה ל&אשפה + + + + Rename + שינוי שם + + + + Extract to... + חילוץ אל… + + + + Extract Here + חילוץ לכאן + + + + Compress + דחיסה + + + + Properties + מאפיינים + + + + Trust selected executables + מתן אמון בקובצי הפעלה נבחרים + + + + Trust this executable + מתן אמון בקובץ הפעלה זה + + + + Output + פלט + + + + &Delete + מחי&קה + + + + Fm::FileOperation + + + Error + שגיאה + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + העברתם של חלק מהקבצים לאשפה אינה אפשרית כיוון שמערכת ההפעלה שתחתיהם אינה תומכת בפעולה זו. +למחוק אותם במקום? + + + + + Confirm + אישור + + + + Do you want to delete the selected files? + למחוק את הקבצים הנבחרים? + + + + Do you want to move the selected files to trash can? + להעביר את הקבצים הנבחרים לסל האשפה? + + + + Fm::FileOperationDialog + + + Move files + העברת קבצים + + + + Moving the following files to destination folder: + הקבצים הבאים מועברים לתיקיית היעד: + + + + Copy Files + העתקת קבצים + + + + Copying the following files to destination folder: + הקבצים הבאים מועתקים לתיקיית היעד: + + + + Trash Files + השלכת קבצים לאשפה + + + + Moving the following files to trash can: + הקבצים הבאים מועברים לסל האשפה: + + + + Delete Files + מחיקת קבצים + + + + Deleting the following files: + הקבצים הבאים נמחקים: + + + + Create Symlinks + יצירת קישורים סמליים + + + + Creating symlinks for the following files: + נוצרים קישורים סמליים לקבצים הבאים: + + + + Change Attributes + שינוי מאפיינים + + + + Changing attributes of the following files: + המאפיינים של הקבצים הבאים משתנים: + + + + Restore Trashed Files + שחזור קבצים מהאשפה + + + + Restoring the following files from trash can: + הקבצים הבאים משוחזרים מסל האשפה: + + + + + Error + שגיאה + + + + Fm::FilePropsDialog + + + View folder content + צפייה בתוכן התיקייה + + + + View and modify folder content + צפייה ועריכה של תוכן התיקייה + + + + Read + קריאה + + + + Read and write + קריאה וכתיבה + + + + Forbidden + אין הרשאה + + + + Files of different types + קבצים מסוגים שונים + + + + Multiple Files + מגוון קבצים + + + + %p% used + + + + + %1 Free of %2 + + + + + no file + + + + + one file + + + + + %1 files + + + + + Select an icon + נא לבחור סמל + + + + Images (*.png *.xpm *.svg *.svgz ) + תמונות ‎(*.png *.xpm *.svg *.svgz ) + + + + Apply changes + החלת השינויים + + + + Do you want to recursively apply these changes to all files and sub-folders? + להחיל את השינויים האלה רקורסיבית על כל הקבצים ותת־התיקיות? + + + + Fm::FileSearchDialog + + + Error + שגיאה + + + + You should add at least one directory to search. + עליך להוסיף לפחות תיקייה אחת לחיפוש. + + + + Select a folder + נא לבחור תיקייה + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + לא ניתן ליצור קישור על מערכת קישור בלתי טבעית + + + + Fm::FolderMenu + + + Create &New + יצירת &חדשה + + + + &Paste + ה&דבקה + + + + Select &All + בחירת ה&כול + + + + Invert Selection + היפוך הבחירה + + + + Sorting + סידור + + + + Show Hidden + הצגת מוסתרים + + + + Folder Pr&operties + מאפייני &תיקייה + + + + Output + פלט + + + + By File Name + לפי שם קובץ + + + + By Modification Time + לפי מועד שינוי + + + + By File Size + לפי גודל קובץ + + + + By File Type + לפי סוג קובץ + + + + By File Owner + לפי בעלות על קובץ + + + + Ascending + עולה + + + + Descending + יורד + + + + Folder First + תיקיות בהתחלה + + + + Case Sensitive + תלוי רישיות + + + + Fm::FolderModel + + + Name + שם + + + + Type + סוג + + + + Size + גודל + + + + Modified + מועד שינוי + + + + Owner + בעלות + + + + Group + קבוצה + + + + Fm::FontButton + + + Bold + מודגש + + + + Italic + נטוי + + + + Fm::MountOperationPasswordDialog + + + &Connect + הת&חברות + + + + Fm::PathBar + + + &Edit Path + ×¢&ריכת נתיב + + + + &Copy Path + ה&עתקת נתיב + + + + Fm::PlacesModel + + + Places + מיקומים + + + + Desktop + שולחן עבודה + + + + Computer + מחשב + + + + Applications + יישומים + + + + Network + רשת + + + + Devices + התקנים + + + + Bookmarks + סימניות + + + + Trash + אשפה + + + + Fm::PlacesView + + + Open in New Tab + פתיחה בלשונית חדשה + + + + Open in New Window + פתיחה בחלון חדש + + + + Empty Trash + פינוי האשפה + + + + + Hide + הסתרה + + + + Move Bookmark Up + העברת הסימנייה למעלה + + + + Move Bookmark Down + העברת הסימנייה למטה + + + + Rename Bookmark + שינוי שם לסימנייה + + + + Remove Bookmark + הסרת סימנייה + + + + + Unmount + ניתוק עיגון + + + + Mount + עיגון + + + + Eject + שליפה + + + + Show All Entries + הצגת כל הרשומות + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + סוג: %1 +גודל: %2 +שינוי: %3 + + + + + Type: %1 +Modified: %2 + סוג: %1 +שינוי: %2 + + + + &Overwrite + &שכתוב + + + + &Rename + שי&נוי שם + + + + Fm::SidePane + + + Places + מיקומים + + + + Directory Tree + ×¢×¥ תיקיות + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + לא ניתן להוציא מהאשפה את ‚%s’: הנתיב המקורי אינו מוכר + + + + MountOperationPasswordDialog + + + Mount + עיגון + + + + Connect &anonymously + התחברות &אלמונית + + + + Connect as u&ser: + התחברות בתור מ&שתמש: + + + + &Username: + שם &משתמש: + + + + &Password: + &ססמה: + + + + &Domain: + שם מת&חם: + + + + Forget password &immediately + לש&כוח את הססמה מיידית + + + + Remember password until you &logout + לשמו&ר את הססמה עד ליציאה + + + + Remember &forever + ל&זכור לתמיד + + + + QObject + + + Rename File + שינוי שם קובץ + + + + Please enter a new name: + נא להקליד שם חדש: + + + + + + + + Error + שגיאה + + + + Create Folder + יצירת תיקייה + + + + Create File + יצירת קובץ + + + + Please enter a new file name: + נא להקליד שם חדש לקובץ: + + + + New text file + קובץ טקסט חדש + + + + Please enter a new folder name: + נא להקליד שם חדש לתיקייה: + + + + New folder + תיקייה חדשה + + + + Enter a name for the new %1: + + + + + Custom Icon Error + שגיאת סמל בהתאמה אישית + + + + The path is not mounted. + הנתיב אינו מעוגן. + + + + Invalid desktop entry file: '%1' + קובץ רשומת שולחן עבודה שגוי: ‚%1’ + + + + No default application is set to launch '%1' + לא מוגדר יישום בררת מחדל להפעלת ‚%1’ + + + + Cannot set working directory to '%1': %2 + לא ניתן להגדיר את תיקייה העבודה אל ‚%1’:‏ %2 + + + + Identifier: + + + + + RenameDialog + + + Confirm to replace files + אישור להחלפת הקבצים + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + <html><head/><body><p><span style=" font-weight:600;">כבר קיים קובץ בשם הזה במיקום הזה.</span></p><p>להחליף את הקובץ הקיים?</p></body></html> + + + + dest + יעד + + + + with the following file? + בקובץ הבא? + + + + src file info + פרטי קובץ מקור + + + + dest file info + פרטי קובץ יעד + + + + src + מקור + + + + &File name: + שם &קובץ: + + + + Apply this option to all existing files + יש להחיל אפשרות זו לכל הקבצים הקיימים + + + + SearchDialog + + + Search Files + חיפוש קבצים + + + + Name/Location + שם/מיקום + + + + File Name Patterns: + תבניות שמות קבצים: + + + + * + + + + + Case insensitive + ללא תלות ברישיות + + + + Use regular expression + שימוש בביטויים רגולריים + + + + Places to Search: + מיקומים לחיפוש: + + + + &Add + הו&ספה + + + + &Remove + הס&רה + + + + Search in sub directories + חיפוש בתת־תיקיות + + + + Search for hidden files + חיפוש אחר קבצים מוסתרים + + + + File Type + סוג קובץ + + + + Only search for files of following types: + לחפש קבצים מהסוגים הבאים בלבד: + + + + Text files + קובצי טקסט + + + + Image files + קובצי תמונות + + + + Audio files + קובצי שמע + + + + Video files + קובצי וידאו + + + + Documents + מסמכים + + + + Folders + תיקיות + + + + Content + תוכן + + + + File contains: + הקובץ מכיל: + + + + Case insensiti&ve + ללא תלות ברי&שיות + + + + &Use regular expression + שימוש ב&ביטויים רגולריים + + + + Properties + מאפיינים + + + + File Size: + גודל קובץ: + + + + Larger than: + גדול מ־: + + + + + Bytes + בתים + + + + + KiB + + + + + + MiB + + + + + + GiB + + + + + Smaller than: + קטן מ־: + + + + Last Modified Time: + מועד השינוי האחרון: + + + + Earlier than: + לפני: + + + + Later than: + אחרי: + + + diff --git a/src/translations/libfm-qt_hu.ts b/src/translations/libfm-qt_hu.ts new file mode 100644 index 0000000..aaff134 --- /dev/null +++ b/src/translations/libfm-qt_hu.ts @@ -0,0 +1,1559 @@ + + + + + AppChooserDialog + + + Choose an Application + Alkalmazás választás + + + + Installed Applications + Telepített alkalmazások + + + + Custom Command + Egyéb parancs + + + + Command line to execute: + Végrehajtandó parancs: + + + + Application name: + Alkalmazás neve: + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + <b>A parancssorban használható speciális karakterek:</b> +<ul> +<li><b>%f</b>: Egy fájnév</li> +<li><b>%F</b>: Több fájlnév</li> +<li><b>%u</b>: Egy URI</li> +<li><b>%U</b>: Több URI</li> +</ul> + + + + Keep terminal window open after command execution + Végrehajtás után a terminál nyitva marad + + + + Execute in terminal emulator + Végrehajtás külső terminálban + + + + Set selected application as default action of this file type + A választott alkalmazás rendelődjék a fáljtípushoz + + + + EditBookmarksDialog + + + Edit Bookmarks + Könyvjelzők szerkesztése + + + + Name + Név + + + + Location + Hely + + + + &Add Item + Ho&zzáadás + + + + &Remove Item + Tö&rlés + + + + Use drag and drop to reorder the items + Húzással rendezhető minden elem + + + + ExecFileDialog + + + Execute file + Fájl futtatás + + + + &Open + &Nyitás + + + + E&xecute + &Futtatás + + + + Execute in &Terminal + Futtatás &terminálban + + + + Cancel + Mégse + + + + FileDialog + + + Location: + Hely: + + + + File name: + + + + + File type: + Fájltípus: + + + + FileOperationDialog + + + Destination: + Cél: + + + + Processing: + Feldolgozva: + + + + Preparing... + Előkészület... + + + + Progress + Folyamat + + + + Time remaining: + Hátralévő idő: + + + + Files processed: + Feldolgozott fájlok: + + + + FilePropsDialog + + + File Properties + Fájljellemzők + + + + General + Általános + + + + Location: + Hely: + + + + File type: + Fájltípus: + + + + MIME type: + Mime típus: + + + + File size: + Fájlméret: + + + + On-disk size: + Mérete a lemezen: + + + + Last modified: + Módosítási idő: + + + + Link target: + Link cél: + + + + Open With: + Megnyitás ezzel: + + + + Last accessed: + Utolsó hozzáférés: + + + + Contains: + + + + + Device Usage: + + + + + Permissions + Jogosultságok + + + + Ownership + Tulajdonos + + + + + + Group: + Csoport: + + + + + + Owner: + Tulajdonos: + + + + Access Control + Hozzáférés + + + + + Other: + Mások: + + + + Make the file executable + Futtatható + + + + + + Read + Olvas + + + + + + Write + Ír + + + + + + Execute + Végrehajt + + + + Sticky + Sticky + + + + SetUID + UID beállítás + + + + SetGID + GID beállítás + + + + Advanced Mode + Haladó mód + + + + Fm::AppChooserComboBox + + + Customize + Saját beállítás + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + Alkalmazás választás a "%1" fájl megnyitásához + + + + Fm::CreateNewMenu + + + Folder + Mappa + + + + Blank File + Üres fájl + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + A '%1' mappa érvenytelen + + + + Fm::DirTreeModel + + + Loading... + Olvasás... + + + + + + <No sub folders> + <Nincs almappa> + + + + Fm::DirTreeView + + + Open in New T&ab + Megnyi&tás új lapon + + + + Open in New Win&dow + Me&gnyitás új ablakban + + + + Open in Termina&l + Megnyitás terminá&lban + + + + Fm::DndActionMenu + + + Copy here + Másolás ide + + + + Move here + Mozgatás ide + + + + Create symlink here + Szimlink ide + + + + Cancel + Mégse + + + + Fm::EditBookmarksDialog + + + New bookmark + Új könyvjelző + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + A '%1' állomány egy asztalbeállító fájlnak tűnik. +Mit szeretne tenni vele? + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + Ez a '%1' fájl futtatható szkriptnek tűnik. +Mi legyen vele? + + + + This file '%1' is executable. Do you want to execute it? + Ez a '%1' fájl futtatható. Futtassuk? + + + + Fm::FileDialog + + + Go Back + + + + + Alt+Left + Go Back + + + + + Go Forward + + + + + Alt+Right + Go Forward + + + + + Reload + + + + + F5 + Reload + + + + + Create Folder + Mappakészítés + + + + Icon View + + + + + Thumbnail View + + + + + Compact View + + + + + Detailed List View + + + + + + Error + Hiba + + + + Please select a file + + + + + %1 already exists. +Do you want to replace it? + + + + + Path "%1" does not exist + + + + + "%1" is not a directory + + + + + "%1" is not a file + + + + + + &Open + &Nyitás + + + + + &Save + + + + + All Files (*) + + + + + Fm::FileDialogHelper + + + Open File + + + + + Save File + + + + + Fm::FileMenu + + + Open + Nyit + + + + Create &New + &Új létrehozása + + + + &Restore + &Visszalép + + + + Cut + Kivág + + + + Copy + Másol + + + + Paste + Beilleszt + + + + + &Move to Trash + Kukába &mozgat + + + + Trust selected executables + + + + + Trust this executable + + + + + Output + Kimenet + + + + &Delete + &Töröl + + + + Rename + Átnevez + + + + Open With... + Megnyitás ezzel... + + + + Other Applications + Más alkalmazások + + + + Extract to... + Kibontás... + + + + Extract Here + Kibontás ide + + + + Compress + Csomagolás + + + + Properties + Tulajdonságok + + + + Fm::FileOperation + + + Error + Hiba + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + Egyes fájlok nem mozgathatók a kukába, mert a rendszer ezt nem engedélyezi. +Töröljük őket véglegesen? + + + + + Confirm + Jóváhagyás + + + + Do you want to delete the selected files? + Töröljük a választott fájlokat? + + + + Do you want to move the selected files to trash can? + Helyezzük a kukába a választott fájlokat? + + + + Fm::FileOperationDialog + + + Move files + Fájlmozgatás + + + + Moving the following files to destination folder: + A következő fájlok mozgatása ide: + + + + Copy Files + Fájlmásolás + + + + Copying the following files to destination folder: + A következő fájlok másolása ide: + + + + Trash Files + Fájlok a kukába + + + + Moving the following files to trash can: + A következő fájlok kukába mozgatása: + + + + Delete Files + Fájltörlés + + + + Deleting the following files: + A következő fájlok törlése: + + + + Create Symlinks + Szimlink létrehozás + + + + Creating symlinks for the following files: + Szimlink készítés a következő fájlokra: + + + + Change Attributes + Attribútum változtatás + + + + Changing attributes of the following files: + A következő fájlok attribútum változtatása: + + + + Restore Trashed Files + Kukázott fájlok visszaállítása + + + + Restoring the following files from trash can: + A következő fájlok visszaállítása a kukából: + + + + + Error + Hiba + + + + Fm::FilePropsDialog + + + View folder content + Mappatartalom + + + + View and modify folder content + Mappatartalom és változtatása + + + + Read + Olvasás + + + + Read and write + Olvasás és írás + + + + Forbidden + Tiltott + + + + Files of different types + Különféle fájltípusok + + + + Multiple Files + Többszörös fájlok + + + + %p% used + + + + + %1 Free of %2 + + + + + no file + + + + + one file + + + + + %1 files + + + + + Select an icon + Ikon választás + + + + Images (*.png *.xpm *.svg *.svgz ) + Képek (*.png *.xpm *.svg *.svgz ) + + + + Apply changes + Változtatások alkalmazása + + + + Do you want to recursively apply these changes to all files and sub-folders? + Minden mappára és fájlra alkalmazzuk a változtatásokat? + + + + Fm::FileSearchDialog + + + Error + Hiba + + + + You should add at least one directory to search. + Kereséshez legalább egy mappa megadandó. + + + + Select a folder + Mappa választás + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + + + + + Fm::FolderMenu + + + Create &New + &Új létrehozása + + + + &Paste + &Beilleszt + + + + Select &All + Minden kivál&asztása + + + + Invert Selection + Kiválasztás fordítása + + + + Sorting + Rendezés + + + + Show Hidden + Rejtettek is + + + + Folder Pr&operties + Mappatulajd&onságok + + + + Output + Kimenet + + + + By File Name + Név + + + + By Modification Time + Módosítási idő + + + + By File Size + Méret + + + + By File Type + Típus + + + + By File Owner + Tulajdonos + + + + Ascending + Emelkedő + + + + Descending + Csökkenő + + + + Folder First + Mappák elől + + + + Case Sensitive + Nagybetűérzékeny + + + + Fm::FolderModel + + + Name + Név + + + + Type + Típus + + + + Size + Méret + + + + Modified + Módosítva + + + + Owner + Tulaj + + + + Group + Csoport + + + + Fm::FontButton + + + Bold + Kövér + + + + Italic + Dőlt + + + + Fm::MountOperationPasswordDialog + + + &Connect + &Kapcsolódás + + + + Fm::PathBar + + + &Edit Path + Útvonal sz&erkesztés + + + + &Copy Path + Útvonal &másolása + + + + Fm::PlacesModel + + + Places + Helyek + + + + Desktop + Asztal + + + + Trash + Kuka + + + + Computer + Számítógép + + + + Applications + Alkalmazások + + + + Network + Hálózat + + + + Devices + Eszközök + + + + Bookmarks + Könyvjelzők + + + + Fm::PlacesView + + + Empty Trash + Kukaürítés + + + + Open in New Tab + Megnyitás új lapon + + + + Open in New Window + Megnyitás új ablakban + + + + + Hide + Elrejtés + + + + Move Bookmark Up + Könyvjelző föl + + + + Move Bookmark Down + Könyvjelző le + + + + Rename Bookmark + Könyvjelző átnevezés + + + + Remove Bookmark + Könyvjelző törlés + + + + + Unmount + Lecsatol + + + + Mount + Csatol + + + + Eject + Kidobat + + + + Show All Entries + Minden bejegyzés mutatása + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + Típus: %1 +Méret: %2 +Módosítva: %3 + + + + + Type: %1 +Modified: %2 + Típus: %1 +Módosítva: %2 + + + + &Overwrite + &Felülír + + + + &Rename + &Átnevez + + + + Fm::SidePane + + + Places + Helyek + + + + Directory Tree + Könyvtárfa + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + + + + + MountOperationPasswordDialog + + + Mount + Csatlakozás + + + + Connect &anonymously + Névtelen cs&atlakozás + + + + Connect as u&ser: + Felha&sználóként: + + + + &Username: + &Név: + + + + &Password: + &Jelszó: + + + + &Domain: + &Hálózati cím: + + + + Forget password &immediately + Jelszófelejtés &iziben + + + + Remember password until you &logout + Je&lszófelejtés kilépéskor + + + + Remember &forever + Jelszóme&gjegyzés örökre + + + + QObject + + + + + + + Error + Hiba + + + + Rename File + Fájlátnevezés + + + + Please enter a new name: + Új név: + + + + Create Folder + Mappakészítés + + + + Please enter a new file name: + Új fájlnév: + + + + New text file + Új szövegfájl + + + + Please enter a new folder name: + Űj mappanév: + + + + New folder + Új mappa + + + + Enter a name for the new %1: + Az új %1 neve: + + + + Create File + Fájlkészítés + + + + Custom Icon Error + Egyéb ikon hiba + + + + The path is not mounted. + Az elérési út nincs csatolva. + + + + Invalid desktop entry file: '%1' + + + + + No default application is set to launch '%1' + + + + + Cannot set working directory to '%1': %2 + + + + + Identifier: + + + + + RenameDialog + + + Confirm to replace files + Fájlfelülírás megerősítése + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + <html><head/><body><p><span style=" font-weight:600;">Itt már van ilyen nevű fájl.</span></p><p>Felülírjuk a meglévőt?</p></body></html> + + + + dest + cél + + + + with the following file? + Ezzel a fájllal? + + + + src file info + forrásfájl infó + + + + dest file info + célfájl infó + + + + src + forrás + + + + &File name: + &Fájlnév: + + + + Apply this option to all existing files + Az összes fájlra vonatkozzon mindez + + + + SearchDialog + + + Search Files + Fájlkeresés + + + + Name/Location + Név/hely + + + + File Name Patterns: + Fájlnév szűrő: + + + + * + * + + + + Case insensitive + Nagybetű érzéketlen + + + + Use regular expression + Szabályos kifejezés + + + + Places to Search: + Keresési helyek: + + + + &Add + Hozzá&ad + + + + &Remove + Tö&röl + + + + Search in sub directories + Alkönyvtárakban is keres + + + + Search for hidden files + Rejtett fájlok is + + + + File Type + Fájltípus + + + + Only search for files of following types: + Csak ilyen típusú fájlokat keres: + + + + Text files + Szöveg + + + + Image files + Kép + + + + Audio files + Hang + + + + Video files + Videó + + + + Documents + Dokumentum + + + + Folders + Könyvtár + + + + Content + Tartalom + + + + File contains: + Fájl tartalmazza: + + + + Case insensiti&ve + Nagybetű ér&zéketlen + + + + &Use regular expression + Szabvá&nyos kifejezés + + + + Properties + Tulajdonságok + + + + File Size: + Méret: + + + + Larger than: + Nagyobb mint: + + + + + Bytes + Bájt + + + + + KiB + Kb + + + + + MiB + Mb + + + + + GiB + Gb + + + + Smaller than: + Kissebb mint: + + + + Last Modified Time: + Utolsó módosítás ideje: + + + + Earlier than: + Korábbi ennél: + + + + Later than: + Későbbi ennél: + + + diff --git a/src/translations/libfm-qt_id.ts b/src/translations/libfm-qt_id.ts new file mode 100644 index 0000000..5412284 --- /dev/null +++ b/src/translations/libfm-qt_id.ts @@ -0,0 +1,1560 @@ + + + + + AppChooserDialog + + + Choose an Application + Pilih Aplikasi + + + + Installed Applications + Aplikasi Terpasang + + + + Custom Command + Perintah Kustom + + + + Command line to execute: + Baris perintah untuk dieksekusi: + + + + Application name: + Nama aplikasi: + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + <b>Kode khusus berikut dapat digunakan dalam baris perintah:</b> +<ul> +<li><b>%f</b>: Mewakili satu nama berkas</li> +<li><b>%F</b>: Mewakili beberapa nama berkas</li> +<li><b>%u</b>: Mewakili satu URI berkas</li> +<li><b>%U</b>: Mewakili beberapa URI berkas</li> +</ul> + + + + Keep terminal window open after command execution + Biarkan jendela terminal terbuka setelah eksekusi perintah + + + + Execute in terminal emulator + Jalankan di emulator terminal + + + + Set selected application as default action of this file type + Atur aplikasi terpilih sebagai tindakan baku dari tipe berkas ini + + + + EditBookmarksDialog + + + Edit Bookmarks + Sunting Markah + + + + Name + Nama + + + + Location + Lokasi + + + + &Add Item + T&ambah Item + + + + &Remove Item + Hapus Item + + + + Use drag and drop to reorder the items + Gunakan drag dan drop untuk menyusun ulang item + + + + ExecFileDialog + + + Execute file + Jalankan berkas + + + + &Open + Buka + + + + E&xecute + E&ksekusi + + + + Execute in &Terminal + Eksekusi di &Terminal + + + + Cancel + Batal + + + + FileDialog + + + Location: + Lokasi: + + + + File name: + Nama berkas: + + + + File type: + Tipe berkas: + + + + FileOperationDialog + + + Destination: + Tujuan: + + + + Processing: + Memproses: + + + + Preparing... + Mempersiapkan... + + + + Progress + Kemajuan + + + + Time remaining: + Sisa waktu: + + + + Files processed: + Berkas diproses: + + + + FilePropsDialog + + + File Properties + Properti Berkas + + + + General + Umum + + + + Location: + Lokasi: + + + + File type: + Tipe berkas: + + + + MIME type: + Tipe MIME: + + + + File size: + Ukuran berkas: + + + + On-disk size: + Ukuran pada disk: + + + + Last modified: + Terakhir diubah: + + + + Link target: + Tautan target: + + + + Open With: + Buka Dengan: + + + + Last accessed: + Terakhir diakses: + + + + Contains: + Berisi: + + + + Device Usage: + Penggunaan Perangkat: + + + + Permissions + Hak akses + + + + Ownership + Kepemilikan + + + + + + Group: + Grup: + + + + + + Owner: + Pemilik: + + + + Access Control + Kontrol Akses + + + + + Other: + Lainnya: + + + + Make the file executable + Buat berkas dapat dieksekusi + + + + + + Read + Baca + + + + + + Write + Tulis + + + + + + Execute + Eksekusi + + + + Sticky + Sticky + + + + SetUID + SetUID + + + + SetGID + SetGID + + + + Advanced Mode + Mode Lanjutan + + + + Fm::AppChooserComboBox + + + Customize + Kustomisasi + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + Pilih aplikasi untuk membuka berkas "%1" + + + + Fm::CreateNewMenu + + + Folder + Folder + + + + Blank File + Berkas Kosong + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + Direktori '%1' yang ditetapkan tidak valid + + + + Fm::DirTreeModel + + + Loading... + Memuat... + + + + + + <No sub folders> + <Tanpa sub folder> + + + + Fm::DirTreeView + + + Open in New T&ab + Buka di T&ab Baru + + + + Open in New Win&dow + Buka di Jen&dela Baru + + + + Open in Termina&l + Buka di Termina&l + + + + Fm::DndActionMenu + + + Copy here + Salin disini + + + + Move here + Pindah disini + + + + Create symlink here + Buat symlink disini + + + + Cancel + Batal + + + + Fm::EditBookmarksDialog + + + New bookmark + Markah baru + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + Berkas '%1' ini tampaknya entri desktop. +Apa yang ingin Anda lakukan? + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + Berkas teks '%1' ini tampaknya sebuah skrip yang dapat dieksekusi. +Apa yang ingin Anda lakukan dengannya? + + + + This file '%1' is executable. Do you want to execute it? + Berkas '%1' ini dapat dieksekusi. Apakah Anda ingin mengeksekusinya? + + + + Fm::FileDialog + + + Go Back + Kembali + + + + Alt+Left + Go Back + Alt+Left + + + + Go Forward + Maju + + + + Alt+Right + Go Forward + Alt+Right + + + + Reload + Muat ulang + + + + F5 + Reload + F5 + + + + Create Folder + Buat Folder + + + + Icon View + Tampilan Ikon + + + + Thumbnail View + Tampilan Gambar Kecil + + + + Compact View + Tampilan Kompak + + + + Detailed List View + Tampilan Daftar Rinci + + + + + Error + Kesalahan + + + + Please select a file + Silakan pilih berkas + + + + %1 already exists. +Do you want to replace it? + %1 sudah ada. +Apakah Anda ingin menggantinya? + + + + Path "%1" does not exist + Jalur "%1" tidak ada + + + + "%1" is not a directory + "%1" bukan sebuah direktori + + + + "%1" is not a file + "%1" bukan sebuah berkas + + + + + &Open + &Buka + + + + + &Save + &Simpan + + + + All Files (*) + Semua Berkas (*) + + + + Fm::FileDialogHelper + + + Open File + Buka Berkas + + + + Save File + Simpan Berkas + + + + Fm::FileMenu + + + Open + Buka + + + + Open With... + Buka Dengan... + + + + Other Applications + Aplikasi Lainnya + + + + Create &New + Buat Baru + + + + &Restore + Pulihkan + + + + Cut + Potong + + + + Copy + Salin + + + + Paste + Tempel + + + + + &Move to Trash + Pindah ke Tempat Sampah + + + + Rename + Ubah nama + + + + Extract to... + Ekstrak ke... + + + + Extract Here + Ekstrak Disini + + + + Compress + Kompres + + + + Properties + Properti + + + + Trust selected executables + Percayai executable yang dipilih + + + + Trust this executable + Percayai executable ini + + + + Output + Keluaran + + + + &Delete + Hapus + + + + Fm::FileOperation + + + Error + Kesalahan + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + Beberapa berkas tidak dapat dipindahkan ke tempat sampah karena sistem berkas yang digunakan tidak mendukung operasi ini. +Apakah Anda ingin menghapusnya saja? + + + + + Confirm + Konfirmasi + + + + Do you want to delete the selected files? + Apakah Anda ingin menghapus berkas yang dipilih? + + + + Do you want to move the selected files to trash can? + Apakah Anda ingin memindahkan berkas yang dipilih ke tempat sampah? + + + + Fm::FileOperationDialog + + + Move files + Pindahkan berkas + + + + Moving the following files to destination folder: + Memindahkan berkas berikut ke folder tujuan: + + + + Copy Files + Salin Berkas + + + + Copying the following files to destination folder: + Menyalin berkas berikut ke folder tujuan: + + + + Trash Files + Buang Berkas + + + + Moving the following files to trash can: + Memindahkan berkas berikut ke tempat sampah: + + + + Delete Files + Hapus Berkas + + + + Deleting the following files: + Menghapus berkas berikut: + + + + Create Symlinks + Buat Symlink + + + + Creating symlinks for the following files: + Membuat symlink untuk berkas berikut: + + + + Change Attributes + Ubah Atribut + + + + Changing attributes of the following files: + Mengubah atribut berkas berikut: + + + + Restore Trashed Files + Pulihkan Berkas yang Dibuang + + + + Restoring the following files from trash can: + Memulihkan berkas berikut ke tempat sampah: + + + + + Error + Kesalahan + + + + Fm::FilePropsDialog + + + View folder content + Lihat konten folder + + + + View and modify folder content + Lihat dan ubah konten folder + + + + Read + Baca + + + + Read and write + Baca dan Tulis + + + + Forbidden + Terlarang + + + + Files of different types + Berkas dari berbagai tipe + + + + Multiple Files + Beberapa Berkas + + + + %p% used + %p% digunakan + + + + %1 Free of %2 + %1 kosong dari %2 + + + + no file + tidak ada berkas + + + + one file + satu berkas + + + + %1 files + %1 berkas + + + + Select an icon + Pilih ikon + + + + Images (*.png *.xpm *.svg *.svgz ) + Gambar (*.png *.xpm *.svg *.svgz ) + + + + Apply changes + Terapkan perubahan + + + + Do you want to recursively apply these changes to all files and sub-folders? + Apakah Anda ingin secara rekursif menerapkan perubahan ini ke semua berkas dan sub-folder? + + + + Fm::FileSearchDialog + + + Error + Kesalahan + + + + You should add at least one directory to search. + Anda harus menambahkan setidaknya satu direktori untuk dicari. + + + + Select a folder + Pilih folder + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + Tidak dapat membuat tautan pada berkas sistem non-native + + + + Fm::FolderMenu + + + Create &New + Buat &Baru + + + + &Paste + Tempel + + + + Select &All + Pilih Semua + + + + Invert Selection + Pilihan Sebaliknya + + + + Sorting + Penyortiran + + + + Show Hidden + Tampilkan Tersembunyi + + + + Folder Pr&operties + Pr&operti Folder + + + + Output + Keluaran + + + + By File Name + Berdasarkan Nama Berkas + + + + By Modification Time + Berdasarkan Waktu Modifikasi + + + + By File Size + Berdasarkan Ukuran Berkas + + + + By File Type + Berdasarkan Tipe Berkas + + + + By File Owner + Berdasarkan Pemilik Berkas + + + + Ascending + Menaik + + + + Descending + Menurun + + + + Folder First + Folder yang Pertama + + + + Case Sensitive + Sensitif Kata + + + + Fm::FolderModel + + + Name + Nama + + + + Type + Tipe + + + + Size + Ukuran + + + + Modified + Dimodifikasi + + + + Owner + Pemilik + + + + Group + Grup + + + + Fm::FontButton + + + Bold + Tebal + + + + Italic + Miring + + + + Fm::MountOperationPasswordDialog + + + &Connect + Sambungkan + + + + Fm::PathBar + + + &Edit Path + Sunting Jalur + + + + &Copy Path + Salin Jalur + + + + Fm::PlacesModel + + + Places + Tempat + + + + Desktop + Desktop + + + + Computer + Komputer + + + + Applications + Aplikasi + + + + Network + Jaringan + + + + Devices + Perangkat + + + + Bookmarks + Markah + + + + Trash + Tempat Sampah + + + + Fm::PlacesView + + + Open in New Tab + Buka di Tab Baru + + + + Open in New Window + Buka di Jendela Baru + + + + Empty Trash + Kosongkan Tempat Sampah + + + + + Hide + Sembunyikan + + + + Move Bookmark Up + Pindahkan Markah Keatas + + + + Move Bookmark Down + Pindahkan Markah Kebawah + + + + Rename Bookmark + Ubah Nama Markah + + + + Remove Bookmark + Hapus Markah + + + + + Unmount + Lepat Kait + + + + Mount + Kaitkan + + + + Eject + Keluarkan + + + + Show All Entries + Tampilkan semua entri + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + Tipe: %1 +Ukuran: %2 +Dimodifikasi: %3 + + + + + Type: %1 +Modified: %2 + Tipe: %1 +Dimodifikasi: %2 + + + + &Overwrite + Timpa + + + + &Rename + Ubah Nama + + + + Fm::SidePane + + + Places + Tempat + + + + Directory Tree + Pohon Direktori + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + Tidak dapat mengembalikan berkas '%s': jalur asal tidak diketahui + + + + MountOperationPasswordDialog + + + Mount + Kait + + + + Connect &anonymously + Hubungkan secara &anonim + + + + Connect as u&ser: + Hubungkan sebagai pengguna: + + + + &Username: + Nama pengguna: + + + + &Password: + Kata sandi: + + + + &Domain: + Ranah: + + + + Forget password &immediately + Lupakan berkas segera + + + + Remember password until you &logout + Ingat kata sandi sampai Anda log keluar + + + + Remember &forever + Ingat selamanya + + + + QObject + + + Rename File + Ubah Nama Berkas + + + + Please enter a new name: + Silakan masukkan nama baru: + + + + + + + + Error + Kesalahan + + + + Create Folder + Buat Folder + + + + Create File + Buat Berkas + + + + Please enter a new file name: + Silakan masukkan nama berkas baru: + + + + New text file + Berkas teks baru + + + + Please enter a new folder name: + Silakan masukkan nama folder baru: + + + + New folder + Folder baru + + + + Enter a name for the new %1: + Masukkan nama untuk %1 baru: + + + + Custom Icon Error + Kesalahan Ikon Kustom + + + + The path is not mounted. + Jalur ini tidak dimount. + + + + Invalid desktop entry file: '%1' + Berkas entri desktop tidak valid: '%1' + + + + No default application is set to launch '%1' + Tidak ada aplikasi default yang diatur untuk menjalankan '%1' + + + + Cannot set working directory to '%1': %2 + Tidak dapat mengatur direktori kerja menjadi '%1': %2 + + + + Identifier: + Pengenal: + + + + RenameDialog + + + Confirm to replace files + Konfirmasi untuk mengganti berkas + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + <html><head/><body><p><span style=" font-weight:600;">Sudah ada berkas dengan nama yang sama di lokasi ini.</span></p><p>Apakah Anda ingin mengganti berkas yang ada?</p></body></html> + + + + dest + tujuan + + + + with the following file? + dengan berkas berikut? + + + + src file info + info berkas sumber + + + + dest file info + info berkas tujuan + + + + src + sumber + + + + &File name: + Nama berkas: + + + + Apply this option to all existing files + Terapkan opsi ini ke semua file yang ada + + + + SearchDialog + + + Search Files + Cari Berkas + + + + Name/Location + Nama/Lokasi + + + + File Name Patterns: + Pola Nama Berkas: + + + + * + * + + + + Case insensitive + Tidak sensitif kata + + + + Use regular expression + Gunakan ekspresi reguler + + + + Places to Search: + Tempat untuk Dicari: + + + + &Add + Tambah + + + + &Remove + Hapus + + + + Search in sub directories + Cari di sub direktori + + + + Search for hidden files + Cari berkas tersembunyi + + + + File Type + Tipe Berkas + + + + Only search for files of following types: + Hanya cari berkas dengan jenis berikut: + + + + Text files + Berkas teks + + + + Image files + Berkas gambar + + + + Audio files + Berkas audio + + + + Video files + Berkas video + + + + Documents + Dokumen + + + + Folders + Folder + + + + Content + Konten + + + + File contains: + Isi berkas: + + + + Case insensiti&ve + Tidak sensitif kata + + + + &Use regular expression + G&unakan ekspresi reguler + + + + Properties + Properti + + + + File Size: + Ukuran Berkas: + + + + Larger than: + Lebih besar dari: + + + + + Bytes + Byte + + + + + KiB + KiB + + + + + MiB + MiB + + + + + GiB + GiB + + + + Smaller than: + Lebih kecil dari: + + + + Last Modified Time: + Waktu Modifikasi Terakhir: + + + + Earlier than: + Lebih awal dari: + + + + Later than: + Lebih lambat dari: + + + diff --git a/src/translations/libfm-qt_it.ts b/src/translations/libfm-qt_it.ts new file mode 100644 index 0000000..5b6d720 --- /dev/null +++ b/src/translations/libfm-qt_it.ts @@ -0,0 +1,1560 @@ + + + + + AppChooserDialog + + + Choose an Application + Scegli un'applicazione + + + + Installed Applications + Applicazioni installate + + + + Custom Command + Comando personalizzato + + + + Command line to execute: + Riga di comando da eseguire: + + + + Application name: + Nome dell'applicazione: + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + <b>Questi caratteri speciali possono essere usati nella riga di comando:</b> +<ul> +<li><b>%f</b>: indica un nome file singolo</li> +<li><b>%F</b>: indica diversi nomi file</li> +<li><b>%u</b>: indica un URI singolo del file</li> +<li><b>%U</b>: indica diversi URI</li> +</ul> + + + + Keep terminal window open after command execution + Non chiudere il terminale dopo l'esecuzione + + + + Execute in terminal emulator + Esegui in un emulatore di terminale + + + + Set selected application as default action of this file type + Ricorda come associazione predefinita per questo tipo di file + + + + EditBookmarksDialog + + + Edit Bookmarks + Modifica segnalibri + + + + Name + Nome + + + + Location + Posizione + + + + &Add Item + &Aggiungi elemento + + + + &Remove Item + &Rimuovi elemento + + + + Use drag and drop to reorder the items + Trascina per riordinare gli elementi + + + + ExecFileDialog + + + Execute file + Esegui file + + + + &Open + &Apri + + + + E&xecute + E&segui + + + + Execute in &Terminal + Esegui in un &terminale + + + + Cancel + Annulla + + + + FileDialog + + + Location: + Posizione: + + + + File name: + Nome del file: + + + + File type: + Tipo file: + + + + FileOperationDialog + + + Destination: + Destinazione: + + + + Processing: + Elaborazione: + + + + Preparing... + Preparazione... + + + + Progress + Avanzamento + + + + Time remaining: + Tempo rimanente: + + + + Files processed: + File elaborati: + + + + FilePropsDialog + + + File Properties + Proprietà file + + + + General + Generale + + + + Location: + Posizione: + + + + File type: + Tipo file: + + + + MIME type: + Tipo MIME: + + + + File size: + Dimensione file: + + + + On-disk size: + Dimensione sul disco: + + + + Last modified: + Ultima modifica: + + + + Link target: + Destinazione collegamento: + + + + Open With: + Apri con: + + + + Last accessed: + Ultimo accesso: + + + + Contains: + Contiene: + + + + Device Usage: + Utilizzo Device: + + + + Permissions + Permessi + + + + Ownership + Proprietà + + + + + + Group: + Gruppo: + + + + + + Owner: + Proprietario: + + + + Access Control + Controllo degli accessi + + + + + Other: + Altri: + + + + Make the file executable + Rendi il file eseguibile + + + + + + Read + Lettura + + + + + + Write + Scrittura + + + + + + Execute + Esecuzione + + + + Sticky + Sticky Bit + + + + SetUID + Setta UID + + + + SetGID + Setta GID + + + + Advanced Mode + Modalità avanzata + + + + Fm::AppChooserComboBox + + + Customize + Personalizza + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + Seleziona un'applicazione per aprire i file "%1" + + + + Fm::CreateNewMenu + + + Folder + Cartella + + + + Blank File + File vuoto + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + La cartella specificata %1 non è valida + + + + Fm::DirTreeModel + + + Loading... + Caricamento in corso... + + + + + + <No sub folders> + <nessuna sottocartella> + + + + Fm::DirTreeView + + + Open in New T&ab + Apri in una nuova &scheda + + + + Open in New Win&dow + Apri in una nuova &finestra + + + + Open in Termina&l + Apri in un &terminale + + + + Fm::DndActionMenu + + + Copy here + Copia qui + + + + Move here + Sposta qui + + + + Create symlink here + Crea collegamento qui + + + + Cancel + Annulla + + + + Fm::EditBookmarksDialog + + + New bookmark + Nuovo segnalibro + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + Questo file '%1' sembra un file .desktop. +Cosa vuoi fare? + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + Questo file di testo "%1" sembra essere uno script eseguibile. +Cosa vuoi fare? + + + + This file '%1' is executable. Do you want to execute it? + Questo file "%1" è eseguibile. Vuoi eseguirlo? + + + + Fm::FileDialog + + + Go Back + Indietro + + + + Alt+Left + Go Back + Alt+Sinistra + + + + Go Forward + Avanti + + + + Alt+Right + Go Forward + Alt+Destra + + + + Reload + Ricarica + + + + F5 + Reload + F5 + + + + Create Folder + Crea cartella + + + + Icon View + Vista icone + + + + Thumbnail View + Vista miniature + + + + Compact View + Vista compatta + + + + Detailed List View + Vista dettagliata + + + + + Error + Errore + + + + Please select a file + Per favore selezionare un file + + + + %1 already exists. +Do you want to replace it? + %1 esiste già. +Sostituirlo? + + + + Path "%1" does not exist + Il percorso "%1" non esiste + + + + "%1" is not a directory + "%1" non è una cartella + + + + "%1" is not a file + "%1" non è un file + + + + + &Open + &Apri + + + + + &Save + &Salva + + + + All Files (*) + Tutti file (*) + + + + Fm::FileDialogHelper + + + Open File + Apri file + + + + Save File + Salva file + + + + Fm::FileMenu + + + Open + Apri + + + + Cut + Taglia + + + + Copy + Copia + + + + Paste + Incolla + + + + + &Move to Trash + Cestin&a + + + + Trust selected executables + Fidati degli eseguibili selezionati + + + + Trust this executable + Fidati del eseguibile + + + + Output + Risultato + + + + &Delete + &Elimina + + + + Rename + Rinomina + + + + Open With... + Apri con... + + + + Other Applications + Altre applicazioni + + + + Create &New + Crea &nuovo + + + + &Restore + &Ripristina + + + + Extract to... + Estrai in... + + + + Extract Here + Estrai qui + + + + Compress + Comprimi + + + + Properties + Proprietà + + + + Fm::FileOperation + + + Error + Errore + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + Alcuni file non possono essere spostati nel cestino perché il file system su cui si trovano non supporta questa operazione. +Vuoi invece eliminarli? + + + + + Confirm + Conferma + + + + Do you want to delete the selected files? + Vuoi eliminare i file selezionati? + + + + Do you want to move the selected files to trash can? + Vuoi spostare nel cestino i file selezionati? + + + + Fm::FileOperationDialog + + + Move files + Sposta file + + + + Moving the following files to destination folder: + Spostamento dei file seguenti nella cartella di destinazione: + + + + Copy Files + Copia file + + + + Copying the following files to destination folder: + Copia dei file seguenti nella cartella di destinazione: + + + + Trash Files + Cestina file + + + + Moving the following files to trash can: + Spostamento dei file seguenti nel cestino: + + + + Delete Files + Elimina file + + + + Deleting the following files: + Eliminazione dei file seguenti: + + + + Create Symlinks + Crea collegamenti simbolici + + + + Creating symlinks for the following files: + Creazione collegamenti simbolici per i seguenti file: + + + + Change Attributes + Cambia attributi + + + + Changing attributes of the following files: + Modifica degli attributi per i seguenti file: + + + + Restore Trashed Files + Ripristina file cestinati + + + + Restoring the following files from trash can: + Ripristino dei file seguenti dal cestino: + + + + + Error + Errore + + + + Fm::FilePropsDialog + + + View folder content + Visualizza il contenuto della cartella + + + + View and modify folder content + Visualizza e modifica il contenuto della cartella + + + + Read + Lettura + + + + Read and write + Lettura e scrittura + + + + Forbidden + Vietato + + + + Files of different types + File di tipi diversi + + + + Multiple Files + File multipli + + + + %p% used + %p% usato + + + + %1 Free of %2 + %1 Liberi di %2 + + + + no file + nessun file + + + + one file + un file + + + + %1 files + %1 file + + + + Select an icon + Seleziona un icona + + + + Images (*.png *.xpm *.svg *.svgz ) + Immagini (*.png *.xpm *.svg *.svgz ) + + + + Apply changes + Applica modifiche + + + + Do you want to recursively apply these changes to all files and sub-folders? + Vuoi applicare ricorsivamente queste modifiche a tutti i file e a tutte le sottocartelle? + + + + Fm::FileSearchDialog + + + Error + Errore + + + + You should add at least one directory to search. + Dovresti aggiungere almeno una cartella in cui cercare. + + + + Select a folder + Seleziona una cartella + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + Impossibile creare un collegamento su un filesystem non nativo + + + + Fm::FolderMenu + + + Create &New + Crea &nuovo + + + + &Paste + &Incolla + + + + Select &All + Seleziona t&utto + + + + Invert Selection + Inverti selezione + + + + Sorting + Ordinamento + + + + Show Hidden + Mostra nascosti + + + + Folder Pr&operties + Pr&oprietà cartella + + + + Output + + + + + By File Name + Per nome file + + + + By Modification Time + Per data modifica + + + + By File Size + Per dimensione file + + + + By File Type + Per tipo file + + + + By File Owner + Per proprietario file + + + + Ascending + Crescente + + + + Descending + Decrescente + + + + Folder First + Prima le cartelle + + + + Case Sensitive + Distingui MAIUSCOLE/minuscole + + + + Fm::FolderModel + + + Name + Nome + + + + Type + Tipo + + + + Size + Dimensione + + + + Modified + Modificato + + + + Owner + Proprietario + + + + Group + Gruppo + + + + Fm::FontButton + + + Bold + Grassetto + + + + Italic + Corsivo + + + + Fm::MountOperationPasswordDialog + + + &Connect + &Connetti + + + + Fm::PathBar + + + &Edit Path + &Modifica percorso + + + + &Copy Path + &Copia percorso + + + + Fm::PlacesModel + + + Places + Risorse + + + + Desktop + Scrivania + + + + Trash + Cestino + + + + Computer + Computer + + + + Applications + Applicazioni + + + + Network + Rete + + + + Devices + Dispositivi + + + + Bookmarks + Segnalibri + + + + Fm::PlacesView + + + Empty Trash + Svuota cestino + + + + Open in New Tab + Apri in una nuova scheda + + + + Open in New Window + Apri in una nuova finestra + + + + + Hide + Nascondi + + + + Move Bookmark Up + Sposta segnalibro in su + + + + Move Bookmark Down + Sposta segnalibro in giù + + + + Rename Bookmark + Rinomina segnalibro + + + + Remove Bookmark + Rimuovi segnalibro + + + + + Unmount + Smonta + + + + Mount + Monta + + + + Eject + Espelli + + + + Show All Entries + Mostra tutte le risorse + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + Tipo: %1 +Dimensione: %2 +Ultima modifica: %3 + + + + + Type: %1 +Modified: %2 + Tipo: %1 +Ultima modifica: %2 + + + + &Overwrite + S&ovrascrivi + + + + &Rename + &Rinomina + + + + Fm::SidePane + + + Places + Risorse + + + + Directory Tree + Albero delle cartelle + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + Impossibile ripristinare il file '%1': percorso originale sconosciuto + + + + MountOperationPasswordDialog + + + Mount + Monta + + + + Connect &anonymously + Connetti in modo &anonimo + + + + Connect as u&ser: + Connetti come &utente: + + + + &Username: + &Nome utente: + + + + &Password: + &Password: + + + + &Domain: + &Dominio: + + + + Forget password &immediately + Dimentica la password &immediatamente + + + + Remember password until you &logout + Ricorda &la password fino al termine sessione + + + + Remember &forever + Ricorda per &sempre + + + + QObject + + + + + + + Error + Errore + + + + Rename File + Rinomina file + + + + Please enter a new name: + Digita un nuovo nome: + + + + Create Folder + Crea cartella + + + + Please enter a new file name: + Digita un nuovo nome del file: + + + + New text file + Nuovo file di testo + + + + Please enter a new folder name: + Digita un nuovo nome della cartella: + + + + New folder + Nuova cartella + + + + Enter a name for the new %1: + Digita un nome per %1: + + + + Create File + Crea file + + + + Custom Icon Error + Errore per l'icona personalizzata + + + + The path is not mounted. + Questo percorso non è montato. + + + + Invalid desktop entry file: '%1' + File desktop non valido: '%1' + + + + No default application is set to launch '%1' + Nessuna applicazione predefinita per eseguire '%1' + + + + Cannot set working directory to '%1': %2 + Impossibile impostare cartella di lavoro a '%1': %2 + + + + Identifier: + Identificatore: + + + + RenameDialog + + + Confirm to replace files + Conferma sostituzione dei file + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + <html><head/><body><p><span style=" font-weight:600;">C'è già un file con lo stesso nome in questa posizione.</span></p><p>Vuoi sostituire il file esistente?</p></body></html> + + + + dest + Destinazione + + + + with the following file? + con il file seguente? + + + + src file info + info file sorg + + + + dest file info + info file dest + + + + src + sorg + + + + &File name: + Nome &file: + + + + Apply this option to all existing files + Applica questa opzione a tutti i file esistenti + + + + SearchDialog + + + Search Files + Cerca file + + + + Name/Location + Nome/Posizione + + + + File Name Patterns: + Modelli di nome dei file: + + + + * + * + + + + Case insensitive + Non distinguere le maiuscole + + + + Use regular expression + Utilizza un'espressione regolare + + + + Places to Search: + Risorse in cui cercare: + + + + &Add + &Aggiungi + + + + &Remove + &Rimuovi + + + + Search in sub directories + Cerca nelle sottocartelle + + + + Search for hidden files + Cerca i file nascosti + + + + File Type + Tipo di file + + + + Only search for files of following types: + Cerca solo i file dei tipi seguenti: + + + + Text files + File di testo + + + + Image files + File di immagini + + + + Audio files + File audio + + + + Video files + File video + + + + Documents + Documenti + + + + Folders + Cartelle + + + + Content + Contenuto + + + + File contains: + Il file contiene: + + + + Case insensiti&ve + Non disting&uere le maiuscole + + + + &Use regular expression + &Utilizza un'espressione regolare + + + + Properties + Proprietà + + + + File Size: + Dimensione file: + + + + Larger than: + Più grande di: + + + + + Bytes + Byte + + + + + KiB + KiB + + + + + MiB + MiB + + + + + GiB + GiB + + + + Smaller than: + Più piccolo di: + + + + Last Modified Time: + Ultima modifica: + + + + Earlier than: + Prima del: + + + + Later than: + Dopo del: + + + diff --git a/src/translations/libfm-qt_ja.ts b/src/translations/libfm-qt_ja.ts new file mode 100644 index 0000000..b0d31fb --- /dev/null +++ b/src/translations/libfm-qt_ja.ts @@ -0,0 +1,1560 @@ + + + + + AppChooserDialog + + + Choose an Application + アプリケーションを選ぶ + + + + Installed Applications + インストール済アプリケーション + + + + Custom Command + カスタムコマンド + + + + Command line to execute: + 実行するコマンドライン: + + + + Application name: + アプリケーション名: + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + <b>コマンドラインには、次の特別なコードを使用することができます:</b> +<ul> +<li><b>%f</b>: 単一のファイル名を表す</li> +<li><b>%F</b>: 複数のファイル名を表す</li> +<li><b>%u</b>: 単一のファイルのURIを表す</li> +<li><b>%U</b>: 複数のURIを表す</li> +</ul> + + + + Keep terminal window open after command execution + コマンド実行後も端末のウィンドウを閉じない + + + + Execute in terminal emulator + 端末エミュレーター内で実行 + + + + Set selected application as default action of this file type + 選択したアプリケーションをこのファイルの種類に関連付ける + + + + EditBookmarksDialog + + + Edit Bookmarks + ブックマークを編集 + + + + Name + 名前 + + + + Location + 場所 + + + + &Add Item + アイテムを追加(&A) + + + + &Remove Item + アイテムを削除(&R) + + + + Use drag and drop to reorder the items + アイテムを並べ替えるにはドラッグアンドドロップ + + + + ExecFileDialog + + + Execute file + ファイルを実行 + + + + &Open + 開く(&O) + + + + E&xecute + 実行(&E) + + + + Execute in &Terminal + 端末内で実行(&T) + + + + Cancel + キャンセル + + + + FileDialog + + + Location: + 場所: + + + + File name: + ファイル名: + + + + File type: + 種類: + + + + FileOperationDialog + + + Destination: + 送り先: + + + + Processing: + 処理中: + + + + Preparing... + 準備中... + + + + Progress + 進行状況 + + + + Time remaining: + 残り時間: + + + + Files processed: + + + + + FilePropsDialog + + + File Properties + ファイルのプロパティー + + + + General + 一般 + + + + Location: + 場所: + + + + File type: + 種類: + + + + MIME type: + MIMEタイプ: + + + + File size: + サイズ: + + + + On-disk size: + ディスク上のサイズ: + + + + Last modified: + 最終更新日時: + + + + Link target: + リンク先: + + + + Open With: + 関連付け: + + + + Last accessed: + 最終アクセス日時: + + + + Contains: + + + + + Device Usage: + + + + + Permissions + パーミッション + + + + Ownership + 所有権 + + + + + + Group: + グループ: + + + + + + Owner: + 所有者: + + + + Access Control + アクセス制限 + + + + + Other: + その他: + + + + Make the file executable + ファイルを実行可能にする + + + + + + Read + 読取り + + + + + + Write + 書込み + + + + + + Execute + 実行 + + + + Sticky + スティッキー + + + + SetUID + UIDを設定 + + + + SetGID + GIDを設定 + + + + Advanced Mode + 高度なモード + + + + Fm::AppChooserComboBox + + + Customize + カスタマイズ + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + ファイル"%1"を開くアプリケーションを選択 + + + + Fm::CreateNewMenu + + + Folder + フォルダー + + + + Blank File + 空のファイル + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + 指定されたディレクトリ '%1' は妥当ではありません + + + + Fm::DirTreeModel + + + Loading... + ロード中... + + + + + + <No sub folders> + <サブフォルダーなし> + + + + Fm::DirTreeView + + + Open in New T&ab + 新しいタブで開く(&a) + + + + Open in New Win&dow + 新しいウィンドウで開く(&d) + + + + Open in Termina&l + ターミナルで開く(&l) + + + + Fm::DndActionMenu + + + Copy here + ここへコピー + + + + Move here + ここへ移動 + + + + Create symlink here + シンボリックリンクを作成 + + + + Cancel + キャンセル + + + + Fm::EditBookmarksDialog + + + New bookmark + 新規ブックマーク + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + このテキストファイル '%1' は実行可能スクリプトです。 +どうしますか? + + + + This file '%1' is executable. Do you want to execute it? + このファイル '%1' は実行可能です。 +実行しますか? + + + + Fm::FileDialog + + + Go Back + 戻る + + + + Alt+Left + Go Back + Alt+Left + + + + Go Forward + 進む + + + + Alt+Right + Go Forward + Alt+Right + + + + Reload + リロード + + + + F5 + Reload + F5 + + + + Create Folder + フォルダの作成 + + + + Icon View + アイコンの表示 + + + + Thumbnail View + サムネールの表示 + + + + Compact View + 概略の表示 + + + + Detailed List View + 詳細なリストの表示 + + + + + Error + エラー + + + + Please select a file + ファイルを1つ選択してください + + + + %1 already exists. +Do you want to replace it? + %1は既にあります。 +これを置き換えますか? + + + + Path "%1" does not exist + パス"%1"はありません + + + + "%1" is not a directory + "%1"はディレクトリではありません + + + + "%1" is not a file + "%1"はファイルではありません + + + + + &Open + 開く(&O) + + + + + &Save + 保存(&S) + + + + All Files (*) + すべてのファイル(*) + + + + Fm::FileDialogHelper + + + Open File + ファイルを開く + + + + Save File + ファイルを保存 + + + + Fm::FileMenu + + + Open + 開く + + + + Cut + 切り取り + + + + Copy + コピー + + + + Paste + 貼り付け + + + + + &Move to Trash + ゴミ箱へ移動(&M) + + + + Trust selected executables + + + + + Trust this executable + + + + + Output + 出力 + + + + &Delete + 削除(&D) + + + + Rename + 名前を変更する + + + + Open With... + アプリケーションで開く... + + + + Other Applications + その他のアプリケーション + + + + Create &New + 新規作成 (&N) + + + + &Restore + 復活(&R) + + + + Extract to... + 展開する... + + + + Extract Here + ここへ展開する + + + + Compress + 圧縮する + + + + Properties + プロパティー + + + + Fm::FileOperation + + + Error + エラー + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + ファイルシステムのサポートがないため、ゴミ箱へ移動できないファイルがあります。 +かわりにこれらを削除しますか? + + + + + Confirm + 確認 + + + + Do you want to delete the selected files? + 選択したファイルを削除しますか? + + + + Do you want to move the selected files to trash can? + 選択したファイルをゴミ箱に移しますか? + + + + Fm::FileOperationDialog + + + Move files + ファイルを移動 + + + + Moving the following files to destination folder: + 次のファイルを対象のフォルダーへ移動: + + + + Copy Files + ファイルをコピー + + + + Copying the following files to destination folder: + 次のファイルを対象のフォルダーへコピー: + + + + Trash Files + ファイルをゴミ箱へ入れる + + + + Moving the following files to trash can: + 次のファイルをゴミ箱へ入れる: + + + + Delete Files + ファイルを削除 + + + + Deleting the following files: + 以下のファイルを削除: + + + + Create Symlinks + シンボリックリンク作成 + + + + Creating symlinks for the following files: + 以下のファイルのシンボリックリンクを作成: + + + + Change Attributes + 属性を変更 + + + + Changing attributes of the following files: + 以下のファイルの属性を変更: + + + + Restore Trashed Files + ゴミ箱から戻す + + + + Restoring the following files from trash can: + 以下のファイルをゴミ箱から戻す: + + + + + Error + エラー + + + + Fm::FilePropsDialog + + + View folder content + フォルダーの内容を表示 + + + + View and modify folder content + フォルダーの内容を表示・変更 + + + + Read + 読取り + + + + Read and write + 読取りおよび書込み + + + + Forbidden + 禁止 + + + + Files of different types + 異なる種類のファイル + + + + Multiple Files + 複数のファイル + + + + %p% used + + + + + %1 Free of %2 + + + + + no file + ファイルがありません + + + + one file + 1つのファイル + + + + %1 files + %1 のファイル + + + + Select an icon + アイコンを選択 + + + + Images (*.png *.xpm *.svg *.svgz ) + 画像 (*.png *.xpm *.svg *.svgz ) + + + + Apply changes + 変更を適用 + + + + Do you want to recursively apply these changes to all files and sub-folders? + 変更をすべてのファイルとサブフォルダーにも再帰的に適用しますか? + + + + Fm::FileSearchDialog + + + Error + エラー + + + + You should add at least one directory to search. + 検索するディレクトリを追加してください。 + + + + Select a folder + フォルダーの選択 + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + + + + + Fm::FolderMenu + + + Create &New + 新規作成 (&N) + + + + &Paste + ペースト(&P) + + + + Select &All + すべてを選択(&A) + + + + Invert Selection + 選択を反転 + + + + Sorting + ソート + + + + Show Hidden + 隠しファイルの表示 + + + + Folder Pr&operties + フォルダのプロパティー(&O) + + + + Output + 出力 + + + + By File Name + 名前 + + + + By Modification Time + 更新時刻 + + + + By File Size + サイズ + + + + By File Type + 種類 + + + + By File Owner + 所有者 + + + + Ascending + 昇順 + + + + Descending + 降順 + + + + Folder First + フォルダーを先に + + + + Case Sensitive + 大文字小文字を区別 + + + + Fm::FolderModel + + + Name + 名前 + + + + Type + 種類 + + + + Size + サイズ + + + + Modified + 更新日時 + + + + Owner + 所有者 + + + + Group + グループ + + + + Fm::FontButton + + + Bold + 太字 + + + + Italic + 斜体 + + + + Fm::MountOperationPasswordDialog + + + &Connect + 接続&(C) + + + + Fm::PathBar + + + &Edit Path + パスの編集(&E) + + + + &Copy Path + パスのコピー(&C) + + + + Fm::PlacesModel + + + Places + 場所 + + + + Desktop + デスクトップ + + + + Trash + ゴミ箱 + + + + Computer + コンピューター + + + + Applications + アプリケーション + + + + Network + ネットワーク + + + + Devices + デバイス + + + + Bookmarks + ブックマーク + + + + Fm::PlacesView + + + Empty Trash + ゴミ箱を空にする + + + + Open in New Tab + 新しいタブで開く + + + + Open in New Window + 新しいウィンドウで開く + + + + + Hide + + + + + Move Bookmark Up + ブックマークを上に移動 + + + + Move Bookmark Down + ブックマークを下に移動 + + + + Rename Bookmark + ブックマークの名前を変更 + + + + Remove Bookmark + ブックマークを削除 + + + + + Unmount + アンマウント + + + + Mount + マウント + + + + Eject + 取出し + + + + Show All Entries + + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + 種類: %1 +サイズ: %2 +更新日時: %3 + + + + + Type: %1 +Modified: %2 + 種類: %1 +更新日時: %2 + + + + &Overwrite + 上書き(&O) + + + + &Rename + 名前を変更(&R) + + + + Fm::SidePane + + + Places + 場所 + + + + Directory Tree + ディレクトリーツリー + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + + + + + MountOperationPasswordDialog + + + Mount + マウント + + + + Connect &anonymously + 匿名で接続(&A) + + + + Connect as u&ser: + ユーザーとして接続(&U): + + + + &Username: + ユーザー名(&U): + + + + &Password: + &パスワード(&P): + + + + &Domain: + &ドメイン(&D): + + + + Forget password &immediately + パスワードを記憶させない(&i) + + + + Remember password until you &logout + パスワードをログアウトするまで記憶させる(&l) + + + + Remember &forever + パスワードを恒久的に記憶させる(&f) + + + + QObject + + + + + + + Error + エラー + + + + Rename File + ファイル名を変更 + + + + Please enter a new name: + 新しい名前を入力してください: + + + + Create Folder + フォルダの作成 + + + + Please enter a new file name: + 新規ファイルの名前を入力してください: + + + + New text file + 新規テキストファイル + + + + Please enter a new folder name: + 新規フォルダの名前を入力してください: + + + + New folder + 新規フォルダー + + + + Enter a name for the new %1: + 新しい %1 の名前を入力してください: + + + + Create File + ファイル作成 + + + + Custom Icon Error + カスタムアイコンのエラー + + + + The path is not mounted. + このパスはマウントされていません + + + + Invalid desktop entry file: '%1' + + + + + No default application is set to launch '%1' + + + + + Cannot set working directory to '%1': %2 + + + + + Identifier: + + + + + RenameDialog + + + Confirm to replace files + ファイルの置換えを確認 + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + <html><head/><body><p><span style=" font-weight:600;">この場所にはすでに、同じ名前のファイルがあります。</span></p><p>既存のファイルを置き換えますか?</p></body></html> + + + + dest + 送り先 + + + + with the following file? + 以下のファイルと置き換えますか? + + + + src file info + + + + + dest file info + 送り先のファイルの情報 + + + + src + 送り元のファイルの情報 + + + + &File name: + ファイル名(&F): + + + + Apply this option to all existing files + 既存のすべてのファイルにも適用する + + + + SearchDialog + + + Search Files + ファイルを検索 + + + + Name/Location + 名前/場所 + + + + File Name Patterns: + ファイル名のパターン: + + + + * + * + + + + Case insensitive + 大文字と小文字を区別しない + + + + Use regular expression + 正規表現を使用する + + + + Places to Search: + 検索の場所: + + + + &Add + 追加(&A) + + + + &Remove + 削除(&R) + + + + Search in sub directories + サブディレクトリを検索する + + + + Search for hidden files + 隠しファイルを検索する + + + + File Type + ファイルの種類 + + + + Only search for files of following types: + 次の種類のファイルのみを検索: + + + + Text files + テキストファイル + + + + Image files + 画像ファイル + + + + Audio files + 音声ファイル + + + + Video files + 動画ファイル + + + + Documents + 文書 + + + + Folders + フォルダ + + + + Content + 内容 + + + + File contains: + ファイルを含む: + + + + Case insensiti&ve + 大文字小文字を区別しない(&v) + + + + &Use regular expression + 正規表現を使う(&U) + + + + Properties + プロパティ + + + + File Size: + ファイルのサイズ: + + + + Larger than: + より大きい: + + + + + Bytes + Bytes + + + + + KiB + + + + + + MiB + + + + + + GiB + + + + + Smaller than: + より小さい: + + + + Last Modified Time: + 更新日時: + + + + Earlier than: + より早い: + + + + Later than: + より遅い: + + + diff --git a/src/translations/libfm-qt_lt.ts b/src/translations/libfm-qt_lt.ts new file mode 100644 index 0000000..79cf773 --- /dev/null +++ b/src/translations/libfm-qt_lt.ts @@ -0,0 +1,1560 @@ + + + + + AppChooserDialog + + + Choose an Application + Pasirinkite programą + + + + Installed Applications + Ä®diegtos programos + + + + Custom Command + Tinkinta komanda + + + + Command line to execute: + Komandų eilutė, kurią vykdyti: + + + + Application name: + Programos pavadinimas: + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + <b>Komandų eilutėje gali bÅ«ti naudojami Å¡ie specialÅ«s kodai:</b> +<ul> +<li><b>%f</b>: Atvaizduoja vieno failo pavadinimą</li> +<li><b>%F</b>: Atvaizduoja kelių failų pavadinimus</li> +<li><b>%u</b>: Atvaizduoja vieną failo URI</li> +<li><b>%U</b>: Atvaizduoja kelis URI</li> +</ul> + + + + Keep terminal window open after command execution + Po komandos įvykdymo, palikti terminalo langą atvertą + + + + Execute in terminal emulator + Vykdyti terminalo emuliatoriuje + + + + Set selected application as default action of this file type + Nustatyti pasirinktą programą kaip numatytąjį veiksmą Å¡iam failo tipui + + + + EditBookmarksDialog + + + Edit Bookmarks + Redaguoti žymeles + + + + Name + Pavadinimas + + + + Location + Vieta + + + + &Add Item + &Pridėti elementą + + + + &Remove Item + Å a&linti elementą + + + + Use drag and drop to reorder the items + Vilkite, norėdami pakeisti elementų tvarką + + + + ExecFileDialog + + + Execute file + Vykdyti failą + + + + &Open + &Atverti + + + + E&xecute + &Vykdyti + + + + Execute in &Terminal + Vykdyti &terminale + + + + Cancel + Atsisakyti + + + + FileDialog + + + Location: + Vieta: + + + + File name: + Failo pavadinimas: + + + + File type: + Failo tipas: + + + + FileOperationDialog + + + Destination: + Paskirties vieta: + + + + Processing: + Vykdoma: + + + + Preparing... + RuoÅ¡iama... + + + + Progress + Eiga + + + + Time remaining: + Liko laiko: + + + + Files processed: + Apdorota failų: + + + + FilePropsDialog + + + File Properties + Failo savybės + + + + General + Bendra + + + + Location: + Vieta: + + + + File type: + Failo tipas: + + + + MIME type: + MIME tipas: + + + + File size: + Failo dydis: + + + + On-disk size: + Dydis diske: + + + + Last modified: + Paskutinis pakeitimas: + + + + Link target: + Nuorodos paskirtis: + + + + Open With: + Atverti naudojant: + + + + Last accessed: + Paskutinė prieiga: + + + + Contains: + Turi: + + + + Device Usage: + Ä®renginio naudojimas: + + + + Permissions + Leidimai + + + + Ownership + Nuosavybė + + + + + + Group: + Grupė: + + + + + + Owner: + Savininkas: + + + + Access Control + Prieigos valdymas + + + + + Other: + Kiti: + + + + Make the file executable + Padaryti failą vykdomuoju + + + + + + Read + Skaityti + + + + + + Write + RaÅ¡yti + + + + + + Execute + Vykdyti + + + + Sticky + Lipnus + + + + SetUID + Nustatyti UID + + + + SetGID + Nustatyti GID + + + + Advanced Mode + IÅ¡plėstinė veiksena + + + + Fm::AppChooserComboBox + + + Customize + Tinkinti + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + Pasirinkite programą, kad atvertumėte failus "%1" + + + + Fm::CreateNewMenu + + + Folder + Aplanką + + + + Blank File + Tuščią failą + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + Nurodytas katalogas "%1" yra neteisingas + + + + Fm::DirTreeModel + + + Loading... + Ä®keliama... + + + + + + <No sub folders> + <Nėra poaplankių> + + + + Fm::DirTreeView + + + Open in New T&ab + Atverti naujoje kort&elėje + + + + Open in New Win&dow + Atverti naujame lan&ge + + + + Open in Termina&l + Atverti termina&le + + + + Fm::DndActionMenu + + + Copy here + Kopijuoti čia + + + + Move here + Perkelti čia + + + + Create symlink here + Sukurti simbolinę nuorodą čia + + + + Cancel + Atsisakyti + + + + Fm::EditBookmarksDialog + + + New bookmark + Nauja žymelė + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + Atrodo, kad Å¡is failas "%1" yra darbalaukio įraÅ¡as. +Ką norėtumėte su juo daryti? + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + Atrodo, kad Å¡is tekstinis failas "%1" yra vykdomasis scenarijus. +Ką norėtumėte su juo daryti? + + + + This file '%1' is executable. Do you want to execute it? + Å is failas "%1" yra vykdomasis. Ar norite jį vykdyti? + + + + Fm::FileDialog + + + Go Back + Grįžti + + + + Alt+Left + Go Back + Alt+Kairėn + + + + Go Forward + Pirmyn + + + + Alt+Right + Go Forward + Alt+DeÅ¡inėn + + + + Reload + Ä®kelti iÅ¡ naujo + + + + F5 + Reload + F5 + + + + Create Folder + Sukurti aplanką + + + + Icon View + Piktogramų rodinys + + + + Thumbnail View + MiniatiÅ«rų rodinys + + + + Compact View + Glaustas rodinys + + + + Detailed List View + IÅ¡samaus sąraÅ¡o rodinys + + + + + Error + Klaida + + + + Please select a file + Pasirinkite failą + + + + %1 already exists. +Do you want to replace it? + %1 jau yra. +Ar norite jį pakeisti? + + + + Path "%1" does not exist + Kelio "%1" nėra + + + + "%1" is not a directory + "%1" nėra katalogas + + + + "%1" is not a file + "%1" nėra failas + + + + + &Open + &Atverti + + + + + &Save + Ä®&raÅ¡yti + + + + All Files (*) + Visi failai (*) + + + + Fm::FileDialogHelper + + + Open File + Atverti failą + + + + Save File + Ä®raÅ¡yti failą + + + + Fm::FileMenu + + + Open + Atverti + + + + Open With... + Atverti naudojant... + + + + Other Applications + Kitas programas + + + + Create &New + Sukurti &naują + + + + &Restore + &Atkurti + + + + Cut + IÅ¡kirpti + + + + Copy + Kopijuoti + + + + Paste + Ä®dėti + + + + + &Move to Trash + &Perkelti į Å¡iukÅ¡linę + + + + Rename + Pervadinti + + + + Extract to... + IÅ¡skleisti į... + + + + Extract Here + IÅ¡skleisti čia + + + + Compress + Glaudinti + + + + Properties + Savybės + + + + Trust selected executables + Pasitikėti pažymėtais vykdomaisiais + + + + Trust this executable + Pasitikėti Å¡iuo vykdomuoju + + + + Output + IÅ¡vestis + + + + &Delete + &IÅ¡trinti + + + + Fm::FileOperation + + + Error + Klaida + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + Kai kurie failai negali bÅ«ti perkelti į Å¡iukÅ¡linę, kadangi esama failų sistema nepalaiko Å¡ios operacijos. +Ar norite vietoj to, juos iÅ¡trinti? + + + + + Confirm + Patvirtinti + + + + Do you want to delete the selected files? + Ar norite iÅ¡trinti pažymėtus failus? + + + + Do you want to move the selected files to trash can? + Ar norite perkelti pažymėtus failus į Å¡iukÅ¡linę? + + + + Fm::FileOperationDialog + + + Move files + Perkelti failus + + + + Moving the following files to destination folder: + Å ie failai perkeliami į paskirties aplanką: + + + + Copy Files + Kopijuoti failus + + + + Copying the following files to destination folder: + Å ie failai kopijuojami į paskirties aplanką: + + + + Trash Files + Perkelti failus į Å¡iukÅ¡linę + + + + Moving the following files to trash can: + Å ie failai perkeliami į Å¡iukÅ¡linę: + + + + Delete Files + IÅ¡trinti failus + + + + Deleting the following files: + IÅ¡trinami Å¡ie failai: + + + + Create Symlinks + Sukurti simbolines nuorodas + + + + Creating symlinks for the following files: + Å iems failams kuriamos simbolinės nuorodos: + + + + Change Attributes + Keisti atributus + + + + Changing attributes of the following files: + Keičiami Å¡ių failų atributai: + + + + Restore Trashed Files + Atkurti į Å¡iukÅ¡linę perkeltus failus + + + + Restoring the following files from trash can: + Å ie failai atkuriami iÅ¡ Å¡iukÅ¡linės: + + + + + Error + Klaida + + + + Fm::FilePropsDialog + + + View folder content + ŽiÅ«rėti aplanko turinį + + + + View and modify folder content + ŽiÅ«rėti ir keisti aplanko turinį + + + + Read + Skaityti + + + + Read and write + Skaityti ir raÅ¡yti + + + + Forbidden + Uždrausta + + + + Files of different types + Ä®vairių tipų failai + + + + Multiple Files + Keli failai + + + + %p% used + Panaudota %p% + + + + %1 Free of %2 + Laisva %1 iÅ¡ %2 + + + + no file + jokių failų + + + + one file + vienas failas + + + + %1 files + %1 failų(-ai) + + + + Select an icon + Pasirinkti piktogramą + + + + Images (*.png *.xpm *.svg *.svgz ) + Paveikslai (*.png *.xpm *.svg *.svgz ) + + + + Apply changes + Taikyti pakeitimus + + + + Do you want to recursively apply these changes to all files and sub-folders? + Ar norite rekursyviai taikyti Å¡iuos pakeitimus visiems failams ir poaplankiams? + + + + Fm::FileSearchDialog + + + Error + Klaida + + + + You should add at least one directory to search. + Norėdami ieÅ¡koti, turite pasirinkti bent vieną katalogą. + + + + Select a folder + Pasirinkti aplanką + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + Nepavyksta sukurti nuorodą ne savoje failų sistemoje + + + + Fm::FolderMenu + + + Create &New + Sukurti &naują + + + + &Paste + Ä®&dėti + + + + Select &All + &Pažymėti viską + + + + Invert Selection + Invertuoti žymėjimą + + + + Sorting + Rikiavimas + + + + Show Hidden + Rodyti paslėptus + + + + Folder Pr&operties + Aplanko &savybės + + + + Output + IÅ¡vestis + + + + By File Name + Pagal failo pavadinimą + + + + By Modification Time + Pagal pakeitimo laiką + + + + By File Size + Pagal failo dydį + + + + By File Type + Pagal failo tipą + + + + By File Owner + Pagal failo savininką + + + + Ascending + Didėjančiai + + + + Descending + Mažėjančiai + + + + Folder First + Pirmiausia aplankai + + + + Case Sensitive + Skirti raidžių dydį + + + + Fm::FolderModel + + + Name + Pavadinimas + + + + Type + Tipas + + + + Size + Dydis + + + + Modified + Pakeista + + + + Owner + Savininkas + + + + Group + Grupė + + + + Fm::FontButton + + + Bold + Pusjuodis + + + + Italic + Kursyvas + + + + Fm::MountOperationPasswordDialog + + + &Connect + &Prijungti + + + + Fm::PathBar + + + &Edit Path + &Taisyti kelią + + + + &Copy Path + &Kopijuoti kelią + + + + Fm::PlacesModel + + + Places + Vietos + + + + Desktop + Darbalaukis + + + + Computer + Kompiuteris + + + + Applications + Programos + + + + Network + Tinklas + + + + Devices + Ä®renginiai + + + + Bookmarks + Žymelės + + + + Trash + Å iukÅ¡linė + + + + Fm::PlacesView + + + Open in New Tab + Atverti naujoje kortelėje + + + + Open in New Window + Atverti naujame lange + + + + Empty Trash + IÅ¡valyti Å¡iukÅ¡linę + + + + + Hide + Slėpti + + + + Move Bookmark Up + Pakelti žymelę + + + + Move Bookmark Down + Nuleisti žymelę + + + + Rename Bookmark + Pervadinti žymelę + + + + Remove Bookmark + Å alinti žymelę + + + + + Unmount + Atjungti + + + + Mount + Prijungti + + + + Eject + IÅ¡stumti + + + + Show All Entries + Rodyti visus įraÅ¡us + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + Tipas: %1 +Dydis: %2 +Pakeista: %3 + + + + + Type: %1 +Modified: %2 + Tipas: %1 +Pakeista: %2 + + + + &Overwrite + PerraÅ¡&yti + + + + &Rename + Pe&rvadinti + + + + Fm::SidePane + + + Places + Vietos + + + + Directory Tree + Katalogų medis + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + Nepavyksta iÅ¡kelti failą "%s" iÅ¡ Å¡iukÅ¡linės: pradinis kelias nėra žinomas + + + + MountOperationPasswordDialog + + + Mount + Prijungti + + + + Connect &anonymously + Prisijungti &anonimiÅ¡kai + + + + Connect as u&ser: + Prisijungti kaip naudotoja&s: + + + + &Username: + Na&udotojo vardas: + + + + &Password: + Sla&ptažodis: + + + + &Domain: + &Sritis: + + + + Forget password &immediately + Nedels&iant užmirÅ¡ti slaptažodį + + + + Remember password until you &logout + Prisiminti slaptažodį tol, kol atsijungsi&te + + + + Remember &forever + Prisiminti visam laik&ui + + + + QObject + + + Rename File + Pervadinti failą + + + + Please enter a new name: + Ä®raÅ¡ykite naują pavadinimą: + + + + + + + + Error + Klaida + + + + Create Folder + Sukurti aplanką + + + + Create File + Sukurti failą + + + + Please enter a new file name: + Ä®raÅ¡ykite naują failo pavadinimą: + + + + New text file + Naujas tekstinis failas + + + + Please enter a new folder name: + Ä®raÅ¡ykite naują aplanko pavadinimą: + + + + New folder + Naujas aplankas + + + + Enter a name for the new %1: + Ä®raÅ¡ykite pavadinimą naujam %1: + + + + Custom Icon Error + Tinkintos piktogramos klaida + + + + The path is not mounted. + Kelias nėra prijungtas. + + + + Invalid desktop entry file: '%1' + Neteisingas darbalaukio įraÅ¡o failas: "%1" + + + + No default application is set to launch '%1' + Nėra nustatyta numatytoji programa, skirta paleisti "%1" + + + + Cannot set working directory to '%1': %2 + Nepavyksta nustatyti darbinį katalogą į "%1": %2 + + + + Identifier: + Identifikatorius: + + + + RenameDialog + + + Confirm to replace files + Patvirtinti failų pakeitimą + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + <html><head/><body><p><span style=" font-weight:600;">Å ioje vietoje jau yra failas tokiu pačiu pavadinimu.</span></p><p>Ar norite pakeisti esamą failą?</p></body></html> + + + + dest + paskirtis + + + + with the following file? + Å¡iuo failu? + + + + src file info + Å¡altinio failo informacija + + + + dest file info + paskirties failo informacija + + + + src + Å¡altinis + + + + &File name: + &Failo pavadinimas: + + + + Apply this option to all existing files + Taikyti Å¡ią parinktį visiems esamiems failams + + + + SearchDialog + + + Search Files + IeÅ¡koti failų + + + + Name/Location + Pavadinimas/Vieta + + + + File Name Patterns: + Failo pavadinimo Å¡ablonai: + + + + * + * + + + + Case insensitive + Neskirti didžiųjų ir mažųjų raidžių + + + + Use regular expression + Naudoti reguliarųjį reiÅ¡kinį + + + + Places to Search: + Vietos, kuriose ieÅ¡koti: + + + + &Add + &Pridėti + + + + &Remove + Å a&linti + + + + Search in sub directories + IeÅ¡koti pakatalogiuose + + + + Search for hidden files + IeÅ¡koti paslėptų failų + + + + File Type + Failo tipas + + + + Only search for files of following types: + IeÅ¡koti tik Å¡ių tipų failų: + + + + Text files + Tekstiniai failai + + + + Image files + Paveikslų failai + + + + Audio files + Garso failai + + + + Video files + Vaizdo failai + + + + Documents + Dokumentai + + + + Folders + Aplankai + + + + Content + Turinys + + + + File contains: + Faile yra: + + + + Case insensiti&ve + Neski&rti didžiųjų ir mažųjų raidžių + + + + &Use regular expression + Na&udoti reguliarųjį reiÅ¡kinį + + + + Properties + Savybės + + + + File Size: + Failo dydis: + + + + Larger than: + Didesnis nei: + + + + + Bytes + Baitų + + + + + KiB + KiB + + + + + MiB + MiB + + + + + GiB + GiB + + + + Smaller than: + Mažesnis nei: + + + + Last Modified Time: + Paskutinio pakeitimo laikas: + + + + Earlier than: + Anksčiau nei: + + + + Later than: + Vėliau nei: + + + diff --git a/src/translations/libfm-qt_nb_NO.ts b/src/translations/libfm-qt_nb_NO.ts new file mode 100644 index 0000000..184cad2 --- /dev/null +++ b/src/translations/libfm-qt_nb_NO.ts @@ -0,0 +1,1560 @@ + + + + + AppChooserDialog + + + Choose an Application + Velg et program + + + + Installed Applications + Installerte programmer + + + + Custom Command + Selvvalgt kommando + + + + Command line to execute: + Kommandolinje Ã¥ kjøre: + + + + Application name: + Programnavn: + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + <b>Disse spesielle kodene kan brukes i en kommandolinje:</b> +<ul> +<li><b>%f</b>: Representerer et enkelt filnavn</li> +<li><b>%F</b>: Representerer flere filnavn</li> +<li><b>%u</b>: Representerer en enkelt URI for filen</li> +<li><b>%U</b>: Representerer flere URIer</li> +</ul> + + + + Keep terminal window open after command execution + Hold terminalvinduet Ã¥pent etter Ã¥ ha kjørt kommandoen + + + + Execute in terminal emulator + Kjør i terminalemulator + + + + Set selected application as default action of this file type + Bruk valgte program som standardprogram for denne filtypen + + + + EditBookmarksDialog + + + Edit Bookmarks + Rediger bokmerker + + + + Name + Navn + + + + Location + Sted + + + + &Add Item + &Legg til + + + + &Remove Item + &Fjern + + + + Use drag and drop to reorder the items + Bruk dra og slipp for Ã¥ forandre rekkefølge + + + + ExecFileDialog + + + Execute file + Kjør fil + + + + &Open + + + + + E&xecute + K&jør + + + + Execute in &Terminal + Kjør i ter&minal + + + + Cancel + Avbryt + + + + FileDialog + + + Location: + Sted: + + + + File name: + Filnavn: + + + + File type: + Filtype: + + + + FileOperationDialog + + + Destination: + MÃ¥l: + + + + Processing: + Behandler: + + + + Preparing... + Forbereder... + + + + Progress + Framdrift + + + + Time remaining: + Tid igjen: + + + + Files processed: + Filer behandlet: + + + + FilePropsDialog + + + File Properties + Filegenskaper + + + + General + Generelt + + + + Location: + Plassering: + + + + File type: + Filtype: + + + + MIME type: + MIME-type: + + + + File size: + Filstørrelse: + + + + On-disk size: + Størrelse pÃ¥ disken: + + + + Last modified: + Sist endret: + + + + Link target: + Lenke til: + + + + Open With: + Åpne med: + + + + Last accessed: + Sist brukt: + + + + Contains: + Inneholder: + + + + Device Usage: + Brukt pÃ¥ enheten: + + + + Permissions + Tillatelser + + + + Ownership + Eierskap + + + + + + Group: + Gruppe: + + + + + + Owner: + Eier: + + + + Access Control + Tilgangskontroll + + + + + Other: + Annet: + + + + Make the file executable + Gjør filen kjørbar + + + + + + Read + Les + + + + + + Write + Skriv + + + + + + Execute + Kjør + + + + Sticky + Blivende + + + + SetUID + + + + + SetGID + + + + + Advanced Mode + Avansert modus + + + + Fm::AppChooserComboBox + + + Customize + Tilpassing + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + Velg et program for Ã¥ Ã¥pne "%1"-filer + + + + Fm::CreateNewMenu + + + Folder + Mappe + + + + Blank File + Tom fil + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + Den valgte mappa '%1' er ikke gyldig + + + + Fm::DirTreeModel + + + Loading... + Jobber... + + + + + + <No sub folders> + <Ingen undermapper> + + + + Fm::DirTreeView + + + Open in New T&ab + Åpne i ny fane + + + + Open in New Win&dow + Åpne i nytt vindu + + + + Open in Termina&l + Åpne i terminal + + + + Fm::DndActionMenu + + + Copy here + Kopier hit + + + + Move here + Flytt hit + + + + Create symlink here + Lag symbolsk lenke hit + + + + Cancel + Avbryt + + + + Fm::EditBookmarksDialog + + + New bookmark + Nytt bokmerke + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + Filen '%1' ser ut til Ã¥ være en skrivebordsfil. +Hva vil du gjøre med den? + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + Tekstfilen '%1' ser ut til Ã¥ være et kjørbart script. +Hva vil du gjøre med det? + + + + This file '%1' is executable. Do you want to execute it? + File '%1' er kjørbar. Hva vil du gjøre med den? + + + + Fm::FileDialog + + + Go Back + GÃ¥ tilbake + + + + Alt+Left + Go Back + Alt+Venstre + + + + Go Forward + GÃ¥ forover + + + + Alt+Right + Go Forward + Alt+Høyre + + + + Reload + Last pÃ¥ nytt + + + + F5 + Reload + + + + + Create Folder + Lag mappe + + + + Icon View + Symbolvisning + + + + Thumbnail View + ForhÃ¥ndsvisningsvisning + + + + Compact View + Kompaktvisning + + + + Detailed List View + Detaljert listevisning + + + + + Error + Feil + + + + Please select a file + Velg en fil + + + + %1 already exists. +Do you want to replace it? + %1 finnes allerede. +Vil du bytte den ut? + + + + Path "%1" does not exist + Stien "%1" finnes ikke + + + + "%1" is not a directory + "%1" er ikke en mappe + + + + "%1" is not a file + "%1" er ikke en fil + + + + + &Open + &Åpne + + + + + &Save + &Lagre + + + + All Files (*) + Alle filer (*) + + + + Fm::FileDialogHelper + + + Open File + Åpne fil + + + + Save File + Lagre fil + + + + Fm::FileMenu + + + Open + Åpne + + + + Open With... + Åpne med... + + + + Other Applications + Andre programmer + + + + Create &New + Lag &Ny + + + + &Restore + &Gjenopprett + + + + Cut + Klipp ut + + + + Copy + Kopier + + + + Paste + Lim inn + + + + + &Move to Trash + &Flytt til søppeldunken + + + + Rename + Gi nytt navn + + + + Extract to... + Pakk ut til... + + + + Extract Here + Pakk ut her + + + + Compress + Pakk sammen + + + + Properties + Egenskaper + + + + Trust selected executables + Stol pÃ¥ valgte programmer + + + + Trust this executable + Stol pÃ¥ dette programmet + + + + Output + Utverdi + + + + &Delete + &Slett + + + + Fm::FileOperation + + + Error + Feil + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + Noen filer kan ikke bli flyttet til søppeldunken fordi det underliggende filsystemet ikke støtter denne operasjonen. +Vil du slette dem i stedet? + + + + + Confirm + Bekreft + + + + Do you want to delete the selected files? + Vil du slette de valgte filene? + + + + Do you want to move the selected files to trash can? + Vil du flytte de valgte filene til søppeldunken? + + + + Fm::FileOperationDialog + + + Move files + Flytt filer + + + + Moving the following files to destination folder: + Flytter følgende filer til valgt mappe: + + + + Copy Files + Kopier filer + + + + Copying the following files to destination folder: + Kopierer følgende filer til valgt mappe: + + + + Trash Files + Søppeldunkfiler + + + + Moving the following files to trash can: + Flytter følgende filer til søppeldunken: + + + + Delete Files + Slett filer + + + + Deleting the following files: + Sletter følgende filer: + + + + Create Symlinks + Skap symbolsk lenke + + + + Creating symlinks for the following files: + Skaper symbolske lenker for følgende filer: + + + + Change Attributes + Forandr egenskaper + + + + Changing attributes of the following files: + Forandrer egenskaper for følgende filer: + + + + Restore Trashed Files + Gjenskapte kastede filer + + + + Restoring the following files from trash can: + Gjenskaper følgende filer fra søppeldunken: + + + + + Error + Feil + + + + Fm::FilePropsDialog + + + View folder content + Se mappeinnhold + + + + View and modify folder content + Se og forandr mappeinnhold + + + + Read + Les + + + + Read and write + Les og skriv + + + + Forbidden + Forbudt + + + + Files of different types + Filer av ulike typer + + + + Multiple Files + Flere filer + + + + %p% used + %p% brukt + + + + %1 Free of %2 + %1 ledig pÃ¥ %2 + + + + no file + ingen fil + + + + one file + en fil + + + + %1 files + %1 filer + + + + Select an icon + Velg et symbol + + + + Images (*.png *.xpm *.svg *.svgz ) + bilder (*.png *.xpm *.svg *.svgz ) + + + + Apply changes + Utfør endringer + + + + Do you want to recursively apply these changes to all files and sub-folders? + Vil du utføre disse endringene for alle filer og undermapper? + + + + Fm::FileSearchDialog + + + Error + Feil + + + + You should add at least one directory to search. + Du bør legge til minst en mappe Ã¥ søke. + + + + Select a folder + Velg en mappe + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + Kan ikke lage en lenke pÃ¥ et fremmed filsystem + + + + Fm::FolderMenu + + + Create &New + Skap &Ny + + + + &Paste + &Lim inn + + + + Select &All + Velg &alt + + + + Invert Selection + Inverterter valg + + + + Sorting + Sortering + + + + Show Hidden + Vis skjulte + + + + Folder Pr&operties + Mappe-&egenskaper + + + + Output + Utverdi + + + + By File Name + Etter filnavn + + + + By Modification Time + Etter sist endret + + + + By File Size + Etter størrelse + + + + By File Type + Etter type + + + + By File Owner + Etter eier + + + + Ascending + Økende + + + + Descending + Minskende + + + + Folder First + Mapper først + + + + Case Sensitive + Forskjell pÃ¥ STORE og smÃ¥ bokstaver + + + + Fm::FolderModel + + + Name + Navn + + + + Type + + + + + Size + Størrelse + + + + Modified + Sist endret + + + + Owner + Eier + + + + Group + Gruppe + + + + Fm::FontButton + + + Bold + Fet + + + + Italic + Kursiv + + + + Fm::MountOperationPasswordDialog + + + &Connect + &Koble til + + + + Fm::PathBar + + + &Edit Path + &Forandr sti + + + + &Copy Path + &Kopier sti + + + + Fm::PlacesModel + + + Places + Steder + + + + Desktop + Skrivebord + + + + Computer + Datamaskin + + + + Applications + Programmer + + + + Network + Nettverk + + + + Devices + Dingser + + + + Bookmarks + Bokmerker + + + + Trash + Søppeldunk + + + + Fm::PlacesView + + + Open in New Tab + Åpne i ny fane + + + + Open in New Window + Åpne i nytt vindu + + + + Empty Trash + Tøm søppeldunk + + + + + Hide + Skjul + + + + Move Bookmark Up + Flytt bokmerke oppover + + + + Move Bookmark Down + Flytt bokmerke nedover + + + + Rename Bookmark + Gi nytt navn til bokmerke + + + + Remove Bookmark + Fjern bokmerke + + + + + Unmount + Løs ut + + + + Mount + Last inn + + + + Eject + Spytt ut + + + + Show All Entries + Vis alle + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + Type: %1 +Størrelse %2 +Sist endret: %3 + + + + + Type: %1 +Modified: %2 + Type: %1 +Sist endret: %2 + + + + &Overwrite + &Skriv over + + + + &Rename + &Gi nytt navn + + + + Fm::SidePane + + + Places + Steder + + + + Directory Tree + Filtre + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + Kan ikke gjenopprette filen '%s': original plassering ukjent + + + + MountOperationPasswordDialog + + + Mount + Last inn + + + + Connect &anonymously + Koble til &anonymt + + + + Connect as u&ser: + Koble til som &bruker: + + + + &Username: + &Brukernavn: + + + + &Password: + &Passord: + + + + &Domain: + &Domene: + + + + Forget password &immediately + Glem passord &umiddelbart + + + + Remember password until you &logout + Husk passord til du &logger ut + + + + Remember &forever + Husk &alltid + + + + QObject + + + Rename File + Gi nytt filnavn + + + + Please enter a new name: + Skriv inn nytt navn: + + + + + + + + Error + Feil + + + + Create Folder + Lag mappe + + + + Create File + Lag fil + + + + Please enter a new file name: + Skriv inn et nytt filnavn: + + + + New text file + Ny tekstfil + + + + Please enter a new folder name: + Skriv inn et nytt mappenavn: + + + + New folder + Ny mappe + + + + Enter a name for the new %1: + Skriv inn et nytt navn for den nye %1: + + + + Custom Icon Error + Feil med selvvalgt symbol + + + + The path is not mounted. + Stien er ikke lastet inn. + + + + Invalid desktop entry file: '%1' + Ugyldig skrivebordsfil: '%1' + + + + No default application is set to launch '%1' + Ingen program er satt til Ã¥ Ã¥pne '%1' + + + + Cannot set working directory to '%1': %2 + Kan ikke sette arbeidsmappen til '%1': %2 + + + + Identifier: + Identifikasjon: + + + + RenameDialog + + + Confirm to replace files + Bekreft for Ã¥ bytte filer + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + <html><head/><body><p><span style=" font-weight:600;">Det er allerede en fil med samme navn pÃ¥ dette stedet.</span></p><p>Ønsker du Ã¥ bytte ut den eksisterende filen?</p></body></html> + + + + dest + til + + + + with the following file? + med følgende fil? + + + + src file info + kildefil info + + + + dest file info + tilfil info + + + + src + kilde + + + + &File name: + &Filnavn: + + + + Apply this option to all existing files + Bruk dette valget pÃ¥ alle eksisterende filer + + + + SearchDialog + + + Search Files + Søk filer + + + + Name/Location + Navn/Sted + + + + File Name Patterns: + Filnavnmønstre: + + + + * + + + + + Case insensitive + Ignorer STORE og smÃ¥ bokstaver + + + + Use regular expression + Bruk regular expression + + + + Places to Search: + Steder Ã¥ søke: + + + + &Add + &Legg til + + + + &Remove + &Fjern + + + + Search in sub directories + Søk i undermapper + + + + Search for hidden files + Søk etter skjulte filer + + + + File Type + Filtype + + + + Only search for files of following types: + Bare søk etter følgende filtyper: + + + + Text files + Tekstfiler + + + + Image files + Bildefiler + + + + Audio files + Lydfiler + + + + Video files + Videofiler + + + + Documents + Dokumenter + + + + Folders + Mapper + + + + Content + Innhold + + + + File contains: + Fil inneholder: + + + + Case insensiti&ve + Ignorerer STORE& og smÃ¥ bokstaver + + + + &Use regular expression + &Bruk regular expression + + + + Properties + Egenskaper + + + + File Size: + Filstørrelse: + + + + Larger than: + Større enn: + + + + + Bytes + + + + + + KiB + + + + + + MiB + + + + + + GiB + + + + + Smaller than: + Mindre enn: + + + + Last Modified Time: + Sist endret: + + + + Earlier than: + Før: + + + + Later than: + Etter: + + + diff --git a/src/translations/libfm-qt_nl.ts b/src/translations/libfm-qt_nl.ts new file mode 100644 index 0000000..bc9ebaa --- /dev/null +++ b/src/translations/libfm-qt_nl.ts @@ -0,0 +1,1560 @@ + + + + + AppChooserDialog + + + Choose an Application + Kies een toepassing + + + + Installed Applications + Geïnstalleerde toepassingen + + + + Custom Command + Aangepaste opdracht + + + + Command line to execute: + Uit te voeren opdrachtregel: + + + + Application name: + Naam van toepassing: + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + <b>Deze speciale codes kunt u gebruiken in de opdrachtregel:</b> +<ul> +<li><b>%f</b>: Vertegenwoordigt een enkele bestandnaam</li> +<li><b>%F</b>: Vertegenwoordigt meerdere bestandnamen</li> +<li><b>%u</b>: Vertegenwoordigt een enkele URI van het bestand</li> +<li><b>%U</b>: Vertegenwoordigt meerdere URI's</li> +</ul> + + + + Keep terminal window open after command execution + Houd terminalvenster open na het uitvoeren van opdrachtregel + + + + Execute in terminal emulator + Voer uit in een terminalnabootser + + + + Set selected application as default action of this file type + Stel gekozen toepassing in als standaardactie voor deze bestandsoort + + + + EditBookmarksDialog + + + Edit Bookmarks + Bladwijzers bewerken + + + + Name + Naam + + + + Location + Plaats + + + + &Add Item + Element &toevoegen + + + + &Remove Item + Element &verwijderen + + + + Use drag and drop to reorder the items + Gebruik sleur en pleur om de elementen opnieuw te rangschikken + + + + ExecFileDialog + + + Execute file + Bestand uitvoeren + + + + &Open + O&penen + + + + E&xecute + &Uitvoeren + + + + Execute in &Terminal + In een &terminal uitvoeren + + + + Cancel + Afbreken + + + + FileDialog + + + Location: + Locatie: + + + + File name: + Bestandsnaam: + + + + File type: + Bestandssoort: + + + + FileOperationDialog + + + Destination: + Doel: + + + + Processing: + Bezig met: + + + + Preparing... + Voorbereiden... + + + + Progress + Voortgang + + + + Time remaining: + Resterende tijd: + + + + Files processed: + Verwerkte bestanden: + + + + FilePropsDialog + + + File Properties + Bestandeigenschappen + + + + General + Algemeen + + + + Location: + Locatie: + + + + File type: + Bestandsoort: + + + + MIME type: + MIME-type: + + + + File size: + Bestandgrootte: + + + + On-disk size: + Omvang op schijf: + + + + Last modified: + Laatst gewijzigd: + + + + Link target: + Koppelingsdoel: + + + + Open With: + Openen met: + + + + Last accessed: + Laatst benaderd: + + + + Contains: + + + + + Device Usage: + + + + + Permissions + Rechten + + + + Ownership + Eigendom + + + + + + Group: + Groep: + + + + + + Owner: + Eigenaar: + + + + Access Control + Toegangsbeheer + + + + + Other: + Overige: + + + + Make the file executable + Bestand uitvoerbaar maken + + + + + + Read + Lezen + + + + + + Write + Schrijven + + + + + + Execute + Uitvoeren + + + + Sticky + Klevend + + + + SetUID + SetUID + + + + SetGID + SetGID + + + + Advanced Mode + Geavanceerde modus + + + + Fm::AppChooserComboBox + + + Customize + Aanpassen + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + Kies een toepassing voor het openen van '%1' bestanden + + + + Fm::CreateNewMenu + + + Folder + Map + + + + Blank File + Leeg bestand + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + De opgegeven map '%1' is niet geldig + + + + Fm::DirTreeModel + + + Loading... + Laden... + + + + + + <No sub folders> + <Geen submappen> + + + + Fm::DirTreeView + + + Open in New T&ab + Open in nieuw t&abblad + + + + Open in New Win&dow + Open in nieuw &venster + + + + Open in Termina&l + Open in termina&lvenster + + + + Fm::DndActionMenu + + + Copy here + Hierheen kopiëren + + + + Move here + Hierheen verplaatsen + + + + Create symlink here + Maak hier een symbolische koppeling + + + + Cancel + Afbreken + + + + Fm::EditBookmarksDialog + + + New bookmark + Nieuwe bladwijzer + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + Dit bestand '%1' lijkt een bureaubladsnelkoppeling te zijn. +Wat wilt u ermee doen? + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + Het tekstbestand '%1' lijkt een uitvoerbaar script te zijn. +Wat wilt u ermee doen? + + + + This file '%1' is executable. Do you want to execute it? + Het bestand '%1' is uitvoerbaar. Wilt u het uitvoeren? + + + + Fm::FileDialog + + + Go Back + Ga terug + + + + Alt+Left + Go Back + Alt+Links + + + + Go Forward + Ga vooruit + + + + Alt+Right + Go Forward + Alt+Rechts + + + + Reload + Herladen + + + + F5 + Reload + F5 + + + + Create Folder + Map maken + + + + Icon View + Pictogramweergave + + + + Thumbnail View + Miniatuurweergave + + + + Compact View + Compacte weergave + + + + Detailed List View + Gedetailleerde lijstweergave + + + + + Error + Fout + + + + Please select a file + Kies a.u.b. een bestand + + + + %1 already exists. +Do you want to replace it? + %1 bestaat reeds. +Wilt u het vervangen? + + + + Path "%1" does not exist + Pad '%1' bestaat niet + + + + "%1" is not a directory + '%1' is geen map + + + + "%1" is not a file + '%1' is geen bestand + + + + + &Open + O&penen + + + + + &Save + &Opslaan + + + + All Files (*) + Alle bestanden (*) + + + + Fm::FileDialogHelper + + + Open File + Bestand openen + + + + Save File + Bestand opslaan + + + + Fm::FileMenu + + + Open + Openen + + + + Create &New + &Nieuw maken + + + + &Restore + Te&rugzetten + + + + Cut + Knippen + + + + Copy + Kopiëren + + + + Paste + Plakken + + + + + &Move to Trash + Naar de &prullenbak verplaatsen + + + + Trust selected executables + Gekozen uitvoerbare bestanden vertrouwen + + + + Trust this executable + Vertrouw dit uitvoerbare bestand + + + + Output + Uitvoer + + + + &Delete + &Wissen + + + + Rename + Hernoemen + + + + Open With... + Openen met... + + + + Other Applications + Andere toepassingen + + + + Extract to... + Uitpakken naar... + + + + Extract Here + Hier uitpakken + + + + Compress + Comprimeren + + + + Properties + Eigenschappen + + + + Fm::FileOperation + + + Error + Fout + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + Sommige bestanden kunnen niet naar de prullenbak worden verplaatst, omdat de onderliggende bestandssystemen deze bewerking niet ondersteunen. +Wilt u ze in plaats daarvan blijvend verwijderen? + + + + + Confirm + Bevestigen + + + + Do you want to delete the selected files? + Wilt u de gekozen bestanden verwijderen? + + + + Do you want to move the selected files to trash can? + Wilt u de gekozen bestanden verplaatsen naar de prullenbak? + + + + Fm::FileOperationDialog + + + Move files + Bestanden verplaatsen + + + + Moving the following files to destination folder: + Verplaats de volgende bestanden naar de bestemmingsmap: + + + + Copy Files + Bestanden kopiëren + + + + Copying the following files to destination folder: + Kopieer de volgende bestanden naar de bestemmingsmap: + + + + Trash Files + Bestanden naar de prullenbak verplaatsen + + + + Moving the following files to trash can: + Verplaats de volgende bestanden naar de prullenbak: + + + + Delete Files + Bestanden wissen + + + + Deleting the following files: + Wis de volgende bestanden: + + + + Create Symlinks + Symbolische koppelingen maken + + + + Creating symlinks for the following files: + Symbolische maken voor de volgende bestanden: + + + + Change Attributes + Eigenschappen veranderen + + + + Changing attributes of the following files: + Eigenschappen van de volgende bestanden veranderen: + + + + Restore Trashed Files + Bestanden terugzetten vanuit de prullenbak + + + + Restoring the following files from trash can: + De volgende bestanden terugzetten vanuit de prullenbak: + + + + + Error + Fout + + + + Fm::FilePropsDialog + + + View folder content + Toon mapinhoud + + + + View and modify folder content + Mapinhoud tonen en wijzigen + + + + Read + Lezen + + + + Read and write + Lezen en schrijven + + + + Forbidden + Verboden + + + + Files of different types + Bestanden van verschillende soorten + + + + Multiple Files + Meerdere bestanden + + + + %p% used + + + + + %1 Free of %2 + + + + + no file + + + + + one file + + + + + %1 files + + + + + Select an icon + Kies een pictogram + + + + Images (*.png *.xpm *.svg *.svgz ) + Afbeeldingen (*.png *.xpm *.svg *.svgz ) + + + + Apply changes + Veranderingen toepassen + + + + Do you want to recursively apply these changes to all files and sub-folders? + Wilt u de veranderingen toepassen op alle bestanden en onderliggende submappen? + + + + Fm::FileSearchDialog + + + Error + Fout + + + + You should add at least one directory to search. + U dient tenminste één te doorzoeken map toe te voegen. + + + + Select a folder + Kies een map + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + Kan geen koppeling aanmaken op een niet-eigen bestandssysteem + + + + Fm::FolderMenu + + + Create &New + &Nieuw maken + + + + &Paste + &Plakken + + + + Select &All + &Alle kiezen + + + + Invert Selection + Keuze omdraaien + + + + Sorting + Rangschikking + + + + Show Hidden + Toon verborgen + + + + Folder Pr&operties + Map&eigenschappen + + + + Output + Uitvoer + + + + By File Name + Op bestandnaam + + + + By Modification Time + Op wijzigingstijdstip + + + + By File Size + Op bestandgrootte + + + + By File Type + Op bestandsoort + + + + By File Owner + Op bestandeigenaar + + + + Ascending + Oplopend + + + + Descending + Aflopend + + + + Folder First + Map eerst + + + + Case Sensitive + Hoofdlettergevoelig + + + + Fm::FolderModel + + + Name + Naam + + + + Type + Soort + + + + Size + Grootte + + + + Modified + Gewijzigd + + + + Owner + Eigenaar + + + + Group + Groep + + + + Fm::FontButton + + + Bold + Vet + + + + Italic + Schuin + + + + Fm::MountOperationPasswordDialog + + + &Connect + &Verbinden + + + + Fm::PathBar + + + &Edit Path + Pad &bewerken + + + + &Copy Path + Pad &kopiëren + + + + Fm::PlacesModel + + + Places + Locaties + + + + Desktop + Bureaublad + + + + Trash + Prullenbak + + + + Computer + Computer + + + + Applications + Toepassingen + + + + Network + Netwerk + + + + Devices + Apparaten + + + + Bookmarks + Bladwijzers + + + + Fm::PlacesView + + + Empty Trash + Prullenbak ledigen + + + + Open in New Tab + Open in nieuw tabblad + + + + Open in New Window + Open in nieuw venster + + + + + Hide + Verbergen + + + + Move Bookmark Up + Bladwijzer omhoog verplaatsen + + + + Move Bookmark Down + Bladwijzer naar beneden verplaatsen + + + + Rename Bookmark + Bladwijzer hernoemen + + + + Remove Bookmark + Bladwijzer verwijderen + + + + + Unmount + Ontkoppelen + + + + Mount + Aankoppelen + + + + Eject + Uitwerpen + + + + Show All Entries + Toon alle invoervelden + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + Soort: %1 +Omvang: %2 +Gewijzigd: %3 + + + + + Type: %1 +Modified: %2 + Soort: %1 +Veranderd: %2 + + + + &Overwrite + Over&schrijven + + + + &Rename + &Hernoemen + + + + Fm::SidePane + + + Places + Locaties + + + + Directory Tree + Boomstructuur van de mappen + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + Kan bestand '%s' niet uit de prullenbak halen: oorspronkelijk pad onbekend + + + + MountOperationPasswordDialog + + + Mount + Aankoppelen + + + + Connect &anonymously + &Anoniem verbinden + + + + Connect as u&ser: + Verbinden als &gebruiker: + + + + &Username: + Gebruikers&naam: + + + + &Password: + &Wachtwoord: + + + + &Domain: + &Domein: + + + + Forget password &immediately + Wachtwoord &direct vergeten + + + + Remember password until you &logout + Onthoud wachtwoord totdat u zich af&meldt + + + + Remember &forever + Wachtwoord &voor altijd onthouden + + + + QObject + + + + + + + Error + Fout + + + + Rename File + Bestand hernoemen + + + + Please enter a new name: + Voer a.u.b. een nieuwe naam in: + + + + Create Folder + Map maken + + + + Please enter a new file name: + Voer a.u.b. een nieuwe bestandnaam in: + + + + New text file + Nieuw tekstbestand + + + + Please enter a new folder name: + Voer a.u.b. een nieuwe mapnaam in: + + + + New folder + Nieuwe map + + + + Enter a name for the new %1: + Voer a.u.b. een naam in voor de/het nieuwe %1: + + + + Create File + Bestand maken + + + + Custom Icon Error + Fout inzake aangepast pictogram + + + + The path is not mounted. + Het pad is niet aangekoppeld. + + + + Invalid desktop entry file: '%1' + Ongeldig bureaubladsnelkoppelingsbestand: '%1' + + + + No default application is set to launch '%1' + Er is geen standaardtoepassing ingesteld om '%1' te starten + + + + Cannot set working directory to '%1': %2 + Kan werkmap niet instellen op '%1': %2 + + + + Identifier: + + + + + RenameDialog + + + Confirm to replace files + Vervangen van bestanden bevestigen + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + <html><head/><body><p><span style=" font-weight:600;">Er is al een bestand met dezelfde naam op deze plek.</span></p><p>Wilt u het bestaande bestand vervangen?</p></body></html> + + + + dest + doel + + + + with the following file? + met het volgende bestand? + + + + src file info + Info over het bronbestand + + + + dest file info + Info over het doelbestand + + + + src + bron + + + + &File name: + &Bestandnaam: + + + + Apply this option to all existing files + Pas deze optie toe op alle bestaande bestanden + + + + SearchDialog + + + Search Files + Bestanden zoeken + + + + Name/Location + Naam/locatie + + + + File Name Patterns: + Patronen voor bestandnaam: + + + + * + * + + + + Case insensitive + Hoofdletterongevoelig + + + + Use regular expression + Gebruik reguliere uitdrukking + + + + Places to Search: + Te doorzoeken locaties: + + + + &Add + &Toevoegen + + + + &Remove + &Verwijderen + + + + Search in sub directories + Zoek in submappen + + + + Search for hidden files + Zoek naar verborgen bestanden + + + + File Type + Bestandsoort + + + + Only search for files of following types: + Zoek alleen naar bestanden van de volgende soorten: + + + + Text files + Tekstbestanden + + + + Image files + Afbeeldingbestanden + + + + Audio files + Geluidbestanden + + + + Video files + Videobestanden + + + + Documents + Documenten + + + + Folders + Mappen + + + + Content + Inhoud + + + + File contains: + Bestand bevat: + + + + Case insensiti&ve + Hoofdletteronge&voelig + + + + &Use regular expression + Gebruik gewone uitdrukkingen + + + + Properties + Eigenschappen + + + + File Size: + Bestandgrootte: + + + + Larger than: + Groter dan: + + + + + Bytes + Bytes + + + + + KiB + KiB + + + + + MiB + MiB + + + + + GiB + GiB + + + + Smaller than: + Kleiner dan: + + + + Last Modified Time: + Laatste wijzigingstijdstip: + + + + Earlier than: + Vroeger dan: + + + + Later than: + Later dan: + + + diff --git a/src/translations/libfm-qt_pl.ts b/src/translations/libfm-qt_pl.ts new file mode 100644 index 0000000..7fe127b --- /dev/null +++ b/src/translations/libfm-qt_pl.ts @@ -0,0 +1,1560 @@ + + + + + AppChooserDialog + + + Choose an Application + Wybór programu + + + + Installed Applications + Zainstalowane programy + + + + Custom Command + Własny wiersz poleceń + + + + Command line to execute: + Wiersz poleceń do wykonania: + + + + Application name: + Nazwa programu: + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + <b>W wierszu poleceń można użyć następujących oznaczeń:</b> +<ul> +<li><b>%f</b>: nazwa pierwszego z zaznaczonych plików</li> +<li><b>%F</b>: nazwy wszystkich zaznaczonych plików</li> +<li><b>%u</b>: ścieżka pierwszego z zaznaczonych plików</li> +<li><b>%U</b>: ścieżki wszystkich zaznaczonych plików</li> +</ul> + + + + Keep terminal window open after command execution + Zachowanie otwartego terminala po wykonaniu polecenia + + + + Execute in terminal emulator + Uruchomienie w emulatorze terminala + + + + Set selected application as default action of this file type + Ustaw wybrany program jako domyślną akcję dla tego typu plików + + + + EditBookmarksDialog + + + Edit Bookmarks + Edycja zakładek + + + + Name + Nazwa + + + + Location + Położenie + + + + &Add Item + &Dodaj + + + + &Remove Item + &Usuń + + + + Use drag and drop to reorder the items + Przeciągnij i upuść aby zmienić kolejność + + + + ExecFileDialog + + + Execute file + Uruchom + + + + &Open + &Otwórz + + + + E&xecute + &Uruchom + + + + Execute in &Terminal + Uruchom w &terminalu + + + + Cancel + Anuluj + + + + FileDialog + + + Location: + Położenie: + + + + File name: + Nazwa pliku: + + + + File type: + Rodzaj pliku: + + + + FileOperationDialog + + + Destination: + Cel: + + + + Processing: + Przetwarzanie: + + + + Preparing... + Przygotowanie… + + + + Progress + Postęp + + + + Time remaining: + Pozostały czas: + + + + Files processed: + Przetworzone pliki: + + + + FilePropsDialog + + + File Properties + Właściwości pliku + + + + General + Ogólne + + + + Location: + Położenie: + + + + File type: + Typ pliku: + + + + MIME type: + Typ mime: + + + + File size: + Rozmiar pliku: + + + + On-disk size: + Rozmiar na dysku: + + + + Last modified: + Ostatnia modyfikacja: + + + + Link target: + Położenie docelowe: + + + + Open With: + Otwieranie za pomocą: + + + + Last accessed: + Ostatni dostęp: + + + + Contains: + Zawiera: + + + + Device Usage: + Użycie urządzenia: + + + + Permissions + Uprawnienia + + + + Ownership + Właściciel + + + + + + Group: + Grupa: + + + + + + Owner: + Właściciel: + + + + Access Control + Prawa dostępu + + + + + Other: + Inni: + + + + Make the file executable + Ustaw prawo wykonywalności + + + + + + Read + Odczyt + + + + + + Write + Zapis + + + + + + Execute + Wykonanie + + + + Sticky + Sticky + + + + SetUID + SetUID + + + + SetGID + SetGID + + + + Advanced Mode + Zaawansowane + + + + Fm::AppChooserComboBox + + + Customize + Dostosuj + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + Wybierz program do otwierania plików "%1" + + + + Fm::CreateNewMenu + + + Folder + Katalog + + + + Blank File + Pusty plik + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + Określony katalog '%1' nie jest prawidłowy + + + + Fm::DirTreeModel + + + Loading... + Wczytywanie… + + + + + + <No sub folders> + <Brak katalogów> + + + + Fm::DirTreeView + + + Open in New T&ab + Otwórz w nowej k&arcie + + + + Open in New Win&dow + Otwórz w nowym o&knie + + + + Open in Termina&l + Otwórz w termina&lu + + + + Fm::DndActionMenu + + + Copy here + Kopiuj tutaj + + + + Move here + Przenieś tutaj + + + + Create symlink here + Utwórz dowiązanie symboliczne + + + + Cancel + Anuluj + + + + Fm::EditBookmarksDialog + + + New bookmark + Nowa zakładka + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + Plik '%1' jest wpisem pulpitu. +Co chcesz z nim zrobić? + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + Plik tekstowy '%1' wygląda na wykonywalny skrypt. +Co chcesz zrobić z tym plikiem? + + + + This file '%1' is executable. Do you want to execute it? + Plik '%1' jest wykonywalny. Czy chcesz go uruchomić? + + + + Fm::FileDialog + + + Go Back + Wróć + + + + Alt+Left + Go Back + Alt+Strzałka w lewo + + + + Go Forward + Idź do tyłu + + + + Alt+Right + Go Forward + Alt+Strzałka w prawo + + + + Reload + Przeładuj + + + + F5 + Reload + F5 + + + + Create Folder + Utwórz katalog + + + + Icon View + Widok ikon + + + + Thumbnail View + Widok miniatur + + + + Compact View + Widok kompaktowy + + + + Detailed List View + Widok listy szczegółowej + + + + + Error + Błąd + + + + Please select a file + Wybierz plik + + + + %1 already exists. +Do you want to replace it? + %1 już istnieje. +Czy chcesz go zamienić? + + + + Path "%1" does not exist + Ścieżka "%1" nie istnieje + + + + "%1" is not a directory + "%1" nie jest katalogiem + + + + "%1" is not a file + "%1" nie jest plikiem + + + + + &Open + &Otwórz + + + + + &Save + Zapi&sz + + + + All Files (*) + Wszystkie pliki (*) + + + + Fm::FileDialogHelper + + + Open File + Otwórz plik + + + + Save File + Zapisz plik + + + + Fm::FileMenu + + + Open + Otwórz + + + + Create &New + Utwórz &nowy + + + + &Restore + &Przywróć + + + + Cut + Wytnij + + + + Copy + Kopiuj + + + + Paste + Wklej + + + + + &Move to Trash + Przenieś do &kosza + + + + Trust selected executables + Ufaj zaznaczonym plikom wykonywalnym + + + + Trust this executable + Ufaj temu plikowi wykonywalnemu + + + + Output + Wyjście + + + + &Delete + &Usuń + + + + Rename + Zmień nazwę + + + + Open With... + Otwórz za pomocą… + + + + Other Applications + Inny program + + + + Extract to... + Rozpakuj do… + + + + Extract Here + Rozpakuj tutaj + + + + Compress + Skompresuj + + + + Properties + Właściwości + + + + Fm::FileOperation + + + Error + Błąd + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + Niektóre pliki nie mogą zostać przeniesione do kosza ponieważ system plików nie obsługuje takiej operacji. +Czy zamiast tego usunąć te pliki? + + + + + Confirm + Potwierdź + + + + Do you want to delete the selected files? + Czy chcesz usunąć wybrane pliki? + + + + Do you want to move the selected files to trash can? + Czy chcesz przenieść wybrane pliki do kosza? + + + + Fm::FileOperationDialog + + + Move files + Przenoszenie plików + + + + Moving the following files to destination folder: + Przenoszenie następujących plików do katalogu docelowego: + + + + Copy Files + Kopiowanie plików + + + + Copying the following files to destination folder: + Kopiowanie następujących plików do katalogu docelowego: + + + + Trash Files + Przenoszenie plików do kosza + + + + Moving the following files to trash can: + Przenoszenie następujących plików do kosza: + + + + Delete Files + Usuwanie plików + + + + Deleting the following files: + Usuwanie następujących plików: + + + + Create Symlinks + Tworzenie dowiązań symbolicznych + + + + Creating symlinks for the following files: + Tworzenie dowiązań dla następujących plików: + + + + Change Attributes + Zmiana uprawnień + + + + Changing attributes of the following files: + Zmiana uprawnień dla następujących plików: + + + + Restore Trashed Files + Przywracanie plików z kosza + + + + Restoring the following files from trash can: + Przywracanie następujących plików z kosza: + + + + + Error + Błąd + + + + Fm::FilePropsDialog + + + View folder content + Wyświetlanie zawartości + + + + View and modify folder content + Wyświetlanie i modyfikowanie zawartości + + + + Read + Odczyt + + + + Read and write + Odczyt i zapis + + + + Forbidden + Zabronione + + + + Files of different types + Pliki różnego typu + + + + Multiple Files + Wiele plików + + + + %p% used + W użyciu %p% + + + + %1 Free of %2 + %1 wolnego z %2 + + + + no file + brak plików + + + + one file + jeden plik + + + + %1 files + %1 plików + + + + Select an icon + Wybierz ikonę + + + + Images (*.png *.xpm *.svg *.svgz ) + Pliki obrazów (*.png *.xpm *.svg *.svgz ) + + + + Apply changes + Zastosuj zmiany + + + + Do you want to recursively apply these changes to all files and sub-folders? + Czy chcesz rekursywnie zastosować zmiany do wszystkich plików i podkatalogów? + + + + Fm::FileSearchDialog + + + Error + Błąd + + + + You should add at least one directory to search. + Musisz dodać przynajmniej jeden katalog. + + + + Select a folder + Wybierz katalog + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + Nie można utworzyć nawiązania na nienatywnym systemie plików + + + + Fm::FolderMenu + + + Create &New + Utwórz &nowy + + + + &Paste + &Wklej + + + + Select &All + Z&aznacz wszystko + + + + Invert Selection + Odwróć zaznaczenie + + + + Sorting + Sortowanie + + + + Show Hidden + Wyświetlanie ukrytych plików + + + + Folder Pr&operties + Właściwości &katalogu + + + + Output + Wyjście + + + + By File Name + Według nazwy + + + + By Modification Time + Według czasu modyfikacji + + + + By File Size + Według rozmiaru + + + + By File Type + Według typu + + + + By File Owner + Wedłuh właściciela pliku + + + + Ascending + Rosnąco + + + + Descending + Malejąco + + + + Folder First + Najpierw katalogi + + + + Case Sensitive + Uwzględnij wielkość liter + + + + Fm::FolderModel + + + Name + Nazwa + + + + Type + Typ + + + + Size + Rozmiar + + + + Modified + Zmodyfikowany + + + + Owner + Właściciel + + + + Group + Grupa + + + + Fm::FontButton + + + Bold + Pogrubienie + + + + Italic + Kursywa + + + + Fm::MountOperationPasswordDialog + + + &Connect + &Podłącz + + + + Fm::PathBar + + + &Edit Path + &Edytuj ścieżkę + + + + &Copy Path + &Kopiuj ścieżkę + + + + Fm::PlacesModel + + + Places + Położenia + + + + Desktop + Pulpit + + + + Trash + Kosz + + + + Computer + Komputer + + + + Applications + Programy + + + + Network + Sieć + + + + Devices + Urządzenia + + + + Bookmarks + Zakładki + + + + Fm::PlacesView + + + Empty Trash + Opróżnij kosz + + + + Open in New Tab + Otwórz w nowej karcie + + + + Open in New Window + Otwórz w nowym oknie + + + + + Hide + Ukryj + + + + Move Bookmark Up + Do góry + + + + Move Bookmark Down + W dół + + + + Rename Bookmark + Zmień nazwę + + + + Remove Bookmark + Usuń + + + + + Unmount + Odmontuj + + + + Mount + Montuj + + + + Eject + Wysuń + + + + Show All Entries + Pokaż wszystkie wpisy + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + Typ: %1 +Rozmiar: %2 +Zmodyfikowany: %3 + + + + + Type: %1 +Modified: %2 + Typ: %1 +Zmodyfikowany: %2 + + + + &Overwrite + &Nadpisz + + + + &Rename + &Zmień nazwę + + + + Fm::SidePane + + + Places + Położenia + + + + Directory Tree + Drzewo katalogów + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + Nie można przywrócić pliku '%s': nieznana oryginalna ścieżka + + + + MountOperationPasswordDialog + + + Mount + Montowanie + + + + Connect &anonymously + Połącz &anonimowo + + + + Connect as u&ser: + Połącz jako &użytkownik: + + + + &Username: + &Użytkownik: + + + + &Password: + &Hasło: + + + + &Domain: + &Domena: + + + + Forget password &immediately + Zapomnij hasło &od razu + + + + Remember password until you &logout + &Pamiętaj hasło do wylogowania + + + + Remember &forever + Pamiętaj na &zawsze + + + + QObject + + + + + + + Error + Błąd + + + + Rename File + Zmiana nazwy + + + + Please enter a new name: + Podaj nową nazwę: + + + + Create Folder + Utwórz katalog + + + + Please enter a new file name: + Podaj nazwę pliku: + + + + New text file + Nowy plik tekstowy + + + + Please enter a new folder name: + Podaj nazwę nowego katalogu: + + + + New folder + Nowy katalog + + + + Enter a name for the new %1: + Podaj nazwę dla pliku %1: + + + + Create File + Utwórz plik + + + + Custom Icon Error + Błąd nieprawidłowej ikony + + + + The path is not mounted. + Ścieżka nie jest zamontowana. + + + + Invalid desktop entry file: '%1' + Nieprawidłowy plik wpisu pulpitu: '%1' + + + + No default application is set to launch '%1' + Nie ustawiono domyślnej aplikacji do uruchomienia '%1' + + + + Cannot set working directory to '%1': %2 + Nie można ustawić katalogu roboczego na '%1': %2 + + + + Identifier: + Identyfikator: + + + + RenameDialog + + + Confirm to replace files + Potwierdź aby nadpisać pliki + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + <html><head/><body><p><span style=" font-weight:600;">Plik o podanej nazwie już istnieje w katalogu docelowym.</span></p><p>Czy chcesz nadpisać istniejący plik?</p></body></html> + + + + dest + doc. + + + + with the following file? + następującym plikiem? + + + + src file info + inf. o pliku źród. + + + + dest file info + inf. o pliku doc. + + + + src + źród. + + + + &File name: + &Nazwa pliku: + + + + Apply this option to all existing files + Zastosuj do wszystkich plików + + + + SearchDialog + + + Search Files + Wyszukiwanie + + + + Name/Location + Nazwa/Lokacja + + + + File Name Patterns: + Nazwa pliku: + + + + * + * + + + + Case insensitive + Nie uwzględniaj wielkości liter + + + + Use regular expression + Użyj wyrażeń regularnych + + + + Places to Search: + Katalogi do przeszukania: + + + + &Add + Dod&aj + + + + &Remove + &Usuń + + + + Search in sub directories + Przeszukuj podkatalogi + + + + Search for hidden files + Przeszukuj pliki ukryte + + + + File Type + Typy plików + + + + Only search for files of following types: + Wyszukaj tylko pliki następujących typów: + + + + Text files + Pliki tekstowe + + + + Image files + Obrazki + + + + Audio files + Pliki audio + + + + Video files + Pliki wideo + + + + Documents + Dokumenty + + + + Folders + Katalogi + + + + Content + Zawartość + + + + File contains: + Plik zawiera: + + + + Case insensiti&ve + &Nie uwzględniaj wielkości liter + + + + &Use regular expression + &Użyj wyrażeń regularnych + + + + Properties + Właściwości + + + + File Size: + Rozmiar pliku: + + + + Larger than: + Większy niż: + + + + + Bytes + Bajty + + + + + KiB + KB + + + + + MiB + MB + + + + + GiB + GB + + + + Smaller than: + Mniejszy niż: + + + + Last Modified Time: + CZas ostatniej modyfikacji: + + + + Earlier than: + Wcześniej niż: + + + + Later than: + Poźniej niż: + + + diff --git a/src/translations/libfm-qt_pt.ts b/src/translations/libfm-qt_pt.ts new file mode 100644 index 0000000..6681969 --- /dev/null +++ b/src/translations/libfm-qt_pt.ts @@ -0,0 +1,1560 @@ + + + + + AppChooserDialog + + + Choose an Application + Escolha uma aplicação + + + + Installed Applications + Aplicações instaladas + + + + Custom Command + Comando personalizado + + + + Command line to execute: + Linha de comandos a executar: + + + + Application name: + Nome da aplicação: + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + <b>Esses códigos especiais podem ser usados na linha de comando:</b> +<ul> +<li><b>%f</b>: Representa um único nome de ficheiro</li> +<li><b>%F</b>: Representa vários nomes de ficheiros</li> +<li><b>%u</b>: Representa um único URI do ficheiro</li> +<li><b>%U</b>: Representa vários URI</li> +</ul> + + + + Keep terminal window open after command execution + Manter janela de terminal aberta depois de executar o comando + + + + Execute in terminal emulator + Executar no emulador de terminal + + + + Set selected application as default action of this file type + Definir a aplicação selecionada como padrão para este tipo de ficheiro + + + + EditBookmarksDialog + + + Edit Bookmarks + Editar marcadores + + + + Name + Nome + + + + Location + Localização + + + + &Add Item + &Adicionar item + + + + &Remove Item + &Remover item + + + + Use drag and drop to reorder the items + Utilize 'arrastar e largar' para organizar os itens + + + + ExecFileDialog + + + Execute file + Executar ficheiro + + + + &Open + &Abrir + + + + E&xecute + E&xecutar + + + + Execute in &Terminal + Executar no &terminal + + + + Cancel + Cancelar + + + + FileDialog + + + Location: + Localização: + + + + File name: + Nome do ficheiro: + + + + File type: + Tipo de ficheiro: + + + + FileOperationDialog + + + Destination: + Destino: + + + + Processing: + Processamento: + + + + Preparing... + Preparação... + + + + Progress + Progresso + + + + Time remaining: + Tempo restante: + + + + Files processed: + Ficheiros processados: + + + + FilePropsDialog + + + File Properties + Propriedades do ficheiro + + + + General + Geral + + + + Location: + Localização: + + + + File type: + Tipo de ficheiro: + + + + MIME type: + Tipo MIME: + + + + File size: + Tamanho do ficheiro: + + + + On-disk size: + Tamanho no disco: + + + + Last modified: + Última modificação: + + + + Link target: + Destino da ligação: + + + + Open With: + Abrir com: + + + + Last accessed: + Último acesso: + + + + Contains: + Contém: + + + + Device Usage: + Utilização do dispositivo: + + + + Permissions + Permissões + + + + Ownership + Titularidade + + + + + + Group: + Grupo: + + + + + + Owner: + Proprietário: + + + + Access Control + Controlo de acesso + + + + + Other: + Outro: + + + + Make the file executable + Marcar ficheiro como executável + + + + + + Read + Leitura + + + + + + Write + Escrita + + + + + + Execute + Execução + + + + Sticky + Fixo + + + + SetUID + Definir UID + + + + SetGID + Definir GID + + + + Advanced Mode + Modo avançado + + + + Fm::AppChooserComboBox + + + Customize + Personalizar + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + Selecione uma aplicação para abrir os ficheiros "%1" + + + + Fm::CreateNewMenu + + + Folder + Pasta + + + + Blank File + Ficheiro vazio + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + O diretório especificado '%1' é inválido + + + + Fm::DirTreeModel + + + Loading... + Carregando... + + + + + + <No sub folders> + <Sem sub-pastas> + + + + Fm::DirTreeView + + + Open in New T&ab + Abrir em novo sep&arador + + + + Open in New Win&dow + Abrir em nova ja&nela + + + + Open in Termina&l + Abrir no termina&l + + + + Fm::DndActionMenu + + + Copy here + Copiar aqui + + + + Move here + Mover aqui + + + + Create symlink here + Criar ligação simbólica aqui + + + + Cancel + Cancelar + + + + Fm::EditBookmarksDialog + + + New bookmark + Novo marcador + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + Parece que '%1' é uma entrada da área de trabalho. +O que deseja fazer? + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + Parece que o ficheiro de texto %1 é um script executável. +O que deseja fazer? + + + + This file '%1' is executable. Do you want to execute it? + O ficheiro %1 é um executável. Deseja executar? + + + + Fm::FileDialog + + + Go Back + Recuar + + + + Alt+Left + Go Back + Alt+Seta esquerda + + + + Go Forward + Avançar + + + + Alt+Right + Go Forward + Alt+Seta direita + + + + Reload + Recarregar + + + + F5 + Reload + F5 + + + + Create Folder + Criar pasta + + + + Icon View + Vista de ícones + + + + Thumbnail View + Vista de miniaturas + + + + Compact View + Vista compacta + + + + Detailed List View + Vista em lista detalhada + + + + + Error + Erro + + + + Please select a file + Selecione um ficheiro + + + + %1 already exists. +Do you want to replace it? + %1 já existe. +Deseja substituir? + + + + Path "%1" does not exist + Caminho '%1' não existe + + + + "%1" is not a directory + '%1' não é um diretório + + + + "%1" is not a file + '%1' não é um ficheiro + + + + + &Open + &Abrir + + + + + &Save + &Guardar + + + + All Files (*) + Todos os ficheiros (*) + + + + Fm::FileDialogHelper + + + Open File + Abrir ficheiro + + + + Save File + Guardar ficheiro + + + + Fm::FileMenu + + + Open + Abrir + + + + Cut + Cortar + + + + Copy + Copiar + + + + Paste + Colar + + + + + &Move to Trash + &Mover para o lixo + + + + Trust selected executables + Confiar nos executáveis selecionados + + + + Trust this executable + Confiar neste executável + + + + Output + Destino + + + + &Delete + &Apagar + + + + Rename + Renomear + + + + Open With... + Abrir com... + + + + Other Applications + Outras aplicações + + + + Create &New + Criar &novo(a) + + + + &Restore + &Restaurar + + + + Extract to... + Extrair para... + + + + Extract Here + Extrair aqui + + + + Compress + Comprimir + + + + Properties + Propriedades + + + + Fm::FileOperation + + + Error + Erro + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + Alguns ficheiros não podem ser movidos para o lixo porque o sistema de ficheiros subjacente não permite esta operação. +Deseja, em alternativa, apagar os ficheiros? + + + + + Confirm + Confirmação + + + + Do you want to delete the selected files? + Deseja apagar os ficheiros selecionados? + + + + Do you want to move the selected files to trash can? + Deseja mover os ficheiros selecionados para o lixo? + + + + Fm::FileOperationDialog + + + Move files + Mover ficheiros + + + + Moving the following files to destination folder: + A mover os ficheiros para a pasta de destino: + + + + Copy Files + Copiar ficheiros + + + + Copying the following files to destination folder: + A copiar os ficheiros para a pasta de destino: + + + + Trash Files + Reciclar ficheiros + + + + Moving the following files to trash can: + A mover os ficheiros para o lixo: + + + + Delete Files + Apagar ficheiros + + + + Deleting the following files: + A apagar estes ficheiros: + + + + Create Symlinks + Criar ligações simbólicas + + + + Creating symlinks for the following files: + A criar ligações simbólicas para estes ficheiros: + + + + Change Attributes + Alterar atributos + + + + Changing attributes of the following files: + A alterar os atributos destes ficheiros: + + + + Restore Trashed Files + Restaurar ficheiros apagados + + + + Restoring the following files from trash can: + A restaurar estes ficheiros do lixo: + + + + + Error + Erro + + + + Fm::FilePropsDialog + + + View folder content + Ver conteúdo da pasta + + + + View and modify folder content + Ver e modificar conteúdo da pasta + + + + Read + Leitura + + + + Read and write + Leitura e escrita + + + + Forbidden + Proibido + + + + Files of different types + Ficheiros de outro tipo + + + + Multiple Files + Vários ficheiros + + + + %p% used + %p% utilizado + + + + %1 Free of %2 + %1 de %2 livre + + + + no file + nenhum ficheiro + + + + one file + um ficheiro + + + + %1 files + %1 ficheiros + + + + Select an icon + Selecione um ícone + + + + Images (*.png *.xpm *.svg *.svgz ) + Imagens (*.png *.xpm *.svg *.svgz ) + + + + Apply changes + Aplicar alterações + + + + Do you want to recursively apply these changes to all files and sub-folders? + Pretende aplicar as alterações recursivamente a todos os ficheiros e sub-pastas? + + + + Fm::FileSearchDialog + + + Error + Erro + + + + You should add at least one directory to search. + Deve adicionar pelo menos um diretório para pesquisar. + + + + Select a folder + Selecionar uma pasta + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + Não é possível criar uma ligação em sistemas não nativos + + + + Fm::FolderMenu + + + Create &New + Criar &novo(a) + + + + &Paste + Co&lar + + + + Select &All + Selecion&ar tudo + + + + Invert Selection + Inverter seleção + + + + Sorting + Ordenação + + + + Show Hidden + Mostrar ocultos + + + + Folder Pr&operties + Pr&opriedades da pasta + + + + Output + Destino + + + + By File Name + Por nome de ficheiro + + + + By Modification Time + Por data de modificação + + + + By File Size + Por tamanho do ficheiro + + + + By File Type + Por tipo de ficheiro + + + + By File Owner + Pelo dono do ficheiro + + + + Ascending + Ascendente + + + + Descending + Descendente + + + + Folder First + Pastas primeiro + + + + Case Sensitive + Diferenciar maiúsculas/minúsculas + + + + Fm::FolderModel + + + Name + Nome + + + + Type + Tipo + + + + Size + Tamanho + + + + Modified + Modificado + + + + Owner + Proprietário + + + + Group + Grupo + + + + Fm::FontButton + + + Bold + Negrito + + + + Italic + Itálico + + + + Fm::MountOperationPasswordDialog + + + &Connect + &Conectar + + + + Fm::PathBar + + + &Edit Path + &Editar caminho + + + + &Copy Path + &Copiar caminho + + + + Fm::PlacesModel + + + Places + Locais + + + + Desktop + Área de trabalho + + + + Trash + Lixo + + + + Computer + Computador + + + + Applications + Aplicações + + + + Network + Rede + + + + Devices + Dispositivos + + + + Bookmarks + Marcadores + + + + Fm::PlacesView + + + Empty Trash + Esvaziar lixo + + + + Open in New Tab + Abrir em novo separador + + + + Open in New Window + Abrir em nova janela + + + + + Hide + Ocultar + + + + Move Bookmark Up + Mover marcador para cima + + + + Move Bookmark Down + Mover marcador para baixo + + + + Rename Bookmark + Renomear marcador + + + + Remove Bookmark + Remover marcador + + + + + Unmount + Desmontar + + + + Mount + Montar + + + + Eject + Ejetar + + + + Show All Entries + Mostrar todas as entradas + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + Tipo: %1 +Tamanho: %2 +Modificado: %3 + + + + + Type: %1 +Modified: %2 + Tipo: %1 +Modificado: %2 + + + + &Overwrite + &Substituir + + + + &Rename + &Renomear + + + + Fm::SidePane + + + Places + Locais + + + + Directory Tree + Árvore de diretórios + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + Não é possível restaurar '%s': caminho desconhecido + + + + MountOperationPasswordDialog + + + Mount + Montar + + + + Connect &anonymously + Conectar &anonimamente + + + + Connect as u&ser: + Conectar com o u&tilizador: + + + + &Username: + Nome de &utilizador: + + + + &Password: + &Palavra-passe: + + + + &Domain: + &Domínio: + + + + Forget password &immediately + Esquecer palavra-passe &imediatamente + + + + Remember password until you &logout + Memorizar pa&lavra-passe até sair da sessão + + + + Remember &forever + Memori&zar eternamente + + + + QObject + + + + + + + Error + Erro + + + + Rename File + Renomear ficheiro + + + + Please enter a new name: + Introduza o novo nome: + + + + Create Folder + Criar pasta + + + + Please enter a new file name: + Introduza o nome para o ficheiro: + + + + New text file + Novo ficheiro de texto + + + + Please enter a new folder name: + Introduza o nome para a pasta: + + + + New folder + Nova pasta + + + + Enter a name for the new %1: + Introduza um nome para %1: + + + + Create File + Criar ficheiro + + + + Custom Icon Error + Erro no ícone personalizado + + + + The path is not mounted. + O caminho não está montado. + + + + Invalid desktop entry file: '%1' + Ficheiro .desktop inválido: '%1' + + + + No default application is set to launch '%1' + Não está definida uma aplicação padrão para iniciar '%1' + + + + Cannot set working directory to '%1': %2 + Não é possível definir o diretório de trabalho para '%1': %2 + + + + Identifier: + Identificador: + + + + RenameDialog + + + Confirm to replace files + Confirmação de substituição + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + <html><head/><body><p><span style=" font-weight:600;">Já existe um ficheiro com o mesmo nome nesta localização.</span></p><p>Deseja substituir o ficheiro existente</p></body></html> + + + + dest + destino + + + + with the following file? + por este? + + + + src file info + informações do ficheiro de origem + + + + dest file info + informações do ficheiro de destino + + + + src + origem + + + + &File name: + Nome do &ficheiro: + + + + Apply this option to all existing files + Aplicar opção para todos os ficheiros existentes + + + + SearchDialog + + + Search Files + Pesquisar ficheiros + + + + Name/Location + Nome/localização + + + + File Name Patterns: + Padrões para nome de ficheiro: + + + + * + * + + + + Case insensitive + Não diferenciar maiúsculas/minúsculas + + + + Use regular expression + Utilizar expressão regular + + + + Places to Search: + Locais a pesquisar: + + + + &Add + &Adicionar + + + + &Remove + &Remover + + + + Search in sub directories + Pesquisar nos sub-diretórios + + + + Search for hidden files + Pesquisar por ficheiros ocultos + + + + File Type + Tipo de ficheiro + + + + Only search for files of following types: + Pesquisar apenas por ficheiros deste tipo: + + + + Text files + Ficheiros de texto + + + + Image files + Imagens + + + + Audio files + Áudio + + + + Video files + Vídeos + + + + Documents + Documentos + + + + Folders + Pastas + + + + Content + Conteúdo + + + + File contains: + Ficheiro contém: + + + + Case insensiti&ve + Não d&iferenciar maiúsculas/minúsculas + + + + &Use regular expression + &Utilizar expressão regular + + + + Properties + Propriedades + + + + File Size: + Tamanho do ficheiro: + + + + Larger than: + Maior do que: + + + + + Bytes + Bytes + + + + + KiB + KiB + + + + + MiB + MiB + + + + + GiB + GiB + + + + Smaller than: + Menor do que: + + + + Last Modified Time: + Data da última modificação: + + + + Earlier than: + Anterior a: + + + + Later than: + Posterior a: + + + diff --git a/src/translations/libfm-qt_pt_BR.ts b/src/translations/libfm-qt_pt_BR.ts new file mode 100644 index 0000000..aa1d79b --- /dev/null +++ b/src/translations/libfm-qt_pt_BR.ts @@ -0,0 +1,1560 @@ + + + + + AppChooserDialog + + + Choose an Application + Escolha uma Aplicação + + + + Installed Applications + Aplicações Instaladas + + + + Custom Command + Comando Personalizado + + + + Command line to execute: + Linha de comandos a executar: + + + + Application name: + Nome da aplicação: + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + <b>Esses códigos especiais podem ser usados na linha de comando:</b> +<ul> +<li><b>%f</b>: Representa um único nome de arquivo</li> +<li><b>%F</b>: Representa vários nomes de arquivos</li> +<li><b>%u</b>: Representa um único URI do arquivo</li> +<li><b>%U</b>: Representa vários URIs</li> +</ul> + + + + Keep terminal window open after command execution + Manter janela de terminal aberta depois da execução do comando + + + + Execute in terminal emulator + Executar no emulador de terminal + + + + Set selected application as default action of this file type + Definir a aplicação selecionada como padrão para este tipo de arquivo + + + + EditBookmarksDialog + + + Edit Bookmarks + Editar Favoritos + + + + Name + Nome + + + + Location + Localização + + + + &Add Item + &Adicionar item + + + + &Remove Item + &Remover item + + + + Use drag and drop to reorder the items + Usar arrastar e soltar para organizar itens + + + + ExecFileDialog + + + Execute file + Executar arquivo + + + + &Open + &Abrir + + + + E&xecute + E&xecutar + + + + Execute in &Terminal + Executar no &Terminal + + + + Cancel + Cancelar + + + + FileDialog + + + Location: + Localização: + + + + File name: + Nome do arquivo: + + + + File type: + Tipo do arquivo: + + + + FileOperationDialog + + + Destination: + Destino: + + + + Processing: + Processamento: + + + + Preparing... + Preparando... + + + + Progress + Progresso + + + + Time remaining: + Tempo restante: + + + + Files processed: + Arquivos processados: + + + + FilePropsDialog + + + File Properties + Propriedades do Arquivo + + + + General + Geral + + + + Location: + Localização: + + + + File type: + Tipo de arquivo: + + + + MIME type: + Tipo MIME: + + + + File size: + Tamanho do arquivo: + + + + On-disk size: + Tamanho no disco: + + + + Last modified: + Última modificação: + + + + Link target: + Destino do link: + + + + Open With: + Abrir com: + + + + Last accessed: + Último acesso: + + + + Contains: + Contém: + + + + Device Usage: + Uso de disco: + + + + Permissions + Permissões + + + + Ownership + Propriedades + + + + + + Group: + Grupo: + + + + + + Owner: + Proprietário: + + + + Access Control + Controle de Acesso + + + + + Other: + Outro: + + + + Make the file executable + Permitir execução do arquivo como um programa + + + + + + Read + Ler + + + + + + Write + Escrever + + + + + + Execute + Executar + + + + Sticky + Fixar + + + + SetUID + Definir UID + + + + SetGID + Definir GID + + + + Advanced Mode + Modo Avançado + + + + Fm::AppChooserComboBox + + + Customize + Customizar + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + Selecione uma aplicação para abrir arquivos "%1" + + + + Fm::CreateNewMenu + + + Folder + Pasta + + + + Blank File + Arquivo em branco + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + O diretório especificado '%1' não é válido + + + + Fm::DirTreeModel + + + Loading... + Carregando... + + + + + + <No sub folders> + <Nenhuma subpasta> + + + + Fm::DirTreeView + + + Open in New T&ab + Abrir em Nova &Aba + + + + Open in New Win&dow + Abrir em Nova Ja&nela + + + + Open in Termina&l + Abrir no Termina&l + + + + Fm::DndActionMenu + + + Copy here + Copiar aqui + + + + Move here + Mover aqui + + + + Create symlink here + Criar link simbólico aqui + + + + Cancel + Cancelar + + + + Fm::EditBookmarksDialog + + + New bookmark + Novo favorito + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + O arquivo '%1' parece ser uma entrada da área de trabalho. +O que você deseja fazer? + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + O arquivo de texto '%1' parece ser um script executável. +O que você deseja fazer? + + + + This file '%1' is executable. Do you want to execute it? + Este arquivo '%1' é executável. Você deseja executá-lo? + + + + Fm::FileDialog + + + Go Back + Voltar + + + + Alt+Left + Go Back + Alt+Seta esquerda + + + + Go Forward + Avançar + + + + Alt+Right + Go Forward + Alt+Seta direita + + + + Reload + Recarregar + + + + F5 + Reload + + + + + Create Folder + Criar pasta + + + + Icon View + Visualização em ícones + + + + Thumbnail View + Visualização em miniaturas + + + + Compact View + Visualização compacta + + + + Detailed List View + Visualização em lista detalhada + + + + + Error + Erro + + + + Please select a file + Por favor, selecione um arquivo + + + + %1 already exists. +Do you want to replace it? + %1 já existe. +Deseja fazer a substituição? + + + + Path "%1" does not exist + O destino "%1" não existe + + + + "%1" is not a directory + "%1" não é um diretório + + + + "%1" is not a file + "%1" não é um arquivo + + + + + &Open + &Abrir + + + + + &Save + &Salvar + + + + All Files (*) + Todos os arquivos (*) + + + + Fm::FileDialogHelper + + + Open File + Abrir arquivo + + + + Save File + Salvar arquivo + + + + Fm::FileMenu + + + Open + Abrir + + + + Open With... + Abrir com... + + + + Other Applications + Outras aplicações + + + + Create &New + Criar &novo(a) + + + + &Restore + &Restaurar + + + + Cut + Recortar + + + + Copy + Copiar + + + + Paste + Colar + + + + + &Move to Trash + &Mover para a lixeira + + + + Rename + Renomear + + + + Extract to... + Extrair para... + + + + Extract Here + Extrair aqui + + + + Compress + Comprimir + + + + Properties + Propriedades + + + + Trust selected executables + Confiar nos executáveis selecionados + + + + Trust this executable + Confiar neste executável + + + + Output + Destino + + + + &Delete + &Deletar + + + + Fm::FileOperation + + + Error + Erro + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + Alguns destes arquivos não podem ser movidos para a lixeira porque o sistema de arquivos não suporta esta operação. +Deseja apagar permanentemente estes arquivos? + + + + + Confirm + Confirmar + + + + Do you want to delete the selected files? + Deseja excluir o(s) arquivo(s) selecionado(s)? + + + + Do you want to move the selected files to trash can? + Deseja mover o(s) arquivo(s) selecionado(s) para a lixeira? + + + + Fm::FileOperationDialog + + + Move files + Mover arquivos + + + + Moving the following files to destination folder: + Movendo os seguintes arquivos para a pasta de destino: + + + + Copy Files + Copiar arquivos + + + + Copying the following files to destination folder: + Copiando os seguintes arquivos para a pasta de destino: + + + + Trash Files + Lixeira + + + + Moving the following files to trash can: + Movendo os seguintes arquivos para a lixeira: + + + + Delete Files + Deletar arquivos + + + + Deleting the following files: + Deletando os seguintes arquivos: + + + + Create Symlinks + Criar links simbólicos + + + + Creating symlinks for the following files: + Criando links simbólicos para os seguintes arquivos: + + + + Change Attributes + Alterar atributos + + + + Changing attributes of the following files: + Alterando atributos dos seguintes arquivos: + + + + Restore Trashed Files + Restaurar arquivos da Lixeira + + + + Restoring the following files from trash can: + Restaurando os seguintes arquivos da Lixeira: + + + + + Error + Erro + + + + Fm::FilePropsDialog + + + View folder content + Ver o conteúdo da pasta + + + + View and modify folder content + Ver e modificar o conteúdo da pasta + + + + Read + Ler + + + + Read and write + Ler e escrever + + + + Forbidden + Proibido + + + + Files of different types + Arquivos de diferentes tipos + + + + Multiple Files + Múltiplos arquivos + + + + %p% used + %p% usados + + + + %1 Free of %2 + % livre, de %2 + + + + no file + nenhum arquivo + + + + one file + um arquivo + + + + %1 files + %1 arquivos + + + + Select an icon + Selecione um ícone + + + + Images (*.png *.xpm *.svg *.svgz ) + Imagens (*.png *.xpm *.svg *.svgz ) + + + + Apply changes + Aplicar modificações + + + + Do you want to recursively apply these changes to all files and sub-folders? + Deseja também aplicar as alterações recursivamente a todos os arquivos e subpastas? + + + + Fm::FileSearchDialog + + + Error + Erro + + + + You should add at least one directory to search. + Você deve adicionar pelo menos um diretório para pesquisar. + + + + Select a folder + Selecione uma pasta + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + Não é possível criar um link em um sistema de arquivos não nativo + + + + Fm::FolderMenu + + + Create &New + Criar &novo(a) + + + + &Paste + &Colar + + + + Select &All + Selecionar &tudo + + + + Invert Selection + Inverter seleção + + + + Sorting + Classificação + + + + Show Hidden + Mostrar ocultos + + + + Folder Pr&operties + Pr&opriedades da pasta + + + + Output + Destino + + + + By File Name + Por nome do arquivo + + + + By Modification Time + Por data de modificação + + + + By File Size + Por tamanho do arquivo + + + + By File Type + Por tipo do arquivo + + + + By File Owner + Por proprietário do arquivo + + + + Ascending + Crescente + + + + Descending + Decrescente + + + + Folder First + Pastas primeiro + + + + Case Sensitive + Diferenciar Maiúsculas/minúsculas + + + + Fm::FolderModel + + + Name + Nome + + + + Type + Tipo + + + + Size + Tamanho + + + + Modified + Modificado + + + + Owner + Proprietário + + + + Group + Grupo + + + + Fm::FontButton + + + Bold + Negrito + + + + Italic + Itálico + + + + Fm::MountOperationPasswordDialog + + + &Connect + &Conectar + + + + Fm::PathBar + + + &Edit Path + &Editar caminho + + + + &Copy Path + &Copiar caminho + + + + Fm::PlacesModel + + + Places + Locais + + + + Desktop + Área de Trabalho + + + + Computer + Computador + + + + Applications + Aplicações + + + + Network + Rede + + + + Devices + Dispositivos + + + + Bookmarks + Favoritos + + + + Trash + Lixeira + + + + Fm::PlacesView + + + Open in New Tab + Abrir em nova aba + + + + Open in New Window + Abrir em nova janela + + + + Empty Trash + Esvaziar Lixeira + + + + + Hide + Ocultar + + + + Move Bookmark Up + Mover Favorito acima + + + + Move Bookmark Down + Mover Favorito abaixo + + + + Rename Bookmark + Renomear Favorito + + + + Remove Bookmark + Remover Favorito + + + + + Unmount + Desmontar + + + + Mount + Montar + + + + Eject + Ejetar + + + + Show All Entries + Mostrar todas as entradas + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + Tipo: %1 +Tamanho: %2 +Modificado: %3 + + + + + Type: %1 +Modified: %2 + Tipo: %1 +Modificado: %2 + + + + &Overwrite + S&obrescrever + + + + &Rename + &Renomear + + + + Fm::SidePane + + + Places + Locais + + + + Directory Tree + Árvore de diretórios + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + Não é possível restaurar o arquivo '%s': destino de origem desconhecido + + + + MountOperationPasswordDialog + + + Mount + Montar + + + + Connect &anonymously + Conectar &anonimamente + + + + Connect as u&ser: + Conectar como u&suário: + + + + &Username: + &Usuário: + + + + &Password: + &Senha: + + + + &Domain: + &Domínio: + + + + Forget password &immediately + Esquecer senha &imediatamente + + + + Remember password until you &logout + Lembrar senha até o final da sua &sessão + + + + Remember &forever + Lembrar para s&empre + + + + QObject + + + Rename File + Renomear arquivo + + + + Please enter a new name: + Por favor, insira um novo nome: + + + + + + + + Error + Erro + + + + Create Folder + Criar pasta + + + + Create File + Criar arquivo + + + + Please enter a new file name: + Por favor, insira um novo nome do arquivo: + + + + New text file + Novo arquivo de texto + + + + Please enter a new folder name: + Por favor, insira um novo nome da pasta: + + + + New folder + Nova pasta + + + + Enter a name for the new %1: + Insira um nome para o novo %1: + + + + Custom Icon Error + Erro no Ícone Personalizado + + + + The path is not mounted. + O destino não está montado. + + + + Invalid desktop entry file: '%1' + Arquivo de entrada da área de trabalho inválido: '%1' + + + + No default application is set to launch '%1' + Nenhum aplicativo padrão foi configurado para iniciar '%1' + + + + Cannot set working directory to '%1': %2 + Não é possível configurar o diretório de trabalho para '%1': %2 + + + + Identifier: + Identificação: + + + + RenameDialog + + + Confirm to replace files + Confirme para substituir os arquivos + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + <html><head/><body><p><span style=" font-weight:600;">Já existe um arquivo com o mesmo nome neste local.</span></p><p>Deseja substituir o arquivo existente?</p></body></html> + + + + dest + destino + + + + with the following file? + com o seguinte arquivo? + + + + src file info + informações do arquivo de origem + + + + dest file info + informações do arquivo de destino + + + + src + origem + + + + &File name: + &Nome do arquivo: + + + + Apply this option to all existing files + Aplicar esta opção a todos os arquivos existentes + + + + SearchDialog + + + Search Files + Pesquisar por arquivos + + + + Name/Location + Nome/Localização + + + + File Name Patterns: + Padrões de nome de arquivos: + + + + * + + + + + Case insensitive + Não diferenciar Maiúsculas/minúsculas + + + + Use regular expression + Utilizar expressão regular + + + + Places to Search: + Locais a pesquisar: + + + + &Add + &Adicionar + + + + &Remove + &Remover + + + + Search in sub directories + Pesquisar nos subdiretórios + + + + Search for hidden files + Pesquisar por arquivos ocultos + + + + File Type + Tipo de arquivo + + + + Only search for files of following types: + Pesquisar somente os seguintes tipos de arquivos: + + + + Text files + Arquivos de texto + + + + Image files + Arquivos de imagem + + + + Audio files + Arquivos de áudio + + + + Video files + Arquivos de vídeo + + + + Documents + Documentos + + + + Folders + Pastas + + + + Content + Conteúdo + + + + File contains: + Arquivo contém: + + + + Case insensiti&ve + &Não diferenciar Maiúsculas/minúsculas + + + + &Use regular expression + &Utilizar expressão regular + + + + Properties + Propriedades + + + + File Size: + Tamanho do arquivo: + + + + Larger than: + Maior que: + + + + + Bytes + + + + + + KiB + + + + + + MiB + + + + + + GiB + + + + + Smaller than: + Menor que: + + + + Last Modified Time: + Data da última modificação: + + + + Earlier than: + Anterior a: + + + + Later than: + Posterior a: + + + diff --git a/src/translations/libfm-qt_ru.ts b/src/translations/libfm-qt_ru.ts new file mode 100644 index 0000000..947dbcb --- /dev/null +++ b/src/translations/libfm-qt_ru.ts @@ -0,0 +1,1560 @@ + + + + + AppChooserDialog + + + Choose an Application + Выбрать приложение + + + + Installed Applications + Установленные приложения + + + + Custom Command + Своя команда + + + + Command line to execute: + Выполняемая командная строка: + + + + Application name: + Имя приложения: + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + <b>Эти спец. коды могут быть использованы в командной строке:</b> +<ul> +<li><b>%f</b>: Означает имя одного файла</li> +<li><b>%F</b>: Означает имена нескольких файлов</li> +<li><b>%u</b>: Означает один URI файла</li> +<li><b>%U</b>: Означает несколько URI файлов</li> +</ul> + + + + Keep terminal window open after command execution + Не закрывать окно терминала после выполнения команды + + + + Execute in terminal emulator + Запустить в эмуляторе терминала + + + + Set selected application as default action of this file type + Использовать выбранное приложение как действие по умолчанию для файлов этого типа + + + + EditBookmarksDialog + + + Edit Bookmarks + Изменить закладки + + + + Name + Имя + + + + Location + Расположение + + + + &Add Item + &Добавить элемент + + + + &Remove Item + &Удалить элемент + + + + Use drag and drop to reorder the items + Перетаскивайте элементы мышкой, чтобы изменить их порядок + + + + ExecFileDialog + + + Execute file + Выполнить файл + + + + &Open + &Открыть + + + + E&xecute + В&ыполнить + + + + Execute in &Terminal + Выполнить в &терминале + + + + Cancel + Отмена + + + + FileDialog + + + Location: + Расположение: + + + + File name: + Имя файла: + + + + File type: + Тип файла: + + + + FileOperationDialog + + + Destination: + Назначение: + + + + Processing: + Обработка: + + + + Preparing... + Подготовка... + + + + Progress + Прогресс + + + + Time remaining: + Времени осталось: + + + + Files processed: + Обработано файлов: + + + + FilePropsDialog + + + File Properties + Свойства файла + + + + General + Общие + + + + Location: + Расположение: + + + + File type: + Тип файла: + + + + MIME type: + Тип Mime: + + + + File size: + Размер файла: + + + + On-disk size: + Размер на диске: + + + + Last modified: + Последнее изменение: + + + + Link target: + Цель ссылки: + + + + Open With: + Открыть с помощью: + + + + Last accessed: + Последний доступ: + + + + Contains: + Содержимое: + + + + Device Usage: + Свободно на диске: + + + + Permissions + Разрешения + + + + Ownership + Владение + + + + + + Group: + Группа: + + + + + + Owner: + Владелец: + + + + Access Control + Контроль доступа + + + + + Other: + Остальные: + + + + Make the file executable + Сделать файл исполняемым + + + + + + Read + Чтение + + + + + + Write + Запись + + + + + + Execute + Выполнение + + + + Sticky + + + + + SetUID + + + + + SetGID + + + + + Advanced Mode + Расширенный режим + + + + Fm::AppChooserComboBox + + + Customize + Настроить + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + Выберите приложение, чтобы открыть "%1" файлов + + + + Fm::CreateNewMenu + + + Folder + Папку + + + + Blank File + Пустой файл + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + Указанный каталог '%1' некорректен + + + + Fm::DirTreeModel + + + Loading... + Загрузка... + + + + + + <No sub folders> + <Нет подпапок> + + + + Fm::DirTreeView + + + Open in New T&ab + Открыть в новой вкл&адке + + + + Open in New Win&dow + Открыть в новом о&кне + + + + Open in Termina&l + Открыть в термина&ле + + + + Fm::DndActionMenu + + + Copy here + Копировать сюда + + + + Move here + Переместить сюда + + + + Create symlink here + Создать здесь ссылку + + + + Cancel + Отмена + + + + Fm::EditBookmarksDialog + + + New bookmark + Новая закладка + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + Файл «%1» является .desktop-файлом. +Что вы хотите с ним сделать? + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + Этот текстовый файл '%1' похож на исполняемый скрипт. +Что вы хотите с ним сделать? + + + + This file '%1' is executable. Do you want to execute it? + Файл '%1' является исполняемым. Вы хотите запустить его? + + + + Fm::FileDialog + + + Go Back + Назад + + + + Alt+Left + Go Back + Alt+Влево + + + + Go Forward + Вперед + + + + Alt+Right + Go Forward + Alt+Вправо + + + + Reload + Обновить + + + + F5 + Reload + F5 + + + + Create Folder + Создать папку + + + + Icon View + В виде значков + + + + Thumbnail View + В виде эскизов + + + + Compact View + Компактно + + + + Detailed List View + В виде подробного списка + + + + + Error + Ошибка + + + + Please select a file + Выберите файл + + + + %1 already exists. +Do you want to replace it? + %1 уже существует. +Вы хотите заменить его? + + + + Path "%1" does not exist + Путь «%1» не существует + + + + "%1" is not a directory + «%1» не является каталогом + + + + "%1" is not a file + «%1» не является файлом + + + + + &Open + &Открыть + + + + + &Save + &Сохранить + + + + All Files (*) + Все файлы (*) + + + + Fm::FileDialogHelper + + + Open File + Открыть файл + + + + Save File + Сохранить файл + + + + Fm::FileMenu + + + Open + Открыть + + + + Open With... + Открыть с помощью... + + + + Other Applications + Другие приложения + + + + Create &New + &Создать + + + + &Restore + &Восстановить + + + + Cut + Вырезать + + + + Copy + Копировать + + + + Paste + Вставить + + + + + &Move to Trash + &Переместить в корзину + + + + Rename + Переименовать + + + + Extract to... + Распаковать в... + + + + Extract Here + Распаковать здесь + + + + Compress + Сжать + + + + Properties + Свойства + + + + Trust selected executables + Доверять выбранным исполняемым файлам + + + + Trust this executable + Доверять этому исполняемому файлу + + + + Output + Вывод + + + + &Delete + &Удалить + + + + Fm::FileOperation + + + Error + Ошибка + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + Некоторые файлы не могут быть помещены в корзину, поскольку файловая система не поддерживает эту операцию. +Вы хотите удалить их вместо перемещения? + + + + + Confirm + Подтвердить + + + + Do you want to delete the selected files? + Вы действительно хотите удалить выбранные файлы? + + + + Do you want to move the selected files to trash can? + Вы действительно хотите переместить выбранные файлы в корзину? + + + + Fm::FileOperationDialog + + + Move files + Перемещение файлов + + + + Moving the following files to destination folder: + Перемещение следующих файлов в папку назначения: + + + + Copy Files + Копирование файлов + + + + Copying the following files to destination folder: + Копирование следующих файлов в папку назначения: + + + + Trash Files + Перемещение файлов в корзину + + + + Moving the following files to trash can: + Перемещение следующих файлов в корзину: + + + + Delete Files + Удаление файлов + + + + Deleting the following files: + Удаление следующих файлов: + + + + Create Symlinks + Создание ссылок + + + + Creating symlinks for the following files: + Создание ссылок на следующие файлы: + + + + Change Attributes + Изменение атрибутов + + + + Changing attributes of the following files: + Изменение атрибутов следующих файлов: + + + + Restore Trashed Files + Восстановление файлов из корзины + + + + Restoring the following files from trash can: + Восстановление следующих файлов из корзины: + + + + + Error + Ошибка + + + + Fm::FilePropsDialog + + + View folder content + Просмотр содержимого папки + + + + View and modify folder content + Просмотр и изменение содержимого папки + + + + Read + Чтение + + + + Read and write + Чтение и запись + + + + Forbidden + Запрещено + + + + Files of different types + Файлы разного типа + + + + Multiple Files + Несколько файлов + + + + %p% used + %p% занято + + + + %1 Free of %2 + %1 свободно из %2 + + + + no file + нет файлов + + + + one file + 1 файл + + + + %1 files + Файлов: %1 + + + + Select an icon + Выберите значок + + + + Images (*.png *.xpm *.svg *.svgz ) + Изображения (*.png *.xpm *.svg *.svgz ) + + + + Apply changes + Применить изменения + + + + Do you want to recursively apply these changes to all files and sub-folders? + Вы хотите применить изменения рекурсивно ко всем файлам и подпапкам? + + + + Fm::FileSearchDialog + + + Error + Ошибка + + + + You should add at least one directory to search. + Вы должны добавить не менее одного каталога для поиска. + + + + Select a folder + Выберите папку + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + Файловая система не поддерживает создание ссылок + + + + Fm::FolderMenu + + + Create &New + &Создать + + + + &Paste + &Вставить + + + + Select &All + Выбрать &всё + + + + Invert Selection + Инвертировать выделение + + + + Sorting + Сортировка + + + + Show Hidden + Показать скрытые + + + + Folder Pr&operties + &Свойства папки + + + + Output + Вывод + + + + By File Name + По имени + + + + By Modification Time + По времени изменения + + + + By File Size + По размеру + + + + By File Type + По типу + + + + By File Owner + По владельцу + + + + Ascending + Возрастающая + + + + Descending + Убывающая + + + + Folder First + Сначала папки + + + + Case Sensitive + Учитывать регистр + + + + Fm::FolderModel + + + Name + Имя + + + + Type + Тип + + + + Size + Размер + + + + Modified + Изменён + + + + Owner + Владелец + + + + Group + Группа + + + + Fm::FontButton + + + Bold + Жирный + + + + Italic + Курсив + + + + Fm::MountOperationPasswordDialog + + + &Connect + &Соединить + + + + Fm::PathBar + + + &Edit Path + &Редактировать путь + + + + &Copy Path + &Копировать путь + + + + Fm::PlacesModel + + + Places + Места + + + + Desktop + Рабочий стол + + + + Computer + Компьютер + + + + Applications + Приложения + + + + Network + Сеть + + + + Devices + Устройства + + + + Bookmarks + Закладки + + + + Trash + Корзина + + + + Fm::PlacesView + + + Empty Trash + Очистить корзину + + + + Open in New Tab + Открыть в новой вкладке + + + + Open in New Window + Открыть в новом окне + + + + + Hide + Скрыть + + + + Move Bookmark Up + Сдвинуть закладку вверх + + + + Move Bookmark Down + Сдвинуть закладку вниз + + + + Rename Bookmark + Переименовать закладку + + + + Remove Bookmark + Удалить закладку + + + + + Unmount + Отключить + + + + Mount + Подключить + + + + Eject + Извлечь + + + + Show All Entries + Показать все элементы + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + Тип: %1 +Размер: %2 +Изменён: %3 + + + + + Type: %1 +Modified: %2 + Тип: %1 +Изменён: %2 + + + + &Overwrite + &Перезаписать + + + + &Rename + &Переименовать + + + + Fm::SidePane + + + Places + Места + + + + Directory Tree + Дерево каталогов + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + Не удаётся восстановить файл «%s»: не найден первоначальный путь + + + + MountOperationPasswordDialog + + + Mount + Подключить + + + + Connect &anonymously + Подключиться &анонимно + + + + Connect as u&ser: + Подключиться как &пользователь: + + + + &Username: + &Имя пользователя: + + + + &Password: + &Пароль: + + + + &Domain: + &Домен: + + + + Forget password &immediately + Забыть пароль &сразу + + + + Remember password until you &logout + Запомнить пароль пока вы не &вышли + + + + Remember &forever + Запомнить &навсегда + + + + QObject + + + + + + + Error + Ошибка + + + + Rename File + Переименовать файл + + + + Please enter a new name: + Введите новое имя: + + + + Create Folder + Создать папку + + + + Please enter a new file name: + Введите имя нового файла: + + + + New text file + Новый текстовый файл + + + + Please enter a new folder name: + Введите имя новой папки: + + + + New folder + Новая папка + + + + Enter a name for the new %1: + Введите имя для нового %1: + + + + Create File + Создать файл + + + + Custom Icon Error + Ошибка пользовательского значка + + + + The path is not mounted. + Путь не примонтирован. + + + + Invalid desktop entry file: '%1' + Неверный файл .desktop: «%1» + + + + No default application is set to launch '%1' + Не задано приложение по умолчанию для открытия «%1» + + + + Cannot set working directory to '%1': %2 + Не удалось задать рабочий каталог «%1»: %2 + + + + Identifier: + Устройство: + + + + RenameDialog + + + Confirm to replace files + Подтвердить замену файлов + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + <html><head/><body><p><span style=" font-weight:600;">В этом месте назначения уже есть файл с таким именем.</span></p><p>Вы хотите заменить существующий файл?</p></body></html> + + + + dest + + + + + with the following file? + следующим файлом? + + + + src file info + + + + + dest file info + + + + + src + + + + + &File name: + &Имя файла: + + + + Apply this option to all existing files + Запомнить выбор для всех уже существующих файлов + + + + SearchDialog + + + Search Files + Искать файлы + + + + Name/Location + Имя/Расположение + + + + File Name Patterns: + Шаблоны имени файла: + + + + * + + + + + Case insensitive + Игнорировать регистр + + + + Use regular expression + Использовать регулярные выражения + + + + Places to Search: + Места для поиска: + + + + &Add + &Добавить + + + + &Remove + &Удалить + + + + Search in sub directories + Искать в подпапках + + + + Search for hidden files + Искать скрытые файлы + + + + File Type + Тип файла + + + + Only search for files of following types: + Искать файлы только следующих типов: + + + + Text files + Текстовые файлы + + + + Image files + Изображения + + + + Audio files + Аудиофайлы + + + + Video files + Видеофайлы + + + + Documents + Документы + + + + Folders + Папки + + + + Content + Содержание + + + + File contains: + Файл содержит: + + + + Case insensiti&ve + Игнорировать регистр + + + + &Use regular expression + &Использовать регулярные выражения + + + + Properties + Свойства + + + + File Size: + Размер файла: + + + + Larger than: + Больше, чем: + + + + + Bytes + Байт + + + + + KiB + КиБ + + + + + MiB + МиБ + + + + + GiB + ГиБ + + + + Smaller than: + Меньше, чем: + + + + Last Modified Time: + Время последнего изменения: + + + + Earlier than: + Раньше, чем: + + + + Later than: + Позже, чем: + + + diff --git a/src/translations/libfm-qt_tr.ts b/src/translations/libfm-qt_tr.ts new file mode 100644 index 0000000..b5b85d4 --- /dev/null +++ b/src/translations/libfm-qt_tr.ts @@ -0,0 +1,1560 @@ + + + + + AppChooserDialog + + + Choose an Application + Bir Uygulama Seç + + + + Installed Applications + Yüklenmiş Uygulamalar + + + + Custom Command + Özel Komut + + + + Command line to execute: + Çalıştırılacak komut satırı: + + + + Application name: + Uygulama Adı: + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + <b>Bu özel kodlar komut satırında kullanılabilir:</b> +<ul> +<li><b>%f</b>: Tek dosya adını temsil eder</li> +<li><b>%F</b>: Birden fazla dosya adını temsil eder</li> +<li><b>%u</b>: Tek dosyanın yolunu temsil eder</li> +<li><b>%U</b>: Birden fazla dosyanın yolunu temsil eder</li> +</ul> + + + + Keep terminal window open after command execution + Komutu çalıştırdıktan sonra uçbirim penceresini açık tut + + + + Execute in terminal emulator + Uçbirimde çalıştır + + + + Set selected application as default action of this file type + Seçilen uygulamayı bu dosya türü için öntanımlı olarak ayarla + + + + EditBookmarksDialog + + + Edit Bookmarks + Yer İmlerini Düzenle + + + + Name + İsim + + + + Location + Konum + + + + &Add Item + &Öge Ekle + + + + &Remove Item + Ögeyi &Sil + + + + Use drag and drop to reorder the items + Ögeleri yeniden sıralamak için sürükle bırak + + + + ExecFileDialog + + + Execute file + Dosyayı çalıştır + + + + &Open + &Aç + + + + E&xecute + Ç&alıştır + + + + Execute in &Terminal + Uçbirimde çalış&tır + + + + Cancel + İptal + + + + FileDialog + + + Location: + Konum: + + + + File name: + Dosya adı: + + + + File type: + Dosya tipi: + + + + FileOperationDialog + + + Destination: + Hedef: + + + + Processing: + İşleniyor: + + + + Preparing... + Hazırlanıyor... + + + + Progress + İlerleme + + + + Time remaining: + Kalan Süre: + + + + Files processed: + İşlenen dosyalar: + + + + FilePropsDialog + + + File Properties + Dosya Özellikleri + + + + General + Genel + + + + Location: + Konum: + + + + File type: + Dosya türü: + + + + MIME type: + Mime Türü: + + + + File size: + Dosya boyutu: + + + + On-disk size: + Diskteki boyutu: + + + + Last modified: + Son değiştirilme: + + + + Link target: + Bağlantı hedefi: + + + + Open With: + Birlikte Aç: + + + + Last accessed: + Son erişim: + + + + Contains: + + + + + Device Usage: + Aygıt Kullanımı: + + + + Permissions + İzinler + + + + Ownership + Sahiplik + + + + + + Group: + Grup: + + + + + + Owner: + Sahibi: + + + + Access Control + Erişim Kontrolü + + + + + Other: + Diğer: + + + + Make the file executable + Dosyayı çalıştırılabilir yap + + + + + + Read + Okuyabilir + + + + + + Write + Yazabilir + + + + + + Execute + Çalıştırabilir + + + + Sticky + Yapışkan + + + + SetUID + SetUID + + + + SetGID + SetGID + + + + Advanced Mode + Gelişmiş Kip + + + + Fm::AppChooserComboBox + + + Customize + Özelleştir + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + "%1" dosyalarını açmak için bir uygulama seçin + + + + Fm::CreateNewMenu + + + Folder + Dizin + + + + Blank File + Boş Dosya + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + Belirtilen dizin '%1' geçerli değil + + + + Fm::DirTreeModel + + + Loading... + Yükleniyor... + + + + + + <No sub folders> + <Alt dizin yok> + + + + Fm::DirTreeView + + + Open in New T&ab + Yeni Sekme&de Aç + + + + Open in New Win&dow + Yeni &Pencerede Aç + + + + Open in Termina&l + &Uçbirimde Aç + + + + Fm::DndActionMenu + + + Copy here + Buraya kopyala + + + + Move here + Buraya taşı + + + + Create symlink here + Buraya sembolik bağlantı oluştur + + + + Cancel + İptal + + + + Fm::EditBookmarksDialog + + + New bookmark + Yeni yerimi + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + Bu dosya '%1' bir masaüstü dosyası gibi görünüyor. +Ne yapmak istersiniz? + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + Bu metin dosyası '%1' çalıştırılabilir bir betik gibi görünüyor. +Ne yapmak istersiniz? + + + + This file '%1' is executable. Do you want to execute it? + Bu dosya '%1' çalıştırılabilir. Çalıştırmak ister misiniz? + + + + Fm::FileDialog + + + Go Back + Geri + + + + Alt+Left + Go Back + Alt+Sol Ok + + + + Go Forward + İleri + + + + Alt+Right + Go Forward + Alt+Sağ Ok + + + + Reload + Yeniden Yükle + + + + F5 + Reload + F5 + + + + Create Folder + Dizin Oluştur + + + + Icon View + Simge Görünümü + + + + Thumbnail View + Önizleme + + + + Compact View + Sıkışık Görünüm + + + + Detailed List View + Detaylı Liste Görünümü + + + + + Error + Hata + + + + Please select a file + Lütfen bir dosya seçin + + + + %1 already exists. +Do you want to replace it? + %1 halihazırda var. +Değiştirmek istiyor musunuz? + + + + Path "%1" does not exist + "%1" yolu mevcut değil + + + + "%1" is not a directory + "%1" bir dizin değil + + + + "%1" is not a file + "%1" bir dosya değil + + + + + &Open + &Aç + + + + + &Save + &Kaydet + + + + All Files (*) + Tüm Dosyalar (*) + + + + Fm::FileDialogHelper + + + Open File + Dosyayı Aç + + + + Save File + Dosyayı Kaydet + + + + Fm::FileMenu + + + Open + Aç + + + + Open With... + Birlikte Aç... + + + + Other Applications + Diğer Uygulamalar + + + + Create &New + Ye&ni Oluştur + + + + &Restore + &Geri Yükle + + + + Cut + Kes + + + + Copy + Kopyala + + + + Paste + Yapıştır + + + + + &Move to Trash + &Çöp Kutusuna Taşı + + + + Rename + Yeniden Adlandır + + + + Extract to... + Buraya çıkart... + + + + Extract Here + Buraya çıkart + + + + Compress + Sıkıştır + + + + Properties + Özellikler + + + + Trust selected executables + Seçilen çalıştırılabilen dosyalara güven + + + + Trust this executable + Bu çalıştırılabilen dosyaya güven + + + + Output + Çıktı + + + + &Delete + &Sil + + + + Fm::FileOperation + + + Error + Hata + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + Bazı dosyalar çöp kutusuna taşınamıyorlar.Çünkü dosya sistemi bu işlemi desteklemiyor. +Onun yerine silmek ister misiniz? + + + + + Confirm + Doğrula + + + + Do you want to delete the selected files? + Seçilen dosyaları silmek istiyor musunuz? + + + + Do you want to move the selected files to trash can? + Seçilen dosyaları çöp kutusuna taşımak istiyor musunuz? + + + + Fm::FileOperationDialog + + + Move files + Dosyaları taşı + + + + Moving the following files to destination folder: + Aşağıdaki dosyalar hedef dizine taşınıyor: + + + + Copy Files + Dosyaları Kopyala + + + + Copying the following files to destination folder: + Aşağıdaki dosyalar hedef dizine kopyalanıyor: + + + + Trash Files + Dosyaları Çöpe Gönder + + + + Moving the following files to trash can: + Aşağıdaki dosyalar çöp kutusuna taşınıyor: + + + + Delete Files + Dosyaları Sil + + + + Deleting the following files: + Aşağıdaki dosyalar siliniyor: + + + + Create Symlinks + Sembolik Bağlantı Oluştur + + + + Creating symlinks for the following files: + Aşağıdaki dosyalar için sembolik bağlantılar oluşturuluyor: + + + + Change Attributes + Öznitelikleri Değiştir + + + + Changing attributes of the following files: + Aşağıdaki dosyaların öznitelikleri değişiyor: + + + + Restore Trashed Files + Çöp Kutusundaki Dosyaları Geri Yükle + + + + Restoring the following files from trash can: + Aşağıdaki dosyalar çöp kutusundan geri yükleniyor: + + + + + Error + Hata + + + + Fm::FilePropsDialog + + + View folder content + Dizin içeriğini görebilir + + + + View and modify folder content + Dizin içeriğini görebilir ve değiştirebilir + + + + Read + Okuyabilir + + + + Read and write + Okuyabilir ve Yazabilir + + + + Forbidden + İzin yok + + + + Files of different types + Farklı türlerin dosyaları + + + + Multiple Files + Çoklu Dosyalar + + + + %p% used + kullanılan %p% + + + + %1 Free of %2 + %2 'nin %1 + + + + no file + dosya yok + + + + one file + bir dosya + + + + %1 files + %1 dosya + + + + Select an icon + Bir simge seçin + + + + Images (*.png *.xpm *.svg *.svgz ) + Resimler (*.png *.xpm *.svg *.svgz ) + + + + Apply changes + Değişiklikleri uygula + + + + Do you want to recursively apply these changes to all files and sub-folders? + Bu değişiklikleri tüm dosya ve alt dizinlere uygulamak istiyor musunuz? + + + + Fm::FileSearchDialog + + + Error + Hata + + + + You should add at least one directory to search. + Arama yapmak için en az bir dizin eklemelisiniz. + + + + Select a folder + Bir dizin seç + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + Yerel olmayan dosya sisteminde bağlantı oluşturulamıyor + + + + Fm::FolderMenu + + + Create &New + Ye&ni Oluştur + + + + &Paste + &Yapıştır + + + + Select &All + Tümünü &Seç + + + + Invert Selection + Seçimi Tersine Çevir + + + + Sorting + Sıralama + + + + Show Hidden + Gizli Dosyaları Göster + + + + Folder Pr&operties + Dizin Ö&zellikleri + + + + Output + Çıktı + + + + By File Name + Dosya Adına Göre + + + + By Modification Time + Değiştirme Tarihine Göre + + + + By File Size + Dosya Boyutuna Göre + + + + By File Type + Dosya Türüne Göre + + + + By File Owner + Sahibine Göre + + + + Ascending + Artan + + + + Descending + Azalan + + + + Folder First + Önce Dizin + + + + Case Sensitive + Büyük Küçük Harf Duyarlı + + + + Fm::FolderModel + + + Name + İsim + + + + Type + Tür + + + + Size + Boyut + + + + Modified + Değiştirildi + + + + Owner + Sahip + + + + Group + Grup + + + + Fm::FontButton + + + Bold + Kalın + + + + Italic + Yatık + + + + Fm::MountOperationPasswordDialog + + + &Connect + &Bağlan + + + + Fm::PathBar + + + &Edit Path + Yolu Düz&enle + + + + &Copy Path + Yolu &Kopyala + + + + Fm::PlacesModel + + + Places + Konumlar + + + + Desktop + Masaüstü + + + + Computer + Bilgisayar + + + + Applications + Uygulamalar + + + + Network + Ağ + + + + Devices + Aygıtlar + + + + Bookmarks + Yer İmleri + + + + Trash + Çöp + + + + Fm::PlacesView + + + Open in New Tab + Yeni Sekmede Aç + + + + Open in New Window + Yeni Pencerede Aç + + + + Empty Trash + Çöp Kutusunu Boşalt + + + + + Hide + Gizle + + + + Move Bookmark Up + Yer İmini Yukarı Taşı + + + + Move Bookmark Down + Yer İmini Aşağı Taşı + + + + Rename Bookmark + Yer İmini Yeniden Adlandır + + + + Remove Bookmark + Yer İmini Sil + + + + + Unmount + Ayır + + + + Mount + Bağla + + + + Eject + Çıkart + + + + Show All Entries + Tüm Girdileri Göster + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + Tür: %1 +Boyut: %2 +Değiştirildi: %3 + + + + + Type: %1 +Modified: %2 + Tür: %1 +Değiştirild: %2 + + + + &Overwrite + &Üzerine Yaz + + + + &Rename + &Yeniden Adlandır + + + + Fm::SidePane + + + Places + Konumlar + + + + Directory Tree + Dizin Ağacı + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + '%s' dosyası çöpten alınamıyor: dosyanın ilk yolu bilinmiyor + + + + MountOperationPasswordDialog + + + Mount + Bağla + + + + Connect &anonymously + Anonim ol&arak bağlan + + + + Connect as u&ser: + K&ullanıcı olarak bağlan: + + + + &Username: + K&ullanıcı adı: + + + + &Password: + &Parola: + + + + &Domain: + &Alan: + + + + Forget password &immediately + Parolayı der&hal unut + + + + Remember password until you &logout + Parolayı &çıkıncaya kadar hatırla + + + + Remember &forever + Her &zaman hatırla + + + + QObject + + + Rename File + Dosyayı Yeniden Adlandır + + + + Please enter a new name: + Lütfen yeni bir isim girin: + + + + + + + + Error + Hata + + + + Create Folder + Dizin Oluştur + + + + Create File + Dosya Oluştur + + + + Please enter a new file name: + Lütfen yeni bir dosya adı girin: + + + + New text file + Yeni metin dosyası + + + + Please enter a new folder name: + Lütfen yeni bir dizin adı girin: + + + + New folder + Yeni dizin + + + + Enter a name for the new %1: + Yeni %1 için bir isim girin: + + + + Custom Icon Error + Özel Simge Hatası + + + + The path is not mounted. + Yol bağlanamadı. + + + + Invalid desktop entry file: '%1' + Geçersiz masaüstü dosyası: '%1' + + + + No default application is set to launch '%1' + '%1' çalıştırmak için öntanımlı uygulama yok + + + + Cannot set working directory to '%1': %2 + Çalışma dizini '%1' olarak ayarlanamıyor: %2 + + + + Identifier: + Tanımlayıcı: + + + + RenameDialog + + + Confirm to replace files + Değiştirilecek dosyaları onaylayın + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + <html><head/><body><p><span style=" font-weight:600;">Bu konumda aynı isimli bir dosya zaten var.</span></p><p>Var olan dosyayı değiştirmek istiyor musunuz?</p></body></html> + + + + dest + hedef + + + + with the following file? + aşağıdaki dosya ile? + + + + src file info + kaynak dosya bilgisi + + + + dest file info + hedef dosya bilgisi + + + + src + kaynak + + + + &File name: + &Dosya adı: + + + + Apply this option to all existing files + Bu seçeneği tüm var olan dosyalara uygula + + + + SearchDialog + + + Search Files + Dosya Ara + + + + Name/Location + İsim/Konum + + + + File Name Patterns: + Dosya Adı Modelleri: + + + + * + * + + + + Case insensitive + Büyük Küçük Harf Duyarlı + + + + Use regular expression + Düzenli ifade kullan + + + + Places to Search: + Aranacak Yerler: + + + + &Add + &Ekle + + + + &Remove + &Sil + + + + Search in sub directories + Alt dizinlerde ara + + + + Search for hidden files + Gizli dosyalarda ara + + + + File Type + Dosya Türü + + + + Only search for files of following types: + Sadece aşağıdaki dosya türleri için arama yap: + + + + Text files + Metin dosyaları + + + + Image files + Resim dosyaları + + + + Audio files + Ses dosyaları + + + + Video files + Video dosyaları + + + + Documents + Belgeler + + + + Folders + Dizinler + + + + Content + İçerik + + + + File contains: + Dosya içeriyor: + + + + Case insensiti&ve + Büyük Küçük &Harf Duyarlı + + + + &Use regular expression + Düzenli ifade k&ullan + + + + Properties + Özellikler + + + + File Size: + Dosya Boyutu: + + + + Larger than: + Bundan daha büyük: + + + + + Bytes + Bit + + + + + KiB + KBit + + + + + MiB + MB + + + + + GiB + GB + + + + Smaller than: + Bundan daha küçük: + + + + Last Modified Time: + Son Değiştirilme Zamanı: + + + + Earlier than: + Bundan önce: + + + + Later than: + Bundan sonra: + + + diff --git a/src/translations/libfm-qt_uk.ts b/src/translations/libfm-qt_uk.ts new file mode 100644 index 0000000..2046467 --- /dev/null +++ b/src/translations/libfm-qt_uk.ts @@ -0,0 +1,1558 @@ + + + + + AppChooserDialog + + + Choose an Application + Вибір програми + + + + Installed Applications + Встановлені програми + + + + Custom Command + Власна команда + + + + Command line to execute: + Командна стрічка для виконання: + + + + Application name: + Назва програми: + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + <b>Ці спеціальні коди можна використати в командній стрічці:</b> +<ul> +<li><b>%f</b>: вказує назву одного файлу</li> +<li><b>%F</b>: вказує назви багатьох файлів</li> +<li><b>%u</b>: вказує URI на один файл</li> +<li><b>%U</b>: вказує на багато URI</li> +</ul> + + + + Keep terminal window open after command execution + Залишати термінал відкритим після виконання команди + + + + Execute in terminal emulator + Виконати в емуляторі терміналу + + + + Set selected application as default action of this file type + Встановити вибрану програму як типову для цього типу файлу + + + + EditBookmarksDialog + + + Edit Bookmarks + Редагувати закладки + + + + Name + Назва + + + + Location + Розташування + + + + &Add Item + &Додати закладку + + + + &Remove Item + &Вилучити закладку + + + + Use drag and drop to reorder the items + Використайте захоплення і переміщення для зміни порядку + + + + ExecFileDialog + + + Execute file + Виконати файл + + + + &Open + &Відкрити + + + + E&xecute + В&иконати + + + + Execute in &Terminal + Виконати в &терміналі + + + + Cancel + Припинити + + + + FileDialog + + + Location: + Розташування: + + + + File name: + + + + + File type: + Тип файлу: + + + + FileOperationDialog + + + Destination: + Призначення: + + + + Processing: + Обробка: + + + + Preparing... + Підготовка... + + + + Progress + Прогрес + + + + Time remaining: + Залишилося часу: + + + + Files processed: + + + + + FilePropsDialog + + + File Properties + Властивості файлу + + + + General + Загальне + + + + Location: + Розташування: + + + + File type: + Тип файлу: + + + + MIME type: + Тип MIME: + + + + File size: + Розмір файлу: + + + + On-disk size: + Розмір на диску: + + + + Last modified: + Востаннє змінено: + + + + Link target: + Посилання: + + + + Open With: + Відкрити з: + + + + Last accessed: + Останній доступ: + + + + Contains: + + + + + Device Usage: + + + + + Permissions + Дозволи + + + + Ownership + Власність + + + + + + Group: + Група: + + + + + + Owner: + Власник: + + + + Access Control + Контроль доступу + + + + + Other: + Інше: + + + + Make the file executable + Зробити файл виконувальним + + + + + + Read + Читати + + + + + + Write + Записувати + + + + + + Execute + Виконувати + + + + Sticky + До уваги + + + + SetUID + ВстUID + + + + SetGID + ВстGID + + + + Advanced Mode + Розширений режим + + + + Fm::AppChooserComboBox + + + Customize + Змінити + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + Вибір програми для відкриття "%1" файлів + + + + Fm::CreateNewMenu + + + Folder + Тека + + + + Blank File + Порожній файл + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + + + + + Fm::DirTreeModel + + + Loading... + Завантаження... + + + + + + <No sub folders> + <Немає підтек> + + + + Fm::DirTreeView + + + Open in New T&ab + Відкрити в новій в&кладці + + + + Open in New Win&dow + Відкрити в новому вік&ні + + + + Open in Termina&l + Відкрити в терміна&лі + + + + Fm::DndActionMenu + + + Copy here + Скопіювати тут + + + + Move here + Перемістити тут + + + + Create symlink here + Створити посилання тут + + + + Cancel + Перервати + + + + Fm::EditBookmarksDialog + + + New bookmark + Нова закладка + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + Цей текстовий файл '%1' здається є виконувальним скриптом. +Що Ви бажаєте зробити з ним? + + + + This file '%1' is executable. Do you want to execute it? + Цей файл '%1' є виконувальним. Ви бажаєте його виконати? + + + + Fm::FileDialog + + + Go Back + + + + + Alt+Left + Go Back + + + + + Go Forward + + + + + Alt+Right + Go Forward + + + + + Reload + + + + + F5 + Reload + + + + + Create Folder + Створити теку + + + + Icon View + + + + + Thumbnail View + + + + + Compact View + + + + + Detailed List View + + + + + + Error + Помилка + + + + Please select a file + + + + + %1 already exists. +Do you want to replace it? + + + + + Path "%1" does not exist + + + + + "%1" is not a directory + + + + + "%1" is not a file + + + + + + &Open + &Відкрити + + + + + &Save + + + + + All Files (*) + + + + + Fm::FileDialogHelper + + + Open File + + + + + Save File + + + + + Fm::FileMenu + + + Open + Відкрити + + + + Open With... + Відкрити з... + + + + Other Applications + Інші програми + + + + Create &New + Створити &новий + + + + &Restore + &Відновити + + + + Cut + Врізати + + + + Copy + Скопіювати + + + + Paste + Вставити + + + + + &Move to Trash + &Перемістити у смітник + + + + Rename + Перейменувати + + + + Extract to... + Розпакувати до... + + + + Extract Here + Розпакувати тут + + + + Compress + Стиснути + + + + Properties + Властивості + + + + Trust selected executables + + + + + Trust this executable + + + + + Output + Вивід + + + + &Delete + Ви&лучити + + + + Fm::FileOperation + + + Error + Помилка + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + Деякі файли неможливо перемістити до смітника, оскільки ця система не підтримує цю операцію. +Ви бажаєте вилучити їх у цьому випадку? + + + + + Confirm + Підтвердження + + + + Do you want to delete the selected files? + Ви бажаєте вилучити вибрані файли? + + + + Do you want to move the selected files to trash can? + Ви бажаєете перемістити вибрані файли до смітника? + + + + Fm::FileOperationDialog + + + Move files + Переміщення файлів + + + + Moving the following files to destination folder: + Переміщення наступних файлів до теки призначення: + + + + Copy Files + Копіювання файлів + + + + Copying the following files to destination folder: + Копіювання файлів до теки призначення: + + + + Trash Files + Переміщення файлів у смітник + + + + Moving the following files to trash can: + Переміщення наступних файлів до смітника: + + + + Delete Files + Вилучення файлів + + + + Deleting the following files: + Вилучення вибраних файлів: + + + + Create Symlinks + Створення символьних посилань + + + + Creating symlinks for the following files: + Створення символьних посилань для настпних файлів: + + + + Change Attributes + Зміна атрибутів + + + + Changing attributes of the following files: + Зміна атрибутів наступних файлів: + + + + Restore Trashed Files + Відновлення файлів з смітника + + + + Restoring the following files from trash can: + Відновлення наступних файлів з смітника: + + + + + Error + Помилка + + + + Fm::FilePropsDialog + + + View folder content + Перегляд вмісту теки + + + + View and modify folder content + Перегляд і зміна вмісту теки + + + + Read + Читання + + + + Read and write + Читання і запис + + + + Forbidden + Заборонено + + + + Files of different types + Файли різних типів + + + + Multiple Files + Багато файлів + + + + %p% used + + + + + %1 Free of %2 + + + + + no file + + + + + one file + + + + + %1 files + + + + + Select an icon + + + + + Images (*.png *.xpm *.svg *.svgz ) + + + + + Apply changes + Застосувати зміни + + + + Do you want to recursively apply these changes to all files and sub-folders? + Ви бажаєте рекурсивно застосувати ці зміни до всіх файлів і підтек? + + + + Fm::FileSearchDialog + + + Error + Помилка + + + + You should add at least one directory to search. + Ви повинні додати хоча б одну теку для пошуку. + + + + Select a folder + Вибір теки + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + + + + + Fm::FolderMenu + + + Create &New + Створити &новий + + + + &Paste + &Вставити + + + + Select &All + Вибрати в&се + + + + Invert Selection + Обернути зазначення + + + + Sorting + Сортування + + + + Show Hidden + Показати приховане + + + + Folder Pr&operties + Властив&ості теки + + + + Output + Вивід + + + + By File Name + По назві файлу + + + + By Modification Time + По даті зміни + + + + By File Size + По розміру файлу + + + + By File Type + По типу файлу + + + + By File Owner + По власнику файлу + + + + Ascending + В порядку зростання + + + + Descending + В порядку спадання + + + + Folder First + Теки спочатку + + + + Case Sensitive + Чутливі до регістру + + + + Fm::FolderModel + + + Name + Назва + + + + Type + Тип + + + + Size + Розмір + + + + Modified + Змінено + + + + Owner + Власник + + + + Group + + + + + Fm::FontButton + + + Bold + Жирний + + + + Italic + Курсив + + + + Fm::MountOperationPasswordDialog + + + &Connect + &З'єднання + + + + Fm::PathBar + + + &Edit Path + &Шлях редагування + + + + &Copy Path + &Шлях копіювання + + + + Fm::PlacesModel + + + Places + Місця + + + + Desktop + Стільниця + + + + Computer + Комп'ютер + + + + Applications + Програми + + + + Network + Сітка + + + + Devices + Пристрої + + + + Bookmarks + Закладки + + + + Trash + Смітник + + + + Fm::PlacesView + + + Open in New Tab + Відкрити в новій вкладці + + + + Open in New Window + Відкрити в новому вікні + + + + Empty Trash + Спорожнити смітник + + + + + Hide + + + + + Move Bookmark Up + Перенести закладку вгору + + + + Move Bookmark Down + Перенести закладку вниз + + + + Rename Bookmark + Перейменувати закладку + + + + Remove Bookmark + Вилучити закладку + + + + + Unmount + Відмонтувати + + + + Mount + Змонтувати + + + + Eject + Витягнути + + + + Show All Entries + + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + Тип: %1 +Розмір: %2 +Змінено: %3 + + + + + Type: %1 +Modified: %2 + Тип: %1 +Змінено: %2 + + + + &Overwrite + Пе&резаписати + + + + &Rename + Пер&ейменувати + + + + Fm::SidePane + + + Places + Місця + + + + Directory Tree + Дерево тек + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + + + + + MountOperationPasswordDialog + + + Mount + Монтування + + + + Connect &anonymously + З'єднати &анонімно + + + + Connect as u&ser: + З'єднати як к&ористувач: + + + + &Username: + &Користувач: + + + + &Password: + &Пароль: + + + + &Domain: + &Домен: + + + + Forget password &immediately + Забути пароль &відразу + + + + Remember password until you &logout + Пам'ятати пароль до ви&ходу + + + + Remember &forever + Запам'ятати назав&жди + + + + QObject + + + Rename File + Перейменувати файл + + + + Please enter a new name: + Будь ласка, введіть нову назву: + + + + + + + + Error + Помилка + + + + Create Folder + Створити теку + + + + Create File + Створити файл + + + + Please enter a new file name: + Будь ласка, введіть назву нового файлу: + + + + New text file + Новий текстовий файл + + + + Please enter a new folder name: + Будь ласка, введіть нову назву теки: + + + + New folder + Нова тека + + + + Enter a name for the new %1: + Введіть назву для нової %1: + + + + Custom Icon Error + + + + + The path is not mounted. + + + + + Invalid desktop entry file: '%1' + + + + + No default application is set to launch '%1' + + + + + Cannot set working directory to '%1': %2 + + + + + Identifier: + + + + + RenameDialog + + + Confirm to replace files + Підтвердіть заміщення файлів + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + <html><head/><body><p><span style=" font-weight:600;">Ту вже є файл з такою самою назвою в тому місці.</span></p><p>Ви бажаєте замістити існуючий файл?</p></body></html> + + + + dest + призн + + + + with the following file? + наступним файлом? + + + + src file info + інф про джер файл + + + + dest file info + інф про файл призн + + + + src + джер + + + + &File name: + &Назва файлу: + + + + Apply this option to all existing files + Застосувати цей параметр до всіх існуючих файлів + + + + SearchDialog + + + Search Files + Пошук файлів + + + + Name/Location + Назва/Місце + + + + File Name Patterns: + Шаблони назв файлів: + + + + * + * + + + + Case insensitive + Чутливість до регістру + + + + Use regular expression + Використовувати регулярні фрази + + + + Places to Search: + Місця для пошуку: + + + + &Add + &Додати + + + + &Remove + &Вилучити + + + + Search in sub directories + Пошук в підтеках + + + + Search for hidden files + Пошук прихованих файлів + + + + File Type + Тип файлу + + + + Only search for files of following types: + Пошук файлів тільки наступних типів: + + + + Text files + Текстові файли + + + + Image files + Файли малюнків + + + + Audio files + Аудіо файли + + + + Video files + Відео файли + + + + Documents + Документи + + + + Folders + Теки + + + + Content + Вміст + + + + File contains: + Файл містить: + + + + Case insensiti&ve + Нечутливість до ре&гістру + + + + &Use regular expression + &Використовувати регулярні вирази + + + + Properties + Властивості + + + + File Size: + Розмір файлу: + + + + Larger than: + Більший ніж: + + + + + Bytes + Байти + + + + + KiB + КіБ + + + + + MiB + МіБ + + + + + GiB + ГіБ + + + + Smaller than: + Менший ніж: + + + + Last Modified Time: + Час останньої зміни: + + + + Earlier than: + Раніше ніж: + + + + Later than: + Пізніше ніж: + + + diff --git a/src/translations/libfm-qt_zh_CN.ts b/src/translations/libfm-qt_zh_CN.ts new file mode 100644 index 0000000..726db4a --- /dev/null +++ b/src/translations/libfm-qt_zh_CN.ts @@ -0,0 +1,1560 @@ + + + + + AppChooserDialog + + + Choose an Application + 选择应用程序 + + + + Installed Applications + 已安装的应用程序 + + + + Custom Command + 自定义命令 + + + + Command line to execute: + 要执行的命令行: + + + + Application name: + 应用程序名称: + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + <b>这些特殊符号可以在命令行参数中使用:</b> +<ul> +<li><b>%f</b>:代表单个文件名称</li> +<li><b>%F</b>:代表多个文件名称</li> +<li><b>%u</b>:代表单一文件 URI</li> +<li><b>%U</b>:代表多个 URI</li> +</ul> + + + + Keep terminal window open after command execution + 执行命令后保持终端窗口开启 + + + + Execute in terminal emulator + 在终端模拟器中执行 + + + + Set selected application as default action of this file type + 将所选应用程序设置为这种文件类型的默认程序 + + + + EditBookmarksDialog + + + Edit Bookmarks + 编辑书签 + + + + Name + 名称 + + + + Location + 位置 + + + + &Add Item + 新增项目(&A) + + + + &Remove Item + 移除项目(&R) + + + + Use drag and drop to reorder the items + 使用拖放重新排序项目 + + + + ExecFileDialog + + + Execute file + 执行文件 + + + + &Open + 打开(&O) + + + + E&xecute + 执行(&X) + + + + Execute in &Terminal + 在终端内执行(&T) + + + + Cancel + 取消 + + + + FileDialog + + + Location: + 位置: + + + + File name: + 文件名: + + + + File type: + 文件类型: + + + + FileOperationDialog + + + Destination: + 目标: + + + + Processing: + 正在处理: + + + + Preparing... + 正在准备... + + + + Progress + 进度 + + + + Time remaining: + 剩余时间: + + + + Files processed: + 已处理文件: + + + + FilePropsDialog + + + File Properties + 文件属性 + + + + General + 常规 + + + + Location: + 位置: + + + + File type: + 文件类型: + + + + MIME type: + MIME 类型: + + + + File size: + 文件大小: + + + + On-disk size: + 占用空间: + + + + Last modified: + 修改时间: + + + + Link target: + 链接目标: + + + + Open With: + 打开方式: + + + + Last accessed: + 访问时间: + + + + Contains: + 包含: + + + + Device Usage: + 设备使用: + + + + Permissions + 权限 + + + + Ownership + 所有权 + + + + + + Group: + 组: + + + + + + Owner: + 所有者: + + + + Access Control + 访问限制 + + + + + Other: + 其他: + + + + Make the file executable + 使文件可执行 + + + + + + Read + 只读 + + + + + + Write + 写入 + + + + + + Execute + 执行 + + + + Sticky + + + + + SetUID + + + + + SetGID + + + + + Advanced Mode + 高级模式 + + + + Fm::AppChooserComboBox + + + Customize + 自定义 + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + 选择用于打开“%1”文件的应用程序 + + + + Fm::CreateNewMenu + + + Folder + 文件夹 + + + + Blank File + 空文件 + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + 指定的目录“%1”无效 + + + + Fm::DirTreeModel + + + Loading... + 正在加载…… + + + + + + <No sub folders> + <没有子文件夹> + + + + Fm::DirTreeView + + + Open in New T&ab + 在新标签页中打开(&A) + + + + Open in New Win&dow + 在新窗口中打开(&D) + + + + Open in Termina&l + 在终端中打开(&I) + + + + Fm::DndActionMenu + + + Copy here + 复制至此 + + + + Move here + 移动至此 + + + + Create symlink here + 在这里创建符号链接 + + + + Cancel + 取消 + + + + Fm::EditBookmarksDialog + + + New bookmark + 新建书签 + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + 此文件“%1”像是桌面快捷方式。 +如何处理? + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + 此文本文件“%1”像是可执行脚本。 +如何处理? + + + + This file '%1' is executable. Do you want to execute it? + 文件“%1”可以执行,是否执行? + + + + Fm::FileDialog + + + Go Back + 返回 + + + + Alt+Left + Go Back + Alt+左方向键 + + + + Go Forward + 前进 + + + + Alt+Right + Go Forward + Alt+右方向键 + + + + Reload + 刷新 + + + + F5 + Reload + F5 + + + + Create Folder + 新建文件夹 + + + + Icon View + 图标视图 + + + + Thumbnail View + 缩略图视图 + + + + Compact View + 列表视图 + + + + Detailed List View + 详细信息视图 + + + + + Error + 错误 + + + + Please select a file + 请选择文件 + + + + %1 already exists. +Do you want to replace it? + %1 已存在。 +是否替换? + + + + Path "%1" does not exist + 路径“%1”不存在 + + + + "%1" is not a directory + “%1”不是目录 + + + + "%1" is not a file + “%1”不是文件 + + + + + &Open + 打开(&O) + + + + + &Save + 保存(&S) + + + + All Files (*) + 所有文件 (*) + + + + Fm::FileDialogHelper + + + Open File + 打开文件 + + + + Save File + 保存文件 + + + + Fm::FileMenu + + + Open + 打开 + + + + Open With... + 打开方式... + + + + Other Applications + 其他应用程序 + + + + Create &New + 新建(&N) + + + + &Restore + 恢复(&R) + + + + Cut + 剪切 + + + + Copy + 复制 + + + + Paste + 粘贴 + + + + + &Move to Trash + 移至回收站(&M) + + + + Rename + 重命名 + + + + Extract to... + 解压到... + + + + Extract Here + 解压至此 + + + + Compress + 压缩 + + + + Properties + 属性 + + + + Trust selected executables + 信任已选择的可执行文件 + + + + Trust this executable + 信任这个可执行文件 + + + + Output + 输出 + + + + &Delete + 删除(&D) + + + + Fm::FileOperation + + + Error + 错误 + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + 因为文件系统不支持,有些文件无法移至回收站。 +是否直接删除这些文件? + + + + + Confirm + 确认 + + + + Do you want to delete the selected files? + 您确定要删除所选的文件吗? + + + + Do you want to move the selected files to trash can? + 您确定要将所选的文件移至回收站吗? + + + + Fm::FileOperationDialog + + + Move files + 移动文件 + + + + Moving the following files to destination folder: + 正在将下列文件移动至目标文件夹: + + + + Copy Files + 复制文件 + + + + Copying the following files to destination folder: + 正在将下列文件复制至目标文件夹: + + + + Trash Files + 将文件移至回收站 + + + + Moving the following files to trash can: + 正在将下列文件移动至回收站: + + + + Delete Files + 删除文件 + + + + Deleting the following files: + 正在删除下列文件: + + + + Create Symlinks + 创建符号链接 + + + + Creating symlinks for the following files: + 正在为下列文件创建符号链接: + + + + Change Attributes + 更改属性 + + + + Changing attributes of the following files: + 正在更改下列文件的属性: + + + + Restore Trashed Files + 恢复已删除的文件 + + + + Restoring the following files from trash can: + 正在从回收站恢复下列文件: + + + + + Error + 错误 + + + + Fm::FilePropsDialog + + + View folder content + 查看文件夹内容 + + + + View and modify folder content + 查看并修改文件夹内容 + + + + Read + 只读 + + + + Read and write + 读写 + + + + Forbidden + 禁止 + + + + Files of different types + 不同类型的文件 + + + + Multiple Files + 多个文件 + + + + %p% used + %p% 已使用 + + + + %1 Free of %2 + %2的%1空闲 + + + + no file + 无文件 + + + + one file + 一个文件 + + + + %1 files + %1文件 + + + + Select an icon + 选择图标 + + + + Images (*.png *.xpm *.svg *.svgz ) + 图像(*.png *.xpm *.svg *.svgz) + + + + Apply changes + 应用更改 + + + + Do you want to recursively apply these changes to all files and sub-folders? + 您是否要将这些更改应用于所有子文件夹和文件? + + + + Fm::FileSearchDialog + + + Error + 错误 + + + + You should add at least one directory to search. + 您至少需要新增一个要搜索的目录。 + + + + Select a folder + 选择一个文件夹 + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + 无法在非原生文件系统中创建链接 + + + + Fm::FolderMenu + + + Create &New + 新建(&N) + + + + &Paste + 粘贴(&P) + + + + Select &All + 全选(&A) + + + + Invert Selection + 反选 + + + + Sorting + 排序 + + + + Show Hidden + 显示隐藏文件 + + + + Folder Pr&operties + 文件夹属性(&O) + + + + Output + 输出 + + + + By File Name + 按名称 + + + + By Modification Time + 按修改时间 + + + + By File Size + 按大小 + + + + By File Type + 按文件类型 + + + + By File Owner + 按所有者 + + + + Ascending + 升序 + + + + Descending + 降序 + + + + Folder First + 文件夹优先 + + + + Case Sensitive + 区分大小写 + + + + Fm::FolderModel + + + Name + 名称 + + + + Type + 类型 + + + + Size + 大小 + + + + Modified + 修改 + + + + Owner + 所有者 + + + + Group + 组 + + + + Fm::FontButton + + + Bold + 粗体 + + + + Italic + 斜体 + + + + Fm::MountOperationPasswordDialog + + + &Connect + 连接(&C) + + + + Fm::PathBar + + + &Edit Path + 编辑路径(&E) + + + + &Copy Path + 复制路径(&C) + + + + Fm::PlacesModel + + + Places + 位置 + + + + Desktop + 桌面 + + + + Computer + 计算机 + + + + Applications + 应用程序 + + + + Network + 网络 + + + + Devices + 设备 + + + + Bookmarks + 书签 + + + + Trash + 回收站 + + + + Fm::PlacesView + + + Open in New Tab + 在新标签页中打开 + + + + Open in New Window + 在新窗口中打开 + + + + Empty Trash + 清空回收站 + + + + + Hide + 隐藏 + + + + Move Bookmark Up + 上移书签 + + + + Move Bookmark Down + 下移书签 + + + + Rename Bookmark + 重命名书签 + + + + Remove Bookmark + 移除书签 + + + + + Unmount + 卸载 + + + + Mount + 挂载 + + + + Eject + 弹出 + + + + Show All Entries + 显示所有快捷方式 + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + 类型: %1 +大小: %2 +修改时间: %3 + + + + + Type: %1 +Modified: %2 + 类型: %1 +修改时间: %2 + + + + &Overwrite + 覆盖(&O) + + + + &Rename + 重命名(&R) + + + + Fm::SidePane + + + Places + 位置 + + + + Directory Tree + 目录树 + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + 无法恢复文件“%s”:原路径未知 + + + + MountOperationPasswordDialog + + + Mount + 挂载 + + + + Connect &anonymously + 匿名连接(&A) + + + + Connect as u&ser: + 作为用户连接(&S): + + + + &Username: + 用户名(&U): + + + + &Password: + 密码(&P): + + + + &Domain: + 域(&D): + + + + Forget password &immediately + 立即遗忘密码(&I) + + + + Remember password until you &logout + 记住密码直到注销(&L) + + + + Remember &forever + 永远记住密码(&F) + + + + QObject + + + Rename File + 重命名文件 + + + + Please enter a new name: + 请输入新名称: + + + + + + + + Error + 错误 + + + + Create Folder + 新建文件夹 + + + + Create File + 新建文件 + + + + Please enter a new file name: + 请输入新文件名: + + + + New text file + 新建文本文件 + + + + Please enter a new folder name: + 请输入新文件夹名: + + + + New folder + 新建文件夹 + + + + Enter a name for the new %1: + 请为新的%1输入名称: + + + + Custom Icon Error + 自定义图标错误 + + + + The path is not mounted. + 该目录未挂载。 + + + + Invalid desktop entry file: '%1' + 无效桌面快捷方式:“%1” + + + + No default application is set to launch '%1' + 没有用于运行“%1”的默认应用 + + + + Cannot set working directory to '%1': %2 + 无法将工作目录切换至“%1”:%2 + + + + Identifier: + 标识符: + + + + RenameDialog + + + Confirm to replace files + 确认替换文件 + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + <html><head/><body><p><span style=" font-weight:600;">此处已有同名文件。</span></p><p>是否覆盖已有文件?</p></body></html> + + + + dest + 目标 + + + + with the following file? + 是否包含下列文件? + + + + src file info + 源文件信息 + + + + dest file info + 目标文件信息 + + + + src + 源 + + + + &File name: + 文件名(&F): + + + + Apply this option to all existing files + 为所有已有文件应用此选项 + + + + SearchDialog + + + Search Files + 搜索文件 + + + + Name/Location + 名称/位置 + + + + File Name Patterns: + 文件名模式: + + + + * + + + + + Case insensitive + 区分大小写 + + + + Use regular expression + 使用正则表达式 + + + + Places to Search: + 搜索位置: + + + + &Add + 新增(&A) + + + + &Remove + 移除(&R) + + + + Search in sub directories + 在子目录中搜索 + + + + Search for hidden files + 搜索隐藏文件 + + + + File Type + 文件类型 + + + + Only search for files of following types: + 仅搜索下列类型的文件: + + + + Text files + 文本文件 + + + + Image files + 图像文件 + + + + Audio files + 音频文件 + + + + Video files + 视频文件 + + + + Documents + 文档 + + + + Folders + 文件夹 + + + + Content + 内容 + + + + File contains: + 文件包括: + + + + Case insensiti&ve + 区分大小写(&V) + + + + &Use regular expression + 使用正则表达式(&U) + + + + Properties + 属性 + + + + File Size: + 文件大小: + + + + Larger than: + 大于: + + + + + Bytes + 字节 + + + + + KiB + + + + + + MiB + + + + + + GiB + + + + + Smaller than: + 小于: + + + + Last Modified Time: + 修改时间: + + + + Earlier than: + 早于: + + + + Later than: + 晚于: + + + diff --git a/src/translations/libfm-qt_zh_TW.ts b/src/translations/libfm-qt_zh_TW.ts new file mode 100644 index 0000000..9860a06 --- /dev/null +++ b/src/translations/libfm-qt_zh_TW.ts @@ -0,0 +1,1560 @@ + + + + + AppChooserDialog + + + Choose an Application + 選擇一個應用程式 + + + + Installed Applications + 安裝的應用程式 + + + + Custom Command + 自訂指令 + + + + Command line to execute: + 要執行的命令列: + + + + Application name: + 應用程式名稱: + + + + <b>These special codes can be used in the command line:</b> +<ul> +<li><b>%f</b>: Represents a single file name</li> +<li><b>%F</b>: Represents multiple file names</li> +<li><b>%u</b>: Represents a single URI of the file</li> +<li><b>%U</b>: Represents multiple URIs</li> +</ul> + <b>這些特殊代號可以在指令列參數使用:</b> +<ul> +<li><b>%f</b>:代表單一檔案名稱</li> +<li><b>%F</b>:代表多個檔案名稱</li> +<li><b>%u</b>:代表單一檔案 URI</li> +<li><b>%U</b>:代表多個 URI</li> +</ul> + + + + Keep terminal window open after command execution + 執行指令後保持終端機視窗開啟 + + + + Execute in terminal emulator + 在終端機模擬器執行 + + + + Set selected application as default action of this file type + 將所選應用程式設定為此類型檔案的預設處理程式 + + + + EditBookmarksDialog + + + Edit Bookmarks + 編輯書籤 + + + + Name + 名稱 + + + + Location + 位置 + + + + &Add Item + 新增項目(&A) + + + + &Remove Item + 移除項目(&R) + + + + Use drag and drop to reorder the items + 使用拖放重新排序項目 + + + + ExecFileDialog + + + Execute file + 執行檔案 + + + + &Open + 開啟(&O) + + + + E&xecute + 執行(&X) + + + + Execute in &Terminal + 在終端機內執行(&T) + + + + Cancel + 取消 + + + + FileDialog + + + Location: + 位置: + + + + File name: + 檔案名稱: + + + + File type: + 檔案類型: + + + + FileOperationDialog + + + Destination: + 目的地: + + + + Processing: + 正在處理: + + + + Preparing... + 準備中... + + + + Progress + 進度 + + + + Time remaining: + 剩餘時間: + + + + Files processed: + 已處理的檔案: + + + + FilePropsDialog + + + File Properties + 檔案屬性 + + + + General + 一般 + + + + Location: + 位置: + + + + File type: + 檔案類型: + + + + MIME type: + Mime 類型: + + + + File size: + 檔案大小: + + + + On-disk size: + 磁碟上大小: + + + + Last modified: + 最後修改: + + + + Link target: + 連結目標: + + + + Open With: + 開啟: + + + + Last accessed: + 最後存取: + + + + Contains: + + + + + Device Usage: + + + + + Permissions + 權限 + + + + Ownership + 所有權 + + + + + + Group: + 群組: + + + + + + Owner: + 擁有者: + + + + Access Control + 存取控制 + + + + + Other: + 其他: + + + + Make the file executable + 使檔案可執行 + + + + + + Read + 讀取 + + + + + + Write + 寫入 + + + + + + Execute + 執行 + + + + Sticky + + + + + SetUID + + + + + SetGID + + + + + Advanced Mode + 進階模式 + + + + Fm::AppChooserComboBox + + + Customize + 自訂 + + + + Fm::AppChooserDialog + + + Select an application to open "%1" files + 選取用來開啟「%1」檔案的應用程式 + + + + Fm::CreateNewMenu + + + Folder + 資料夾 + + + + Blank File + 空白檔案 + + + + Fm::DirListJob + + + The specified directory '%1' is not valid + 指定的目錄 '%1' 無效 + + + + Fm::DirTreeModel + + + Loading... + 載入中... + + + + + + <No sub folders> + <沒有子資料夾> + + + + Fm::DirTreeView + + + Open in New T&ab + 在新分頁開啟 (&A) + + + + Open in New Win&dow + 在新視窗開啟 (&D) + + + + Open in Termina&l + 在終端機內開啟 (&I) + + + + Fm::DndActionMenu + + + Copy here + 複製到這裡 + + + + Move here + 移動到這裡 + + + + Create symlink here + 建立符號連結到這裡 + + + + Cancel + 取消 + + + + Fm::EditBookmarksDialog + + + New bookmark + 新書籤 + + + + Fm::ExecFileDialog + + + This file '%1' seems to be a desktop entry. +What do you want to do with it? + 檔案 '%1' 看起來是桌面捷徑檔。 +你想要對它進行什麼操作? + + + + This text file '%1' seems to be an executable script. +What do you want to do with it? + 這個文字檔 '%1' 似乎是可執行的 script。 +想要進行什麼操作? + + + + This file '%1' is executable. Do you want to execute it? + 這個檔案 '%1' 是可執行檔,是否想要執行? + + + + Fm::FileDialog + + + Go Back + 上一頁 + + + + Alt+Left + Go Back + Alt+Left + + + + Go Forward + 下一頁 + + + + Alt+Right + Go Forward + Alt+Right + + + + Reload + 重新整理 + + + + F5 + Reload + F5 + + + + Create Folder + 建立資料夾 + + + + Icon View + 圖示檢視 + + + + Thumbnail View + 縮圖檢視 + + + + Compact View + 精簡檢視 + + + + Detailed List View + 詳細清單檢視 + + + + + Error + 錯誤 + + + + Please select a file + 請選擇一個檔案 + + + + %1 already exists. +Do you want to replace it? + %1 已經存在。 +你想要取代它嗎? + + + + Path "%1" does not exist + 路徑 "%1" 不存在 + + + + "%1" is not a directory + "%1" 不是一個目錄 + + + + "%1" is not a file + "%1" 不是一個檔案 + + + + + &Open + 開啟(&O) + + + + + &Save + 儲存 (&S) + + + + All Files (*) + 所有檔案 (*) + + + + Fm::FileDialogHelper + + + Open File + 開啟檔案 + + + + Save File + 儲存檔案 + + + + Fm::FileMenu + + + Open + 開啟 + + + + Open With... + 用其他程式開啟... + + + + Other Applications + 其他應用程式 + + + + Create &New + 新建(&N) + + + + &Restore + 恢復(&R) + + + + Cut + 剪下 + + + + Copy + 複製 + + + + Paste + 貼上 + + + + + &Move to Trash + 移動到垃圾桶(&M) + + + + Trust selected executables + 信任已選擇的執行檔 + + + + Trust this executable + 信任此執行檔 + + + + Output + 輸出 + + + + &Delete + 刪除(&D) + + + + Rename + 重新命名 + + + + Extract to... + 解壓縮到... + + + + Extract Here + 在此解壓縮 + + + + Compress + 壓縮 + + + + Properties + 屬性 + + + + Fm::FileOperation + + + Error + 錯誤 + + + + Some files cannot be moved to trash can because the underlying file systems don't support this operation. +Do you want to delete them instead? + 因為檔案系統不支援,有些檔案無法丟到垃圾桶 +是否直接刪除這些檔案? + + + + + Confirm + 確認 + + + + Do you want to delete the selected files? + 你確定要刪除選取的檔案嗎? + + + + Do you want to move the selected files to trash can? + 你確定要把選取的檔案移到垃圾桶嗎? + + + + Fm::FileOperationDialog + + + Move files + 移動檔案 + + + + Moving the following files to destination folder: + 正在移動下列檔案到目的資料夾: + + + + Copy Files + 複製檔案 + + + + Copying the following files to destination folder: + 正在複製下列檔案到目的資料夾: + + + + Trash Files + 將檔案丟到垃圾桶 + + + + Moving the following files to trash can: + 正在將下列檔案移動到垃圾桶: + + + + Delete Files + 刪除檔案 + + + + Deleting the following files: + 正在刪除下列檔案: + + + + Create Symlinks + 建立符號連結 + + + + Creating symlinks for the following files: + 正在建立下列檔案的符號連結: + + + + Change Attributes + 改變屬性 + + + + Changing attributes of the following files: + 正在更改下列檔案的屬性: + + + + Restore Trashed Files + 恢復被刪除的檔案 + + + + Restoring the following files from trash can: + 正在從垃圾桶恢復下列被刪除的檔案: + + + + + Error + 錯誤 + + + + Fm::FilePropsDialog + + + View folder content + 檢視資料夾內容 + + + + View and modify folder content + 檢視及修改資料夾內容 + + + + Read + 讀取 + + + + Read and write + 讀取及寫入 + + + + Forbidden + 禁止 + + + + Files of different types + 不同類型的檔案 + + + + Multiple Files + 多個檔案 + + + + %p% used + + + + + %1 Free of %2 + + + + + no file + + + + + one file + + + + + %1 files + + + + + Select an icon + 選擇一個圖示 + + + + Images (*.png *.xpm *.svg *.svgz ) + 影像檔 (*.png *.xpm *.svg *.svgz ) + + + + Apply changes + 套用變更 + + + + Do you want to recursively apply these changes to all files and sub-folders? + 你是否想將這些變更套用到所有子資料夾和其內的檔案? + + + + Fm::FileSearchDialog + + + Error + 錯誤 + + + + You should add at least one directory to search. + 你至少需要新增一個要搜尋的目錄。 + + + + Select a folder + 選擇一個資料夾 + + + + Fm::FileTransferJob + + + Cannot create a link on non-native filesystem + 無法在非原生檔案系統上建立連結 + + + + Fm::FolderMenu + + + Create &New + 新建(&N) + + + + &Paste + 貼上(&P) + + + + Select &All + 全選(&A) + + + + Invert Selection + 反向選取 + + + + Sorting + 排序 + + + + Show Hidden + 顯示隱藏檔 + + + + Folder Pr&operties + 資料夾屬性(&O) + + + + Output + 輸出 + + + + By File Name + 依照檔名 + + + + By Modification Time + 依照修改時間 + + + + By File Size + 依照檔案大小 + + + + By File Type + 依照檔案型態 + + + + By File Owner + 依照檔案所有者 + + + + Ascending + 升冪排列 + + + + Descending + 降冪排列 + + + + Folder First + 資料夾優先 + + + + Case Sensitive + 區分大小寫 + + + + Fm::FolderModel + + + Name + 名稱 + + + + Type + 類型 + + + + Size + 大小 + + + + Modified + 修改 + + + + Owner + 所有者 + + + + Group + 群組 + + + + Fm::FontButton + + + Bold + 粗體 + + + + Italic + 斜體 + + + + Fm::MountOperationPasswordDialog + + + &Connect + 連接(&C) + + + + Fm::PathBar + + + &Edit Path + 編輯路徑 (&E) + + + + &Copy Path + 複製路徑 (&C) + + + + Fm::PlacesModel + + + Places + 位置 + + + + Desktop + 桌面 + + + + Trash + 垃圾桶 + + + + Computer + 電腦 + + + + Applications + 應用程式 + + + + Network + 網路 + + + + Devices + 裝置 + + + + Bookmarks + 書籤 + + + + Fm::PlacesView + + + Empty Trash + 清空垃圾桶 + + + + Open in New Tab + 在新分頁中開啟 + + + + Open in New Window + 在新視窗中開啟 + + + + + Hide + 隱藏 + + + + Move Bookmark Up + 往上移動書籤 + + + + Move Bookmark Down + 往下移動書籤 + + + + Rename Bookmark + 重新命名書籤 + + + + Remove Bookmark + 移除書籤 + + + + + Unmount + 卸載 + + + + Mount + 掛載 + + + + Eject + 退出 + + + + Show All Entries + 顯示全部的項目 + + + + Fm::RenameDialog + + + + Type: %1 +Size: %2 +Modified: %3 + 類型: %1 +大小: %2 +最後修改: %3 + + + + + Type: %1 +Modified: %2 + 類型: %1 +最後修改: %2 + + + + &Overwrite + 覆蓋(&O) + + + + &Rename + 重新命名(&R) + + + + Fm::SidePane + + + Places + 位置 + + + + Directory Tree + 目錄樹 + + + + Fm::UntrashJob + + + Cannot untrash file '%s': original path not known + 無法將檔案從垃圾桶復原 '%s': 原來路徑未知 + + + + MountOperationPasswordDialog + + + Mount + 掛載 + + + + Connect &anonymously + 匿名連線(&A) + + + + Connect as u&ser: + 以使用者帳號連線(&S): + + + + &Username: + 使用者名稱(&U): + + + + &Password: + 密碼(&P): + + + + &Domain: + 域名(&D): + + + + Forget password &immediately + 立刻忘記密碼(&I) + + + + Remember password until you &logout + 記住密碼直到登出(&L) + + + + Remember &forever + 永遠記住密碼(&F) + + + + QObject + + + + + + + Error + 錯誤 + + + + Rename File + 重新命名 + + + + Please enter a new name: + 請輸入一個新名稱: + + + + Create Folder + 建立資料夾 + + + + Please enter a new file name: + 請輸入一個新檔名: + + + + Please enter a new folder name: + 請輸入一個新資料夾名稱: + + + + New folder + 新資料夾 + + + + New text file + 新文字檔 + + + + Enter a name for the new %1: + 幫新的 %1 輸入一個名稱: + + + + Create File + 建立檔案 + + + + Custom Icon Error + 自訂圖示錯誤 + + + + The path is not mounted. + 這個路徑尚未掛載。 + + + + Invalid desktop entry file: '%1' + 無效的桌面捷徑檔: '%1' + + + + No default application is set to launch '%1' + 沒有設定預設的應用程式來啟動 '%1' + + + + Cannot set working directory to '%1': %2 + 無法將目前工作目錄設定為 '%1': %2 + + + + Identifier: + + + + + RenameDialog + + + Confirm to replace files + 確認取代檔案 + + + + <html><head/><body><p><span style=" font-weight:600;">There is already a file with the same name in this location.</span></p><p>Do you want to replace the existing file?</p></body></html> + <html><head/><body><p><span style=" font-weight:600;">這個位置已經有一個同名的檔案</span></p><p>你要取代現有的檔案嗎?</p></body></html> + + + + dest + 目標 + + + + with the following file? + 用下列檔案? + + + + src file info + 來源檔案資訊 + + + + dest file info + 目標檔案資訊 + + + + src + 來源 + + + + &File name: + 檔名(&F): + + + + Apply this option to all existing files + 套用這個選項到所有已存在的檔案 + + + + SearchDialog + + + Search Files + 搜尋檔案 + + + + Name/Location + 名稱/位置 + + + + File Name Patterns: + 檔案名稱樣式: + + + + * + + + + + Case insensitive + 區分大小寫 + + + + Use regular expression + 使用正規表示式 (regexp) + + + + Places to Search: + 要搜尋的地方: + + + + &Add + 新增 (&A) + + + + &Remove + 移除 (&R) + + + + Search in sub directories + 在子目錄中搜尋 + + + + Search for hidden files + 搜尋隱藏檔 + + + + File Type + 檔案型態 + + + + Only search for files of following types: + 只搜尋下列型態的檔案: + + + + Text files + 文字檔 + + + + Image files + 圖片檔 + + + + Audio files + 聲音檔 + + + + Video files + 影片檔 + + + + Documents + 文件 + + + + Folders + 資料夾 + + + + Content + 內容 + + + + File contains: + 檔案包含: + + + + Case insensiti&ve + 區分大小寫 (&V) + + + + &Use regular expression + 使用正規表示式 (&U) + + + + Properties + 屬性 + + + + File Size: + 檔案大小: + + + + Larger than: + 大於: + + + + + Bytes + 位元組 + + + + + KiB + + + + + + MiB + + + + + + GiB + + + + + Smaller than: + 小於: + + + + Last Modified Time: + 最後修改時間: + + + + Earlier than: + 早於: + + + + Later than: + 晚於: + + + diff --git a/src/utilities.cpp b/src/utilities.cpp new file mode 100644 index 0000000..9da7a55 --- /dev/null +++ b/src/utilities.cpp @@ -0,0 +1,374 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "utilities.h" +#include "utilities_p.h" +#include +#include +#include +#include +#include +#include +#include +#include "fileoperation.h" +#include + +#include +#include +#include +#include + +namespace Fm { + +Fm::FilePathList pathListFromUriList(const char* uriList) { + Fm::FilePathList pathList; + char** uris = g_strsplit_set(uriList, "\r\n", -1); + for(char** uri = uris; *uri; ++uri) { + if(**uri != '\0') { + pathList.push_back(Fm::FilePath::fromUri(*uri)); + } + } + g_strfreev(uris); + return pathList; +} + +QByteArray pathListToUriList(const Fm::FilePathList& paths) { + QByteArray uriList; + for(auto& path: paths) { + uriList += path.uri().get(); + uriList += "\r\n"; + } + return uriList; +} + +Fm::FilePathList pathListFromQUrls(QList urls) { + Fm::FilePathList pathList; + for(auto it = urls.cbegin(); it != urls.cend(); ++it) { + auto path = Fm::FilePath::fromUri(it->toString().toUtf8().constData()); + pathList.push_back(std::move(path)); + } + return pathList; +} + +std::pair parseClipboardData(const QMimeData& data) { + bool isCut = false; + Fm::FilePathList paths; + + if(data.hasFormat("x-special/gnome-copied-files")) { + // Gnome, LXDE, and XFCE + QByteArray gnomeData = data.data("x-special/gnome-copied-files"); + char* pdata = gnomeData.data(); + char* eol = strchr(pdata, '\n'); + + if(eol) { + *eol = '\0'; + isCut = (strcmp(pdata, "cut") == 0 ? true : false); + paths = pathListFromUriList(eol + 1); + } + } + + if(paths.empty() && data.hasUrls()) { + // The KDE way + paths = Fm::pathListFromQUrls(data.urls()); + QByteArray cut = data.data(QStringLiteral("application/x-kde-cutselection")); + if(!cut.isEmpty() && QChar::fromLatin1(cut.at(0)) == QLatin1Char('1')) { + isCut = true; + } + } + + return std::make_pair(paths, isCut); +} + +void pasteFilesFromClipboard(const Fm::FilePath& destPath, QWidget* parent) { + QClipboard* clipboard = QApplication::clipboard(); + const QMimeData* data = clipboard->mimeData(); + Fm::FilePathList paths; + bool isCut = false; + + std::tie(paths, isCut) = parseClipboardData(*data); + + if(!paths.empty()) { + if(isCut) { + FileOperation::moveFiles(paths, destPath, parent); + clipboard->clear(QClipboard::Clipboard); + } + else { + FileOperation::copyFiles(paths, destPath, parent); + } + } +} + +void copyFilesToClipboard(const Fm::FilePathList& files) { + QClipboard* clipboard = QApplication::clipboard(); + QMimeData* data = new QMimeData(); + QByteArray ba; + auto urilist = pathListToUriList(files); + + // Add current pid to trace cut/copy operations to current app + data->setData(QStringLiteral("text/x-libfmqt-pid"), ba.setNum(QCoreApplication::applicationPid())); + // Gnome, LXDE, and XFCE + // Note: the standard text/urilist format uses CRLF for line breaks, but gnome format uses LF only + data->setData("x-special/gnome-copied-files", QByteArray("copy\n") + urilist.replace("\r\n", "\n")); + // The KDE way + data->setData("text/uri-list", urilist); + // data->setData(QStringLiteral("application/x-kde-cutselection"), QByteArrayLiteral("0")); + clipboard->setMimeData(data); +} + +void cutFilesToClipboard(const Fm::FilePathList& files) { + QClipboard* clipboard = QApplication::clipboard(); + QMimeData* data = new QMimeData(); + QByteArray ba; + auto urilist = pathListToUriList(files); + + // Add current pid to trace cut/copy operations to current app + data->setData(QStringLiteral("text/x-libfmqt-pid"), ba.setNum(QCoreApplication::applicationPid())); + // Gnome, LXDE, and XFCE + // Note: the standard text/urilist format uses CRLF for line breaks, but gnome format uses LF only + data->setData("x-special/gnome-copied-files", QByteArray("cut\n") + urilist.replace("\r\n", "\n")); + // The KDE way + data->setData("text/uri-list", urilist); + data->setData(QStringLiteral("application/x-kde-cutselection"), QByteArrayLiteral("1")); + clipboard->setMimeData(data); +} + +bool isCurrentPidClipboardData(const QMimeData& data) { + QByteArray clip_pid = data.data(QStringLiteral("text/x-libfmqt-pid")); + QByteArray curr_pid; + curr_pid.setNum(QCoreApplication::applicationPid()); + + return !clip_pid.isEmpty() && clip_pid == curr_pid; +} + +bool changeFileName(const Fm::FilePath& filePath, const QString& newName, QWidget* parent, bool showMessage) { + auto dest = filePath.parent().child(newName.toLocal8Bit().constData()); + Fm::GErrorPtr err; + if(!g_file_move(filePath.gfile().get(), dest.gfile().get(), + GFileCopyFlags(G_FILE_COPY_ALL_METADATA | + G_FILE_COPY_NO_FALLBACK_FOR_MOVE | + G_FILE_COPY_NOFOLLOW_SYMLINKS), + nullptr, /* make this cancellable later. */ + nullptr, nullptr, &err)) { + if (showMessage){ + QMessageBox::critical(parent ? parent->window() : nullptr, QObject::tr("Error"), err.message()); + } + return false; + } + return true; +} + +bool renameFile(std::shared_ptr file, QWidget* parent) { + FilenameDialog dlg(parent ? parent->window() : nullptr); + dlg.setWindowTitle(QObject::tr("Rename File")); + dlg.setLabelText(QObject::tr("Please enter a new name:")); + // FIXME: what's the best way to handle non-UTF8 filename encoding here? + auto old_name = QString::fromStdString(file->name()); + dlg.setTextValue(old_name); + + if(file->isDir()) { // select filename extension for directories + dlg.setSelectExtension(true); + } + + if(dlg.exec() != QDialog::Accepted) { + return false; // stop multiple renaming + } + + QString new_name = dlg.textValue(); + if(new_name == old_name) { + return true; // let multiple renaming continue + } + changeFileName(file->path(), new_name, parent); + return true; +} + +// templateFile is a file path used as a template of the new file. +void createFileOrFolder(CreateFileType type, FilePath parentDir, const TemplateItem* 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: { + auto mime = templ->mimeType(); + prompt = QObject::tr("Enter a name for the new %1:").arg(mime->desc()); + defaultNewName = QString::fromStdString(templ->name()); + } + break; + } + +_retry: + // ask the user to input a file name + bool ok; + QString new_name = QInputDialog::getText(parent ? parent->window() : nullptr, + dialogTitle, + prompt, + QLineEdit::Normal, + defaultNewName, + &ok); + + if(!ok) { + return; + } + + auto dest = parentDir.child(new_name.toLocal8Bit().data()); + Fm::GErrorPtr err; + switch(type) { + case CreateNewTextFile: { + Fm::GFileOutputStreamPtr f{g_file_create(dest.gfile().get(), G_FILE_CREATE_NONE, nullptr, &err), false}; + if(f) { + g_output_stream_close(G_OUTPUT_STREAM(f.get()), nullptr, nullptr); + } + break; + } + case CreateNewFolder: + g_file_make_directory(dest.gfile().get(), nullptr, &err); + break; + case CreateWithTemplate: + // copy the template file to its destination + FileOperation::copyFile(templ->filePath(), dest, parent); + break; + } + if(err) { + if(err.domain() == G_IO_ERROR && err.code() == G_IO_ERROR_EXISTS) { + err.reset(); + goto _retry; + } + + QMessageBox::critical(parent ? parent->window() : nullptr, QObject::tr("Error"), err.message()); + } +} + +uid_t uidFromName(QString name) { + uid_t ret; + if(name.isEmpty()) { + return INVALID_UID; + } + 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 : INVALID_UID; + } + + 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 INVALID_GID; + } + 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 : INVALID_GID; + } + + 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 lxqt/lxqt#512 +// https://github.com/lxqt/lxqt/issues/512 +// Use uriExists() whenever possible. +bool isUriSchemeSupported(const char* uriScheme) { + const gchar* const* schemes = g_vfs_get_supported_uri_schemes(g_vfs_get_default()); + if(Q_UNLIKELY(schemes == nullptr)) { + return false; + } + for(const gchar * const* scheme = schemes; *scheme; ++scheme) + if(strcmp(uriScheme, *scheme) == 0) { + return true; + } + return false; +} + +// check if the URI exists. +// NOTE: this is a blocking call possibly involving I/O. +// So it's better to use it in limited cases, like checking trash:// or computer://. +// Avoid calling this on a slow filesystem. +// Checking "network:///" is very slow, for example. +bool uriExists(const char* uri) { + GFile* gf = g_file_new_for_uri(uri); + bool ret = (bool)g_file_query_exists(gf, nullptr); + g_object_unref(gf); + return ret; +} + +QString formatFileSize(uint64_t size, bool useSI) { + Fm::CStrPtr str{g_format_size_full(size, useSI ? G_FORMAT_SIZE_DEFAULT : G_FORMAT_SIZE_IEC_UNITS)}; + return QString(str.get()); +} + +} // namespace Fm diff --git a/src/utilities.h b/src/utilities.h new file mode 100644 index 0000000..f624b26 --- /dev/null +++ b/src/utilities.h @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef FM_UTILITIES_H +#define FM_UTILITIES_H + +#include "libfmqtglobals.h" +#include +#include +#include +#include + +#include +#include + +#include "core/filepath.h" +#include "core/fileinfo.h" +#include "core/templates.h" + +class QDialog; + +namespace Fm { + +LIBFM_QT_API Fm::FilePathList pathListFromUriList(const char* uriList); + +LIBFM_QT_API QByteArray pathListToUriList(const Fm::FilePathList& paths); + +LIBFM_QT_API Fm::FilePathList pathListFromQUrls(QList urls); + +LIBFM_QT_API std::pair parseClipboardData(const QMimeData& data); + +LIBFM_QT_API void pasteFilesFromClipboard(const Fm::FilePath& destPath, QWidget* parent = nullptr); + +LIBFM_QT_API void copyFilesToClipboard(const Fm::FilePathList& files); + +LIBFM_QT_API void cutFilesToClipboard(const Fm::FilePathList& files); + +LIBFM_QT_API bool isCurrentPidClipboardData(const QMimeData& data); + +LIBFM_QT_API bool changeFileName(const Fm::FilePath& path, const QString& newName, QWidget* parent, bool showMessage = true); + +LIBFM_QT_API bool renameFile(std::shared_ptr file, QWidget* parent = nullptr); + +enum CreateFileType { + CreateNewFolder, + CreateNewTextFile, + CreateWithTemplate +}; + +LIBFM_QT_API void createFileOrFolder(CreateFileType type, FilePath parentDir, const TemplateItem* templ = nullptr, QWidget* parent = nullptr); + +constexpr uid_t INVALID_UID = uid_t(-1); + +LIBFM_QT_API uid_t uidFromName(QString name); + +LIBFM_QT_API QString uidToName(uid_t uid); + +constexpr gid_t INVALID_GID = gid_t(-1); + +LIBFM_QT_API gid_t gidFromName(QString name); + +LIBFM_QT_API QString gidToName(gid_t gid); + +LIBFM_QT_API int execModelessDialog(QDialog* dlg); + +// NOTE: this does not work reliably due to some problems in gio/gvfs +// Use uriExists() whenever possible. +LIBFM_QT_API bool isUriSchemeSupported(const char* uriScheme); + +LIBFM_QT_API bool uriExists(const char* uri); + +LIBFM_QT_API QString formatFileSize(std::uint64_t size, bool useSI = false); + +} + +#endif // FM_UTILITIES_H diff --git a/src/utilities_p.h b/src/utilities_p.h new file mode 100644 index 0000000..7852f99 --- /dev/null +++ b/src/utilities_p.h @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2014 - 2015 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef __UTILITS_P_H__ +#define __UTILITS_P_H__ + +#include +#include +#include + +namespace Fm { + +// private class used in internal implementation +class FilenameDialog : public QInputDialog { + Q_OBJECT +public: + FilenameDialog(QWidget* parent = 0, Qt::WindowFlags flags = 0): + QInputDialog(parent, flags), + selectExtension_(false) { + } + + virtual void showEvent(QShowEvent * event) { + QWidget::showEvent(event); + if(!selectExtension_) // dot not select filename extension + QTimer::singleShot(0, this, SLOT(initSelection())); + } + + bool selectExtension() const { + return selectExtension_; + } + + void setSelectExtension(bool value) { + selectExtension_ = value; + } + +private Q_SLOTS: + // do not select filename extensions + void initSelection() { + // find the QLineEdit child widget + QLineEdit* lineEdit = findChild(); + if(lineEdit) { + QString filename = lineEdit->text(); + if(!filename.isEmpty()) { + // only select filename part without extension name. + int ext = filename.lastIndexOf('.'); + if(ext != -1) { + // add special cases for tar.gz, tar.bz2, and other tar.* files + if(filename.leftRef(ext).endsWith(".tar")) + ext -= 4; + // FIXME: should we also handle other special cases? + lineEdit->setSelection(0, ext); + } + } + } + } + +private: + bool selectExtension_; +}; + +} // namespace Fm + +#endif diff --git a/src/utils.h b/src/utils.h new file mode 100644 index 0000000..558b52e --- /dev/null +++ b/src/utils.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2016 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef __LIBFM_QT_FM_UTILS_H__ +#define __LIBFM_QT_FM_UTILS_H__ + +#include +#include +#include "libfmqtglobals.h" + + +namespace Fm { + + + +} + +#endif // __LIBFM_QT_FM_UTILS_H__ diff --git a/src/xdndworkaround.cpp b/src/xdndworkaround.cpp new file mode 100644 index 0000000..f0a35d2 --- /dev/null +++ b/src/xdndworkaround.cpp @@ -0,0 +1,292 @@ +/* + * Copyright (C) 2016 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include +#include "xdndworkaround.h" +#include +#include +#include +#include +#include +#include + +// This part is for Qt >= 5.4 only +#if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)) +#include +#include +#include + +// these are private Qt headers which are not part of Qt APIs +#include // Too bad that we need to use private headers of Qt :-( + +// For some unknown reasons, the event type constants defined in +// xcb/input.h are different from that in X11/extension/XI2.h +// To be safe, we define it ourselves. +#undef XI_ButtonRelease +#define XI_ButtonRelease 5 + +#endif // (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)) + +XdndWorkaround::XdndWorkaround() { + if(!QX11Info::isPlatformX11()) { + return; + } + + // we need to filter all X11 events + qApp->installNativeEventFilter(this); + +// This part is for Qt >= 5.4 only +#if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)) + lastDrag_ = nullptr; + + // initialize xinput2 since newer versions of Qt5 uses it. + static char xi_name[] = "XInputExtension"; + xcb_connection_t* conn = QX11Info::connection(); + xcb_query_extension_cookie_t cookie = xcb_query_extension(conn, strlen(xi_name), xi_name); + xcb_generic_error_t* err = nullptr; + xcb_query_extension_reply_t* reply = xcb_query_extension_reply(conn, cookie, &err); + if(err == nullptr) { + xinput2Enabled_ = true; + xinputOpCode_ = reply->major_opcode; + xinputEventBase_ = reply->first_event; + xinputErrorBase_ = reply->first_error; + // qDebug() << "xinput: " << m_xi2Enabled << m_xiOpCode << m_xiEventBase; + } + else { + xinput2Enabled_ = false; + free(err); + } + free(reply); +#endif // (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)) +} + +XdndWorkaround::~XdndWorkaround() { + if(!QX11Info::isPlatformX11()) { + return; + } + qApp->removeNativeEventFilter(this); +} + +bool XdndWorkaround::nativeEventFilter(const QByteArray& eventType, void* message, long* /*result*/) { + if(Q_LIKELY(eventType == "xcb_generic_event_t")) { + xcb_generic_event_t* event = static_cast(message); + switch(event->response_type & ~0x80) { + case XCB_CLIENT_MESSAGE: + return clientMessage(reinterpret_cast(event)); + case XCB_SELECTION_NOTIFY: + return selectionNotify(reinterpret_cast(event)); +// This part is for Qt >= 5.4 only +#if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)) + case XCB_SELECTION_REQUEST: + return selectionRequest(reinterpret_cast(event)); + case XCB_GE_GENERIC: + // newer versions of Qt5 supports xinput2, which sends its mouse events via XGE. + return genericEvent(reinterpret_cast(event)); + case XCB_BUTTON_RELEASE: + // older versions of Qt5 receive mouse events via old XCB events. + buttonRelease(); + break; +#endif // Qt >= 5.4 + default: + break; + } + } + return false; +} + +// static +QByteArray XdndWorkaround::atomName(xcb_atom_t atom) { + QByteArray name; + xcb_connection_t* conn = QX11Info::connection(); + xcb_get_atom_name_cookie_t cookie = xcb_get_atom_name(conn, atom); + xcb_get_atom_name_reply_t* reply = xcb_get_atom_name_reply(conn, cookie, nullptr); + int len = xcb_get_atom_name_name_length(reply); + if(len > 0) { + name.append(xcb_get_atom_name_name(reply), len); + } + free(reply); + return name; +} + +// static +xcb_atom_t XdndWorkaround::internAtom(const char* name, int len) { + xcb_atom_t atom = 0; + if(len == -1) { + len = strlen(name); + } + xcb_connection_t* conn = QX11Info::connection(); + xcb_intern_atom_cookie_t cookie = xcb_intern_atom(conn, false, len, name); + xcb_generic_error_t* err = nullptr; + xcb_intern_atom_reply_t* reply = xcb_intern_atom_reply(conn, cookie, &err); + if(reply != nullptr) { + atom = reply->atom; + free(reply); + } + if(err != nullptr) { + free(err); + } + return atom; +} + +// static +QByteArray XdndWorkaround::windowProperty(xcb_window_t window, xcb_atom_t propAtom, xcb_atom_t typeAtom, int len) { + QByteArray data; + xcb_connection_t* conn = QX11Info::connection(); + xcb_get_property_cookie_t cookie = xcb_get_property(conn, false, window, propAtom, typeAtom, 0, len); + xcb_generic_error_t* err = nullptr; + xcb_get_property_reply_t* reply = xcb_get_property_reply(conn, cookie, &err); + if(reply != nullptr) { + len = xcb_get_property_value_length(reply); + const char* buf = (const char*)xcb_get_property_value(reply); + data.append(buf, len); + free(reply); + } + if(err != nullptr) { + free(err); + } + return data; +} + +// static +void XdndWorkaround::setWindowProperty(xcb_window_t window, xcb_atom_t propAtom, xcb_atom_t typeAtom, void* data, int len, int format) { + xcb_connection_t* conn = QX11Info::connection(); + xcb_change_property(conn, XCB_PROP_MODE_REPLACE, window, propAtom, typeAtom, format, len, data); +} + + +bool XdndWorkaround::clientMessage(xcb_client_message_event_t* event) { + QByteArray event_type = atomName(event->type); + // qDebug() << "client message:" << event_type; + + // NOTE: Because of the limitation of Qt, this hack is required to provide + // Xdnd direct save (XDS) protocol support. + // https://www.freedesktop.org/wiki/Specifications/XDS/#index4h2 + // + // XDS requires that the drop target should get and set the window property of the + // drag source to pass the file path, but in Qt there is NO way to know the + // window ID of the drag source so it's not possible to implement XDS with Qt alone. + // Here is a simple hack. We get the drag source window ID with raw XCB code. + // Then, save it on the drop target widget using QObject dynamic property. + // So in the drop event handler of the target widget, it can obtain the + // window ID of the drag source with QObject::property(). + // This hack works 99.99% of the time, but it's not bullet-proof. + // In theory, there is one corner case for which this will not work. + // That is, when you drag multiple XDS sources at the same time and drop + // all of them on the same widget. (Does XDND support doing this?) + // I do not think that any app at the moment support this. + // Even if somebody is using it, X11 will die and we should solve this in Wayland instead. + // + if(event_type == "XdndDrop") { + // data.l[0] contains the XID of the source window. + // data.l[1] is reserved for future use (flags). + // data.l[2] contains the time stamp for retrieving the data. (new in version 1) + QWidget* target = QWidget::find(event->window); + if(target != nullptr) { // drop on our widget + target = qApp->widgetAt(QCursor::pos()); // get the exact child widget that receives the drop + if(target != nullptr) { + target->setProperty("xdnd::lastDragSource", event->data.data32[0]); + target->setProperty("xdnd::lastDropTime", event->data.data32[2]); + } + } + } + // This part is for Qt >= 5.4 only +#if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)) + else if(event_type == "XdndFinished") { + lastDrag_ = nullptr; + } +#endif // Qt >= 5.4 + return false; +} + +bool XdndWorkaround::selectionNotify(xcb_selection_notify_event_t* event) { + qDebug() << "selection notify" << atomName(event->selection); + return false; +} + +// This part is for Qt >= 5.4 only +#if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)) + +bool XdndWorkaround::selectionRequest(xcb_selection_request_event_t* event) { + xcb_connection_t* conn = QX11Info::connection(); + if(event->property == XCB_ATOM_PRIMARY || event->property == XCB_ATOM_SECONDARY) { + return false; // we only touch selection requests related to XDnd + } + QByteArray prop_name = atomName(event->property); + if(prop_name == "CLIPBOARD") { + return false; // we do not touch clipboard, either + } + + xcb_atom_t atomFormat = event->target; + QByteArray type_name = atomName(atomFormat); + // qDebug() << "selection request" << prop_name << type_name; + // We only want to handle text/x-moz-url and text/uri-list + if(type_name == "text/x-moz-url" || type_name.startsWith("text/uri-list")) { + QDragManager* mgr = QDragManager::self(); + QDrag* drag = mgr->object(); + if(drag == nullptr) { + drag = lastDrag_; + } + QMimeData* mime = drag ? drag->mimeData() : nullptr; + if(mime != nullptr && mime->hasUrls()) { + QByteArray data; + QList uris = mime->urls(); + if(type_name == "text/x-moz-url") { + QString mozurl = uris.at(0).toString(QUrl::FullyEncoded); + data.append((const char*)mozurl.utf16(), mozurl.length() * 2); + } + else { // text/uri-list + for(const QUrl& uri : uris) { + data.append(uri.toString(QUrl::FullyEncoded)); + data.append("\r\n"); + } + } + xcb_change_property(conn, XCB_PROP_MODE_REPLACE, event->requestor, event->property, + atomFormat, 8, data.size(), (const void*)data.constData()); + xcb_selection_notify_event_t notify; + notify.response_type = XCB_SELECTION_NOTIFY; + notify.requestor = event->requestor; + notify.selection = event->selection; + notify.time = event->time; + notify.property = event->property; + notify.target = atomFormat; + xcb_window_t proxy_target = event->requestor; + xcb_send_event(conn, false, proxy_target, XCB_EVENT_MASK_NO_EVENT, (const char*)¬ify); + return true; // stop Qt 5 from touching the event + } + } + return false; // let Qt handle this +} + +bool XdndWorkaround::genericEvent(xcb_ge_generic_event_t* event) { + // check this is an xinput event + if(xinput2Enabled_ && event->extension == xinputOpCode_) { + if(event->event_type == XI_ButtonRelease) { + buttonRelease(); + } + } + return false; +} + +void XdndWorkaround::buttonRelease() { + QDragManager* mgr = QDragManager::self(); + lastDrag_ = mgr->object(); + // qDebug() << "BUTTON RELEASE!!!!" << xcbDrag()->canDrop() << lastDrag_; +} + +#endif // QT_VERSION >= QT_VERSION_CHECK(5, 4, 0) diff --git a/src/xdndworkaround.h b/src/xdndworkaround.h new file mode 100644 index 0000000..9d40c11 --- /dev/null +++ b/src/xdndworkaround.h @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2016 Hong Jen Yee (PCMan) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +/* + * Note: + * This is a workaround for the following Qt5 bugs. + * + * #49947: Drop events have broken mimeData()->urls() and text/uri-list. + * #47981: Qt5.4 regression: Dropping text/urilist over browser windows stop working. + * + * Related LXQt bug: https://github.com/lxqt/lxqt/issues/688 + * + * This workaround is not 100% reliable, but it should work most of the time. + * In theory, when there are multiple drag and drops at nearly the same time and + * you are using a remote X11 instance via a slow network connection, this workaround + * might break. However, that should be a really rare corner case. + * + * How this fix works: + * 1. Hook QApplication to filter raw X11 events + * 2. Intercept SelectionRequest events sent from XDnd target window. + * 3. Check if the data requested have the type "text/uri-list" or "x-moz-url" + * 4. Bypass the broken Qt5 code and send the mime data to the target with our own code. + * + * The mime data is obtained during the most recent mouse button release event. + * This can be incorrect in some corner cases, but it is still a simple and + * good enough approximation that returns the correct data most of the time. + * Anyway, a workarond is just a workaround. Ask Qt developers to fix their bugs. + */ + +#ifndef XDNDWORKAROUND_H +#define XDNDWORKAROUND_H + +#include + +#include +#include +#include +#include +#include + +class QDrag; + +class XdndWorkaround : public QAbstractNativeEventFilter { +public: + explicit XdndWorkaround(); + ~XdndWorkaround(); + bool nativeEventFilter(const QByteArray& eventType, void* message, long* result) override; + static QByteArray atomName(xcb_atom_t atom); + static xcb_atom_t internAtom(const char* name, int len = -1); + static QByteArray windowProperty(xcb_window_t window, xcb_atom_t propAtom, xcb_atom_t typeAtom, int len); + static void setWindowProperty(xcb_window_t window, xcb_atom_t propAtom, xcb_atom_t typeAtom, void* data, int len, int format = 8); + +private: + bool clientMessage(xcb_client_message_event_t* event); + bool selectionNotify(xcb_selection_notify_event_t* event); + +// This part is for Qt >= 5.4 only +#if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)) +private: + bool selectionRequest(xcb_selection_request_event_t* event); + bool genericEvent(xcb_ge_generic_event_t* event); + // _QBasicDrag* xcbDrag() const; + void buttonRelease(); + + QPointer lastDrag_; + // xinput related + bool xinput2Enabled_; + int xinputOpCode_; + int xinputEventBase_; + int xinputErrorBase_; +#endif // Qt >= 5.4 +}; + +#endif // XDNDWORKAROUND_H -- 2.30.2