Channels: Channel Options dialog to make alpha channels spot colours (#3761) (#3784)

* feat(channels): Channel Options dialog to make alpha channels spot colours (#3761)

Double-clicking an alpha channel's thumbnail in the Channels panel now opens
Photoshop's Channel Options dialog (double-clicking the name still renames it):
Name, Color Indicates Masked Areas / Selected Areas / Spot Color, the overlay
or ink colour (Color Picker) and Opacity, which becomes Solidity for spot
channels. A saved selection can now be turned into a spot channel with its own
ink for screen-print separations. The row context menu gains Channel Options…
and Quick Mask Options…, and `channel.options` without params from a menu
opens the dialog on the targeted channel. OK runs the existing engine command,
so agents can still call it directly with params.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* style: rustfmt module order in ui-egui lib.rs

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat(channels): New Spot Channel dialog (#3761)

Channels › New Spot Channel… now opens Photoshop's dialog instead of adding
a channel straight away: Name (prefilled with the next free "Spot Color N"),
and Ink Characteristics: Color (opens the Color Picker) and Solidity. OK
creates the spot channel from the selection, as before, and remembers the ink
and solidity for next time. `channel.newSpot` with params still runs the
engine directly for agents, the CLI and MCP. The engine's `next_alpha_name`
helper is now public so the dialog's name matches the engine's default.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* i18n: keep main's Czech rows, add Vietnamese strings (#3761 review)

- cs.tsv: main has removed its duplicate copies of Tool, Stroke Path…,
  Clone Stamp, History Brush, Dodge, Burn and Sponge, so this branch keeps
  main's single rows; the only Czech change is the Channel Options / New Spot
  Channel block.
- vi.tsv: the Vietnamese catalog is complete since #2636, so it gets rows for
  Channel Color, Color Indicates:, Solidity: and Ink Characteristics.
- it.tsv: main now translates Masked Areas and Selected Areas, so this
  branch's duplicate rows are dropped.

Every catalog now only adds lines relative to main.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Channel Options: Serbian, Hungarian and Turkish rows for the new strings

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-13 02:59:12 -04:00
1 parent 04f0b0e86b
commit 5c579ba81a
27 files changed
+569 -4

No files matched your search

+2 -2
View File
@@ -194,8 +194,8 @@ pub fn channel_format(doc: &Document) -> PixelFormat {
PixelFormat::new(ColorMode::Grayscale, doc.depth, false)
}
/// Next free "Alpha N" name.
fn next_alpha_name(doc: &Document, base: &str) -> String {
/// Next free "`base` N" name (Alpha 1, Spot Color 2, …).
pub fn next_alpha_name(doc: &Document, base: &str) -> String {
// `channels.len() + 1` candidates always include a free one.
(1..=doc.channels.len() + 1).map(|n| format!("{base} {n}")).find(|n| !doc.channels.iter().any(|c| &c.name == n)).unwrap_or_else(|| base.to_string())
}
+215
View File
@@ -0,0 +1,215 @@
//! Channels panel › Channel Options… (double-click an alpha or spot channel's thumbnail, or the
//! Quick Mask row): name, Color Indicates Masked Areas / Selected Areas / Spot Color, the overlay
//! or ink colour, and opacity (solidity for spot channels). OK runs the engine's
//! `channel.options`, which agents can also call directly with params (#3761).
use egui::{Align2, Sense, pos2, vec2};
use photocraft_doc::ColorIndicates;
use serde_json::{Map, Value, json};
use crate::PhotocraftApp;
use crate::theme::Tokens;
pub const COMMAND: &str = "channel.options";
const MARK: &str = "__channelOptions";
pub fn owns(fields: &Map<String, Value>) -> bool {
fields.contains_key(MARK)
}
fn hex(rgb: [f32; 3]) -> String {
crate::color_picker_ui::hex(rgb)
}
/// The dialog's fields for `channel` (an alpha/spot channel index, or `"quickMask"`), or `None`
/// when there's no document or no such channel.
pub fn fields(app: &PhotocraftApp, channel: &Value) -> Option<Map<String, Value>> {
let doc = &app.session.active()?.doc;
let kind = |i: ColorIndicates| if i == ColorIndicates::SelectedAreas { "selected" } else { "masked" };
let (name, indicates, color, percent) = if channel.as_str() == Some("quickMask") {
let o = app.session.quick_mask_options;
(None, kind(o.indicates), o.color, o.opacity)
} else {
let ch = doc.channels.get(usize::try_from(channel.as_u64()?).ok()?)?;
match ch.spot {
Some((ink, solidity)) => (Some(ch.name.clone()), "spot", ink, solidity),
None => (Some(ch.name.clone()), kind(ch.indicates), ch.color, ch.opacity),
}
};
let mut f = Map::new();
f.insert(MARK.into(), json!(true));
f.insert("__command".into(), json!(COMMAND));
f.insert("__label".into(), json!(if name.is_some() { "Channel Options…" } else { "Quick Mask Options…" }));
f.insert("channel".into(), channel.clone());
if let Some(n) = name {
f.insert("name".into(), json!(n));
}
f.insert("indicates".into(), json!(indicates));
f.insert("color".into(), json!(hex(color.to_rgb())));
f.insert("opacity".into(), json!((percent.clamp(0.0, 1.0) * 100.0).round()));
Some(f)
}
/// Opens Channel Options on `channel`, or `None` if it doesn't exist.
pub fn open(app: &mut PhotocraftApp, channel: &Value) -> Option<u64> {
let f = fields(app, channel)?;
Some(app.ui.open_dialog(crate::state::DialogKind::Command, f))
}
/// The `channel.options` params for the dialog's fields: spot channels take `solidity`, the
/// others `opacity`.
pub fn params(f: &Map<String, Value>) -> Value {
let spot = f.get("indicates").and_then(Value::as_str) == Some("spot");
let mut p = Map::new();
for key in ["channel", "name", "indicates", "color"] {
if let Some(v) = f.get(key) {
p.insert(key.into(), v.clone());
}
}
if let Some(v) = f.get("opacity") {
p.insert(if spot { "solidity" } else { "opacity" }.into(), v.clone());
}
Value::Object(p)
}
pub fn confirm(app: &mut PhotocraftApp, f: &Map<String, Value>) -> Result<Value, String> {
app.run(COMMAND, params(f))
}
fn label(ui: &mut egui::Ui, text: &str) {
let t = Tokens::get(ui.ctx());
let (r, _) = ui.allocate_exact_size(vec2(84.0, 22.0), Sense::hover());
ui.painter().text(pos2(r.right() - 6.0, r.center().y), Align2::RIGHT_CENTER, text, egui::FontId::proportional(12.0), t.text_dim);
}
pub fn body(ui: &mut egui::Ui, f: &mut Map<String, Value>) {
ui.spacing_mut().item_spacing.y = 6.0;
let quick = f.get("channel").and_then(Value::as_str) == Some("quickMask");
if let Some(mut name) = f.get("name").and_then(Value::as_str).map(str::to_string) {
ui.horizontal(|ui| {
label(ui, tl!("Name:"));
if ui.add(egui::TextEdit::singleline(&mut name).desired_width(200.0)).changed() {
f.insert("name".into(), json!(name));
}
});
}
crate::widgets::section_label(ui, tl!("Color Indicates:"));
crate::widgets::hairline(ui);
let mut indicates = f.get("indicates").and_then(Value::as_str).unwrap_or("masked").to_string();
let before = indicates.clone();
ui.indent("channel-options-indicates", |ui| {
ui.radio_value(&mut indicates, "masked".to_string(), tl!("Masked Areas"));
ui.radio_value(&mut indicates, "selected".to_string(), tl!("Selected Areas"));
// Quick Mask can't be a spot channel.
if !quick {
ui.radio_value(&mut indicates, "spot".to_string(), tl!("Spot Color"));
}
});
if indicates != before {
f.insert("indicates".into(), json!(indicates));
}
let spot = indicates == "spot";
ui.add_space(4.0);
crate::widgets::section_label(ui, tl!("Color"));
crate::widgets::hairline(ui);
ui.horizontal(|ui| {
label(ui, "");
let current = f.get("color").and_then(Value::as_str).and_then(crate::color_picker_ui::parse_hex).unwrap_or([1.0, 0.0, 0.0]);
let c = current.map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8);
let what = if spot { tl!("Spot Color") } else { tl!("Channel Color") };
if crate::widgets::color_swatch_button(ui, egui::Color32::from_rgb(c[0], c[1], c[2]), what).clicked() {
crate::color_picker_ui::request(f, &crate::color_picker_ui::title_for(what));
}
ui.add_space(12.0);
ui.label(if spot { tl!("Solidity:") } else { tl!("Opacity:") });
let mut pct = f.get("opacity").and_then(Value::as_f64).unwrap_or(50.0) as f32;
if crate::widgets::value_field(ui, &mut pct, 0.0..=100.0, "%", 64.0).changed() {
f.insert("opacity".into(), json!(pct.round()));
}
});
}
// ---------------------------------------------------------------------------------------------
// New Spot Channel…: Name and Ink Characteristics (Color, Solidity). OK runs `channel.newSpot`
// (from the selection, as Photoshop does) and remembers the ink and solidity for next time.
pub const NEW_SPOT: &str = "channel.newSpot";
const NEW_SPOT_MARK: &str = "__newSpot";
pub fn owns_new_spot(fields: &Map<String, Value>) -> bool {
fields.contains_key(NEW_SPOT_MARK)
}
/// The New Spot Channel fields: the next "Spot Color N" name and the last ink and solidity
/// (red and 0% at first), or `None` without a document.
pub fn new_spot_fields(app: &PhotocraftApp) -> Option<Map<String, Value>> {
let doc = &app.session.active()?.doc;
let saved = app.session.prefs().dialogs.get(NEW_SPOT).cloned().unwrap_or_default();
let color = saved.get("color").and_then(Value::as_str).filter(|h| crate::color_picker_ui::parse_hex(h).is_some()).unwrap_or("#ff0000");
let solidity = saved.get("solidity").and_then(Value::as_f64).filter(|v| (0.0..=100.0).contains(v)).unwrap_or(0.0);
let mut f = Map::new();
f.insert(NEW_SPOT_MARK.into(), json!(true));
f.insert("__command".into(), json!(NEW_SPOT));
f.insert("__label".into(), json!("New Spot Channel…"));
f.insert("name".into(), json!(photocraft_engine::channel_cmds::next_alpha_name(doc, "Spot Color")));
f.insert("color".into(), json!(color));
f.insert("solidity".into(), json!(solidity));
Some(f)
}
/// Opens New Spot Channel, or `None` without a document.
pub fn open_new_spot(app: &mut PhotocraftApp) -> Option<u64> {
let f = new_spot_fields(app)?;
Some(app.ui.open_dialog(crate::state::DialogKind::Command, f))
}
pub fn new_spot_params(f: &Map<String, Value>) -> Value {
let mut p = Map::new();
for key in ["name", "color", "solidity"] {
if let Some(v) = f.get(key) {
p.insert(key.into(), v.clone());
}
}
Value::Object(p)
}
pub fn confirm_new_spot(app: &mut PhotocraftApp, f: &Map<String, Value>) -> Result<Value, String> {
let p = new_spot_params(f);
let result = app.run(NEW_SPOT, p.clone());
if result.is_ok() {
let remembered = json!({ "color": p.get("color"), "solidity": p.get("solidity") });
app.session.prefs.edit(|prefs| prefs.dialogs.insert(NEW_SPOT.into(), remembered));
}
result
}
pub fn new_spot_body(ui: &mut egui::Ui, f: &mut Map<String, Value>) {
ui.spacing_mut().item_spacing.y = 6.0;
let mut name = f.get("name").and_then(Value::as_str).unwrap_or_default().to_string();
ui.horizontal(|ui| {
label(ui, tl!("Name:"));
if ui.add(egui::TextEdit::singleline(&mut name).desired_width(200.0)).changed() {
f.insert("name".into(), json!(name));
}
});
crate::widgets::section_label(ui, tl!("Ink Characteristics"));
crate::widgets::hairline(ui);
ui.horizontal(|ui| {
label(ui, tl!("Color:"));
let current = f.get("color").and_then(Value::as_str).and_then(crate::color_picker_ui::parse_hex).unwrap_or([1.0, 0.0, 0.0]);
let c = current.map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8);
if crate::widgets::color_swatch_button(ui, egui::Color32::from_rgb(c[0], c[1], c[2]), tl!("Spot Color")).clicked() {
crate::color_picker_ui::request(f, &crate::color_picker_ui::title_for(tl!("Spot Color")));
}
ui.add_space(12.0);
ui.label(tl!("Solidity:"));
let mut pct = f.get("solidity").and_then(Value::as_f64).unwrap_or(0.0) as f32;
if crate::widgets::value_field(ui, &mut pct, 0.0..=100.0, "%", 64.0).changed() {
f.insert("solidity".into(), json!(pct.round()));
}
});
}
#[cfg(test)]
#[path = "channel_options_ui_tests.rs"]
mod tests;
@@ -0,0 +1,177 @@
use serde_json::json;
use super::*;
fn app() -> PhotocraftApp {
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), Default::default());
app.run("file.new", json!({"width": 16, "height": 16})).unwrap();
app.run("select.rect", json!({"x": 2, "y": 2, "width": 8, "height": 8})).unwrap();
app.run("select.saveSelection", json!({})).unwrap();
app
}
fn channel(app: &PhotocraftApp) -> photocraft_doc::AlphaChannel {
app.session.active().unwrap().doc.channels[0].clone()
}
#[test]
fn saved_selection_becomes_a_spot_channel_with_its_ink_and_solidity() {
let mut app = app();
assert!(channel(&app).spot.is_none());
let id = open(&mut app, &json!(0)).unwrap();
let f = &app.ui.dialogs.last().unwrap().fields;
assert!(owns(f));
assert_eq!(f["indicates"], json!("masked"));
assert_eq!(f["name"], json!(channel(&app).name));
let d = app.ui.dialog_mut(id).unwrap();
d.fields.insert("name".into(), json!("PMS 185"));
d.fields.insert("indicates".into(), json!("spot"));
d.fields.insert("color".into(), json!("#ff0000"));
d.fields.insert("opacity".into(), json!(40.0));
crate::dialogs::confirm(&mut app, id).unwrap();
assert!(app.ui.dialogs.is_empty());
let ch = channel(&app);
assert_eq!(ch.name, "PMS 185");
let (ink, solidity) = ch.spot.expect("a spot channel");
assert_eq!(ink.to_rgb(), [1.0, 0.0, 0.0]);
assert!((solidity - 0.4).abs() < 1e-6);
// Reopened, the dialog shows the spot settings; switching back makes it an alpha channel.
let id = open(&mut app, &json!(0)).unwrap();
let f = &app.ui.dialog_mut(id).unwrap().fields;
assert_eq!((f["indicates"].clone(), f["color"].clone(), f["opacity"].clone()), (json!("spot"), json!("#ff0000"), json!(40.0)));
app.ui.dialog_mut(id).unwrap().fields.insert("indicates".into(), json!("selected"));
crate::dialogs::confirm(&mut app, id).unwrap();
let ch = channel(&app);
assert!(ch.spot.is_none());
assert_eq!(ch.indicates, ColorIndicates::SelectedAreas);
}
#[test]
fn cancel_leaves_the_channel_alone() {
let mut app = app();
let before = app.session.active().unwrap().history.past_len();
let id = open(&mut app, &json!(0)).unwrap();
app.ui.dialog_mut(id).unwrap().fields.insert("indicates".into(), json!("spot"));
app.ui.close_dialog(id);
assert!(channel(&app).spot.is_none());
assert_eq!(app.session.active().unwrap().history.past_len(), before);
}
#[test]
fn spot_params_send_solidity_and_alpha_params_send_opacity() {
let mut f = Map::new();
f.insert(MARK.into(), json!(true));
f.insert("channel".into(), json!(0));
f.insert("indicates".into(), json!("spot"));
f.insert("opacity".into(), json!(25.0));
assert_eq!(params(&f), json!({"channel": 0, "indicates": "spot", "solidity": 25.0}));
f.insert("indicates".into(), json!("masked"));
assert_eq!(params(&f), json!({"channel": 0, "indicates": "masked", "opacity": 25.0}));
}
#[test]
fn menu_opens_the_dialog_on_the_targeted_channel_and_refuses_otherwise() {
let ctx = egui::Context::default();
let mut empty = PhotocraftApp::new(photocraft_engine::Session::new(), Default::default());
assert!(crate::menus::invoke(&mut empty, &ctx, COMMAND, json!({})).is_err());
let mut app = app();
// The composite is targeted: nothing to edit.
assert!(crate::menus::invoke(&mut app, &ctx, COMMAND, json!({})).is_err());
assert!(app.ui.dialogs.is_empty());
app.run("channel.target", json!({"channel": 0})).unwrap();
let r = crate::menus::invoke(&mut app, &ctx, COMMAND, json!({})).unwrap();
assert!(r["dialog"].is_u64());
assert_eq!(app.ui.dialogs.last().unwrap().fields["channel"], json!(0));
// Explicit params run the engine directly, no dialog.
app.ui.dialogs.clear();
crate::menus::invoke(&mut app, &ctx, COMMAND, json!({"channel": 0, "indicates": "spot"})).unwrap();
assert!(app.ui.dialogs.is_empty());
assert!(channel(&app).spot.is_some());
}
#[test]
fn quick_mask_options_have_no_name_or_spot_and_bad_channels_open_nothing() {
let mut app = app();
let f = fields(&app, &json!("quickMask")).unwrap();
assert!(!f.contains_key("name"));
assert_eq!(f["__label"], json!("Quick Mask Options…"));
assert!(fields(&app, &json!(7)).is_none());
assert!(fields(&app, &json!("nope")).is_none());
assert!(open(&mut app, &json!(-1)).is_none());
assert!(app.ui.dialogs.is_empty());
}
#[test]
fn dialog_body_renders_for_alpha_spot_and_quick_mask() {
let mut app = app();
app.run("channel.options", json!({"channel": 0, "indicates": "spot"})).unwrap();
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
for channel in [json!(0), json!("quickMask")] {
let mut f = fields(&app, &channel).unwrap();
ctx.run_ui(Default::default(), |ui| body(ui, &mut f)).textures_delta.clear();
assert!(owns(&f));
}
}
#[test]
fn new_spot_channel_dialog_creates_the_ink_from_the_selection_and_remembers_it() {
let mut app = app();
app.run("select.rect", json!({"x": 2, "y": 2, "width": 8, "height": 8})).unwrap();
let id = open_new_spot(&mut app).unwrap();
let f = &app.ui.dialogs.last().unwrap().fields;
assert!(owns_new_spot(f) && !owns(f));
assert_eq!((f["name"].clone(), f["color"].clone(), f["solidity"].clone()), (json!("Spot Color 1"), json!("#ff0000"), json!(0.0)));
let d = app.ui.dialog_mut(id).unwrap();
d.fields.insert("name".into(), json!("PMS 300"));
d.fields.insert("color".into(), json!("#0050ff"));
d.fields.insert("solidity".into(), json!(60.0));
crate::dialogs::confirm(&mut app, id).unwrap();
let doc = &app.session.active().unwrap().doc;
let ch = doc.channels.last().unwrap();
assert_eq!(ch.name, "PMS 300");
let (ink, solidity) = ch.spot.expect("a spot channel");
assert_eq!(ink.to_rgba8()[..3], [0x00, 0x50, 0xff]);
assert!((solidity - 0.6).abs() < 1e-6);
// Inked where the selection was, empty outside it.
let (inside, outside) = (ch.surface.rgba(4, 4)[0], ch.surface.rgba(14, 14)[0]);
assert!(inside > 0.99 && outside < 0.01, "{inside} {outside}");
// The next one gets a fresh name and the last ink and solidity.
let f = new_spot_fields(&app).unwrap();
assert_eq!((f["name"].clone(), f["color"].clone(), f["solidity"].clone()), (json!("Spot Color 1"), json!("#0050ff"), json!(60.0)));
}
#[test]
fn new_spot_channel_cancel_menu_routing_and_no_document() {
let ctx = egui::Context::default();
let mut empty = PhotocraftApp::new(photocraft_engine::Session::new(), Default::default());
assert!(open_new_spot(&mut empty).is_none());
assert!(crate::menus::invoke(&mut empty, &ctx, NEW_SPOT, json!({})).is_err());
let mut app = app();
let n = app.session.active().unwrap().doc.channels.len();
let r = crate::menus::invoke(&mut app, &ctx, NEW_SPOT, json!({})).unwrap();
let id = r["dialog"].as_u64().unwrap();
assert!(owns_new_spot(&app.ui.dialog_mut(id).unwrap().fields));
app.ui.close_dialog(id);
assert_eq!(app.session.active().unwrap().doc.channels.len(), n, "Cancel adds nothing");
// Explicit params run the engine directly, no dialog.
crate::menus::invoke(&mut app, &ctx, NEW_SPOT, json!({"name": "Direct"})).unwrap();
assert!(app.ui.dialogs.is_empty());
assert_eq!(app.session.active().unwrap().doc.channels.last().unwrap().name, "Direct");
}
#[test]
fn new_spot_body_renders_and_bad_saved_prefs_fall_back() {
let mut app = app();
app.session.prefs.edit(|p| p.dialogs.insert(NEW_SPOT.into(), json!({"color": "nope", "solidity": 900})));
let mut f = new_spot_fields(&app).unwrap();
assert_eq!((f["color"].clone(), f["solidity"].clone()), (json!("#ff0000"), json!(0.0)));
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
ctx.run_ui(Default::default(), |ui| new_spot_body(ui, &mut f)).textures_delta.clear();
assert!(owns_new_spot(&f));
}
+20 -2
View File
@@ -217,8 +217,16 @@ pub fn show(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
actions.push(("channel.target".into(), json!({ "channel": target })));
}
}
// Like Photoshop: double-clicking the thumbnail opens Channel Options (where an alpha
// channel becomes a spot channel, #3761), double-clicking the name renames it.
let on_thumb = resp.interact_pointer_pos().is_some_and(|p| p.x < cell.right() + 4.0);
if row == Row::QuickMask && resp.double_clicked() {
actions.push(("ui.channelOptions".into(), json!("quickMask")));
}
if let Row::Alpha(i) = row {
if resp.double_clicked() {
if resp.double_clicked() && on_thumb {
actions.push(("ui.channelOptions".into(), json!(i)));
} else if resp.double_clicked() {
ctx.data_mut(|d| d.insert_temp(rename_id, name.clone()));
}
if let Some(mut text) = ctx.data(|d| d.get_temp::<String>(rename_id)) {
@@ -250,6 +258,7 @@ pub fn show(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
item(ui, a, "Duplicate Channel", "channel.duplicate", json!({ "channel": i }));
item(ui, a, "Delete Channel", "channel.delete", json!({ "channel": i }));
item(ui, a, "Rename Channel…", "ui.renameChannel", json!(i));
item(ui, a, "Channel Options…", "ui.channelOptions", json!(i));
ui.separator();
if ch.spot.is_some() {
item(ui, a, "Merge Spot Channel", "channel.mergeSpot", json!({ "channel": i }));
@@ -261,6 +270,7 @@ pub fn show(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
}
Row::QuickMask => {
item(ui, a, "Exit Quick Mask", "select.editInQuickMaskMode", json!({ "on": false }));
item(ui, a, "Quick Mask Options…", "ui.channelOptions", json!("quickMask"));
indicates_items(ui, a, json!("quickMask"), doc.quick_mask.as_ref().map(|q| q.indicates).unwrap_or_default());
overlay_colors(ui, a, json!("quickMask"));
}
@@ -275,7 +285,7 @@ pub fn show(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
}
ui.separator();
item(ui, a, "New Channel…", "channel.new", json!({}));
item(ui, a, "New Spot Channel…", "channel.newSpot", json!({}));
item(ui, a, "New Spot Channel…", "ui.newSpotChannel", json!({}));
ui.separator();
item(ui, a, "Split Channels", "channel.split", json!({}));
item(ui, a, "Merge Channels…", "channel.merge", json!({}));
@@ -337,6 +347,14 @@ pub fn show(app: &mut PhotocraftApp, ui: &mut egui::Ui) {
}
continue;
}
if id == "ui.newSpotChannel" {
crate::channel_options_ui::open_new_spot(app);
continue;
}
if id == "ui.channelOptions" {
crate::channel_options_ui::open(app, &p);
continue;
}
let _ = app.run(&id, p);
}
if let Some(on) = mask_click {
+4
View File
@@ -270,6 +270,8 @@ pub fn show(app: &mut PhotocraftApp, ctx: &egui::Context) {
DialogKind::Command if crate::fill_ui::owns(&fields) => crate::fill_ui::body(app, ui, &mut fields),
DialogKind::Command if crate::pattern_fill_ui::owns(&fields) => crate::pattern_fill_ui::body(app, ui, &mut fields),
DialogKind::Command if crate::stroke_ui::owns(&fields) => crate::stroke_ui::body(ui, &mut fields),
DialogKind::Command if crate::channel_options_ui::owns(&fields) => crate::channel_options_ui::body(ui, &mut fields),
DialogKind::Command if crate::channel_options_ui::owns_new_spot(&fields) => crate::channel_options_ui::new_spot_body(ui, &mut fields),
DialogKind::Command if crate::shape_dialog::owns(&fields) => crate::shape_dialog::body(app, ui, &mut fields),
DialogKind::Command if crate::delete_layer_prompt::owns(&fields) => crate::delete_layer_prompt::body(ui, &mut fields),
DialogKind::Command if crate::actions::owns_insert_stop(&fields) => crate::actions::insert_stop_body(ui, &mut fields),
@@ -551,6 +553,8 @@ pub fn confirm(app: &mut PhotocraftApp, id: u64) -> Result<Value, String> {
DialogKind::Command if crate::fill_ui::owns(&d.fields) => crate::fill_ui::confirm(app, &d.fields),
DialogKind::Command if crate::pattern_fill_ui::owns(&d.fields) => crate::pattern_fill_ui::confirm(app, &d.fields),
DialogKind::Command if crate::stroke_ui::owns(&d.fields) => crate::stroke_ui::confirm(app, &d.fields),
DialogKind::Command if crate::channel_options_ui::owns(&d.fields) => crate::channel_options_ui::confirm(app, &d.fields),
DialogKind::Command if crate::channel_options_ui::owns_new_spot(&d.fields) => crate::channel_options_ui::confirm_new_spot(app, &d.fields),
DialogKind::Command if crate::shape_dialog::owns(&d.fields) => crate::shape_dialog::confirm(app, &d.fields),
DialogKind::Command if crate::delete_layer_prompt::owns(&d.fields) => crate::delete_layer_prompt::confirm(app, &d.fields),
DialogKind::Command if crate::actions::owns_insert_stop(&d.fields) => crate::actions::insert_stop_confirm(app, &d.fields),
+6
View File
@@ -19,6 +19,12 @@
# (PhotoCraft, ArtCraft, OpenType, CMYK, Lab ...) stay in Latin letters; the trailing "…" of
# dialog commands is kept.
# Channel Options and New Spot Channel (#3761)
Channel Color Barva kanálu
Color Indicates: Barva označuje:
Solidity: Plnost:
Ink Characteristics Vlastnosti inkoustu
# Guide layout dialog
# Custom print paper (#3820)
Can't render this file because it contains an unexpected character in line 6 and column 47.
+8
View File
@@ -15,6 +15,14 @@
# way. Product and technology names (PhotoCraft, ArtCraft, OpenType, CMYK, Lab ...) stay in Latin
# letters; the trailing "…" of dialog commands is kept. Plural rule: one|other (n != 1 -> other).
# Channel Options and New Spot Channel (#3761)
Channel Color Kanalfarbe
Color Indicates: Farbe kennzeichnet:
Solidity: Solidität:
Masked Areas Maskierte Bereiche
Selected Areas Ausgewählte Bereiche
Ink Characteristics Farbeigenschaften
# Guide layout dialog
# Custom print paper (#3820)
Can't render this file because it contains an unexpected character in line 6 and column 47.
+6
View File
@@ -3,6 +3,12 @@
# Original translations based on general Greek graphics/computing terminology.
# Covers every source label the i18n coverage check lists.
# Channel Options and New Spot Channel (#3761)
Channel Color Χρώμα καναλιού
Color Indicates: Το χρώμα υποδεικνύει:
Solidity: Συμπαγότητα:
Ink Characteristics Χαρακτηριστικά μελανιού
# Guide layout dialog
# Custom print paper (#3820)
1 # Greek (el) PhotoCraft interface catalog.
3 # Original translations based on general Greek graphics/computing terminology.
4 # Covers every source label the i18n coverage check lists.
5 # Guide layout dialog # Channel Options and New Spot Channel (#3761)
6
7
8
9
10 # Guide layout dialog
11 # Custom print paper (#3820)
12 # Custom print paper (#3820)
13
14
+8
View File
@@ -12,6 +12,14 @@
# Product and technology names (PhotoCraft, ArtCraft, OpenType, CMYK, Lab ...) stay in Latin
# letters; the trailing "…" of dialog commands is kept.
# Channel Options and New Spot Channel (#3761)
Channel Color Color del canal
Color Indicates: El color indica:
Solidity: Solidez:
Masked Areas Áreas enmascaradas
Selected Areas Áreas seleccionadas
Ink Characteristics Características de la tinta
# Guide layout dialog
# Custom print paper (#3820)
Can't render this file because it contains an unexpected character in line 4 and column 47.
+6
View File
@@ -19,6 +19,12 @@
# ArtCraft, OpenType, Lab ...) stay as they are; RGB/CMYK read RVB/CMJN; the trailing "…" of
# dialog commands is kept.
# Channel Options and New Spot Channel (#3761)
Channel Color Couleur de la couche
Color Indicates: La couleur indique :
Solidity: Solidité :
Ink Characteristics Caractéristiques de l’encre
# Guide layout dialog
# Custom print paper (#3820)
Can't render this file because it contains an unexpected character in line 6 and column 47.
+6
View File
@@ -4041,3 +4041,9 @@ cameraRaw Yellow Sárga
Loaded Camera Raw settings from {file} ({n} not supported) Camera Raw-beállítások betöltve innen: {file} ({n} nem támogatott)
Auto Add/Delete Automatikus hozzáadás/törlés
ICO is limited to 256 × 256 pixels Az ICO legfeljebb 256 × 256 képpont lehet
# Channel Options and New Spot Channel (#3761)
Channel Color Csatorna színe
Color Indicates: A szín jelzése:
Ink Characteristics Festék jellemzői
Solidity: Fedőképesség:
Can't render this file because it contains an unexpected character in line 6 and column 47.
+6
View File
@@ -12,6 +12,12 @@
# Product and technology names (PhotoCraft, ArtCraft, OpenType, CMYK, Lab ...) stay in Latin
# letters; the trailing "…" of dialog commands is kept.
# Channel Options and New Spot Channel (#3761)
Channel Color Warna Saluran
Color Indicates: Warna Menunjukkan:
Solidity: Soliditas:
Ink Characteristics Karakteristik Tinta
# Guide layout dialog
# Custom print paper (#3820)
Can't render this file because it contains an unexpected character in line 4 and column 47.
+6
View File
@@ -12,6 +12,12 @@
# Product and technology names (PhotoCraft, ArtCraft, OpenType, CMYK, Lab ...) stay in Latin
# letters; the trailing … of dialog commands is kept.
# Channel Options and New Spot Channel (#3761)
Channel Color Colore canale
Color Indicates: Il colore indica:
Solidity: Solidità:
Ink Characteristics Caratteristiche inchiostro
# Guide layout dialog
# Custom print paper (#3820)
1 # Italian (it) catalog. Columns, separated by a TAB: context <TAB> source <TAB> translation
12 # Product and technology names (PhotoCraft, ArtCraft, OpenType, CMYK, Lab ...) stay in Latin
13 # letters; the trailing … of dialog commands is kept.
14 # Guide layout dialog # Channel Options and New Spot Channel (#3761)
15
16
17
18
19 # Guide layout dialog
20 # Custom print paper (#3820)
21 # Custom print paper (#3820)
22
23
+6
View File
@@ -15,6 +15,12 @@
# Product and technology names (PhotoCraft, ArtCraft, OpenType, CMYK, Lab ...) stay in Latin
# letters; the trailing "…" of dialog commands is kept.
# Channel Options and New Spot Channel (#3761)
Channel Color チャンネルカラー
Color Indicates: 着色表示:
Solidity: 濃度:
Ink Characteristics インキの特性
# Guide layout dialog
# Custom print paper (#3820)
Can't render this file because it contains an unexpected character in line 6 and column 47.
+9
View File
@@ -7,6 +7,15 @@
# Licensed MIT OR Apache-2.0. See LICENSE-translations.txt and ATTRIBUTION.md.
# Product names and technology names retain their spelling.
# Channel Options and New Spot Channel (#3761)
Channel Color 채널 색상
Color Indicates: 색상 표시 내용:
Solidity: 견고도:
Masked Areas 마스크 영역
Selected Areas 선택 영역
Spot Color 별색
Ink Characteristics 잉크 특성
# Guide layout dialog
# Custom print paper (#3820)
1 # Korean (ko) catalog. UTF-8: context<TAB>English source<TAB>translation.
7 # Licensed MIT OR Apache-2.0. See LICENSE-translations.txt and ATTRIBUTION.md.
8 # Product names and technology names retain their spelling.
9 # Guide layout dialog # Channel Options and New Spot Channel (#3761)
10
11
12
13
14
15
16
17 # Guide layout dialog
18 # Custom print paper (#3820)
19 # Custom print paper (#3820)
20
21
+6
View File
@@ -8,6 +8,12 @@
# stay as they are; the trailing "…" of dialog commands is kept. UI text addresses the user
# informally ("je") and uses sentence case.
# Channel Options and New Spot Channel (#3761)
Channel Color Kanaalkleur
Color Indicates: Kleur geeft aan:
Solidity: Soliditeit:
Ink Characteristics Inktkenmerken
# Guide layout dialog
# Custom print paper (#3820)
Can't render this file because it contains an unexpected character in line 8 and column 34.
+6
View File
@@ -18,6 +18,12 @@
# Conventions: menus are nouns (Plik, Edycja, Obraz ...), commands are infinitives (Otwórz…,
# Zapisz jako…), sentence case as in the English source. Quotes: „…”.
# Channel Options and New Spot Channel (#3761)
Channel Color Kolor kanału
Color Indicates: Kolor oznacza:
Solidity: Gęstość:
Ink Characteristics Właściwości farby
# Guide layout dialog
# Custom print paper (#3820)
Can't render this file because it contains an unexpected character in line 6 and column 47.
+6
View File
@@ -15,6 +15,12 @@
# entries the same way. Product and technology names (PhotoCraft, ArtCraft, OpenType, CMYK, Lab ...)
# stay as they are; the trailing "…" of dialog commands is kept.
# Channel Options and New Spot Channel (#3761)
Channel Color Cor do canal
Color Indicates: A cor indica:
Solidity: Solidez:
Ink Characteristics Características da tinta
# Guide layout dialog
# Custom print paper (#3820)
Can't render this file because it contains an unexpected character in line 6 and column 47.
+6
View File
@@ -17,6 +17,12 @@
# letters; the trailing "…" of dialog commands is kept; the colon of a label ("Width:") becomes
# the Russian typographic equivalent, with the space the source carries.
# Channel Options and New Spot Channel (#3761)
Channel Color Цвет канала
Color Indicates: Цветом обозначены:
Solidity: Плотность:
Ink Characteristics Характеристики краски
# Menus
# Guide layout dialog
Can't render this file because it contains an unexpected character in line 6 and column 47.
+6
View File
@@ -4195,3 +4195,9 @@ shape Appearance Izgled
Paper Width Širina papira
Paper Height Visina papira
ICO is limited to 256 × 256 pixels ICO je ograničen na 256 × 256 piksela
# Channel Options and New Spot Channel (#3761)
Channel Color Boja kanala
Color Indicates: Boja označava:
Ink Characteristics Karakteristike mastila
Solidity: Punoća:
Can't render this file because it contains an unexpected character in line 6 and column 47.
+6
View File
@@ -3852,3 +3852,9 @@ cameraData Lens Lens
Loaded Camera Raw settings from {file} ({n} not supported) Camera Raw ayarları {file} dosyasından yüklendi ({n} desteklenmiyor)
Auto Add/Delete Otomatik Ekle/Sil
ICO is limited to 256 × 256 pixels ICO 256 × 256 piksel ile sınırlıdır
# Channel Options and New Spot Channel (#3761)
Channel Color Kanal Rengi
Color Indicates: Rengin Gösterdiği:
Ink Characteristics Mürekkep Özellikleri
Solidity: Doluluk:
Can't render this file because it contains an unexpected character in line 8 and column 34.
+6
View File
@@ -8,6 +8,12 @@
# No proprietary translation resources were used.
# Licensed MIT OR Apache-2.0. See LICENSE-translations.txt and ATTRIBUTION.md.
# Channel Options and New Spot Channel (#3761)
Channel Color Колір каналу
Color Indicates: Кольором позначено:
Solidity: Щільність:
Ink Characteristics Характеристики фарби
# Menus
# Guide layout dialog
1 # Ukrainian (uk) catalog. UTF-8: context<TAB>English source<TAB>translation.
8 # No proprietary translation resources were used.
9 # Licensed MIT OR Apache-2.0. See LICENSE-translations.txt and ATTRIBUTION.md.
10 # Menus # Channel Options and New Spot Channel (#3761)
11
12
13
14
15 # Menus
16 # Guide layout dialog
17 # Guide layout dialog # Custom print paper (#3820)
18 # Custom print paper (#3820)
19
+6
View File
@@ -20,6 +20,12 @@
# (`plural_none` in i18n/mod.rs).
# Channel Options and New Spot Channel (#3761)
Channel Color Màu kênh
Color Indicates: Màu biểu thị:
Solidity: Độ đặc:
Ink Characteristics Đặc tính mực
# Phase 3.1 — top-level menus, File menu, Edit menu
# Custom paper dimensions in Print Settings.
Paper Width Chiều rộng giấy
Can't render this file because it contains an unexpected character in line 6 and column 47.
+9
View File
@@ -11,6 +11,15 @@
# Licensed MIT OR Apache-2.0, as the code.
# Product and technology names retain their spelling. See docs/localization-zh-hans.md.
# Channel Options and New Spot Channel (#3761)
Channel Color 通道颜色
Color Indicates: 色彩指示:
Solidity: 密度:
Masked Areas 被蒙版区域
Selected Areas 所选区域
Spot Color 专色
Ink Characteristics 油墨特性
# Guide layout dialog
# Custom print paper (#3820)
1 # Simplified Chinese (zh-hans) catalog. UTF-8, TAB-separated columns:
11 # Licensed MIT OR Apache-2.0, as the code.
12 # Product and technology names retain their spelling. See docs/localization-zh-hans.md.
13 # Guide layout dialog # Channel Options and New Spot Channel (#3761)
14
15
16
17
18
19
20
21 # Guide layout dialog
22 # Custom print paper (#3820)
23 # Custom print paper (#3820)
24
25
+9
View File
@@ -19,6 +19,15 @@
# kept. Full-width punctuation (,。:) inside sentences; half-width parentheses and the " · "
# separators as in the English.
# Channel Options and New Spot Channel (#3761)
Channel Color 色版顏色
Color Indicates: 色彩指示:
Solidity: 硬度:
Masked Areas 遮色片區域
Selected Areas 選取區域
Spot Color 特別色
Ink Characteristics 油墨特性
# Guide layout dialog
# Custom print paper (#3820)
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -46,6 +46,7 @@ pub mod canvas_autoscroll;
mod canvas_popup;
pub mod canvas_tool_menu;
pub mod cas_transform;
pub mod channel_options_ui;
pub mod channel_view;
pub mod channels_panel;
pub mod chrome_ui;
+17
View File
@@ -487,6 +487,23 @@ pub(crate) fn invoke_unguarded(app: &mut PhotocraftApp, ctx: &egui::Context, id:
}
Ok(json!({"dialog": crate::stroke_ui::open(app)}))
}
// Channels › Channel Options… without params: the dialog on the targeted alpha/spot
// channel, or Quick Mask Options while in Quick Mask (with params: the engine directly).
crate::channel_options_ui::COMMAND if params.as_object().is_none_or(|o| o.is_empty()) => {
let st = app.session.active().ok_or("no document is open")?;
let channel = match st.channel_view.target {
_ if st.doc.quick_mask.is_some() => json!("quickMask"),
photocraft_engine::channel_cmds::ChannelTarget::Alpha(i) => json!(i),
_ => return Err("target an alpha or spot channel first".into()),
};
let dialog = crate::channel_options_ui::open(app, &channel).ok_or("no such channel")?;
Ok(json!({"dialog": dialog}))
}
// Channels › New Spot Channel… without params: its dialog (with params: the engine).
crate::channel_options_ui::NEW_SPOT if params.as_object().is_none_or(|o| o.is_empty()) => {
let dialog = crate::channel_options_ui::open_new_spot(app).ok_or("no document is open")?;
Ok(json!({"dialog": dialog}))
}
// Photoshop's "Select and Mask…" is the engine's select.refineEdge.
// Select › Color Range… from the menu: the dialog (with params: the engine directly).
"select.colorRange" if params.as_object().is_none_or(|o| o.is_empty()) => Ok(json!({"dialog": crate::color_range_ui::open(app)})),