Merge pull request #4674 from storytold/feat/control-bar-symbol-replace

Control bar: Replace menu for selected symbol instances (#4121)
This commit is contained in:
BFlat
2026-10-30 11:22:15 +07:00
committed by GitHub
28 changed files with 124 additions and 1 deletions
+1
View File
@@ -190,6 +190,7 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
let image = crate::place::control_bar_details(app, ui) || crate::panels::image_trace::control_bar(app, ui);
crate::toolbar::control_bar_options(app, ui);
crate::dialogs::envelope::control_bar(app, ui);
crate::panels::symbols::control_bar(app, ui);
let opacity = if first.is_some() { crate::panels::current_transparency(app).map_or(1.0, |t| t.0) } else { 1.0 };
if !image {
let shown_stroke = crate::panels::current_stroke(app);
+1
View File
@@ -2583,6 +2583,7 @@ font-technology Variable Променливи
Repeat Tab Повтаряне на табулатора
Repeat {name} Повтаряне на {name}
Replace Замяна
Replace: Замяна:
Replace Font Замяна на шрифт
Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Заменете го с друг файл или го игнорирайте: неговите изображения ще продължат да показват записания с документа преглед.
Replace Spine Замяна на оста
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3285,6 +3285,7 @@
Repeat {name} Repeteix {name}
Repeating Repetició
Replace Reemplaça
Replace: Reemplaça:
Replace Font Reemplaça el tipus de lletra
Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Reemplaça’l per un altre fitxer o ignora’l: les seves imatges continuaran mostrant la previsualització desada amb el document.
Replace Spine Reemplaça l’espina
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3800,6 +3800,7 @@ axis Both Obě
Set Text Nastavit text
Insert Character Vložit znak
Replace Nahradit
Replace: Nahradit:
Type on a Path Options Možnosti textu na cestě
Highlight Variable Zvýraznit proměnnou
New Variable Nová proměnná
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3537,6 +3537,7 @@ axis Both Begge
Set Text Angiv tekst
Insert Character Indsæt tegn
Replace Erstat
Replace: Erstat:
Type on a Path Options Indstillinger for tekst på kurve
Highlight Variable Fremhæv variabel
New Variable Ny variabel
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2603,6 +2603,7 @@ mark-style Japanese Japanisch
Repeat Tab Tabulator wiederholen
Repeat {name} {name} wiederholen
Replace Ersetzen
Replace: Ersetzen:
Replace Font Schrift ersetzen
Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Ersetzen Sie sie durch eine andere Datei oder ignorieren Sie sie: Ihre Bilder zeigen weiterhin die mit dem Dokument gespeicherte Vorschau.
Replace Spine Achse ersetzen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1798,6 +1798,7 @@ font-technology Variable Μεταβλητές
Rename Μετονομασία
Repeat Επανάληψη
Replace Αντικατάσταση
Replace: Αντικατάσταση:
Replace… Αντικατάσταση…
Reselect Επαναφορά προηγούμενης επιλογής
Reset Επαναφορά
Can't render this file because it contains an unexpected character in line 164 and column 5.
+1
View File
@@ -2641,6 +2641,7 @@ font-technology Variable Variables
Repeat Tab Repetir tabulación
Repeat {name} Repetir {name}
Replace Reemplazar
Replace: Reemplazar:
Replace Font Reemplazar fuente
Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Reemplázalo con otro archivo o ignóralo: sus imágenes seguirán mostrando la vista previa guardada con el documento.
Replace Spine Reemplazar eje
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3720,6 +3720,7 @@ axis Both Molemmat
Set Text Aseta teksti
Insert Character Lisää merkki
Replace Korvaa
Replace: Korvaa:
Type on a Path Options Polulla olevan tekstin asetukset
Highlight Variable Korosta muuttuja
New Variable Uusi muuttuja
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2638,6 +2638,7 @@ font-technology Variable Variables
Repeat Tab Répéter la tabulation
Repeat {name} Répéter {name}
Replace Remplacer
Replace: Remplacer :
Replace Font Remplacer la police
Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Remplacez-le par un autre fichier ou ignorez-le : ses images continueront d’afficher l’aperçu enregistré avec le document.
Replace Spine Remplacer la dorsale
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3169,6 +3169,7 @@ mark-style Japanese Japán
Repeat Tab Tabulátor ismétlése
Repeat {name} Ismétlés: {name}
Replace Csere
Replace: Csere:
Replace Font Betűtípus cseréje
Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Cseréld le egy másik fájlra, vagy hagyd ki: a képei a dokumentummal mentett előnézetet mutatják tovább.
Replace Spine Gerinc cseréje
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3537,6 +3537,7 @@ axis Both Keduanya
Set Text Atur teks
Insert Character Sisipkan karakter
Replace Ganti
Replace: Ganti:
Type on a Path Options Opsi teks pada jalur
Highlight Variable Sorot variabel
New Variable Variabel baru
Can't render this file because it contains an unexpected character in line 2169 and column 5.
+1
View File
@@ -2672,6 +2672,7 @@ mark-style Japanese Giapponese
Repeat Tab Ripeti tabulazione
Repeat {name} Ripeti {name}
Replace Sostituisci
Replace: Sostituisci:
Replace Font Sostituisci font
Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Sostituiscilo con un altro file oppure ignoralo: le sue immagini continueranno a mostrare l'anteprima salvata con il documento.
Replace Spine Sostituisci dorsale
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2652,6 +2652,7 @@ mark-style Japanese 日本式
Repeat Tab タブを繰り返し
Repeat {name} {name}を繰り返す
Replace 置換
Replace: 置換:
Replace Font フォントを置換
Replace Symbol シンボルを置き換え
Replace With Font 置換するフォント
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3322,6 +3322,7 @@ axis Both ორივე
Set Text ტექსტის დაყენება
Insert Character სიმბოლოს ჩასმა
Replace ჩანაცვლება
Replace: ჩანაცვლება:
Type on a Path Options ტრაექტორიაზე ტექსტის პარამეტრები
Highlight Variable ცვლადის გამოკვეთა
New Variable ახალი ცვლადი
Can't render this file because it is too large.
+1
View File
@@ -2639,6 +2639,7 @@ mark-style Japanese 일본식
Repeat Tab 탭 반복
Repeat {name} {name} 반복
Replace 바꾸기
Replace: 바꾸기:
Replace Font 글꼴 바꾸기
Replace Symbol 심볼 바꾸기
Replace With Font 바꿀 글꼴
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3551,6 +3551,7 @@ axis Both Beide
Set Text Tekst instellen
Insert Character Teken invoegen
Replace Vervangen
Replace: Vervangen:
Type on a Path Options Opties voor tekst op pad
Highlight Variable Variabele markeren
New Variable Nieuwe variabele
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3642,6 +3642,7 @@ axis Both Obie
Set Text Ustaw tekst
Insert Character Wstaw znak
Replace Zastąp
Replace: Zastąp:
Type on a Path Options Opcje tekstu na ścieżce
Highlight Variable Wyróżnij zmienną
New Variable Nowa zmienna
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2179,6 +2179,7 @@
Repeat Tab Repetir Parada de Tabulação
Repeat {name} Repetir {name}
Replace Substituir
Replace: Substituir:
Replace Font Substituir Fonte
Replace Spine Substituir Coluna
Replace Symbol Substituir Símbolo
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -2668,6 +2668,7 @@ mark-style Japanese Японский
Repeat Tab Повторить табуляцию
Repeat {name} Повторить {name}
Replace Заменить
Replace: Заменить:
Replace Font Заменить шрифт
Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Замените его другим файлом или игнорируйте: его изображения продолжат показывать предварительный просмотр, сохранённый с документом.
Replace Spine Заменить остов
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2583,6 +2583,7 @@ font-technology Variable Variabla
Repeat Tab Upprepa tabb
Repeat {name} Upprepa {name}
Replace Ersätt
Replace: Ersätt:
Replace Font Ersätt typsnitt
Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Ersätt den med en annan fil eller ignorera den: dess bilder fortsätter att visa förhandsgranskningen som sparats med dokumentet.
Replace Spine Ersätt ryggrad
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -2460,6 +2460,7 @@ shape Open Açık
Repeat Tab Sekmeyi Tekrarla
Repeat {name} {name}'yi tekrarla
Replace Değiştir
Replace: Değiştir:
Replace Font Yazı Tipini Değiştir
Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Onu başka bir dosyayla değiştirin veya yok sayın: resimleri, belgeyle birlikte kaydedilen önizlemeyi göstermeye devam eder.
Replace Spine Omurgayı Değiştir
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -2664,6 +2664,7 @@ mark-style Japanese Японський
Repeat Tab Повторити табуляцію
Repeat {name} Повторити {name}
Replace Замінити
Replace: Замінити:
Replace Font Замінити шрифт
Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Замініть його іншим файлом або ігноруйте: зображення продовжать показувати попередній перегляд, збережений з документом.
Replace Spine Замінити вісь
Can't render this file because it contains an unexpected character in line 178 and column 5.
+1
View File
@@ -2935,6 +2935,7 @@ mark-style Japanese Kiểu Nhật
Repeat Tab Tab lặp lại
Repeat {name} Lặp lại {name}
Replace Thay thế
Replace: Thay thế:
Replace Font Thay thế phông chữ
Replace it with another file, or ignore it: its images keep showing the preview saved with the document. Thay thế bằng tệp khác hoặc bỏ qua: ảnh của nó vẫn hiển thị bản xem trước đã lưu cùng tài liệu.
Replace Spine Thay thế trục
Can't render this file because it contains an unexpected character in line 200 and column 5.
+1
View File
@@ -2541,6 +2541,7 @@ font-script Japanese 汉字
Repeat Tab 重复制表位
Repeat {name} 重复 {name}
Replace 替换
Replace: 替换:
Replace Font 替换字体
Replace it with another file, or ignore it: its images keep showing the preview saved with the document. 将其替换为其他文件,或忽略:其图像继续显示随文档保存的预览。
Replace Spine 替换脊线
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3478,6 +3478,7 @@ font-script Japanese 漢字
Repeat Tab 重複定位點
Repeat {name} 重複 {name}
Replace 取代
Replace: 取代:
Replace Font 取代字體
Replace it with another file, or ignore it: its images keep showing the preview saved with the document. 以其他檔案取代,或忽略:其影像會繼續顯示隨文件儲存的預覽。
Replace Spine 取代脊線
Can't render this file because it contains an unexpected character in line 6 and column 47.
+96
View File
@@ -149,6 +149,41 @@ fn has_instances_selected(app: &VectorcraftApp) -> bool {
})
}
/// The symbols of the selected objects when every one is a symbol instance; `None` otherwise.
fn selected_instance_symbols(app: &VectorcraftApp) -> Option<Vec<String>> {
let st = app.session.active()?;
let symbols: Option<Vec<String>> = st
.selection
.objects
.iter()
.map(|id| match st.doc.node(*id).map(|n| &n.kind) {
Some(NodeKind::SymbolInstance { symbol, .. }) => Some(symbol.clone()),
_ => None,
})
.collect();
symbols.filter(|s| !s.is_empty())
}
/// The Control bar's Replace menu while symbol instances alone are selected: the document's
/// symbols, showing the one the instances share (blank when they differ); picking another
/// replaces them all, as "Select a new symbol from the Replace dropdown in the Control panel"
/// (<https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/edit-symbols.html>).
pub(crate) fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
let Some(symbols) = selected_instance_symbols(app) else { return };
let Some(st) = app.session.active() else { return };
let names: Vec<String> = st.doc.symbols.iter().map(|s| s.name.clone()).collect();
let shared = symbols.first().filter(|first| symbols.iter().all(|s| s == *first)).cloned().unwrap_or_default();
ui.label(egui::RichText::new(tl!("Replace:")).size(12.0).color(Tokens::get(ui.ctx()).text));
let options: Vec<&str> = names.iter().map(String::as_str).collect();
if let Some(name) = widgets::dropdown_names(ui, "cb-symbol-replace", &shared, &options, 140.0).and_then(|i| names.get(i))
&& *name != shared
&& let Err(e) = app.run("symbol.replace", json!({ "name": name }))
{
app.status(e);
}
ui.add_space(4.0);
}
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 };
@@ -702,4 +737,65 @@ mod tests {
};
assert!(orange(3, 3) && orange(36, 36), "{:?} {:?}", thumb.pixels[3 * 40 + 3], thumb.pixels[36 * 40 + 36]);
}
/// #4121: with symbol instances selected, the Control bar's Replace menu lists the document's
/// symbols and shows the instance's own; picking another replaces it.
#[test]
fn the_control_bar_replace_menu_swaps_the_selected_instances_symbol() {
fn bar(app: &mut VectorcraftApp, ctx: &egui::Context, events: Vec<egui::Event>) -> Vec<egui::Shape> {
let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(800.0, 400.0))), events, ..Default::default() };
let mut out = ctx.run_ui(raw, |ui| {
ui.horizontal(|ui| control_bar(app, ui));
});
out.textures_delta.clear();
out.shapes.into_iter().map(|c| c.shape).collect()
}
fn text_at(shapes: &[egui::Shape], s: &str) -> Option<Pos2> {
shapes.iter().find_map(|sh| match sh {
egui::Shape::Text(t) if t.galley.text().trim() == s => Some(t.visual_bounding_rect().center()),
egui::Shape::Vec(v) => text_at(v, s),
_ => None,
})
}
fn click(app: &mut VectorcraftApp, ctx: &egui::Context, at: Pos2) {
bar(app, ctx, vec![egui::Event::PointerMoved(at), button(at, true)]);
bar(app, ctx, vec![button(at, false)]);
}
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
for (name, x) in [("Leaf", 10), ("Star", 150)] {
app.run("shape.rectangle", json!({"x": x, "y": 10, "width": 30, "height": 30})).unwrap();
app.run("symbol.new", json!({"name": name})).unwrap();
}
let instance_of = |app: &VectorcraftApp, name: &str| {
let mut found = None;
for l in &app.session.active().unwrap().doc.layers {
l.walk(&mut |n| {
if let NodeKind::SymbolInstance { symbol, .. } = &n.kind
&& symbol == name
{
found = Some(n.id);
}
});
}
found
};
let leaf = instance_of(&app, "Leaf").expect("a Leaf instance");
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
// Art, not an instance: no Replace menu.
let rect = app.run("shape.rectangle", json!({"x": 10, "y": 100, "width": 30, "height": 30})).unwrap()["id"].as_u64().unwrap();
app.run("select.set", json!({"ids": [rect]})).unwrap();
assert!(text_at(&bar(&mut app, &ctx, vec![]), "Replace:").is_none());
app.run("select.set", json!({"ids": [leaf.0]})).unwrap();
bar(&mut app, &ctx, vec![]);
let shapes = bar(&mut app, &ctx, vec![]);
assert!(text_at(&shapes, "Replace:").is_some());
let shown = text_at(&shapes, "Leaf").expect("the menu shows the instance's symbol");
click(&mut app, &ctx, shown);
let star = text_at(&bar(&mut app, &ctx, vec![]), "Star").expect("the menu lists the other symbol");
click(&mut app, &ctx, star);
let doc = &app.session.active().unwrap().doc;
assert!(matches!(&doc.node(leaf).unwrap().kind, NodeKind::SymbolInstance { symbol, .. } if symbol == "Star"), "the instance is replaced");
}
}
+2 -1
View File
@@ -1,6 +1,6 @@
# UI and interaction parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Color Mixer popover: the Color panel's menu with its color modes (#4249); scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Control bar: a Replace menu swaps the selected symbol instances' symbol (#4121); scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
## Feature submission evidence
@@ -361,6 +361,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development
| Date | Change | Summary |
|---|---|---|
| 2026-10-30 | minor | Control bar: a Replace menu swaps the selected symbol instances' symbol (#4121); scores and estimates unchanged |
| 2026-10-30 | minor | Color Mixer popover: the Color panel's menu with its color modes (#4249); scores and estimates unchanged |
| 2026-10-30 | minor | Object › Path › Average & Join… (`path.averageJoin`, Cmd+Alt+Shift+J) moves two open path ends to their midpoint and joins them there, the joined anchor a corner or a smooth point as its dialog's Points choice asks, in one undo step; one row done; scores and estimates unchanged. |
| 2026-10-30 | minor | G12: Preferences audit (#394): 5 options are stored but not acted on (Open Documents As Tabs, Units › East Asian Type, Hyphenation › Default Language, Scratch Disks, Legacy vector clipboard), each waiting on a subsystem; Highlight Alternate Glyphs, Real-time Drawing and the Devices options act; scores and estimates unchanged |