mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-14 19:26:51 -04:00
* 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:
27 files changed
+569
-4
No files matched your search
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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.
|
@@ -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.
|
@@ -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)
|
||||
|
||||
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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)
|
||||
|
||||
|
@@ -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.
|
@@ -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)
|
||||
|
||||
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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
|
||||
|
||||
|
@@ -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.
|
@@ -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)
|
||||
|
||||
|
@@ -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.
|
@@ -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;
|
||||
|
||||
@@ -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)})),
|
||||
|
||||
Reference in new issue
Block a user