Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions crates/engine/src/cmd/labels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@ pub fn commands() -> Vec<C> {
),
// 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)
Comment thread
jgregoriorincon marked this conversation as resolved.
.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)?;
Expand All @@ -79,7 +79,7 @@ pub fn commands() -> Vec<C> {
.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();
Expand All @@ -90,7 +90,7 @@ pub fn commands() -> Vec<C> {
})
.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| {
Expand All @@ -101,7 +101,7 @@ pub fn commands() -> Vec<C> {
})
.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");
Expand All @@ -116,7 +116,7 @@ pub fn commands() -> Vec<C> {
})
.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| {
Expand Down
124 changes: 72 additions & 52 deletions crates/ui-egui/src/labelpane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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::<ClassTab>(key)).unwrap_or(ClassTab::Field);
ui.horizontal(|ui| {
Expand Down Expand Up @@ -199,16 +197,18 @@ 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
// pane was last hidden, is dropped.
let pass = ui.ctx().cumulative_pass_nr();
let renaming = ui.data(|d| d.get_temp::<Renaming>(rename_key)).filter(|r| r.owner == owner && r.pass + 1 >= pass);
ui.data_mut(|d| d.remove::<Renaming>(rename_key));
let editing = renaming.is_some();
grid(ui, "class_pick", |ui| {
let input_label = ui.label(tl!("Class"));
match renaming {
Expand All @@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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());
Expand Down
9 changes: 5 additions & 4 deletions docs/user-guide/cartography.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading