From 322a1b2ac01a56d926406f784a48a0d5cdeda79e Mon Sep 17 00:00:00 2001 From: abc2cb Date: Thu, 22 Oct 2026 00:33:59 +0300 Subject: [PATCH] M7: lay out the Edit panel as Acrobat's (#2245) * M7: the Edit tab turns on Edit text & images, as in Acrobat Entering Edit boxes every paragraph and image on the page at once, so text is editable with one click instead of first finding Edit text & images in the tool list. The floating toolbar gets an Edit text & images button while Edit is open or the tool is in use. Leaving Edit (or turning the tool off there) applies a paragraph still open in the editor, as clicking away from it does; left open, its page kept showing the paragraph's text removed. Co-Authored-By: Claude Opus 5.5 * M7: parity notes for the Edit tab turning on Edit text & images Co-Authored-By: Claude Opus 5.5 * M7: the panel-reveal test looks for the Edit panel's own row With Edit text & images on, its name is on the floating toolbar as well as in the panel, so the label alone matched twice and no longer showed that the Tools tab was revealed. Co-Authored-By: Claude Opus 5.5 * M7: lay out the Edit panel as Acrobat's Under the Edit tab the panel is titled Edit, with Preferences and Close; Modify page is a row of icon buttons (rotate, insert, delete, extract) and Organize pages; Add content lists Text, Image, Header and footer, Watermark and Link, with the rest under More; Other options links Combine files, Redact a PDF and Prepare a form. The milestone banner and Ready chips are gone; a planned item is muted and says when it ships. Every row still comes from the catalogue. Edit text & images is on the floating toolbar. Co-Authored-By: Claude Opus 5.5 * M7: Organize pages in the Edit panel opens the Organize workspace It ran page.organize, which showed the page grid behind the Edit panel; Acrobat replaces the panel with Organize pages' own. It now selects the Organize tool, so its panel and the grid open together. Co-Authored-By: Claude Opus 5.5 * M7: translate the Edit panel's Less and Other options Every catalogue gets the two strings the Acrobat-style Edit panel adds. Co-Authored-By: Claude Opus 5.5 * chore(i18n): refresh the catalogue hashes in ATTRIBUTION.toml Co-Authored-By: Claude Opus 5.5 * M7: no idle selection hint above Format text in the Edit panel "Shift-click to select several objects. Drag empty space to select an area." sat at the top of the Edit panel whenever Edit text & images was on, pushing Format text down; Acrobat's panel has no such line. The selection count and how to move it still show while objects are selected. Tests that read the hint as "nothing selected" check that no selection count shows instead. Co-Authored-By: Claude Opus 5.5 * M7: let auto-deref find the Edit panel's buttons (clippy) Co-Authored-By: Claude Opus 5.5 * M7: the Edit panel keeps main's Add / Update / Remove menu Main gave Header and footer, Watermark and Background a menu of Add, Update and Remove in the tool panel (#1857). This branch draws the Edit group in its own panel, so those rows ran only Add and Update / Remove were unreachable there. Both panels now share one helper for the caret and the menu. Co-Authored-By: Claude Opus 5.5 * Edit panel: Modify page buttons follow command enablement; Organize pages with no document asks for a file CodeRabbit on #2245: the four Modify page buttons were enabled from the catalogue (always Ready) while execute() refuses them on a document that forbids page assembly; they now use command_enabled. Organize pages with no document open selected a panel with no grid to show; it now runs page.organize, which asks for a file first. Co-Authored-By: Claude Sonnet 5.5 --------- Co-authored-by: abc2cb <271337822+abc2cb@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 --- ATTRIBUTION.toml | 44 ++--- crates/ui-egui/src/i18n/ar.tsv | 2 + crates/ui-egui/src/i18n/bg.tsv | 2 + crates/ui-egui/src/i18n/cs.tsv | 2 + crates/ui-egui/src/i18n/de.tsv | 2 + crates/ui-egui/src/i18n/el.tsv | 2 + crates/ui-egui/src/i18n/es.tsv | 2 + crates/ui-egui/src/i18n/fr.tsv | 2 + crates/ui-egui/src/i18n/hu.tsv | 2 + crates/ui-egui/src/i18n/it.tsv | 2 + crates/ui-egui/src/i18n/ja.tsv | 2 + crates/ui-egui/src/i18n/kk.tsv | 2 + crates/ui-egui/src/i18n/lt.tsv | 2 + crates/ui-egui/src/i18n/pl.tsv | 2 + crates/ui-egui/src/i18n/pt-br.tsv | 2 + crates/ui-egui/src/i18n/ru.tsv | 2 + crates/ui-egui/src/i18n/sl.tsv | 2 + crates/ui-egui/src/i18n/te.tsv | 2 + crates/ui-egui/src/i18n/tr.tsv | 2 + crates/ui-egui/src/i18n/uk.tsv | 2 + crates/ui-egui/src/i18n/vi.tsv | 2 + crates/ui-egui/src/i18n/zh-hans.tsv | 2 + crates/ui-egui/src/i18n/zh-hant.tsv | 2 + crates/ui-egui/src/panels.rs | 210 +++++++++++++++++++---- crates/ui-egui/tests/editing.rs | 10 +- crates/ui-egui/tests/object_selection.rs | 15 +- crates/ui-egui/tests/ui.rs | 35 ++++ 27 files changed, 295 insertions(+), 63 deletions(-) diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index db3dd6124f..fbabdf6d44 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -7,7 +7,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Greek UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "6eee0b34db6529f26235bfc07fa1fdb9030b8e7b0b10ede271ca9242cb491919" +sha256 = "e2ea97b4c68e13c22ef7f6f39f9a29d21af0e562005e2fdc9e7dfdf29effe0bb" # ATTRIBUTION.toml: one entry per asset (AGENTS.md §1). The source of truth for provenance and licences. # ATTRIBUTION.md is generated from this file: `cargo xtask assets --write`. Checked by `cargo xtask assets` (part of `xtask ci`). @@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "9870c0fb9f7198155be16adee7f45d36cb9a254af90b545a3ce5b04cbbbf24d4" +sha256 = "ac0a78f36b91c005b9543e6028b78bb44949fea1bf7991bf0791d407696cd832" [[asset]] path = "crates/ui-egui/src/i18n/zh-hant.tsv" @@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "fc31e40877ce115cfb7033c57dd6acabcae3725b249364387e3fe23e04d94e5e" +sha256 = "c52b9a2aa403937e70859fe60b51719c97475c24927322d7534f5da0bdb5f420" [[asset]] path = "crates/ui-egui/src/i18n/cs.tsv" @@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "5de19834f293efaea68cb6b7f51a71d320391f53345f56cfebcd73dab828b665" +sha256 = "0dcad418fa6399955adf2a8398aec307c2c73b13a37ca78ffe142e00491ef586" [[asset]] path = "crates/ui-egui/src/i18n/pt-br.tsv" @@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "cef0d22341d0722f029c6543fc846fce164331b7ad8f693a6754b7567b9d9e95" +sha256 = "130f0256d4ba09bc9cb6a87fc973bad96d6b155703b713341f0afead9ad6b4b3" [[asset]] path = "crates/ui-egui/src/i18n/es.tsv" @@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "c0ca7761cbabd8096bc9af54cdf6b88b3ae0cd8210003db8b3356e2706e44fc8" +sha256 = "d1093647d2af3d51f77a7b53f05ef1f45442128d40e696022624fbbece5bd0f0" [[asset]] path = "crates/ui-egui/src/i18n/fr.tsv" @@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "a8afd51c3296f44b0446d6b06492b1348444b3e3c448a326ad213d18ce7af714" +sha256 = "135d00bc60ef62f45ebee7c7fd83bc7433fd4054a04fb3901b2d2e8bdd2871c7" [[asset]] path = "crates/ui-egui/src/i18n/de.tsv" @@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "7676a2dfaa7d66e2e9ef608d2d9de7f863557dcecbef6d94bafa981097405e6d" +sha256 = "0cb38f42fe45831495699f81837fb0b2d5bb16b1b69153359c4376e9bc1e6e04" [[asset]] path = "crates/ui-egui/src/i18n/te.tsv" @@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "9a22996c0308ea5b20ec5e328504cd83cbb560147763faaf0e6987f974844c29" +sha256 = "ce97821a0fd6bf758e88e654f70985ed1e94c81e36f4e31ee3fc2f05ca39d99f" [[asset]] path = "crates/ui-egui/src/i18n/it.tsv" @@ -118,7 +118,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "eacc972d65a2d951e94e1a8cf944638e8328f73f6c05759de6059a425f159cd7" +sha256 = "45567329bdb7884b0b54e5c5161e6e73452d85200945f1ba3661eab1fa35da06" [[asset]] path = "crates/ui-egui/src/i18n/pl.tsv" @@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "dfa4aa540cc7d61fa22a27a949e6eec0f78dcc5b883d0e4e004d695e460b95aa" +sha256 = "421ef329305624ebfc661a8d9cd82131e79ec279b8be47d4baec29a226e494d9" [[asset]] path = "crates/ui-egui/src/i18n/ru.tsv" @@ -140,7 +140,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "27e02e7bb3d378bceca76988a9dceaa68d974f57cbee962954deca6184cf62aa" +sha256 = "97ed35e8959777a5f15b087136e53385430241e8522c7dc72a383c72dc0a27fd" [[asset]] path = "crates/ui-egui/src/i18n/hu.tsv" @@ -151,7 +151,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "9675edd36c5ae54d9893fd10c4325aab664941bf714c08fbfeef9134fa42056c" +sha256 = "bc012e0dd9ad0d212a32b71bfc75a42ffcd058484aab28d173078384c9da480f" [[asset]] path = "crates/ui-egui/src/i18n/bg.tsv" @@ -162,7 +162,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "ce18fc96f8516aa917e4a8bcfd290edec13a9800b3d1ee102e4b2a71b8467d2b" +sha256 = "bd611d470f40a01b4442119f7189dabbe58b4904dbce398115399f02b2ecddaa" [[asset]] path = "crates/ui-egui/src/i18n/kk.tsv" @@ -173,7 +173,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "286a1e2e051748eb4bcee3906918dc78d0704f8a37bde22dc50d3376d5e75017" +sha256 = "1132e904bc1a2bcc8239987aa74b2fa75ca17d8eb20b5458eec7e2c075c0445a" [[asset]] path = "crates/ui-egui/src/i18n/ja.tsv" @@ -184,7 +184,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "77593b7497c67f4a79cc31fa43fdd6eae11601dd87c079a8e666b25cdd9117ba" +sha256 = "11dd8250c667ca49c40b827632b2d83cad93d07a9d5a5f27b5b9d657293efb6b" [[asset]] path = "crates/ui-egui/src/i18n/uk.tsv" @@ -195,7 +195,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "d7d93a03f22ab380696f69b20b12104bf30e78c4440b683df63594af716894e2" +sha256 = "6aa0788d784532fb67a0b589cecc27f727e6178221ee892c3de7fe8f75790607" [[asset]] path = "crates/ui-egui/src/i18n/ar.tsv" @@ -206,7 +206,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "889a0d0a9ea53ca0a818c2ba672c053c232d2cca140f8d63127353e6f36505fe" +sha256 = "ed84a08b83035bc6f4d328b916692d40140c2748d383741d04fba6e013a76bb3" [[asset]] path = "crates/ui-egui/src/i18n/tr.tsv" @@ -217,7 +217,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "bae8b21651ae2a69c71c91b69ba79727254dbfb078c1be312a13383332934927" +sha256 = "1a5b3e9ad3f524e04bdb60e11f4a1424ca749a5ed4840e22abeaa97d24d235e8" [[asset]] path = "crates/ui-egui/src/i18n/lt.tsv" @@ -228,7 +228,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "7fa70f68a9fa24d0b1cac05fe3aaabaeaca9750c57416124bd317a4eda806d5a" +sha256 = "136f1e68fc4caa5e6e8fe395551bffdc9ace93bb00e2da5ba59318846494420d" [[asset]] @@ -3352,7 +3352,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "95b5fc6d10f7bc8371688772741870448de716fa2dd9af62fdfe4e009d269b45" +sha256 = "052ecb803601ebc547934136c936fb5527d6dc97e49cf8cfbde0aa2b1a6cb8c7" [[asset]] path = "crates/ui-egui/src/i18n/sl.tsv" @@ -3363,7 +3363,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "3ac2b113045b7e399dd6d9ddb7773e5b0874c8b43c2ba4cca5854598a2a7ba92" +sha256 = "299c274d071478c49430d8dbc2a1979085fa4f8735324697e8ac0db3d3adce7c" [[asset]] diff --git a/crates/ui-egui/src/i18n/ar.tsv b/crates/ui-egui/src/i18n/ar.tsv index 8697887e10..4bb58ba8e1 100644 --- a/crates/ui-egui/src/i18n/ar.tsv +++ b/crates/ui-egui/src/i18n/ar.tsv @@ -2711,3 +2711,5 @@ certificate Version الإصدار Make properties default جعل الخصائص افتراضية Pages selected الصفحات المحددة Page properties خصائص الصفحة + Less أقل + Other options خيارات أخرى diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 813a2c7ae8..584fafda3f 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -2397,3 +2397,5 @@ animation effects Off Изключени Make properties default Задаване като свойства по подразбиране Pages selected Избрани страници Page properties Свойства на страницата + Less По-малко + Other options Други опции diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 9936325523..d347ca95d7 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -2508,3 +2508,5 @@ animation effects Off Vypnuto Make properties default Nastavit vlastnosti jako výchozí Pages selected Vybrané stránky Page properties Vlastnosti stránky + Less Méně + Other options Další možnosti diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 4d9207eb9d..cb1ffc2fd9 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -2435,3 +2435,5 @@ animation effects Off Aus Make properties default Eigenschaften als Standard festlegen Pages selected Ausgewählte Seiten Page properties Seiteneigenschaften + Less Weniger + Other options Weitere Optionen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 06fe2b540b..2bde0b50b9 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -2440,3 +2440,5 @@ animation effects Off Ανενεργά Make properties default Ορισμός ιδιοτήτων ως προεπιλογή Pages selected Επιλεγμένες σελίδες Page properties Ιδιότητες σελίδας + Less Λιγότερα + Other options Άλλες επιλογές diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 79c668992c..fbc9f54c98 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -2570,6 +2570,8 @@ animation effects Off Desactivados Make properties default Usar como propiedades predeterminadas Pages selected Páginas seleccionadas Page properties Propiedades de página + Less Menos + Other options Otras opciones # Historical snapshot notice bar (View signed version / View revision). You are viewing the signed version of “{from}” (revision {rev}). It is a snapshot: changes here won't affect the original. Estás viendo la versión firmada de «{from}» (revisión {rev}). Es una instantánea: los cambios aquí no afectarán al original. diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 9bdcf0bc8a..bde247a8d8 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -2434,3 +2434,5 @@ animation effects Off Désactivés Make properties default Définir comme propriétés par défaut Pages selected Pages sélectionnées Page properties Propriétés de la page + Less Moins + Other options Autres options diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index d537677c7d..16efc30a0d 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -2433,3 +2433,5 @@ animation effects Off Ki Make properties default Tulajdonságok beállítása alapértelmezettként Pages selected Kijelölt oldalak Page properties Oldaltulajdonságok + Less Kevesebb + Other options Egyéb beállítások diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 35cba7a410..c46f9cf81b 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -2646,3 +2646,5 @@ certificate Key usage Utilizzo chiave certificate Self-signed Autofirmato certificate SHA-1 digest Impronta SHA-1 certificate SHA-256 digest Impronta SHA-256 + Less Meno + Other options Altre opzioni diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index e845a736da..54bc546338 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -2434,3 +2434,5 @@ animation effects Off オフ Make properties default プロパティをデフォルトに設定 Pages selected 選択したページ Page properties ページのプロパティ + Less 折りたたむ + Other options その他のオプション diff --git a/crates/ui-egui/src/i18n/kk.tsv b/crates/ui-egui/src/i18n/kk.tsv index 9acb4ec1be..5ca11b941d 100644 --- a/crates/ui-egui/src/i18n/kk.tsv +++ b/crates/ui-egui/src/i18n/kk.tsv @@ -2432,3 +2432,5 @@ animation effects Off Өшірулі Make properties default Қасиеттерді әдепкі ету Pages selected Таңдалған беттер Page properties Бет қасиеттері + Less Азырақ + Other options Басқа параметрлер diff --git a/crates/ui-egui/src/i18n/lt.tsv b/crates/ui-egui/src/i18n/lt.tsv index d1dfc33044..2674dfc86b 100644 --- a/crates/ui-egui/src/i18n/lt.tsv +++ b/crates/ui-egui/src/i18n/lt.tsv @@ -2685,3 +2685,5 @@ key Space Tarpas The password must have at least 6 characters. Slaptažodį turi sudaryti bent 6 simboliai. This file changed on disk. Reload it to see the new version; your unsaved changes will be lost. Šis failas diske pasikeitė. Įkelkite jį iš naujo, kad pamatytumėte naują versiją; neįrašyti pakeitimai bus prarasti. theme Appearance Išvaizda + Less Mažiau + Other options Kitos parinktys diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index c432a6a92e..d6d5e5e93b 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -2410,3 +2410,5 @@ animation effects Off Wyłączone Make properties default Ustaw właściwości jako domyślne Pages selected Zaznaczone strony Page properties Właściwości strony + Less Mniej + Other options Inne opcje diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 89ff80f9dd..ee041f4af1 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2448,3 +2448,5 @@ animation effects Off Desativados Make properties default Tornar propriedades padrão Pages selected Páginas selecionadas Page properties Propriedades da página + Less Menos + Other options Outras opções diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 73f21aa0d8..72fcb1ddcf 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2347,3 +2347,5 @@ animation effects Off Выкл Make properties default Сделать свойства стандартными Pages selected Выделенные страницы Page properties Свойства страницы + Less Меньше + Other options Другие параметры diff --git a/crates/ui-egui/src/i18n/sl.tsv b/crates/ui-egui/src/i18n/sl.tsv index dd169bc961..009131307f 100644 --- a/crates/ui-egui/src/i18n/sl.tsv +++ b/crates/ui-egui/src/i18n/sl.tsv @@ -2439,3 +2439,5 @@ animation effects Off Izklopljeni Make properties default Nastavi lastnosti kot privzete Pages selected Izbrane strani Page properties Lastnosti strani + Less Manj + Other options Druge možnosti diff --git a/crates/ui-egui/src/i18n/te.tsv b/crates/ui-egui/src/i18n/te.tsv index 47aee36f8b..80008d1912 100644 --- a/crates/ui-egui/src/i18n/te.tsv +++ b/crates/ui-egui/src/i18n/te.tsv @@ -2343,3 +2343,5 @@ animation effects Off ఆఫ్ Make properties default లక్షణాలను డిఫాల్ట్‌గా చేయి Pages selected ఎంచుకున్న పేజీలు Page properties పేజీ లక్షణాలు + Less తక్కువ + Other options ఇతర ఎంపికలు diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 95ca0b608c..b185f0262e 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -2430,3 +2430,5 @@ animation effects Off Kapalı Make properties default Özellikleri varsayılan yap Pages selected Seçilen sayfalar Page properties Sayfa özellikleri + Less Daha az + Other options Diğer seçenekler diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 7993a06185..c25eba25b7 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -2456,3 +2456,5 @@ animation effects Off Вимкнено Make properties default Зробити властивості типовими Pages selected Вибрані сторінки Page properties Властивості сторінки + Less Менше + Other options Інші параметри diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index bae599bf6a..dbf9eb2245 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -2683,6 +2683,8 @@ key Space Space Words Từ “{name}” changed on disk after it was opened or last saved. Saving now replaces that newer version with this one. “{name}” đã thay đổi trên đĩa sau khi được mở hoặc lưu lần cuối. Lưu lúc này sẽ thay thế phiên bản mới hơn đó bằng phiên bản này. @id file.word_count Đếm từ… + Less Ít hơn + Other options Tùy chọn khác # Historical snapshot notice bar (View signed version / View revision). You are viewing the signed version of “{from}” (revision {rev}). It is a snapshot: changes here won't affect the original. Bạn đang xem phiên bản đã ký của “{from}” (bản sửa đổi {rev}). Đây là ảnh nhanh: các thay đổi ở đây không được áp dụng đến bản gốc. diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 1ddecc655f..a71b3f30e9 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2398,3 +2398,5 @@ animation effects Off 关 Make properties default 将属性设为默认 Pages selected 选定的页面 Page properties 页面属性 + Less 收起 + Other options 其他选项 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index bb5ec3649b..630bfbf34b 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2384,3 +2384,5 @@ animation effects Off 關 Make properties default 將內容設為預設 Pages selected 選取的頁面 Page properties 頁面屬性 + Less 收起 + Other options 其他選項 diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index 0f2c601497..8d6660b643 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -2,7 +2,7 @@ //! right-hand panels (Comments, Bookmarks, Pages, Fields, Layers, Attachments). use egui::{Align, Align2, Color32, CornerRadius, Layout, Rect, Sense, Stroke, pos2, vec2}; -use pdfcraft_engine::catalog::{self, Availability, TOOL_GROUPS, ToolGroup}; +use pdfcraft_engine::catalog::{self, Availability, TOOL_GROUPS, ToolGroup, ToolItem}; use pdfcraft_render::{DocInfo, FieldKind, OutlineItem}; use crate::theme::{self, Tokens}; @@ -144,6 +144,10 @@ fn tool_row(ui: &mut egui::Ui, t: &Tokens, g: &ToolGroup) -> egui::Response { } fn tool_detail(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens, g: &'static ToolGroup) { + if g.id == "edit" { + edit_panel(app, ui, t, g); + return; + } let (back, close) = panel_header(ui, t, g.label, true); if back { app.left = LeftPanel::AllTools; @@ -185,20 +189,6 @@ fn tool_detail(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens, g: &'static }); ui.add_space(6.0); } - // Edit a PDF shows Format text at the top while text is selected or being added. - if g.id == "edit" { - if app.quick_tool == crate::QuickTool::EditText { - let count = app.active_ids().map_or(0, |(i, _)| app.views[i].objects.count()); - if count > 0 { - ui.label(crate::i18n::trn(crate::i18n::current(), count as u64, "{n} object selected", "{n} objects selected")); - ui.label(tl!("Drag the selection to move it. Esc clears the selection.")); - } else { - ui.label(tl!("Shift-click to select several objects. Drag empty space to select an area.")); - } - ui.add_space(6.0); - } - format_section(app, ui, t); - } let mut run = None; // Redact a PDF has Acrobat's footer: Clear all / Redact all. let footer = g.id == "redact"; @@ -238,31 +228,13 @@ fn tool_detail(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens, g: &'static ui.painter().rect_filled(r, CornerRadius::same(4), fill); ui.painter().text(r.center(), Align2::CENTER_CENTER, chip_label, font, cfg); let resp = theme::hand(resp).on_hover_text(format!("{} — {}", tl!(item.label), item.command)); + // An entry with further commands opens a menu; its caret goes before the chip. if item.actions.is_empty() { if resp.clicked() { run = Some(item.command); } - } else { - // A caret before the chip says the entry opens a menu (Add / Update / Remove). - let c = r.left_center() - vec2(10.0, 0.0); - ui.painter().add(egui::Shape::convex_polygon( - vec![c + vec2(-3.5, -2.0), c + vec2(3.5, -2.0), c + vec2(0.0, 2.5)], - fg, - egui::Stroke::NONE, - )); - egui::Popup::menu(&resp).show(|ui| { - ui.set_min_width(180.0); - for (n, id) in item.actions.iter().enumerate() { - let Some(spec) = pdfcraft_engine::commands::command(id) else { continue }; - // Add stays usable with no document open: it asks for a file first. - let enabled = (n == 0 && app.active.is_none()) || app.command_enabled(spec); - let label = crate::i18n::menu_label(spec.id, spec.label); - if ui.add_enabled(enabled, egui::Button::new(label)).clicked() { - run = Some(spec.id); - ui.close(); - } - } - }); + } else if let Some(cmd) = actions_menu(app, ui, &resp, item, r.left_center() - vec2(10.0, 0.0), fg) { + run = Some(cmd); } } } @@ -301,6 +273,172 @@ fn tool_detail(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens, g: &'static } } +/// The page operations Edit shows as a row of icon buttons, as Acrobat's Edit panel does. +const EDIT_PAGE_BUTTONS: [&str; 4] = ["page.rotate", "page.insert", "page.delete", "page.extract"]; +/// Add content items shown before More. +const EDIT_CONTENT_SHOWN: usize = 5; +/// Other options: the tools Acrobat's Edit panel links to. +const EDIT_OTHER_TOOLS: [&str; 3] = ["combine", "redact", "form"]; + +/// Edit a PDF, laid out as Acrobat's Edit panel: the title with Preferences and Close; Format text +/// while text is being edited or added; Modify page as a row of icon buttons and Organize pages; +/// Add content with the rarer items under More; and Other options, the neighbouring tools. +/// Edit text & images itself is on the floating toolbar, and on from the moment Edit opens. +/// Every item comes from the catalogue, so its label, icon and availability have one source. +fn edit_panel(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens, g: &'static ToolGroup) { + let items = || g.sections.iter().flat_map(|s| s.items.iter()); + let item = |command: &str| items().find(|i| i.command == command); + let mut run = None; + let mut tool = None; + // Back to All tools only when Edit was opened from there; the Edit tab is its own way back. + let from_all_tools = app.mode != crate::Mode::Edit; + ui.horizontal(|ui| { + if from_all_tools && icons::button(ui, "chevron-left", 26.0, false, tl!("Back to all tools")).clicked() { + app.left = LeftPanel::AllTools; + app.mode = crate::Mode::AllTools; + } + // "Edit" under the Edit tab, as Acrobat titles it; the tool's own name from All tools. + let title = if from_all_tools { tl!(g.label) } else { tl!("Edit") }; + let title_width = (ui.available_width() - 2.0 * (26.0 + ui.spacing().item_spacing.x)).max(1.0); + ui.add_sized([title_width, 26.0], egui::Label::new(egui::RichText::new(title).font(theme::semibold(15.5)).color(t.text)).truncate()) + .on_hover_text(title); + if icons::button(ui, "settings", 26.0, false, tl!("Preferences")).clicked() { + run = Some("app.preferences"); + } + if icons::button(ui, "x", 26.0, false, tl!("Close panel")).clicked() { + app.left_open = false; + } + }); + ui.add_space(2.0); + ui.separator(); + ui.add_space(4.0); + // Several objects selected: how many, and how to move them. Acrobat's panel has no idle hint. + let count = app.active_ids().filter(|_| app.quick_tool == crate::QuickTool::EditText).map_or(0, |(i, _)| app.views[i].objects.count()); + if count > 0 { + ui.label(crate::i18n::trn(crate::i18n::current(), count as u64, "{n} object selected", "{n} objects selected")); + ui.label(tl!("Drag the selection to move it. Esc clears the selection.")); + ui.add_space(6.0); + } + // Format text at the top while text is selected or being added. + format_section(app, ui, t); + egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| { + widgets::section_title(ui, tl!("Modify page")); + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 6.0; + for i in EDIT_PAGE_BUTTONS.iter().filter_map(|c| item(c)) { + // Enabled as the command itself is: a document that forbids page assembly refuses these. + let enabled = pdfcraft_engine::commands::command(i.command).is_some_and(|spec| app.command_enabled(spec)); + let resp = ui.add_enabled_ui(enabled, |ui| icons::button(ui, i.icon, 34.0, false, tl!(i.label))).inner; + if resp.clicked() { + run = Some(i.command); + } + } + }); + ui.add_space(2.0); + // Organize pages is a workspace of its own: its panel replaces this one, with the grid. + if let Some(i) = item("page.organize") + && edit_row(ui, t, i.icon, tl!(i.label), i.availability).clicked() + { + // With no document there is no grid to open: the command asks for a file first. + if app.active.is_some() { + tool = Some("organize"); + } else { + run = Some("page.organize"); + } + } + + widgets::section_title(ui, tl!("Add content")); + let content: Vec<_> = + g.sections.iter().filter(|s| s.title == "Add content").flat_map(|s| s.items.iter()).filter(|i| i.command != "edit.edit_text").collect(); + let more_id = egui::Id::new("edit_panel_more"); + let more = ui.data(|d| d.get_temp::(more_id)).unwrap_or(false); + let shown = if more { content.len() } else { EDIT_CONTENT_SHOWN.min(content.len()) }; + for i in content.iter().take(shown) { + let resp = edit_row(ui, t, i.icon, tl!(i.label), i.availability); + // Header and footer, Watermark and Background open their Add / Update / Remove menu. + if i.actions.is_empty() { + if resp.clicked() { + run = Some(i.command); + } + } else if let Some(cmd) = actions_menu(app, ui, &resp, i, resp.rect.right_center() - vec2(14.0, 0.0), t.text) { + run = Some(cmd); + } + } + if content.len() > EDIT_CONTENT_SHOWN { + ui.add_space(4.0); + let label = if more { tl!("Less") } else { tl!("More") }; + let resp = ui + .add( + egui::Label::new(egui::RichText::new(label).color(t.accent_text).font(theme::medium(13.0))) + .selectable(false) + .sense(Sense::click()), + ) + .on_hover_cursor(egui::CursorIcon::PointingHand); + if resp.clicked() { + ui.data_mut(|d| d.insert_temp(more_id, !more)); + } + } + + widgets::section_title(ui, tl!("Other options")); + for other in EDIT_OTHER_TOOLS.iter().filter_map(|id| catalog::group(id)) { + if edit_row(ui, t, other.icon, tl!(other.label), other.availability).clicked() { + tool = Some(other.id); + } + } + }); + if let Some(id) = tool { + app.select_tool(id); + } + if let Some(cmd) = run { + app.run_command(cmd); + } +} + +/// A tool entry with further commands (Add / Update / Remove): a caret at `caret` says it opens a +/// menu, and each command is enabled by its own needs. Returns the command chosen. +fn actions_menu(app: &PdfCraftApp, ui: &egui::Ui, resp: &egui::Response, item: &ToolItem, caret: egui::Pos2, color: Color32) -> Option<&'static str> { + ui.painter().add(egui::Shape::convex_polygon( + vec![caret + vec2(-3.5, -2.0), caret + vec2(3.5, -2.0), caret + vec2(0.0, 2.5)], + color, + egui::Stroke::NONE, + )); + let mut run = None; + egui::Popup::menu(resp).show(|ui| { + ui.set_min_width(180.0); + for (n, id) in item.actions.iter().enumerate() { + let Some(spec) = pdfcraft_engine::commands::command(id) else { continue }; + // Add stays usable with no document open: it asks for a file first. + let enabled = (n == 0 && app.active.is_none()) || app.command_enabled(spec); + let label = crate::i18n::menu_label(spec.id, spec.label); + if ui.add_enabled(enabled, egui::Button::new(label)).clicked() { + run = Some(spec.id); + ui.close(); + } + } + }); + run +} + +/// One row of the Edit panel: a monochrome icon and the label, as Acrobat draws them. A planned +/// item is muted and says when it ships when hovered. +fn edit_row(ui: &mut egui::Ui, t: &Tokens, icon: &str, label: &str, availability: Availability) -> egui::Response { + let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 34.0), Sense::click()); + resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, label)); + let press = theme::Press::track(ui, &resp); + press.wash(ui, rect, 6, false); + let body = rect.translate(press.offset()); + let ready = availability == Availability::Ready; + let fg = if ready { t.text } else { t.text_muted }; + icons::paint(ui, Rect::from_min_size(body.min + vec2(6.0, 8.0), vec2(18.0, 18.0)), icon, 17.0, if ready { t.text } else { t.text_faint }); + row_label(ui, label, theme::regular(13.5), fg, body.left_center() + vec2(34.0, 0.0), rect.width() - 42.0); + let resp = theme::hand(resp); + match availability { + Availability::Ready => resp, + Availability::Planned(m) => resp.on_hover_text(format!("{} {m}", tl!("Planned for milestone"))), + Availability::Provider => resp.on_hover_text(tl!("Optional: needs an AI provider you configure")), + } +} + /// Add a stamp: Dynamic, Sign Here and Standard Business stamps; click one, then click on the /// page to place it. fn stamp_palette(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens) { diff --git a/crates/ui-egui/tests/editing.rs b/crates/ui-egui/tests/editing.rs index d41041e52e..1890c1c320 100644 --- a/crates/ui-egui/tests/editing.rs +++ b/crates/ui-egui/tests/editing.rs @@ -3256,11 +3256,17 @@ fn paragraph_drag_points(h: &Harness<'static, PdfCraftApp>, text: &str, offset: fn drag_paragraph(h: &mut Harness<'static, PdfCraftApp>, text: &str, offset: [f64; 2]) { let (from, to) = paragraph_drag_points(h, text, offset); - h.get_by_label("Shift-click to select several objects. Drag empty space to select an area."); + assert!( + h.query_all_by_label_contains("object selected").next().is_none() && h.query_all_by_label_contains("objects selected").next().is_none(), + "no objects selected" + ); assert!(h.state().views[0].line_editor.is_none()); drag(h, from, to); assert!(h.state().views[0].line_editor.is_none(), "a body drag must not open the typing editor"); - h.get_by_label("Shift-click to select several objects. Drag empty space to select an area."); + assert!( + h.query_all_by_label_contains("object selected").next().is_none() && h.query_all_by_label_contains("objects selected").next().is_none(), + "no objects selected" + ); } fn assert_dragged_paragraphs(actual: &[pdfcraft_engine::TextBlock], expected: &[pdfcraft_engine::TextBlock]) { diff --git a/crates/ui-egui/tests/object_selection.rs b/crates/ui-egui/tests/object_selection.rs index 343483378c..ceb700a1c0 100644 --- a/crates/ui-egui/tests/object_selection.rs +++ b/crates/ui-egui/tests/object_selection.rs @@ -146,7 +146,10 @@ fn marquee_and_mixed_group_drag_move_once_and_undo_together() { h.key_press_modifiers(Modifiers::COMMAND, Key::Z); h.run_steps(4); assert_eq!(objects(&h), before); - h.get_by_label("Shift-click to select several objects. Drag empty space to select an area."); + assert!( + h.query_all_by_label_contains("object selected").next().is_none() && h.query_all_by_label_contains("objects selected").next().is_none(), + "no objects selected" + ); h.key_press_modifiers(Modifiers::COMMAND | Modifiers::SHIFT, Key::Z); h.run_steps(4); assert_eq!(objects(&h), after); @@ -171,7 +174,10 @@ fn modifier_clicks_extend_an_ordinary_selection_and_toggle_objects() { assert!(!h.state().session.get(h.state().views[0].id).unwrap().dirty); h.key_press(Key::Escape); h.run_steps(3); - h.get_by_label("Shift-click to select several objects. Drag empty space to select an area."); + assert!( + h.query_all_by_label_contains("object selected").next().is_none() && h.query_all_by_label_contains("objects selected").next().is_none(), + "no objects selected" + ); } #[test] @@ -221,7 +227,10 @@ fn a_document_change_invalidates_old_selection_before_the_next_drag() { .apply(id, Edit::AddText { page: 0, text: AddedText { text: "A new item".into(), rect: [40.0, 210.0, 150.0, 240.0], ..Default::default() } }) .unwrap(); h.run_steps(4); - h.get_by_label("Shift-click to select several objects. Drag empty space to select an area."); + assert!( + h.query_all_by_label_contains("object selected").next().is_none() && h.query_all_by_label_contains("objects selected").next().is_none(), + "no objects selected" + ); assert_eq!(objects(&h).len(), 5); } diff --git a/crates/ui-egui/tests/ui.rs b/crates/ui-egui/tests/ui.rs index d30ab1b8dd..118dee8f74 100644 --- a/crates/ui-egui/tests/ui.rs +++ b/crates/ui-egui/tests/ui.rs @@ -1425,3 +1425,38 @@ fn the_edit_tab_turns_on_edit_text_and_images() { h.run_steps(3); assert_eq!(h.state().quick_tool, QuickTool::Hand); } + +/// Organize pages in the Edit panel opens the Organize workspace: its own panel and the page grid, +/// not the grid behind the Edit panel. +#[test] +fn organize_pages_from_the_edit_panel_opens_its_panel() { + use pdfcraft_ui_egui::LeftPanel; + let mut h = harness(|app| app.open_bytes("fixture.pdf", None, FIXTURE.to_vec()).expect("fixture opens")); + h.get_by_label("Edit").click(); + h.run_steps(3); + assert_eq!(h.state().left, LeftPanel::Tool("edit")); + assert!( + h.query_by_label("Shift-click to select several objects. Drag empty space to select an area.").is_none(), + "no idle hint above Format text, as in Acrobat" + ); + h.get_by_label("Organize pages").click(); + h.run_steps(3); + assert_eq!(h.state().left, LeftPanel::Tool("organize")); + assert!(h.state().views[0].organize, "the page grid is showing"); +} + +/// Header and footer, Watermark and Background keep their Add / Update / Remove menu in the Edit +/// panel, as under All tools: Update and Remove wait for the document to have that kind of mark. +#[test] +fn the_edit_panel_keeps_the_add_update_remove_menu() { + let mut h = harness(|app| app.open_bytes("fixture.pdf", None, FIXTURE.to_vec()).expect("fixture opens")); + h.get_by_label("Edit").click(); + h.run_steps(3); + h.get_by_label("Watermark").click(); + h.run_steps(3); + assert_eq!(enabled(&h, "Add watermark…"), Some(true)); + assert_eq!(enabled(&h, "Remove watermark"), Some(false), "nothing to remove yet"); + h.get_by_label("Add watermark…").click(); + h.run_steps(3); + assert_eq!(h.state().dialog, Some(pdfcraft_ui_egui::Dialog::Marks(pdfcraft_engine::MarkKind::Watermark))); +}