further improved customizability
to add a new module, simply add an entry to MODULE_PROFILES below. direct usage of action paths from darktable's shortcut interface.
This commit is contained in:
@@ -51,6 +51,10 @@ midi:C0=iop/channelmixerrgb;focus
|
||||
midi:C#0=iop/colorequal;focus
|
||||
midi:D0=iop/colorequal/page;previous
|
||||
midi:D#0=iop/colorequal/page;next
|
||||
|
||||
USAGE:
|
||||
To add a new module, simply add an entry to MODULE_PROFILES below.
|
||||
Get action paths from darktable's shortcut interface.
|
||||
]]
|
||||
|
||||
local dt = require "darktable"
|
||||
@@ -66,71 +70,160 @@ end
|
||||
-- Constants
|
||||
local NUM_KNOBS = 8
|
||||
local NAN = 0/0
|
||||
local darkroom_view = dt.gui.views.darkroom
|
||||
|
||||
-- Configuration tables
|
||||
local MASK_PROPERTIES = {
|
||||
"opacity", "size", "feather", "hardness",
|
||||
"rotation", "curvature", "compression"
|
||||
}
|
||||
-- ============================================================================
|
||||
-- USER CONFIGURATION
|
||||
-- ============================================================================
|
||||
|
||||
local COLOR_ELEMENTS = {
|
||||
-- Module Profiles: Define behavior when each module has focus
|
||||
-- Structure: [module_name] = { knob1_action, knob2_action, ... }
|
||||
local MODULE_PROFILES = {
|
||||
|
||||
-- Color Zones: Control individual color channels
|
||||
colorzones = {
|
||||
"red", "orange", "yellow", "green",
|
||||
"aqua", "blue", "purple", "magenta"
|
||||
{"iop/colorzones/graph", "red"},
|
||||
{"iop/colorzones/graph", "orange"},
|
||||
{"iop/colorzones/graph", "yellow"},
|
||||
{"iop/colorzones/graph", "green"},
|
||||
{"iop/colorzones/graph", "aqua"},
|
||||
{"iop/colorzones/graph", "blue"},
|
||||
{"iop/colorzones/graph", "purple"},
|
||||
{"iop/colorzones/graph", "magenta"}
|
||||
},
|
||||
|
||||
-- Color Equalizer: Control color channels
|
||||
colorequal = {
|
||||
"red", "orange", "yellow", "green",
|
||||
"cyan", "blue", "lavender", "magenta"
|
||||
}
|
||||
}
|
||||
|
||||
local MODULE_CONFIGS = {
|
||||
{"iop/colorequal/graph", "red"},
|
||||
{"iop/colorequal/graph", "orange"},
|
||||
{"iop/colorequal/graph", "yellow"},
|
||||
{"iop/colorequal/graph", "green"},
|
||||
{"iop/colorequal/graph", "cyan"},
|
||||
{"iop/colorequal/graph", "blue"},
|
||||
{"iop/colorequal/graph", "lavender"},
|
||||
{"iop/colorequal/graph", "magenta"}
|
||||
},
|
||||
|
||||
-- Sigmoid: Control primary color adjustments
|
||||
sigmoid = {
|
||||
"red attenuation", "red rotation",
|
||||
"green attenuation", "green rotation",
|
||||
"blue attenuation", "blue rotation",
|
||||
"recover purity"
|
||||
{"iop/sigmoid/primaries/red attenuation"},
|
||||
{"iop/sigmoid/primaries/red rotation"},
|
||||
{"iop/sigmoid/primaries/green attenuation"},
|
||||
{"iop/sigmoid/primaries/green rotation"},
|
||||
{"iop/sigmoid/primaries/blue attenuation"},
|
||||
{"iop/sigmoid/primaries/blue rotation"},
|
||||
{"iop/sigmoid/primaries/recover purity"}
|
||||
},
|
||||
|
||||
-- Color Calibration (Primaries)
|
||||
primaries = {
|
||||
"red hue", "red purity", "green hue", "green purity",
|
||||
"blue hue", "blue purity", "tint hue", "tint purity"
|
||||
{"iop/primaries/red hue"},
|
||||
{"iop/primaries/red purity"},
|
||||
{"iop/primaries/green hue"},
|
||||
{"iop/primaries/green purity"},
|
||||
{"iop/primaries/blue hue"},
|
||||
{"iop/primaries/blue purity"},
|
||||
{"iop/primaries/tint hue"},
|
||||
{"iop/primaries/tint purity"}
|
||||
},
|
||||
|
||||
-- AgX
|
||||
agx = {
|
||||
"white relative exposure",
|
||||
"black relative exposure",
|
||||
"curve/pivot input shift",
|
||||
"curve/pivot target output",
|
||||
"curve/contrast",
|
||||
"curve/toe power",
|
||||
"curve/shoulder power",
|
||||
"curve/curve y gamma"
|
||||
}
|
||||
{"iop/agx/white relative exposure"},
|
||||
{"iop/agx/black relative exposure"},
|
||||
{"iop/agx/curve/pivot input shift"},
|
||||
{"iop/agx/curve/pivot target output"},
|
||||
{"iop/agx/curve/contrast"},
|
||||
{"iop/agx/curve/toe power"},
|
||||
{"iop/agx/curve/shoulder power"},
|
||||
{"iop/agx/curve/curve y gamma"}
|
||||
},
|
||||
|
||||
-- Tone Equalizer: Control EV bands
|
||||
-- For 8 knobs, using offsets -8 to get bands
|
||||
toneequal = {
|
||||
{"iop/toneequal/simple/-8 EV"},
|
||||
{"iop/toneequal/simple/-7 EV"},
|
||||
{"iop/toneequal/simple/-6 EV"},
|
||||
{"iop/toneequal/simple/-5 EV"},
|
||||
{"iop/toneequal/simple/-4 EV"},
|
||||
{"iop/toneequal/simple/-3 EV"},
|
||||
{"iop/toneequal/simple/-2 EV"},
|
||||
{"iop/toneequal/simple/-1 EV"}
|
||||
},
|
||||
|
||||
-- Color Balance RGB: Selected controls
|
||||
colorbalancergb = {
|
||||
nil, -- Knob 1: unused --> default
|
||||
{"iop/colorbalancergb/global vibrance"},
|
||||
{"iop/colorbalancergb/contrast"},
|
||||
{"iop/colorbalancergb/global chroma"},
|
||||
{"iop/colorbalancergb/global saturation"},
|
||||
{"iop/colorbalancergb/global brilliance"},
|
||||
nil, -- Knob 7: --> default
|
||||
nil -- Knob 8: --> default
|
||||
},
|
||||
|
||||
-- Channel Mixer RGB (Color Calibration): Tab control and sliders
|
||||
channelmixerrgb = {
|
||||
nil, -- Knob 1-4: --> default
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
{"iop/channelmixerrgb/page", "CAT", "tab"}, -- Knob 5: Tab selector
|
||||
{"iop/focus/sliders", "1st"}, -- Knob 6: First slider
|
||||
{"iop/focus/sliders", "2nd"}, -- Knob 7: Second slider
|
||||
{"iop/focus/sliders", "3rd"} -- Knob 8: Third slider
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
local DEFAULT_MAPPINGS = {
|
||||
"iop/exposure/exposure",
|
||||
"iop/filmicrgb/white relative exposure",
|
||||
"iop/filmicrgb/black relative exposure",
|
||||
"iop/filmicrgb/contrast",
|
||||
"iop/crop/left",
|
||||
"iop/crop/right",
|
||||
"iop/crop/top",
|
||||
"iop/crop/bottom"
|
||||
{"iop/exposure/exposure"},
|
||||
{"iop/filmicrgb/white relative exposure"},
|
||||
{"iop/filmicrgb/black relative exposure"},
|
||||
{"iop/filmicrgb/contrast"},
|
||||
{"iop/crop/left"},
|
||||
{"iop/crop/right"},
|
||||
{"iop/crop/top"},
|
||||
{"iop/crop/bottom"}
|
||||
}
|
||||
-- Mask properties: Always checked first, regardless of module focus
|
||||
local MASK_PROPERTIES = {
|
||||
{"lib/masks/properties/opacity"},
|
||||
{"lib/masks/properties/size"},
|
||||
{"lib/masks/properties/feather"},
|
||||
{"lib/masks/properties/hardness"},
|
||||
{"lib/masks/properties/rotation"},
|
||||
{"lib/masks/properties/curvature"},
|
||||
{"lib/masks/properties/compression"}
|
||||
}
|
||||
|
||||
-- Priority order for checking module focus (most commonly used first)
|
||||
local MODULE_CHECK_ORDER = {
|
||||
"toneequal",
|
||||
"colorequal",
|
||||
"colorbalancergb",
|
||||
"agx",
|
||||
"sigmoid",
|
||||
"primaries",
|
||||
"channelmixerrgb",
|
||||
"colorzones"
|
||||
}
|
||||
|
||||
-- ============================================================================
|
||||
-- CORE LOGIC - No need to edit below unless changing architecture
|
||||
-- ============================================================================
|
||||
|
||||
-- Helper functions
|
||||
local function is_in_darkroom()
|
||||
return dt.gui.current_view() == dt.gui.views.darkroom
|
||||
return dt.gui.current_view() == darkroom_view
|
||||
end
|
||||
|
||||
local function is_valid(value)
|
||||
return value == value -- NaN check
|
||||
end
|
||||
|
||||
local function is_module_focused(module_name)
|
||||
return dt.gui.action("iop/" .. module_name, "focus") ~= 0
|
||||
end
|
||||
|
||||
local function translate_tab_effect(effect)
|
||||
if effect == "up" then
|
||||
return "next"
|
||||
@@ -141,156 +234,84 @@ local function translate_tab_effect(effect)
|
||||
end
|
||||
end
|
||||
|
||||
-- Handler functions for different module types
|
||||
local function handle_mask_properties(knob_idx, element, effect, size)
|
||||
-- Execute an action configuration
|
||||
local function execute_action(action_config, element, effect, size)
|
||||
if not action_config then
|
||||
return nil
|
||||
end
|
||||
|
||||
local path = action_config[1]
|
||||
local elem = action_config[2] or element
|
||||
local action_type = action_config[3]
|
||||
|
||||
-- Handle special action types
|
||||
if action_type == "tab" then
|
||||
effect = translate_tab_effect(effect)
|
||||
end
|
||||
|
||||
return dt.gui.action(path, elem, effect, size)
|
||||
end
|
||||
|
||||
-- Try mask properties first
|
||||
local function try_mask_properties(knob_idx, element, effect, size)
|
||||
if knob_idx >= NUM_KNOBS then
|
||||
return NAN
|
||||
end
|
||||
|
||||
local property = MASK_PROPERTIES[knob_idx]
|
||||
local value = dt.gui.action("lib/masks/properties/" .. property,
|
||||
element, effect, size)
|
||||
local mask_action = MASK_PROPERTIES[knob_idx]
|
||||
if not mask_action then
|
||||
return NAN
|
||||
end
|
||||
|
||||
local value = execute_action(mask_action, element, effect, size)
|
||||
return value
|
||||
end
|
||||
|
||||
local function handle_color_module(module_name, knob_idx, element, effect, size)
|
||||
local elements = COLOR_ELEMENTS[module_name]
|
||||
if not elements then
|
||||
return nil
|
||||
end
|
||||
|
||||
return dt.gui.action("iop/" .. module_name .. "/graph",
|
||||
elements[knob_idx], effect, size)
|
||||
end
|
||||
|
||||
local function handle_sigmoid(knob_idx, element, effect, size)
|
||||
if knob_idx >= NUM_KNOBS then
|
||||
return nil
|
||||
end
|
||||
|
||||
local slider = MODULE_CONFIGS.sigmoid[knob_idx]
|
||||
return dt.gui.action("iop/sigmoid/primaries/" .. slider,
|
||||
element, effect, size)
|
||||
end
|
||||
|
||||
local function handle_primaries(knob_idx, element, effect, size)
|
||||
local slider = MODULE_CONFIGS.primaries[knob_idx]
|
||||
return dt.gui.action("iop/primaries/" .. slider,
|
||||
element, effect, size)
|
||||
end
|
||||
|
||||
local function handle_agx(knob_idx, element, effect, size)
|
||||
local slider = MODULE_CONFIGS.agx[knob_idx]
|
||||
return dt.gui.action("iop/agx/" .. slider,
|
||||
element, effect, size)
|
||||
end
|
||||
|
||||
local function handle_tone_equalizer(knob_idx, element, effect, size)
|
||||
return dt.gui.action("iop/toneequal/simple/" .. (knob_idx - 9) .. " EV",
|
||||
element, effect, size)
|
||||
end
|
||||
|
||||
local function handle_channel_mixer(knob_idx, element, effect, size)
|
||||
if knob_idx < 5 then
|
||||
return nil
|
||||
end
|
||||
|
||||
if knob_idx == 5 then
|
||||
-- Tab selection
|
||||
return dt.gui.action("iop/channelmixerrgb/page",
|
||||
"CAT",
|
||||
translate_tab_effect(effect),
|
||||
size)
|
||||
else
|
||||
-- Color sliders
|
||||
local positions = {"1st", "2nd", "3rd"}
|
||||
return dt.gui.action("iop/focus/sliders",
|
||||
positions[knob_idx - 5],
|
||||
effect, size)
|
||||
end
|
||||
end
|
||||
|
||||
local function handle_color_balance_contrast(knob_idx, element, effect, size)
|
||||
if knob_idx ~= 4 then
|
||||
return nil
|
||||
end
|
||||
return dt.gui.action("iop/colorbalancergb/contrast",
|
||||
element, effect, size)
|
||||
end
|
||||
|
||||
local function handle_default_mapping(knob_idx, element, effect, size)
|
||||
local mapping = DEFAULT_MAPPINGS[knob_idx]
|
||||
return dt.gui.action(mapping, element, effect, size)
|
||||
end
|
||||
|
||||
-- Main knob handler
|
||||
local function handle_knob_action(knob_idx, element, effect, size)
|
||||
-- Early exit for non-darkroom view
|
||||
if not is_in_darkroom() then
|
||||
return NAN
|
||||
end
|
||||
|
||||
-- Try mask properties first
|
||||
local mask_value = handle_mask_properties(knob_idx, element, effect, size)
|
||||
-- Try mask properties first (no focus check needed)
|
||||
local mask_value = try_mask_properties(knob_idx, element, effect, size)
|
||||
if is_valid(mask_value) then
|
||||
return mask_value
|
||||
end
|
||||
|
||||
-- Try color modules
|
||||
if is_module_focused("colorzones") then
|
||||
return handle_color_module("colorzones", knob_idx, element, effect, size)
|
||||
end
|
||||
|
||||
if is_module_focused("colorequal") then
|
||||
return handle_color_module("colorequal", knob_idx, element, effect, size)
|
||||
end
|
||||
|
||||
-- Try sigmoid module
|
||||
if is_module_focused("sigmoid") then
|
||||
local result = handle_sigmoid(knob_idx, element, effect, size)
|
||||
if result then
|
||||
return result
|
||||
-- Check focused modules in priority order
|
||||
for _, module_name in ipairs(MODULE_CHECK_ORDER) do
|
||||
local focus_status = dt.gui.action("iop/" .. module_name, "focus")
|
||||
|
||||
if focus_status ~= 0 then
|
||||
-- Module has focus, get its profile
|
||||
local profile = MODULE_PROFILES[module_name]
|
||||
|
||||
if profile then
|
||||
local action_config = profile[knob_idx]
|
||||
|
||||
-- If profile has explicit mapping for this knob, use it
|
||||
if action_config then
|
||||
local result = execute_action(action_config, element, effect, size)
|
||||
if result then
|
||||
return result
|
||||
end
|
||||
else
|
||||
-- Profile exists but this knob is nil -> use default
|
||||
-- This avoids checking other modules when we know the focused one
|
||||
local default_action = DEFAULT_MAPPINGS[knob_idx]
|
||||
return execute_action(default_action, element, effect, size) or NAN
|
||||
end
|
||||
end
|
||||
|
||||
-- If no profile exists for this module, continue checking others
|
||||
end
|
||||
end
|
||||
|
||||
-- Try primaries module
|
||||
if is_module_focused("primaries") then
|
||||
local result = handle_primaries(knob_idx, element, effect, size)
|
||||
if result then
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
-- Try agx module
|
||||
if is_module_focused("agx") then
|
||||
local result = handle_agx(knob_idx, element, effect, size)
|
||||
if result then
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
-- Try tone equalizer
|
||||
if is_module_focused("toneequal") then
|
||||
return handle_tone_equalizer(knob_idx, element, effect, size)
|
||||
end
|
||||
|
||||
-- Try channel mixer (color calibration)
|
||||
if is_module_focused("channelmixerrgb") then
|
||||
local result = handle_channel_mixer(knob_idx, element, effect, size)
|
||||
if result then
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
-- Try color balance contrast
|
||||
if is_module_focused("colorbalancergb") then
|
||||
local result = handle_color_balance_contrast(knob_idx, element, effect, size)
|
||||
if result then
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
-- Fall back to default mapping
|
||||
return handle_default_mapping(knob_idx, element, effect, size)
|
||||
-- No module has focus or no profile matched, fall back to default
|
||||
local default_action = DEFAULT_MAPPINGS[knob_idx]
|
||||
return execute_action(default_action, element, effect, size) or NAN
|
||||
end
|
||||
|
||||
-- Setup mimic sliders
|
||||
@@ -308,8 +329,8 @@ end
|
||||
local script_data = {
|
||||
metadata = {
|
||||
name = _("x-touch"),
|
||||
purpose = _("example of how to control an x-touch midi device"),
|
||||
author = "Diederik ter Rahe",
|
||||
purpose = _("data-driven x-touch midi controller configuration"),
|
||||
author = "Diederik ter Rahe (modified)",
|
||||
help = "https://docs.darktable.org/lua/stable/lua.scripts.manual/scripts/examples/x-touch"
|
||||
},
|
||||
destroy = function() end,
|
||||
|
||||
Reference in New Issue
Block a user