Say which camera profile a photograph renders through, and offer to copy it
The info panel gains a line under the lens: "Adobe Standard · in the file", the .dcp it came from, "· off" when the photographer switched it off, or "No camera profile · matrix only" — the ordinary case for a CR2, worded as a fact rather than a failure. A DNG whose embedded profile may be copied, for a body with no installed profile, also gets "Use this profile for every Canon EOS 6D →", which saves it into the profiles directory; the body's CR2s render through it from their next decode. The profiles directory is <data>/profiles, read at start-up on desktop and Android before anything decodes. The library open path never set the lens line; it now sets both. Labels: Camera Profile, Use Profile, Look Amount.
This commit is contained in:
+50
-50
File diff suppressed because one or more lines are too long
@@ -70,6 +70,12 @@ pub struct DevelopSession {
|
|||||||
/// satisfies, and this says whether a *measurement* was found. Only the
|
/// satisfies, and this says whether a *measurement* was found. Only the
|
||||||
/// second can honestly caption "no profile".
|
/// second can honestly caption "no profile".
|
||||||
pub(super) lens_profile_found: bool,
|
pub(super) lens_profile_found: bool,
|
||||||
|
/// TRACES: FR-DEV-3e
|
||||||
|
/// The camera profile embedded in the file, where its embed policy lets
|
||||||
|
/// it be copied and no installed profile already covers the body: what
|
||||||
|
/// the info panel offers to save for every photograph from that camera
|
||||||
|
/// (D20). `None` once taken up.
|
||||||
|
pub(super) profile_offer: Option<Arc<dr_decode::dcp::Dcp>>,
|
||||||
/// Kept so the session can build GPU resources after construction.
|
/// Kept so the session can build GPU resources after construction.
|
||||||
///
|
///
|
||||||
/// The distance fields behind a subject mask are made when a layer is
|
/// The distance fields behind a subject mask are made when a layer is
|
||||||
@@ -423,6 +429,7 @@ impl DevelopSession {
|
|||||||
// Nothing has been looked up, which is not the same as "looked up
|
// Nothing has been looked up, which is not the same as "looked up
|
||||||
// and not found" — `lens_summary` distinguishes them.
|
// and not found" — `lens_summary` distinguishes them.
|
||||||
lens_profile_found: false,
|
lens_profile_found: false,
|
||||||
|
profile_offer: None,
|
||||||
ctx: ctx.clone(),
|
ctx: ctx.clone(),
|
||||||
graph,
|
graph,
|
||||||
history,
|
history,
|
||||||
@@ -580,6 +587,74 @@ impl DevelopSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// TRACES: FR-DEV-3e
|
||||||
|
/// Remember the profile embedded in the file, if it may be offered for
|
||||||
|
/// copying (see [`Self::profile_offer`]).
|
||||||
|
pub fn set_embedded_profile(&mut self, embedded: Option<dr_decode::dcp::Dcp>) {
|
||||||
|
self.profile_offer = embedded
|
||||||
|
.filter(|p| p.may_copy())
|
||||||
|
.filter(|p| {
|
||||||
|
let model = p.unique_camera_model.as_deref();
|
||||||
|
model.is_some() && dr_decode::dcp::find(model, "", "").is_none()
|
||||||
|
})
|
||||||
|
.map(Arc::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TRACES: FR-DEV-3e
|
||||||
|
/// What to tell the photographer about the camera profile (D20).
|
||||||
|
///
|
||||||
|
/// Empty for an already-rendered source — a JPEG has no camera profile
|
||||||
|
/// to speak of. Otherwise the profile's name and where it came from, or
|
||||||
|
/// that there is none, which is the ordinary case for a CR2 and must read
|
||||||
|
/// as a fact: the matrix alone is a correct rendering, only a plainer one.
|
||||||
|
pub fn profile_summary(&self) -> String {
|
||||||
|
if self.demosaiced.is_non_linear() {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
let Some(tables) = self.demosaiced.profile_tables() else {
|
||||||
|
return "No camera profile · matrix only".into();
|
||||||
|
};
|
||||||
|
let name = if tables.name.is_empty() {
|
||||||
|
"Camera profile"
|
||||||
|
} else {
|
||||||
|
tables.name.as_str()
|
||||||
|
};
|
||||||
|
let applied = self
|
||||||
|
.graph
|
||||||
|
.param(
|
||||||
|
dr_pipeline::ops::camera_profile::ID,
|
||||||
|
dr_pipeline::ops::camera_profile::APPLY,
|
||||||
|
)
|
||||||
|
.is_none_or(|v| v != 0.0);
|
||||||
|
if !applied {
|
||||||
|
return format!("{name} · off");
|
||||||
|
}
|
||||||
|
match &tables.origin {
|
||||||
|
dr_types::ProfileOrigin::Embedded => format!("{name} · in the file"),
|
||||||
|
dr_types::ProfileOrigin::File(file) => format!("{name} · {file}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TRACES: FR-DEV-3e
|
||||||
|
/// The action the info panel offers for this file's embedded profile,
|
||||||
|
/// worded with the body it would apply to; `None` where there is nothing
|
||||||
|
/// to offer.
|
||||||
|
pub fn profile_offer(&self) -> Option<String> {
|
||||||
|
let model = self.profile_offer.as_ref()?.unique_camera_model.clone()?;
|
||||||
|
Some(format!("Use this profile for every {model}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TRACES: FR-DEV-3e
|
||||||
|
/// Save the embedded profile into the profiles directory, so every other
|
||||||
|
/// photograph from this body — its CR2s, above all — renders through it
|
||||||
|
/// from the next time it is opened.
|
||||||
|
pub fn adopt_profile(&mut self) -> Result<std::path::PathBuf, String> {
|
||||||
|
let profile = self.profile_offer.as_ref().ok_or("nothing to adopt")?;
|
||||||
|
let path = dr_decode::dcp::save(profile)?;
|
||||||
|
self.profile_offer = None;
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
/// TRACES: FR-EXP-8
|
/// TRACES: FR-EXP-8
|
||||||
/// The header this session was opened from, where there was one.
|
/// The header this session was opened from, where there was one.
|
||||||
///
|
///
|
||||||
@@ -733,6 +808,107 @@ mod tests {
|
|||||||
/// own rule is that the interface must be plain about which it is, because
|
/// own rule is that the interface must be plain about which it is, because
|
||||||
/// a correction that silently did nothing is worse than one visibly
|
/// a correction that silently did nothing is worse than one visibly
|
||||||
/// unavailable.
|
/// unavailable.
|
||||||
|
/// A flat 8×8 raw carrying `tables`, for the camera profile line.
|
||||||
|
fn raw_with(tables: Option<dr_types::ProfileTables>) -> dr_decode::RawImage {
|
||||||
|
dr_decode::RawImage {
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
data: vec![20_000; 64],
|
||||||
|
cfa_pattern: dr_decode::CfaPattern::Rggb,
|
||||||
|
black_level: [0; 4],
|
||||||
|
white_level: u16::MAX,
|
||||||
|
wb_coeffs: [1.0; 4],
|
||||||
|
color_matrix: None,
|
||||||
|
samples_per_pixel: 1,
|
||||||
|
profile: None,
|
||||||
|
profile_tables: tables.map(Arc::new),
|
||||||
|
make: "Canon".into(),
|
||||||
|
model: "EOS 6D".into(),
|
||||||
|
crop: dr_decode::CropRect {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_profile_line_names_the_profile_and_where_it_came_from() {
|
||||||
|
// TRACES: FR-DEV-3e
|
||||||
|
let Some(ctx) = headless() else { return };
|
||||||
|
let tables = |origin| dr_types::ProfileTables {
|
||||||
|
name: "Adobe Standard".into(),
|
||||||
|
origin,
|
||||||
|
hue_sat: None,
|
||||||
|
look: dr_types::HueSatTable::new(2, 2, 1, false, vec![[5.0, 1.1, 1.0]; 4]),
|
||||||
|
};
|
||||||
|
let open = |t| {
|
||||||
|
DevelopSession::open(&ctx, &raw_with(t), dr_types::Orientation::NORMAL)
|
||||||
|
.expect("session")
|
||||||
|
};
|
||||||
|
|
||||||
|
let none = open(None);
|
||||||
|
assert_eq!(none.profile_summary(), "No camera profile · matrix only");
|
||||||
|
assert_eq!(none.profile_offer(), None);
|
||||||
|
|
||||||
|
let mut embedded = open(Some(tables(dr_types::ProfileOrigin::Embedded)));
|
||||||
|
assert_eq!(embedded.profile_summary(), "Adobe Standard · in the file");
|
||||||
|
embedded.graph.set_param(
|
||||||
|
dr_pipeline::ops::camera_profile::ID,
|
||||||
|
dr_pipeline::ops::camera_profile::APPLY,
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
assert_eq!(embedded.profile_summary(), "Adobe Standard · off");
|
||||||
|
|
||||||
|
let file = open(Some(tables(dr_types::ProfileOrigin::File(
|
||||||
|
"Canon EOS 6D Adobe Standard.dcp".into(),
|
||||||
|
))));
|
||||||
|
assert_eq!(
|
||||||
|
file.profile_summary(),
|
||||||
|
"Adobe Standard · Canon EOS 6D Adobe Standard.dcp"
|
||||||
|
);
|
||||||
|
|
||||||
|
// A JPEG has no camera profile to speak of.
|
||||||
|
let rgba: Vec<u8> = (0..8 * 8).flat_map(|_| [128u8, 128, 128, 255]).collect();
|
||||||
|
let jpeg = DevelopSession::open_rgb(&ctx, &rgba, 8, 8, dr_types::Orientation::NORMAL)
|
||||||
|
.expect("session");
|
||||||
|
assert_eq!(jpeg.profile_summary(), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_a_copyable_profile_is_offered() {
|
||||||
|
// TRACES: FR-DEV-3e
|
||||||
|
let Some(ctx) = headless() else { return };
|
||||||
|
let mut s = DevelopSession::open(&ctx, &raw_with(None), dr_types::Orientation::NORMAL)
|
||||||
|
.expect("session");
|
||||||
|
let profile = |policy| dr_decode::dcp::Dcp {
|
||||||
|
name: "Adobe Standard".into(),
|
||||||
|
unique_camera_model: Some("Nonexistent Body 1".into()),
|
||||||
|
copyright: None,
|
||||||
|
calibration_signature: None,
|
||||||
|
embed_policy: policy,
|
||||||
|
illuminants: [Some(21), None],
|
||||||
|
color_matrix: [
|
||||||
|
Some([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]),
|
||||||
|
None,
|
||||||
|
],
|
||||||
|
forward_matrix: [None, None],
|
||||||
|
hue_sat: [None, None],
|
||||||
|
look: dr_types::HueSatTable::new(2, 2, 1, false, vec![[5.0, 1.1, 1.0]; 4]),
|
||||||
|
tone_curve: None,
|
||||||
|
};
|
||||||
|
s.set_embedded_profile(Some(profile(0)));
|
||||||
|
assert_eq!(
|
||||||
|
s.profile_offer().as_deref(),
|
||||||
|
Some("Use this profile for every Nonexistent Body 1")
|
||||||
|
);
|
||||||
|
s.set_embedded_profile(Some(profile(2)));
|
||||||
|
assert_eq!(s.profile_offer(), None, "embed never");
|
||||||
|
s.set_embedded_profile(None);
|
||||||
|
assert_eq!(s.profile_offer(), None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_lens_line_says_which_kind_of_nothing_it_found() {
|
fn the_lens_line_says_which_kind_of_nothing_it_found() {
|
||||||
let Some(ctx) = headless() else { return };
|
let Some(ctx) = headless() else { return };
|
||||||
|
|||||||
@@ -208,12 +208,19 @@ fn catalogued(key: &str) -> Option<&'static str> {
|
|||||||
// the pipeline, and the photographer is choosing how the scene's range
|
// the pipeline, and the photographer is choosing how the scene's range
|
||||||
// is fitted onto the screen.
|
// is fitted onto the screen.
|
||||||
"op.view_transform" => "Tone Mapping",
|
"op.view_transform" => "Tone Mapping",
|
||||||
|
// The DNG camera profile's tables (D20). "Camera Profile", as Camera
|
||||||
|
// Raw calls the same thing, so a photographer arriving from Lightroom
|
||||||
|
// finds it under the name they know.
|
||||||
|
"op.camera_profile" => "Camera Profile",
|
||||||
|
|
||||||
// Parameters
|
// Parameters
|
||||||
// Named for what it does rather than what it is, since a lone
|
// Named for what it does rather than what it is, since a lone
|
||||||
// parameter is titled by its operation and this one never reaches the
|
// parameter is titled by its operation and this one never reaches the
|
||||||
// panel under its own name — see `rows_filtered`.
|
// panel under its own name — see `rows_filtered`.
|
||||||
"param.lens_profile.apply" => "Apply",
|
"param.lens_profile.apply" => "Apply",
|
||||||
|
"param.camera_profile.apply" => "Use Profile",
|
||||||
|
// The LookTable's strength, as Lightroom's profile "Amount".
|
||||||
|
"param.camera_profile.look" => "Look Amount",
|
||||||
"param.view_transform.contrast" => "Contrast",
|
"param.view_transform.contrast" => "Contrast",
|
||||||
// In stops above middle grey: where the scene reaches display white.
|
// In stops above middle grey: where the scene reaches display white.
|
||||||
"param.view_transform.white" => "White Point",
|
"param.view_transform.white" => "White Point",
|
||||||
|
|||||||
@@ -324,6 +324,13 @@ pub(crate) fn open_session(
|
|||||||
// the capture date because of this line; before it, the same photograph
|
// the capture date because of this line; before it, the same photograph
|
||||||
// exported from the grid kept them and exported from develop did not.
|
// exported from the grid kept them and exported from develop did not.
|
||||||
session.set_source_metadata(meta.clone());
|
session.set_source_metadata(meta.clone());
|
||||||
|
// TRACES: FR-DEV-3e
|
||||||
|
// The embedded camera profile, read again from the header, for the info
|
||||||
|
// panel's offer to copy it. Only a DNG carries one; the read is the
|
||||||
|
// header's IFDs, not the photosites.
|
||||||
|
if dr_decode::probe(bytes) != Some(dr_types::Format::Jpeg) {
|
||||||
|
session.set_embedded_profile(dr_decode::dcp::embedded_in(bytes));
|
||||||
|
}
|
||||||
Ok(session)
|
Ok(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1194,6 +1201,19 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
|||||||
log::info!("gpu opened in {} ms", launch_began.elapsed().as_millis());
|
log::info!("gpu opened in {} ms", launch_began.elapsed().as_millis());
|
||||||
|
|
||||||
let window = init_window(&gpu)?;
|
let window = init_window(&gpu)?;
|
||||||
|
|
||||||
|
// TRACES: FR-DEV-3e
|
||||||
|
// The installed camera profiles, before anything decodes (D20). A
|
||||||
|
// directory of a few `.dcp` files, each a couple of hundred kilobytes,
|
||||||
|
// so read here rather than deferred: a photograph opened before a
|
||||||
|
// background load finished would render without its profile once and
|
||||||
|
// with it the next time.
|
||||||
|
let profiles_began = std::time::Instant::now();
|
||||||
|
dr_decode::dcp::set_profiles_directory(library::data_root().join("profiles"));
|
||||||
|
log::info!(
|
||||||
|
"camera profiles read in {} ms",
|
||||||
|
profiles_began.elapsed().as_millis()
|
||||||
|
);
|
||||||
wire_inference_status(&window);
|
wire_inference_status(&window);
|
||||||
wire_diagnostics(&window, &gpu);
|
wire_diagnostics(&window, &gpu);
|
||||||
|
|
||||||
@@ -1246,6 +1266,7 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
|
|||||||
let index = Rc::new(RefCell::new(0usize));
|
let index = Rc::new(RefCell::new(0usize));
|
||||||
// The current develop session, if the file yielded sensor data.
|
// The current develop session, if the file yielded sensor data.
|
||||||
let session: Rc<RefCell<Option<DevelopSession>>> = Rc::new(RefCell::new(None));
|
let session: Rc<RefCell<Option<DevelopSession>>> = Rc::new(RefCell::new(None));
|
||||||
|
wire_profile_offer(&window, &session);
|
||||||
|
|
||||||
// TRACES: FR-PLAT-AND-5
|
// TRACES: FR-PLAT-AND-5
|
||||||
// The GPU tier — the first thing given back under memory pressure, and on
|
// The GPU tier — the first thing given back under memory pressure, and on
|
||||||
@@ -1479,6 +1500,43 @@ fn init_window(gpu: &Option<dr_gpu::GpuContext>) -> Result<AppWindow> {
|
|||||||
/// What the models run on. Re-read every two seconds because the answer
|
/// What the models run on. Re-read every two seconds because the answer
|
||||||
/// changes twice after launch — when the probe reports and as each
|
/// changes twice after launch — when the probe reports and as each
|
||||||
/// engine lands — and the page is open for longer than either takes.
|
/// engine lands — and the page is open for longer than either takes.
|
||||||
|
/// TRACES: FR-DEV-3e
|
||||||
|
/// The info panel's camera profile line and its offer, for the session just
|
||||||
|
/// opened — or cleared, for none.
|
||||||
|
fn show_camera_profile(window: &AppWindow, session: Option<&DevelopSession>) {
|
||||||
|
window.global::<Capture>().set_profile(
|
||||||
|
session
|
||||||
|
.map(DevelopSession::profile_summary)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
window.global::<ProfileOffer>().set_offer(
|
||||||
|
session
|
||||||
|
.and_then(DevelopSession::profile_offer)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TRACES: FR-DEV-3e
|
||||||
|
/// Saving the open DNG's embedded profile for every photograph from its
|
||||||
|
/// body (D20). The photograph on screen already renders through it; the
|
||||||
|
/// others pick it up when they are next decoded.
|
||||||
|
fn wire_profile_offer(window: &AppWindow, session: &Rc<RefCell<Option<DevelopSession>>>) {
|
||||||
|
let weak = window.as_weak();
|
||||||
|
let session = session.clone();
|
||||||
|
window.global::<ProfileOffer>().on_adopt(move || {
|
||||||
|
let Some(w) = weak.upgrade() else { return };
|
||||||
|
let mut guard = session.borrow_mut();
|
||||||
|
let Some(s) = guard.as_mut() else { return };
|
||||||
|
match s.adopt_profile() {
|
||||||
|
Ok(path) => log::info!("camera profile saved to {}", path.display()),
|
||||||
|
Err(e) => log::warn!("camera profile not saved: {e}"),
|
||||||
|
}
|
||||||
|
show_camera_profile(&w, Some(s));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn wire_inference_status(window: &AppWindow) {
|
fn wire_inference_status(window: &AppWindow) {
|
||||||
let set = |w: &AppWindow| {
|
let set = |w: &AppWindow| {
|
||||||
let (line, detail) = inference::about_lines();
|
let (line, detail) = inference::about_lines();
|
||||||
@@ -2547,6 +2605,7 @@ fn build_show(
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
|
show_camera_profile(window, l.session.as_ref());
|
||||||
|
|
||||||
// The panel is built from what the pipeline reports, so
|
// The panel is built from what the pipeline reports, so
|
||||||
// this code names no operation (FR-DEV-3a).
|
// this code names no operation (FR-DEV-3a).
|
||||||
@@ -2604,6 +2663,7 @@ fn build_show(
|
|||||||
capture.set_exposure("".into());
|
capture.set_exposure("".into());
|
||||||
capture.set_dimensions("".into());
|
capture.set_dimensions("".into());
|
||||||
capture.set_lens("".into());
|
capture.set_lens("".into());
|
||||||
|
show_camera_profile(window, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -2691,6 +2751,8 @@ fn wire_remote_open(
|
|||||||
capture.set_camera("".into());
|
capture.set_camera("".into());
|
||||||
capture.set_exposure("".into());
|
capture.set_exposure("".into());
|
||||||
capture.set_dimensions("".into());
|
capture.set_dimensions("".into());
|
||||||
|
capture.set_lens("".into());
|
||||||
|
show_camera_profile(&w, None);
|
||||||
// The grid is one image at a time, so next/previous have nothing
|
// The grid is one image at a time, so next/previous have nothing
|
||||||
// to walk. Shown as 1 of 1 rather than left reading 0.
|
// to walk. Shown as 1 of 1 rather than left reading 0.
|
||||||
w.set_index(0);
|
w.set_index(0);
|
||||||
@@ -2868,6 +2930,14 @@ fn wire_remote_open(
|
|||||||
capture.set_camera(describe_camera(&l.meta).into());
|
capture.set_camera(describe_camera(&l.meta).into());
|
||||||
capture.set_exposure(describe_exposure(&l.meta).into());
|
capture.set_exposure(describe_exposure(&l.meta).into());
|
||||||
capture.set_dimensions(format!("{} × {}", l.width, l.height).into());
|
capture.set_dimensions(format!("{} × {}", l.width, l.height).into());
|
||||||
|
capture.set_lens(
|
||||||
|
l.session
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.lens_summary())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
show_camera_profile(&w, l.session.as_ref());
|
||||||
match l.session {
|
match l.session {
|
||||||
Some(mut s) => {
|
Some(mut s) => {
|
||||||
w.global::<Develop>().set_enabled(true);
|
w.global::<Develop>().set_enabled(true);
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ pub fn place_path(account: &Account) -> PathBuf {
|
|||||||
/// desktop declares nothing and takes the platform's data directory from
|
/// desktop declares nothing and takes the platform's data directory from
|
||||||
/// `dr_plat::dirs` — XDG on Linux, `%LOCALAPPDATA%` on Windows — which keeps
|
/// `dr_plat::dirs` — XDG on Linux, `%LOCALAPPDATA%` on Windows — which keeps
|
||||||
/// the established location on Linux rather than moving anyone's catalog.
|
/// the established location on Linux rather than moving anyone's catalog.
|
||||||
pub(super) fn data_root() -> PathBuf {
|
pub(crate) fn data_root() -> PathBuf {
|
||||||
match dr_sync::account::declared_data_dir() {
|
match dr_sync::account::declared_data_dir() {
|
||||||
Some(declared) => declared.join("darkroom"),
|
Some(declared) => declared.join("darkroom"),
|
||||||
None => dr_plat::base_dir(dr_plat::Base::Data),
|
None => dr_plat::base_dir(dr_plat::Base::Data),
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { SettingsPage } from "settings.slint";
|
|||||||
import { ImportPage } from "import.slint";
|
import { ImportPage } from "import.slint";
|
||||||
import { MergePage, MergeFrameRow } from "merge.slint";
|
import { MergePage, MergeFrameRow } from "merge.slint";
|
||||||
import { DuplicatesPage, Duplicates, DuplicateRow, DuplicateCopy } from "duplicates.slint";
|
import { DuplicatesPage, Duplicates, DuplicateRow, DuplicateCopy } from "duplicates.slint";
|
||||||
import { Capture, StatusBar, InfoPanel } from "develop.slint";
|
import { Capture, ProfileOffer, StatusBar, InfoPanel } from "develop.slint";
|
||||||
import { ToolRail } from "toolrail.slint";
|
import { ToolRail } from "toolrail.slint";
|
||||||
import { Keys } from "keys.slint";
|
import { Keys } from "keys.slint";
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ export { ViewMode, GradientHandle, HandleRole, SpotHandle, SpotRole }
|
|||||||
// hooked through `window.global::<Adjustments>()` and only what this document
|
// hooked through `window.global::<Adjustments>()` and only what this document
|
||||||
// exports appears in the generated API. `session.slint` says why the develop
|
// exports appears in the generated API. `session.slint` says why the develop
|
||||||
// panels take their wiring this way and why there is one global per family.
|
// panels take their wiring this way and why there is one global per family.
|
||||||
export { Develop, Capture, Levels, Peaking, Adjustments, Framing, Transfer, Masking, Repair, Steps }
|
export { Develop, Capture, ProfileOffer, Levels, Peaking, Adjustments, Framing, Transfer, Masking, Repair, Steps }
|
||||||
export { Collections }
|
export { Collections }
|
||||||
export { Library }
|
export { Library }
|
||||||
export { ExportOptions }
|
export { ExportOptions }
|
||||||
|
|||||||
@@ -383,10 +383,51 @@ export global Capture {
|
|||||||
/// same kind of thing as the body and the exposure, and it wants reading
|
/// same kind of thing as the body and the exposure, and it wants reading
|
||||||
/// once on opening rather than hunting for under a group filter.
|
/// once on opening rather than hunting for under a group filter.
|
||||||
in property <string> lens;
|
in property <string> lens;
|
||||||
|
/// TRACES: FR-DEV-3e
|
||||||
|
/// The camera profile the photograph renders through, and where it was
|
||||||
|
/// found — or that there is none (D20). A fact about the file and the
|
||||||
|
/// installed profiles, read once on opening, as the lens line is.
|
||||||
|
in property <string> profile;
|
||||||
in property <string> exposure;
|
in property <string> exposure;
|
||||||
in property <string> dimensions;
|
in property <string> dimensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// TRACES: FR-DEV-3e
|
||||||
|
/// The one action the info panel offers: saving the camera profile embedded
|
||||||
|
/// in this file for every photograph from the same body (D20).
|
||||||
|
///
|
||||||
|
/// A global of its own because `Capture` only reports and carries no
|
||||||
|
/// callback. `offer` is the wording, naming the body; empty when there is
|
||||||
|
/// nothing to offer, which is every file but a DNG whose profile may be
|
||||||
|
/// copied and is not installed yet.
|
||||||
|
export global ProfileOffer {
|
||||||
|
in property <string> offer;
|
||||||
|
callback adopt();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The offer as a line of text that acts, rather than a button: a button
|
||||||
|
// does not wrap, and a label naming a camera is long enough to widen the
|
||||||
|
// whole column.
|
||||||
|
component OfferLine inherits Rectangle {
|
||||||
|
in property <string> text;
|
||||||
|
callback clicked();
|
||||||
|
accessible-role: button;
|
||||||
|
accessible-label: root.text;
|
||||||
|
accessible-action-default => { root.clicked(); }
|
||||||
|
height: line.preferred-height;
|
||||||
|
|
||||||
|
line := Caption {
|
||||||
|
width: 100%;
|
||||||
|
text: root.text + " →";
|
||||||
|
wrap: word-wrap;
|
||||||
|
emphasised: touch.has-hover;
|
||||||
|
}
|
||||||
|
touch := TouchArea {
|
||||||
|
mouse-cursor: pointer;
|
||||||
|
clicked => { root.clicked(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Capture metadata. Read-only; the adjustment controls live in AdjustPanel,
|
// Capture metadata. Read-only; the adjustment controls live in AdjustPanel,
|
||||||
// which is generated from pipeline capabilities rather than written here.
|
// which is generated from pipeline capabilities rather than written here.
|
||||||
//
|
//
|
||||||
@@ -424,6 +465,17 @@ export component InfoPanel inherits Rectangle {
|
|||||||
wrap: word-wrap;
|
wrap: word-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Caption {
|
||||||
|
text: Capture.profile;
|
||||||
|
visible: Capture.profile != "";
|
||||||
|
wrap: word-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ProfileOffer.offer != "": OfferLine {
|
||||||
|
text: ProfileOffer.offer;
|
||||||
|
clicked => { ProfileOffer.adopt(); }
|
||||||
|
}
|
||||||
|
|
||||||
Caption { text: Capture.dimensions; }
|
Caption { text: Capture.dimensions; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user