diff --git a/rtdata/icons/rawtherapee/scalable/apps/folder-subfolder.svg b/rtdata/icons/rawtherapee/scalable/apps/folder-subfolder.svg new file mode 100644 index 000000000..eb858b543 --- /dev/null +++ b/rtdata/icons/rawtherapee/scalable/apps/folder-subfolder.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/rtdata/languages/default b/rtdata/languages/default index 07b5f18c9..0397ecf2c 100644 --- a/rtdata/languages/default +++ b/rtdata/languages/default @@ -203,6 +203,7 @@ FILEBROWSER_SHOWRANK4HINT;Show images ranked as 4-star.\nShortcut: Shift-4Shift-5 FILEBROWSER_SHOWRECENTLYSAVEDHINT;Show saved images.\nShortcut: Alt-7 FILEBROWSER_SHOWRECENTLYSAVEDNOTHINT;Show unsaved images.\nShortcut: Alt-6 +FILEBROWSER_SHOWRECURSIVE;Show images in sub-folders recursively. FILEBROWSER_SHOWTRASHHINT;Show contents of trash.\nShortcut: Ctrl-t FILEBROWSER_SHOWUNCOLORHINT;Show images without a color label.\nShortcut: Alt-0 FILEBROWSER_SHOWUNRANKHINT;Show unranked images.\nShortcut: Shift-0 @@ -1844,6 +1845,9 @@ PREFERENCES_BEHADDALLHINT;Set all parameters to the Add mode.\nAdjustment PREFERENCES_BEHAVIOR;Behavior PREFERENCES_BEHSETALL;All to 'Set' PREFERENCES_BEHSETALLHINT;Set all parameters to the Set mode.\nAdjustments of parameters in the batch tool panel will be absolute, the actual values will be displayed. +PREFERENCES_BROWSERECURSIVEDEPTH;Browse sub-folders depth +PREFERENCES_BROWSERECURSIVEFOLLOWLINKS;Follow symbolic links when browsing sub-folders +PREFERENCES_BROWSERECURSIVEMAXDIRS;Maximum sub-folders PREFERENCES_CACHECLEAR;Clear PREFERENCES_CACHECLEAR_ALL;Clear all cached files: PREFERENCES_CACHECLEAR_ALLBUTPROFILES;Clear all cached files except for cached processing profiles: diff --git a/rtgui/filecatalog.cc b/rtgui/filecatalog.cc index f03edf5a6..69b9e6dc9 100644 --- a/rtgui/filecatalog.cc +++ b/rtgui/filecatalog.cc @@ -19,6 +19,8 @@ */ #include "filecatalog.h" +#include +#include #include #include @@ -347,9 +349,17 @@ FileCatalog::FileCatalog (CoarsePanel* cp, ToolBar* tb, FilePanel* filepanel) : bCateg[19] = bOriginal->signal_toggled().connect (sigc::bind(sigc::mem_fun(*this, &FileCatalog::categoryButtonToggled), bOriginal, true)); bOriginal->signal_button_press_event().connect (sigc::mem_fun(*this, &FileCatalog::capture_event), false); + bRecursive = Gtk::manage(new Gtk::ToggleButton()); + bRecursive->set_image(*Gtk::manage(new RTImage("folder-subfolder", Gtk::ICON_SIZE_LARGE_TOOLBAR))); + bRecursive->set_tooltip_text(M("FILEBROWSER_SHOWRECURSIVE")); + bRecursive->set_relief(Gtk::RELIEF_NONE); + bRecursive->set_active(options.browseRecursive); + bRecursive->signal_toggled().connect(sigc::mem_fun(*this, &FileCatalog::showRecursiveToggled)); + buttonBar->pack_start (*bTrash, Gtk::PACK_SHRINK); buttonBar->pack_start (*bNotTrash, Gtk::PACK_SHRINK); buttonBar->pack_start (*bOriginal, Gtk::PACK_SHRINK); + buttonBar->pack_start(*bRecursive, Gtk::PACK_SHRINK); buttonBar->pack_start (*Gtk::manage(new Gtk::Separator(Gtk::ORIENTATION_VERTICAL)), Gtk::PACK_SHRINK); fileBrowser->trash_changed().connect( sigc::mem_fun(*this, &FileCatalog::trashChanged) ); @@ -541,9 +551,7 @@ void FileCatalog::closeDir () exportPanel->set_sensitive (false); } - if (dirMonitor) { - dirMonitor->cancel (); - } + dirMonitors.clear(); // ignore old requests ++selectedDirectoryId; @@ -567,60 +575,82 @@ void FileCatalog::closeDir () redrawAll (); } -std::vector FileCatalog::getFileList() +std::vector FileCatalog::getFileList(std::vector> *dirs_explored) { std::vector names; const std::set& extensions = options.parsedExtensionsSet; - try { + static void (*getFilesRecursively)(const Glib::ustring &, int, int &, std::vector &, std::vector> *) = [](const Glib::ustring &dir_path, int max_depth, int &dir_quota, std::vector &file_names, std::vector> * directories_explored) { + try { - const auto dir = Gio::File::create_for_path(selectedDirectory); + const auto dir = Gio::File::create_for_path(dir_path); - auto enumerator = dir->enumerate_children("standard::name,standard::type,standard::is-hidden"); + static const auto enumerate_attrs = + std::string(G_FILE_ATTRIBUTE_STANDARD_NAME) + "," + + G_FILE_ATTRIBUTE_STANDARD_TYPE + "," + + G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN + "," + + G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET; + auto enumerator = dir->enumerate_children( + enumerate_attrs, + options.browseRecursiveFollowLinks + ? Gio::FileQueryInfoFlags::FILE_QUERY_INFO_NONE + : Gio::FileQueryInfoFlags::FILE_QUERY_INFO_NOFOLLOW_SYMLINKS); - while (true) { - try { - const auto file = enumerator->next_file(); - if (!file) { - break; - } + if (directories_explored) { + directories_explored->push_back(dir); + } - if (file->get_file_type() == Gio::FILE_TYPE_DIRECTORY) { - continue; - } + while (true) { + try { + const auto file = enumerator->next_file(); + if (!file) { + break; + } - if (!options.fbShowHidden && file->is_hidden()) { - continue; - } + if (!options.fbShowHidden && file->is_hidden()) { + continue; + } - const Glib::ustring fname = file->get_name(); - const auto lastdot = fname.find_last_of('.'); + if (file->get_file_type() == Gio::FILE_TYPE_DIRECTORY) { + if (max_depth > 0 && dir_quota > 0) { + const Glib::ustring child_dir_path = Glib::build_filename(dir_path, file->get_name()); + getFilesRecursively(child_dir_path, max_depth - 1, --dir_quota, file_names, directories_explored); + } + continue; + } - if (lastdot >= fname.length() - 1) { - continue; - } + const Glib::ustring fname = file->get_name(); + const auto lastdot = fname.find_last_of('.'); - if (extensions.find(fname.substr(lastdot + 1).lowercase()) == extensions.end()) { - continue; - } + if (lastdot >= fname.length() - 1) { + continue; + } - names.push_back(Glib::build_filename(selectedDirectory, fname)); - } catch (Glib::Exception& exception) { - if (rtengine::settings->verbose) { - std::cerr << exception.what() << std::endl; + if (extensions.find(fname.substr(lastdot + 1).lowercase()) == extensions.end()) { + continue; + } + + file_names.emplace_back(Glib::build_filename(dir_path, fname)); + } catch (Glib::Exception& exception) { + if (rtengine::settings->verbose) { + std::cerr << exception.what() << std::endl; + } } } + + } catch (Glib::Exception& exception) { + + if (rtengine::settings->verbose) { + std::cerr << "Failed to list directory \"" << dir_path << "\": " << exception.what() << std::endl; + } + } + }; - } catch (Glib::Exception& exception) { - - if (rtengine::settings->verbose) { - std::cerr << "Failed to list directory \"" << selectedDirectory << "\": " << exception.what() << std::endl; - } - - } + int dirs_left = options.browseRecursive ? options.browseRecursiveMaxDirs : 0; + getFilesRecursively(selectedDirectory, options.browseRecursiveDepth, dirs_left, names, dirs_explored); return names; } @@ -646,9 +676,10 @@ void FileCatalog::dirSelected (const Glib::ustring& dirname, const Glib::ustring selectedDirectory = dir->get_parse_name(); + std::vector> allDirs; BrowsePath->set_text(selectedDirectory); buttonBrowsePath->set_image(*iRefreshWhite); - fileNameList = getFileList(); + fileNameList = getFileList(&allDirs); for (unsigned int i = 0; i < fileNameList.size(); i++) { if (openfile.empty() || fileNameList[i] != openfile) { // if we opened a file at the beginning don't add it again @@ -664,13 +695,45 @@ void FileCatalog::dirSelected (const Glib::ustring& dirname, const Glib::ustring filepanel->loadingThumbs(M("PROGRESSBAR_LOADINGTHUMBS"), 0); } - dirMonitor = dir->monitor_directory (); - dirMonitor->signal_changed().connect (sigc::bind(sigc::mem_fun(*this, &FileCatalog::on_dir_changed), false)); + refreshDirectoryMonitors(allDirs); } catch (Glib::Exception& ex) { std::cout << ex.what(); } } +void FileCatalog::refreshDirectoryMonitors(const std::vector> &dirs_to_monitor) +{ + std::vector updated_dir_names; + std::transform( + dirs_to_monitor.cbegin(), dirs_to_monitor.cend(), + std::back_inserter(updated_dir_names), + [](const Glib::RefPtr &updated_dir) { return updated_dir->get_path(); }); + + // Remove monitors on directories that are no longer shown. + dirMonitors.erase( + std::remove_if(dirMonitors.begin(), dirMonitors.end(), + [&updated_dir_names](const FileMonitorInfo &fileMonitorInfo) { + return std::find(updated_dir_names.cbegin(), updated_dir_names.cend(), fileMonitorInfo.filePath) == updated_dir_names.cend(); + }), + dirMonitors.end()); + + // Add monitors that do not exist yet. + std::vector monitored_dir_names; + std::transform( + dirMonitors.cbegin(), dirMonitors.cend(), + std::back_inserter(monitored_dir_names), + [](const FileMonitorInfo &dir_monitor) { return dir_monitor.filePath; }); + for (const auto &dir_to_monitor : dirs_to_monitor) { + const auto dir_path = dir_to_monitor->get_path(); + if (std::find(monitored_dir_names.cbegin(), monitored_dir_names.cend(), dir_path) != monitored_dir_names.cend()) { + continue; // A monitor exists already. + } + auto dir_monitor = dir_to_monitor->monitor_directory(); + dir_monitor->signal_changed().connect(sigc::bind(sigc::mem_fun(*this, &FileCatalog::on_dir_changed), false)); + dirMonitors.emplace_back(dir_monitor, dir_path); + } +} + void FileCatalog::enableTabMode(bool enable) { inTabMode = enable; @@ -1579,6 +1642,12 @@ void FileCatalog::categoryButtonToggled (Gtk::ToggleButton* b, bool isMouseClick } } +void FileCatalog::showRecursiveToggled() +{ + options.browseRecursive = bRecursive->get_active(); + reparseDirectory(); +} + BrowserFilter FileCatalog::getFilter () { @@ -1712,21 +1781,28 @@ void FileCatalog::reparseDirectory () return; } - // check if a thumbnailed file has been deleted + // check if a thumbnailed file has been deleted or is not in a directory of interest const std::vector& t = fileBrowser->getEntries(); std::vector fileNamesToDel; + std::vector fileNamesToRemove; for (const auto& entry : t) { if (!Glib::file_test(entry->filename, Glib::FILE_TEST_EXISTS)) { fileNamesToDel.push_back(entry->filename); + fileNamesToRemove.push_back(entry->filename); + } + else if (!options.browseRecursive && Glib::path_get_dirname(entry->filename) != selectedDirectory) { + fileNamesToRemove.push_back(entry->filename); } } - for (const auto& toDelete : fileNamesToDel) { - delete fileBrowser->delEntry(toDelete); - cacheMgr->deleteEntry(toDelete); + for (const auto& toRemove : fileNamesToRemove) { + delete fileBrowser->delEntry(toRemove); --previewsLoaded; } + for (const auto& toDelete : fileNamesToDel) { + cacheMgr->deleteEntry(toDelete); + } if (!fileNamesToDel.empty()) { _refreshProgressBar(); @@ -1739,7 +1815,8 @@ void FileCatalog::reparseDirectory () oldNames.insert(oldName.collate_key()); } - fileNameList = getFileList(); + std::vector> allDirs; + fileNameList = getFileList(&allDirs); for (const auto& newName : fileNameList) { if (oldNames.find(newName.collate_key()) == oldNames.end()) { addFile(newName); @@ -1747,13 +1824,16 @@ void FileCatalog::reparseDirectory () } } + refreshDirectoryMonitors(allDirs); } void FileCatalog::on_dir_changed (const Glib::RefPtr& file, const Glib::RefPtr& other_file, Gio::FileMonitorEvent event_type, bool internal) { - if (options.has_retained_extention(file->get_parse_name()) - && (event_type == Gio::FILE_MONITOR_EVENT_CREATED || event_type == Gio::FILE_MONITOR_EVENT_DELETED || event_type == Gio::FILE_MONITOR_EVENT_CHANGED)) { + if ((options.has_retained_extention(file->get_parse_name()) + && (event_type == Gio::FILE_MONITOR_EVENT_CREATED || event_type == Gio::FILE_MONITOR_EVENT_DELETED || event_type == Gio::FILE_MONITOR_EVENT_CHANGED)) + || (event_type == Gio::FILE_MONITOR_EVENT_CREATED && Glib::file_test(file->get_path(), Glib::FileTest::FILE_TEST_IS_DIR)) + || (event_type == Gio::FILE_MONITOR_EVENT_DELETED && std::find_if(dirMonitors.cbegin(), dirMonitors.cend(), [&file](const FileMonitorInfo &monitor) { return monitor.filePath == file->get_path(); }) != dirMonitors.cend())) { if (!internal) { GThreadLock lock; reparseDirectory (); diff --git a/rtgui/filecatalog.h b/rtgui/filecatalog.h index 23d56af73..f41c42475 100644 --- a/rtgui/filecatalog.h +++ b/rtgui/filecatalog.h @@ -54,6 +54,13 @@ public: typedef sigc::slot DirSelectionSlot; private: + struct FileMonitorInfo { + FileMonitorInfo(const Glib::RefPtr &file_monitor, const Glib::ustring &file_path) : + fileMonitor(file_monitor), filePath(file_path) {} + Glib::RefPtr fileMonitor; + Glib::ustring filePath; + }; + FilePanel* filepanel; Gtk::Box* hBox; Glib::ustring selectedDirectory; @@ -95,6 +102,7 @@ private: Gtk::ToggleButton* bTrash; Gtk::ToggleButton* bNotTrash; Gtk::ToggleButton* bOriginal; + Gtk::ToggleButton* bRecursive; Gtk::ToggleButton* categoryButtons[20]; Gtk::ToggleButton* exifInfo; sigc::connection bCateg[20]; @@ -143,14 +151,15 @@ private: std::set editedFiles; guint modifierKey; // any modifiers held when rank button was pressed - Glib::RefPtr dirMonitor; + std::vector dirMonitors; IdleRegister idle_register; void addAndOpenFile (const Glib::ustring& fname); void addFile (const Glib::ustring& fName); - std::vector getFileList (); + std::vector getFileList(std::vector> *dirs_explored = nullptr); BrowserFilter getFilter (); + void refreshDirectoryMonitors(const std::vector> &dirs_to_monitor); void trashChanged (); public: @@ -240,6 +249,7 @@ public: void setExportPanel (ExportPanel* expanel); void exifInfoButtonToggled(); void categoryButtonToggled (Gtk::ToggleButton* b, bool isMouseClick); + void showRecursiveToggled(); bool capture_event(GdkEventButton* event); void filterChanged (); void runFilterDialog (); diff --git a/rtgui/options.cc b/rtgui/options.cc index 03c84f913..81271acde 100644 --- a/rtgui/options.cc +++ b/rtgui/options.cc @@ -435,6 +435,10 @@ void Options::setDefaults() parseExtensionsEnabled.clear(); parsedExtensions.clear(); parsedExtensionsSet.clear(); + browseRecursive = false; + browseRecursiveDepth = 10; + browseRecursiveMaxDirs = 100; + browseRecursiveFollowLinks = true; renameUseTemplates = false; renameTemplates.clear(); thumbnailZoomRatios.clear(); @@ -1346,6 +1350,22 @@ void Options::readFromFile(Glib::ustring fname) if (keyFile.has_key("File Browser", "SortDescending")) { sortDescending = keyFile.get_boolean("File Browser", "SortDescending"); } + + if (keyFile.has_key("File Browser", "BrowseRecursive")) { + browseRecursive = keyFile.get_boolean("File Browser", "BrowseRecursive"); + } + + if (keyFile.has_key("File Browser", "BrowseRecursiveDepth")) { + browseRecursiveDepth = keyFile.get_integer("File Browser", "BrowseRecursiveDepth"); + } + + if (keyFile.has_key("File Browser", "BrowseRecursiveMaxDirs")) { + browseRecursiveMaxDirs = keyFile.get_integer("File Browser", "BrowseRecursiveMaxDirs"); + } + + if (keyFile.has_key("File Browser", "BrowseRecursiveFollowLinks")) { + browseRecursiveFollowLinks = keyFile.get_integer("File Browser", "BrowseRecursiveFollowLinks"); + } } if (keyFile.has_group("Clipping Indication")) { @@ -2444,6 +2464,10 @@ void Options::saveToFile(Glib::ustring fname) } keyFile.set_integer("File Browser", "SortMethod", sortMethod); keyFile.set_boolean("File Browser", "SortDescending", sortDescending); + keyFile.set_boolean("File Browser", "BrowseRecursive", browseRecursive); + keyFile.set_integer("File Browser", "BrowseRecursiveDepth", browseRecursiveDepth); + keyFile.set_integer("File Browser", "BrowseRecursiveMaxDirs", browseRecursiveMaxDirs); + keyFile.set_boolean("File Browser", "BrowseRecursiveFollowLinks", browseRecursiveFollowLinks); keyFile.set_integer("Clipping Indication", "HighlightThreshold", highlightThreshold); keyFile.set_integer("Clipping Indication", "ShadowThreshold", shadowThreshold); keyFile.set_boolean("Clipping Indication", "BlinkClipped", blinkClipped); diff --git a/rtgui/options.h b/rtgui/options.h index 26a06999d..2b5df3aa6 100644 --- a/rtgui/options.h +++ b/rtgui/options.h @@ -314,6 +314,10 @@ public: std::vector parseExtensionsEnabled; // List of bool to retain extension or not std::vector parsedExtensions; // List containing all retained extensions (lowercase) std::set parsedExtensionsSet; // Set containing all retained extensions (lowercase) + bool browseRecursive; + int browseRecursiveDepth; + int browseRecursiveMaxDirs; + bool browseRecursiveFollowLinks; std::vector tpOpen; bool autoSaveTpOpen; //std::vector crvOpen; diff --git a/rtgui/preferences.cc b/rtgui/preferences.cc index 5bd2644d9..527800cb6 100644 --- a/rtgui/preferences.cc +++ b/rtgui/preferences.cc @@ -1480,6 +1480,28 @@ Gtk::Widget* Preferences::getFileBrowserPanel() maxRecentFolders->set_range(1, 25); vbro->pack_start(*hbrecent, Gtk::PACK_SHRINK, 4); + // Recursive browsing options. + Gtk::Box *hbBrowseRecursive = Gtk::manage(new Gtk::Box()); + Gtk::Label *labBrowseRecursiveDepth = Gtk::manage(new Gtk::Label(M("PREFERENCES_BROWSERECURSIVEDEPTH") + ":")); + browseRecursiveDepth = Gtk::manage(new Gtk::SpinButton()); + browseRecursiveDepth->set_digits(0); + browseRecursiveDepth->set_increments(1, 5); + browseRecursiveDepth->set_range(1, 999); + Gtk::Label *labBrowseRecursiveMaxDirs = Gtk::manage(new Gtk::Label(M("PREFERENCES_BROWSERECURSIVEMAXDIRS") + ":")); + browseRecursiveMaxDirs = Gtk::manage(new Gtk::SpinButton()); + browseRecursiveMaxDirs->set_digits(0); + browseRecursiveMaxDirs->set_increments(1, 5); + browseRecursiveMaxDirs->set_range(1, 999); + hbBrowseRecursive->pack_start(*labBrowseRecursiveDepth, Gtk::PACK_SHRINK, 4); + hbBrowseRecursive->pack_start(*browseRecursiveDepth, Gtk::PACK_SHRINK, 4); + hbBrowseRecursive->pack_start(*labBrowseRecursiveMaxDirs, Gtk::PACK_SHRINK, 4); + hbBrowseRecursive->pack_start(*browseRecursiveMaxDirs, Gtk::PACK_SHRINK, 4); + vbro->pack_start(*hbBrowseRecursive, Gtk::PACK_SHRINK, 0); +#ifndef _WIN32 + browseRecursiveFollowLinks = Gtk::manage(new Gtk::CheckButton(M("PREFERENCES_BROWSERECURSIVEFOLLOWLINKS"))); + vbro->pack_start(*browseRecursiveFollowLinks, Gtk::PACK_SHRINK, 0); +#endif + fro->add(*vbro); @@ -1936,6 +1958,11 @@ void Preferences::storePreferences() moptions.filmStripOverlayedFileNames = filmStripOverlayedFileNames->get_active(); moptions.sameThumbSize = sameThumbSize->get_active(); moptions.internalThumbIfUntouched = ckbInternalThumbIfUntouched->get_active(); + moptions.browseRecursiveDepth = static_cast(browseRecursiveDepth->get_value()); + moptions.browseRecursiveMaxDirs = static_cast(browseRecursiveMaxDirs->get_value()); + if (browseRecursiveFollowLinks) { + moptions.browseRecursiveFollowLinks = browseRecursiveFollowLinks->get_active(); + } auto save_where = saveParamsPreference->get_active_row_number(); moptions.saveParamsFile = save_where == 0 || save_where == 2; @@ -2165,6 +2192,11 @@ void Preferences::fillPreferences() filmStripOverlayedFileNames->set_active(moptions.filmStripOverlayedFileNames); sameThumbSize->set_active(moptions.sameThumbSize); ckbInternalThumbIfUntouched->set_active(moptions.internalThumbIfUntouched); + browseRecursiveDepth->set_value(moptions.browseRecursiveDepth); + browseRecursiveMaxDirs->set_value(moptions.browseRecursiveMaxDirs); + if (browseRecursiveFollowLinks) { + browseRecursiveFollowLinks->set_active(moptions.browseRecursiveFollowLinks); + } saveParamsPreference->set_active(moptions.saveParamsFile ? (moptions.saveParamsCache ? 2 : 0) : 1); diff --git a/rtgui/preferences.h b/rtgui/preferences.h index c6c3f32aa..b34a8348f 100644 --- a/rtgui/preferences.h +++ b/rtgui/preferences.h @@ -186,6 +186,9 @@ class Preferences final : Gtk::CheckButton* overlayedFileNames; Gtk::CheckButton* filmStripOverlayedFileNames; Gtk::CheckButton* sameThumbSize; + Gtk::SpinButton* browseRecursiveDepth; + Gtk::SpinButton* browseRecursiveMaxDirs; + Gtk::CheckButton* browseRecursiveFollowLinks{nullptr}; Gtk::SpinButton* threadsSpinBtn; Gtk::SpinButton* clutCacheSizeSB;