diff --git a/crates/engine/src/cmd/labels.rs b/crates/engine/src/cmd/labels.rs index ac4322bfd..dc7893c72 100644 --- a/crates/engine/src/cmd/labels.rs +++ b/crates/engine/src/cmd/labels.rs @@ -65,10 +65,10 @@ pub fn commands() -> Vec { ), // Label classes: each labels every feature its clause takes, as in ArcGIS Pro; the classes // listed first, Default last, are placed first (`Labeling`, #732). - C::new("labels.class.add", "Add Label Class", "Style › Label Class", add_class) + C::new("labels.class.add", "Add Label Class", "View › Windows › Labeling", add_class) .when(need_feature_layer) .params(r#"{"layer"?, "name": string, "where"?: sql (a definition query's language; none takes every feature), "from"?: class (whose field and text symbol it starts with; none: the Default class)}"#), - C::new("labels.class.remove", "Remove Label Class", "Style › Label Class", |s, p| { + C::new("labels.class.remove", "Remove Label Class", "View › Windows › Labeling", |s, p| { let name = s_req(p, "class")?.trim().to_string(); with_labels(s, p, |labels| { let i = extra_class(labels, &name)?; @@ -79,7 +79,7 @@ pub fn commands() -> Vec { .when(need_feature_layer) .params(r#"{"layer"?, "class": name (not Default)}"#), // Every class but Default; none to remove is no error, so a script can start with it. - C::new("labels.class.clear", "Remove All Label Classes", "Style › Label Class", |s, p| { + C::new("labels.class.clear", "Remove All Label Classes", "View › Windows › Labeling", |s, p| { let mut removed = 0; with_labels(s, p, |labels| { removed = labels.classes.len(); @@ -90,7 +90,7 @@ pub fn commands() -> Vec { }) .when(need_feature_layer) .params(r#"{"layer"?}"#), - C::new("labels.class.rename", "Rename Label Class", "Style › Label Class", |s, p| { + C::new("labels.class.rename", "Rename Label Class", "View › Windows › Labeling", |s, p| { let class = s_req(p, "class")?.trim().to_string(); let name = s_req(p, "name")?.to_string(); with_labels(s, p, |labels| { @@ -101,7 +101,7 @@ pub fn commands() -> Vec { }) .when(need_feature_layer) .params(r#"{"layer"?, "class": name, "name": string}"#), - C::new("labels.class.update", "Update Label Class", "Style › Label Class", |s, p| { + C::new("labels.class.update", "Update Label Class", "View › Windows › Labeling", |s, p| { let id = s.layer_id(p)?; let clause = s.map().layer(id).map(|l| clause(p, &l.dataset)).transpose()?.flatten(); let visible = b(p, "visible"); @@ -116,7 +116,7 @@ pub fn commands() -> Vec { }) .when(need_feature_layer) .params(r#"{"layer"?, "class"?: name (none: the Default class, by whatever name it has), "where"?: sql|null, "visible"?: bool}"#), - C::new("labels.class.move", "Move Label Class", "Style › Label Class", |s, p| { + C::new("labels.class.move", "Move Label Class", "View › Windows › Labeling", |s, p| { let name = s_req(p, "class")?.trim().to_string(); let to = f(p, "to").filter(|v| v.is_finite()).ok_or("to is required: the class's place in the list, 0 first")?; with_labels(s, p, |labels| { diff --git a/crates/ui-egui/src/labelpane.rs b/crates/ui-egui/src/labelpane.rs index 4a85b1112..a5d0f53e3 100644 --- a/crates/ui-egui/src/labelpane.rs +++ b/crates/ui-egui/src/labelpane.rs @@ -2,8 +2,8 @@ //! from a layer's right-click menu (Labeling Properties…) or View › Windows › Labeling. The ribbon //! keeps its label groups; both run the same `labels.*` commands, so the pane holds no state of its //! own (it reads the layer each frame, as the Symbology pane does) and every change undoes. It -//! edits one label class at a time, the one chosen in its Class section (#722); the ribbon's label -//! groups edit and choose the same one (#858). +//! edits one label class at a time, the one chosen in its header (#722); the ribbon's label groups +//! edit and choose the same one (#858). use egui::{RichText, Ui, vec2}; use rustgis_engine::map::symbology::{DEFAULT_LABEL_CLASS, LABEL_SIZE, LabelClass, Labeling, MAX_LABEL_CLASSES, label_class_name}; @@ -42,6 +42,7 @@ pub fn show(app: &mut RustGisApp, ui: &mut Ui, t: &Tokens) { run(app, "labels.enable", json!({"on": on})); } let c = chosen(ui, app, &l); + classes(app, ui, &l, &c, t); class(app, ui, &l, &c, t); text_symbol(app, ui, &l, &c); position(app, ui, &l, &c); @@ -164,12 +165,9 @@ enum ClassTab { Sql, } -/// The class chosen, what makes, renames, removes and orders classes (#722), and the chosen -/// class's field and SQL clause (#618) choosing the features it labels. +/// The chosen class's field and SQL clause (#618) choosing the features it labels. fn class(app: &mut RustGisApp, ui: &mut Ui, l: &Layer, c: &Class, t: &Tokens) { section(ui, "Class", |ui| { - classes(app, ui, l, c); - ui.add_space(4.0); let key = egui::Id::unique("label_pane_class_tab"); let mut tab = ui.data(|d| d.get_temp::(key)).unwrap_or(ClassTab::Field); ui.horizontal(|ui| { @@ -199,9 +197,10 @@ struct Renaming { pass: u64, } -/// The class chooser, and New, Rename, Move Up, Move Down, Remove and Visible for the chosen -/// class. Default, last in the list, can be renamed or hidden, but not removed or moved. -fn classes(app: &mut RustGisApp, ui: &mut Ui, l: &Layer, c: &Class) { +/// The pane's header for the chosen class (#858): its chooser, a row with New, Rename, Move Up, +/// Move Down and Remove for it (#722), and Visible. Default, last in the list, can be renamed or +/// hidden, but not removed or moved. +fn classes(app: &mut RustGisApp, ui: &mut Ui, l: &Layer, c: &Class, t: &Tokens) { let rename_key = egui::Id::unique("label_pane_rename"); let owner = egui::Id::unique((layer_key(app, l), c.name)); // A rename started on another class or layer (chosen since, or by an undo), or left when the @@ -209,6 +208,7 @@ fn classes(app: &mut RustGisApp, ui: &mut Ui, l: &Layer, c: &Class) { let pass = ui.ctx().cumulative_pass_nr(); let renaming = ui.data(|d| d.get_temp::(rename_key)).filter(|r| r.owner == owner && r.pass + 1 >= pass); ui.data_mut(|d| d.remove::(rename_key)); + let editing = renaming.is_some(); grid(ui, "class_pick", |ui| { let input_label = ui.label(tl!("Class")); match renaming { @@ -219,53 +219,69 @@ fn classes(app: &mut RustGisApp, ui: &mut Ui, l: &Layer, c: &Class) { } ui.end_row(); }); - let count = extras(l).count(); - let at = c.name.and_then(|n| extras(l).position(|k| k.name == n)); - ui.horizontal_wrapped(|ui| { - let full = l.labels.classes.len() >= MAX_LABEL_CLASSES; - let max = MAX_LABEL_CLASSES.to_string(); - let new = ui - .add_enabled(!full, egui::Button::new(tl!("New"))) - .on_hover_text(tl!("Add a class with this one's field, text symbol, placement and range.")) - .on_disabled_hover_text(crate::i18n::fmt("A layer keeps at most {max} label classes besides Default.", &[("max", &max)])); - if new.clicked() { - let p = named(json!({"name": new_name(&l.labels)}), "from", c.name); - if let Some(added) = run(app, "labels.class.add", p) { - choose(ui, app, l, added.get("class").and_then(Value::as_str)); + // Off while the class is renamed: a click on them ends the rename (the box loses focus), so + // they would act on the class by the name it had before. + ui.add_enabled_ui(!editing, |ui| { + let count = extras(l).count(); + let at = c.name.and_then(|n| extras(l).position(|k| k.name == n)); + ui.horizontal(|ui| { + let full = l.labels.classes.len() >= MAX_LABEL_CLASSES; + let max = MAX_LABEL_CLASSES.to_string(); + let new = ui + .add_enabled(!full, egui::Button::new(tl!("New"))) + .on_hover_text(tl!("Add a class with this one's field, text symbol, placement and range.")) + .on_disabled_hover_text(crate::i18n::fmt("A layer keeps at most {max} label classes besides Default.", &[("max", &max)])); + if new.clicked() { + let p = named(json!({"name": new_name(&l.labels)}), "from", c.name); + if let Some(added) = run(app, "labels.class.add", p) { + choose(ui, app, l, added.get("class").and_then(Value::as_str)); + } } - } - if ui.button(tl!("Rename")).clicked() { - let r = Renaming { owner, text: c.it.name.clone(), focused: false, pass }; - ui.data_mut(|d| d.insert_temp(rename_key, r)); - } - let up = at.filter(|i| *i > 0).map(|i| i - 1); - let down = at.filter(|i| i + 1 < count).map(|i| i + 1); - for (label, to) in [("Move Up", up), ("Move Down", down)] { - let moved = ui - .add_enabled(to.is_some(), egui::Button::new(tl!(label))) - .on_hover_text(tl!("Labels of the classes listed first are placed first when they compete for room.")); - if moved.clicked() { - run(app, "labels.class.move", c.params(json!({"to": to}))); + if ui.button(tl!("Rename")).clicked() { + let r = Renaming { owner, text: c.it.name.clone(), focused: false, pass }; + ui.data_mut(|d| d.insert_temp(rename_key, r)); } - } - // Last and set apart: it is the one that deletes, and its name is close to Rename's. - ui.add_space(12.0); - if ui - .add_enabled(at.is_some(), egui::Button::new(tl!("Remove"))) - .on_disabled_hover_text(tl!("Default can't be removed. You can rename or hide it.")) - .clicked() - { - // Default chosen outright: a class added later by the removed one's name (an undo, a - // script) isn't chosen again by the name left behind. - if run(app, "labels.class.remove", c.params(json!({}))).is_some() { - choose(ui, app, l, None); + let up = at.filter(|i| *i > 0).map(|i| i - 1); + let down = at.filter(|i| i + 1 < count).map(|i| i + 1); + let moves = [ + ("up", "Move Up", "Move up: labels of the classes listed first are placed first when they compete for room.", up), + ("down", "Move Down", "Move down: labels of the classes listed first are placed first when they compete for room.", down), + ]; + for (icon, label, tip, to) in moves { + let moved = ui + .add_enabled_ui(to.is_some(), |ui| { + // Named for screen readers (and tests) by its label, as the Bookmarks + // pane's icons; its one tooltip says why the order matters. + let enabled = ui.is_enabled(); + let r = crate::widgets::icon_button(ui, icon, tl!(tip)); + r.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, enabled, tl!(label))); + r + }) + .inner; + if moved.clicked() { + run(app, "labels.class.move", c.params(json!({"to": to}))); + } } + // At the far end and in red: it is the one that deletes, and its name is close to + // Rename's. + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let remove = ui + .add_enabled(at.is_some(), egui::Button::new(RichText::new(tl!("Remove")).color(t.red))) + .on_disabled_hover_text(tl!("Default can't be removed. You can rename or hide it.")); + if remove.clicked() { + // Default chosen outright: a class added later by the removed one's name (an + // undo, a script) isn't chosen again by the name left behind. + if run(app, "labels.class.remove", c.params(json!({}))).is_some() { + choose(ui, app, l, None); + } + } + }); + }); + let mut visible = c.it.visible; + if ui.checkbox(&mut visible, tl!("Visible")).on_hover_text(tl!("A hidden class leaves its features unlabelled.")).changed() { + run(app, "labels.class.update", c.params(json!({"visible": visible}))); } }); - let mut visible = c.it.visible; - if ui.checkbox(&mut visible, tl!("Visible")).on_hover_text(tl!("A hidden class leaves its features unlabelled.")).changed() { - run(app, "labels.class.update", c.params(json!({"visible": visible}))); - } } /// The box the chosen class is renamed in: Enter (or clicking away) renames it, Escape leaves it as @@ -1189,7 +1205,8 @@ mod tests { assert!(shown(&h, "Default")); } - /// New, Move Up, Rename and Remove each run their command once: one undo step each. + /// New, Move Up, Rename and Remove, under the header's chooser (#858), each run their command + /// once: one undo step each. #[test] fn classes_are_made_renamed_ordered_and_removed_from_the_pane() { let app = with_cities(); @@ -1246,6 +1263,9 @@ mod tests { assert_eq!(names(h.state()), ["Big", "Big 2", "Default"]); assert!(h.state().session.notices.iter().any(|n| n.error && n.text.contains("already a label class"))); assert!(h.query_by_role(egui::accesskit::Role::TextInput).is_some(), "the box stays open"); + for off in ["New", "Rename", "Move Up", "Move Down", "Remove", "Visible"] { + assert!(h.get_by_label(off).accesskit_node().is_disabled(), "{off} waits for the rename"); + } h.key_press(egui::Key::Escape); h.run(); assert!(h.query_by_role(egui::accesskit::Role::TextInput).is_none()); diff --git a/docs/user-guide/cartography.md b/docs/user-guide/cartography.md index f4899ff12..ec46f7455 100644 --- a/docs/user-guide/cartography.md +++ b/docs/user-guide/cartography.md @@ -286,10 +286,11 @@ Turn labels on per layer (View › Labeling › Label, or the Style tab), then: `pop_max <= 5000000` for Default. Classes are placed in list order, Default last; a hidden class or one outside its visibility range labels nothing without excluding features from other classes. There can be up to 64 classes besides Default. In the Labeling pane, the - **Class** list chooses the class every section shows and edits; **New** adds one with the - chosen class's field, text symbol, placement and range, listed before Default, and - **Rename**, **Move Up** / **Move Down**, **Remove** and **Visible** act on the chosen class - (Default can be renamed or hidden, not moved or removed). The ribbon's label groups edit the + **Class** list at the top chooses the class every section shows and edits, and **Visible** + under it hides or shows that class. The row under the list has **New**, which adds a class + with the chosen class's field, text symbol, placement and range, listed before Default; + **Rename** and **Move Up** / **Move Down** (the arrows) for the chosen class; and, at its far + end, **Remove** (Default can be renamed or hidden, not moved or removed). The ribbon's label groups edit the same chosen class, and **Style › Label Class › Class** chooses it as the pane's list does. By command, make them with `labels.class.add` (a new class copies Default's field, text symbol, placement and range, or