mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 09:39:52 +00:00
Window menu: a panel's entry shows the panel Tab hid instead of closing it
The Window menu checked a panel whenever it was in front in the layout, even while Tab, Shift+Tab or Presentation Mode hid every panel, so choosing it, or pressing its function key, closed the panel it was meant to show. - A new panel_docking::visible adds the dock and Presentation Mode to showing. The Window menu's check marks and the toggle use it, and window.panel puts a flyout or floating panel away only while it can be seen. With the dock hidden, the dock shows again with the panel in its earlier place; the toolbar and the Control bar stay as they are. - In Presentation Mode window.panel fails with "panels can't be shown in Presentation Mode" and changes nothing. - reveal_panel, the Gradient and Magic Wand tool double-click, the Character field shortcuts and the Corner and Shared Edge Merge tool items show a panel the dock hid instead of reporting it open or closing a floating Anchor Inspector. Docs: window.panel params text, docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -594,7 +594,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"window.panel",
|
||||
"Show Panel",
|
||||
"",
|
||||
"{panel: id, toggle?: bool} e.g. layers, swatches, stroke (case-insensitive; display labels like \"Layers\" work too); toggle (the Window menu) hides a panel that shows, and shows it again where it was",
|
||||
"{panel: id, toggle?: bool} e.g. layers, swatches, stroke (case-insensitive; display labels like \"Layers\" work too); toggle (the Window menu) hides a panel that can be seen, and shows it again where it was; with the dock hidden by Tab or Shift+Tab, the dock shows again with the panel in front, leaving the toolbar and Control bar as they are; fails and changes nothing in Presentation Mode, which hides every panel",
|
||||
),
|
||||
(
|
||||
"window.panel.layout",
|
||||
|
||||
@@ -20,7 +20,7 @@ const FIELD_W: f32 = 96.0;
|
||||
/// Object › Plug-ins › Path Precision Tools › Corner Tool.
|
||||
pub fn open(app: &mut VectorcraftApp) -> Result<Value, String> {
|
||||
app.select_tool(ID);
|
||||
app.run("window.panel", json!({ "panel": super::ID }))
|
||||
crate::panel_docking::reveal_panel(app, super::ID)
|
||||
}
|
||||
|
||||
/// How many corners Apply would edit: the selected paths' direct-selected ones, or all of a path
|
||||
|
||||
@@ -18,7 +18,7 @@ const LABEL_W: f32 = 80.0;
|
||||
/// Object › Plug-ins › Path Precision Tools › Shared Edge Merge Tool.
|
||||
pub fn open(app: &mut VectorcraftApp) -> Result<Value, String> {
|
||||
app.select_tool(ID);
|
||||
app.run("window.panel", json!({ "panel": super::ID }))
|
||||
crate::panel_docking::reveal_panel(app, super::ID)
|
||||
}
|
||||
|
||||
pub(super) fn section(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
|
||||
@@ -142,6 +142,24 @@ fn the_corner_tool_item_selects_the_tool_and_opens_its_panel_section() {
|
||||
assert!(!vectorcraft_tools::TOOL_GROUPS.iter().flat_map(|g| g.iter()).any(|t| t.id == "cornerTool" || t.id == "slideAnchor"));
|
||||
}
|
||||
|
||||
/// The Corner Tool and Shared Edge Merge Tool items leave a floating Anchor Inspector showing
|
||||
/// instead of closing it, and in Presentation Mode they choose the tool without failing.
|
||||
#[test]
|
||||
fn the_tool_items_keep_a_floating_anchor_inspector_showing() {
|
||||
let mut app = app();
|
||||
app.run("window.panel.float", json!({ "panel": ID })).unwrap();
|
||||
assert!(crate::panel_docking::visible(&app.ui, ID));
|
||||
for command in [super::corner::COMMAND, super::shared_edge::COMMAND] {
|
||||
app.run(command, json!({})).unwrap();
|
||||
assert!(crate::panel_docking::visible(&app.ui, ID), "{command} leaves the panel showing");
|
||||
}
|
||||
app.ui.screen_mode = 3;
|
||||
app.run(super::corner::COMMAND, json!({})).unwrap();
|
||||
assert_eq!(app.session.tool_id(), "cornerTool");
|
||||
app.ui.screen_mode = 0;
|
||||
assert!(crate::panel_docking::visible(&app.ui, ID), "the panel shows when Presentation Mode ends");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_shared_edge_item_selects_the_tool_and_opens_its_panel_section() {
|
||||
let mut app = app();
|
||||
|
||||
@@ -6046,6 +6046,7 @@ digits Malayalam Malaiàlam
|
||||
@msg unknown option `{path}.{k}` opció desconeguda `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) sobreimpressions desconegudes `{o}` (preserve, simulate o discard)
|
||||
@msg unknown panel `{raw}` panell desconegut `{raw}`
|
||||
@msg panels can't be shown in Presentation Mode els panells no es poden mostrar en el mode de presentació
|
||||
@msg unknown parameter `{key}`; did you mean `{near}`? paràmetre desconegut `{key}`; volies dir `{near}`?
|
||||
@msg unknown parameter `{key}`; the command takes no parameters paràmetre desconegut `{key}`; l’ordre no admet cap paràmetre
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} paràmetre desconegut `{key}`; l’ordre admet: {keys}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5089,6 +5089,7 @@ imageTrace Outline Obrys
|
||||
@msg unknown option `{path}.{k}` neznámá možnost `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) neznámé přetisky `{o}` (preserve, simulate nebo discard)
|
||||
@msg unknown panel `{raw}` neznámý panel `{raw}`
|
||||
@msg panels can't be shown in Presentation Mode v režimu prezentace nelze panely zobrazit
|
||||
@msg unknown preference `{key}` neznámá předvolba `{key}`
|
||||
@msg Preferences imported. Předvolby byly importovány.
|
||||
@msg not a preferences file: {e} nejde o soubor předvoleb: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4706,6 +4706,7 @@ theme Appearance Erscheinungsbild
|
||||
@msg unknown option `{path}.{k}` unbekannte Option `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) unbekannter Wert für overprints `{o}` (preserve, simulate oder discard)
|
||||
@msg unknown panel `{raw}` unbekanntes Bedienfeld `{raw}`
|
||||
@msg panels can't be shown in Presentation Mode im Präsentationsmodus können keine Bedienfelder angezeigt werden
|
||||
@msg unknown preference `{key}` unbekannte Voreinstellung `{key}`
|
||||
@msg Preferences imported. Voreinstellungen importiert.
|
||||
@msg not a preferences file: {e} keine Voreinstellungsdatei: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4730,6 +4730,7 @@ digits Malayalam Μαλαγιάλαμ
|
||||
@msg unknown option `{path}.{k}` άγνωστη επιλογή `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) άγνωστη επιλογή υπερεκτύπωσης `{o}` (preserve, simulate ή discard)
|
||||
@msg unknown panel `{raw}` άγνωστος πίνακας `{raw}`
|
||||
@msg panels can't be shown in Presentation Mode τα πλαίσια δεν μπορούν να εμφανιστούν στη λειτουργία παρουσίασης
|
||||
@msg unknown preference `{key}` άγνωστη προτίμηση `{key}`
|
||||
@msg Preferences imported. Οι προτιμήσεις εισήχθησαν.
|
||||
@msg not a preferences file: {e} δεν είναι αρχείο προτιμήσεων: {e}
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -4781,6 +4781,7 @@ axis Both Ambos
|
||||
@msg unknown option `{path}.{k}` opción desconocida `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) sobreimpresiones desconocidas `{o}` (preserve, simulate o discard)
|
||||
@msg unknown panel `{raw}` panel desconocido `{raw}`
|
||||
@msg panels can't be shown in Presentation Mode los paneles no se pueden mostrar en el modo de presentación
|
||||
@msg unknown preference `{key}` preferencia desconocida `{key}`
|
||||
@msg Preferences imported. Preferencias importadas.
|
||||
@msg not a preferences file: {e} no es un archivo de preferencias: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4991,6 +4991,7 @@ imageTrace Outline Ääriviiva
|
||||
@msg unknown option `{path}.{k}` tuntematon asetus `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) tuntematon päällepainatusten käsittely `{o}` (preserve, simulate tai discard)
|
||||
@msg unknown panel `{raw}` tuntematon paneeli `{raw}`
|
||||
@msg panels can't be shown in Presentation Mode paneeleja ei voi näyttää esitystilassa
|
||||
@msg unknown preference `{key}` tuntematon asetus `{key}`
|
||||
@msg Preferences imported. Asetukset tuotiin.
|
||||
@msg not a preferences file: {e} ei ole asetustiedosto: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4770,6 +4770,7 @@ axis Both Les deux
|
||||
@msg unknown option `{path}.{k}` option inconnue `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) surimpressions inconnues `{o}` (preserve, simulate ou discard)
|
||||
@msg unknown panel `{raw}` panneau inconnu `{raw}`
|
||||
@msg panels can't be shown in Presentation Mode les panneaux ne peuvent pas être affichés en mode Présentation
|
||||
@msg unknown preference `{key}` préférence inconnue `{key}`
|
||||
@msg Preferences imported. Préférences importées.
|
||||
@msg not a preferences file: {e} ce n’est pas un fichier de préférences : {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -8020,6 +8020,7 @@ antiAlias Sharp शार्प
|
||||
@msg unknown overprints `{v}` (preserve, simulate or discard) अज्ञात ओवरप्रिंट `{v}` (प्ररक्षित, अनुकरण या त्यागपत्र)
|
||||
@msg unknown panel अज्ञात पैनल
|
||||
@msg unknown panel `{raw}` अज्ञात पैनल `{raw}`
|
||||
@msg panels can't be shown in Presentation Mode प्रस्तुति मोड में पैनल्स नहीं दिखाए जा सकते
|
||||
@msg unknown parameter `{key}`; did you mean `{near}`? अज्ञात पैरामीटर `{key}`; क्या आपको `{near}` का मतलब है?
|
||||
@msg unknown parameter `{key}`; the command takes no parameters अज्ञात पैरामीटर `{key}`; कमांड कोई पैरामीटर नहीं लेता है
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} अज्ञात पैरामीटर `{key}`; कमांड लेता है: {keys}
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -5594,6 +5594,7 @@ imageTrace Outline Körvonal
|
||||
@msg unknown option `{path}.{k}` ismeretlen beállítás: `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) ismeretlen felülnyomás-kezelés: `{o}` (preserve, simulate vagy discard)
|
||||
@msg unknown panel `{raw}` ismeretlen panel: `{raw}`
|
||||
@msg panels can't be shown in Presentation Mode bemutató módban a panelek nem jeleníthetők meg
|
||||
@msg unknown preference `{key}` ismeretlen beállítás: `{key}`
|
||||
@msg Preferences imported. A beállítások importálva.
|
||||
@msg not a preferences file: {e} nem beállításfájl: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4832,6 +4832,7 @@ axis Both Entrambi
|
||||
@msg unknown option `{path}.{k}` opzione sconosciuta `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) valore di sovrastampa sconosciuto `{o}` (preserve, simulate o discard)
|
||||
@msg unknown panel `{raw}` pannello sconosciuto `{raw}`
|
||||
@msg panels can't be shown in Presentation Mode i pannelli non possono essere mostrati in modalità presentazione
|
||||
@msg unknown preference `{key}` preferenza sconosciuta `{key}`
|
||||
@msg Preferences imported. Preferenze importate.
|
||||
@msg not a preferences file: {e} non è un file di preferenze: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4861,6 +4861,7 @@ swatch Bright Violet ビビッドバイオレット
|
||||
@msg give only one of onto, below and above onto、below、above のうち 1 つだけを指定してください
|
||||
@msg column must be true or a number from 1 column には true または 1 以上の数値を指定してください
|
||||
@msg unknown panel `{raw}` `{raw}` は不明なパネルです
|
||||
@msg panels can't be shown in Presentation Mode プレゼンテーションモードではパネルを表示できません
|
||||
@msg x and y must both be numbers x と y はどちらも数値で指定してください
|
||||
@msg Downloaded {what} {what} をダウンロードしました
|
||||
@msg Imported {what}: {_1} {what} を読み込みました: {_1}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4526,6 +4526,7 @@ font-technology Variable ცვლადი
|
||||
@msg unknown option `{path}.{k}` უცნობი პარამეტრი `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) უცნობი ზედდაბეჭდვა `{o}` (preserve, simulate ან discard)
|
||||
@msg unknown panel `{raw}` უცნობი პანელი `{raw}`
|
||||
@msg panels can't be shown in Presentation Mode პრეზენტაციის რეჟიმში პანელების ჩვენება შეუძლებელია
|
||||
@msg unknown preference `{key}` უცნობი პარამეტრი `{key}`
|
||||
@msg Preferences imported. პარამეტრები იმპორტირებულია.
|
||||
@msg not a preferences file: {e} ეს არ არის პარამეტრების ფაილი: {e}
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -4788,6 +4788,7 @@ theme Appearance 외관
|
||||
@msg give only one of onto, below and above onto, below, above 중 하나만 지정하세요
|
||||
@msg column must be true or a number from 1 column은 true이거나 1 이상의 숫자여야 합니다
|
||||
@msg unknown panel `{raw}` 알 수 없는 패널 `{raw}`
|
||||
@msg panels can't be shown in Presentation Mode 프레젠테이션 모드에서는 패널을 표시할 수 없습니다
|
||||
@msg x and y must both be numbers x와 y는 모두 숫자여야 합니다
|
||||
@msg Downloaded {what} {what}을(를) 다운로드했습니다
|
||||
@msg Imported {what}: {_1} {what}을(를) 가져왔습니다: {_1}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4915,6 +4915,7 @@ imageTrace Outline Kontur
|
||||
@msg unknown option `{path}.{k}` nieznana opcja `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) nieznana wartość nadruków `{o}` (preserve, simulate lub discard)
|
||||
@msg unknown panel `{raw}` nieznany panel `{raw}`
|
||||
@msg panels can't be shown in Presentation Mode w trybie prezentacji nie można wyświetlić paneli
|
||||
@msg unknown preference `{key}` nieznana preferencja `{key}`
|
||||
@msg Preferences imported. Zaimportowano preferencje.
|
||||
@msg not a preferences file: {e} to nie jest plik preferencji: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4318,6 +4318,7 @@ axis Both Ambos
|
||||
@msg unknown option `{path}.{k}` opção desconhecida `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) sobreimpressões desconhecidas `{o}` (preserve, simulate ou discard)
|
||||
@msg unknown panel `{raw}` painel desconhecido `{raw}`
|
||||
@msg panels can't be shown in Presentation Mode os painéis não podem ser mostrados no Modo de Apresentação
|
||||
@msg unknown preference `{key}` preferência desconhecida `{key}`
|
||||
@msg Preferences imported. Preferências importadas.
|
||||
@msg not a preferences file: {e} não é um arquivo de preferências: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -4818,6 +4818,7 @@ axis Both Оба
|
||||
@msg unknown option `{path}.{k}` неизвестный параметр `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) неизвестные наложения `{o}` (preserve, simulate или discard)
|
||||
@msg unknown panel `{raw}` неизвестная панель `{raw}`
|
||||
@msg panels can't be shown in Presentation Mode в режиме презентации панели показать нельзя
|
||||
@msg unknown preference `{key}` неизвестная настройка `{key}`
|
||||
@msg Preferences imported. Настройки импортированы.
|
||||
@msg not a preferences file: {e} не является файлом настроек: {e}
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -4678,6 +4678,7 @@ axis Both Båda
|
||||
@msg unknown option `{path}.{k}` okänt alternativ `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) okänt värde för övertryck `{o}` (preserve, simulate eller discard)
|
||||
@msg unknown panel `{raw}` okänd panel `{raw}`
|
||||
@msg panels can't be shown in Presentation Mode paneler kan inte visas i presentationsläge
|
||||
@msg unknown preference `{key}` okänd inställning `{key}`
|
||||
@msg Preferences imported. Inställningarna har importerats.
|
||||
@msg not a preferences file: {e} inte en inställningsfil: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -4528,6 +4528,7 @@ axis Both Her İkisi
|
||||
@msg unknown option `{path}.{k}` bilinmeyen seçenek `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) bilinmeyen üst baskılar `{o}` (koru, benzet veya at)
|
||||
@msg unknown panel `{raw}` bilinmeyen panel `{raw}`
|
||||
@msg panels can't be shown in Presentation Mode Sunum Modunda paneller gösterilemez
|
||||
@msg unknown preference `{key}` bilinmeyen tercih `{key}`
|
||||
@msg Preferences imported. Tercihler içe aktarıldı.
|
||||
@msg not a preferences file: {e} tercih dosyası değil: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -4816,6 +4816,7 @@ axis Both Обидві
|
||||
@msg unknown option `{path}.{k}` невідомий параметр `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) невідомі накладення `{o}` (preserve, simulate або discard)
|
||||
@msg unknown panel `{raw}` невідома панель `{raw}`
|
||||
@msg panels can't be shown in Presentation Mode у режимі презентації панелі показати не можна
|
||||
@msg unknown preference `{key}` невідоме налаштування `{key}`
|
||||
@msg Preferences imported. Налаштування імпортовано.
|
||||
@msg not a preferences file: {e} не є файлом налаштувань: {e}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 182 and column 5.
|
@@ -5262,6 +5262,7 @@ theme Appearance Giao diện
|
||||
@msg unknown option `{path}.{k}` tùy chọn không xác định `{path}.{k}`
|
||||
@msg unknown overprints `{o}` (preserve, simulate or discard) chế độ in đè không xác định `{o}` (preserve, simulate hoặc discard)
|
||||
@msg unknown panel `{raw}` bảng không xác định `{raw}`
|
||||
@msg panels can't be shown in Presentation Mode không thể hiển thị bảng trong chế độ trình bày
|
||||
@msg unknown parameter `{key}`; did you mean `{near}`? tham số không xác định `{key}`; ý bạn là `{near}`?
|
||||
@msg unknown parameter `{key}`; the command takes no parameters tham số không xác định `{key}`; lệnh này không nhận tham số
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} tham số không xác định `{key}`; lệnh nhận: {keys}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 204 and column 5.
|
@@ -4556,6 +4556,7 @@ imageTrace Outline 轮廓
|
||||
@msg give only one of onto, below and above onto、below 和 above 只能给出一个
|
||||
@msg column must be true or a number from 1 column 必须是 true 或从 1 开始的数字
|
||||
@msg unknown panel `{raw}` 未知面板 `{raw}`
|
||||
@msg panels can't be shown in Presentation Mode 演示模式下无法显示面板
|
||||
@msg x and y must both be numbers x 和 y 都必须是数字
|
||||
@msg Downloaded {what} 已下载 {what}
|
||||
@msg Imported {what}: {_1} 已导入 {what}:{_1}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2349,6 +2349,7 @@ file-filter Templates 範本
|
||||
@msg give only one of onto, below and above onto、below 和 above 只能提供其中一個
|
||||
@msg column must be true or a number from 1 column 必須是 true 或從 1 起算的數字
|
||||
@msg unknown panel `{raw}` 未知的面板 `{raw}`
|
||||
@msg panels can't be shown in Presentation Mode 簡報模式下無法顯示面板
|
||||
@msg x and y must both be numbers x 和 y 都必須是數字
|
||||
@msg Downloaded {what} 已下載 {what}
|
||||
@msg Imported {what}: {_1} 已匯入 {what}:{_1}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -96,7 +96,7 @@ pub fn checked(app: &VectorcraftApp, id: &str, p: &Value) -> Option<bool> {
|
||||
"window.learn" => crate::panels::learn::visible(&app.ui),
|
||||
"window.panel" => {
|
||||
let panel = p.get("panel").and_then(Value::as_str).unwrap_or("");
|
||||
normalize_panel(panel).is_some_and(|id| crate::panel_docking::showing(&app.ui, id))
|
||||
normalize_panel(panel).is_some_and(|id| crate::panel_docking::visible(&app.ui, id))
|
||||
}
|
||||
"window.collapseDock" => crate::panel_docking::dock_collapsed(&app.ui),
|
||||
"window.workspace" => p.get("name").and_then(Value::as_str) == Some(app.ui.workspace.as_str()),
|
||||
|
||||
@@ -595,7 +595,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"window.panel",
|
||||
"Show Panel",
|
||||
"",
|
||||
"{panel: id, toggle?: bool} e.g. layers, swatches, stroke (case-insensitive; display labels like \"Layers\" work too); toggle (the Window menu) hides a panel that shows, and shows it again where it was",
|
||||
"{panel: id, toggle?: bool} e.g. layers, swatches, stroke (case-insensitive; display labels like \"Layers\" work too); toggle (the Window menu) hides a panel that can be seen, and shows it again where it was; with the dock hidden by Tab or Shift+Tab, the dock shows again with the panel in front, leaving the toolbar and Control bar as they are; fails and changes nothing in Presentation Mode, which hides every panel",
|
||||
),
|
||||
(
|
||||
"window.panel.layout",
|
||||
|
||||
@@ -270,6 +270,12 @@ pub(crate) fn showing(ui: &UiState, panel: &str) -> bool {
|
||||
(ui.open_panel.as_deref() == Some(panel) && iconic) || (!iconic && layout.panel_state(&panel.to_string()) == craft_ui::docking::PanelState::Front)
|
||||
}
|
||||
|
||||
/// Can `panel` be seen: [`showing`] while the dock is shown (Tab and Shift+Tab hide it) and
|
||||
/// Presentation Mode is off? The Window menu's check marks follow this.
|
||||
pub(crate) fn visible(ui: &UiState, panel: &str) -> bool {
|
||||
ui.dock && ui.screen_mode < 3 && showing(ui, panel)
|
||||
}
|
||||
|
||||
/// Can either panel that uses row thumbnails be seen, including in a floating window or flyout?
|
||||
/// An iconic group's open flyout is named for the icon that opened it; its active tab can differ.
|
||||
fn thumbnail_panel_showing(ui: &UiState, ctx: &egui::Context) -> bool {
|
||||
@@ -343,19 +349,31 @@ pub(crate) fn command(app: &mut VectorcraftApp, id: &str, params: &Value) -> Opt
|
||||
|
||||
/// `window.panel {panel}`: show a panel. One in an icon column opens (or puts away) its flyout;
|
||||
/// one in front of its floating window hides; one in a group comes to the front; one closed with a remembered place comes back there; a
|
||||
/// library panel or a panel the dock has no place for floats.
|
||||
/// library panel or a panel the dock has no place for floats. A flyout or floating panel is put
|
||||
/// away only while it can be seen ([`visible`]): with the dock hidden by Tab or Shift+Tab, the
|
||||
/// dock shows again with the panel where it was, and the toolbar and Control bar stay as they are.
|
||||
/// Presentation Mode hides every panel, so there the command fails and changes nothing.
|
||||
fn show_panel(app: &mut VectorcraftApp, params: &Value) -> Result<Value, String> {
|
||||
let raw = params.get("panel").and_then(Value::as_str).unwrap_or_default();
|
||||
let panel = normalize(raw).ok_or_else(|| format!("unknown panel `{raw}`"))?;
|
||||
if app.ui.screen_mode >= 3 {
|
||||
return Err("panels can't be shown in Presentation Mode".into());
|
||||
}
|
||||
place_panel(app, panel)
|
||||
}
|
||||
|
||||
/// [`show_panel`] for a known panel, in any screen mode.
|
||||
fn place_panel(app: &mut VectorcraftApp, panel: &str) -> Result<Value, String> {
|
||||
let was_visible = visible(&app.ui, panel);
|
||||
let layout = layout_of(&app.ui);
|
||||
app.ui.dock = true;
|
||||
if in_iconic_column(&layout, panel) {
|
||||
app.ui.open_panel = if app.ui.open_panel.as_deref() == Some(panel) { None } else { Some(panel.into()) };
|
||||
app.ui.open_panel = if was_visible { None } else { Some(panel.into()) };
|
||||
return Ok(json!({"open": app.ui.open_panel}));
|
||||
}
|
||||
// A floating panel in front of its window hides again, as an icon panel's flyout does: its
|
||||
// shortcut shows and hides it.
|
||||
if floating_window(&layout, panel).is_some() && layout.panel_state(&panel.to_string()) == craft_ui::docking::PanelState::Front {
|
||||
if was_visible && floating_window(&layout, panel).is_some() {
|
||||
return shared_command(app, "window.panel.close", &json!({"panel": panel}));
|
||||
}
|
||||
if !layout.contains(&panel.to_string()) && !app.ui.docking_hidden.contains_key(panel) && crate::panels::library_panel::parse(panel).is_some() {
|
||||
@@ -404,14 +422,15 @@ fn restore_hidden_group(app: &mut VectorcraftApp, panel: &str) -> Result<Value,
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Show `panel` as `window.panel` does, but leave it be when it already shows ([`showing`]): a
|
||||
/// command that opens a panel to work in (Edit › Color Settings…) never puts it away.
|
||||
/// Show `panel` as `window.panel` does, but leave it be when it can be seen already ([`visible`]):
|
||||
/// a command that opens a panel to work in (Edit › Color Settings…) never puts it away. In
|
||||
/// Presentation Mode the panel comes to the front of its group and shows when the mode ends.
|
||||
pub(crate) fn reveal_panel(app: &mut VectorcraftApp, panel: &str) -> Result<Value, String> {
|
||||
if showing(&app.ui, panel) {
|
||||
app.ui.dock = true;
|
||||
if visible(&app.ui, panel) {
|
||||
return Ok(json!({"open": panel}));
|
||||
}
|
||||
show_panel(app, &json!({ "panel": panel }))
|
||||
let id = normalize(panel).ok_or_else(|| format!("unknown panel `{panel}`"))?;
|
||||
place_panel(app, id)
|
||||
}
|
||||
|
||||
/// `window.collapseDock {collapsed?}`: collapse every dock column to icons, or expand the outer
|
||||
|
||||
@@ -346,6 +346,28 @@ fn a_character_field_shortcut_brings_the_floating_character_panel_forward() {
|
||||
}
|
||||
}
|
||||
|
||||
/// With the panels hidden by Tab, a Character field shortcut shows the dock with the floating
|
||||
/// Character panel, so the field it focuses can be seen.
|
||||
#[test]
|
||||
fn a_character_field_shortcut_shows_the_panels_tab_hid() {
|
||||
let mut h = Harness::new();
|
||||
let id = h.app.run("text.create", json!({"x": 20, "y": 50, "text": "AVAX"})).unwrap()["id"].clone();
|
||||
h.app.run("select.set", json!({"ids": [id]})).unwrap();
|
||||
h.app.run("window.panel.float", json!({"panel": "character"})).unwrap();
|
||||
h.frame(vec![]);
|
||||
let rect = floating_window(&h.layout(), "character").map(|w| w.rect);
|
||||
h.app.run("window.dock", json!({})).unwrap();
|
||||
assert!(!visible(&h.app.ui, "character"));
|
||||
h.app.run("type.highlightKerning", json!({})).unwrap();
|
||||
assert!(h.app.ui.dock && visible(&h.app.ui, "character"), "the panel shows");
|
||||
assert_eq!(floating_window(&h.layout(), "character").map(|w| w.rect), rect, "where it was");
|
||||
// In Presentation Mode the shortcut still focuses the field, and the panel shows when the mode ends.
|
||||
h.app.ui.screen_mode = 3;
|
||||
assert_eq!(h.app.run("type.highlightTracking", json!({})).unwrap(), json!({"field": "tracking"}));
|
||||
h.app.ui.screen_mode = 0;
|
||||
assert!(visible(&h.app.ui, "character"), "the panel stays in place");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_dock_collapses_to_icons_and_back() {
|
||||
let mut h = Harness::new();
|
||||
@@ -1112,6 +1134,144 @@ fn panel_function_keys_toggle_icon_flyouts_and_floating_groups() {
|
||||
assert!(!showing(&h.app.ui, "layers"), "the expanded dock also toggles");
|
||||
}
|
||||
|
||||
/// Window › Layers: the menu item's command and params.
|
||||
fn window_layers() -> Value {
|
||||
json!({"panel": "layers", "toggle": true})
|
||||
}
|
||||
|
||||
fn checked(h: &Harness, params: &Value) -> Option<bool> {
|
||||
crate::menus::checked(&h.app, "window.panel", params)
|
||||
}
|
||||
|
||||
/// With the panels hidden by Tab, Window › Layers has no check mark, and choosing it shows the dock
|
||||
/// with Layers where it was instead of closing the tab; the toolbar and the Control bar stay hidden
|
||||
/// until the next Tab shows them.
|
||||
#[test]
|
||||
fn window_panel_from_tab_hidden_dock_reveals_named_front_tab() {
|
||||
let mut h = Harness::new();
|
||||
let item = window_layers();
|
||||
h.app.run("window.panel", json!({"panel": "layers"})).unwrap();
|
||||
assert_eq!(checked(&h, &item), Some(true));
|
||||
let before = h.layout();
|
||||
h.app.ui.toolbar = true;
|
||||
h.app.ui.control_bar = true;
|
||||
h.app.run("window.dock", json!({})).unwrap();
|
||||
assert!(!h.app.ui.dock && !h.app.ui.toolbar && !h.app.ui.control_bar);
|
||||
assert_eq!(checked(&h, &item), Some(false), "no check mark while Tab hides the panels");
|
||||
h.app.run("window.panel", item.clone()).unwrap();
|
||||
assert!(h.app.ui.dock && visible(&h.app.ui, "layers"), "Layers shows");
|
||||
assert_eq!(h.layout(), before, "in its place");
|
||||
assert!(!h.app.ui.toolbar && !h.app.ui.control_bar, "the toolbar and the Control bar stay hidden");
|
||||
assert_eq!(checked(&h, &item), Some(true));
|
||||
h.app.run("window.dock", json!({})).unwrap();
|
||||
assert!(h.app.ui.dock && h.app.ui.toolbar && h.app.ui.control_bar, "the next Tab shows the bars the first Tab hid");
|
||||
h.app.run("window.panel", item.clone()).unwrap();
|
||||
assert!(!h.layout().contains(&"layers".to_string()), "a visible panel closes");
|
||||
assert!(h.layout().contains(&"properties".to_string()), "only the panel named");
|
||||
assert_eq!(checked(&h, &item), Some(false));
|
||||
}
|
||||
|
||||
/// With the panels hidden by Shift+Tab, Window › Layers and its plain command show a floating
|
||||
/// Layers panel again where it was instead of closing it.
|
||||
#[test]
|
||||
fn window_panel_from_shift_tab_hidden_floating_window_reveals_it() {
|
||||
let mut h = Harness::new();
|
||||
let item = window_layers();
|
||||
h.app.run("window.panel.float", json!({"panel": "layers"})).unwrap();
|
||||
h.frame(vec![]);
|
||||
let rect = floating_window(&h.layout(), "layers").map(|w| w.rect);
|
||||
assert!(rect.is_some());
|
||||
h.app.ui.toolbar = true;
|
||||
h.app.ui.control_bar = true;
|
||||
for params in [item.clone(), json!({"panel": "layers"})] {
|
||||
h.app.run("window.panels", json!({})).unwrap();
|
||||
assert!(!h.app.ui.dock && h.app.ui.toolbar && h.app.ui.control_bar);
|
||||
assert_eq!(checked(&h, &item), Some(false), "{params}: no check mark while Shift+Tab hides the panels");
|
||||
h.app.run("window.panel", params.clone()).unwrap();
|
||||
assert!(h.app.ui.dock && visible(&h.app.ui, "layers"), "{params}: Layers shows");
|
||||
assert_eq!(floating_window(&h.layout(), "layers").map(|w| w.rect), rect, "{params}: where it was");
|
||||
}
|
||||
}
|
||||
|
||||
/// While the dock is hidden, Window › Layers opens a flyout that was open in the icon column
|
||||
/// instead of putting it away; a closed panel comes back and a background tab comes to the front.
|
||||
/// With the dock showing, choosing the checked item again hides only that panel.
|
||||
#[test]
|
||||
fn window_panel_hidden_iconic_and_closed_cases() {
|
||||
let mut h = Harness::new();
|
||||
let item = window_layers();
|
||||
h.app.run("window.collapseDock", json!({"collapsed": true})).unwrap();
|
||||
h.app.run("window.panel", item.clone()).unwrap();
|
||||
assert_eq!(h.app.ui.open_panel.as_deref(), Some("layers"));
|
||||
assert_eq!(checked(&h, &item), Some(true));
|
||||
h.app.ui.dock = false;
|
||||
assert_eq!(checked(&h, &item), Some(false), "no check mark with the dock hidden");
|
||||
h.app.run("window.panel", item.clone()).unwrap();
|
||||
assert!(h.app.ui.dock, "the dock shows");
|
||||
assert_eq!(h.app.ui.open_panel.as_deref(), Some("layers"), "the flyout stays open");
|
||||
assert_eq!(checked(&h, &item), Some(true));
|
||||
h.app.run("window.panel", item.clone()).unwrap();
|
||||
assert_eq!(h.app.ui.open_panel, None, "a visible flyout puts away");
|
||||
|
||||
let mut h = Harness::new();
|
||||
let libraries = json!({"panel": "libraries", "toggle": true});
|
||||
h.app.run("window.panel.close", json!({"panel": "libraries"})).unwrap();
|
||||
h.app.run("window.panel.activate", json!({"panel": "properties"})).unwrap();
|
||||
h.app.run("window.dock", json!({})).unwrap();
|
||||
assert_eq!((checked(&h, &libraries), checked(&h, &item)), (Some(false), Some(false)));
|
||||
h.app.run("window.panel", libraries.clone()).unwrap();
|
||||
assert!(h.app.ui.dock && visible(&h.app.ui, "libraries"), "a closed panel comes back");
|
||||
h.app.run("window.panel", item.clone()).unwrap();
|
||||
assert!(visible(&h.app.ui, "layers") && !visible(&h.app.ui, "properties"), "a background tab comes to the front");
|
||||
assert_eq!((checked(&h, &libraries), checked(&h, &item)), (Some(false), Some(true)), "the tab in front is checked");
|
||||
}
|
||||
|
||||
/// F7 with the panels hidden by Tab shows the dock with the Layers group instead of hiding the
|
||||
/// group; the next F7 hides it.
|
||||
#[test]
|
||||
fn function_key_from_tab_hidden_dock_reveals_named_group() {
|
||||
let mut h = Harness::new();
|
||||
h.press(egui::Key::F7, egui::Modifiers::NONE);
|
||||
assert!(visible(&h.app.ui, "layers"));
|
||||
let before = h.layout();
|
||||
h.app.run("window.dock", json!({})).unwrap();
|
||||
h.press(egui::Key::F7, egui::Modifiers::NONE);
|
||||
assert!(h.app.ui.dock && visible(&h.app.ui, "layers"), "F7 shows the hidden group");
|
||||
assert_eq!(h.layout(), before);
|
||||
h.press(egui::Key::F7, egui::Modifiers::NONE);
|
||||
assert!(["properties", "layers", "libraries"].iter().all(|panel| !h.layout().contains(&panel.to_string())), "the next press hides the group");
|
||||
}
|
||||
|
||||
/// Presentation Mode hides every panel: no panel is checked, `window.panel` fails and changes
|
||||
/// nothing, and F7 is ignored. The other full screen modes show the panels as Normal does.
|
||||
#[test]
|
||||
fn presentation_panel_command_reports_unavailable_without_mutating_layout() {
|
||||
let mut h = Harness::new();
|
||||
let item = window_layers();
|
||||
h.app.run("window.panel", json!({"panel": "layers"})).unwrap();
|
||||
h.app.run("window.panel.close", json!({"panel": "swatches"})).unwrap();
|
||||
h.app.run("view.presentation", json!({})).unwrap();
|
||||
assert_eq!(h.app.ui.screen_mode, 3);
|
||||
assert_eq!(checked(&h, &item), Some(false), "no check mark in Presentation Mode");
|
||||
let before = (h.layout(), h.app.ui.dock, h.app.ui.open_panel.clone(), h.app.ui.docking_hidden.len());
|
||||
for params in [item.clone(), json!({"panel": "layers"}), json!({"panel": "swatches"}), json!({"panel": "brushes"})] {
|
||||
let error = h.app.run("window.panel", params.clone()).unwrap_err();
|
||||
assert_eq!(error, "panels can't be shown in Presentation Mode", "{params}");
|
||||
assert_eq!((h.layout(), h.app.ui.dock, h.app.ui.open_panel.clone(), h.app.ui.docking_hidden.len()), before, "{params} changed nothing");
|
||||
}
|
||||
h.press(egui::Key::F7, egui::Modifiers::NONE);
|
||||
assert_eq!(h.app.ui.screen_mode, 3);
|
||||
assert_eq!(h.layout(), before.0, "F7 is ignored");
|
||||
for mode in [1, 2] {
|
||||
h.app.run("view.screenMode", json!({"mode": mode})).unwrap();
|
||||
assert_eq!(checked(&h, &item), Some(true), "mode {mode}");
|
||||
h.app.run("window.panel", item.clone()).unwrap();
|
||||
assert!(!visible(&h.app.ui, "layers"), "mode {mode}: the checked item hides the panel");
|
||||
h.app.run("window.panel", item.clone()).unwrap();
|
||||
assert!(visible(&h.app.ui, "layers"), "mode {mode}: and shows it again");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn other_function_keys_and_keyboard_guards_keep_their_routes() {
|
||||
let mut h = Harness::new();
|
||||
|
||||
@@ -450,10 +450,10 @@ impl Field {
|
||||
/// Type tool edits, the panel shows, nothing takes the focus and the command fails. → `{field}`.
|
||||
pub(crate) fn highlight(app: &mut VectorcraftApp, field: Field) -> Result<Value, String> {
|
||||
// `window.panel` would hide a floating panel in front of its window, or an open flyout.
|
||||
if crate::panel_docking::showing(&app.ui, "character") {
|
||||
if crate::panel_docking::visible(&app.ui, "character") {
|
||||
app.run("window.panel.activate", json!({"panel": "character"}))?;
|
||||
} else {
|
||||
app.run("window.panel", json!({"panel": "character"}))?;
|
||||
crate::panel_docking::reveal_panel(app, "character")?;
|
||||
}
|
||||
if text_style(app).is_none() {
|
||||
app.character_focus = None;
|
||||
|
||||
@@ -416,8 +416,7 @@ pub fn open_options(app: &mut VectorcraftApp, tool: &str) -> Result<serde_json::
|
||||
Ok(json!({ "dialog": dialog }))
|
||||
}
|
||||
// The Gradient and Magic Wand tools: their panels.
|
||||
"gradient" | "magicWand" if crate::panel_docking::showing(&app.ui, tool) => Ok(json!({ "open": tool })),
|
||||
"gradient" | "magicWand" => app.run("window.panel", json!({ "panel": tool })),
|
||||
"gradient" | "magicWand" => crate::panel_docking::reveal_panel(app, tool),
|
||||
"artboard" => crate::dialogs::artboard_options::open(app),
|
||||
"eyedropper" => {
|
||||
crate::dialogs::eyedropper::open(app);
|
||||
|
||||
@@ -155,6 +155,15 @@ fn more_tools_open_their_options() {
|
||||
assert!(crate::panel_docking::showing(&app.ui, "magicWand"));
|
||||
open_options(&mut app, "magicWand").unwrap();
|
||||
assert!(crate::panel_docking::showing(&app.ui, "magicWand"));
|
||||
// With the panels hidden by Tab, the double-click shows them with the panel.
|
||||
app.run("window.dock", json!({})).unwrap();
|
||||
open_options(&mut app, "magicWand").unwrap();
|
||||
assert!(app.ui.dock && crate::panel_docking::visible(&app.ui, "magicWand"), "the Magic Wand panel shows");
|
||||
// In Presentation Mode the double-click does not fail, and the panel shows when the mode ends.
|
||||
app.ui.screen_mode = 3;
|
||||
open_options(&mut app, "magicWand").unwrap();
|
||||
app.ui.screen_mode = 0;
|
||||
assert!(crate::panel_docking::visible(&app.ui, "magicWand"), "the Magic Wand panel stays in place");
|
||||
}
|
||||
|
||||
/// A double-click at `at`, `time` seconds in (a second apart from the last).
|
||||
|
||||
@@ -1381,6 +1381,11 @@ key, the Batch dialog or `ui.action.play`), a key a command no longer reads, rec
|
||||
and the action still plays. `Session::replay` plays a journal the same way. The command still refuses an effect
|
||||
`color` or `channel` it can't read.
|
||||
|
||||
`window.panel` refuses every request in Presentation Mode, which hides every panel, and changes nothing. With
|
||||
`toggle` it hides only a panel that can be seen, which is the panel the Window menu checks. While Tab or Shift+Tab
|
||||
hides the dock, no panel is checked, and the panel shows again with the dock in its earlier place; the toolbar and the
|
||||
Control bar stay as they are.
|
||||
|
||||
The export commands limit those keys. `document.export`, `document.exportSelection`, `document.serialize`, `app.export`
|
||||
and `file.export` refuse a key that no format takes, and ignore a key that only another format takes
|
||||
(`{"format": "pdf", "quality": 80}` writes the PDF). `document.exportPdf`, `document.pdfSettings` and `pdf.preset.save`
|
||||
|
||||
@@ -890,6 +890,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Tab shows and hides the panels (in the Type tool it types a tab) without moving the keyboard focus onto a field, so the next shortcut or letter still works; in dialogs and panel fields it moves between fields, one press each — a numeric field's scrub label is not a tab stop of its own, and the Transform panel's, Properties panel's and Control bar's W/H link comes after the fields it sits between (`one_tab_moves_from_a_field_to_the_next`). A field's up/down spinner, the chevron that opens its presets (Stroke Weight, font size, Rotate, Export for Screens' scale) and the Transparency row's opacity slider chevron are not tab stops either. Alt+↓ with no other modifier in a field with presets opens them with the keyboard focus on the chosen one, instead of stepping the field down; Enter picks the focused preset without pressing a dialog's OK, and after a pick or Escape the field has the keyboard focus again (`tab_goes_from_field_to_field_past_the_spinner_and_the_preset_chevron`, `tab_goes_from_the_opacity_field_past_its_slider_chevron`, `alt_down_opens_a_fields_presets_for_the_keyboard`, `escape_closes_a_fields_presets_and_returns_to_the_field`).
|
||||
- Shift+Tab (`window.panels`) shows and hides the dock and the floating panels and leaves the toolbar and the Control bar as they are; hiding the panels with Tab or Shift+Tab also closes a panel popped out of the icon column.
|
||||
- The second Tab (`window.dock`) shows again only the bars the first Tab hid, after a restart too: a Control bar that was off stays off, a toolbar hidden with Window › Tools stays hidden, and panels shown with Shift+Tab in between stay shown.
|
||||
- While Tab or Shift+Tab hides the panels, the Window menu checks no panel, and choosing a panel there, or pressing its function key, shows the dock with that panel in its earlier place instead of closing it; the toolbar and the Control bar stay as they are. In Presentation Mode no panel is checked, and `window.panel` fails and changes nothing. A tool double-click that opens the Gradient or Magic Wand panel and a Character field shortcut also show the hidden dock (`window_panel_from_tab_hidden_dock_reveals_named_front_tab`, `window_panel_from_shift_tab_hidden_floating_window_reveals_it`, `function_key_from_tab_hidden_dock_reveals_named_group`, `presentation_panel_command_reports_unavailable_without_mutating_layout`).
|
||||
- The function key shortcuts for Brushes, Color, Layers, Info, Gradient, Stroke, Attributes, Graphic Styles, Appearance, Align, Transform, Pathfinder, Transparency, and Symbols activate a background tab on the first press, hide its expanded dock or floating group when that tab is in front, and restore the group with all its tabs together on the next press; in an icon column they open and close the flyout while keeping the icons. A reassigned chord keeps that behavior, and Window > a hidden sibling restores the group.
|
||||
- On Windows and Linux, F2, F3 and F4 run Cut, Copy and Paste alongside Ctrl+X, Ctrl+C and Ctrl+V, and in the web build they do so everywhere except macOS. The Edit menu keeps the Ctrl shortcuts, and Keyboard Shortcuts lists both keys of each row. On macOS only Cmd+X, Cmd+C and Cmd+V are the defaults. F2, F3 and F4 do not run these commands in a focused text field or while the Type tool edits text. A saved shortcut set that assigns one of these keys to another command keeps that assignment. Public reference: [Default keyboard shortcuts](https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html).
|
||||
- Cmd/Ctrl+A follows the keyboard: in a panel, dialog or search field it selects the field's text, while the Type tool edits it selects all of that text (also `select.all` over MCP), and only otherwise every object. On macOS the menu bar's Select All, Cut, Copy and Paste (keys or clicks) act on a focused field too, and elsewhere the in-window menus' do: the field that had the keyboard when the menu opened gets them, with its selection, and the keyboard back (#516). Not yet: Select All across threaded frames (one frame's text is selected).
|
||||
|
||||
Reference in New Issue
Block a user