Merge pull request #7699 from dotCooCoo/fix/panel-view-command

Panels: ui.panelView sets the Swatches, Graphic Styles and Symbols panel views
This commit is contained in:
BFlat
2026-11-15 09:47:37 +07:00
committed by GitHub
12 changed files with 149 additions and 33 deletions
+6
View File
@@ -1087,6 +1087,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"",
"{objects?: [paths|shapes|text|images|effects|groups|linkedFiles|guides], layers?: [locked|hidden]} set the Layers panel's Filter (By Object, By Layer): a row stays when it is of a kind in `objects` and in a state in `layers` (its own lock or eye; an empty list takes every row), or when a row inside it stays; Search All narrows the rows further. A key left out keeps its list. The filter lasts the session and does not change the document, its open rows or the selection → {objects, layers}",
),
(
"ui.panelView",
"Set Panel View",
"",
"{panel: swatches|graphicStyles|symbols, view?} set a panel's view, as the view items of its panel menu do: for swatches smallThumb|mediumThumb (the default)|largeThumb|smallList|largeList, for graphicStyles and symbols thumbnail (the default)|smallList|largeList; without `view`, answer the current one. The view lasts the session and does not change the document; an unknown panel or view is an error and changes nothing → {panel, view}",
),
(
"ui.tileEdgeColor",
"Tile Edge Color…",
+1
View File
@@ -845,6 +845,7 @@ fn run_ui_part8(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result<
"ui.navigatorPanelOptions" => crate::panels::navigator::set_options(app, p),
"ui.layersExpand" => crate::panels::layers::expand(app, p),
"ui.layersFilter" => crate::panels::layers::set_filter(app, p),
"ui.panelView" => crate::panels::set_view(app, p),
"ui.flattenTransparencyDialog" => {
crate::dialogs::flatten::open(app);
Ok(Value::Null)
+6
View File
@@ -1088,6 +1088,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"",
"{objects?: [paths|shapes|text|images|effects|groups|linkedFiles|guides], layers?: [locked|hidden]} set the Layers panel's Filter (By Object, By Layer): a row stays when it is of a kind in `objects` and in a state in `layers` (its own lock or eye; an empty list takes every row), or when a row inside it stays; Search All narrows the rows further. A key left out keeps its list. The filter lasts the session and does not change the document, its open rows or the selection → {objects, layers}",
),
(
"ui.panelView",
"Set Panel View",
"",
"{panel: swatches|graphicStyles|symbols, view?} set a panel's view, as the view items of its panel menu do: for swatches smallThumb|mediumThumb (the default)|largeThumb|smallList|largeList, for graphicStyles and symbols thumbnail (the default)|smallList|largeList; without `view`, answer the current one. The view lasts the session and does not change the document; an unknown panel or view is an error and changes nothing → {panel, view}",
),
(
"ui.tileEdgeColor",
"Tile Edge Color…",
+36
View File
@@ -514,6 +514,42 @@ pub(crate) fn set_pstate<T: Clone + Send + Sync + 'static>(ctx: &egui::Context,
ctx.data_mut(|d| d.insert_temp(egui::Id::unique(("panel-state", key)), v));
}
/// `ui.panelView {panel, view?}`: set the view of the Swatches, Graphic Styles or Symbols panel
/// (`panel`: swatches, graphicStyles or symbols), as the view items of its panel menu do; without
/// `view`, answer the current one. An unknown panel or view is an error, and then nothing changes.
/// → {panel, view}
pub(crate) fn set_view(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
fn given<T: serde::de::DeserializeOwned>(p: &Value, err: &str) -> Result<Option<T>, String> {
match p.get("view") {
None | Some(Value::Null) => Ok(None),
Some(v) => serde_json::from_value(v.clone()).map(Some).map_err(|_| err.to_string()),
}
}
let ui = &mut app.ui;
let (panel, view) = match p.get("panel").and_then(Value::as_str) {
Some("swatches") => {
if let Some(v) = given(p, "`view` is smallThumb, mediumThumb, largeThumb, smallList or largeList")? {
ui.swatches_view = v;
}
("swatches", json!(ui.swatches_view))
}
Some("graphicStyles") => {
if let Some(v) = given(p, "`view` is thumbnail, smallList or largeList")? {
ui.graphic_styles_view = v;
}
("graphicStyles", json!(ui.graphic_styles_view))
}
Some("symbols") => {
if let Some(v) = given(p, "`view` is thumbnail, smallList or largeList")? {
ui.symbols_view = v;
}
("symbols", json!(ui.symbols_view))
}
_ => return Err("`panel` is swatches, graphicStyles or symbols".to_string()),
};
Ok(json!({ "panel": panel, "view": view }))
}
/// Whether the panel whose state is `key` has the keyboard for its rows (Delete and Backspace
/// remove its highlighted rows): it took it with a click on a row ([`set_row_keys`]) and kept it in
/// every frame since, so it lapses when the panel isn't drawn.
+15 -12
View File
@@ -30,9 +30,10 @@ use crate::menus::Item;
use crate::theme::Tokens;
use crate::widgets::{self, PanelDrag, menu_item};
/// The panel's views.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum View {
/// The panel's views (`ui.panelView {panel: "graphicStyles", view}`).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum View {
#[default]
Thumbnail,
SmallList,
@@ -463,7 +464,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
let looks = look_hashes(ui.ctx(), app);
let shown = Shown { d: &doc, looks: &looks, preview: pstate(ui.ctx(), "gs-preview") };
let sel = selected(ui.ctx(), &doc);
let view: View = pstate(ui.ctx(), "gs-view");
let view = app.ui.graphic_styles_view;
let mut ev = Events::default();
// In a sized panel the list fills the space above the footer (#2992), as the Artboards list
// does; elsewhere it keeps its 220 pt.
@@ -605,10 +606,12 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
app.run("graphicStyle.sortByName", json!({})).ok();
}
ui.separator();
let view: View = pstate(ui.ctx(), "gs-view");
let view = app.ui.graphic_styles_view;
for (v, label) in View::ALL {
if menu_item(ui, tl!(label), true, view == v) {
set_pstate(ui.ctx(), "gs-view", v);
if menu_item(ui, tl!(label), true, view == v)
&& let Err(e) = app.run("ui.panelView", json!({ "panel": "graphicStyles", "view": v }))
{
app.status(e);
}
}
ui.separator();
@@ -865,7 +868,7 @@ mod tests {
app.session.execute("graphicStyle.new", &json!({"name": format!("Style {i:02}")})).unwrap();
}
let ctx = egui::Context::default();
set_pstate(&ctx, "gs-view", View::LargeList);
app.ui.graphic_styles_view = View::LargeList;
// The clip rect of the first style's name: the list's visible area.
let mut list = |bottom: Option<f32>| {
let mut clip = None;
@@ -891,7 +894,7 @@ mod tests {
let mut app = app();
app.session.execute("graphicStyle.new", &json!({"name": "Mine"})).unwrap();
let ctx = egui::Context::default();
set_pstate(&ctx, "gs-view", View::LargeList);
app.ui.graphic_styles_view = View::LargeList;
let texts = frame(&ctx, |ui| {
show(&mut app, ui);
menu(&mut app, ui);
@@ -906,7 +909,7 @@ mod tests {
let unused: Vec<String> = serde_json::from_value(app.session.execute("graphicStyle.unused", &json!({})).unwrap()["names"].clone()).unwrap();
assert_eq!(unused.len(), 4);
set_pstate(&ctx, "gs-sel", unused.clone());
set_pstate(&ctx, "gs-view", View::SmallList);
app.ui.graphic_styles_view = View::SmallList;
frame(&ctx, |ui| show(&mut app, ui));
// Delete asks first; OK deletes, and the panel selection follows.
delete(&mut app, &unused, false);
@@ -967,7 +970,7 @@ mod tests {
// A style dragged onto the top half of the first row lands first (Small List View: rows
// of 18 points from the top of the list).
set_pstate(&ctx, "gs-view", View::SmallList);
app.ui.graphic_styles_view = View::SmallList;
frame(&ctx, |ui| show(&mut app, ui));
let release = |at: egui::Pos2| {
let up = egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() };
@@ -992,7 +995,7 @@ mod tests {
let n = names(&app);
assert_eq!(n.len(), 5);
let ctx = egui::Context::default();
set_pstate(&ctx, "gs-view", View::SmallList);
app.ui.graphic_styles_view = View::SmallList;
frame(&ctx, |ui| show(&mut app, ui));
// A click on row `i` (Small List View: rows of 18 points, 3 apart) with `modifiers` held,
// a second after the last one so that two clicks on a row are not a double-click.
@@ -121,7 +121,7 @@ pub(super) fn new_swatch(app: &mut VectorcraftApp, sel: &[String], m: egui::Modi
}
pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
let view: View = pstate(ui.ctx(), "swatch-view");
let view = app.ui.swatches_view;
let selected = selection(app, ui);
let del = deletable(&selected);
let opts = editable(app, &selected);
@@ -174,7 +174,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
ui.separator();
for (v, label) in View::ALL {
if menu_item(ui, label, true, v == view) {
set_pstate(ui.ctx(), "swatch-view", v);
app.run("ui.panelView", json!({ "panel": "swatches", "view": v })).ok();
}
}
ui.separator();
+7 -3
View File
@@ -16,17 +16,21 @@ pub(super) fn body(app: &mut VectorcraftApp, ui: &mut Ui, salt: &'static str) {
crate::panels::empty_state(ui, "swatch-book", tl!("No document"), tl!("Open a document to see its swatches."));
return;
}
let view: View = pstate(ui.ctx(), "swatch-view");
let view = app.ui.swatches_view;
let kind: Kind = pstate(ui.ctx(), "swatch-kind");
// Top row: proxy at left, list / grid view toggles at right.
ui.horizontal(|ui| {
crate::panels::proxy(app, ui, 34.0);
ui.add_space(ui.available_width() - 58.0);
let mut to = None;
if widgets::icon_button(ui, "dc-list-view", tl!("Show List View"), view.is_list(), 26.0).clicked() {
set_pstate(ui.ctx(), "swatch-view", View::SmallList);
to = Some(View::SmallList);
}
if widgets::icon_button(ui, "dc-grid-view", tl!("Show Thumbnail View"), !view.is_list(), 26.0).clicked() {
set_pstate(ui.ctx(), "swatch-view", View::MediumThumb);
to = Some(View::MediumThumb);
}
if let Some(v) = to {
app.run("ui.panelView", json!({ "panel": "swatches", "view": v })).ok();
}
});
ui.add_space(4.0);
+2 -2
View File
@@ -489,7 +489,7 @@ fn find_field_filters_and_shift_extends_over_what_is_shown() {
fn list_view_rows_take_double_clicks_and_folder_clicks_select_the_group() {
let mut app = app();
let ctx = context();
set_pstate(&ctx, "swatch-view", View::SmallList);
app.ui.swatches_view = View::SmallList;
double_click_tile(&mut app, &ctx, "Red");
assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(KIND), "double-click in a list opens Swatch Options");
let d = &app.session.active().unwrap().doc;
@@ -1013,7 +1013,7 @@ fn the_list_fills_a_tall_panel_above_its_footer() {
app.run("swatch.new", json!({"name": format!("Tone {i:02}"), "color": format!("#{:02x}3060", i * 4)})).unwrap();
}
let ctx = context();
set_pstate(&ctx, "swatch-view", View::SmallList);
app.ui.swatches_view = View::SmallList;
// The clip rect of the first swatch row's name: the list's visible area.
let mut list = |bottom: Option<f32>| {
let mut clip = None;
+55 -13
View File
@@ -286,7 +286,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
let t = Tokens::get(ui.ctx());
let Some((doc, keys)) = app.session.active().map(|d| (d.doc.clone(), document_symbols(d))) else { return };
let names: Vec<String> = doc.symbols.iter().map(|s| s.name.clone()).collect();
let view: View = pstate(ui.ctx(), VIEW);
let view = app.ui.symbols_view;
let sel = current(app);
let uid = app.session.active().map(|d| d.uid);
let marked = marked(ui.ctx(), uid, &names);
@@ -458,10 +458,12 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
app.status(e);
}
ui.separator();
let view: View = pstate(ui.ctx(), VIEW);
let view = app.ui.symbols_view;
for (v, label) in View::ALL {
if menu_item(ui, tl!(label), true, view == v) {
set_pstate(ui.ctx(), VIEW, v);
if menu_item(ui, tl!(label), true, view == v)
&& let Err(e) = app.run("ui.panelView", json!({ "panel": "symbols", "view": v }))
{
app.status(e);
}
}
ui.separator();
@@ -475,13 +477,11 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
/// The panel state key of the symbols Select All Unused picked.
const MARKED: &str = "sym-unused";
/// The panel state key of the panel's [`View`].
const VIEW: &str = "sym-view";
/// The panel's views: Thumbnail View (the default) and the Small and Large List Views, whose rows
/// show each symbol's thumbnail and name.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum View {
/// show each symbol's thumbnail and name (`ui.panelView {panel: "symbols", view}`).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum View {
#[default]
Thumbnail,
SmallList,
@@ -1170,7 +1170,7 @@ mod tests {
// Large List View: the row named Dot.
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
set_pstate(&ctx, VIEW, View::LargeList);
app.ui.symbols_view = View::LargeList;
let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(236.0, 400.0))), ..Default::default() };
let mut out = ctx.run_ui(raw, |ui| show(&mut app, ui));
out.textures_delta.clear();
@@ -1238,7 +1238,6 @@ mod tests {
assert_eq!((editing(&app), current(&mut app)), (None, Some("Cat".into())), "{view}");
let ctx2 = egui::Context::default();
crate::theme::install_fonts(&ctx2);
set_pstate(&ctx2, VIEW, pstate::<View>(&ctx, VIEW));
frame(&ctx2, &mut app, vec![]);
frame(&ctx2, &mut app, double_click(row));
assert_eq!(editing(&app).as_deref(), Some("Cat"), "{view}");
@@ -1253,6 +1252,49 @@ mod tests {
assert_eq!(app.session.active().unwrap().history.undo.len(), undo);
}
/// `ui.panelView` sets the Symbols, Graphic Styles and Swatches panels' views as the view items
/// of their panel menus do, and answers the current view without `view`. An unknown panel or
/// view, or a view of another panel, is an error and changes nothing.
#[test]
fn panel_view_sets_the_view_of_each_panel() {
use super::super::{graphic_styles, swatches};
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 100})).unwrap();
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
for (i, name) in ["ant", "bee"].into_iter().enumerate() {
app.run("shape.rectangle", json!({"x": 10.0 + 40.0 * i as f64, "y": 10, "width": 30, "height": 20})).unwrap();
app.run("symbol.new", json!({"name": name})).unwrap();
}
let views = |checked: &str| ["Thumbnail View", "Small List View", "Large List View"].map(|v| (v.to_string(), v == checked)).to_vec();
assert_eq!(app.run("ui.panelView", json!({"panel": "symbols"})).unwrap(), json!({"panel": "symbols", "view": "thumbnail"}));
assert!(panel_labels(&ctx, &mut app, &["ant", "bee"]).is_empty(), "the thumbnails draw no names");
let r = app.run("ui.panelView", json!({"panel": "symbols", "view": "largeList"})).unwrap();
assert_eq!(r, json!({"panel": "symbols", "view": "largeList"}));
assert_eq!(view_items(&ctx, &mut app), views("Large List View"), "the panel menu checks it");
let names: Vec<String> = panel_labels(&ctx, &mut app, &["ant", "bee"]).into_iter().map(|(t, _)| t).collect();
assert_eq!(names, ["ant", "bee"], "the list names the symbols");
click_menu(&ctx, &mut app, "Small List View");
assert_eq!(app.run("ui.panelView", json!({"panel": "symbols"})).unwrap()["view"], "smallList", "the menu item sets it too");
for bad in [
json!({"panel": "symbols", "view": "mediumThumb"}),
json!({"panel": "symbols", "view": 3}),
json!({"panel": "brushes", "view": "smallList"}),
json!({"view": "largeList"}),
] {
assert!(app.run("ui.panelView", bad.clone()).is_err(), "{bad}");
}
assert_eq!(app.ui.symbols_view, View::SmallList, "unchanged");
let r = app.run("ui.panelView", json!({"panel": "graphicStyles", "view": "largeList"})).unwrap();
assert_eq!(r, json!({"panel": "graphicStyles", "view": "largeList"}));
assert_eq!(app.ui.graphic_styles_view, graphic_styles::View::LargeList);
assert_eq!(app.run("ui.panelView", json!({"panel": "swatches"})).unwrap()["view"], "mediumThumb");
app.run("ui.panelView", json!({"panel": "swatches", "view": "largeThumb"})).unwrap();
assert!(app.run("ui.panelView", json!({"panel": "swatches", "view": "thumbnail"})).is_err());
assert_eq!(app.ui.swatches_view, swatches::View::LargeThumb, "unchanged by the error");
assert_eq!(app.ui.symbols_view, View::SmallList, "each panel keeps its own view");
}
/// The panel menu's Edit Symbol edits the selected instance's symbol, and with no instance
/// selected the symbol current in the panel.
#[test]
@@ -1724,7 +1766,7 @@ mod tests {
assert!(!symbols(h.state_mut()).contains(&"B".to_string()));
// Also test list view
set_pstate(&h.ctx, VIEW, View::SmallList);
h.state_mut().ui.symbols_view = View::SmallList;
h.run_steps(2);
let node_a = h.get_by_label("A");
let pos_a = node_a.rect().center();
+11
View File
@@ -838,6 +838,14 @@ pub struct UiState {
/// Search All text does.
#[serde(skip)]
pub layers_filter: crate::panels::layers::Filter,
/// The Swatches, Graphic Styles and Symbols panels' views, chosen in their panel menus
/// (`ui.panelView`). They are not saved and last the session.
#[serde(skip)]
pub swatches_view: crate::panels::swatches::View,
#[serde(skip)]
pub graphic_styles_view: crate::panels::graphic_styles::View,
#[serde(skip)]
pub symbols_view: crate::panels::symbols::View,
/// The Image Trace panel's Advanced section is open (as it was last left).
#[serde(default = "yes")]
pub image_trace_advanced: bool,
@@ -1242,6 +1250,9 @@ impl Default for UiState {
layers_panel: Default::default(),
navigator_panel: Default::default(),
layers_filter: Default::default(),
swatches_view: Default::default(),
graphic_styles_view: Default::default(),
symbols_view: Default::default(),
image_trace_advanced: true,
image_trace_preview: true,
glyphs_preview: 0,
+7
View File
@@ -3238,6 +3238,13 @@ already walks Image Trace. Short agent entry points:
unchanged. No undo step is added when the symbols are already in order. The panel menu's Thumbnail View, Small List
View and Large List View change only how the panel shows the symbols: thumbnails, or rows with a thumbnail and the
name (smaller rows in Small List View).
- Panel views: `ui.panelView {panel, view?}` sets the view of the Swatches, Graphic Styles or Symbols panel, as the
view items of its panel menu (and the Swatches panel's list and thumbnail buttons) do. `panel` is `swatches`,
`graphicStyles` or `symbols`. The Swatches panel takes `smallThumb`, `mediumThumb` (the default), `largeThumb`,
`smallList` or `largeList`; the Graphic Styles and Symbols panels take `thumbnail` (the default), `smallList` or
`largeList`. Without `view` the command answers the current one. Each panel keeps its own view for the session, and
the document does not change. An unknown panel or view is an error, and then nothing changes. It answers
`{panel, view}`.
- The Brushes panel menu's Select All Unused: `brush.unused` (a query) lists the brushes no stroke paints with, in
library order, and the panel selects them. Strokes in the art (opacity masks included), symbols, graph designs,
pattern tiles (with the tiles that pattern editing's Cancel puts back) and graphic styles count, and so do the
+1 -1
View File
@@ -990,7 +990,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- **Delete Symbol asks first** ([Symbols panel](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/symbols-panel-options.html), [symbol libraries](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-or-import-symbol-libraries.html), [Alt-click Delete](https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)): the Symbols panel's Delete Symbol button and menu item ask "Delete the selected symbol?" with Yes and No for a symbol no instance uses. For a symbol with placed instances they ask with Expand Instances (the instances become ordinary art), Delete Instances and Cancel; either Instances button deletes the symbol in one undo step. Alt-click on the button deletes an unused symbol without asking and still asks about one with instances. A question left open while another document became active deletes nothing. `symbol.delete` itself does not ask. Tests: `panels::symbols::tests::delete_symbol_asks_first_and_alt_click_deletes_an_unused_symbol_at_once`, `the_in_use_question_expands_or_deletes_the_instances`, `a_stale_delete_symbol_question_deletes_nothing`, `the_delete_symbol_questions_show_their_buttons`.
- **Symbols panel context menu** ([Symbols panel](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/symbols-panel-options.html)): right-clicking a symbol in thumbnail view or list view displays a context menu with Place Symbol Instance (passes view center or artboard center), Edit Symbol, Duplicate Symbol, Delete Symbol, Break Link to Symbol (enabled when canvas selection includes instances of this symbol), and Select All Instances. Right-clicking an unselected symbol selects it first (`symbol.setCurrent`). Tests: `panels::symbols::tests::right_click_context_menu_in_symbols_panel_runs_actions`.
- **Symbols › Select All Unused** ([Symbols panel](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/symbols-panel-options.html), #4918): the Symbols panel menu's Select All Unused shows the symbols nothing places selected (`symbol.unused`): instances in the art (opacity masks included), graph designs, pattern tiles and brush art count, and so do instances in a used symbol's art. Delete Symbol, the button or the menu item, then asks "Delete the selected symbols?" once and deletes those still unused in one undo step (`symbol.delete {names}`); Alt-click on the button deletes them without asking, and a click on a symbol picks just it again. Tests: `tests::brushsym::symbol_unused_lists_the_symbols_nothing_places_and_delete_takes_several`, `panels::symbols::tests::select_all_unused_picks_the_unused_symbols_and_delete_deletes_them`, `dialogs::symbol_delete::tests::several_symbols_get_the_plural_question`.
- **Symbols › Sort by Name and the list views** ([the help page](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/symbols-panel-options.html)): the Symbols panel menu's Sort by Name puts the symbols in alphabetical order (`symbol.sortByName`), and its Thumbnail View, Small List View and Large List View show them as thumbnails, in a compact list or in a list with larger thumbnails. VectorCraft's own rules: the sort ignores case and keeps names that differ only in case in their order; it changes the document's order, which `symbol.list` reports, as one undo step, keeps the current symbol and adds no undo step when the symbols are already in order; Thumbnail View is the default, Small List View has 18-point rows with 14-point thumbnails and Large List View 26-point rows with 22-point thumbnails, and a click or double-click on a symbol does the same in every view. Tests: `tests::brushsym::symbol_sort_by_name_orders_the_symbols_in_one_undo_step`, `panels::symbols::tests::sort_by_name_and_the_small_and_large_list_views`.
- **Symbols › Sort by Name and the list views** ([the help page](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/symbols-panel-options.html)): the Symbols panel menu's Sort by Name puts the symbols in alphabetical order (`symbol.sortByName`), and its Thumbnail View, Small List View and Large List View show them as thumbnails, in a compact list or in a list with larger thumbnails. VectorCraft's own rules: the sort ignores case and keeps names that differ only in case in their order; it changes the document's order, which `symbol.list` reports, as one undo step, keeps the current symbol and adds no undo step when the symbols are already in order; Thumbnail View is the default, Small List View has 18-point rows with 14-point thumbnails and Large List View 26-point rows with 22-point thumbnails, and a click or double-click on a symbol does the same in every view. `ui.panelView` sets the view of the Symbols panel, and of the Swatches and Graphic Styles panels, as their panel menus do. Tests: `tests::brushsym::symbol_sort_by_name_orders_the_symbols_in_one_undo_step`, `panels::symbols::tests::sort_by_name_and_the_small_and_large_list_views`, `panels::symbols::tests::panel_view_sets_the_view_of_each_panel`.
- **Expand of symbol instances:** Object › Expand turns selected symbol instances (without effects, not in perspective, in groups or placed documents too) into their art as Break Link to Symbol places it, one level deep, as one undo step; the Expand dialog's Fill and Stroke reach that art (`object.expand`, `crates/engine/src/tests/expand_symbols.rs`; estimated, score unchanged).
- **Reset Transformation** ([Transform symbols](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/transform-symbols.html), [Symbols panel options](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/symbols-panel-options.html)): the Symbols panel menu's Reset Transformation and the Control bar's Reset, shown while symbol instances are selected, give the selected instances their symbol's own scale again, without rotation or shear, as one undo step (`symbol.instance.resetTransform`); an instance in perspective is left as it is. VectorCraft's own rules: each instance keeps the center of its bounding box in place, reflection is removed too, and the menu item and the button are enabled only while a selected instance would change. Tests: `tests::brushsym::reset_transformation_restores_the_symbols_scale_and_keeps_the_center`, `reset_transformation_keeps_the_center_of_art_away_from_the_origin`, `reset_transformation_ignores_a_stored_size_that_is_not_positive`, `reset_transformation_leaves_unchanged_instances_and_refuses_bad_ids`, `panels::symbols::tests::reset_transformation_is_enabled_for_a_transformed_instance_and_resets_it`.
- **Expand of dimensions:** Object › Expand, Expand Appearance, Create Outlines and Outline Stroke turn a dimension whose art they change into a plain group of that art, which stays as it is through later moves, scales and edits of what it measured; undo gives the live dimension back (`crates/engine/tests/it/dimensions.rs`; VectorCraft's own rule; estimated, score unchanged).