diff --git a/rtdata/languages/default b/rtdata/languages/default index 87ecd740f..0e922fc4f 100644 --- a/rtdata/languages/default +++ b/rtdata/languages/default @@ -2107,7 +2107,26 @@ QUEUE_DESTPREVIEW_TOOLTIP;Destination path for the first selected image appears QUEUE_FORMAT_TITLE;File Format QUEUE_LOCATION_FOLDER;Save to folder QUEUE_LOCATION_TEMPLATE;Use template -QUEUE_LOCATION_TEMPLATE_TOOLTIP;Specify the output location based on the source photo's location, rank, trash status or position in the queue.\n\n%dN, %d-N, %pN, %p-N, %PN and %P-N (N = 1..9) will be replaced by elements of the image file's directory path (not including the file name):\n%dN = Nth directory from the end of the path\n%d-N = Nth directory from the start of the path\n%pN = all directories up to the Nth from the end of the path\n%p-N = the first N directories in the path\n%PN = the last N directories in the path\n%P-N = all directories from the Nth to the end of the path\n%f = base filename (no extension)\nFor Windows paths, %d-1 is the drive letter and colon, and %d-2 is the base directory on that drive.\n\nUsing the following pathname as an example:\n/home/tom/photos/2010-10-31/photo1.raw\nthe meaning of the formatting strings follows:\n%d4 = %d-1 = home\n%d3 = %d-2 = tom\n%d2 = %d-3 = photos\n%d1 = %d-4 = 2010-10-31\n%p1 = %p-4 = /home/tom/photos/2010-10-31/\n%p2 = %p-3 = /home/tom/photos/\n%p3 = %p-2 = /home/tom/\n%p4 = %p-1 = /home/\n%P1 = %P-4 = 2010-10-31/\n%P2 = %P-3 = photos/2010-10-31/\n%P3 = %P-2 = tom/photos/2010-10-31/\n%P4 = %P-1 = /home/tom/photos/2010-10-31/\n%f = photo1\n\n%r will be replaced by the photo's rank. If the photo is unranked, '0' is used. If the photo is in the trash, 'x' is used.\n\n%s1, ..., %s9 will be replaced by the photo's initial position in the queue at the time the queue is started. The number specifies the padding, e.g. %s3 results in '001'.\n\nIf you want to save the output image alongside the source image, write:\n%p1/%f\n\nIf you want to save the output image in a folder named 'converted' located in the source photo's folder, write:\n%p1/converted/%f\n\nIf you want to save the output image in\n'/home/tom/photos/converted/2010-10-31', write:\n%p-3/converted/%P-4/%f +QUEUE_LOCATION_TEMPLATE_TOOLTIP;Specify the output location based on characteristics such as the source photo's location, rank, trash status or position in the queue.\n\nThe output template field value can include specifiers beginning with %, which are replaced by those characteristics in the actual destination path.\n\nPress the ? button for full instructions. +QUEUE_LOCATION_TEMPLATE_HELP_BUTTON_TOOLTIP;Show or hide a help panel with instructions for creating location templates +QUEUE_LOCATION_TEMPLATE_HELP_TITLE;Creating an output template +QUEUE_LOCATION_TEMPLATE_HELP_INTRO;The output template field allows you to to dynamically customize the destination folder and filename. When you include certain specifiers, which begin with %, they are replaced by the program when each file is being saved.\n\nThe sections below describe each type of specifier. +QUEUE_LOCATION_TEMPLATE_HELP_PATHS_TITLE;Directories and partial paths +QUEUE_LOCATION_TEMPLATE_HELP_PATHS_INTRO;The %dN, %d-N, %pN, %p-N, %PN and %P-N (N = 1..9) specifiers will be replaced by elements of the image file's directory path.\nThe format specifiers operate as follows:\n %dN = Nth directory from the end of the path\n %d-N = Nth directory from the start of the path\n %pN = all directories up to the Nth from the end of the path\n %p-N = the first N directories in the path\n %PN = the last N directories in the path\n %P-N = all directories from the Nth to the end of the path\n %f = base filename (no extension) +QUEUE_LOCATION_TEMPLATE_HELP_PATHS_INTRO_WINDOWS;For Windows paths, %d-1 is the drive letter and colon, and %d-2 is the base directory on that drive. +QUEUE_LOCATION_TEMPLATE_HELP_PATHS_BODY_1;Using this pathname as an example: +QUEUE_LOCATION_TEMPLATE_HELP_PATHS_EXAMPLE_LINUX;/home/tom/photos/2010-10-31/photo1.raw +QUEUE_LOCATION_TEMPLATE_HELP_PATHS_EXAMPLE_WINDOWS;D:\tom\photos\2010-10-31\photo1.raw +QUEUE_LOCATION_TEMPLATE_HELP_PATHS_BODY_2;The meanings of the formatting strings are: +QUEUE_LOCATION_TEMPLATE_HELP_RESULT_MISMATCH;ERROR: 2nd result is different: +QUEUE_LOCATION_TEMPLATE_HELP_RANK_TITLE;Rank +QUEUE_LOCATION_TEMPLATE_HELP_RANK_BODY;%r will be replaced by the photo's rank. If the photo is unranked, '0' is used. If the photo is in the trash, 'x' is used. +QUEUE_LOCATION_TEMPLATE_HELP_SEQUENCE_TITLE;Position/sequence in queue +QUEUE_LOCATION_TEMPLATE_HELP_SEQUENCE_BODY;%s1, ..., %s9 will be replaced by the photo's initial position in the queue at the time the queue is started. The number specifies the padding, e.g. %s3 results in '001'. +QUEUE_LOCATION_TEMPLATE_HELP_TIMESTAMP_TITLE;Date and time +QUEUE_LOCATION_TEMPLATE_HELP_TIMESTAMP_BODY;Three different date/time values may be used in templates:\n %tE"%Y-%m-%d" = when export started\n %tF"%Y-%m-%d" = when file was last saved\n %tP"%Y-%m-%d" = when photo was taken\nThe quoted string defines the format of the resulting date and/or time. The format string %tF"%Y-%m-%d" is just one example. The string can use all conversion specifiers defined for the g_date_time_format function (see https://docs.gtk.org/glib/method.DateTime.format.html).\n\nExample format strings: +QUEUE_LOCATION_TEMPLATE_HELP_EXAMPLES_TITLE;Common examples +QUEUE_LOCATION_TEMPLATE_HELP_EXAMPLES_BODY;If you want to save the output image alongside the source image, write:\n%p1/%f\n\nIf you want to save the output image in a folder named 'converted' located in the source photo's folder, write:\n%p1/converted/%f\n\nIf you want to save the output image in\n'/home/tom/photos/converted/2010-10-31', write:\n%p-3/converted/%P-4/%f QUEUE_LOCATION_TITLE;Output Location QUEUE_STARTSTOP_TOOLTIP;Start or stop processing the images in the queue.\n\nShortcut: Ctrl+s SAMPLEFORMAT_0;Unknown data format diff --git a/rtgui/batchqueue.cc b/rtgui/batchqueue.cc index 2811e3445..c275ee675 100644 --- a/rtgui/batchqueue.cc +++ b/rtgui/batchqueue.cc @@ -20,6 +20,7 @@ #include #include #include +#include "../rtengine/imagedata.h" #include "../rtengine/rt_math.h" #include "../rtengine/procparams.h" @@ -94,6 +95,21 @@ namespace // local helper functions } } } + + // Look in templateText at index ix for quoted string containing a time format string, and + // use that string to format dateTime. Append the formatted time to path. + void appendFormattedTime(Glib::ustring& path, unsigned int& ix, const Glib::ustring& templateText, const Glib::DateTime& dateTime) + { + constexpr gunichar quoteMark('"'); + if ((ix + 1) < templateText.size() && templateText[ix] == quoteMark) { + const auto endPos = templateText.find_first_of(quoteMark, ++ix); + if (endPos != Glib::ustring::npos) { + Glib::ustring formatString(templateText, ix, endPos-ix); + path += dateTime.format(formatString); + ix = endPos; + } + } + } } BatchQueue::BatchQueue (FileCatalog* aFileCatalog) : processing(nullptr), fileCatalog(aFileCatalog), sequence(0), listener(nullptr) @@ -1001,6 +1017,43 @@ Glib::ustring BatchQueue::calcAutoFileNameBase (const Glib::ustring& origFileNam seqstr << sequence; path += seqstr.str (); + } else if (options.savePathTemplate[ix] == 't') { + // Insert formatted date/time value. Character after 't' defines time source + if (++ix < options.savePathTemplate.size()) { + Glib::DateTime dateTime; + switch(options.savePathTemplate[ix++]) + { + case 'E': // (approximate) time when export started + { + dateTime = Glib::DateTime::create_now_local(); + break; + } + case 'F': // time when file was last saved + { + Glib::RefPtr file = Gio::File::create_for_path(origFileName); + if (file) { + Glib::RefPtr info = file->query_info(G_FILE_ATTRIBUTE_TIME_MODIFIED); + if (info) { + dateTime = info->get_modification_date_time(); + } + } + break; + } + case 'P': // time when picture was taken + { + const auto timestamp = FramesData(origFileName).getDateTimeAsTS(); + dateTime = Glib::DateTime::create_now_local(timestamp); + break; + } + default: + { + break; + } + } + if (dateTime) { + appendFormattedTime(path, ix, options.savePathTemplate, dateTime); + } + } } } diff --git a/rtgui/batchqueuepanel.cc b/rtgui/batchqueuepanel.cc index e35149326..bf6cc0772 100644 --- a/rtgui/batchqueuepanel.cc +++ b/rtgui/batchqueuepanel.cc @@ -73,7 +73,10 @@ BatchQueuePanel::BatchQueuePanel (FileCatalog* aFileCatalog) : parent(nullptr) hb2->pack_start (*useTemplate, Gtk::PACK_SHRINK, 4); outdirTemplate = Gtk::manage (new Gtk::Entry ()); hb2->pack_start (*outdirTemplate); - odvb->pack_start (*hb2, Gtk::PACK_SHRINK, 4); + templateHelpButton = Gtk::manage (new Gtk::ToggleButton("?")); + templateHelpButton->set_tooltip_markup (M ("QUEUE_LOCATION_TEMPLATE_HELP_BUTTON_TOOLTIP")); + hb2->pack_start (*templateHelpButton, Gtk::PACK_SHRINK, 0); + odvb->pack_start (*hb2, Gtk::PACK_SHRINK, 0); outdirTemplate->set_tooltip_markup (M("QUEUE_LOCATION_TEMPLATE_TOOLTIP")); useTemplate->set_tooltip_markup (M("QUEUE_LOCATION_TEMPLATE_TOOLTIP")); Gtk::Box* hb3 = Gtk::manage (new Gtk::Box ()); @@ -136,6 +139,7 @@ BatchQueuePanel::BatchQueuePanel (FileCatalog* aFileCatalog) : parent(nullptr) outdirTemplate->signal_changed().connect (sigc::mem_fun(*this, &BatchQueuePanel::saveOptions)); useTemplate->signal_toggled().connect (sigc::mem_fun(*this, &BatchQueuePanel::saveOptions)); useFolder->signal_toggled().connect (sigc::mem_fun(*this, &BatchQueuePanel::saveOptions)); + templateHelpButton->signal_toggled().connect (sigc::mem_fun(*this, &BatchQueuePanel::templateHelpButtonToggled)); saveFormatPanel->setListener (this); // setup button bar @@ -147,8 +151,19 @@ BatchQueuePanel::BatchQueuePanel (FileCatalog* aFileCatalog) : parent(nullptr) topBox->pack_start (*fdir, Gtk::PACK_EXPAND_WIDGET, 4); topBox->pack_start (*fformat, Gtk::PACK_EXPAND_WIDGET, 4); + middleSplitPane = Gtk::manage (new Gtk::Paned(Gtk::ORIENTATION_HORIZONTAL)); + templateHelpTextView = Gtk::manage (new Gtk::TextView()); + templateHelpTextView->set_editable(false); + templateHelpTextView->set_wrap_mode(Gtk::WRAP_WORD); + scrolledTemplateHelpWindow = Gtk::manage(new Gtk::ScrolledWindow()); + scrolledTemplateHelpWindow->add(*templateHelpTextView); + middleSplitPane->pack1 (*scrolledTemplateHelpWindow); + middleSplitPane->pack2 (*batchQueue); + scrolledTemplateHelpWindow->set_visible(false); // initially hidden, templateHelpButton shows it + scrolledTemplateHelpWindow->set_no_show_all(true); + // add middle browser area - pack_start (*batchQueue); + pack_start (*middleSplitPane); // lower box with thumbnail zoom bottomBox = Gtk::manage (new Gtk::Box ()); @@ -322,6 +337,122 @@ void BatchQueuePanel::setGuiFromBatchState(bool queueRunning, int qsize) updateTab(qsize); } +void BatchQueuePanel::templateHelpButtonToggled() +{ + bool visible = templateHelpButton->get_active(); + auto buffer = templateHelpTextView->get_buffer(); + if (buffer->get_text().empty()) { + // Populate the help text the first time it's shown + populateTemplateHelpBuffer(buffer); + const auto fullWidth = middleSplitPane->get_width(); + middleSplitPane->set_position(fullWidth / 2); + } + scrolledTemplateHelpWindow->set_visible(visible); + templateHelpTextView->set_visible(visible); +} + +void BatchQueuePanel::populateTemplateHelpBuffer(Glib::RefPtr buffer) +{ + auto pos = buffer->begin(); + const auto insertTopicHeading = [&pos, buffer](const Glib::ustring& text) { + pos = buffer->insert_markup(pos, Glib::ustring::format("\n\n", text, "\n")); + }; + const auto insertTopicBody = [&pos, buffer](const Glib::ustring& text) { + pos = buffer->insert_markup(pos, Glib::ustring::format("\n", text, "\n")); + }; + const auto mainTitle = M("QUEUE_LOCATION_TEMPLATE_HELP_TITLE"); + pos = buffer->insert_markup(pos, Glib::ustring::format("", mainTitle, "\n")); + pos = buffer->insert_markup(pos, M("QUEUE_LOCATION_TEMPLATE_HELP_INTRO")); + + insertTopicHeading(M("QUEUE_LOCATION_TEMPLATE_HELP_EXAMPLES_TITLE")); + pos = buffer->insert_markup(pos, M("QUEUE_LOCATION_TEMPLATE_HELP_EXAMPLES_BODY")); + + insertTopicHeading(M("QUEUE_LOCATION_TEMPLATE_HELP_PATHS_TITLE")); + pos = buffer->insert_markup(pos, M("QUEUE_LOCATION_TEMPLATE_HELP_PATHS_INTRO")); + pos = buffer->insert(pos, "\n"); +#ifdef _WIN32 + pos = buffer->insert_markup(pos, M("QUEUE_LOCATION_TEMPLATE_HELP_PATHS_INTRO_WINDOWS")); + pos = buffer->insert(pos, "\n"); +#endif + pos = buffer->insert(pos, "\n"); + pos = buffer->insert_markup(pos, M("QUEUE_LOCATION_TEMPLATE_HELP_PATHS_BODY_1")); +#ifdef _WIN32 + const auto exampleFilePath = M("QUEUE_LOCATION_TEMPLATE_HELP_PATHS_EXAMPLE_WINDOWS"); +#else + const auto exampleFilePath = M("QUEUE_LOCATION_TEMPLATE_HELP_PATHS_EXAMPLE_LINUX"); +#endif + pos = buffer->insert_markup(pos, Glib::ustring::format("\n ", exampleFilePath, "\n")); + pos = buffer->insert_markup(pos, M("QUEUE_LOCATION_TEMPLATE_HELP_PATHS_BODY_2")); + // Examples are generated from exampleFilePath using the actual template processing function + const Options savedOptions = options; // to be restored after generating example results + options.saveUsePathTemplate = true; + // Since this code only ever runs once (the first time the help text is presented), no attempt is + // made to be efficient. Use a brute-force method to discover the number of elements in exampleFilePath. + int pathElementCount = 0; + for (int n=9; n>=0; n--) { + options.savePathTemplate = Glib::ustring::format("%d", n); + const auto result = BatchQueue::calcAutoFileNameBase(exampleFilePath); + if (!result.empty()) { + // The 'd' specifier returns an empty string if N exceeds the number of path elements, so + // the largest N that does not return an empty string is the number of elements in exampleFilePath. + pathElementCount = n; + break; + } + } + // Function inserts examples for a particular specifier, with every valid N value for the + // number of elements in the path. + const auto insertPathExamples = [&buffer, &pos, pathElementCount, exampleFilePath](char letter, int offset1, int mult1, int offset2, int mult2) + { + for (int n=0; ninsert_markup(pos, Glib::ustring::format("\n ", path1, " = ", path2, " = ", result1, "")); + if (result1 != result2) { + // If this error appears, it indicates a coding error in either BatchQueue::calcAutoFileNameBase + // or BatchQueuePanel::populateTemplateHelpBuffer. + pos = buffer->insert_markup(pos, Glib::ustring::format(" ", M("QUEUE_LOCATION_TEMPLATE_HELP_RESULT_MISMATCH"), " ", result2)); + } + } + }; + // Example outputs in comments below are for a 4-element path. + insertPathExamples('d', pathElementCount, -1, -1, -1); // %d4 = %d-1 = home + insertPathExamples('p', 1, 1, -pathElementCount, 1); // %p1 = %p-4 = /home/tom/photos/2010-10-31/ + insertPathExamples('P', 1, 1, -pathElementCount, 1); // %P1 = %P-4 = 2010-10-31/ + { + const Glib::ustring fspecifier("%f"); + options.savePathTemplate = fspecifier; + const auto result = BatchQueue::calcAutoFileNameBase(exampleFilePath); + pos = buffer->insert_markup(pos, Glib::ustring::format("\n ", fspecifier, " = ", result, "")); + } + + insertTopicHeading(M("QUEUE_LOCATION_TEMPLATE_HELP_RANK_TITLE")); + pos = buffer->insert_markup(pos, M("QUEUE_LOCATION_TEMPLATE_HELP_RANK_BODY")); + + insertTopicHeading(M("QUEUE_LOCATION_TEMPLATE_HELP_SEQUENCE_TITLE")); + pos = buffer->insert_markup(pos, M("QUEUE_LOCATION_TEMPLATE_HELP_SEQUENCE_BODY")); + + insertTopicHeading(M("QUEUE_LOCATION_TEMPLATE_HELP_TIMESTAMP_TITLE")); + pos = buffer->insert_markup(pos, M("QUEUE_LOCATION_TEMPLATE_HELP_TIMESTAMP_BODY")); + const Glib::ustring dateTimeFormatExamples[] = { + "%Y-%m-%d", + "%Y%m%d_%H%M%S", + "%y/%b/%-d/" + }; + const auto timezone = Glib::DateTime::create_now_local().get_timezone(); + const auto timeForExamples = Glib::DateTime::create_from_iso8601("2001-02-03T04:05:06.123456", timezone); + for (auto && fmt : dateTimeFormatExamples) { + const auto result = timeForExamples.format(fmt); + pos = buffer->insert_markup(pos, Glib::ustring::format("\n %tE\"", fmt, "\" = ", result, "")); + } + + pos = buffer->insert(pos, "\n"); + options = savedOptions; // Do not add any lines in this function below here +} + void BatchQueuePanel::addBatchQueueJobs(const std::vector& entries, bool head) { batchQueue->addEntries(entries, head); diff --git a/rtgui/batchqueuepanel.h b/rtgui/batchqueuepanel.h index d73f8b893..417d8a4cb 100644 --- a/rtgui/batchqueuepanel.h +++ b/rtgui/batchqueuepanel.h @@ -52,8 +52,12 @@ class BatchQueuePanel : public Gtk::Box, RTWindow* parent; BatchQueue* batchQueue; + Gtk::TextView* templateHelpTextView; + Gtk::ScrolledWindow* scrolledTemplateHelpWindow; + Gtk::ToggleButton* templateHelpButton; Gtk::Box* bottomBox; Gtk::Box* topBox; + Gtk::Paned* middleSplitPane; std::atomic queueShouldRun; @@ -80,6 +84,8 @@ private: void stopBatchProc (); void startOrStopBatchProc(); void setGuiFromBatchState(bool queueRunning, int qsize); + void templateHelpButtonToggled(); + void populateTemplateHelpBuffer(Glib::RefPtr buffer); void pathFolderChanged (); void pathFolderButtonPressed ();