mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-30 13:16:56 -06:00
feat(ui): Window menu in InDesign's order; Window > Workspace with Essentials Classic
LYH Studio episode 6 starts with "Window > Workspace > Essential Classic" and opens panels with "Window > Color > Gradient & Swatches". DesignCraft's Window menu had no Workspace or Color submenu (panels sat in one "Panels" list) and no Essentials Classic workspace. - Window menu: Arrange, Workspace, then the panels alphabetically with InDesign's groups (Color: Color, Gradient, Swatches; Editorial; Interactive; Object & Layout; Output; Styles; Type & Tables; Utilities). Every panel stays reachable; Next/Previous Document moved into Arrange. - Window > Workspace: the built-in workspaces as "[Essentials]", the current one checked, Reset <current>, New Workspace. The app bar's switcher uses the same list and order. - Essentials Classic: the Control panel, no Properties stack, and an icon column of named panel groups ([Pages, Layers, Links] [Stroke, Color] [Swatches] [Effects] [Paragraph] [Text Wrap] [Character Styles, Paragraph Styles] [Gradient]); Window > Pages opens Pages from its group. Saved workspaces keep the groups. - Flyouts and floating panels name Pages, Layers and Properties. - Translations for Editorial, Object & Layout, Styles and Essentials Classic. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e862e0a613
commit
0a34316b82
@@ -129,6 +129,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
|
||||
|
||||
Newest first.
|
||||
|
||||
- **2026-10-13** · The Window menu follows InDesign's order (Arrange, Workspace, then the panels alphabetically with Color, Editorial, Object & Layout, Styles… submenus), and Window › Workspace lists the workspaces as InDesign does, now with Essentials Classic: the Control panel and an icon column of named panel groups. No readiness percentage change.
|
||||
- **2026-10-12** · macOS Dock Quit uses the same unsaved-document prompts as window and menu Quit. Repeated requests keep the current answers; Cancel retains every tab and unsaved recovery checkpoint, and completed Save/Don't Save answers exit through the ordinary event loop. No readiness percentage change.
|
||||
- **2026-10-12** · Orderly native exit writes the latest recovery state after finalizing current quit approvals; incomplete or stale plans keep their documents, and Undo to the saved state clears stale recovery. Unavailable or failed recovery is reported. Cancelable macOS Dock Quit remains open (#544). No readiness percentage change.
|
||||
- **2026-10-12** · Recovered documents keep a checkpoint until saved, closed or reverted, including a second crash before autosave. Restarted document IDs and explicit recovery folders preserve other snapshots; failed republishing retains the original. No readiness percentage change.
|
||||
|
||||
@@ -63,19 +63,7 @@ pub fn app_bar(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
crate::rtl::widget(ui, egui::RichText::new(format!("{shown} ▾")).font(semibold(11.5)).color(t.text)),
|
||||
|ui| {
|
||||
let customs: Vec<String> = app.ui.custom_workspaces.iter().map(|w| w.name.clone()).collect();
|
||||
for w in [
|
||||
"Essentials",
|
||||
"Advanced",
|
||||
"Book",
|
||||
"Digital Publishing",
|
||||
"Interactive for PDF",
|
||||
"Printing and Proofing",
|
||||
"Typography",
|
||||
]
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.chain(customs.iter().cloned())
|
||||
{
|
||||
for w in crate::menus::WORKSPACES.iter().map(|w| w.to_string()).chain(customs.iter().cloned()) {
|
||||
if ui
|
||||
.selectable_label(w == current, crate::rtl::widget(ui, crate::i18n::workspace_name(&app.ui.language, &w)))
|
||||
.clicked()
|
||||
|
||||
+66
-19
@@ -113,30 +113,77 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
},
|
||||
);
|
||||
}
|
||||
// Collapsed icon strip (37 pt) to the left of the panel stack.
|
||||
egui::Panel::right("icon_column").exact_size(37.0).resizable(false).frame(egui::Frame::NONE.fill(t.panel)).show(ui, |ui| {
|
||||
// Collapsed icon strip (37 pt) to the left of the panel stack; a workspace's iconic groups
|
||||
// (Essentials Classic) show their names.
|
||||
let groups = app.ui.icon_groups.clone();
|
||||
egui::Panel::right("icon_column").exact_size(icon_column_width(app)).resizable(false).frame(egui::Frame::NONE.fill(t.panel)).show(ui, |ui| {
|
||||
dock_header(ui, &t, "«");
|
||||
ui.add_space(4.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
for (id, label, icon) in ICON_PANELS {
|
||||
let open = app.ui.open_panel.as_deref() == Some(*id);
|
||||
let floats = app.ui.floating.iter().any(|(p, _)| p == id);
|
||||
let response =
|
||||
icons::button(ui, icon, 28.0, open || floats, crate::i18n::tr(&app.ui.language, label)).interact(Sense::click_and_drag());
|
||||
if response.clicked() {
|
||||
if floats {
|
||||
ui.ctx().move_to_top(egui::LayerId::new(egui::Order::Middle, egui::Id::new(("floating_panel", *id))));
|
||||
} else {
|
||||
app.ui.open_panel = if open { None } else { Some(id.to_string()) };
|
||||
match &groups {
|
||||
Some(groups) => {
|
||||
for (i, group) in groups.iter().enumerate() {
|
||||
if i > 0 {
|
||||
let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 7.0), Sense::hover());
|
||||
ui.painter().line_segment([r.left_center(), r.right_center()], Stroke::new(1.0, t.border));
|
||||
}
|
||||
for (id, label, icon) in group.iter().filter_map(|id| panel_entry(id)) {
|
||||
let on = app.ui.open_panel.as_deref() == Some(id) || app.ui.floating.iter().any(|(p, _)| p == id);
|
||||
let (r, response) = ui.allocate_exact_size(vec2(ui.available_width(), 26.0), Sense::click_and_drag());
|
||||
if on {
|
||||
ui.painter().rect_filled(r.shrink2(vec2(3.0, 1.0)), 3.0, t.tool_active);
|
||||
} else if response.hovered() {
|
||||
ui.painter().rect_filled(r.shrink2(vec2(3.0, 1.0)), 3.0, t.hover);
|
||||
}
|
||||
icons::paint(ui.painter(), egui::Rect::from_min_size(r.min + vec2(8.0, 4.0), vec2(18.0, 18.0)), icon, t.icon);
|
||||
crate::rtl::paint(
|
||||
ui.painter(),
|
||||
r.left_center() + vec2(32.0, 0.0),
|
||||
egui::Align2::LEFT_CENTER,
|
||||
crate::i18n::tr(&app.ui.language, label),
|
||||
semibold(11.5),
|
||||
t.text,
|
||||
);
|
||||
icon_clicked(app, ui, id, &response);
|
||||
}
|
||||
}
|
||||
crate::panel_docking::legacy_tab(app, ui, id, &response);
|
||||
ui.add_space(1.0);
|
||||
}
|
||||
});
|
||||
None => {
|
||||
ui.vertical_centered(|ui| {
|
||||
for (id, label, icon) in ICON_PANELS {
|
||||
let on = app.ui.open_panel.as_deref() == Some(*id) || app.ui.floating.iter().any(|(p, _)| p == id);
|
||||
let response = icons::button(ui, icon, 28.0, on, crate::i18n::tr(&app.ui.language, label)).interact(Sense::click_and_drag());
|
||||
icon_clicked(app, ui, id, &response);
|
||||
ui.add_space(1.0);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Width of the icon column: icons only, or icons with names for a workspace's groups.
|
||||
fn icon_column_width(app: &DesignApp) -> f32 {
|
||||
if app.ui.icon_groups.is_some() { 150.0 } else { 37.0 }
|
||||
}
|
||||
|
||||
/// (id, label, icon) of any dock panel.
|
||||
fn panel_entry(id: &str) -> Option<(&'static str, &'static str, &'static str)> {
|
||||
DOCK_TABS.iter().chain(ICON_PANELS).find(|p| p.0 == id).copied()
|
||||
}
|
||||
|
||||
/// A click on a panel's icon: raise it when it floats, else open or close its flyout.
|
||||
fn icon_clicked(app: &mut DesignApp, ui: &mut egui::Ui, id: &str, response: &egui::Response) {
|
||||
if response.clicked() {
|
||||
if app.ui.floating.iter().any(|(p, _)| p == id) {
|
||||
ui.ctx().move_to_top(egui::LayerId::new(egui::Order::Middle, egui::Id::new(("floating_panel", id))));
|
||||
} else {
|
||||
let open = app.ui.open_panel.as_deref() == Some(id);
|
||||
app.ui.open_panel = if open { None } else { Some(id.to_string()) };
|
||||
}
|
||||
}
|
||||
crate::panel_docking::legacy_tab(app, ui, id, response);
|
||||
}
|
||||
|
||||
/// 11 pt dock header strip with a collapse chevron.
|
||||
fn dock_header(ui: &mut egui::Ui, t: &Tokens, chevron: &str) {
|
||||
let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 11.0), Sense::hover());
|
||||
@@ -152,10 +199,10 @@ pub fn flyout(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
}
|
||||
let Some(id) = app.ui.open_panel.clone() else { return };
|
||||
let t = Tokens::get(ctx);
|
||||
let label = crate::i18n::tr(&app.ui.language, ICON_PANELS.iter().find(|p| p.0 == id).map(|p| p.1).unwrap_or("Panel")).to_owned();
|
||||
let label = crate::i18n::tr(&app.ui.language, panel_entry(&id).map(|p| p.1).unwrap_or("Panel")).to_owned();
|
||||
let screen = ctx.content_rect();
|
||||
let dock_w = if app.ui.dock_expanded { ctx.memory(|m| m.area_rect(egui::Id::new("dock")).map(|r| r.width())).unwrap_or(280.0) } else { 0.0 };
|
||||
let pos = egui::pos2(screen.max.x - 37.0 - dock_w - 262.0, 120.0);
|
||||
let pos = egui::pos2(screen.max.x - icon_column_width(app) - dock_w - 262.0, 120.0);
|
||||
let mut open = true;
|
||||
egui::Area::new(egui::Id::new("panel_flyout")).order(egui::Order::Foreground).fixed_pos(pos).show(ctx, |ui| {
|
||||
egui::Frame::popup(ui.style()).fill(t.panel).inner_margin(egui::Margin::same(0)).show(ui, |ui| {
|
||||
@@ -262,7 +309,7 @@ pub fn floating(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
}
|
||||
let t = Tokens::get(ctx);
|
||||
for (id, at) in app.ui.floating.clone() {
|
||||
let label = crate::i18n::tr(&app.ui.language, ICON_PANELS.iter().find(|p| p.0 == id).map(|p| p.1).unwrap_or("Panel")).to_owned();
|
||||
let label = crate::i18n::tr(&app.ui.language, panel_entry(&id).map(|p| p.1).unwrap_or("Panel")).to_owned();
|
||||
let mut open = true;
|
||||
let mut dock = false;
|
||||
let r = egui::Window::new(label.clone())
|
||||
|
||||
@@ -55,6 +55,10 @@ const TABLE: &[(&str, [&str; 5])] = &[
|
||||
("Cut", ["Ausschneiden", "Couper", "Cortar", "カット", "剪切"]),
|
||||
("Copy", ["Kopieren", "Copier", "Copiar", "コピー", "复制"]),
|
||||
("Paste", ["Einfügen", "Coller", "Pegar", "ペースト", "粘贴"]),
|
||||
("Editorial", ["Redaktion", "Éditorial", "Editorial", "エディトリアル", "编辑"]),
|
||||
("Object & Layout", ["Objekt und Layout", "Objet et mise en page", "Objeto y diseño", "オブジェクト&レイアウト", "对象和版面"]),
|
||||
("Styles", ["Formate", "Styles", "Estilos", "スタイル", "样式"]),
|
||||
("Essentials Classic", ["Essentials Classic", "Essentials Classic", "Essentials Classic", "Essentials Classic", "基本功能(经典)"]),
|
||||
("Reverse Path", ["Pfadrichtung umkehren", "Inverser le tracé", "Invertir trazado", "パスの方向反転", "反转路径"]),
|
||||
("Set Anchor", ["Ankerpunkt festlegen", "Définir le point d’ancrage", "Definir punto de ancla", "アンカーポイントを設定", "设置锚点"]),
|
||||
("Clear", ["Löschen", "Effacer", "Borrar", "消去", "清除"]),
|
||||
@@ -2921,7 +2925,14 @@ pub fn style_name<'a>(lang: &str, name: &'a str) -> &'a str {
|
||||
/// Built-in workspace captions; custom workspace names remain unchanged.
|
||||
pub fn workspace_name<'a>(lang: &str, name: &'a str) -> &'a str {
|
||||
match name {
|
||||
"Essentials" | "Advanced" | "Book" | "Digital Publishing" | "Interactive for PDF" | "Printing and Proofing" | "Typography" => tr(lang, name),
|
||||
"Essentials"
|
||||
| "Essentials Classic"
|
||||
| "Advanced"
|
||||
| "Book"
|
||||
| "Digital Publishing"
|
||||
| "Interactive for PDF"
|
||||
| "Printing and Proofing"
|
||||
| "Typography" => tr(lang, name),
|
||||
_ => name,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Cut", "قص"),
|
||||
("Copy", "نسخ"),
|
||||
("Paste", "لصق"),
|
||||
("Editorial", "التحرير"),
|
||||
("Object & Layout", "الكائن والتخطيط"),
|
||||
("Styles", "الأنماط"),
|
||||
("Essentials Classic", "الأساسيات الكلاسيكية"),
|
||||
("Reverse Path", "عكس المسار"),
|
||||
("Set Anchor", "تعيين نقطة الارتساء"),
|
||||
("Clear", "مسح"),
|
||||
|
||||
@@ -28,6 +28,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Cut", "Taglia"),
|
||||
("Copy", "Copia"),
|
||||
("Paste", "Incolla"),
|
||||
("Editorial", "Editoriale"),
|
||||
("Object & Layout", "Oggetto e layout"),
|
||||
("Styles", "Stili"),
|
||||
("Essentials Classic", "Essenziali classico"),
|
||||
("Reverse Path", "Inverti tracciato"),
|
||||
("Set Anchor", "Imposta ancoraggio"),
|
||||
("Clear", "Cancella"),
|
||||
|
||||
@@ -30,6 +30,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Cut", "カット"),
|
||||
("Copy", "コピー"),
|
||||
("Paste", "ペースト"),
|
||||
("Editorial", "エディトリアル"),
|
||||
("Object & Layout", "オブジェクト&レイアウト"),
|
||||
("Styles", "スタイル"),
|
||||
("Essentials Classic", "基本(クラシック)"),
|
||||
("Reverse Path", "パスの方向反転"),
|
||||
("Set Anchor", "アンカーポイントを設定"),
|
||||
("Clear", "消去"),
|
||||
|
||||
@@ -31,6 +31,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Cut", "Recortar"),
|
||||
("Copy", "Copiar"),
|
||||
("Paste", "Colar"),
|
||||
("Editorial", "Editorial"),
|
||||
("Object & Layout", "Objeto e layout"),
|
||||
("Styles", "Estilos"),
|
||||
("Essentials Classic", "Essencial clássico"),
|
||||
("Reverse Path", "Inverter demarcador"),
|
||||
("Set Anchor", "Definir ponto de ancoragem"),
|
||||
("Clear", "Limpar"),
|
||||
|
||||
@@ -31,6 +31,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Cut", "Вирізати"),
|
||||
("Copy", "Копіювати"),
|
||||
("Paste", "Вставити"),
|
||||
("Editorial", "Редакція"),
|
||||
("Object & Layout", "Об’єкт і макет"),
|
||||
("Styles", "Стилі"),
|
||||
("Essentials Classic", "Основне (класичне)"),
|
||||
("Reverse Path", "Розвернути контур"),
|
||||
("Set Anchor", "Задати прив’язку"),
|
||||
("Clear", "Очистити"),
|
||||
|
||||
@@ -104,6 +104,9 @@ pub struct SavedWorkspace {
|
||||
pub docking: Option<craft_ui::docking::Layout<String>>,
|
||||
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
|
||||
pub docking_hidden: std::collections::BTreeMap<String, craft_ui::docking::Location<String>>,
|
||||
/// The workspace's own icon column (see [`UiState::icon_groups`]).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub icon_groups: Option<Vec<Vec<String>>>,
|
||||
}
|
||||
|
||||
/// A missing on-by-default bool stays on. `bool::default` is false.
|
||||
@@ -239,6 +242,10 @@ pub struct UiState {
|
||||
#[serde(skip)]
|
||||
pub docking_opened: Option<String>,
|
||||
pub dock_expanded: bool,
|
||||
/// A workspace's iconic panel groups (Essentials Classic), shown with their names in the icon
|
||||
/// column instead of every panel; `None` is the full icon column.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub icon_groups: Option<Vec<Vec<String>>>,
|
||||
pub units: Unit,
|
||||
pub workspace: String,
|
||||
/// Window › Workspace › New Workspace: saved panel arrangements.
|
||||
@@ -341,6 +348,7 @@ impl Default for UiState {
|
||||
docking_raise: None,
|
||||
docking_opened: None,
|
||||
dock_expanded: true,
|
||||
icon_groups: None,
|
||||
units: Unit::Picas,
|
||||
workspace: "Essentials".into(),
|
||||
custom_workspaces: Vec::new(),
|
||||
|
||||
+174
-38
@@ -190,7 +190,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
|
||||
"window.workspace",
|
||||
"Workspace",
|
||||
None,
|
||||
"{name: Essentials|Advanced|Book|Digital Publishing|Interactive for PDF|Printing and Proofing|Typography}",
|
||||
"{name: Advanced|Book|Digital Publishing|Essentials|Essentials Classic|Interactive for PDF|Printing and Proofing|Typography or a saved workspace}",
|
||||
),
|
||||
("window.newWorkspace", "New Workspace…", None, "{name} — saves the current bars and panel arrangement"),
|
||||
("window.deleteWorkspace", "Delete Workspace…", None, "{name}"),
|
||||
@@ -682,50 +682,77 @@ pub const MENUS: &[(&str, &[&str])] = &[
|
||||
),
|
||||
(
|
||||
"Window",
|
||||
// InDesign's order: Arrange, Workspace, then the panels alphabetically with their groups.
|
||||
&[
|
||||
">Arrange",
|
||||
"ui:window.newWindow",
|
||||
"ui:window.split",
|
||||
"-",
|
||||
"ui:window.nextDocument",
|
||||
"ui:window.previousDocument",
|
||||
"<",
|
||||
"ui:window.workspace",
|
||||
"-",
|
||||
"ui:window.panel|Articles|{\"panel\": \"articles\"}",
|
||||
">Color",
|
||||
"ui:window.panel|Color|{\"panel\": \"color\"}",
|
||||
"ui:window.panel|Gradient|{\"panel\": \"gradient\"}",
|
||||
"ui:window.panel|Swatches|{\"panel\": \"swatches\"}",
|
||||
"<",
|
||||
"ui:window.taskBar",
|
||||
"ui:window.controlBar",
|
||||
">Editorial",
|
||||
"ui:window.panel|Notes|{\"panel\": \"notes\"}",
|
||||
"ui:window.panel|Track Changes|{\"panel\": \"trackChanges\"}",
|
||||
"<",
|
||||
"ui:window.panel|Effects|{\"panel\": \"effects\"}",
|
||||
"ui:window.panel|Info|{\"panel\": \"info\"}",
|
||||
">Interactive",
|
||||
"ui:window.panel|Bookmarks|{\"panel\": \"bookmarks\"}",
|
||||
"ui:window.panel|Buttons and Forms|{\"panel\": \"buttons\"}",
|
||||
"ui:window.panel|Hyperlinks|{\"panel\": \"hyperlinks\"}",
|
||||
"ui:window.panel|Liquid Layout|{\"panel\": \"liquid\"}",
|
||||
"ui:window.panel|Media|{\"panel\": \"media\"}",
|
||||
"ui:window.panel|Object States|{\"panel\": \"states\"}",
|
||||
"ui:window.panel|Page Transitions|{\"panel\": \"transitions\"}",
|
||||
"<",
|
||||
"ui:window.panel|Layers|{\"panel\": \"layers\"}",
|
||||
"ui:window.panel|Links|{\"panel\": \"links\"}",
|
||||
">Object & Layout",
|
||||
"ui:window.panel|Align|{\"panel\": \"align\"}",
|
||||
"ui:window.panel|Pathfinder|{\"panel\": \"pathfinder\"}",
|
||||
"<",
|
||||
">Output",
|
||||
"ui:window.panel|Attributes|{\"panel\": \"attributes\"}",
|
||||
"ui:window.panel|Preflight|{\"panel\": \"preflight\"}",
|
||||
"<",
|
||||
"ui:window.panel|Pages|{\"panel\": \"pages\"}",
|
||||
"ui:window.panel|Properties|{\"panel\": \"properties\"}",
|
||||
"ui:window.panel|Stroke|{\"panel\": \"stroke\"}",
|
||||
">Styles",
|
||||
"ui:window.panel|Character Styles|{\"panel\": \"characterStyles\"}",
|
||||
"ui:window.panel|Paragraph Styles|{\"panel\": \"paragraphStyles\"}",
|
||||
"<",
|
||||
"ui:window.panel|Text Wrap|{\"panel\": \"textWrap\"}",
|
||||
">Type & Tables",
|
||||
"ui:window.panel|Character|{\"panel\": \"character\"}",
|
||||
"ui:window.panel|Conditional Text|{\"panel\": \"conditions\"}",
|
||||
"ui:window.panel|Glyphs|{\"panel\": \"glyphs\"}",
|
||||
"ui:window.panel|Paragraph|{\"panel\": \"paragraph\"}",
|
||||
"ui:window.panel|Table|{\"panel\": \"table\"}",
|
||||
"<",
|
||||
">Utilities",
|
||||
"ui:window.panel|Book|{\"panel\": \"book\"}",
|
||||
"ui:window.panel|Data Merge|{\"panel\": \"dataMerge\"}",
|
||||
"ui:window.panel|Library|{\"panel\": \"library\"}",
|
||||
"ui:window.panel|Scripts|{\"panel\": \"scripts\"}",
|
||||
"ui:window.panel|Tags|{\"panel\": \"tags\"}",
|
||||
"<",
|
||||
"-",
|
||||
"ui:window.hidePanels",
|
||||
"ui:window.hidePanelsExceptTools",
|
||||
"ui:window.nextDocument",
|
||||
"ui:window.previousDocument",
|
||||
"-",
|
||||
"ui:window.controlBar",
|
||||
"ui:window.taskBar",
|
||||
"ui:window.toolsDoubleColumn",
|
||||
"-",
|
||||
"ui:window.panel",
|
||||
"-",
|
||||
">Type & Tables",
|
||||
"ui:window.panel|Conditional Text|{\"panel\": \"conditions\"}",
|
||||
"ui:window.panel|Notes|{\"panel\": \"notes\"}",
|
||||
"ui:window.panel|Glyphs|{\"panel\": \"glyphs\"}",
|
||||
"<",
|
||||
">Interactive",
|
||||
"ui:window.panel|Hyperlinks|{\"panel\": \"hyperlinks\"}",
|
||||
"ui:window.panel|Bookmarks|{\"panel\": \"bookmarks\"}",
|
||||
"ui:window.panel|Articles|{\"panel\": \"articles\"}",
|
||||
"ui:window.panel|Object States|{\"panel\": \"states\"}",
|
||||
"ui:window.panel|Buttons and Forms|{\"panel\": \"buttons\"}",
|
||||
"ui:window.panel|Media|{\"panel\": \"media\"}",
|
||||
"ui:window.panel|Page Transitions|{\"panel\": \"transitions\"}",
|
||||
"ui:window.panel|Track Changes|{\"panel\": \"trackChanges\"}",
|
||||
"ui:window.panel|Scripts|{\"panel\": \"scripts\"}",
|
||||
"ui:window.panel|Liquid Layout|{\"panel\": \"liquid\"}",
|
||||
"ui:window.panel|Tags|{\"panel\": \"tags\"}",
|
||||
"ui:window.panel|Book|{\"panel\": \"book\"}",
|
||||
"<",
|
||||
">Utilities",
|
||||
"ui:window.panel|Data Merge|{\"panel\":\"dataMerge\"}",
|
||||
"<",
|
||||
">Output",
|
||||
"ui:window.panel|Attributes|{\"panel\": \"attributes\"}",
|
||||
"cmd:preflight.run",
|
||||
"<",
|
||||
"-",
|
||||
"ui:window.brightness",
|
||||
],
|
||||
),
|
||||
@@ -746,6 +773,22 @@ pub const MENUS: &[(&str, &[&str])] = &[
|
||||
),
|
||||
];
|
||||
|
||||
/// The built-in workspaces, in InDesign's order.
|
||||
pub const WORKSPACES: &[&str] =
|
||||
&["Advanced", "Book", "Digital Publishing", "Essentials", "Essentials Classic", "Interactive for PDF", "Printing and Proofing", "Typography"];
|
||||
|
||||
/// Essentials Classic's icon column: InDesign's iconic panel groups, top to bottom.
|
||||
pub const CLASSIC_GROUPS: &[&[&str]] = &[
|
||||
&["pages", "layers", "links"],
|
||||
&["stroke", "color"],
|
||||
&["swatches"],
|
||||
&["effects"],
|
||||
&["paragraph"],
|
||||
&["textWrap"],
|
||||
&["characterStyles", "paragraphStyles"],
|
||||
&["gradient"],
|
||||
];
|
||||
|
||||
pub fn ui_label(id: &str) -> Option<(&'static str, Option<&'static str>)> {
|
||||
UI_COMMANDS.iter().find(|c| c.0 == id).map(|c| (c.1, c.2))
|
||||
}
|
||||
@@ -1260,7 +1303,9 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
|
||||
}
|
||||
"window.panel" => {
|
||||
let panel = p.get("panel").and_then(Value::as_str).unwrap_or("properties").to_string();
|
||||
if crate::dock::DOCK_TABS.iter().any(|(id, _, _)| *id == panel) {
|
||||
// A panel in the workspace's icon groups opens from there (Essentials Classic's Pages).
|
||||
let iconic = app.ui.icon_groups.as_ref().is_some_and(|g| g.iter().flatten().any(|p| *p == panel));
|
||||
if !iconic && crate::dock::DOCK_TABS.iter().any(|(id, _, _)| *id == panel) {
|
||||
app.ui.dock_tab = panel;
|
||||
app.ui.dock_expanded = true;
|
||||
app.ui.open_panel = None;
|
||||
@@ -1437,6 +1482,7 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
|
||||
floating: u.floating.clone(),
|
||||
docking: u.docking.clone(),
|
||||
docking_hidden: u.docking_hidden.clone(),
|
||||
icon_groups: u.icon_groups.clone(),
|
||||
};
|
||||
app.ui.custom_workspaces.retain(|x| x.name != name);
|
||||
app.ui.custom_workspaces.push(w);
|
||||
@@ -1475,6 +1521,7 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
|
||||
app.ui.floating = w.floating;
|
||||
app.ui.docking = w.docking;
|
||||
app.ui.docking_hidden = w.docking_hidden;
|
||||
app.ui.icon_groups = w.icon_groups;
|
||||
app.ui.workspace = w.name;
|
||||
return Some(Ok(Value::Null));
|
||||
}
|
||||
@@ -1484,7 +1531,12 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
|
||||
}
|
||||
app.ui.docking = None;
|
||||
app.ui.docking_hidden.clear();
|
||||
app.ui.control_bar = matches!(name, "Advanced" | "Typography" | "Printing and Proofing" | "Book");
|
||||
app.ui.control_bar = matches!(name, "Advanced" | "Typography" | "Printing and Proofing" | "Book" | "Essentials Classic");
|
||||
// Essentials Classic: the Control panel and an icon column of grouped panels, no
|
||||
// Properties stack.
|
||||
let classic = name == "Essentials Classic";
|
||||
app.ui.icon_groups = classic.then(|| CLASSIC_GROUPS.iter().map(|g| g.iter().map(|p| p.to_string()).collect()).collect());
|
||||
app.ui.dock_expanded = !classic;
|
||||
let dock_tab = match name {
|
||||
"Typography" => Some("properties"),
|
||||
"Interactive for PDF" | "Digital Publishing" => Some("pages"),
|
||||
@@ -1753,6 +1805,16 @@ fn parse_entries(entries: &[&str]) -> Vec<Item> {
|
||||
.map(|(id, label, _)| Item::Cmd { label: label.to_string(), id: "window.panel".into(), params: json!({"panel": id}), shortcut: None })
|
||||
.collect();
|
||||
out.push(Item::Sub("Panels".into(), items));
|
||||
} else if e == "ui:window.workspace" {
|
||||
// Shown as InDesign's "[Essentials]"; Reset names the current workspace (see `menu_items`).
|
||||
let mut items: Vec<Item> = WORKSPACES
|
||||
.iter()
|
||||
.map(|w| Item::Cmd { label: w.to_string(), id: "window.workspace".into(), params: json!({"name": w}), shortcut: None })
|
||||
.collect();
|
||||
items.push(Item::Sep);
|
||||
items.push(cmd_item("window.resetWorkspace", Value::Null));
|
||||
items.push(cmd_item("window.newWorkspace", Value::Null));
|
||||
out.push(Item::Sub("Workspace".into(), items));
|
||||
} else if e == "ui:window.brightness" {
|
||||
let items = crate::theme::Brightness::ALL
|
||||
.iter()
|
||||
@@ -1792,6 +1854,7 @@ pub fn menu_tree() -> Vec<(&'static str, Vec<Item>)> {
|
||||
/// Check state of a toggle command (None = not a toggle).
|
||||
pub fn checked(app: &DesignApp, id: &str, params: &Value) -> Option<bool> {
|
||||
Some(match id {
|
||||
"window.workspace" => params.get("name").and_then(Value::as_str) == Some(app.ui.workspace.as_str()),
|
||||
"view.rulers" => app.ui.rulers,
|
||||
"view.frameEdges" => app.ui.frame_edges,
|
||||
"view.overprintPreview" => app.ui.overprint_preview,
|
||||
@@ -2089,7 +2152,12 @@ fn menu_items(app: &mut DesignApp, ui: &mut egui::Ui, items: &[Item], path: &str
|
||||
}
|
||||
Item::Cmd { label, .. } if !app.ui.show_full_menus && app.ui.hidden_menu_items.contains(&menu_key(path, label)) => hidden += 1,
|
||||
Item::Cmd { label, id, params, shortcut: _ } => {
|
||||
let label = crate::i18n::tr(&app.ui.language, label);
|
||||
let lang = &app.ui.language;
|
||||
let label = match id.as_str() {
|
||||
"window.workspace" => format!("[{}]", crate::i18n::workspace_name(lang, label)),
|
||||
"window.resetWorkspace" => format!("{} {}", crate::i18n::tr(lang, "Reset"), crate::i18n::workspace_name(lang, &app.ui.workspace)),
|
||||
_ => crate::i18n::tr(lang, label).to_owned(),
|
||||
};
|
||||
let text = match checked(app, id, params) {
|
||||
Some(true) => format!("✓ {label}"),
|
||||
Some(false) => format!(" {label}"),
|
||||
@@ -3379,6 +3447,74 @@ mod tests {
|
||||
assert_eq!(app.ui.workspace, "Essentials");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn essentials_classic_is_the_control_panel_and_iconic_groups() {
|
||||
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
run_ui(&mut app, "window.workspace", &json!({"name": "Essentials Classic"})).unwrap().unwrap();
|
||||
assert!(app.ui.control_bar && !app.ui.dock_expanded);
|
||||
let groups = app.ui.icon_groups.clone().unwrap();
|
||||
assert_eq!(groups[0], ["pages", "layers", "links"]);
|
||||
assert_eq!(groups.last().unwrap(), &["gradient"]);
|
||||
assert_eq!(checked(&app, "window.workspace", &json!({"name": "Essentials Classic"})), Some(true));
|
||||
assert_eq!(checked(&app, "window.workspace", &json!({"name": "Essentials"})), Some(false));
|
||||
// Window › Pages opens Pages from its icon group, not the Properties stack.
|
||||
run_ui(&mut app, "window.panel", &json!({"panel": "pages"})).unwrap().unwrap();
|
||||
assert_eq!((app.ui.open_panel.as_deref(), app.ui.dock_expanded), (Some("pages"), false));
|
||||
// A workspace saved from it keeps the groups.
|
||||
run_ui(&mut app, "window.newWorkspace", &json!({"name": "Mine"})).unwrap().unwrap();
|
||||
run_ui(&mut app, "window.workspace", &json!({"name": "Essentials"})).unwrap().unwrap();
|
||||
assert_eq!((app.ui.icon_groups.as_ref(), app.ui.dock_expanded, app.ui.control_bar), (None, true, false));
|
||||
run_ui(&mut app, "window.workspace", &json!({"name": "Mine"})).unwrap().unwrap();
|
||||
assert_eq!(app.ui.icon_groups, Some(groups));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_window_menu_is_in_indesigns_order_and_reaches_every_panel() {
|
||||
let window = menu_tree().into_iter().find(|(m, _)| *m == "Window").unwrap().1;
|
||||
let names: Vec<&str> = window
|
||||
.iter()
|
||||
.filter_map(|i| match i {
|
||||
Item::Sub(n, _) => Some(n.as_str()),
|
||||
Item::Cmd { label, .. } => Some(label.as_str()),
|
||||
Item::Sep => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(&names[..4], ["Arrange", "Workspace", "Articles", "Color"]);
|
||||
let sub = |name: &str| {
|
||||
window
|
||||
.iter()
|
||||
.find_map(|i| match i {
|
||||
Item::Sub(n, c) if n == name => Some(c.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap()
|
||||
};
|
||||
// Window › Workspace › Essentials Classic, Window › Color › Gradient & Swatches.
|
||||
assert!(
|
||||
sub("Workspace")
|
||||
.iter()
|
||||
.any(|i| matches!(i, Item::Cmd { id, params, .. } if id == "window.workspace" && params["name"] == "Essentials Classic"))
|
||||
);
|
||||
let color: Vec<String> =
|
||||
sub("Color").iter().filter_map(|i| if let Item::Cmd { label, .. } = i { Some(label.clone()) } else { None }).collect();
|
||||
assert_eq!(color, ["Color", "Gradient", "Swatches"]);
|
||||
// Every panel is in the menu.
|
||||
fn panels(items: &[Item], out: &mut Vec<String>) {
|
||||
for i in items {
|
||||
match i {
|
||||
Item::Sub(_, c) => panels(c, out),
|
||||
Item::Cmd { id, params, .. } if id == "window.panel" => out.extend(params["panel"].as_str().map(str::to_string)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut found = Vec::new();
|
||||
panels(&window, &mut found);
|
||||
for (id, _, _) in crate::dock::DOCK_TABS.iter().chain(crate::dock::ICON_PANELS) {
|
||||
assert!(found.iter().any(|p| p == id), "Window menu misses the {id} panel");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interactive_workspaces_show_their_panels() {
|
||||
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
|
||||
@@ -329,7 +329,7 @@ It is evidence of breadth: each item exists. It is not a claim of InDesign-level
|
||||
- Font menu: search, favourites (stars, Show Favorites Only; `favoriteFonts` preference) and each family previewed in its own face. Western (and other-script) families come first, then Japanese, Simplified Chinese, Traditional Chinese and Korean ones, each group alphabetical and set off by a separator; the group comes from the font's own data (`meta` languages, family names in a CJK language, OS/2 code pages, else what it covers), read once (installed fonts by the font scan, without loading them). CJK families show their native names, the English name on hover; Preferences › Type › Show Font Names in English (`showFontNamesInEnglish`) shows English names throughout. Search matches either name; documents store the English name. Every family list (Character panel, Control bar, Properties, style dialogs, Find Font, Glyphs) follows this order.
|
||||
- Application preferences (engine `Prefs`) now persist across launches (desktop: prefs.json beside ui.json).
|
||||
- View ▸ Fit Selection in Window (⌥⌘=) and Find/Change ▸ Object (`find.objects`, `find.changeObjects`: by fill, stroke, weight, opacity, kind, layer, label).
|
||||
- Custom workspaces (`window.newWorkspace/deleteWorkspace/resetWorkspace`; workspace switcher shows the current one): bars and panel arrangement saved by name.
|
||||
- Custom workspaces (`window.newWorkspace/deleteWorkspace/resetWorkspace`; workspace switcher shows the current one): bars and panel arrangement saved by name. Essentials Classic shows the Control panel and InDesign's iconic panel groups with names. The Window menu follows InDesign's: Arrange, Workspace, then the panels alphabetically with their Color, Editorial, Interactive, Object & Layout, Output, Styles, Type & Tables and Utilities groups.
|
||||
- Word/RTF Import Options (File ▸ Place with Import Options…, `place.styles`, `file.place {styleMap, styleConflicts}`): map imported styles onto the document's, use existing / redefine / auto-rename on conflicts, or remove formatting.
|
||||
- Custom anchored objects (`anchored.insert/options {position: custom}`): placed relative to the anchor, frame, column, page margins or page edge with reference points and offsets, no space in the text, keep within column; IDML Anchored position.
|
||||
- Variable font axes: Character panel sliders; any axis values (`fontStyle: "Bold {wght:650}"`) make that instance for layout, screen and PDF.
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ basics of editing fail for too many users on Windows and Linux.
|
||||
| Colour fields: hex, picker from any swatch field | ✓ | ✓ | ~ | hex entry and picker issues (#323, #190) |
|
||||
| Panels: Pages, Layers, Swatches, Color, Stroke, Character, Paragraph, Styles, Links, Effects, Text Wrap, Align, Pathfinder, Table, Index, TOC, Hyperlinks, Bookmarks, Articles, Tags, Notes, Track Changes, Object States, Buttons and Forms, Preflight, Data Merge, Library, Scripts, Glyphs, Info | ✓ | all present | ~ | **Tabs panel missing** (#326); Navigator, Trap Presets, Animation, Timing missing; pull-down panels can't scroll on Linux (#267) |
|
||||
| Dialogs: New Document, Document Setup, Preferences (all pages), Paragraph/Character/Object/Table/Cell Style Options, Export PDF, Print, Package, Find/Change | ✓ | present | ~ | generated command dialogs fill the long tail; depth varies |
|
||||
| Workspaces | Essentials, Advanced, Book, Digital Publishing, Interactive for PDF, Printing and Proofing, Typography, Touch | ✓ named workspaces + custom | ~ | tab bar overlaps menus in Book/Advanced (#182) |
|
||||
| Workspaces | Essentials, Essentials Classic, Advanced, Book, Digital Publishing, Interactive for PDF, Printing and Proofing, Review, Typography, Touch | ✓ named workspaces (Essentials Classic: Control panel and iconic panel groups) + custom; Window › Workspace; no Review or Touch | ~ | tab bar overlaps menus in Book/Advanced (#182) |
|
||||
| Story Editor | ✓ | ✓ | ~ | style-name column, depth ruler missing |
|
||||
| Undo granularity (a drag = one step) | ✓ | ✓ (Begin/Preview/Commit) | ✓ | |
|
||||
| Closing with unsaved changes asks | ✓ | ✓ File › Close and window/menu quit (#146, #545) | ✓ | macOS Dock Quit still bypasses the prompt (#544) |
|
||||
|
||||
Reference in New Issue
Block a user