From cb209d1d563497ebb2381ec0f6f2ddc715442ef5 Mon Sep 17 00:00:00 2001 From: Bill Ferguson Date: Tue, 24 Mar 2026 20:29:35 -0400 Subject: [PATCH] official/selection2collection - create a temporary collection from a selection. Temporary collections are removed when darktable exits. Temporary collections can be kept by setting a preference in the Lua options. --- official/selection2collection.lua | 354 ++++++++++++++++++++++++++++++ 1 file changed, 354 insertions(+) create mode 100644 official/selection2collection.lua diff --git a/official/selection2collection.lua b/official/selection2collection.lua new file mode 100644 index 00000000..16631609 --- /dev/null +++ b/official/selection2collection.lua @@ -0,0 +1,354 @@ +--[[ + + selection2collection.lua - create a temporary collection from a selection + + Copyright (C) 2026 Bill Ferguson . + + 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 3 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, see . +]] +--[[ + selection2collection - create a temporary collection from a selection + + selection2collection creates a temporary collection, using functional + tagging, from a selection. The collection lasts until darktable exits + and then the collections are removed, but not the images. + + To create the temporary collection, a function tag (darktable|functional|s2c) + tag with a date and a time is applied to the selected images. The collection + module filters on the tag to display the connection. More than one temporary + collection can be created and collections can be changed by selecting the + appropriate tag. + + The tags are autmatically destroyed when darktable exits, thus removing the + collections. There is a preference in the Lua options to keep the collections + across darktable runs. + + ADDITIONAL SOFTWARE NEEDED FOR THIS SCRIPT + + None + + USAGE + + * start from script_manager + * make a selection + * click the create temporary collection button the the actions on selected images + module + + BUGS, COMMENTS, SUGGESTIONS + + Bill Ferguson + + CHANGES +]] + +local dt = require "darktable" +local du = require "lib/dtutils" +-- local df = require "lib/dtutils.file" +-- local ds = require "lib/dtutils.string" +-- local dtsys = require "lib/dtutils.system" +local log = require "lib/dtutils.log" +-- local debug = require "darktable.debug" + + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- A P I C H E C K +-- - - - - - - - - - - - - - - - - - - - - - - - + +du.check_min_api_version("9.6.0", MODULE) -- choose the minimum version that contains the features you need + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - +-- I 1 8 N +-- - - - - - - - - - - - - - - - - - - - - - - - - - + +local gettext = dt.gettext.gettext + +local function _(msgid) + return gettext(msgid) +end + + +-- - - - - - - - - - - - - - - - - - - - - - - - - - +-- S C R I P T M A N A G E R I N T E G R A T I O N +-- - - - - - - - - - - - - - - - - - - - - - - - - - + +local script_data = {} + +script_data.destroy = nil -- function to destory the script +script_data.destroy_method = nil -- set to hide for libs since we can't destroy them commpletely yet +script_data.restart = nil -- how to restart the (lib) script after it's been hidden - i.e. make it visible again +script_data.show = nil -- only required for libs since the destroy_method only hides them + +script_data.metadata = { + name = _("selection2collection"), -- visible name of script + purpose = _("create a temporary collection from a selection"), -- purpose of script + author = "Bill Ferguson ", -- your name and optionally e-mail address + help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/official/selection2collection/" -- URL to help/documentation +} + + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- C O N S T A N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local MODULE = "selection2collection" +local DEFAULT_LOG_LEVEL = log.info +local TMP_DIR = dt.configuration.tmp_dir + +-- path separator +local PS = dt.configuration.running_os == "windows" and "\\" or "/" + +-- command separator +local CS = dt.configuration.running_os == "windows" and "&" or ";" + +-- script specific + +local TAG_HEADER = "darktable|functional|s2c|" + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- L O G L E V E L +-- - - - - - - - - - - - - - - - - - - - - - - - + +log.log_level(DEFAULT_LOG_LEVEL) + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- N A M E S P A C E +-- - - - - - - - - - - - - - - - - - - - - - - - + +local selection2collection = {} + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- G L O B A L V A R I A B L E S +-- - - - - - - - - - - - - - - - - - - - - - - - + +selection2collection.log_level = DEFAULT_LOG_LEVEL +selection2collection.keep_collections = false +selection2collection.collection_rules = {} + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- P R E F E R E N C E S +-- - - - - - - - - - - - - - - - - - - - - - - - + +dt.preferences.register(MODULE, "keep_collections", "bool", + script_data.metadata.name .. _(" keep collections created from selection"), + _("keep collections created from selections"), false) + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- A L I A S E S +-- - - - - - - - - - - - - - - - - - - - - - - - + +local namespace = selection2collection +local s2c = selection2collection + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- F U N C T I O N S +-- - - - - - - - - - - - - - - - - - - - - - - - + +------------------- +-- helper functions +------------------- + +local function set_log_level(level) + if not level then + level = namespace.log_level + end + + local old_log_level = log.log_level() + log.log_level(level) + + return old_log_level +end + +local function restore_log_level(level) + if not level then + level = namespace.log_level + end + log.log_level(level) +end + +local function reset_log_level() + log.log_level(DEFAULT_LOG_LEVEL) + namespace.log_level = DEFAULT_LOG_LEVEL +end + +local function pref_read(name, pref_type) + local old_log_level = set_log_level(namespace.log_level) + + log.msg(log.debug, "name is " .. name .. " and type is " .. pref_type) + + local val = dt.preferences.read(MODULE, name, pref_type) + + log.msg(log.debug, "read value " .. tostring(val)) + + restore_log_level(old_log_level) + return val +end + +local function pref_write(name, pref_type, value) + local old_log_level = set_log_level(namespace.log_level) + + log.msg(log.debug, "writing value " .. tostring(value) .. " for name " .. name) + + dt.preferences.write(MODULE, name, pref_type, value) + + restore_log_level(old_log_level) +end + +------------------- +-- program functions +------------------- + +local function clear_collections() + s2c.keep_collections = pref_read("keep_collections", "bool") + + if not s2c.keep_collections then + for _, tag in ipairs(dt.tags) do + if string.match(tag.name, TAG_HEADER) then + tag:delete() + end + end + end +end + +local function create_collection_tag() + + local t = os.date("*t") + + local ymd = string.format("%4d:%02d:%02d", t.year, t.month, t.day) + local hms = string.format("%02d:%02d:%02d", t.hour, t.min, t.sec) + + local tag_string = TAG_HEADER .. ymd .. "|" .. hms + dt.print_log("tag_string is " .. tag_string) + + local tag = dt.tags.create(tag_string) + + return tag +end + +local function display_collection(tag_name) + local collection_rules = dt.gui.libs.collect.filter() + + local rule = dt.gui.libs.collect.new_rule() + + rule.mode = "DT_LIB_COLLECT_MODE_AND" + rule.data = tostring(tag_name) + rule.item = "DT_COLLECTION_PROP_TAG" + + table.insert(collection_rules, rule) + + -- dt.gui.libs.collect.filter returns the previous rule set when you install another + s2c.collection_rules = dt.gui.libs.collect.filter(collection_rules) +end + +local function create_collection() + local images = dt.gui.selection() + + if #images > 1 then + + local tag = create_collection_tag() + + if tag then + for _, image in ipairs(images) do + image:attach_tag(tag) + end + end + + display_collection(tag.name) + else + log.msg(log.screen, _("not enough images to create a collection")) + end +end + +local function communications_handler(sender, receiver, message) + if receiver == MODULE then + if message == "create" then + create_collection() + end + end +end + +local function set_button_sensitive(images) + if #images > 1 then + dt.gui.libs.image.set_sensitive(MODULE, true) + else + dt.gui.libs.image.set_sensitive(MODULE, false) + end +end + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- M A I N P R O G R A M +-- - - - - - - - - - - - - - - - - - - - - - - - + +s2c.keep_collections = pref_read("keep_collections", "bool") + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- U S E R I N T E R F A C E +-- - - - - - - - - - - - - - - - - - - - - - - - + +dt.gui.libs.image.register_action( + MODULE, _("create temporary collection"), + function(event, images) + create_collection() + end, + _("convert selection to collection") +) + +set_button_sensitive(dt.gui.selection()) +-- - - - - - - - - - - - - - - - - - - - - - - - +-- D A R K T A B L E I N T E G R A T I O N +-- - - - - - - - - - - - - - - - - - - - - - - - + +local function destroy() + dt.destroy_event(MODULE, "inter-script-communication") + dt.destroy_event(MODULE, "exit") + dt.destroy_event(MODULE, "selection_changed") + dt.gui.libs.destroy_action(MODULE, "create_collection") +end + +script_data.destroy = destroy + +-- - - - - - - - - - - - - - - - - - - - - - - - +-- E V E N T S +-- - - - - - - - - - - - - - - - - - - - - - - - + +dt.register_event(MODULE, "inter-script-communication", + function(event, sender, receiver, message) + communications_handler(sender, receiver, message) + end +) + +if not dt.query_event(MODULE, "shortcut") then -- skip if shortcut already registered + dt.register_event(MODULE, "shortcut", + function(event, shortcut) + create_collection() + end, _("convert selection to collection") + ) +end + +dt.register_event(MODULE, "exit", + function(event) + if not s2c.keep_collections then + dt.gui.libs.collect.filter(s2c.collection_rules) + clear_collections() + end + end +) + +dt.register_event(MODULE, "selection-changed", + function(event) + set_button_sensitive(dt.gui.selection()) + end +) + +return script_data