mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-11 00:11:46 +00:00
Merge pull request #7121 from texjer/feat/camera-raw-sidecars
Camera Raw: Done saves the raws' settings to their sidecars without opening them
This commit is contained in:
31 files changed
+835
-77
No files matched your search
@@ -170,7 +170,7 @@ impl PhotocraftApp {
|
||||
distort: Default::default(),
|
||||
gradient: Default::default(),
|
||||
camera_raw: None,
|
||||
pending_raw_open: None,
|
||||
raw_shelf: Vec::new(),
|
||||
raw_redevelop: Vec::new(),
|
||||
wide_angle: None,
|
||||
dialog_edits: Default::default(),
|
||||
|
||||
@@ -30,6 +30,7 @@ use crate::widgets;
|
||||
|
||||
mod controls;
|
||||
mod convert;
|
||||
mod filmstrip;
|
||||
mod menu;
|
||||
mod open;
|
||||
mod proxy;
|
||||
@@ -38,6 +39,7 @@ mod sidecar;
|
||||
pub use controls::show;
|
||||
#[cfg(test)]
|
||||
use convert::open_pixels;
|
||||
pub use filmstrip::HeldRaw;
|
||||
pub use menu::menu;
|
||||
#[cfg(test)]
|
||||
use open::{Coverage, commit_open_raw, document_index, finish_redevelop, open_raw, replace_raw_document};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::open::{Target, cancel, commit_open_raw, open_raw, raw_stage};
|
||||
use super::open::{Target, cancel, raw_stage};
|
||||
use super::sidecar::{load_settings, report_file_error, save_settings};
|
||||
use super::{
|
||||
Align2, BANDS, CameraRaw, CameraRawDialog, Color32, ERect, FontId, PANEL_W, PhotocraftApp, Pos2, Sense, Stroke, Tokens, ToneZone, Value, Wheel, curve_lut,
|
||||
@@ -69,7 +69,7 @@ pub(super) fn commit(app: &mut PhotocraftApp) -> Result<Value, String> {
|
||||
Target::OpenRaw => {
|
||||
let raw = d.raw_open.clone().ok_or("no raw file is being opened")?;
|
||||
let (params, original) = (d.params.clone(), d.original.clone());
|
||||
commit_open_raw(app, ¶ms, &raw, Some(&original))?
|
||||
super::filmstrip::open_all(app, ¶ms, &raw, &original)?
|
||||
}
|
||||
};
|
||||
app.camera_raw = None;
|
||||
@@ -188,10 +188,11 @@ fn curve_editor(ui: &mut egui::Ui, p: &mut CameraRaw, dirty: &mut bool, state: &
|
||||
|
||||
pub fn show(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
||||
if app.camera_raw.is_none()
|
||||
&& let Some(raw) = app.pending_raw_open.take()
|
||||
&& let Err(e) = open_raw(app, ctx, raw)
|
||||
&& !app.raw_shelf.is_empty()
|
||||
&& let Err(e) = super::filmstrip::open_held(app, ctx, 0)
|
||||
{
|
||||
// The document stays open, developed with the defaults.
|
||||
app.raw_shelf.remove(0);
|
||||
app.ui.status = e;
|
||||
app.ui.status_error = true;
|
||||
}
|
||||
@@ -241,6 +242,7 @@ fn draw(app: &mut PhotocraftApp, ctx: &egui::Context, own_window: bool) {
|
||||
let screen = ctx.content_rect();
|
||||
let current_display = app.session.active().and_then(|st| app.session.color.canvas_display(&st.doc).ok());
|
||||
let mut action: Option<&str> = None;
|
||||
let mut show_held: Option<usize> = None;
|
||||
egui::Area::new(egui::Id::unique("camera-raw-dialog")).order(egui::Order::Foreground).fixed_pos(screen.min).show(ctx, |ui| {
|
||||
let Some(d) = app.camera_raw.as_mut() else { return };
|
||||
history_keys(d, ctx);
|
||||
@@ -278,7 +280,16 @@ fn draw(app: &mut PhotocraftApp, ctx: &egui::Context, own_window: bool) {
|
||||
let animated_zoom = app.session.prefs().general.animated_zoom;
|
||||
let scope = &mut app.ui.camera_raw_scope;
|
||||
// Clip both pixels and scope overlays before painting the settings dock/footer.
|
||||
let preview_body = ERect::from_min_max(body.min, pos2((body.right() - PANEL_W).max(body.left() + 1.0), body.bottom()));
|
||||
// Several raws being opened: the filmstrip on the left.
|
||||
let film_w = if d.target == Target::OpenRaw && app.raw_shelf.len() > 1 { super::filmstrip::FILM_W } else { 0.0 };
|
||||
if film_w > 0.0 {
|
||||
let film = ERect::from_min_max(body.min, pos2(body.left() + film_w - 1.0, body.bottom()));
|
||||
painter.rect_filled(film, 0.0, t.dock);
|
||||
painter.line_segment([film.right_top(), film.right_bottom()], Stroke::new(1.0, t.separator));
|
||||
show_held = super::filmstrip::filmstrip(ui, film, &mut app.raw_shelf, d.raw_open.as_ref().map(|r| r.document), &t);
|
||||
}
|
||||
let preview_body =
|
||||
ERect::from_min_max(pos2(body.left() + film_w, body.top()), pos2((body.right() - PANEL_W).max(body.left() + film_w + 1.0), body.bottom()));
|
||||
let view = ERect::from_min_max(
|
||||
preview_body.min + vec2(16.0, 16.0),
|
||||
pos2((preview_body.right() - 16.0).max(preview_body.left() + 17.0), (preview_body.bottom() - 48.0).max(preview_body.top() + 17.0)),
|
||||
@@ -476,6 +487,15 @@ fn draw(app: &mut PhotocraftApp, ctx: &egui::Context, own_window: bool) {
|
||||
) {
|
||||
action = Some(if role == widgets::ButtonRole::Default { "ok" } else { "cancel" });
|
||||
}
|
||||
// Done (open-time dialog): keep the settings in the raws' sidecars, open nothing.
|
||||
if d.target == Target::OpenRaw
|
||||
&& fu
|
||||
.add_sized([90.0, 28.0], egui::Button::new(tl!("Done")))
|
||||
.on_hover_text(tl!("Save the settings to each raw's sidecar without opening it"))
|
||||
.clicked()
|
||||
{
|
||||
action = Some("done");
|
||||
}
|
||||
fu.add_space(12.0);
|
||||
widgets::checkbox(&mut fu, &mut d.show_before, tl!("Before (Y)"));
|
||||
fu.label(egui::RichText::new(format!("{:.0} ms", d.render_ms)).color(t.text_faint));
|
||||
@@ -496,6 +516,12 @@ fn draw(app: &mut PhotocraftApp, ctx: &egui::Context, own_window: bool) {
|
||||
}
|
||||
});
|
||||
crate::camera_raw_scope_ui::persist(app, ctx);
|
||||
if let Some(i) = show_held
|
||||
&& let Err(e) = super::filmstrip::show_raw(app, ctx, i)
|
||||
{
|
||||
app.ui.status = e;
|
||||
app.ui.status_error = true;
|
||||
}
|
||||
if ctx.input(|i| i.key_pressed(egui::Key::Y))
|
||||
&& let Some(d) = app.camera_raw.as_mut()
|
||||
{
|
||||
@@ -511,6 +537,9 @@ fn draw(app: &mut PhotocraftApp, ctx: &egui::Context, own_window: bool) {
|
||||
}
|
||||
}
|
||||
Some("cancel") => cancel(app),
|
||||
Some("done") => {
|
||||
super::filmstrip::done(app);
|
||||
}
|
||||
Some("save") => {
|
||||
let r = save_settings(app);
|
||||
report_file_error(app, r);
|
||||
|
||||
@@ -48,24 +48,10 @@ pub(super) fn apply_raw_conversions(app: &mut PhotocraftApp, index: usize, d: &p
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn open_pixels(
|
||||
app: &mut PhotocraftApp,
|
||||
ctx: &egui::Context,
|
||||
layer: LayerId,
|
||||
surf: photocraft_raster::Surface,
|
||||
params: CameraRaw,
|
||||
coverage: Coverage,
|
||||
target: Target,
|
||||
) -> Result<(), String> {
|
||||
let st = app.session.active().ok_or("no document")?;
|
||||
let canvas = st.doc.bounds();
|
||||
let name = st.doc.layer(layer).map(|l| l.name.clone()).unwrap_or_default();
|
||||
let mut params = params;
|
||||
if params.encoding_gamma == 0.0 {
|
||||
params.encoding_gamma = photocraft_engine::lens_cmds::encoding_gamma(&st.doc);
|
||||
}
|
||||
// Use the engine filter domain, including pixels outside the canvas: spatial Camera Raw
|
||||
// stages (vignette, grain, local contrast) must agree with the committed result.
|
||||
/// What the preview develops, and its size: the engine filter domain, including pixels outside the
|
||||
/// canvas, since spatial Camera Raw stages (vignette, grain, local contrast) must agree with the
|
||||
/// committed result.
|
||||
pub(super) fn preview_area(canvas: Rect, surf: &photocraft_raster::Surface) -> Result<(Rect, usize, usize), String> {
|
||||
let area = canvas.union(&surf.content_bounds());
|
||||
let width = area.x1.checked_sub(area.x0).filter(|n| (1..=1_048_576).contains(n));
|
||||
let height = area.y1.checked_sub(area.y0).filter(|n| (1..=1_048_576).contains(n));
|
||||
@@ -76,6 +62,12 @@ pub(super) fn open_pixels(
|
||||
if w.checked_mul(h).is_none_or(|pixels| pixels > 512_000_000) {
|
||||
return Err("Camera Raw preview bounds are too large".into());
|
||||
}
|
||||
Ok((area, w, h))
|
||||
}
|
||||
|
||||
/// `area` of `surf` (`w`×`h`) box-averaged down to at most [`PROXY_SIDE`] pixels on its long side:
|
||||
/// the proxy, its size, and the side of the blocks averaged.
|
||||
pub(super) fn box_proxy(surf: &photocraft_raster::Surface, area: Rect, w: usize, h: usize) -> (Vec<[f32; 4]>, usize, usize, usize) {
|
||||
let k = w.max(h).div_ceil(PROXY_SIDE).max(1);
|
||||
let (pw, ph) = (w.div_ceil(k), h.div_ceil(k));
|
||||
let mut proxy = vec![[0.0f32; 4]; pw * ph];
|
||||
@@ -98,6 +90,27 @@ pub(super) fn open_pixels(
|
||||
proxy[py * pw + x] = [a[0] / a[4], a[1] / a[4], a[2] / a[4], a[3] / a[4]];
|
||||
}
|
||||
}
|
||||
(proxy, pw, ph, k)
|
||||
}
|
||||
|
||||
pub(super) fn open_pixels(
|
||||
app: &mut PhotocraftApp,
|
||||
ctx: &egui::Context,
|
||||
layer: LayerId,
|
||||
surf: photocraft_raster::Surface,
|
||||
params: CameraRaw,
|
||||
coverage: Coverage,
|
||||
target: Target,
|
||||
) -> Result<(), String> {
|
||||
let st = app.session.active().ok_or("no document")?;
|
||||
let canvas = st.doc.bounds();
|
||||
let name = st.doc.layer(layer).map(|l| l.name.clone()).unwrap_or_default();
|
||||
let mut params = params;
|
||||
if params.encoding_gamma == 0.0 {
|
||||
params.encoding_gamma = photocraft_engine::lens_cmds::encoding_gamma(&st.doc);
|
||||
}
|
||||
let (area, w, h) = preview_area(canvas, &surf)?;
|
||||
let (proxy, pw, ph, k) = box_proxy(&surf, area, w, h);
|
||||
// Coverage at the centre of each proxy block.
|
||||
let sample = |value: &dyn Fn(i32, i32) -> f32| {
|
||||
(0..ph)
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
//! The open-time dialog's filmstrip: raws opened together wait side by side in one Camera Raw
|
||||
//! dialog, as in Photoshop's. Each keeps its own settings and demosaic choice when another is
|
||||
//! shown, Open develops every one and Cancel closes every one.
|
||||
|
||||
use super::open::{Target, commit_open_raw, open_raw_with, return_on_failure};
|
||||
use super::sidecar::read_sidecar;
|
||||
use super::{
|
||||
Align2, CameraRaw, CameraRawDialog, Color32, ERect, FontId, PhotocraftApp, RawOpen, Rect, Sense, Stroke, Tokens, Value, json, pos2, vec2, widgets,
|
||||
};
|
||||
|
||||
/// The filmstrip column, shown when more than one raw is being opened.
|
||||
pub(super) const FILM_W: f32 = 150.0;
|
||||
/// Long side of a filmstrip thumbnail, in pixels.
|
||||
const THUMB_SIDE: usize = 240;
|
||||
|
||||
/// A raw waiting in the open-time dialog: opened (developed with the defaults) and reviewed
|
||||
/// before Open keeps it.
|
||||
pub struct HeldRaw {
|
||||
pub raw: RawOpen,
|
||||
/// Its settings and the settings it started from (its sidecar's, or the defaults) once it
|
||||
/// has been shown and left for another (`None`: not edited yet).
|
||||
pub(super) kept: Option<(CameraRaw, CameraRaw)>,
|
||||
/// The filmstrip thumbnail until it is first drawn, then its texture.
|
||||
pub(super) thumb: Option<egui::ColorImage>,
|
||||
tex: Option<egui::TextureHandle>,
|
||||
}
|
||||
|
||||
impl HeldRaw {
|
||||
pub(super) fn new(app: &PhotocraftApp, raw: RawOpen) -> Self {
|
||||
let thumb = super::open::document_index(app, raw.document).and_then(|i| thumbnail(app, i));
|
||||
HeldRaw { raw, kept: None, thumb, tex: None }
|
||||
}
|
||||
}
|
||||
|
||||
/// A filmstrip thumbnail of document `index` (a just-developed raw: its first pixel layer).
|
||||
fn thumbnail(app: &PhotocraftApp, index: usize) -> Option<egui::ColorImage> {
|
||||
let doc = &app.session.documents().get(index)?.doc;
|
||||
let surf = doc.layers.iter().find_map(|l| l.surface())?;
|
||||
let area = doc.bounds();
|
||||
let (w, h) = (area.width() as usize, area.height() as usize);
|
||||
if w == 0 || h == 0 {
|
||||
return None;
|
||||
}
|
||||
let k = w.max(h).div_ceil(THUMB_SIDE).max(1);
|
||||
let (tw, th) = (w.div_ceil(k), h.div_ceil(k));
|
||||
let mut acc = vec![[0.0f32; 5]; tw * th];
|
||||
let mut row = vec![[0.0f32; 4]; w];
|
||||
for y in (0..h).step_by(k.div_ceil(2).max(1)) {
|
||||
surf.read_rgba_into(Rect::new(area.x0, area.y0 + y as i32, area.x1, area.y0 + y as i32 + 1), &mut row);
|
||||
for (x, q) in row.iter().enumerate().step_by(k.div_ceil(2).max(1)) {
|
||||
let a = &mut acc[(y / k) * tw + x / k];
|
||||
for c in 0..4 {
|
||||
a[c] += q[c];
|
||||
}
|
||||
a[4] += 1.0;
|
||||
}
|
||||
}
|
||||
let px: Vec<[f32; 4]> = acc.iter().map(|a| if a[4] > 0.0 { [a[0] / a[4], a[1] / a[4], a[2] / a[4], a[3] / a[4]] } else { [0.0; 4] }).collect();
|
||||
let display = app.session.color.canvas_display(doc).ok()?;
|
||||
Some(CameraRawDialog::image(&px, tw, th, &display))
|
||||
}
|
||||
|
||||
/// `px` (`w`×`h`) box-averaged down to at most [`THUMB_SIDE`] pixels on its long side.
|
||||
fn shrink(px: &[[f32; 4]], w: usize, h: usize) -> (Vec<[f32; 4]>, usize, usize) {
|
||||
let k = w.max(h).div_ceil(THUMB_SIDE).max(1);
|
||||
let (tw, th) = (w.div_ceil(k), h.div_ceil(k));
|
||||
let mut acc = vec![[0.0f32; 5]; tw * th];
|
||||
for (i, q) in px.iter().enumerate().take(w * h) {
|
||||
if let Some(a) = acc.get_mut((i / w / k) * tw + (i % w) / k) {
|
||||
for c in 0..4 {
|
||||
a[c] += q[c];
|
||||
}
|
||||
a[4] += 1.0;
|
||||
}
|
||||
}
|
||||
(acc.iter().map(|a| if a[4] > 0.0 { [a[0] / a[4], a[1] / a[4], a[2] / a[4], a[3] / a[4]] } else { [0.0; 4] }).collect(), tw, th)
|
||||
}
|
||||
|
||||
/// Shows raw `i` of the filmstrip, with the settings it was left with (its sidecar's, the
|
||||
/// Camera Raw Defaults' Auto, or the defaults the first time).
|
||||
pub(super) fn open_held(app: &mut PhotocraftApp, ctx: &egui::Context, i: usize) -> Result<(), String> {
|
||||
let held = app.raw_shelf.get(i).ok_or("no raw file at that place in the filmstrip")?;
|
||||
let (raw, kept) = (held.raw.clone(), held.kept.clone());
|
||||
open_raw_with(app, ctx, raw, kept)
|
||||
}
|
||||
|
||||
/// Shows raw `to` of the filmstrip; the one being left keeps its settings, and its thumbnail
|
||||
/// shows them.
|
||||
pub(super) fn show_raw(app: &mut PhotocraftApp, ctx: &egui::Context, to: usize) -> Result<(), String> {
|
||||
let Some(d) = app.camera_raw.as_ref() else { return Ok(()) };
|
||||
let current = d.raw_open.as_ref().map(|r| r.document);
|
||||
if d.target != Target::OpenRaw || app.raw_shelf.get(to).is_none_or(|h| Some(h.raw.document) == current) {
|
||||
return Ok(());
|
||||
}
|
||||
keep_settings(app);
|
||||
// On success this replaces the dialog. On failure the dialog stays on the raw being left, and
|
||||
// the other keeps its place in the filmstrip.
|
||||
let active = app.session.active_index();
|
||||
let shown = open_held(app, ctx, to);
|
||||
if shown.is_err()
|
||||
&& let Some(i) = active
|
||||
{
|
||||
app.session.set_active(i);
|
||||
}
|
||||
shown
|
||||
}
|
||||
|
||||
/// Stores the dialog's settings (and demosaic choice) on its filmstrip raw, and refreshes the
|
||||
/// raw's thumbnail from the preview.
|
||||
pub(super) fn keep_settings(app: &mut PhotocraftApp) {
|
||||
let Some(d) = app.camera_raw.as_ref() else { return };
|
||||
let Some(raw) = &d.raw_open else { return };
|
||||
let Some(held) = app.raw_shelf.iter_mut().find(|h| h.raw.document == raw.document) else { return };
|
||||
held.kept = Some((d.params.clone(), d.original.clone()));
|
||||
held.raw.demosaic = raw.demosaic;
|
||||
// The preview may not have caught up with the last edit: the thumbnail shows what was kept.
|
||||
let frame = (d.dirty || d.render_pending()).then(|| d.proxy_job().run().px);
|
||||
let developed = frame.as_deref().unwrap_or(&d.processed);
|
||||
if !developed.is_empty() {
|
||||
let (px, w, h) = shrink(developed, d.pw, d.ph);
|
||||
held.thumb = Some(CameraRawDialog::image(&px, w, h, &d.display));
|
||||
held.tex = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// The settings Open develops a never-shown filmstrip raw with, and what they started from:
|
||||
/// its sidecar's, else the Camera Raw Defaults' Auto when that is on, else the defaults.
|
||||
pub(super) fn first_settings(app: &PhotocraftApp, raw: &RawOpen) -> (CameraRaw, Option<CameraRaw>) {
|
||||
if let Some((loaded, _, _)) = raw.path.as_deref().and_then(read_sidecar) {
|
||||
return (loaded.clone(), Some(loaded));
|
||||
}
|
||||
let mut p = CameraRaw::default();
|
||||
// From the proxy the dialog would use, so showing a raw doesn't change what Auto gives it.
|
||||
if app.session.prefs().raw_defaults.apply_auto_tone
|
||||
&& let Some(st) = super::open::document_index(app, raw.document).and_then(|i| app.session.documents().get(i))
|
||||
&& let Some(surf) = st.doc.layers.iter().find_map(|l| l.surface())
|
||||
&& let Ok((area, w, h)) = super::convert::preview_area(st.doc.bounds(), surf)
|
||||
{
|
||||
let (proxy, ..) = super::convert::box_proxy(surf, area, w, h);
|
||||
photocraft_algo::camera_raw::auto_tone(&proxy, &mut p);
|
||||
}
|
||||
(p, None)
|
||||
}
|
||||
|
||||
/// Open with raws in the filmstrip: the shown one as the dialog has it, then every other with
|
||||
/// its own settings. One reply for one raw, `{"opened": [...]}` for several. A raw leaves the
|
||||
/// filmstrip once it has opened or its background develop has started: one that fails stays or
|
||||
/// comes back there with its settings, so the dialog returns for it (or Cancel still closes it).
|
||||
pub(super) fn open_all(app: &mut PhotocraftApp, params: &CameraRaw, raw: &RawOpen, original: &CameraRaw) -> Result<Value, String> {
|
||||
keep_settings(app);
|
||||
let others: Vec<photocraft_doc::DocId> = app.raw_shelf.iter().map(|h| h.raw.document).filter(|id| *id != raw.document).collect();
|
||||
let first = commit_open_raw(app, params, raw, Some(original));
|
||||
if let Ok(v) = &first {
|
||||
return_on_failure(app, v, original);
|
||||
app.raw_shelf.retain(|h| h.raw.document != raw.document);
|
||||
}
|
||||
if others.is_empty() {
|
||||
return first;
|
||||
}
|
||||
let mut opened = Vec::new();
|
||||
let mut failed = Vec::new();
|
||||
match first {
|
||||
Ok(v) => opened.push(v),
|
||||
Err(e) => failed.push(format!("{}: {e}", raw.name)),
|
||||
}
|
||||
for id in others {
|
||||
let Some(held) = app.raw_shelf.iter().find(|h| h.raw.document == id) else { continue };
|
||||
let (held_raw, kept) = (held.raw.clone(), held.kept.clone());
|
||||
let (p, loaded) = match kept {
|
||||
Some((p, original)) => (p, Some(original)),
|
||||
None => first_settings(app, &held_raw),
|
||||
};
|
||||
match commit_open_raw(app, &p, &held_raw, loaded.as_ref()) {
|
||||
Ok(v) => {
|
||||
return_on_failure(app, &v, loaded.as_ref().unwrap_or(&CameraRaw::default()));
|
||||
opened.push(v);
|
||||
app.raw_shelf.retain(|h| h.raw.document != id);
|
||||
}
|
||||
Err(e) => failed.push(format!("{}: {e}", held_raw.name)),
|
||||
}
|
||||
}
|
||||
if !failed.is_empty() {
|
||||
let e = failed.join("; ");
|
||||
if opened.is_empty() {
|
||||
return Err(e);
|
||||
}
|
||||
crate::notices::error(app, e);
|
||||
}
|
||||
Ok(json!({"opened": opened}))
|
||||
}
|
||||
|
||||
/// Done: saves each raw's settings to its sidecar, as Camera Raw's Done does, and opens none of
|
||||
/// them (they close again).
|
||||
pub(super) fn done(app: &mut PhotocraftApp) -> Value {
|
||||
use super::sidecar::Stored;
|
||||
keep_settings(app);
|
||||
let Some(d) = app.camera_raw.as_ref() else { return Value::Null };
|
||||
let Some(raw) = d.raw_open.clone() else { return Value::Null };
|
||||
let mut raws = vec![(raw.clone(), d.params.clone())];
|
||||
for held in app.raw_shelf.iter().filter(|h| h.raw.document != raw.document) {
|
||||
let p = match &held.kept {
|
||||
Some((p, _)) => p.clone(),
|
||||
None => first_settings(app, &held.raw).0,
|
||||
};
|
||||
raws.push((held.raw.clone(), p));
|
||||
}
|
||||
let stored: Vec<Stored> = raws.iter().map(|(raw, p)| super::sidecar::store_sidecar(app, raw, p)).collect();
|
||||
let saved: Vec<&String> = stored.iter().filter_map(|s| if let Stored::Written(name) = s { Some(name) } else { None }).collect();
|
||||
// A failed write (already in a notice) keeps the dialog and every raw open, so no edit is lost.
|
||||
if stored.contains(&Stored::Failed) {
|
||||
app.ui.status = tl!("Some settings could not be saved; the raws stay open").to_string();
|
||||
app.ui.status_error = true;
|
||||
return json!({"done": false, "saved": saved});
|
||||
}
|
||||
app.ui.status = if !saved.is_empty() {
|
||||
crate::i18n::fmt(tl!("Saved Camera Raw settings to {file}"), &[("file", &saved.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", "))])
|
||||
} else if stored.iter().all(|s| *s == Stored::Defaults) {
|
||||
tl!("Nothing to save: these raws are at their default settings").to_string()
|
||||
} else {
|
||||
tl!("No settings saved: these raws can't keep a sidecar here").to_string()
|
||||
};
|
||||
app.ui.status_error = saved.is_empty() && stored.contains(&Stored::Skipped);
|
||||
let saved = json!(saved);
|
||||
app.camera_raw = None;
|
||||
app.raw_shelf.clear();
|
||||
for (raw, _) in &raws {
|
||||
if let Some(index) = super::open::document_index(app, raw.document) {
|
||||
app.session.close(index);
|
||||
}
|
||||
}
|
||||
app.sync_views();
|
||||
json!({"done": true, "saved": saved})
|
||||
}
|
||||
|
||||
/// The filmstrip of raws being opened: a thumbnail and name each, the shown one outlined, a dot
|
||||
/// on edited ones. Returns the one clicked.
|
||||
pub(super) fn filmstrip(ui: &mut egui::Ui, rect: ERect, shelf: &mut [HeldRaw], current: Option<photocraft_doc::DocId>, t: &Tokens) -> Option<usize> {
|
||||
let mut clicked = None;
|
||||
let mut film = ui.new_child(egui::UiBuilder::new().max_rect(rect.shrink2(vec2(6.0, 6.0))));
|
||||
film.set_clip_rect(rect.intersect(ui.clip_rect()));
|
||||
egui::ScrollArea::vertical().id_salt("camera-raw-filmstrip").auto_shrink([false, false]).show(&mut film, |ui| {
|
||||
ui.spacing_mut().item_spacing.y = 6.0;
|
||||
let w = ui.available_width();
|
||||
for (i, held) in shelf.iter_mut().enumerate() {
|
||||
if held.tex.is_none()
|
||||
&& let Some(img) = held.thumb.take()
|
||||
{
|
||||
held.tex = Some(ui.ctx().load_texture(format!("camera-raw-thumb-{}", held.raw.document.0), img, egui::TextureOptions::LINEAR));
|
||||
}
|
||||
let (tile, response) = ui.allocate_exact_size(vec2(w, w * 0.75 + 18.0), Sense::click());
|
||||
let frame = ERect::from_min_size(tile.min, vec2(w, w * 0.75));
|
||||
let p = ui.painter();
|
||||
p.rect_filled(frame, 2.0, if response.hovered() { t.hover } else { t.canvas });
|
||||
if let Some(tex) = &held.tex {
|
||||
let size = tex.size_vec2();
|
||||
let fit = ((frame.width() - 8.0) / size.x.max(1.0)).min((frame.height() - 8.0) / size.y.max(1.0));
|
||||
let r = ERect::from_center_size(frame.center(), size * fit);
|
||||
widgets::checker(p, r, 6.0);
|
||||
p.image(tex.id(), r, ERect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
|
||||
}
|
||||
if Some(held.raw.document) == current {
|
||||
p.rect_stroke(frame, 2.0, Stroke::new(2.0, t.accent), egui::StrokeKind::Inside);
|
||||
}
|
||||
if held.kept.as_ref().is_some_and(|(p, _)| !p.is_identity()) {
|
||||
p.circle_filled(frame.right_top() + vec2(-7.0, 7.0), 3.0, t.text);
|
||||
}
|
||||
let name = ERect::from_min_max(pos2(tile.left(), frame.bottom()), tile.max);
|
||||
let font = FontId::proportional(10.5);
|
||||
let label = tail_fitting(p, &held.raw.name, font.clone(), name.width());
|
||||
p.with_clip_rect(name).text(name.center(), Align2::CENTER_CENTER, label, font, t.text_dim);
|
||||
let response = response.on_hover_text(&held.raw.name);
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, true, Some(held.raw.document) == current, &held.raw.name));
|
||||
if response.clicked() {
|
||||
clicked = Some(i);
|
||||
}
|
||||
}
|
||||
});
|
||||
clicked
|
||||
}
|
||||
|
||||
/// `name`, or "…" and as much of its end as fits in `width`: raws from one shoot differ at the
|
||||
/// end of their names (the frame number).
|
||||
fn tail_fitting(p: &egui::Painter, name: &str, font: FontId, width: f32) -> String {
|
||||
let fits = |s: &str| p.layout_no_wrap(s.to_owned(), font.clone(), Color32::WHITE).size().x <= width;
|
||||
if fits(name) {
|
||||
return name.to_owned();
|
||||
}
|
||||
let chars: Vec<char> = name.chars().collect();
|
||||
(1..chars.len()).map(|start| format!("…{}", chars[start..].iter().collect::<String>())).find(|s| fits(s)).unwrap_or_default()
|
||||
}
|
||||
@@ -149,7 +149,7 @@ fn queue_identity_targets_an_inactive_raw_without_changing_the_active_document()
|
||||
app.session.set_active(0);
|
||||
assert_eq!(app.session.active().unwrap().doc.id, control.doc.id);
|
||||
queue_open_dialog(&mut app, raw.document, &raw.name, raw.path.as_deref(), raw.bytes.as_deref().map(Vec::as_slice), &[]).unwrap();
|
||||
let queued = app.pending_raw_open.as_ref().expect("the explicitly named RAW was queued");
|
||||
let queued = app.raw_shelf.first().map(|h| &h.raw).expect("the explicitly named RAW was queued");
|
||||
assert_eq!(queued.document, raw.document);
|
||||
assert_eq!((&queued.name, &queued.path, &queued.bytes), (&raw.name, &raw.path, &raw.bytes));
|
||||
assert_eq!(app.session.active().unwrap().doc.id, control.doc.id, "queueing does not activate the target");
|
||||
@@ -160,12 +160,12 @@ fn queue_identity_targets_an_inactive_raw_without_changing_the_active_document()
|
||||
fn queue_identity_rejects_a_missing_document_without_replacing_the_pending_review() {
|
||||
let (mut app, raw, control) = identity_app();
|
||||
queue_open_dialog(&mut app, raw.document, &raw.name, raw.path.as_deref(), raw.bytes.as_deref().map(Vec::as_slice), &[]).unwrap();
|
||||
let pending = app.pending_raw_open.as_ref().unwrap().clone();
|
||||
let pending = app.raw_shelf.first().map(|h| &h.raw).unwrap().clone();
|
||||
app.session.set_active(0);
|
||||
let missing = photocraft_doc::DocId::fresh();
|
||||
assert!(!app.session.documents().iter().any(|state| state.doc.id == missing));
|
||||
assert!(queue_open_dialog(&mut app, missing, "missing.dng", Some("missing.dng"), Some(&[1, 2, 3]), &[]).is_err());
|
||||
let queued = app.pending_raw_open.as_ref().expect("the existing review is preserved");
|
||||
let queued = app.raw_shelf.first().map(|h| &h.raw).expect("the existing review is preserved");
|
||||
assert_eq!(queued.document, pending.document);
|
||||
assert_eq!((&queued.name, &queued.path, &queued.bytes), (&pending.name, &pending.path, &pending.bytes));
|
||||
assert_eq!(app.session.active().unwrap().doc.id, control.doc.id);
|
||||
|
||||
@@ -35,15 +35,16 @@ pub fn menu(app: &mut PhotocraftApp, ctx: &egui::Context, id: &str, params: &Val
|
||||
for (key, value) in fields {
|
||||
match key.as_str() {
|
||||
"set" | "scope" | "view" | "demosaic" => {}
|
||||
"before" | "commit" | "cancel" | "saveSettings" | "loadSettings" | "undo" | "redo" | "auto" if value.is_boolean() => {}
|
||||
"before" | "commit" | "cancel" | "saveSettings" | "loadSettings" | "undo" | "redo" | "auto" => {
|
||||
"before" | "commit" | "cancel" | "done" | "saveSettings" | "loadSettings" | "undo" | "redo" | "auto" if value.is_boolean() => {}
|
||||
"before" | "commit" | "cancel" | "done" | "saveSettings" | "loadSettings" | "undo" | "redo" | "auto" => {
|
||||
return Some(Err(format!("Camera Raw {key} must be a boolean")));
|
||||
}
|
||||
_ => return Some(Err(format!("unknown Camera Raw ui property {key}"))),
|
||||
}
|
||||
}
|
||||
if ui.get("commit").and_then(Value::as_bool) == Some(true) && ui.get("cancel").and_then(Value::as_bool) == Some(true) {
|
||||
return Some(Err("Camera Raw cannot commit and cancel together".into()));
|
||||
let ends = ["commit", "cancel", "done"].into_iter().filter(|k| ui.get(*k).and_then(Value::as_bool) == Some(true)).count();
|
||||
if ends > 1 {
|
||||
return Some(Err("Camera Raw takes one of commit, cancel and done".into()));
|
||||
}
|
||||
for (key, run) in [("saveSettings", save_settings as fn(&mut PhotocraftApp) -> Result<Value, String>), ("loadSettings", load_settings)] {
|
||||
if ui.get(key).and_then(Value::as_bool) == Some(true) {
|
||||
@@ -72,6 +73,9 @@ fn menu_update(app: &mut PhotocraftApp, ctx: &egui::Context, ui: &Value) -> Opti
|
||||
// Stage both filter and presentation updates. No texture, preference or dialog mutation
|
||||
// occurs until every part of the request has passed validation.
|
||||
let d = app.camera_raw.as_ref()?;
|
||||
if ui.get("done").and_then(Value::as_bool) == Some(true) && d.target != super::open::Target::OpenRaw {
|
||||
return Some(Err("Done is for raws being opened".into()));
|
||||
}
|
||||
let mut next_params = d.params.clone();
|
||||
// Auto (the Light panel's button): the tone sliders from the image's histogram.
|
||||
if ui.get("auto").and_then(Value::as_bool) == Some(true) {
|
||||
@@ -165,5 +169,8 @@ fn menu_update(app: &mut PhotocraftApp, ctx: &egui::Context, ui: &Value) -> Opti
|
||||
if ui.get("commit").and_then(Value::as_bool) == Some(true) {
|
||||
return Some(commit(app));
|
||||
}
|
||||
if ui.get("done").and_then(Value::as_bool) == Some(true) {
|
||||
return Some(Ok(super::filmstrip::done(app)));
|
||||
}
|
||||
Some(Ok(app.camera_raw.as_ref().map(|d| d.describe(&app.ui.camera_raw_scope, &app.ui.camera_raw_preview)).unwrap_or(Value::Null)))
|
||||
}
|
||||
@@ -45,7 +45,8 @@ pub fn without_develop_note(warnings: &[String]) -> Vec<String> {
|
||||
}
|
||||
|
||||
/// Queues the open-time dialog for the document whose import completed, independent of the
|
||||
/// active tab. A delayed completion for a closed document must not review another photograph.
|
||||
/// active tab: the dialog shows on the next frame, or adds it to its filmstrip when it is already
|
||||
/// open. A delayed completion for a closed document must not review another photograph.
|
||||
/// `warnings` are the import's: its note says which demosaic method the open developed with.
|
||||
pub fn queue_open_dialog(
|
||||
app: &mut PhotocraftApp,
|
||||
@@ -61,22 +62,42 @@ pub fn queue_open_dialog(
|
||||
// The method the open actually used (`jobs::import_options` at the job's start), from its
|
||||
// note: the preference may have changed while a background open ran.
|
||||
let method = photocraft_io::raw::developed_demosaic(warnings).unwrap_or_else(|| app.session.prefs().raw_defaults.demosaic.method());
|
||||
app.pending_raw_open = Some(RawOpen {
|
||||
if app.raw_shelf.iter().any(|h| h.raw.document == document) {
|
||||
return Ok(());
|
||||
}
|
||||
let raw = RawOpen {
|
||||
document,
|
||||
name: name.to_string(),
|
||||
path: path.map(str::to_string),
|
||||
bytes: bytes.map(|b| std::sync::Arc::new(b.to_vec())),
|
||||
developed_with: method,
|
||||
demosaic: method,
|
||||
});
|
||||
};
|
||||
let held = super::filmstrip::HeldRaw::new(app, raw);
|
||||
app.raw_shelf.push(held);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Opens the queued open-time dialog on its document's (only) layer.
|
||||
/// Opens the open-time dialog on a queued raw's (only) layer.
|
||||
#[cfg(test)]
|
||||
pub(super) fn open_raw(app: &mut PhotocraftApp, ctx: &egui::Context, raw: RawOpen) -> Result<(), String> {
|
||||
open_raw_with(app, ctx, raw, None)
|
||||
}
|
||||
|
||||
/// [`open_raw`], or with `kept` settings and the settings they started from (a filmstrip raw
|
||||
/// shown again).
|
||||
pub(super) fn open_raw_with(app: &mut PhotocraftApp, ctx: &egui::Context, raw: RawOpen, kept: Option<(CameraRaw, CameraRaw)>) -> Result<(), String> {
|
||||
let index = document_index(app, raw.document).ok_or("the raw document was closed")?;
|
||||
app.session.set_active(index);
|
||||
let (layer, surf, _) = crate::distort_ui::active_pixels(app)?;
|
||||
if let Some((params, original)) = kept {
|
||||
open_pixels(app, ctx, layer, surf, params, Coverage::None, Target::OpenRaw)?;
|
||||
if let Some(d) = app.camera_raw.as_mut() {
|
||||
d.original = original;
|
||||
}
|
||||
attach_raw(app, raw);
|
||||
return Ok(());
|
||||
}
|
||||
// The file's sidecar (`IMG_0001.xmp` beside it) fills the dialog, as Camera Raw's does; the
|
||||
// status bar names what it could not apply.
|
||||
let mut params = CameraRaw::default();
|
||||
@@ -103,6 +124,12 @@ pub(super) fn open_raw(app: &mut PhotocraftApp, ctx: &egui::Context, raw: RawOpe
|
||||
photocraft_algo::camera_raw::auto_tone(&d.proxy, &mut d.params);
|
||||
d.dirty = true;
|
||||
}
|
||||
attach_raw(app, raw);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gives the open dialog its raw: the file name, and the camera tone curve it was developed with.
|
||||
fn attach_raw(app: &mut PhotocraftApp, raw: RawOpen) {
|
||||
let curve = match (&raw.bytes, &raw.path) {
|
||||
(Some(b), _) => Some(photocraft_io::raw::tone_curve(b)),
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -117,7 +144,6 @@ pub(super) fn open_raw(app: &mut PhotocraftApp, ctx: &egui::Context, raw: RawOpe
|
||||
d.raw_curve = curve;
|
||||
d.raw_open = Some(raw);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn document_index(app: &PhotocraftApp, id: photocraft_doc::DocId) -> Option<usize> {
|
||||
@@ -153,6 +179,9 @@ pub struct Redevelop {
|
||||
filter_step: bool,
|
||||
/// The Camera Raw Defaults the open started with, for the conversions after the filter step.
|
||||
defaults: photocraft_engine::prefs::RawDefaults,
|
||||
/// The dialog's Open started this, from these settings: if the re-develop fails, the raw goes
|
||||
/// back to the filmstrip to be tried again or cancelled.
|
||||
back_to_filmstrip: Option<CameraRaw>,
|
||||
}
|
||||
|
||||
/// The sliders a raw open applies while developing, before the tone curve (as Camera Raw does).
|
||||
@@ -225,7 +254,7 @@ pub(super) fn commit_open_raw(app: &mut PhotocraftApp, params: &CameraRaw, raw:
|
||||
}
|
||||
photocraft_engine::jobs::Started::Job(job) => {
|
||||
let defaults = app.session.prefs().raw_defaults.clone();
|
||||
app.raw_redevelop.push(Redevelop { job, params: params.clone(), raw: raw.clone(), filter_step: false, defaults });
|
||||
app.raw_redevelop.push(Redevelop { job, params: params.clone(), raw: raw.clone(), filter_step: false, defaults, back_to_filmstrip: None });
|
||||
app.ui.status = label;
|
||||
app.ui.status_error = false;
|
||||
Ok(json!({"job": job.0, "pending": true}))
|
||||
@@ -233,6 +262,15 @@ pub(super) fn commit_open_raw(app: &mut PhotocraftApp, params: &CameraRaw, raw:
|
||||
}
|
||||
}
|
||||
|
||||
/// `reply` is [`commit_open_raw`]'s from the dialog's Open: if it started a background re-develop
|
||||
/// and that fails, the raw comes back to the filmstrip, its settings having started from `original`.
|
||||
pub(super) fn return_on_failure(app: &mut PhotocraftApp, reply: &Value, original: &CameraRaw) {
|
||||
let job = reply.get("job").and_then(Value::as_u64);
|
||||
if let Some(pending) = app.raw_redevelop.iter_mut().find(|r| Some(r.job.0) == job && !r.filter_step) {
|
||||
pending.back_to_filmstrip = Some(original.clone());
|
||||
}
|
||||
}
|
||||
|
||||
/// Swap the default-developed document `doc_id` for the re-developed one, at the same tab
|
||||
/// position and with the same path. Returns `{document, docId, previous, warnings, wb}`:
|
||||
/// `document` is the tab index at apply time, `docId` is the replacement's stable identity.
|
||||
@@ -324,7 +362,7 @@ fn finish_open_raw(
|
||||
&& let Some(job) = r.get("job").and_then(Value::as_u64)
|
||||
{
|
||||
let job = photocraft_engine::jobs::JobId(job);
|
||||
app.raw_redevelop.push(Redevelop { job, params: rest.clone(), raw: raw.clone(), filter_step: true, defaults: d.clone() });
|
||||
app.raw_redevelop.push(Redevelop { job, params: rest.clone(), raw: raw.clone(), filter_step: true, defaults: d.clone(), back_to_filmstrip: None });
|
||||
result["filter"] = r;
|
||||
mark_unmodified(app, index);
|
||||
return Ok(result);
|
||||
@@ -373,7 +411,7 @@ pub fn opened_without_dialog(app: &mut PhotocraftApp, index: usize, name: &str,
|
||||
|
||||
/// Every k-th pixel of a surface's content, k chosen so the longer side has about `max_side`
|
||||
/// samples: enough for a histogram (Auto tone) without reading a whole raw.
|
||||
fn sample_pixels(surf: &photocraft_raster::Surface, max_side: usize) -> Vec<[f32; 4]> {
|
||||
pub(super) fn sample_pixels(surf: &photocraft_raster::Surface, max_side: usize) -> Vec<[f32; 4]> {
|
||||
let area = surf.content_bounds();
|
||||
let (w, h) = (area.x1.saturating_sub(area.x0).max(0) as usize, area.y1.saturating_sub(area.y0).max(0) as usize);
|
||||
if w == 0 || h == 0 {
|
||||
@@ -435,7 +473,15 @@ pub fn on_redevelop_event(app: &mut PhotocraftApp, e: &photocraft_engine::jobs::
|
||||
Err(err) => crate::notices::error(app, format!("{}: {err}", e.label)),
|
||||
},
|
||||
photocraft_engine::jobs::JobOutcome::Failed(err) => {
|
||||
// The document stays open, developed with the defaults.
|
||||
// The document stays open, developed with the defaults; from the dialog, it goes back
|
||||
// to the filmstrip with its settings, which may have no sidecar to keep them.
|
||||
if let Some(original) = pending.back_to_filmstrip
|
||||
&& document_index(app, pending.raw.document).is_some()
|
||||
{
|
||||
let mut held = super::filmstrip::HeldRaw::new(app, pending.raw);
|
||||
held.kept = Some((pending.params, original));
|
||||
app.raw_shelf.push(held);
|
||||
}
|
||||
app.sync_views();
|
||||
crate::notices::error(app, format!("{}: {err}", e.label));
|
||||
}
|
||||
@@ -447,13 +493,18 @@ pub fn on_redevelop_event(app: &mut PhotocraftApp, e: &photocraft_engine::jobs::
|
||||
true
|
||||
}
|
||||
|
||||
/// Cancel: closes the dialog; for the open-time dialog also the document it was opening.
|
||||
/// Cancel: closes the dialog; for the open-time dialog also the documents it was opening (every
|
||||
/// raw in its filmstrip).
|
||||
pub(super) fn cancel(app: &mut PhotocraftApp) {
|
||||
let Some(d) = app.camera_raw.take() else { return };
|
||||
if let (Target::OpenRaw, Some(raw)) = (d.target, &d.raw_open)
|
||||
&& let Some(index) = document_index(app, raw.document)
|
||||
{
|
||||
app.session.close(index);
|
||||
if let (Target::OpenRaw, Some(raw)) = (d.target, &d.raw_open) {
|
||||
let mut opening: Vec<_> = std::mem::take(&mut app.raw_shelf).into_iter().map(|h| h.raw.document).collect();
|
||||
opening.push(raw.document);
|
||||
for id in opening {
|
||||
if let Some(index) = document_index(app, id) {
|
||||
app.session.close(index);
|
||||
}
|
||||
}
|
||||
app.sync_views();
|
||||
app.ui.status = crate::i18n::fmt(tl!("Canceled opening {name}"), &[("name", &raw.name)]);
|
||||
app.ui.status_error = false;
|
||||
|
||||
@@ -52,27 +52,49 @@ pub(super) fn write_sidecar(app: &mut PhotocraftApp, raw: &RawOpen, params: &Cam
|
||||
if !app.session.prefs().raw_defaults.write_xmp_sidecars || loaded == Some(params) {
|
||||
return;
|
||||
}
|
||||
store_sidecar(app, raw, params);
|
||||
}
|
||||
|
||||
/// What [`store_sidecar`] did.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(super) enum Stored {
|
||||
/// Written: the sidecar's name.
|
||||
Written(String),
|
||||
/// Nothing to keep: default settings and no sidecar of ours to update.
|
||||
Defaults,
|
||||
/// This raw can't keep a sidecar here: an automation request, a DNG or a raw without a path,
|
||||
/// another application's sidecar, or no file writer.
|
||||
Skipped,
|
||||
/// The write failed (already reported in a notice).
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// Writes `params` to `raw`'s sidecar under [`write_sidecar`]'s rules, whatever the preference
|
||||
/// (Done asks for it).
|
||||
pub(super) fn store_sidecar(app: &mut PhotocraftApp, raw: &RawOpen, params: &CameraRaw) -> Stored {
|
||||
if app.automation_input || app.session.authorize.is_some() {
|
||||
log::info!("{}: sidecar not written for an automation request", raw.name);
|
||||
return;
|
||||
return Stored::Skipped;
|
||||
}
|
||||
let Some(path) = raw.path.as_deref().filter(|p| !is_dng(p)) else { return };
|
||||
let Some(path) = raw.path.as_deref().filter(|p| !is_dng(p)) else { return Stored::Skipped };
|
||||
let sidecar = sidecar_path(path);
|
||||
let name = crate::file_open::display_name(&sidecar);
|
||||
match existing_sidecar_is_ours(&sidecar) {
|
||||
Some(false) => {
|
||||
let kept = crate::i18n::fmt(tl!("Kept {name}: written by another application"), &[("name", &name)]);
|
||||
crate::notices::post(app, kept, Vec::new(), false, None);
|
||||
return;
|
||||
return Stored::Skipped;
|
||||
}
|
||||
None if params.is_identity() => return,
|
||||
None if params.is_identity() => return Stored::Defaults,
|
||||
_ => {}
|
||||
}
|
||||
let bytes = photocraft_io::camera_raw_xmp::save(params).into_bytes();
|
||||
let Some(write) = app.services.write.as_mut() else { return };
|
||||
let Some(write) = app.services.write.as_mut() else { return Stored::Skipped };
|
||||
if let Err(e) = write(&sidecar, &bytes) {
|
||||
crate::notices::error(app, crate::i18n::fmt(tl!("Could not write {name}: {error}"), &[("name", &name), ("error", &e)]));
|
||||
return Stored::Failed;
|
||||
}
|
||||
Stored::Written(name)
|
||||
}
|
||||
|
||||
/// `None` when there is no sidecar at `sidecar`, else whether PhotoCraft wrote it. One that
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
use super::*;
|
||||
|
||||
/// The first raw waiting for the open-time dialog, taken off its filmstrip.
|
||||
fn take_queued(app: &mut PhotocraftApp) -> Option<RawOpen> {
|
||||
(!app.raw_shelf.is_empty()).then(|| app.raw_shelf.remove(0).raw)
|
||||
}
|
||||
|
||||
/// #9550: a Color Mixer Hue track runs through the neighbouring bands' hues (Reds: magenta to
|
||||
/// orange, wrapping round), not from grey; Saturation and Luminance keep grey to the colour.
|
||||
#[test]
|
||||
@@ -358,7 +363,7 @@ fn raw_opens_in_camera_raw_and_open_redevelops() {
|
||||
// A raw has no embedded profile (ProPhoto RGB is the space it is developed into), so Ask
|
||||
// When Opening (on by default) has nothing to ask about.
|
||||
assert!(app.ui.dialogs.is_empty(), "no profile prompt for a raw");
|
||||
let raw = app.pending_raw_open.take().expect("a raw open queues the dialog");
|
||||
let raw = take_queued(&mut app).expect("a raw open queues the dialog");
|
||||
open_raw(&mut app, &ctx, raw).unwrap();
|
||||
let d = app.camera_raw.as_ref().unwrap();
|
||||
assert_eq!(d.target, Target::OpenRaw);
|
||||
@@ -388,7 +393,7 @@ fn the_demosaic_method_is_chosen_at_open() {
|
||||
let ctx = egui::Context::default();
|
||||
app.open_bytes("IMG_0002.dng", &dng).unwrap();
|
||||
let ahd = pixels(&app);
|
||||
let raw = app.pending_raw_open.take().unwrap();
|
||||
let raw = take_queued(&mut app).unwrap();
|
||||
open_raw(&mut app, &ctx, raw).unwrap();
|
||||
let d = menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"demosaic": "bilinear"}})).unwrap().unwrap();
|
||||
assert_eq!(d["openingRaw"]["demosaic"], "bilinear");
|
||||
@@ -405,7 +410,7 @@ fn the_demosaic_method_is_chosen_at_open() {
|
||||
app.run("prefs.set", json!({"path": "rawDefaults.demosaic", "value": "bilinear"})).unwrap();
|
||||
app.open_bytes("IMG_0003.dng", &dng).unwrap();
|
||||
assert_eq!(pixels(&app), bilinear, "developed with the preference's method");
|
||||
let raw = app.pending_raw_open.take().unwrap();
|
||||
let raw = take_queued(&mut app).unwrap();
|
||||
open_raw(&mut app, &ctx, raw).unwrap();
|
||||
let d = app.camera_raw.as_ref().unwrap().describe(&app.ui.camera_raw_scope, &app.ui.camera_raw_preview);
|
||||
assert_eq!(d["openingRaw"]["demosaic"], "bilinear");
|
||||
@@ -419,11 +424,11 @@ fn the_demosaic_method_is_chosen_at_open() {
|
||||
// The preference may change while a background open runs: the dialog starts from the
|
||||
// method the open developed with (its note), not from the newer preference.
|
||||
let warnings = app.open_bytes("IMG_0004.dng", &dng).unwrap();
|
||||
app.pending_raw_open.take();
|
||||
take_queued(&mut app);
|
||||
app.run("prefs.set", json!({"path": "rawDefaults.demosaic", "value": "ahd"})).unwrap();
|
||||
let document = app.session.active().unwrap().doc.id;
|
||||
queue_open_dialog(&mut app, document, "IMG_0004.dng", None, Some(&dng), &warnings).unwrap();
|
||||
let raw = app.pending_raw_open.take().unwrap();
|
||||
let raw = take_queued(&mut app).unwrap();
|
||||
use photocraft_io::raw::Demosaic;
|
||||
assert_eq!((raw.developed_with, raw.demosaic), (Demosaic::Bilinear, Demosaic::Bilinear));
|
||||
}
|
||||
@@ -433,7 +438,7 @@ fn shadows_of_a_raw_open_act_on_the_raw_data_and_the_preview_shows_it() {
|
||||
let (mut app, dng) = raw_app();
|
||||
let ctx = egui::Context::default();
|
||||
app.open_bytes("IMG_0002.dng", &dng).unwrap();
|
||||
let raw = app.pending_raw_open.take().unwrap();
|
||||
let raw = take_queued(&mut app).unwrap();
|
||||
open_raw(&mut app, &ctx, raw).unwrap();
|
||||
assert!(app.camera_raw.as_ref().unwrap().raw_curve.is_some(), "the preview knows how the raw was developed");
|
||||
let mean = |px: &[[f32; 4]]| px.iter().map(|p| p[0] + p[1] + p[2]).sum::<f32>() / px.len() as f32;
|
||||
@@ -454,7 +459,7 @@ fn clarity_texture_and_dehaze_of_a_raw_open_act_on_the_raw_data() {
|
||||
let (mut app, dng) = raw_app();
|
||||
let ctx = egui::Context::default();
|
||||
app.open_bytes("IMG_0003.dng", &dng).unwrap();
|
||||
let raw = app.pending_raw_open.take().unwrap();
|
||||
let raw = take_queued(&mut app).unwrap();
|
||||
open_raw(&mut app, &ctx, raw).unwrap();
|
||||
let before = mean_red(&app);
|
||||
let preview_before = app.camera_raw.as_ref().unwrap().processed.clone();
|
||||
@@ -476,7 +481,7 @@ fn background_redevelop_keeps_the_tab_position_and_view() {
|
||||
app.open_bytes("mid.dng", &dng).unwrap();
|
||||
app.run("file.new", json!({"width": 12, "height": 12})).unwrap();
|
||||
app.sync_views();
|
||||
let raw = app.pending_raw_open.take().unwrap();
|
||||
let raw = take_queued(&mut app).unwrap();
|
||||
let old_id = raw.document;
|
||||
assert_eq!(document_index(&app, old_id), Some(1));
|
||||
app.ui.views[1].zoom = 3.0;
|
||||
@@ -516,7 +521,7 @@ fn a_raw_opens_and_redevelops_at_the_raw_defaults_resolution() {
|
||||
app.run("prefs.set", json!({"path": "rawDefaults.resolution", "value": 240})).unwrap();
|
||||
app.open_bytes("IMG_0004.dng", &dng).unwrap();
|
||||
assert_eq!(app.session.active().unwrap().doc.resolution_dpi, 240.0);
|
||||
let raw = app.pending_raw_open.take().unwrap();
|
||||
let raw = take_queued(&mut app).unwrap();
|
||||
open_raw(&mut app, &ctx, raw).unwrap();
|
||||
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"set": {"exposure": 1.0}}})).unwrap().unwrap();
|
||||
let r = menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"commit": true}})).unwrap().unwrap();
|
||||
@@ -526,19 +531,201 @@ fn a_raw_opens_and_redevelops_at_the_raw_defaults_resolution() {
|
||||
assert_eq!(app.session.active().unwrap().doc.resolution_dpi, 240.0, "automation open");
|
||||
}
|
||||
|
||||
/// Raws opened together share the dialog in a filmstrip, each with its own settings; Open
|
||||
/// develops them all, Cancel closes them all.
|
||||
#[test]
|
||||
fn several_raws_share_the_dialog_and_keep_their_own_settings() {
|
||||
let (mut app, dng) = raw_app();
|
||||
let ctx = egui::Context::default();
|
||||
app.open_bytes("one.dng", &dng).unwrap();
|
||||
app.open_bytes("two.dng", &dng).unwrap();
|
||||
app.open_bytes("three.dng", &dng).unwrap();
|
||||
assert_eq!(app.raw_shelf.len(), 3);
|
||||
assert!(app.raw_shelf.iter().all(|h| h.thumb.is_some()), "each has a thumbnail");
|
||||
let ids: Vec<_> = app.raw_shelf.iter().map(|h| h.raw.document).collect();
|
||||
filmstrip::open_held(&mut app, &ctx, 0).unwrap();
|
||||
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"set": {"exposure": 1.0}}})).unwrap().unwrap();
|
||||
app.camera_raw.as_mut().unwrap().render(&ctx);
|
||||
filmstrip::show_raw(&mut app, &ctx, 1).unwrap();
|
||||
let d = app.camera_raw.as_ref().unwrap();
|
||||
assert_eq!((d.raw_open.as_ref().unwrap().document, d.layer_name.as_str(), d.params.exposure), (ids[1], "two.dng", 0.0));
|
||||
assert_eq!(app.raw_shelf[0].kept.as_ref().map(|(p, _)| p.exposure), Some(1.0), "the first keeps its settings");
|
||||
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"set": {"contrast": 30}}})).unwrap().unwrap();
|
||||
filmstrip::show_raw(&mut app, &ctx, 0).unwrap();
|
||||
assert_eq!(app.camera_raw.as_ref().unwrap().params.exposure, 1.0, "back to the first, as it was left");
|
||||
let r = menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"commit": true}})).unwrap().unwrap();
|
||||
assert_eq!(r["opened"].as_array().map(Vec::len), Some(3), "{r}");
|
||||
assert!(app.raw_shelf.is_empty() && app.camera_raw.is_none());
|
||||
assert_eq!(app.session.documents().len(), 3, "the never-shown third opens too");
|
||||
assert!(app.session.documents().iter().all(|st| st.saved_revision == st.revision), "Open leaves them unmodified");
|
||||
// Cancel closes every raw in the filmstrip.
|
||||
app.open_bytes("four.dng", &dng).unwrap();
|
||||
app.open_bytes("five.dng", &dng).unwrap();
|
||||
filmstrip::open_held(&mut app, &ctx, 0).unwrap();
|
||||
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"cancel": true}})).unwrap().unwrap();
|
||||
assert!(app.raw_shelf.is_empty());
|
||||
assert_eq!(app.session.documents().len(), 3);
|
||||
}
|
||||
|
||||
/// A raw whose Open fails stays in the filmstrip with its settings, and the dialog comes back
|
||||
/// for it; the raws that opened leave it.
|
||||
#[test]
|
||||
fn a_raw_that_fails_to_open_stays_in_the_filmstrip_with_its_settings() {
|
||||
let (mut app, dng) = raw_app();
|
||||
let ctx = egui::Context::default();
|
||||
app.open_bytes("one.dng", &dng).unwrap();
|
||||
app.open_bytes("two.dng", &dng).unwrap();
|
||||
app.open_bytes("three.dng", &dng).unwrap();
|
||||
let ids: Vec<_> = app.raw_shelf.iter().map(|h| h.raw.document).collect();
|
||||
// Two's file can no longer be developed, and it was left with its own settings.
|
||||
let mut kept = CameraRaw { exposure: 0.5, ..Default::default() };
|
||||
kept.contrast = 20.0;
|
||||
app.raw_shelf[1].raw.bytes = Some(std::sync::Arc::new(b"not a raw file".to_vec()));
|
||||
app.raw_shelf[1].kept = Some((kept.clone(), CameraRaw::default()));
|
||||
filmstrip::open_held(&mut app, &ctx, 0).unwrap();
|
||||
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"set": {"exposure": 1.0}}})).unwrap().unwrap();
|
||||
let r = menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"commit": true}})).unwrap().unwrap();
|
||||
assert_eq!(r["opened"].as_array().map(Vec::len), Some(2), "one and three open: {r}");
|
||||
let left: Vec<_> = app.raw_shelf.iter().map(|h| h.raw.document).collect();
|
||||
assert_eq!(left, vec![ids[1]], "only the failed raw stays in the filmstrip");
|
||||
assert_eq!(app.raw_shelf[0].kept.as_ref().map(|(p, _)| p), Some(&kept), "with its settings");
|
||||
// The dialog comes back for it with them; when every raw fails, the dialog and its
|
||||
// filmstrip stay.
|
||||
filmstrip::open_held(&mut app, &ctx, 0).unwrap();
|
||||
let shown = &app.camera_raw.as_ref().unwrap().params;
|
||||
assert_eq!((shown.exposure, shown.contrast), (0.5, 20.0));
|
||||
app.open_bytes("four.dng", &dng).unwrap();
|
||||
app.raw_shelf[1].raw.bytes = Some(std::sync::Arc::new(b"not a raw file either".to_vec()));
|
||||
app.raw_shelf[1].kept = Some((kept.clone(), CameraRaw::default()));
|
||||
assert!(menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"commit": true}})).unwrap().is_err());
|
||||
assert_eq!(app.raw_shelf.len(), 2, "both raws stay in the filmstrip");
|
||||
assert!(app.camera_raw.is_some(), "and the dialog stays open");
|
||||
// Cancel still closes every raw in it.
|
||||
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"cancel": true}})).unwrap().unwrap();
|
||||
assert!(app.raw_shelf.is_empty());
|
||||
assert_eq!(app.session.documents().len(), 2, "one and three stay open");
|
||||
}
|
||||
|
||||
/// A raw that can't be shown leaves things as they were: the dialog stays on the raw being left,
|
||||
/// and the other keeps its place in the filmstrip with its settings.
|
||||
#[test]
|
||||
fn a_raw_that_cannot_be_shown_keeps_the_dialog_and_the_filmstrip() {
|
||||
let (mut app, dng) = raw_app();
|
||||
let ctx = egui::Context::default();
|
||||
app.open_bytes("one.dng", &dng).unwrap();
|
||||
app.open_bytes("two.dng", &dng).unwrap();
|
||||
let ids: Vec<_> = app.raw_shelf.iter().map(|h| h.raw.document).collect();
|
||||
let kept = CameraRaw { exposure: 0.5, ..Default::default() };
|
||||
app.raw_shelf[1].kept = Some((kept.clone(), CameraRaw::default()));
|
||||
filmstrip::open_held(&mut app, &ctx, 0).unwrap();
|
||||
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"set": {"exposure": 1.0}}})).unwrap().unwrap();
|
||||
let two = document_index(&app, ids[1]).unwrap();
|
||||
app.session.close(two);
|
||||
assert!(filmstrip::show_raw(&mut app, &ctx, 1).is_err());
|
||||
let d = app.camera_raw.as_ref().expect("the dialog stays");
|
||||
assert_eq!((d.raw_open.as_ref().unwrap().document, d.params.exposure), (ids[0], 1.0));
|
||||
assert_eq!(app.raw_shelf.len(), 2);
|
||||
assert_eq!(app.raw_shelf[1].kept.as_ref().map(|(p, _)| p), Some(&kept));
|
||||
}
|
||||
|
||||
/// Leaving a raw before its preview has caught up: its thumbnail shows the settings it keeps, not
|
||||
/// the frame before them.
|
||||
#[test]
|
||||
fn a_raw_left_before_its_preview_lands_gets_a_thumbnail_of_its_settings() {
|
||||
let (mut app, dng) = raw_app();
|
||||
let ctx = egui::Context::default();
|
||||
app.open_bytes("one.dng", &dng).unwrap();
|
||||
app.open_bytes("two.dng", &dng).unwrap();
|
||||
filmstrip::open_held(&mut app, &ctx, 0).unwrap();
|
||||
let d = app.camera_raw.as_mut().unwrap();
|
||||
d.params.exposure = 2.0;
|
||||
d.dirty = true;
|
||||
filmstrip::keep_settings(&mut app);
|
||||
let thumb = app.raw_shelf[0].thumb.clone().expect("a thumbnail");
|
||||
app.camera_raw.as_mut().unwrap().render(&ctx);
|
||||
filmstrip::keep_settings(&mut app);
|
||||
assert!(thumb == app.raw_shelf[0].thumb.clone().unwrap(), "the thumbnail matches a finished develop of the same settings");
|
||||
}
|
||||
|
||||
/// Open returns once a background develop has started; if that develop then fails, the raw comes
|
||||
/// back to the filmstrip with its settings, as one that failed at once stays there.
|
||||
#[test]
|
||||
fn a_raw_whose_background_develop_fails_comes_back_to_the_filmstrip() {
|
||||
let (mut app, dng) = raw_app();
|
||||
let ctx = egui::Context::default();
|
||||
app.open_bytes("one.dng", &dng).unwrap();
|
||||
app.open_bytes("two.dng", &dng).unwrap();
|
||||
let ids: Vec<_> = app.raw_shelf.iter().map(|h| h.raw.document).collect();
|
||||
let (kept, from) = (CameraRaw { exposure: 0.5, contrast: 20.0, ..Default::default() }, CameraRaw { contrast: 5.0, ..Default::default() });
|
||||
app.raw_shelf[1].raw.bytes = Some(std::sync::Arc::new(b"not a raw file".to_vec()));
|
||||
app.raw_shelf[1].kept = Some((kept.clone(), from.clone()));
|
||||
filmstrip::open_held(&mut app, &ctx, 0).unwrap();
|
||||
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"set": {"exposure": 1.0}}})).unwrap().unwrap();
|
||||
app.background_jobs = true;
|
||||
let r = menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"commit": true}})).unwrap().unwrap();
|
||||
assert_eq!(r["opened"].as_array().map(Vec::len), Some(2), "both started: {r}");
|
||||
assert!(app.raw_shelf.is_empty());
|
||||
for _ in 0..3000 {
|
||||
crate::jobs_ui::tick(&mut app, &ctx);
|
||||
if app.raw_redevelop.is_empty() && !app.session.has_jobs() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
assert!(app.raw_redevelop.is_empty() && !app.session.has_jobs(), "the jobs never finished");
|
||||
let left: Vec<_> = app.raw_shelf.iter().map(|h| h.raw.document).collect();
|
||||
assert_eq!(left, vec![ids[1]], "only the failed raw comes back");
|
||||
assert_eq!(app.raw_shelf[0].kept, Some((kept, from)), "with its settings");
|
||||
app.background_jobs = false;
|
||||
filmstrip::open_held(&mut app, &ctx, 0).unwrap();
|
||||
assert_eq!(app.camera_raw.as_ref().unwrap().params.contrast, 20.0);
|
||||
}
|
||||
|
||||
/// Camera Raw Defaults' Auto reads the same pixels whether a raw was shown or not, so Open gives
|
||||
/// it the same settings either way: a fine pattern that a sparse sample misreads shows it.
|
||||
#[test]
|
||||
fn auto_tone_of_a_never_shown_raw_matches_the_dialogs() {
|
||||
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), Default::default());
|
||||
let ctx = egui::Context::default();
|
||||
app.run("prefs.set", json!({"path": "rawDefaults.applyAutoTone", "value": true})).unwrap();
|
||||
app.run("file.new", json!({"width": 1800, "height": 1200})).unwrap();
|
||||
app.run("layer.new.layer", json!({})).unwrap();
|
||||
let st = app.session.active_mut().unwrap();
|
||||
let id = st.doc.layers.iter().find(|l| l.surface().is_some()).unwrap().id;
|
||||
st.active_layer = Some(id);
|
||||
let document = st.doc.id;
|
||||
let Some(photocraft_doc::LayerContent::Raster(s)) = std::sync::Arc::make_mut(&mut st.doc).layer_mut(id).map(|l| &mut l.content) else {
|
||||
panic!("a pixel layer")
|
||||
};
|
||||
let format = s.format();
|
||||
let (dark, light) = (photocraft_raster::from_rgba(&format, [0.1, 0.1, 0.1, 1.0]), photocraft_raster::from_rgba(&format, [0.9, 0.9, 0.9, 1.0]));
|
||||
for y in 0..1200 {
|
||||
for x in 0..1800 {
|
||||
s.write_pixel(x, y, if (x + y) % 2 == 0 { &dark } else { &light });
|
||||
}
|
||||
}
|
||||
let method = photocraft_io::raw::Demosaic::Ahd;
|
||||
let raw = RawOpen { document, name: "pattern.dng".into(), path: None, bytes: None, developed_with: method, demosaic: method };
|
||||
let (unshown, _) = filmstrip::first_settings(&app, &raw);
|
||||
open_raw(&mut app, &ctx, raw).unwrap();
|
||||
let shown = &app.camera_raw.as_ref().unwrap().params;
|
||||
let tone = |p: &CameraRaw| [p.exposure, p.contrast, p.highlights, p.shadows, p.whites, p.blacks];
|
||||
assert_eq!(tone(&unshown), tone(shown));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancel_closes_the_raw_and_the_preference_skips_the_dialog() {
|
||||
let (mut app, dng) = raw_app();
|
||||
let ctx = egui::Context::default();
|
||||
app.open_bytes("a.dng", &dng).unwrap();
|
||||
let raw = app.pending_raw_open.take().unwrap();
|
||||
let raw = take_queued(&mut app).unwrap();
|
||||
open_raw(&mut app, &ctx, raw).unwrap();
|
||||
let r = menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"cancel": true}})).unwrap().unwrap();
|
||||
assert_eq!(r["cancelled"], true);
|
||||
assert!(app.session.documents().is_empty(), "Cancel closes the raw being opened");
|
||||
// Unchanged settings: Open keeps the default develop and adds no step.
|
||||
app.open_bytes("b.dng", &dng).unwrap();
|
||||
let raw = app.pending_raw_open.take().unwrap();
|
||||
let raw = take_queued(&mut app).unwrap();
|
||||
open_raw(&mut app, &ctx, raw).unwrap();
|
||||
let r = menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"commit": true}})).unwrap().unwrap();
|
||||
assert_eq!((r["document"].as_u64(), r["redeveloped"].as_bool(), r.get("filter")), (Some(0), Some(false), None), "{r}");
|
||||
@@ -546,7 +733,7 @@ fn cancel_closes_the_raw_and_the_preference_skips_the_dialog() {
|
||||
// Preference off: raws open straight away.
|
||||
app.run("prefs.set", json!({"path": "rawDefaults.openInCameraRaw", "value": false})).unwrap();
|
||||
app.open_bytes("c.dng", &dng).unwrap();
|
||||
assert!(app.pending_raw_open.is_none());
|
||||
assert!(app.raw_shelf.is_empty());
|
||||
// A non-raw never queues the dialog; a closed document is an error, not a panic.
|
||||
assert!(
|
||||
open_raw(
|
||||
@@ -570,7 +757,7 @@ fn calibration_changes_the_preview_and_round_trips_through_xmp() {
|
||||
let (mut app, dng) = raw_app();
|
||||
let ctx = egui::Context::default();
|
||||
app.open_bytes("IMG_0005.dng", &dng).unwrap();
|
||||
let raw = app.pending_raw_open.take().unwrap();
|
||||
let raw = take_queued(&mut app).unwrap();
|
||||
open_raw(&mut app, &ctx, raw).unwrap();
|
||||
let before = app.camera_raw.as_ref().unwrap().processed.clone();
|
||||
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"set": {"shadowTint": 100, "calibrationHue": [100, 0, 0], "calibrationSat": [0, 0, -100]}}}))
|
||||
@@ -595,7 +782,7 @@ fn auto_sets_the_light_sliders_from_the_image() {
|
||||
let (mut app, dng) = raw_app();
|
||||
let ctx = egui::Context::default();
|
||||
app.open_bytes("IMG_0006.dng", &dng).unwrap();
|
||||
let raw = app.pending_raw_open.take().unwrap();
|
||||
let raw = take_queued(&mut app).unwrap();
|
||||
open_raw(&mut app, &ctx, raw).unwrap();
|
||||
assert!(app.camera_raw.as_ref().unwrap().params.is_identity());
|
||||
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"auto": true}})).unwrap().unwrap();
|
||||
@@ -605,7 +792,7 @@ fn auto_sets_the_light_sliders_from_the_image() {
|
||||
// The preference does the same when a raw opens without a sidecar.
|
||||
app.run("prefs.set", json!({"path": "rawDefaults.applyAutoTone", "value": true})).unwrap();
|
||||
app.open_bytes("IMG_0007.dng", &dng).unwrap();
|
||||
let raw = app.pending_raw_open.take().unwrap();
|
||||
let raw = take_queued(&mut app).unwrap();
|
||||
open_raw(&mut app, &ctx, raw).unwrap();
|
||||
assert_eq!(app.camera_raw.as_ref().unwrap().params, p, "Apply auto tone opens the dialog with Auto's settings");
|
||||
}
|
||||
@@ -635,7 +822,7 @@ fn a_sidecar_fills_the_dialog_and_open_writes_it_back() {
|
||||
}));
|
||||
let ctx = egui::Context::default();
|
||||
app.open_bytes("IMG_0008.cr2", &dng).unwrap();
|
||||
let mut raw = app.pending_raw_open.take().unwrap();
|
||||
let mut raw = take_queued(&mut app).unwrap();
|
||||
raw.path = Some(raw_path.clone());
|
||||
open_raw(&mut app, &ctx, raw).unwrap();
|
||||
let d = app.camera_raw.as_ref().unwrap();
|
||||
@@ -657,14 +844,14 @@ fn a_sidecar_fills_the_dialog_and_open_writes_it_back() {
|
||||
let dng_path = dir.join("IMG_0009.dng").to_string_lossy().into_owned();
|
||||
std::fs::write(&dng_path, &dng).unwrap();
|
||||
app.open_bytes("IMG_0009.dng", &dng).unwrap();
|
||||
let mut raw = app.pending_raw_open.take().unwrap();
|
||||
let mut raw = take_queued(&mut app).unwrap();
|
||||
raw.path = Some(dng_path);
|
||||
open_raw(&mut app, &ctx, raw).unwrap();
|
||||
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"set": {"contrast": 15}, "commit": true}})).unwrap().unwrap();
|
||||
assert!(written.lock().unwrap().is_empty(), "no sidecar for a DNG");
|
||||
app.run("prefs.set", json!({"path": "rawDefaults.writeXmpSidecars", "value": false})).unwrap();
|
||||
app.open_bytes("IMG_0008.cr2", &dng).unwrap();
|
||||
let mut raw = app.pending_raw_open.take().unwrap();
|
||||
let mut raw = take_queued(&mut app).unwrap();
|
||||
raw.path = Some(raw_path.clone());
|
||||
open_raw(&mut app, &ctx, raw).unwrap();
|
||||
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"set": {"contrast": 30}, "commit": true}})).unwrap().unwrap();
|
||||
@@ -673,7 +860,7 @@ fn a_sidecar_fills_the_dialog_and_open_writes_it_back() {
|
||||
app.run("prefs.set", json!({"path": "rawDefaults.openInCameraRaw", "value": false})).unwrap();
|
||||
let before = app.session.documents().len();
|
||||
app.open_file(&raw_path, &dng).unwrap();
|
||||
assert!(app.pending_raw_open.is_none());
|
||||
assert!(app.raw_shelf.is_empty());
|
||||
assert_eq!(app.session.documents().len(), before + 1);
|
||||
let st = app.session.active().unwrap();
|
||||
assert_eq!(st.saved_revision, st.revision, "opening with a sidecar leaves the document unmodified");
|
||||
@@ -686,7 +873,7 @@ fn camera_raw_defaults_apply_when_a_raw_opens() {
|
||||
let (mut app, dng) = raw_app();
|
||||
app.run("prefs.set", json!({"values": {"rawDefaults.openInCameraRaw": false, "rawDefaults.colorSpace": "srgb", "rawDefaults.bitDepth": "8", "rawDefaults.resolution": 240.0, "rawDefaults.sharpenFor": "screen", "rawDefaults.openAsSmartObject": true}})).unwrap();
|
||||
app.open_bytes("IMG_0010.dng", &dng).unwrap();
|
||||
assert!(app.pending_raw_open.is_none());
|
||||
assert!(app.raw_shelf.is_empty());
|
||||
let st = app.session.active().unwrap();
|
||||
assert_eq!(st.doc.depth, photocraft_color::SampleType::U8, "Bit depth 8");
|
||||
assert_eq!(st.doc.resolution_dpi, 240.0, "Resolution");
|
||||
@@ -700,7 +887,7 @@ fn camera_raw_defaults_apply_when_a_raw_opens() {
|
||||
.unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
app.open_bytes("IMG_0011.dng", &dng).unwrap();
|
||||
let raw = app.pending_raw_open.take().unwrap();
|
||||
let raw = take_queued(&mut app).unwrap();
|
||||
open_raw(&mut app, &ctx, raw).unwrap();
|
||||
let r = menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"commit": true}})).unwrap().unwrap();
|
||||
assert_eq!(r["defaults"], json!(["sharpenFor", "colorSpace"]), "{r}");
|
||||
@@ -756,7 +943,7 @@ fn raw_with_sidecar(tag: &str, sidecar: Option<&str>) -> (PhotocraftApp, Vec<u8>
|
||||
/// Opens the raw at `raw_path` in the open-time dialog, as File › Open does.
|
||||
fn open_dialog_at(app: &mut PhotocraftApp, ctx: &egui::Context, dng: &[u8], raw_path: &str) {
|
||||
app.open_bytes("IMG_0100.cr2", dng).unwrap();
|
||||
let mut raw = app.pending_raw_open.take().unwrap();
|
||||
let mut raw = take_queued(app).unwrap();
|
||||
raw.path = Some(raw_path.to_string());
|
||||
open_raw(app, ctx, raw).unwrap();
|
||||
}
|
||||
@@ -808,7 +995,7 @@ fn opening_without_the_dialog_does_not_rewrite_the_sidecar() {
|
||||
let (mut app, dng, dir, raw_path, written) = raw_with_sidecar("nodialog", Some(LIGHTROOM_SIDECAR));
|
||||
app.run("prefs.set", json!({"path": "rawDefaults.openInCameraRaw", "value": false})).unwrap();
|
||||
app.open_file(&raw_path, &dng).unwrap();
|
||||
assert!(app.pending_raw_open.is_none());
|
||||
assert!(app.raw_shelf.is_empty());
|
||||
assert_eq!(app.session.documents().len(), 1);
|
||||
assert!(written.lock().unwrap().is_empty(), "just opening never writes the sidecar");
|
||||
assert_eq!(std::fs::read_to_string(sidecar_path(&raw_path)).unwrap(), LIGHTROOM_SIDECAR);
|
||||
@@ -855,7 +1042,7 @@ fn a_sidecar_write_error_stays_reported() {
|
||||
|
||||
fn open_with_settings(app: &mut PhotocraftApp, ctx: &egui::Context, dng: &[u8], name: &str) {
|
||||
app.open_bytes(name, dng).unwrap();
|
||||
let raw = app.pending_raw_open.take().unwrap();
|
||||
let raw = take_queued(app).unwrap();
|
||||
open_raw(app, ctx, raw).unwrap();
|
||||
menu(app, ctx, "filter.cameraRaw", &json!({"ui": {"set": {"exposure": 0.5, "contrast": 60, "saturation": 40, "vibrance": 30}}})).unwrap().unwrap();
|
||||
}
|
||||
@@ -1022,3 +1209,60 @@ fn save_and_load_settings_round_trip_through_xmp_files() {
|
||||
frame(&mut app);
|
||||
assert_eq!(app.camera_raw.as_ref().unwrap().params.clarity, 25.0);
|
||||
}
|
||||
|
||||
/// Done keeps the settings in the raw's sidecar and opens nothing,
|
||||
/// even with the preference that writes sidecars on Open turned off.
|
||||
#[test]
|
||||
fn done_saves_the_sidecar_and_opens_nothing() {
|
||||
let (mut app, dng, dir, raw_path, written) = raw_with_sidecar("done", None);
|
||||
let ctx = egui::Context::default();
|
||||
app.run("prefs.set", json!({"path": "rawDefaults.writeXmpSidecars", "value": false})).unwrap();
|
||||
let before = app.session.documents().len();
|
||||
open_dialog_at(&mut app, &ctx, &dng, &raw_path);
|
||||
let set = json!({"contrast": 20, "clarity": 15});
|
||||
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"set": set}})).unwrap().unwrap();
|
||||
let r = menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"done": true}})).unwrap().unwrap();
|
||||
assert_eq!(r["saved"], json!(["IMG_0100.xmp"]), "{r}");
|
||||
assert!(app.camera_raw.is_none() && app.raw_shelf.is_empty());
|
||||
assert_eq!(app.session.documents().len(), before, "Done opens nothing");
|
||||
let w = written.lock().unwrap().clone();
|
||||
assert_eq!(w.len(), 1);
|
||||
let mut back = CameraRaw::default();
|
||||
photocraft_io::camera_raw_xmp::load(&mut back, &w[0].1).unwrap();
|
||||
assert_eq!((back.contrast, back.clarity), (20.0, 15.0));
|
||||
// Done is only for raws being opened, and checked before anything else in the request
|
||||
// changes the dialog.
|
||||
app.run("file.new", json!({"width": 8, "height": 8})).unwrap();
|
||||
assert!(menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"done": true}})).unwrap().is_err());
|
||||
menu(&mut app, &ctx, "filter.cameraRaw", &json!({})).unwrap().unwrap();
|
||||
let before = app.camera_raw.as_ref().unwrap().params.clone();
|
||||
assert!(menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"set": {"contrast": 40}, "done": true}})).unwrap().is_err());
|
||||
assert_eq!(app.camera_raw.as_ref().unwrap().params, before, "a refused Done changes nothing");
|
||||
for other in ["commit", "cancel"] {
|
||||
assert!(menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"done": true, other: true}})).unwrap().is_err(), "{other}");
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// A sidecar that can't be written keeps the dialog and the raw open with their settings;
|
||||
/// default settings are nothing to save, not an error.
|
||||
#[test]
|
||||
fn done_keeps_the_raws_open_when_a_sidecar_write_fails() {
|
||||
let (mut app, dng, dir, raw_path, _) = raw_with_sidecar("done-fails", None);
|
||||
let ctx = egui::Context::default();
|
||||
app.services.write = Some(Box::new(|_: &str, _: &[u8]| Err("read-only volume".to_string())));
|
||||
open_dialog_at(&mut app, &ctx, &dng, &raw_path);
|
||||
let docs = app.session.documents().len();
|
||||
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"set": {"contrast": 20}}})).unwrap().unwrap();
|
||||
let r = menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"done": true}})).unwrap().unwrap();
|
||||
assert_eq!(r["done"], false, "{r}");
|
||||
assert_eq!(app.camera_raw.as_ref().map(|d| d.params.contrast), Some(20.0), "the edit is still there");
|
||||
assert_eq!(app.session.documents().len(), docs, "the raw stays open");
|
||||
assert!(app.ui.status_error);
|
||||
// At the defaults there is nothing to write: Done closes, without an error.
|
||||
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"set": {"contrast": 0}}})).unwrap().unwrap();
|
||||
let r = menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"done": true}})).unwrap().unwrap();
|
||||
assert_eq!((r["done"].clone(), r["saved"].clone()), (json!(true), json!([])), "{r}");
|
||||
assert!(app.camera_raw.is_none() && !app.ui.status_error, "{}", app.ui.status);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
@@ -4356,6 +4356,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
|
||||
Show layer bounds on hover Zobrazit hranice vrstvy při najetí
|
||||
Expand layer group on click Rozbalit skupinu vrstev kliknutím
|
||||
Show hover bounds from Layers Zobrazit hranice při najetí z panelu Vrstvy
|
||||
Saved Camera Raw settings to {file} Nastavení Camera Raw uložena do {file}
|
||||
Save the settings to each raw's sidecar without opening it Uložit nastavení do souboru sidecar každého snímku raw bez jeho otevření
|
||||
No settings saved: these raws can't keep a sidecar here Nic neuloženo: tyto snímky raw zde nemohou mít soubor sidecar
|
||||
Some settings could not be saved; the raws stay open Některá nastavení nelze uložit; snímky raw zůstávají otevřené
|
||||
Nothing to save: these raws are at their default settings Není co ukládat: tyto snímky raw mají výchozí nastavení
|
||||
# Artboard tool options bar (#11385)
|
||||
Distribute Widths Rozmístit šířky
|
||||
Distribute Heights Rozmístit výšky
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -3651,6 +3651,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
|
||||
Show layer bounds on hover Ebenenbegrenzung beim Darüberfahren anzeigen
|
||||
Expand layer group on click Ebenengruppe per Klick erweitern
|
||||
Show hover bounds from Layers Begrenzung beim Darüberfahren aus Ebenen anzeigen
|
||||
Saved Camera Raw settings to {file} Camera-Raw-Einstellungen in {file} gespeichert
|
||||
Save the settings to each raw's sidecar without opening it Die Einstellungen in der Filialdatei jeder Raw-Datei speichern, ohne sie zu öffnen
|
||||
No settings saved: these raws can't keep a sidecar here Nichts gespeichert: Diese Raw-Dateien können hier keine Filialdatei haben
|
||||
Some settings could not be saved; the raws stay open Einige Einstellungen konnten nicht gespeichert werden; die Raw-Dateien bleiben geöffnet
|
||||
Nothing to save: these raws are at their default settings Nichts zu speichern: Diese Raw-Dateien haben ihre Standardeinstellungen
|
||||
# Artboard tool options bar (#11385)
|
||||
Distribute Widths Breiten verteilen
|
||||
Distribute Heights Höhen verteilen
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -4315,6 +4315,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
|
||||
Show layer bounds on hover Εμφάνιση ορίων στρώσης στο πέρασμα του δείκτη
|
||||
Expand layer group on click Ανάπτυξη ομάδας στρώσεων με κλικ
|
||||
Show hover bounds from Layers Εμφάνιση ορίων στο πέρασμα από τις Στρώσεις
|
||||
Saved Camera Raw settings to {file} Οι ρυθμίσεις Camera Raw αποθηκεύτηκαν στο {file}
|
||||
Save the settings to each raw's sidecar without opening it Αποθήκευση των ρυθμίσεων στο συνοδευτικό αρχείο κάθε raw χωρίς άνοιγμα
|
||||
No settings saved: these raws can't keep a sidecar here Δεν αποθηκεύτηκε τίποτα: αυτά τα raw δεν μπορούν να έχουν συνοδευτικό αρχείο εδώ
|
||||
Some settings could not be saved; the raws stay open Ορισμένες ρυθμίσεις δεν ήταν δυνατό να αποθηκευτούν· τα raw παραμένουν ανοιχτά
|
||||
Nothing to save: these raws are at their default settings Τίποτα για αποθήκευση: αυτά τα raw έχουν τις προεπιλεγμένες ρυθμίσεις
|
||||
# Artboard tool options bar (#11385)
|
||||
Distribute Widths Κατανομή πλατών
|
||||
Distribute Heights Κατανομή υψών
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 47 and column 53.
|
@@ -4515,6 +4515,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
|
||||
Show layer bounds on hover Mostrar límites de capa al pasar el puntero
|
||||
Expand layer group on click Expandir grupo de capas al hacer clic
|
||||
Show hover bounds from Layers Mostrar límites al pasar el puntero desde Capas
|
||||
Saved Camera Raw settings to {file} Ajustes de Camera Raw guardados en {file}
|
||||
Save the settings to each raw's sidecar without opening it Guardar los ajustes en el archivo sidecar de cada raw sin abrirlo
|
||||
No settings saved: these raws can't keep a sidecar here No se guardó nada: estos raw no pueden tener un archivo sidecar aquí
|
||||
Some settings could not be saved; the raws stay open No se pudieron guardar algunos ajustes; los raw siguen abiertos
|
||||
Nothing to save: these raws are at their default settings Nada que guardar: estos raw tienen los ajustes predeterminados
|
||||
# Artboard tool options bar (#11385)
|
||||
Distribute Widths Distribuir anchos
|
||||
Distribute Heights Distribuir alturas
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
@@ -4638,6 +4638,11 @@ shape Appearance Apparence
|
||||
Show layer bounds on hover Afficher les limites du calque au survol
|
||||
Expand layer group on click Développer le groupe de calques au clic
|
||||
Show hover bounds from Layers Afficher les limites au survol depuis Calques
|
||||
Saved Camera Raw settings to {file} Réglages Camera Raw enregistrés dans {file}
|
||||
Save the settings to each raw's sidecar without opening it Enregistrer les réglages dans le fichier annexe de chaque raw sans l’ouvrir
|
||||
No settings saved: these raws can't keep a sidecar here Rien n’a été enregistré : ces raw ne peuvent pas avoir de fichier annexe ici
|
||||
Some settings could not be saved; the raws stay open Certains réglages n’ont pas pu être enregistrés ; les raw restent ouverts
|
||||
Nothing to save: these raws are at their default settings Rien à enregistrer : ces raw ont leurs réglages par défaut
|
||||
# Artboard tool options bar (#11385)
|
||||
Distribute Widths Répartir les largeurs
|
||||
Distribute Heights Répartir les hauteurs
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -4522,6 +4522,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
|
||||
Show layer bounds on hover Réteghatárok megjelenítése rámutatáskor
|
||||
Expand layer group on click Rétegcsoport kibontása kattintásra
|
||||
Show hover bounds from Layers Határok megjelenítése a Rétegek panelről rámutatáskor
|
||||
Saved Camera Raw settings to {file} Camera Raw-beállítások mentve ide: {file}
|
||||
Save the settings to each raw's sidecar without opening it A beállítások mentése minden raw kísérőfájljába megnyitás nélkül
|
||||
No settings saved: these raws can't keep a sidecar here Nincs mentve semmi: ezek a raw fájlok itt nem kaphatnak kísérőfájlt
|
||||
Some settings could not be saved; the raws stay open Néhány beállítást nem sikerült menteni; a raw fájlok nyitva maradnak
|
||||
Nothing to save: these raws are at their default settings Nincs mit menteni: ezek a raw fájlok alapértelmezett beállításokon vannak
|
||||
# Artboard tool options bar (#11385)
|
||||
Distribute Widths Szélességek elosztása
|
||||
Distribute Heights Magasságok elosztása
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -3638,6 +3638,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
|
||||
Show layer bounds on hover Tampilkan batas layer saat kursor di atasnya
|
||||
Expand layer group on click Perluas grup layer saat diklik
|
||||
Show hover bounds from Layers Tampilkan batas saat kursor di atas dari Layer
|
||||
Saved Camera Raw settings to {file} Pengaturan Camera Raw disimpan ke {file}
|
||||
Save the settings to each raw's sidecar without opening it Simpan pengaturan ke file sidecar setiap raw tanpa membukanya
|
||||
No settings saved: these raws can't keep a sidecar here Tidak ada yang disimpan: raw ini tidak dapat memiliki file sidecar di sini
|
||||
Some settings could not be saved; the raws stay open Beberapa pengaturan tidak dapat disimpan; raw tetap terbuka
|
||||
Nothing to save: these raws are at their default settings Tidak ada yang perlu disimpan: raw ini memakai pengaturan default
|
||||
# Artboard tool options bar (#11385)
|
||||
Distribute Widths Distribusi lebar
|
||||
Distribute Heights Distribusi tinggi
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
@@ -4714,6 +4714,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
|
||||
Show layer bounds on hover Mostra i limiti del livello al passaggio del mouse
|
||||
Expand layer group on click Espandi il gruppo di livelli al clic
|
||||
Show hover bounds from Layers Mostra i limiti al passaggio dal pannello Livelli
|
||||
Saved Camera Raw settings to {file} Impostazioni Camera Raw salvate in {file}
|
||||
Save the settings to each raw's sidecar without opening it Salva le impostazioni nel file sidecar di ogni raw senza aprirlo
|
||||
No settings saved: these raws can't keep a sidecar here Nulla salvato: questi raw non possono avere un file sidecar qui
|
||||
Some settings could not be saved; the raws stay open Alcune impostazioni non sono state salvate; i raw restano aperti
|
||||
Nothing to save: these raws are at their default settings Niente da salvare: questi raw hanno le impostazioni predefinite
|
||||
# Artboard tool options bar (#11385)
|
||||
Distribute Widths Distribuisci larghezze
|
||||
Distribute Heights Distribuisci altezze
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 79 and column 53.
|
@@ -4286,6 +4286,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
|
||||
Show layer bounds on hover ホバー時にレイヤーの境界を表示
|
||||
Expand layer group on click クリックでレイヤーグループを展開
|
||||
Show hover bounds from Layers レイヤーパネルからのホバー境界を表示
|
||||
Saved Camera Raw settings to {file} Camera Raw の設定を {file} に保存しました
|
||||
Save the settings to each raw's sidecar without opening it 各 Raw のサイドカーファイルに設定を保存し、開かずに閉じます
|
||||
No settings saved: these raws can't keep a sidecar here 保存されませんでした:これらの Raw にはここでサイドカーファイルを作成できません
|
||||
Some settings could not be saved; the raws stay open 一部の設定を保存できませんでした。Raw は開いたままです
|
||||
Nothing to save: these raws are at their default settings 保存するものはありません:これらの Raw は初期設定のままです
|
||||
# Artboard tool options bar (#11385)
|
||||
Distribute Widths 幅を分布
|
||||
Distribute Heights 高さを分布
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -4188,6 +4188,11 @@ shape Appearance 화면 모양
|
||||
Show layer bounds on hover 마우스를 올리면 레이어 경계 표시
|
||||
Expand layer group on click 클릭하면 레이어 그룹 펼치기
|
||||
Show hover bounds from Layers 레이어 패널에서 마우스를 올릴 때 경계 표시
|
||||
Saved Camera Raw settings to {file} Camera Raw 설정을 {file}에 저장했습니다
|
||||
Save the settings to each raw's sidecar without opening it 각 Raw의 사이드카 파일에 설정을 저장하고 열지 않습니다
|
||||
No settings saved: these raws can't keep a sidecar here 저장된 항목 없음: 이 Raw 파일은 여기에 사이드카 파일을 둘 수 없습니다
|
||||
Some settings could not be saved; the raws stay open 일부 설정을 저장할 수 없습니다. Raw 파일은 열린 상태로 유지됩니다
|
||||
Nothing to save: these raws are at their default settings 저장할 항목 없음: 이 Raw 파일은 기본 설정입니다
|
||||
# Artboard tool options bar (#11385)
|
||||
Distribute Widths 너비 분포
|
||||
Distribute Heights 높이 분포
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 63 and column 53.
|
@@ -4369,6 +4369,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
|
||||
Show layer bounds on hover Laaggrenzen tonen bij aanwijzen
|
||||
Expand layer group on click Laaggroep uitvouwen bij klikken
|
||||
Show hover bounds from Layers Aanwijsgrenzen tonen vanuit Lagen
|
||||
Saved Camera Raw settings to {file} Camera Raw-instellingen opgeslagen in {file}
|
||||
Save the settings to each raw's sidecar without opening it De instellingen in het sidecarbestand van elke raw opslaan zonder die te openen
|
||||
No settings saved: these raws can't keep a sidecar here Niets opgeslagen: deze raws kunnen hier geen sidecarbestand krijgen
|
||||
Some settings could not be saved; the raws stay open Sommige instellingen konden niet worden opgeslagen; de raws blijven open
|
||||
Nothing to save: these raws are at their default settings Niets op te slaan: deze raws staan op hun standaardinstellingen
|
||||
# Artboard tool options bar (#11385)
|
||||
Distribute Widths Breedtes verdelen
|
||||
Distribute Heights Hoogtes verdelen
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 8 and column 34.
|
@@ -4233,6 +4233,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
|
||||
Show layer bounds on hover Pokaż granice warstwy po najechaniu
|
||||
Expand layer group on click Rozwiń grupę warstw po kliknięciu
|
||||
Show hover bounds from Layers Pokaż granice po najechaniu z panelu Warstwy
|
||||
Saved Camera Raw settings to {file} Zapisano ustawienia Camera Raw w {file}
|
||||
Save the settings to each raw's sidecar without opening it Zapisz ustawienia w pliku sidecar każdego pliku raw bez otwierania go
|
||||
No settings saved: these raws can't keep a sidecar here Nic nie zapisano: te pliki raw nie mogą tu mieć pliku sidecar
|
||||
Some settings could not be saved; the raws stay open Nie udało się zapisać niektórych ustawień; pliki raw pozostają otwarte
|
||||
Nothing to save: these raws are at their default settings Nic do zapisania: te pliki raw mają ustawienia domyślne
|
||||
# Artboard tool options bar (#11385)
|
||||
Distribute Widths Rozmieść szerokości równomiernie
|
||||
Distribute Heights Rozmieść wysokości równomiernie
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -4529,6 +4529,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
|
||||
Show layer bounds on hover Mostrar limites da camada ao passar o cursor
|
||||
Expand layer group on click Expandir grupo de camadas ao clicar
|
||||
Show hover bounds from Layers Mostrar limites ao passar o cursor em Camadas
|
||||
Saved Camera Raw settings to {file} Configurações do Camera Raw salvas em {file}
|
||||
Save the settings to each raw's sidecar without opening it Salvar as configurações no arquivo sidecar de cada raw sem abri-lo
|
||||
No settings saved: these raws can't keep a sidecar here Nada foi salvo: estes raw não podem ter um arquivo sidecar aqui
|
||||
Some settings could not be saved; the raws stay open Não foi possível salvar algumas configurações; os raw continuam abertos
|
||||
Nothing to save: these raws are at their default settings Nada para salvar: estes raw estão nas configurações padrão
|
||||
# Artboard tool options bar (#11385)
|
||||
Distribute Widths Distribuir larguras
|
||||
Distribute Heights Distribuir alturas
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -4800,6 +4800,11 @@ shape Appearance Оформление
|
||||
Show layer bounds on hover Показывать границы слоя при наведении
|
||||
Expand layer group on click Раскрывать группу слоёв по щелчку
|
||||
Show hover bounds from Layers Показывать границы при наведении из панели «Слои»
|
||||
Saved Camera Raw settings to {file} Настройки Camera Raw сохранены в {file}
|
||||
Save the settings to each raw's sidecar without opening it Сохранить настройки в сопроводительный файл каждого raw, не открывая его
|
||||
No settings saved: these raws can't keep a sidecar here Ничего не сохранено: у этих raw здесь не может быть сопроводительного файла
|
||||
Some settings could not be saved; the raws stay open Не удалось сохранить некоторые настройки; raw остаются открытыми
|
||||
Nothing to save: these raws are at their default settings Нечего сохранять: у этих raw настройки по умолчанию
|
||||
# Artboard tool options bar (#11385)
|
||||
Distribute Widths Распределить ширину
|
||||
Distribute Heights Распределить высоту
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -4681,6 +4681,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
|
||||
Show layer bounds on hover Prikaži granice sloja pri prelasku miša
|
||||
Expand layer group on click Proširi grupu slojeva klikom
|
||||
Show hover bounds from Layers Prikaži granice pri prelasku iz panela Slojevi
|
||||
Saved Camera Raw settings to {file} Подешавања Camera Raw сачувана у {file}
|
||||
Save the settings to each raw's sidecar without opening it Sačuvaj podešavanja u prateću datoteku svakog raw snimka bez otvaranja
|
||||
No settings saved: these raws can't keep a sidecar here Ništa nije sačuvano: ovi raw snimci ovde ne mogu imati prateću datoteku
|
||||
Some settings could not be saved; the raws stay open Neka podešavanja nije moguće sačuvati; raw snimci ostaju otvoreni
|
||||
Nothing to save: these raws are at their default settings Nema šta da se sačuva: ovi raw snimci imaju podrazumevana podešavanja
|
||||
# Artboard tool options bar (#11385)
|
||||
Distribute Widths Raspodeli širine
|
||||
Distribute Heights Raspodeli visine
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -4346,6 +4346,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
|
||||
Show layer bounds on hover Üzerine gelince katman sınırlarını göster
|
||||
Expand layer group on click Tıklayınca katman grubunu genişlet
|
||||
Show hover bounds from Layers Katmanlar'dan üzerine gelme sınırlarını göster
|
||||
Saved Camera Raw settings to {file} Camera Raw ayarları {file} dosyasına kaydedildi
|
||||
Save the settings to each raw's sidecar without opening it Ayarları her raw dosyasının yan dosyasına kaydet, açmadan
|
||||
No settings saved: these raws can't keep a sidecar here Hiçbir şey kaydedilmedi: bu raw dosyaları burada yan dosyaya sahip olamaz
|
||||
Some settings could not be saved; the raws stay open Bazı ayarlar kaydedilemedi; raw dosyaları açık kalıyor
|
||||
Nothing to save: these raws are at their default settings Kaydedilecek bir şey yok: bu raw dosyaları varsayılan ayarlarda
|
||||
# Artboard tool options bar (#11385)
|
||||
Distribute Widths Genişlikleri Dağıt
|
||||
Distribute Heights Yükseklikleri Dağıt
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 8 and column 34.
|
@@ -4326,6 +4326,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
|
||||
Show layer bounds on hover Показувати межі шару під час наведення
|
||||
Expand layer group on click Розгортати групу шарів за клацанням
|
||||
Show hover bounds from Layers Показувати межі під час наведення з панелі «Шари»
|
||||
Saved Camera Raw settings to {file} Налаштування Camera Raw збережено в {file}
|
||||
Save the settings to each raw's sidecar without opening it Зберегти налаштування в супровідний файл кожного raw, не відкриваючи його
|
||||
No settings saved: these raws can't keep a sidecar here Нічого не збережено: ці raw тут не можуть мати супровідного файлу
|
||||
Some settings could not be saved; the raws stay open Не вдалося зберегти деякі налаштування; raw залишаються відкритими
|
||||
Nothing to save: these raws are at their default settings Нічого зберігати: ці raw мають типові налаштування
|
||||
# Artboard tool options bar (#11385)
|
||||
Distribute Widths Розподілити ширину
|
||||
Distribute Heights Розподілити висоту
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 152 and column 53.
|
@@ -4120,6 +4120,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
|
||||
Show layer bounds on hover Hiện ranh giới lớp khi di chuột
|
||||
Expand layer group on click Mở rộng nhóm lớp khi nhấp
|
||||
Show hover bounds from Layers Hiện ranh giới khi di chuột từ bảng Lớp
|
||||
Saved Camera Raw settings to {file} Đã lưu cài đặt Camera Raw vào {file}
|
||||
Save the settings to each raw's sidecar without opening it Lưu thiết lập vào tệp sidecar của từng ảnh raw mà không mở
|
||||
No settings saved: these raws can't keep a sidecar here Không lưu được gì: các ảnh raw này không thể có tệp sidecar ở đây
|
||||
Some settings could not be saved; the raws stay open Không thể lưu một số cài đặt; các ảnh raw vẫn mở
|
||||
Nothing to save: these raws are at their default settings Không có gì để lưu: các ảnh raw này đang ở cài đặt mặc định
|
||||
# Artboard tool options bar (#11385)
|
||||
Distribute Widths Phân bố chiều rộng
|
||||
Distribute Heights Phân bố chiều cao
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -3307,6 +3307,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
|
||||
Show layer bounds on hover 悬停时显示图层边界
|
||||
Expand layer group on click 单击时展开图层组
|
||||
Show hover bounds from Layers 显示来自图层面板的悬停边界
|
||||
Saved Camera Raw settings to {file} 已将 Camera Raw 设置保存到 {file}
|
||||
Save the settings to each raw's sidecar without opening it 将设置保存到每个 Raw 的附属文件,不打开它
|
||||
No settings saved: these raws can't keep a sidecar here 未保存任何内容:这些 Raw 在此处无法拥有附属文件
|
||||
Some settings could not be saved; the raws stay open 部分设置无法保存;Raw 保持打开
|
||||
Nothing to save: these raws are at their default settings 无需保存:这些 Raw 均为默认设置
|
||||
# Artboard tool options bar (#11385)
|
||||
Distribute Widths 分布宽度
|
||||
Distribute Heights 分布高度
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 63 and column 53.
|
@@ -3264,6 +3264,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
|
||||
Show layer bounds on hover 滑過時顯示圖層邊界
|
||||
Expand layer group on click 按一下時展開圖層群組
|
||||
Show hover bounds from Layers 顯示來自圖層面板的滑過邊界
|
||||
Saved Camera Raw settings to {file} 已將 Camera Raw 設定儲存到 {file}
|
||||
Save the settings to each raw's sidecar without opening it 將設定儲存到每個 Raw 的附屬檔案,不開啟它
|
||||
No settings saved: these raws can't keep a sidecar here 未儲存任何內容:這些 Raw 在此處無法擁有附屬檔案
|
||||
Some settings could not be saved; the raws stay open 部分設定無法儲存;Raw 保持開啟
|
||||
Nothing to save: these raws are at their default settings 無需儲存:這些 Raw 均為預設設定
|
||||
# Artboard tool options bar (#11385)
|
||||
Distribute Widths 均分寬度
|
||||
Distribute Heights 均分高度
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -606,9 +606,9 @@ pub struct PhotocraftApp {
|
||||
pub(crate) gradient: gradient_ui::LiveGradient,
|
||||
/// Filter › Camera Raw Filter dialog (camera_raw_ui).
|
||||
pub(crate) camera_raw: Option<camera_raw_ui::CameraRawDialog>,
|
||||
/// A raw file just opened interactively, waiting for the open-time Camera Raw dialog (shown
|
||||
/// on the next frame, which has the egui context).
|
||||
pub(crate) pending_raw_open: Option<camera_raw_ui::RawOpen>,
|
||||
/// Raws just opened interactively, waiting in the open-time Camera Raw dialog (shown on the
|
||||
/// next frame, which has the egui context); several share it in a filmstrip.
|
||||
pub(crate) raw_shelf: Vec<camera_raw_ui::HeldRaw>,
|
||||
/// The open-time re-develops (or their Camera Raw steps) running in the background, one per
|
||||
/// raw being opened.
|
||||
pub(crate) raw_redevelop: Vec<camera_raw_ui::Redevelop>,
|
||||
|
||||
Reference in new issue
Block a user