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 <noreply@anthropic.com>

* M7: parity notes for the Edit tab turning on Edit text & images

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* chore(i18n): refresh the catalogue hashes in ATTRIBUTION.toml

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* M7: let auto-deref find the Edit panel's buttons (clippy)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: abc2cb <271337822+abc2cb@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abc2cb
2026-10-21 17:33:59 -04:00
committed by GitHub
co-authored by Claude Sonnet 5.5 abc2cb
parent b1bf1cf01f
commit 322a1b2ac0
27 changed files with 295 additions and 63 deletions
+22 -22
View File
@@ -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]]
+2
View File
@@ -2711,3 +2711,5 @@ certificate Version الإصدار
Make properties default جعل الخصائص افتراضية
Pages selected الصفحات المحددة
Page properties خصائص الصفحة
Less أقل
Other options خيارات أخرى
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -2397,3 +2397,5 @@ animation effects Off Изключени
Make properties default Задаване като свойства по подразбиране
Pages selected Избрани страници
Page properties Свойства на страницата
Less По-малко
Other options Други опции
Can't render this file because it contains an unexpected character in line 8 and column 16.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 6 and column 51.
+2
View File
@@ -2440,3 +2440,5 @@ animation effects Off Ανενεργά
Make properties default Ορισμός ιδιοτήτων ως προεπιλογή
Pages selected Επιλεγμένες σελίδες
Page properties Ιδιότητες σελίδας
Less Λιγότερα
Other options Άλλες επιλογές
Can't render this file because it contains an unexpected character in line 477 and column 9.
+2
View File
@@ -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.
Can't render this file because it contains an unexpected character in line 6 and column 60.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 6 and column 39.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 7 and column 44.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -2434,3 +2434,5 @@ animation effects Off オフ
Make properties default プロパティをデフォルトに設定
Pages selected 選択したページ
Page properties ページのプロパティ
Less 折りたたむ
Other options その他のオプション
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -2432,3 +2432,5 @@ animation effects Off Өшірулі
Make properties default Қасиеттерді әдепкі ету
Pages selected Таңдалған беттер
Page properties Бет қасиеттері
Less Азырақ
Other options Басқа параметрлер
Can't render this file because it contains an unexpected character in line 826 and column 9.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 404 and column 9.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 9 and column 16.
+2
View File
@@ -2347,3 +2347,5 @@ animation effects Off Выкл
Make properties default Сделать свойства стандартными
Pages selected Выделенные страницы
Page properties Свойства страницы
Less Меньше
Other options Другие параметры
Can't render this file because it contains an unexpected character in line 9 and column 16.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 9 and column 16.
+2
View File
@@ -2343,3 +2343,5 @@ animation effects Off ఆఫ్
Make properties default లక్షణాలను డిఫాల్ట్‌గా చేయి
Pages selected ఎంచుకున్న పేజీలు
Page properties పేజీ లక్షణాలు
Less తక్కువ
Other options ఇతర ఎంపికలు
Can't render this file because it contains an unexpected character in line 13 and column 16.
+2
View File
@@ -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
Can't render this file because it contains an unexpected character in line 825 and column 9.
+2
View File
@@ -2456,3 +2456,5 @@ animation effects Off Вимкнено
Make properties default Зробити властивості типовими
Pages selected Вибрані сторінки
Page properties Властивості сторінки
Less Менше
Other options Інші параметри
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -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.
Can't render this file because it contains an unexpected character in line 491 and column 12.
+2
View File
@@ -2398,3 +2398,5 @@ animation effects Off 关
Make properties default 将属性设为默认
Pages selected 选定的页面
Page properties 页面属性
Less 收起
Other options 其他选项
Can't render this file because it contains an unexpected character in line 9 and column 16.
+2
View File
@@ -2384,3 +2384,5 @@ animation effects Off 關
Make properties default 將內容設為預設
Pages selected 選取的頁面
Page properties 頁面屬性
Less 收起
Other options 其他選項
Can't render this file because it contains an unexpected character in line 9 and column 16.
+174 -36
View File
@@ -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::<bool>(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) {
+8 -2
View File
@@ -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]) {
+12 -3
View File
@@ -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);
}
+35
View File
@@ -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)));
}