Merge pull request #196 from storytold/fix/191-saved-workspaces-menu
Window: saved workspaces are listed in Window ▸ Workspace (#191)
This commit is contained in:
commit
2bd6eeb647
|
|
@ -52,6 +52,8 @@ pub enum MenuNode {
|
|||
pub struct DynCtx<'a> {
|
||||
/// The current workspace (Window ▸ Assign Shortcut to "…" Workspace).
|
||||
pub workspace: Option<&'a str>,
|
||||
/// Workspaces the user saved (Save as New Workspace), listed in Window ▸ Workspace.
|
||||
pub saved_workspaces: &'a [String],
|
||||
}
|
||||
|
||||
fn file_name(p: &str) -> String {
|
||||
|
|
@ -70,8 +72,9 @@ fn holder_of(s: &Session, keys: &str) -> Option<String> {
|
|||
|
||||
/// The entries of a `@dynamic:<name>` node, and the disabled placeholder shown when there are
|
||||
/// none: `recentProjects`, `recentFootage`, `recentPresets`, `history` (undo steps, newest
|
||||
/// first), `view3dShortcuts`, `workspaceShortcuts` and `openViewers` (open compositions other
|
||||
/// than the active one), `scripts` (File ▸ Scripts) and `scriptPanels` (ScriptUI panels).
|
||||
/// first), `view3dShortcuts`, `workspaceShortcuts`, `savedWorkspaces` (Window ▸ Workspace) and
|
||||
/// `openViewers` (open compositions other than the active one), `scripts` (File ▸ Scripts) and
|
||||
/// `scriptPanels` (ScriptUI panels).
|
||||
pub fn dynamic(s: &Session, name: &str, cx: &DynCtx) -> (Vec<MenuEntry>, Option<&'static str>) {
|
||||
use serde_json::json;
|
||||
let indexed = |list: &[String], cmd: &str, stem: bool| -> Vec<MenuEntry> {
|
||||
|
|
@ -122,6 +125,7 @@ pub fn dynamic(s: &Session, name: &str, cx: &DynCtx) -> (Vec<MenuEntry>, Option<
|
|||
.collect(),
|
||||
None,
|
||||
),
|
||||
"savedWorkspaces" => (cx.saved_workspaces.iter().map(|w| dyn_entry(w.clone(), "window.workspace", json!({"name": w}))).collect(), None),
|
||||
"openViewers" => (
|
||||
s.state
|
||||
.open_comps
|
||||
|
|
@ -1151,6 +1155,7 @@ Window
|
|||
Paint | window.workspace {"name":"Paint"}
|
||||
Text | window.workspace {"name":"Text"}
|
||||
Undocked Panels | window.workspace {"name":"Undocked Panels"}
|
||||
@dynamic:savedWorkspaces
|
||||
---
|
||||
Reset to Saved Layout | window.resetWorkspace
|
||||
Save Changes to this Workspace | window.saveWorkspace
|
||||
|
|
|
|||
|
|
@ -386,10 +386,16 @@ impl EffectcraftApp {
|
|||
/// Built-in workspaces followed by the saved ones (Window ▸ Workspace ▸ Save as New Workspace).
|
||||
pub fn workspace_names(&self) -> Vec<String> {
|
||||
let mut v: Vec<String> = dock::WORKSPACES.iter().map(|s| s.to_string()).collect();
|
||||
v.extend(self.ui.saved_workspaces.keys().filter(|k| !dock::WORKSPACES.contains(&k.as_str())).cloned());
|
||||
v.extend(self.saved_workspace_names());
|
||||
v
|
||||
}
|
||||
|
||||
/// The workspaces the user saved under a new name (built-ins whose changes were saved are
|
||||
/// not repeated), in name order.
|
||||
pub fn saved_workspace_names(&self) -> Vec<String> {
|
||||
self.ui.saved_workspaces.keys().filter(|k| !dock::WORKSPACES.contains(&k.as_str())).cloned().collect()
|
||||
}
|
||||
|
||||
/// Show panel `p` (opening it in its usual place if it is closed), bring it to the front and
|
||||
/// give it the focus: Window ▸ <panel>.
|
||||
pub fn show_panel(&mut self, p: PanelKind) {
|
||||
|
|
|
|||
|
|
@ -1500,9 +1500,11 @@ fn menu_nodes(app: &mut EffectcraftApp, ui: &mut egui::Ui, nodes: &[MenuNode], c
|
|||
ui.separator();
|
||||
}
|
||||
MenuNode::Dynamic { name } => {
|
||||
// Recent projects / footage / presets, undo history, shortcut slots, viewers.
|
||||
// Recent projects / footage / presets, undo history, shortcut slots, viewers,
|
||||
// saved workspaces.
|
||||
let ws = app.ui.workspace.clone();
|
||||
let (entries, empty) = effectcraft_engine::menus::dynamic(&app.session, name, &dyn_ctx(&ws));
|
||||
let saved = app.saved_workspace_names();
|
||||
let (entries, empty) = effectcraft_engine::menus::dynamic(&app.session, name, &dyn_ctx(&ws, &saved));
|
||||
if entries.is_empty()
|
||||
&& let Some(e) = empty
|
||||
{
|
||||
|
|
@ -1515,7 +1517,7 @@ fn menu_nodes(app: &mut EffectcraftApp, ui: &mut egui::Ui, nodes: &[MenuNode], c
|
|||
"recentPresets" => app.session.prefs.recent_presets.get(i).cloned(),
|
||||
_ => None,
|
||||
};
|
||||
let r = ui.add_enabled(entry_enabled(app, e), egui::Button::new((gutter(false), e.label.as_str())));
|
||||
let r = ui.add_enabled(entry_enabled(app, e), egui::Button::new((gutter(entry_checked(app, e) == Some(true)), e.label.as_str())));
|
||||
let r = match &tip {
|
||||
Some(t) => r.on_hover_text(t),
|
||||
None => r,
|
||||
|
|
@ -1529,13 +1531,15 @@ fn menu_nodes(app: &mut EffectcraftApp, ui: &mut egui::Ui, nodes: &[MenuNode], c
|
|||
}
|
||||
MenuNode::Submenu { label, children } => {
|
||||
let ws = app.ui.workspace.clone();
|
||||
let shown = crate::i18n::submenu(app, label, effectcraft_engine::menus::submenu_label(&app.session, label, &dyn_ctx(&ws)));
|
||||
let shown = crate::i18n::submenu(app, label, effectcraft_engine::menus::submenu_label(&app.session, label, &dyn_ctx(&ws, &[])));
|
||||
ui.menu_button((gutter(false), shown.as_str()), |ui| {
|
||||
ui.set_min_width(if children.len() > 30 { 200.0 } else { 240.0 });
|
||||
// Long submenus (Blending Mode, effect categories) scroll instead of running
|
||||
// off the screen.
|
||||
// off the screen. Others show whole: egui sizes a new submenu from a default
|
||||
// 400 pt area, so without a minimum a longer one (Window ▸ Workspace) got
|
||||
// stuck at that height with its last entries scrolled out of view (#191).
|
||||
let max_h = ui.ctx().content_rect().height() - 40.0;
|
||||
egui::ScrollArea::vertical().max_height(max_h).show(ui, |ui| menu_nodes(app, ui, children, clicked));
|
||||
egui::ScrollArea::vertical().max_height(max_h).min_scrolled_height(max_h).show(ui, |ui| menu_nodes(app, ui, children, clicked));
|
||||
});
|
||||
}
|
||||
MenuNode::Item(e) => {
|
||||
|
|
@ -1548,9 +1552,9 @@ fn menu_nodes(app: &mut EffectcraftApp, ui: &mut egui::Ui, nodes: &[MenuNode], c
|
|||
}
|
||||
}
|
||||
|
||||
/// Frontend state for dynamic menus (the current workspace).
|
||||
pub(crate) fn dyn_ctx(workspace: &str) -> effectcraft_engine::menus::DynCtx<'_> {
|
||||
effectcraft_engine::menus::DynCtx { workspace: Some(workspace) }
|
||||
/// Frontend state for dynamic menus (the current workspace and the saved ones).
|
||||
pub(crate) fn dyn_ctx<'a>(workspace: &'a str, saved_workspaces: &'a [String]) -> effectcraft_engine::menus::DynCtx<'a> {
|
||||
effectcraft_engine::menus::DynCtx { workspace: Some(workspace), saved_workspaces }
|
||||
}
|
||||
|
||||
/// The check-mark column every menu row reserves (like macOS / After Effects menus).
|
||||
|
|
|
|||
|
|
@ -135,8 +135,10 @@ pub fn build(app: &EffectcraftApp) -> NativeMenu {
|
|||
match n {
|
||||
MenuNode::Separator => out.push(NativeNode::Separator),
|
||||
MenuNode::Dynamic { name } => {
|
||||
// Recent projects / footage / presets, undo history, shortcut slots, viewers.
|
||||
let (entries, empty) = effectcraft_engine::menus::dynamic(&app.session, name, &crate::menus::dyn_ctx(&app.ui.workspace));
|
||||
// Recent projects / footage / presets, undo history, shortcut slots, viewers,
|
||||
// saved workspaces.
|
||||
let saved = app.saved_workspace_names();
|
||||
let (entries, empty) = effectcraft_engine::menus::dynamic(&app.session, name, &crate::menus::dyn_ctx(&app.ui.workspace, &saved));
|
||||
if entries.is_empty()
|
||||
&& let Some(e) = empty
|
||||
{
|
||||
|
|
@ -160,14 +162,14 @@ pub fn build(app: &EffectcraftApp) -> NativeMenu {
|
|||
shortcut: None,
|
||||
accelerator: None,
|
||||
enabled: crate::menus::entry_enabled(app, e),
|
||||
checked: None,
|
||||
checked: crate::menus::entry_checked(app, e),
|
||||
}));
|
||||
}
|
||||
}
|
||||
MenuNode::Submenu { label, children } => {
|
||||
let mut kids = vec![];
|
||||
rec(app, children, &id, &mut kids);
|
||||
let shown = effectcraft_engine::menus::submenu_label(&app.session, label, &crate::menus::dyn_ctx(&app.ui.workspace));
|
||||
let shown = effectcraft_engine::menus::submenu_label(&app.session, label, &crate::menus::dyn_ctx(&app.ui.workspace, &[]));
|
||||
out.push(NativeNode::Submenu { label: crate::i18n::submenu(app, label, shown), children: kids });
|
||||
}
|
||||
MenuNode::Item(e) => {
|
||||
|
|
@ -394,6 +396,19 @@ mod tests {
|
|||
assert!(v["menus"].as_array().is_some_and(|a| a.len() >= 9));
|
||||
}
|
||||
|
||||
/// Issue #191: saved workspaces are in the native Window ▸ Workspace menu, checked while current.
|
||||
#[test]
|
||||
fn saved_workspaces_are_in_the_native_workspace_menu() {
|
||||
let mut app = app();
|
||||
let ctx = egui::Context::default();
|
||||
crate::menus::invoke(&mut app, &ctx, "window.saveWorkspaceAs", json!({"name": "My Layout"})).unwrap();
|
||||
let entry = |app: &EffectcraftApp| build(app).items().into_iter().find(|i| i.label == "My Layout").cloned();
|
||||
let it = entry(&app).expect("the saved workspace is listed");
|
||||
assert_eq!((it.command.as_str(), &it.params, it.checked), ("window.workspace", &json!({"name": "My Layout"}), Some(true)));
|
||||
crate::menus::invoke(&mut app, &ctx, "window.workspace", json!({"name": "Default"})).unwrap();
|
||||
assert_eq!(entry(&app).and_then(|i| i.checked), Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hide_items_are_predefined_on_macos() {
|
||||
let spec = build(&app());
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ use effectcraft_ui_egui::EffectcraftApp;
|
|||
use effectcraft_ui_egui::dock::PanelKind;
|
||||
use egui::{Event, Pos2, pos2};
|
||||
use egui_kittest::Harness;
|
||||
use egui_kittest::kittest::Queryable;
|
||||
use serde_json::json;
|
||||
|
||||
fn harness() -> Harness<'static, EffectcraftApp> {
|
||||
|
|
@ -103,6 +104,55 @@ fn progress_panel_cancels_a_job() {
|
|||
assert_eq!(h.state().session.job_log.last().unwrap().status, "cancelled");
|
||||
}
|
||||
|
||||
fn click_at(h: &mut Harness<'_, EffectcraftApp>, p: Pos2) {
|
||||
h.input_mut().events.push(Event::PointerMoved(p));
|
||||
h.input_mut().events.push(Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() });
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() });
|
||||
h.run_steps(2);
|
||||
}
|
||||
|
||||
fn hover(h: &mut Harness<'_, EffectcraftApp>, p: Pos2) {
|
||||
h.input_mut().events.push(Event::PointerMoved(p));
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
/// Issue #191: a workspace saved with Save as New Workspace is listed in Window ▸ Workspace
|
||||
/// (below the built-ins, where the submenu used to be cut off) and choosing it there brings its
|
||||
/// layout back.
|
||||
#[test]
|
||||
fn saved_workspace_is_listed_in_the_workspace_menu() {
|
||||
let mut h = harness();
|
||||
let ctx = h.ctx.clone();
|
||||
effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "window.workspace", json!({"name": "Minimal"})).unwrap();
|
||||
effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "window.saveWorkspaceAs", json!({"name": "My Layout"})).unwrap();
|
||||
h.run_steps(3);
|
||||
let saved = h.state().ui.dock.clone();
|
||||
assert_eq!(h.state().saved_workspace_names(), ["My Layout"]);
|
||||
let cx = effectcraft_engine::menus::DynCtx { workspace: Some("My Layout"), saved_workspaces: &["My Layout".to_string()] };
|
||||
let (entries, _) = effectcraft_engine::menus::dynamic(&h.state().session, "savedWorkspaces", &cx);
|
||||
assert_eq!(entries.len(), 1);
|
||||
assert_eq!((entries[0].label.as_str(), entries[0].command.as_str(), &entries[0].params), ("My Layout", "window.workspace", &json!({"name": "My Layout"})));
|
||||
// Leave it, then pick it from the in-window menu bar.
|
||||
effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "window.workspace", json!({"name": "Default"})).unwrap();
|
||||
h.run_steps(3);
|
||||
assert_ne!(h.state().ui.dock, saved);
|
||||
click(&mut h, "menu.Window");
|
||||
let ws = h.query_by_label(" Workspace ⏵").expect("Window ▸ Workspace").rect();
|
||||
hover(&mut h, ws.center());
|
||||
let entry = h.state().auto.find("menu.savedWorkspaces.0").expect("the saved workspace is listed").clone();
|
||||
assert_eq!(entry.label, "My Layout");
|
||||
let at = h.query_by_label(" My Layout").expect("the saved workspace entry").rect().center();
|
||||
// Into the submenu, then down it (as a pointer moves).
|
||||
hover(&mut h, pos2(at.x, ws.center().y));
|
||||
for k in 1..=10 {
|
||||
hover(&mut h, pos2(at.x, ws.center().y + (at.y - ws.center().y) * k as f32 / 10.0));
|
||||
}
|
||||
click_at(&mut h, at);
|
||||
assert_eq!(h.state().ui.workspace, "My Layout");
|
||||
assert_eq!(h.state().ui.dock, saved, "its layout came back");
|
||||
}
|
||||
|
||||
/// Headless look at the panels (wgpu offscreen; needs a GPU adapter). Run with
|
||||
/// `PANELS_SNAPSHOT=/abs/dir cargo test -p effectcraft-ui-egui --test ui_panels -- --ignored`.
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in New Issue