mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-24 10:09:01 -06:00
LR-EDIT-COPY-PASTE: Enter applies the Copy and Paste Settings dialogs (#735)
Return presses OK in Choose Edit Settings to Copy and Paste Selected Settings when no widget has the keyboard focus (a Tab-focused button or checkbox takes Return itself, as egui does). Esc already cancelled them; tests now cover both keys.
This commit is contained in:
@@ -41,6 +41,8 @@ mod tests_activity;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests_album_picker;
|
mod tests_album_picker;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
mod tests_copy_paste_dialogs;
|
||||||
|
#[cfg(test)]
|
||||||
mod tests_crop_rotate;
|
mod tests_crop_rotate;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests_curve;
|
mod tests_curve;
|
||||||
|
|||||||
@@ -77,6 +77,16 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||||||
});
|
});
|
||||||
let mut close = false;
|
let mut close = false;
|
||||||
let mut confirm = false;
|
let mut confirm = false;
|
||||||
|
// Return presses OK in a dialog without a text field of its own (Lightroom's default button);
|
||||||
|
// a widget that has the keyboard focus (a text field, a button or checkbox reached with Tab)
|
||||||
|
// takes Return itself. Consumed before the dialog is drawn, so nothing else sees it too.
|
||||||
|
if return_confirms(&dlg)
|
||||||
|
&& ctx.memory(|m| m.focused().is_none())
|
||||||
|
&& !egui::Popup::is_any_open(ctx)
|
||||||
|
&& ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Enter))
|
||||||
|
{
|
||||||
|
confirm = true;
|
||||||
|
}
|
||||||
let title: String = match &dlg {
|
let title: String = match &dlg {
|
||||||
Dialog::TextPrompt { title, .. } => title.as_str(),
|
Dialog::TextPrompt { title, .. } => title.as_str(),
|
||||||
Dialog::NewAlbum { folder: true, .. } => "Create Folder",
|
Dialog::NewAlbum { folder: true, .. } => "Create Folder",
|
||||||
@@ -1556,6 +1566,12 @@ fn choices<V: PartialEq + Copy>(ui: &mut egui::Ui, label: &str, id: &str, option
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Dialogs that Return confirms with nothing focused (those with text fields confirm from the
|
||||||
|
/// field: `dialog_field`).
|
||||||
|
fn return_confirms(dlg: &Dialog) -> bool {
|
||||||
|
matches!(dlg, Dialog::CopySettings { .. } | Dialog::PasteSettings { .. })
|
||||||
|
}
|
||||||
|
|
||||||
/// A dialog's one text field: it keeps the focus (except while its own menu has it) and opens
|
/// A dialog's one text field: it keeps the focus (except while its own menu has it) and opens
|
||||||
/// with its text selected, so typing replaces it. True when Return confirms the dialog; Esc
|
/// with its text selected, so typing replaces it. True when Return confirms the dialog; Esc
|
||||||
/// cancels the dialog (`show`). Taking the focus back with a bare `TextEdit` every frame hid the
|
/// cancels the dialog (`show`). Taking the focus back with a bare `TextEdit` every frame hid the
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
//! Return presses OK in Copy Settings (⇧⌘C) and Paste Selected Settings (⇧⌘V); Esc cancels (#735).
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use lightcraft_catalog::PhotoId;
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
|
use crate::LightcraftApp;
|
||||||
|
use crate::headless::Headless;
|
||||||
|
use crate::state::Dialog;
|
||||||
|
|
||||||
|
const SETTLE: Duration = Duration::from_secs(120);
|
||||||
|
const T: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
|
/// The demo library with an edited first photo selected; returns the first two photos' ids.
|
||||||
|
fn edited() -> (Headless, [u64; 2]) {
|
||||||
|
let services = crate::Services { png: None, ..Default::default() };
|
||||||
|
let mut app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), services);
|
||||||
|
app.ui.view = crate::state::ViewMode::PhotoGrid;
|
||||||
|
let mut h = Headless::new(app, [1200.0, 900.0], 1.0);
|
||||||
|
let ids: Vec<u64> = h.app.session.visible_cloned().iter().take(2).map(|p| p.0).collect();
|
||||||
|
let ids = [ids[0], ids[1]];
|
||||||
|
exec(&mut h, "library.select", json!({"ids": [ids[0]]}));
|
||||||
|
exec(&mut h, "develop.set", json!({"values": {"light.exposure": 1.0, "color.vibrance": 30}}));
|
||||||
|
(h, ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exec(h: &mut Headless, command: &str, params: Value) {
|
||||||
|
let r = h.request("engine.execute", json!({"command": command, "params": params}), T);
|
||||||
|
assert_eq!(r["ok"], true, "{command}: {r}");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key(h: &mut Headless, k: Value) {
|
||||||
|
let r = h.request("ui.key", k.clone(), T);
|
||||||
|
assert_eq!(r["ok"], true, "{k}: {r}");
|
||||||
|
h.settle(SETTLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn copy_dialog(h: &mut Headless) {
|
||||||
|
key(h, json!({"key": "C", "cmd": true, "shift": true}));
|
||||||
|
assert!(matches!(h.app.ui.dialog, Some(Dialog::CopySettings { .. })), "dialog not open: {:?}", h.app.ui.dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copies light and colour from the first photo, selects the second and opens ⇧⌘V.
|
||||||
|
fn paste_dialog(h: &mut Headless, ids: [u64; 2]) {
|
||||||
|
exec(h, "develop.copy", json!({"groups": ["light", "color"]}));
|
||||||
|
exec(h, "library.select", json!({"ids": [ids[1]]}));
|
||||||
|
key(h, json!({"key": "V", "cmd": true, "shift": true}));
|
||||||
|
assert!(matches!(h.app.ui.dialog, Some(Dialog::PasteSettings { .. })), "dialog not open: {:?}", h.app.ui.dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exposure(h: &Headless, id: u64) -> f64 {
|
||||||
|
h.app.session.develop_of(PhotoId(id)).map(|d| d.light.exposure).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn return_copies_the_chosen_settings() {
|
||||||
|
let (mut h, _) = edited();
|
||||||
|
copy_dialog(&mut h);
|
||||||
|
assert!(h.app.session.clipboard.is_none());
|
||||||
|
key(&mut h, json!({"key": "Enter"}));
|
||||||
|
assert!(h.app.ui.dialog.is_none(), "Return closes the dialog");
|
||||||
|
assert!(h.app.session.clipboard.is_some(), "Return copies the settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escape_closes_copy_settings_without_copying() {
|
||||||
|
let (mut h, _) = edited();
|
||||||
|
copy_dialog(&mut h);
|
||||||
|
key(&mut h, json!({"key": "Escape"}));
|
||||||
|
assert!(h.app.ui.dialog.is_none(), "Esc closes the dialog");
|
||||||
|
assert!(h.app.session.clipboard.is_none(), "Esc copies nothing");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn return_pastes_into_the_selected_photos() {
|
||||||
|
let (mut h, ids) = edited();
|
||||||
|
paste_dialog(&mut h, ids);
|
||||||
|
key(&mut h, json!({"key": "Enter"}));
|
||||||
|
assert!(h.app.ui.dialog.is_none(), "Return closes the dialog");
|
||||||
|
assert_eq!(exposure(&h, ids[1]), 1.0, "Return pastes the settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escape_closes_paste_settings_without_pasting() {
|
||||||
|
let (mut h, ids) = edited();
|
||||||
|
paste_dialog(&mut h, ids);
|
||||||
|
key(&mut h, json!({"key": "Escape"}));
|
||||||
|
assert!(h.app.ui.dialog.is_none(), "Esc closes the dialog");
|
||||||
|
assert_eq!(exposure(&h, ids[1]), 0.0, "Esc pastes nothing");
|
||||||
|
}
|
||||||
+2
-2
@@ -572,8 +572,8 @@ visible menu bar yet: the menu model is only exposed through the control channel
|
|||||||
| MENU-EDIT-UNDO | Undo | P0 | ✅ | `cmd:edit.undo` | label does not name the step |
|
| MENU-EDIT-UNDO | Undo | P0 | ✅ | `cmd:edit.undo` | label does not name the step |
|
||||||
| MENU-EDIT-REDO | Redo | P0 | ✅ | `cmd:edit.redo` | |
|
| MENU-EDIT-REDO | Redo | P0 | ✅ | `cmd:edit.redo` | |
|
||||||
| MENU-EDIT-COPYPASTE | Copy / paste (edit settings) | P0 | ✅ | `cmd:develop.copy`, `cmd:develop.paste` | |
|
| MENU-EDIT-COPYPASTE | Copy / paste (edit settings) | P0 | ✅ | `cmd:develop.copy`, `cmd:develop.paste` | |
|
||||||
| MENU-EDIT-CHOOSECOPY | Choose Edit Settings to Copy… | P0 | ✅ | `cmd:dialog.copySettings` | |
|
| MENU-EDIT-CHOOSECOPY | Choose Edit Settings to Copy… | P0 | ✅ | `cmd:dialog.copySettings` | Return copies, Esc cancels |
|
||||||
| MENU-EDIT-PASTESELECTED | Paste Selected Settings | P0 | ✅ | `cmd:dialog.pasteSettings`, `cmd:develop.paste` (`groups`) | checklist prefilled with the copied groups; only copied groups can be pasted |
|
| MENU-EDIT-PASTESELECTED | Paste Selected Settings | P0 | ✅ | `cmd:dialog.pasteSettings`, `cmd:develop.paste` (`groups`) | checklist prefilled with the copied groups; only copied groups can be pasted; Return pastes, Esc cancels |
|
||||||
| MENU-EDIT-SELECTALL | Select All | P0 | ✅ | `cmd:library.selectAll` | |
|
| MENU-EDIT-SELECTALL | Select All | P0 | ✅ | `cmd:library.selectAll` | |
|
||||||
| MENU-EDIT-SELECTNONE | Select None | P0 | ✅ | `cmd:library.selectNone` | |
|
| MENU-EDIT-SELECTNONE | Select None | P0 | ✅ | `cmd:library.selectNone` | |
|
||||||
| MENU-EDIT-SELECTBY | Select by flag / rating | P1 | ✅ | `cmd:library.selectBy` | Edit → Select by: picks, rejects, unflagged, ★…★★★★★ and higher, unrated, colour labels; `add` extends the selection |
|
| MENU-EDIT-SELECTBY | Select by flag / rating | P1 | ✅ | `cmd:library.selectBy` | Edit → Select by: picks, rejects, unflagged, ★…★★★★★ and higher, unrated, colour labels; `add` extends the selection |
|
||||||
|
|||||||
@@ -157,6 +157,8 @@
|
|||||||
- Shortcuts are editable: Help ▸ Keyboard Shortcuts (⌘/) lists every command with a search box; click a shortcut and
|
- Shortcuts are editable: Help ▸ Keyboard Shortcuts (⌘/) lists every command with a search box; click a shortcut and
|
||||||
press the new keys (Esc cancels), × removes it, ↺ restores the original, Reset All undoes every change. A key that
|
press the new keys (Esc cancels), × removes it, ↺ restores the original, Reset All undoes every change. A key that
|
||||||
belonged to another command moves to the new one. Menus show the new keys; agents use `app.setShortcut`.
|
belonged to another command moves to the new one. Menus show the new keys; agents use `app.setShortcut`.
|
||||||
|
- Return presses OK in Choose Edit Settings to Copy (⇧⌘C) and Paste Selected Settings (⇧⌘V), and Esc cancels them
|
||||||
|
(issue #735).
|
||||||
|
|
||||||
### RAW decoding
|
### RAW decoding
|
||||||
- Olympus ORF raws get the starting look fitted to the camera's own JPEG too, instead of opening flat and grey (the
|
- Olympus ORF raws get the starting look fitted to the camera's own JPEG too, instead of opening flat and grey (the
|
||||||
|
|||||||
Reference in New Issue
Block a user