Merge main into #1739; keep both footage re-exports (Cineon + VIDEO_EXTENSIONS) and main's gaps.md localization row with the PR's AVI row

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-30 07:44:20 +07:00
co-authored by Claude Opus 5.5
181 changed files with 8437 additions and 619 deletions
+8
View File
@@ -144,6 +144,9 @@ impl EffectcraftApp {
// commands handled here use the right catalog already.
let language = crate::i18n::language(self);
crate::i18n::set_current(language);
// #1089: a font-menu rescan runs off the UI thread. Poll it on the logic tick so the
// status and the menu update even when the window is not painting.
crate::panels::text_panels::poll_font_rescan(self, ctx);
if self.styled_language != language {
self.want_paint = true;
}
@@ -384,6 +387,11 @@ impl EffectcraftApp {
let full = ui.max_rect();
if self.dialog.is_some() {
ui.disable();
// A dialog that edits what is behind it live (Preview) leaves the window at full
// brightness: clicks still don't reach it, but the canvas is not greyed out.
if self.dialog == Some(Dialog::GradientEditor) || panels::live_preview::active(self) {
ui.set_opacity(1.0);
}
}
ui.painter().rect_filled(full, 0.0, t.app_bg);
let mut top = full.min.y;
+12
View File
@@ -56,6 +56,18 @@ impl EffectcraftApp {
&mut self.dialog_state.gradient
}
/// The open settings dialog's draft and Preview state (`panels::dialogs`): Solid, Camera,
/// Light and Composition Settings keep what OK would apply here.
pub fn dialog_state(&self) -> &panels::dialogs::DialogState {
&self.dialog_state
}
/// The open settings dialog's draft, to edit (tests and automation; the dialog's own fields
/// do the same).
pub fn dialog_state_mut(&mut self) -> &mut panels::dialogs::DialogState {
&mut self.dialog_state
}
/// Show panel `p` (opening it in its usual place if it is closed), bring it to the front and
/// give it the focus: Window ▸ `<panel>`.
pub fn show_panel(&mut self, p: PanelKind) {
+1 -1
View File
@@ -532,7 +532,7 @@ mod tests {
let item = p.add_item("long.flac", Label::SeaFoam, None, ItemKind::Footage(footage));
let mut comp = Comp::new(64, 36, FrameRate::new(30, 1), Tick::from_seconds_f64(10.0));
let layer = build::layer(&mut p, &comp, "long.flac", LayerSource::Footage { item }, (0, 0), None);
comp.layers = vec![layer];
comp.layers = vec![layer].into();
let cid = p.add_item("Comp", Label::Sandstone, None, ItemKind::Comp(comp.into()));
let src = RenderSource {
project: Arc::new(p),
+4 -4
View File
@@ -246,7 +246,7 @@ pub fn contributors_ui(ui: &mut egui::Ui, auto: &mut Registry) {
ui.horizontal_wrapped(|ui| {
ui.label(tr("Show"));
for m in NameMode::ALL {
let r = ui.selectable_label(v.names == m, m.label());
let r = ui.selectable_label(v.names == m, tr(m.label()));
auto.add(&format!("about.credits.show.{}", m.id()), r.rect, m.label());
if r.clicked() {
v.names = m;
@@ -254,9 +254,9 @@ pub fn contributors_ui(ui: &mut egui::Ui, auto: &mut Registry) {
}
ui.separator();
ui.label(tr("Sort"));
let combo = craft_ui::ComboBox::from_id_salt("credits_sort").selected_text(v.key.label().0).show_ui(ui, |ui| {
let combo = craft_ui::ComboBox::from_id_salt("credits_sort").selected_text(tr(v.key.label().0)).show_ui(ui, |ui| {
for k in SortKey::ALL {
let r = ui.selectable_label(v.key == k, k.label().0);
let r = ui.selectable_label(v.key == k, tr(k.label().0));
auto.add(&format!("about.credits.sort.{}", k.id()), r.rect, k.label().0);
if r.clicked() {
v.key = k;
@@ -309,7 +309,7 @@ fn table(ui: &mut egui::Ui, list: &[&Contributor], v: &mut View, auto: &mut Regi
egui::Grid::new("credits_table").striped(true).num_columns(SortKey::ALL.len()).show(ui, |ui| {
for k in SortKey::ALL {
let arrow = if v.key == k { if v.ascending { " ▲" } else { " ▼" } } else { "" };
let r = ui.button(RichText::new(format!("{}{arrow}", k.label().1)).strong()).on_hover_text(k.label().0);
let r = ui.button(RichText::new(format!("{}{arrow}", k.label().1)).strong()).on_hover_text(tr(k.label().0));
auto.add(&format!("about.credits.header.{}", k.id()), r.rect, k.label().0);
if r.clicked() {
if v.key == k {
+5 -1
View File
@@ -799,7 +799,11 @@ impl DockContent<PanelKind> for EffectContent<'_> {
}
fn logical_label(&self, panel: &PanelKind) -> String {
crate::i18n::command_label("window.panel", panel.title()).to_string()
let title = panel.title();
// The Window menu names most panels; `Markers` is only in the panel catalog, and the two
// panels in neither (`History`, `ScriptUI Panel`) keep their English names.
let listed = crate::i18n::command_label("window.panel", title);
if listed == title { crate::i18n::tr(title).to_string() } else { listed.to_string() }
}
/// The tab names what the panel shows ("Composition Intro"), after the label swatch of its
+775 -2
View File
@@ -15,7 +15,7 @@ pub(crate) use ui::catalog_characters;
pub(crate) use ui::{set_current, tr, tr_args};
/// Every language with a catalog, in the order Settings ▸ General ▸ Language lists them.
const CATALOG_CODES: [&str; 11] = ["cs", "de", "es", "fr", "pt-br", "ru", "uk", "ka", "ja", "zh-hans", "zh-hant"];
const CATALOG_CODES: [&str; 12] = ["cs", "de", "es", "fr", "pl", "pt-br", "ru", "uk", "ka", "ja", "zh-hans", "zh-hant"];
/// The active interface language: the stored preference when it names a catalog, otherwise `en`
/// (English is the source of every label and needs no lookup).
@@ -66,6 +66,11 @@ pub(crate) fn czech(app: &EffectcraftApp) -> bool {
language(app) == "cs"
}
/// The UI is in Polish.
pub(crate) fn polish(app: &EffectcraftApp) -> bool {
language(app) == "pl"
}
/// The UI is in Georgian.
pub(crate) fn georgian(app: &EffectcraftApp) -> bool {
language(app) == "ka"
@@ -134,6 +139,7 @@ fn supported(locale: Option<&str>) -> &'static str {
"uk" => "uk",
"es" => "es",
"cs" => "cs",
"pl" => "pl",
"ka" => "ka",
"ru" => "ru",
"de" => "de",
@@ -173,6 +179,13 @@ pub(crate) fn label<'a>(app: &EffectcraftApp, command: &str, source: &'a str) ->
_ => {}
}
}
if language == "pl" && matches!(command, "file.revealInFinder" | "layer.revealInFinder") {
match source {
"Reveal in Explorer" => return "Pokaż w Eksploratorze",
"Reveal in File Manager" => return "Pokaż w menedżerze plików",
_ => {}
}
}
if language == "ka" && matches!(command, "file.revealInFinder" | "layer.revealInFinder") {
match source {
"Reveal in Explorer" => return "Explorer-ში ჩვენება",
@@ -196,6 +209,7 @@ fn menu_catalog(language: &str) -> Option<&'static [(&'static str, &'static str,
"fr" => FRENCH,
"de" => GERMAN,
"cs" => CZECH,
"pl" => POLISH,
"ka" => GEORGIAN,
_ => return None,
})
@@ -266,6 +280,9 @@ pub(crate) fn entry(app: &EffectcraftApp, e: &MenuEntry, shown: String) -> Strin
"es" => Some(("Deshacer", "No se puede deshacer", "Rehacer", "No se puede rehacer")),
"pt-br" => Some(("Desfazer", "Não há nada para desfazer", "Refazer", "Não há nada para refazer")),
"cs" => Some(("Zpět", "Není co vrátit", "Znovu", "Není co zopakovat")),
"fr" => Some(("Annuler", "Impossible d'annuler", "Rétablir", "Impossible de rétablir")),
"de" => Some(("Rückgängig", "Rückgängig nicht möglich", "Wiederholen", "Wiederholen nicht möglich")),
"pl" => Some(("Cofnij", "Nie można cofnąć", "Ponów", "Nie można ponowić")),
// A colon before the history's (untranslated) action name: "დაბრუნება: Comp1".
"ka" => Some(("დაბრუნება:", "დაბრუნება შეუძლებელია", "ხელახლა შესრულება:", "ხელახლა შესრულება შეუძლებელია")),
// A colon before the history's (untranslated) action name: "Отменить: Comp1".
@@ -306,8 +323,23 @@ pub(crate) fn effect_category(children: &[MenuNode], source: &str) -> Option<Str
(!children.is_empty() && children.iter().all(|c| matches!(c, MenuNode::Item(e) if e.command == "effect.apply"))).then(|| effects::category(source))
}
/// Polish names for the Assign Shortcut submenus that carry a workspace's or 3D view's name.
fn polish_assign_shortcut(source: &str, shown: &str) -> Option<String> {
let named = shown.strip_prefix("Assign Shortcut to “")?;
match source {
"Assign Shortcut to Workspace" => named.strip_suffix("” Workspace").map(|name| format!("Przypisz skrót do obszaru roboczego „{name}”")),
"Assign Shortcut to 3D View" => named.strip_suffix('”').map(|name| format!("Przypisz skrót do widoku 3D „{name}”")),
_ => None,
}
}
pub(crate) fn submenu(app: &EffectcraftApp, source: &str, shown: String) -> String {
match language(app) {
"pl" => {
if let Some(label) = polish_assign_shortcut(source, &shown) {
return label;
}
}
"cs" => {
if source == "Assign Shortcut to Workspace" {
if let Some(name) = shown.strip_prefix("Assign Shortcut to “").and_then(|s| s.strip_suffix("” Workspace")) {
@@ -836,6 +868,8 @@ const SIMPLIFIED_CHINESE: &[(&str, &str, &str)] = &[
("layer.applyTextPreset", "Drop In Lines", "行落下"),
("anim.addKeyframe", "Add Keyframe", "添加关键帧"),
("keys.toggleHold", "Toggle Hold Keyframe", "切换定格关键帧"),
("keys.setMuted", "Mute Keyframes", "静音关键帧"),
("keys.setMuted", "Unmute Keyframes", "取消静音关键帧"),
("keys.interpolation", "Keyframe Interpolation...", "关键帧插值..."),
("keys.velocity", "Keyframe Velocity...", "关键帧速度..."),
("", "Keyframe Assistant", "关键帧辅助"),
@@ -1472,6 +1506,8 @@ const TRADITIONAL_CHINESE: &[(&str, &str, &str)] = &[
("layer.applyTextPreset", "Drop In Lines", "行落下"),
("anim.addKeyframe", "Add Keyframe", "新增關鍵影格"),
("keys.toggleHold", "Toggle Hold Keyframe", "切換定格關鍵影格"),
("keys.setMuted", "Mute Keyframes", "靜音關鍵影格"),
("keys.setMuted", "Unmute Keyframes", "取消靜音關鍵影格"),
("keys.interpolation", "Keyframe Interpolation...", "關鍵影格插補..."),
("keys.velocity", "Keyframe Velocity...", "關鍵影格速度..."),
("", "Keyframe Assistant", "關鍵影格輔助"),
@@ -2105,6 +2141,8 @@ const JAPANESE: &[(&str, &str, &str)] = &[
("layer.applyTextPreset", "Drop In Lines", "行をドロップイン"),
("anim.addKeyframe", "Add Keyframe", "キーフレームを追加"),
("keys.toggleHold", "Toggle Hold Keyframe", "停止したキーフレームの切り替え"),
("keys.setMuted", "Mute Keyframes", "キーフレームをミュート"),
("keys.setMuted", "Unmute Keyframes", "キーフレームのミュートを解除"),
("keys.interpolation", "Keyframe Interpolation...", "キーフレーム補間法..."),
("keys.velocity", "Keyframe Velocity...", "キーフレーム速度..."),
("", "Keyframe Assistant", "キーフレーム補助"),
@@ -2741,6 +2779,8 @@ const UKRAINIAN: &[(&str, &str, &str)] = &[
("layer.applyTextPreset", "Drop In Lines", "Поява рядків згори"),
("anim.addKeyframe", "Add Keyframe", "Додати ключовий кадр"),
("keys.toggleHold", "Toggle Hold Keyframe", "Перемкнути утримання ключового кадру"),
("keys.setMuted", "Mute Keyframes", "Заглушити ключові кадри"),
("keys.setMuted", "Unmute Keyframes", "Зняти заглушення ключових кадрів"),
("keys.interpolation", "Keyframe Interpolation...", "Інтерполяція ключових кадрів..."),
("keys.velocity", "Keyframe Velocity...", "Швидкість ключових кадрів..."),
("", "Keyframe Assistant", "Помічник ключових кадрів"),
@@ -3378,6 +3418,8 @@ const SPANISH: &[(&str, &str, &str)] = &[
("layer.applyTextPreset", "Drop In Lines", "Entrada de líneas cayendo"),
("anim.addKeyframe", "Add Keyframe", "Añadir fotograma clave"),
("keys.toggleHold", "Toggle Hold Keyframe", "Conmutar mantener fotograma clave"),
("keys.setMuted", "Mute Keyframes", "Silenciar fotogramas clave"),
("keys.setMuted", "Unmute Keyframes", "Reactivar fotogramas clave"),
("keys.interpolation", "Keyframe Interpolation...", "Interpolación de fotogramas clave..."),
("keys.velocity", "Keyframe Velocity...", "Velocidad de fotogramas clave..."),
("", "Keyframe Assistant", "Asistente de fotogramas clave"),
@@ -4013,6 +4055,8 @@ const PORTUGUESE: &[(&str, &str, &str)] = &[
("layer.applyTextPreset", "Drop In Lines", "Entrar com queda de linhas"),
("anim.addKeyframe", "Add Keyframe", "Adicionar quadro-chave"),
("keys.toggleHold", "Toggle Hold Keyframe", "Alternar quadro-chave de retenção"),
("keys.setMuted", "Mute Keyframes", "Silenciar quadros-chave"),
("keys.setMuted", "Unmute Keyframes", "Reativar quadros-chave"),
("keys.interpolation", "Keyframe Interpolation...", "Interpolação de quadro-chave..."),
("keys.velocity", "Keyframe Velocity...", "Velocidade do quadro-chave..."),
("", "Keyframe Assistant", "Assistente de quadro-chave"),
@@ -4655,6 +4699,8 @@ const RUSSIAN: &[(&str, &str, &str)] = &[
("layer.applyTextPreset", "Drop In Lines", "Появление строк сверху"),
("anim.addKeyframe", "Add Keyframe", "Добавить ключевой кадр"),
("keys.toggleHold", "Toggle Hold Keyframe", "Переключить фиксацию ключевого кадра"),
("keys.setMuted", "Mute Keyframes", "Отключить ключевые кадры"),
("keys.setMuted", "Unmute Keyframes", "Включить ключевые кадры"),
("keys.interpolation", "Keyframe Interpolation...", "Интерполяция ключевых кадров..."),
("keys.velocity", "Keyframe Velocity...", "Скорость ключевых кадров..."),
("", "Keyframe Assistant", "Помощник по ключевым кадрам"),
@@ -5290,6 +5336,8 @@ const FRENCH: &[(&str, &str, &str)] = &[
("layer.applyTextPreset", "Drop In Lines", "Chute des lignes depuis le haut"),
("anim.addKeyframe", "Add Keyframe", "Ajouter une image clé"),
("keys.toggleHold", "Toggle Hold Keyframe", "Activer/Désactiver l'image clé figée"),
("keys.setMuted", "Mute Keyframes", "Désactiver les images clés"),
("keys.setMuted", "Unmute Keyframes", "Réactiver les images clés"),
("keys.interpolation", "Keyframe Interpolation...", "Interpolation des images clés..."),
("keys.velocity", "Keyframe Velocity...", "Vitesse des images clés..."),
("", "Keyframe Assistant", "Assistant d'images clés"),
@@ -5925,6 +5973,8 @@ const GERMAN: &[(&str, &str, &str)] = &[
("layer.applyTextPreset", "Drop In Lines", "Zeilen fallen ein"),
("anim.addKeyframe", "Add Keyframe", "Keyframe hinzufügen"),
("keys.toggleHold", "Toggle Hold Keyframe", "Halte-Keyframe umschalten"),
("keys.setMuted", "Mute Keyframes", "Keyframes stummschalten"),
("keys.setMuted", "Unmute Keyframes", "Keyframes wieder aktivieren"),
("keys.interpolation", "Keyframe Interpolation...", "Keyframe-Interpolation..."),
("keys.velocity", "Keyframe Velocity...", "Keyframe-Geschwindigkeit..."),
("", "Keyframe Assistant", "Keyframe-Assistent"),
@@ -6124,6 +6174,640 @@ const GERMAN: &[(&str, &str, &str)] = &[
// <<< generated i18n catalogs
/// Polish (pl): original phrase translations of every fixed menu entry.
const POLISH: &[(&str, &str, &str)] = &[
("", "Front", "Przód"),
("", "Bevel", "Fazowanie"),
("", "Side", "Bok"),
("", "Back", "Tył"),
("layer.addTextAnimator", "Color", "Kolor"),
("", "EffectCraft", "EffectCraft"),
("app.about", "About EffectCraft...", "O programie EffectCraft..."),
("", "Settings...", "Ustawienia..."),
("app.settings", "General...", "Ogólne..."),
("app.settings", "Startup & Repair...", "Uruchamianie i naprawa..."),
("app.settings", "Project...", "Projekt..."),
("app.settings", "Composition...", "Kompozycja..."),
("app.settings", "Previews...", "Podglądy..."),
("app.settings", "Appearance...", "Wygląd..."),
("app.settings", "Grids & Guides...", "Siatki i linie pomocnicze..."),
("app.settings", "Labels...", "Etykiety..."),
("app.settings", "Type...", "Tekst..."),
("app.settings", "Import...", "Import..."),
("app.settings", "Export...", "Eksport..."),
("app.settings", "Audio...", "Audio..."),
("app.settings", "Disk...", "Dysk..."),
("app.settings", "Memory & CPU...", "Pamięć i procesor..."),
("app.settings", "Video...", "Wideo..."),
("app.settings", "3D...", "3D..."),
("app.settings", "Scripting & Expressions...", "Skrypty i wyrażenia..."),
("app.hide", "Hide EffectCraft", "Ukryj EffectCraft"),
("app.hideOthers", "Hide Others", "Ukryj pozostałe"),
("app.showAll", "Show All", "Pokaż wszystko"),
("app.quit", "Quit EffectCraft", "Zakończ EffectCraft"),
("", "File", "Plik"),
("", "New", "Nowy"),
("file.newProject", "New Project", "Nowy projekt"),
("file.newFromTemplate", "New Project from Template...", "Nowy projekt z szablonu..."),
("project.newFolder", "New Folder", "Nowy folder"),
("file.open", "Open Project...", "Otwórz projekt..."),
("", "Open Recent", "Otwórz ostatnie"),
("file.clearRecent", "Clear Recent Projects", "Wyczyść ostatnie projekty"),
("file.close", "Close", "Zamknij"),
("file.closeProject", "Close Project", "Zamknij projekt"),
("file.save", "Save", "Zapisz"),
("", "Save As", "Zapisz jako"),
("file.saveAs", "Save As...", "Zapisz jako..."),
("file.saveCopy", "Save a Copy...", "Zapisz kopię..."),
("file.saveCopyAsXml", "Save a Copy As XML...", "Zapisz kopię jako XML..."),
("templates.saveAs", "Save as Template...", "Zapisz jako szablon..."),
("file.incrementAndSave", "Increment and Save", "Zapisz z kolejnym numerem"),
("file.revert", "Revert", "Przywróć"),
("", "Import", "Importuj"),
("file.import", "File...", "Plik..."),
("file.importMultiple", "Multiple Files...", "Wiele plików..."),
("file.importTimeline", "Adobe Premiere Pro Project...", "Projekt Adobe Premiere Pro..."),
("file.importProject", "EffectCraft Project...", "Projekt EffectCraft..."),
("file.importPlaceholder", "Placeholder...", "Element zastępczy..."),
("file.importSolid", "Solid...", "Plansza..."),
("file.importLottie", "Lottie...", "Lottie..."),
("file.importVanishingPoint", "Vanishing Point (.vpe)...", "Punkt zbiegu (.vpe)..."),
("essential.importTemplate", "Essential Graphics Template...", "Szablon Essential Graphics..."),
("", "Import Recent Footage", "Importuj ostatnie materiały"),
("file.clearRecentFootage", "Clear Recent Footage", "Wyczyść ostatnie materiały"),
("", "Export", "Eksportuj"),
("renderQueue.add", "Add to Render Queue", "Dodaj do kolejki renderowania"),
("file.exportTimeline", "Adobe Premiere Pro Project...", "Projekt Adobe Premiere Pro..."),
("file.exportLottie", "Lottie JSON...", "Lottie JSON..."),
("essential.exportTemplate", "Essential Graphics Template...", "Szablon Essential Graphics..."),
("app.find", "Find", "Znajdź"),
("layer.addItem", "Add Footage to Comp", "Dodaj materiał do kompozycji"),
("file.newCompFromSelection", "New Comp from Selection...", "Nowa kompozycja z zaznaczenia..."),
("", "Dependencies", "Zależności"),
("file.collectFiles", "Collect Files...", "Zbierz pliki..."),
("file.consolidateFootage", "Consolidate All Footage", "Scal wszystkie materiały"),
("file.removeUnusedFootage", "Remove Unused Footage", "Usuń nieużywane materiały"),
("file.reduceProject", "Reduce Project", "Zredukuj projekt"),
("file.findMissing", "Find Missing Effects", "Znajdź brakujące efekty"),
("file.findMissing", "Find Missing Fonts", "Znajdź brakujące czcionki"),
("file.findMissing", "Find Missing Footage", "Znajdź brakujące materiały"),
("file.relinkFootage", "Relink Missing Footage...", "Połącz ponownie brakujące materiały..."),
("file.watchFolder", "Watch Folder...", "Folder obserwowany..."),
("", "Scripts", "Skrypty"),
("file.installScript", "Install Script File...", "Zainstaluj plik skryptu..."),
("file.installScriptUIPanel", "Install ScriptUI Panel...", "Zainstaluj panel ScriptUI..."),
("file.runScript", "Run Script File...", "Uruchom plik skryptu..."),
("", "Create Proxy", "Utwórz proxy"),
("file.createProxy", "Still...", "Klatka..."),
("file.createProxy", "Movie...", "Film..."),
("", "Set Proxy", "Ustaw proxy"),
("file.setProxy", "File...", "Plik..."),
("file.setProxyNone", "None", "Brak"),
("", "Interpret Footage", "Interpretuj materiał"),
("file.interpretFootage", "Main...", "Główny..."),
("file.interpretProxy", "Proxy...", "Proxy..."),
("file.rememberInterpretation", "Remember Interpretation", "Zapamiętaj interpretację"),
("file.applyInterpretation", "Apply Interpretation", "Zastosuj interpretację"),
("", "Replace Footage", "Zastąp materiał"),
("file.replaceFootage", "File...", "Plik..."),
("file.replaceWithPlaceholder", "Placeholder...", "Element zastępczy..."),
("file.replaceWithSolid", "Solid...", "Plansza..."),
("file.replaceWithLayeredComp", "With Layered Comp", "Z warstwową kompozycją"),
("file.reloadFootage", "Reload Footage", "Wczytaj ponownie materiał"),
("file.revealInFinder", "Reveal in Finder", "Pokaż w Finderze"),
("file.projectSettings", "Project Settings...", "Ustawienia projektu..."),
("app.quit", "Exit", "Zakończ"),
("", "Edit", "Edycja"),
("edit.undo", "Undo", "Cofnij"),
("edit.redo", "Redo", "Ponów"),
("", "History", "Historia"),
("app.commandPalette", "Quick Apply...", "Szybkie stosowanie..."),
("edit.cut", "Cut", "Wytnij"),
("edit.copy", "Copy", "Kopiuj"),
("edit.copyWithPropertyLinks", "Copy with Property Links", "Kopiuj z łączami właściwości"),
("edit.copyWithRelativePropertyLinks", "Copy with Relative Property Links", "Kopiuj ze względnymi łączami właściwości"),
("edit.copyExpressionOnly", "Copy Expression Only", "Kopiuj tylko wyrażenie"),
("edit.paste", "Paste", "Wklej"),
("edit.pasteReversedKeyframes", "Paste Reversed Keyframes", "Wklej odwrócone klatki kluczowe"),
("edit.pasteTextMatchFormatting", "Paste Text and Match Formatting", "Wklej tekst i dopasuj formatowanie"),
("edit.pasteTextFormattingOnly", "Paste Text Formatting Only", "Wklej tylko formatowanie tekstu"),
("edit.clear", "Clear", "Wyczyść"),
("edit.duplicate", "Duplicate", "Powiel"),
("edit.splitLayer", "Split Layer", "Podziel warstwę"),
("edit.liftWorkArea", "Lift Work Area", "Wytnij obszar roboczy"),
("edit.extractWorkArea", "Extract Work Area", "Wyodrębnij obszar roboczy"),
("edit.selectAll", "Select All", "Zaznacz wszystko"),
("edit.deselectAll", "Deselect All", "Usuń zaznaczenie"),
("", "Label", "Etykieta"),
("edit.selectLabelGroup", "Select Label Group", "Zaznacz grupę etykiet"),
("edit.label", "None", "Brak"),
("edit.label", "Red", "Czerwony"),
("edit.label", "Yellow", "Żółty"),
("edit.label", "Aqua", "Akwamaryna"),
("edit.label", "Pink", "Różowy"),
("edit.label", "Lavender", "Lawendowy"),
("edit.label", "Peach", "Brzoskwiniowy"),
("edit.label", "Sea Foam", "Morska piana"),
("edit.label", "Blue", "Niebieski"),
("edit.label", "Green", "Zielony"),
("edit.label", "Purple", "Fioletowy"),
("edit.label", "Orange", "Pomarańczowy"),
("edit.label", "Brown", "Brązowy"),
("edit.label", "Fuchsia", "Fuksja"),
("edit.label", "Cyan", "Cyjan"),
("edit.label", "Sandstone", "Piaskowiec"),
("edit.label", "Dark Green", "Ciemnozielony"),
("app.settings", "Edit Label Colors...", "Edytuj kolory etykiet..."),
("", "Select Keyframe Label Group", "Zaznacz grupę etykiet klatek kluczowych"),
("keys.selectLabelGroup", "On Selected Layers", "Na zaznaczonych warstwach"),
("keys.selectLabelGroup", "On All Layers", "Na wszystkich warstwach"),
("keys.selectLabelGroup", "Visible Keyframes on Selected Layers", "Widoczne klatki kluczowe na zaznaczonych warstwach"),
("keys.selectLabelGroup", "Visible Keyframes on All Layers", "Widoczne klatki kluczowe na wszystkich warstwach"),
("", "Purge", "Wyczyść pamięć"),
("edit.purge", "All Cache...", "Całą pamięć podręczną..."),
("edit.purge", "All Memory & Disk Cache...", "Całą pamięć i pamięć dyskową..."),
("edit.purge", "All Memory", "Całą pamięć"),
("edit.purge", "All Disk Cache...", "Całą pamięć dyskową..."),
("edit.purge", "All 3D Cache...", "Całą pamięć 3D..."),
("edit.purgeUndo", "Undo", "Cofanie"),
("edit.purge", "Image Cache Memory", "Pamięć podręczna obrazów"),
("edit.purge", "Snapshot", "Migawka"),
("edit.editOriginal", "Edit Original...", "Edytuj oryginał..."),
("", "Templates", "Szablony"),
("app.templates", "Render Settings...", "Ustawienia renderowania..."),
("app.templates", "Output Module...", "Moduł wyjściowy..."),
("app.keyboardShortcuts", "Keyboard Shortcuts", "Skróty klawiaturowe"),
("", "Preferences", "Preferencje"),
("", "Composition", "Kompozycja"),
("comp.new", "New Composition...", "Nowa kompozycja..."),
("comp.settings", "Composition Settings...", "Ustawienia kompozycji..."),
("comp.setPosterTime", "Set Poster Time", "Ustaw czas plakatu"),
("comp.trimToWorkArea", "Trim Comp to Work Area", "Przytnij kompozycję do obszaru roboczego"),
("comp.cropToRegionOfInterest", "Crop Comp to Region of Interest", "Kadruj kompozycję do obszaru zainteresowania"),
("comp.cropToLayerBounds", "Crop Comp to Selected Layer(s) Bounds", "Kadruj kompozycję do granic zaznaczonych warstw (warstwy)"),
("render.addOutputModule", "Add Output Module", "Dodaj moduł wyjściowy"),
("", "Preview", "Podgląd"),
("playback.toggle", "Play Current Preview", "Odtwórz bieżący podgląd"),
("playback.cacheWhenIdle", "Cache Frames When Idle", "Buforuj klatki w bezczynności"),
("playback.audio", "Audio", "Audio"),
("", "Save Frame As", "Zapisz klatkę jako"),
("comp.saveFrameAs", "File...", "Plik..."),
("comp.saveFrameAsPsd", "Photoshop Layers...", "Warstwy Photoshopa..."),
("comp.saveFrameAsExr", "ProEXR...", "ProEXR..."),
("comp.copyFrame", "Copy Frame to Clipboard", "Kopiuj klatkę do schowka"),
("render.preRender", "Pre-render...", "Wstępne renderowanie..."),
("render.saveCurrentPreview", "Save Current Preview...", "Zapisz bieżący podgląd..."),
("comp.openInEssentialGraphics", "Open in Essential Graphics", "Otwórz w Essential Graphics"),
("", "Responsive Design — Time", "Projekt responsywny — czas"),
("comp.responsiveTime", "Create Intro", "Utwórz intro"),
("comp.responsiveTime", "Create Outro", "Utwórz outro"),
("comp.responsiveTime", "Create Protected Region from Work Area", "Utwórz chroniony obszar z obszaru roboczego"),
("comp.flowchart", "Composition Flowchart", "Schemat kompozycji"),
("comp.miniFlowchart", "Composition Mini-Flowchart", "Miniaturowy schemat kompozycji"),
("", "VR", "VR"),
("comp.vr.createEnvironment", "Create VR Environment...", "Utwórz środowisko VR..."),
("comp.vr.extractCubemap", "Extract Cubemap...", "Wyodrębnij cubemapę..."),
("", "Layer", "Warstwa"),
("layer.newText", "Text", "Tekst"),
("layer.newSolid", "Solid...", "Plansza..."),
("layer.newLight", "Light...", "Światło..."),
("layer.newCamera", "Camera...", "Kamera..."),
("layer.newNull", "Null Object", "Obiekt zerowy"),
("layer.newShape", "Shape Layer", "Warstwa kształtu"),
("layer.newAdjustment", "Adjustment Layer", "Warstwa korekcyjna"),
("layer.newContentAwareFill", "Content-Aware Fill Layer...", "Warstwa wypełnienia z uwzględnieniem zawartości..."),
("layer.new3dPrimitive", "Cube", "Sześcian"),
("layer.new3dPrimitive", "Sphere", "Kula"),
("layer.new3dPrimitive", "Plane", "Płaszczyzna"),
("layer.new3dPrimitive", "Torus", "Torus"),
("layer.new3dPrimitive", "Cone", "Stożek"),
("layer.new3dPrimitive", "Cylinder", "Walec"),
("layer.settings", "Layer Settings...", "Ustawienia warstwy..."),
("layer.openLayer", "Open Layer", "Otwórz warstwę"),
("layer.openSource", "Open Layer Source", "Otwórz źródło warstwy"),
("layer.revealInFinder", "Reveal in Finder", "Pokaż w Finderze"),
("", "Mask", "Maska"),
("layer.addMask", "New Mask", "Nowa maska"),
("layer.mask.shape", "Mask Shape...", "Kształt maski..."),
("layer.mask.set", "Mask Feather...", "Wtapianie maski..."),
("layer.mask.set", "Mask Opacity...", "Krycie maski..."),
("layer.mask.set", "Mask Expansion...", "Rozszerzenie maski..."),
("layer.mask.reset", "Reset Mask", "Resetuj maskę"),
("layer.mask.remove", "Remove Mask", "Usuń maskę"),
("layer.mask.removeAll", "Remove All Masks", "Usuń wszystkie maski"),
("track.mask", "Track Mask", "Śledź maskę"),
("", "Mode", "Tryb"),
("layer.mask.mode", "None", "Brak"),
("layer.mask.mode", "Add", "Dodaj"),
("layer.mask.mode", "Subtract", "Odejmij"),
("layer.mask.mode", "Intersect", "Część wspólna"),
("layer.mask.mode", "Lighten", "Rozjaśnij"),
("layer.mask.mode", "Darken", "Przyciemnij"),
("layer.mask.mode", "Difference", "Różnica"),
("layer.mask.invert", "Inverted", "Odwrócona"),
("layer.mask.lock", "Locked", "Zablokowana"),
("", "Motion Blur", "Rozmycie ruchu"),
("layer.mask.motionBlur", "Same As Layer", "Jak warstwa"),
("layer.mask.motionBlur", "On", "Wł."),
("layer.mask.motionBlur", "Off", "Wył."),
("", "Feather Falloff", "Zanikanie wtapiania"),
("layer.mask.featherFalloff", "Smooth", "Gładkie"),
("layer.mask.featherFalloff", "Linear", "Liniowe"),
("layer.mask.unlockAll", "Unlock All Masks", "Odblokuj wszystkie maski"),
("layer.mask.lockOthers", "Lock Other Masks", "Zablokuj pozostałe maski"),
("layer.mask.hideLocked", "Hide Locked Masks", "Ukryj zablokowane maski"),
("", "Mask and Shape Path", "Ścieżka maski i kształtu"),
("path.rotoBezier", "RotoBezier", "RotoBezier"),
("mask.setClosed", "Closed", "Zamknięta"),
("path.convertToBezier", "Convert To Bezier Path", "Konwertuj na ścieżkę Béziera"),
("path.setFirstVertex", "Set First Vertex", "Ustaw pierwszy wierzchołek"),
("path.freeTransform", "Free Transform Points", "Swobodne przekształcanie punktów"),
("", "Quality", "Jakość"),
("layer.quality", "Best", "Najlepsza"),
("layer.quality", "Draft", "Robocza"),
("layer.quality", "Wireframe", "Szkielet"),
("layer.sampling", "Bilinear", "Dwuliniowa"),
("layer.sampling", "Bicubic", "Dwusześcienna"),
("", "Switches", "Przełączniki"),
("layer.hideOtherVideo", "Hide Other Video", "Ukryj pozostałe wideo"),
("layer.showAllVideo", "Show All Video", "Pokaż całe wideo"),
("layer.unlockAll", "Unlock All Layers", "Odblokuj wszystkie warstwy"),
("layer.expressions", "Enable Expressions", "Włącz wyrażenia"),
("layer.expressions", "Disable Expressions", "Wyłącz wyrażenia"),
("layer.setSwitch", "Shy", "Nieśmiała"),
("layer.setSwitch", "Lock", "Zablokuj"),
("layer.setSwitch", "Audio", "Audio"),
("layer.setSwitch", "Video", "Wideo"),
("layer.setSwitch", "Solo", "Solo"),
("layer.setSwitch", "Effect", "Efekt"),
("layer.setSwitch", "Collapse", "Zwiń"),
("layer.setSwitch", "Motion Blur", "Rozmycie ruchu"),
("layer.setSwitch", "Adjustment Layer", "Warstwa korekcyjna"),
("", "Transform", "Przekształć"),
("layer.transform", "Reset", "Resetuj"),
("layer.setTransform", "Anchor Point...", "Punkt zakotwiczenia..."),
("layer.setTransform", "Position...", "Położenie..."),
("layer.setTransform", "Scale...", "Skala..."),
("layer.setTransform", "Orientation...", "Orientacja..."),
("layer.setTransform", "Rotation...", "Obrót..."),
("layer.setTransform", "Opacity...", "Krycie..."),
("layer.transform", "Flip Horizontal", "Odbij w poziomie"),
("layer.transform", "Flip Vertical", "Odbij w pionie"),
("layer.transform", "Center In View", "Wyśrodkuj w widoku"),
("layer.centerAnchor", "Center Anchor Point in Layer Content", "Wyśrodkuj punkt zakotwiczenia w zawartości warstwy"),
("layer.transform", "Fit to Comp", "Dopasuj do kompozycji"),
("layer.transform", "Fit to Comp Width", "Dopasuj do szerokości kompozycji"),
("layer.transform", "Fit to Comp Height", "Dopasuj do wysokości kompozycji"),
("layer.autoOrient", "Auto-Orient...", "Automatyczna orientacja..."),
("", "Time", "Czas"),
("layer.enableTimeRemap", "Enable Time Remapping", "Włącz przemapowanie czasu"),
("layer.timeReverse", "Time-Reverse Layer", "Odwróć czas warstwy"),
("layer.timeStretch", "Time Stretch...", "Rozciągnięcie czasu..."),
("layer.freezeFrame", "Freeze Frame", "Zamrożenie klatki"),
("layer.freezeOnLastFrame", "Freeze On Last Frame", "Zamroź na ostatniej klatce"),
("layer.alignVideoToData", "Align Video to Data", "Wyrównaj wideo do danych"),
("", "Frame Blending", "Mieszanie klatek"),
("layer.frameBlending", "Off", "Wył."),
("layer.frameBlending", "Frame Mix", "Miks klatek"),
("layer.frameBlending", "Pixel Motion", "Ruch pikseli"),
("layer.setSwitch", "3D Layer", "Warstwa 3D"),
("layer.setSwitch", "Guide Layer", "Warstwa pomocnicza"),
("layer.environment", "Environment Layer", "Warstwa środowiska"),
("", "Markers", "Znaczniki"),
("layer.addMarker", "Add Marker", "Dodaj znacznik"),
("layer.updateMarkersFromSource", "Update Markers From Source", "Aktualizuj znaczniki ze źródła"),
("layer.markersLock", "Lock Markers", "Zablokuj znaczniki"),
("layer.deleteAllMarkers", "Delete All Markers", "Usuń wszystkie znaczniki"),
("layer.setSwitch", "Preserve Transparency", "Zachowaj przezroczystość"),
("", "Blending Mode", "Tryb mieszania"),
("layer.setBlendMode", "Normal", "Normalny"),
("layer.setBlendMode", "Dissolve", "Rozpuszczenie"),
("layer.setBlendMode", "Dancing Dissolve", "Tańczące rozpuszczenie"),
("layer.setBlendMode", "Darken", "Ciemniej"),
("layer.setBlendMode", "Multiply", "Mnożenie"),
("layer.setBlendMode", "Color Burn", "Ściemnianie koloru"),
("layer.setBlendMode", "Classic Color Burn", "Klasyczne ściemnianie koloru"),
("layer.setBlendMode", "Linear Burn", "Ściemnianie liniowe"),
("layer.setBlendMode", "Darker Color", "Ciemniejszy kolor"),
("layer.setBlendMode", "Add", "Dodaj"),
("layer.setBlendMode", "Lighten", "Jaśniej"),
("layer.setBlendMode", "Screen", "Mnożenie odwrotne"),
("layer.setBlendMode", "Color Dodge", "Rozjaśnianie koloru"),
("layer.setBlendMode", "Classic Color Dodge", "Klasyczne rozjaśnianie koloru"),
("layer.setBlendMode", "Linear Dodge", "Rozjaśnianie liniowe"),
("layer.setBlendMode", "Lighter Color", "Jaśniejszy kolor"),
("layer.setBlendMode", "Overlay", "Nakładka"),
("layer.setBlendMode", "Soft Light", "Łagodne światło"),
("layer.setBlendMode", "Hard Light", "Ostre światło"),
("layer.setBlendMode", "Linear Light", "Światło liniowe"),
("layer.setBlendMode", "Vivid Light", "Jaskrawe światło"),
("layer.setBlendMode", "Pin Light", "Światło punktowe"),
("layer.setBlendMode", "Hard Mix", "Twarde mieszanie"),
("layer.setBlendMode", "Difference", "Różnica"),
("layer.setBlendMode", "Classic Difference", "Klasyczna różnica"),
("layer.setBlendMode", "Exclusion", "Wykluczenie"),
("layer.setBlendMode", "Subtract", "Odejmowanie"),
("layer.setBlendMode", "Divide", "Dzielenie"),
("layer.setBlendMode", "Hue", "Barwa"),
("layer.setBlendMode", "Saturation", "Nasycenie"),
("layer.setBlendMode", "Color", "Kolor"),
("layer.setBlendMode", "Luminosity", "Jasność"),
("layer.setBlendMode", "Stencil Alpha", "Szablon alfa"),
("layer.setBlendMode", "Stencil Luma", "Szablon luma"),
("layer.setBlendMode", "Silhouette Alpha", "Sylweta alfa"),
("layer.setBlendMode", "Silhouette Luma", "Sylweta luma"),
("layer.setBlendMode", "Alpha Add", "Dodawanie alfa"),
("layer.setBlendMode", "Luminescent Premul", "Luminescencja premul"),
("layer.setBlendMode", "Next Blending Mode", "Następny tryb mieszania"),
("layer.setBlendMode", "Previous Blending Mode", "Poprzedni tryb mieszania"),
("", "Track Matte", "Matowa ścieżka"),
("layer.trackMatte", "No Track Matte", "Bez matowej ścieżki"),
("layer.trackMatte", "Alpha Matte", "Matowa alfa"),
("layer.trackMatte", "Alpha Inverted Matte", "Odwrócona matowa alfa"),
("layer.trackMatte", "Luma Matte", "Matowa luma"),
("layer.trackMatte", "Luma Inverted Matte", "Odwrócona matowa luma"),
("layer.trackMatte", "Matte with Layer Above", "Matowa z warstwą powyżej"),
("layer.trackMatte", "Matte with Layer Below", "Matowa z warstwą poniżej"),
("", "Layer Styles", "Style warstwy"),
("layer.style.options", "Layer Style Options...", "Opcje stylu warstwy..."),
("layer.style.convertToEditable", "Convert to Editable Styles", "Konwertuj na edytowalne style"),
("layer.style.showAll", "Show All", "Pokaż wszystkie"),
("layer.style.removeAll", "Remove All", "Usuń wszystkie"),
("layer.style.dropShadow", "Drop Shadow", "Cień"),
("layer.style.innerShadow", "Inner Shadow", "Cień wewnętrzny"),
("layer.style.outerGlow", "Outer Glow", "Blask zewnętrzny"),
("layer.style.innerGlow", "Inner Glow", "Blask wewnętrzny"),
("layer.style.bevelEmboss", "Bevel and Emboss", "Faza i płaskorzeźba"),
("layer.style.satin", "Satin", "Satyna"),
("layer.style.colorOverlay", "Color Overlay", "Nakładka koloru"),
("layer.style.gradientOverlay", "Gradient Overlay", "Nakładka gradientu"),
("layer.style.stroke", "Stroke", "Obrys"),
("path.groupShapes", "Group Shapes", "Grupuj kształty"),
("path.ungroupShapes", "Ungroup Shapes", "Rozgrupuj kształty"),
("", "Arrange", "Kolejność"),
("layer.arrange", "Bring Layer to Front", "Przesuń warstwę na wierzch"),
("layer.arrange", "Bring Layer Forward", "Przesuń warstwę do przodu"),
("layer.arrange", "Send Layer Backward", "Przesuń warstwę do tyłu"),
("layer.arrange", "Send Layer to Back", "Przesuń warstwę na spód"),
("", "Reveal", "Pokaż"),
("layer.revealSource", "Reveal Layer Source in Project", "Pokaż źródło warstwy w projekcie"),
("comp.flowchart", "Reveal Layer in Project Flowchart", "Pokaż warstwę w schemacie projektu"),
("comp.revealInProject", "Reveal Composition in Project", "Pokaż kompozycję w projekcie"),
("layer.revealExpressionErrors", "Reveal Expression Errors", "Pokaż błędy wyrażeń"),
("", "Create", "Utwórz"),
("layer.create", "Convert to Editable Text", "Konwertuj na edytowalny tekst"),
("layer.create", "Create Shapes from Text", "Utwórz kształty z tekstu"),
("layer.create", "Create Masks from Text", "Utwórz maski z tekstu"),
("layer.create", "Create Shapes from Vector Layer", "Utwórz kształty z warstwy wektorowej"),
("layer.create", "Create Keyframes from Data", "Utwórz klatki kluczowe z danych"),
("layer.create", "Null Controllers for Positional Points", "Kontrolery zerowe dla punktów położenia"),
("layer.create", "Null Controllers for Path Points", "Kontrolery zerowe dla punktów ścieżki"),
("layer.create", "Nulls Following Path Points", "Obiekty zerowe podążające za punktami ścieżki"),
("layer.create", "Null Tracing Along Path", "Obiekt zerowy wzdłuż ścieżki"),
("layer.create", "Create 3D Layer Instance", "Utwórz instancję warstwy 3D"),
("", "Camera", "Kamera"),
("camera.fromView", "Create Camera from 3D View", "Utwórz kamerę z widoku 3D"),
("camera.stereoRig", "Create Stereo 3D Rig", "Utwórz zestaw stereo 3D"),
("camera.orbitNull", "Create Orbit Null", "Utwórz obiekt zerowy orbity"),
("camera.fromModel", "Create Cameras from 3D Model", "Utwórz kamery z modelu 3D"),
("camera.linkFocusToPoi", "Link Focus Distance to Point of Interest", "Powiąż odległość ostrości z punktem zainteresowania"),
("camera.linkFocusToLayer", "Link Focus Distance to Layer", "Powiąż odległość ostrości z warstwą"),
("camera.setFocusToLayer", "Set Focus Distance to Layer", "Ustaw odległość ostrości na warstwę"),
("layer.cameraSettings", "Camera Settings...", "Ustawienia kamery..."),
("view.reset3DView", "Reset 3D View", "Resetuj widok 3D"),
("", "Light", "Światło"),
("light.fromModel", "Create Lights from 3D Model", "Utwórz światła z modelu 3D"),
("light.controlWithCamera", "Control Light with Camera", "Steruj światłem za pomocą kamery"),
("light.environmentBackground", "Create Environment Light Background Layer", "Utwórz warstwę tła światła otoczenia"),
("", "Material", "Materiał"),
("material.revealSource", "Reveal Material Source in Project", "Pokaż źródło materiału w projekcie"),
("material.reset", "Reset Material", "Resetuj materiał"),
("material.duplicateAssign", "Duplicate and Assign Material", "Powiel i przypisz materiał"),
("layer.autoTrace", "Auto-trace...", "Automatyczne obrysowanie..."),
("layer.precompose", "Pre-compose...", "Utwórz kompozycję wstępną..."),
("layer.sceneEditDetection", "Scene Edit Detection...", "Wykrywanie zmian scen..."),
("", "Effect", "Efekt"),
("window.panel", "Effect Controls", "Ustawienia efektów"),
("effect.applyLast", "Last Effect", "Ostatni efekt"),
("effect.removeAll", "Remove All", "Usuń wszystkie"),
("effect.manage", "Manage Effects...", "Zarządzaj efektami..."),
("effect.plugins.load", "Load Effect Plug-in...", "Wczytaj wtyczkę efektu..."),
("", "Animation", "Animacja"),
("anim.savePreset", "Save Animation Preset...", "Zapisz ustawienie animacji..."),
("anim.applyPreset", "Apply Animation Preset...", "Zastosuj ustawienie animacji..."),
("", "Recent Animation Presets", "Ostatnie ustawienia animacji"),
("anim.clearRecentPresets", "Clear Recent Presets", "Wyczyść ostatnie ustawienia"),
("anim.browsePresets", "Browse Presets...", "Przeglądaj ustawienia..."),
("", "Text Animation Presets", "Ustawienia animacji tekstu"),
("layer.applyTextPreset", "Typewriter", "Maszyna do pisania"),
("layer.applyTextPreset", "Fade Up Characters", "Pojawianie się znaków"),
("layer.applyTextPreset", "Bounce In Words", "Odbijające się słowa"),
("layer.applyTextPreset", "Tracking In", "Zbliżanie liter"),
("layer.applyTextPreset", "Scramble", "Mieszanie znaków"),
("layer.applyTextPreset", "Blur In", "Wyostrzanie z rozmycia"),
("layer.applyTextPreset", "Jitter", "Drganie"),
("layer.applyTextPreset", "Drop In Lines", "Wpadające wiersze"),
("anim.addKeyframe", "Add Keyframe", "Dodaj klatkę kluczową"),
("keys.toggleHold", "Toggle Hold Keyframe", "Przełącz klatkę zatrzymania"),
("keys.interpolation", "Keyframe Interpolation...", "Interpolacja klatek kluczowych..."),
("keys.velocity", "Keyframe Velocity...", "Prędkość klatek kluczowych..."),
("", "Keyframe Assistant", "Asystent klatek kluczowych"),
("keys.audioToKeyframes", "Convert Audio to Keyframes", "Konwertuj audio na klatki kluczowe"),
("prop.convertExpressionToKeyframes", "Convert Expression to Keyframes", "Konwertuj wyrażenie na klatki kluczowe"),
("keys.easyEase", "Easy Ease", "Łagodne przejście"),
("keys.easyEaseIn", "Easy Ease In", "Łagodne wejście"),
("keys.easyEaseOut", "Easy Ease Out", "Łagodne wyjście"),
("keys.exponentialScale", "Exponential Scale", "Skalowanie wykładnicze"),
("keys.rpfCameraImport", "RPF Camera Import", "Import kamery RPF"),
("layer.sequence", "Sequence Layers...", "Ułóż warstwy w sekwencję..."),
("keys.timeReverse", "Time-Reverse Keyframes", "Odwróć czas klatek kluczowych"),
("", "Animate Text", "Animuj tekst"),
("layer.enablePerChar3D", "Enable Per-character 3D", "Włącz 3D dla każdego znaku"),
("layer.addTextAnimator", "Anchor Point", "Punkt zakotwiczenia"),
("layer.addTextAnimator", "Position", "Położenie"),
("layer.addTextAnimator", "Scale", "Skala"),
("layer.addTextAnimator", "Skew", "Pochylenie"),
("layer.addTextAnimator", "Rotation", "Obrót"),
("layer.addTextAnimator", "Opacity", "Krycie"),
("layer.addTextAnimator", "All Transform Properties", "Wszystkie właściwości przekształcenia"),
("", "Fill Color", "Kolor wypełnienia"),
("layer.addTextAnimator", "RGB", "RGB"),
("layer.addTextAnimator", "Hue", "Barwa"),
("layer.addTextAnimator", "Saturation", "Nasycenie"),
("layer.addTextAnimator", "Brightness", "Jasność"),
("", "Stroke Color", "Kolor obrysu"),
("layer.addTextAnimator", "Stroke Width", "Szerokość obrysu"),
("layer.addTextAnimator", "Tracking", "Światło"),
("layer.addTextAnimator", "Line Anchor", "Zakotwiczenie wiersza"),
("layer.addTextAnimator", "Line Spacing", "Odstęp między wierszami"),
("layer.addTextAnimator", "Character Offset", "Przesunięcie znaku"),
("layer.addTextAnimator", "Character Value", "Wartość znaku"),
("layer.addTextAnimator", "Blur", "Rozmycie"),
("text.animatorFontAxes", "Variable Font Axes", "Osie czcionki zmiennej"),
("", "Add Text Selector", "Dodaj selektor tekstu"),
("text.addSelector", "Range", "Zakres"),
("text.addSelector", "Wiggly", "Falowanie"),
("text.addSelector", "Expression", "Wyrażenie"),
("text.removeAllAnimators", "Remove All Text Animators", "Usuń wszystkie animatory tekstu"),
("prop.setExpression", "Add Expression", "Dodaj wyrażenie"),
("essential.addProperty", "Add Property to Essential Graphics", "Dodaj właściwość do Essential Graphics"),
("prop.separateDimensions", "Separate Dimensions", "Rozdziel wymiary"),
("track.camera", "Track Camera", "Śledź kamerę"),
("track.warpStabilizer", "Warp Stabilizer VFX", "Warp Stabilizer VFX"),
("track.motion", "Track Motion", "Śledź ruch"),
("track.stabilize", "Stabilize Motion", "Stabilizuj ruch"),
("track.property", "Track this Property", "Śledź tę właściwość"),
("anim.reveal", "Reveal Properties with Keyframes", "Pokaż właściwości z klatkami kluczowymi"),
("anim.reveal", "Reveal Properties with Animation", "Pokaż właściwości z animacją"),
("anim.reveal", "Reveal All Modified Properties", "Pokaż wszystkie zmienione właściwości"),
("", "View", "Widok"),
("view.newViewer", "New Viewer", "Nowa przeglądarka"),
("view.splitLockedViewer", "Split with New Locked Viewer", "Podziel z nową zablokowaną przeglądarką"),
("view.zoomIn", "Zoom In", "Powiększ"),
("view.zoomOut", "Zoom Out", "Pomniejsz"),
("", "Resolution", "Rozdzielczość"),
("view.res.full", "Full", "Pełna"),
("view.res.half", "Half", "Połowa"),
("view.res.third", "Third", "Jedna trzecia"),
("view.res.quarter", "Quarter", "Jedna czwarta"),
("view.res.custom", "Custom...", "Niestandardowa..."),
("view.displayColorManagement", "Use Display Color Management", "Używaj zarządzania kolorem wyświetlacza"),
("", "Simulate Output", "Symuluj wyjście"),
("view.simulateOutput", "No Output Simulation", "Bez symulacji wyjścia"),
("view.simulateOutput", "HDTV (Rec. 709)", "HDTV (Rec. 709)"),
("view.simulateOutput", "SDTV NTSC", "SDTV NTSC"),
("view.simulateOutput", "SDTV PAL", "SDTV PAL"),
("view.simulateOutput", "Legacy Macintosh RGB (Gamma 1.8)", "Starsze RGB komputerów Macintosh (gamma 1,8)"),
("view.simulateOutput", "Internet Standard RGB (sRGB)", "Standardowe internetowe RGB (sRGB)"),
("view.simulateOutput", "UHDTV (Rec. 2020)", "UHDTV (Rec. 2020)"),
("view.simulateOutput", "Display P3", "Display P3"),
("view.simulateOutput", "Linear (1.0 Gamma)", "Liniowe (gamma 1,0)"),
("view.customRgb", "My Custom RGB...", "Moje niestandardowe RGB..."),
("view.simulateOutput", "Custom...", "Niestandardowe..."),
("view.rulers", "Show Rulers", "Pokaż linijki"),
("", "Panel Background Color", "Kolor tła panelu"),
("view.panelBackground", "Black", "Czarny"),
("view.panelBackground", "Dark Gray", "Ciemnoszary"),
("view.panelBackground", "Medium Gray (Default)", "Średnioszary (domyślny)"),
("view.panelBackground", "Light Gray", "Jasnoszary"),
("view.panelBackground", "White", "Biały"),
("view.panelBackground", "Custom", "Niestandardowy"),
("view.panelBackground", "Select Custom Background Color...", "Wybierz niestandardowy kolor tła..."),
("view.guides", "Show Guides", "Pokaż linie pomocnicze"),
("view.snapToGuides", "Snap to Guides", "Przyciągaj do linii pomocniczych"),
("view.lockGuides", "Lock Guides", "Zablokuj linie pomocnicze"),
("view.addGuide", "Add Guide...", "Dodaj linię pomocniczą..."),
("view.addPercentGuide", "Add Percentage Guide...", "Dodaj procentową linię pomocniczą..."),
("view.convertGuides", "Convert Guides to Percentage", "Konwertuj linie pomocnicze na procenty"),
("view.convertGuides", "Convert Guides to Pixels", "Konwertuj linie pomocnicze na piksele"),
("view.clearGuides", "Clear Guides", "Wyczyść linie pomocnicze"),
("view.importGuides", "Import Guides...", "Importuj linie pomocnicze..."),
("view.exportGuides", "Export Guides...", "Eksportuj linie pomocnicze..."),
("view.grid", "Show Grid", "Pokaż siatkę"),
("view.snapToGrid", "Snap to Grid", "Przyciągaj do siatki"),
("view.snapping", "Snapping", "Przyciąganie"),
("view.options", "View Options...", "Opcje widoku..."),
("view.layerControls", "Show Layer Controls", "Pokaż elementy sterujące warstwy"),
("", "Switch View Layout", "Przełącz układ widoków"),
("view.layout", "1 View", "1 widok"),
("view.layout", "2 Views", "2 widoki"),
("view.layout", "4 Views", "4 widoki"),
("view.shareViewOptions", "Share View Options", "Wspólne opcje widoku"),
("", "Switch 3D View", "Przełącz widok 3D"),
("view.3d.activeCamera", "Active Camera", "Aktywna kamera"),
("view.3d.default", "Default", "Domyślny"),
("view.3d.front", "Front", "Przód"),
("view.3d.left", "Left", "Lewo"),
("view.3d.top", "Top", "Góra"),
("view.3d.back", "Back", "Tył"),
("view.3d.right", "Right", "Prawo"),
("view.3d.bottom", "Bottom", "Dół"),
("view.3d.custom1", "Custom View 1", "Widok niestandardowy 1"),
("view.3d.custom2", "Custom View 2", "Widok niestandardowy 2"),
("view.3d.custom3", "Custom View 3", "Widok niestandardowy 3"),
("", "Assign Shortcut to 3D View", "Przypisz skrót do widoku 3D"),
("view.3d.last", "Switch to Last 3D View", "Przełącz na ostatni widok 3D"),
("view.lookAtSelected", "Look at Selected Layers", "Patrz na zaznaczone warstwy"),
("view.lookAtAll", "Look at All Layers", "Patrz na wszystkie warstwy"),
("time.set", "Go to Time...", "Przejdź do czasu..."),
("view.fullScreen", "Enter Full Screen", "Pełny ekran"),
("", "Window", "Okno"),
("", "Workspace", "Obszar roboczy"),
("window.workspace", "Default", "Domyślny"),
("window.workspace", "Review", "Przegląd"),
("window.workspace", "Learn", "Nauka"),
("window.workspace", "Small Screen", "Mały ekran"),
("window.workspace", "Standard", "Standardowy"),
("window.workspace", "All Panels", "Wszystkie panele"),
("window.workspace", "Animation", "Animacja"),
("window.workspace", "Color", "Kolor"),
("window.workspace", "Effects", "Efekty"),
("window.workspace", "Essential Graphics", "Essential Graphics"),
("window.workspace", "Minimal", "Minimalny"),
("window.workspace", "Motion Tracking", "Śledzenie ruchu"),
("window.workspace", "Paint", "Malowanie"),
("window.workspace", "Text", "Tekst"),
("window.workspace", "Undocked Panels", "Panele odpięte"),
("window.resetWorkspace", "Reset to Saved Layout", "Przywróć zapisany układ"),
("window.saveWorkspace", "Save Changes to this Workspace", "Zapisz zmiany w tym obszarze roboczym"),
("window.saveWorkspaceAs", "Save as New Workspace...", "Zapisz jako nowy obszar roboczy..."),
("window.editWorkspaces", "Edit Workspaces...", "Edytuj obszary robocze..."),
("", "Assign Shortcut to Workspace", "Przypisz skrót do obszaru roboczego"),
("window.panel", "Align", "Wyrównanie"),
("window.panel", "Audio", "Audio"),
("window.panel", "Brushes", "Pędzle"),
("window.panel", "Character", "Znak"),
("window.panel", "Content-Aware Fill", "Wypełnienie z uwzględnieniem zawartości"),
("window.panel", "Effects & Presets", "Efekty i ustawienia"),
("window.panel", "Essential Graphics", "Essential Graphics"),
("window.panel", "Info", "Informacje"),
("help.inAppTutorials", "Learn", "Nauka"),
("window.panel", "Lumetri Scopes", "Zakresy Lumetri"),
("window.panel", "Mask Interpolation", "Interpolacja maski"),
("window.panel", "Media Browser", "Przeglądarka multimediów"),
("window.panel", "Metadata", "Metadane"),
("window.panel", "Motion Sketch", "Szkic ruchu"),
("window.panel", "Paint", "Malowanie"),
("window.panel", "Paragraph", "Akapit"),
("window.panel", "Preview", "Podgląd"),
("window.panel", "Progress", "Postęp"),
("window.panel", "Properties", "Właściwości"),
("window.panel", "Script Console", "Konsola skryptów"),
("window.panel", "Smoother", "Wygładzanie"),
("window.panel", "Tools", "Narzędzia"),
("window.panel", "Tracker", "Śledzenie"),
("window.panel", "Wiggler", "Wahanie"),
("window.panel", "Composition", "Kompozycja"),
("window.panel", "Flowchart", "Schemat"),
("window.panel", "Footage", "Materiał"),
("window.panel", "Layer", "Warstwa"),
("window.panel", "Project", "Projekt"),
("window.panel", "Render Queue", "Kolejka renderowania"),
("window.panel", "Timeline", "Oś czasu"),
("window.panel", "Create Nulls From Paths", "Utwórz obiekty zerowe ze ścieżek"),
("window.panel", "VR Comp Editor", "Edytor kompozycji VR"),
("", "Help", "Pomoc"),
("help.docs", "EffectCraft Help...", "Pomoc EffectCraft..."),
("help.docs", "Scripting Help...", "Pomoc dotycząca skryptów..."),
("help.docs", "Expression Reference...", "Dokumentacja wyrażeń..."),
("help.docs", "Effect Reference...", "Dokumentacja efektów..."),
("anim.browsePresets", "Animation Presets...", "Ustawienia animacji..."),
("app.keyboardShortcuts", "Keyboard Shortcuts...", "Skróty klawiaturowe..."),
("help.inAppTutorials", "In-App Tutorials...", "Samouczki w aplikacji..."),
("help.onlineTutorials", "Online Tutorials...", "Samouczki online..."),
("help.systemReport", "System Compatibility Report...", "Raport zgodności systemu..."),
("help.enableLogging", "Enable Logging", "Włącz rejestrowanie"),
("help.revealLogFile", "Reveal Logging File", "Pokaż plik dziennika"),
("help.discord", "Join the ArtCraft Discord...", "Dołącz do Discorda ArtCraft..."),
("help.reportIssue", "Provide Feedback...", "Prześlij opinię..."),
("help.website", "ArtCraft Website", "Witryna ArtCraft"),
("help.appPage", "EffectCraft Home Page", "Strona główna EffectCraft"),
("help.github", "EffectCraft on GitHub", "EffectCraft w serwisie GitHub"),
("file.openDemoProject", "Open Demo Project", "Otwórz projekt demonstracyjny"),
("", "Recent Script Files", "Ostatnie pliki skryptów"),
("layer.newPrimitive", "3D Primitive...", "Bryła podstawowa 3D..."),
];
/// Czech (cs): original phrase translations of every fixed menu entry.
const CZECH: &[(&str, &str, &str)] = &[
("", "Front", "Přední strana"),
@@ -6561,6 +7245,8 @@ const CZECH: &[(&str, &str, &str)] = &[
("layer.applyTextPreset", "Drop In Lines", "Spustit řádky shora"),
("anim.addKeyframe", "Add Keyframe", "Přidat klíčový snímek"),
("keys.toggleHold", "Toggle Hold Keyframe", "Přepnout podržení klíčového snímku"),
("keys.setMuted", "Mute Keyframes", "Ztlumit klíčové snímky"),
("keys.setMuted", "Unmute Keyframes", "Obnovit klíčové snímky"),
("keys.interpolation", "Keyframe Interpolation...", "Interpolace klíčových snímků..."),
("keys.velocity", "Keyframe Velocity...", "Rychlost klíčového snímku..."),
("", "Keyframe Assistant", "Pomocník klíčových snímků"),
@@ -7192,6 +7878,8 @@ const GEORGIAN: &[(&str, &str, &str)] = &[
("layer.applyTextPreset", "Drop In Lines", "სტრიქონების ჩამოვარდნა"),
("anim.addKeyframe", "Add Keyframe", "საკვანძო კადრის დამატება"),
("keys.toggleHold", "Toggle Hold Keyframe", "ფიქსირებული საკვანძო კადრის გადართვა"),
("keys.setMuted", "Mute Keyframes", "საკვანძო კადრების დადუმება"),
("keys.setMuted", "Unmute Keyframes", "საკვანძო კადრების დადუმების მოხსნა"),
("keys.interpolation", "Keyframe Interpolation...", "საკვანძო კადრის ინტერპოლაცია..."),
("keys.velocity", "Keyframe Velocity...", "საკვანძო კადრის სიჩქარე..."),
("", "Keyframe Assistant", "საკვანძო კადრების ასისტენტი"),
@@ -7512,6 +8200,19 @@ mod tests {
println!("total: {total}");
}
/// Prints the translated-row counts per language, the numbers `docs/localization-parity.md`
/// quotes. `cargo xtask i18n-report` runs it with `--nocapture`.
#[test]
fn catalog_row_counts_report() {
println!("Localization catalog rows:");
for code in CATALOG_CODES {
let menus = menu_catalog(code).map_or(0, |catalog| catalog.len());
let panels = ui::translated_rows(code);
let effect_names = effects::translated_rows(code);
println!("{code}: menus {menus}, panels {panels}, effect names {effect_names}, total {}", menus + panels + effect_names);
}
}
#[test]
fn menu_catalogs_have_no_stale_or_duplicate_rows() {
let mut actual = BTreeSet::new();
@@ -7550,6 +8251,43 @@ mod tests {
app.session.execute("prefs.set", json!({"key":"general.language", "value":"en"})).unwrap();
assert_eq!(label(&app, "", "Composition"), "Composition");
}
/// Match System in the browser also picks the Russian, French and German catalogs, in the
/// order of the browser's preferences (#1700).
#[test]
fn browser_preferences_choose_russian_french_and_german() {
let tags = |t: &[&str]| t.iter().map(|s| s.to_string()).collect::<Vec<_>>();
for (locales, expected) in
[(vec!["de-DE", "es-ES"], "de"), (vec!["xx-XX", "fr-CA"], "fr"), (vec!["ru-RU", "cs"], "ru"), (vec!["en-US", "de-DE"], "en"), (vec!["xx-XX"], "en")]
{
assert_eq!(super::first_supported(None, &tags(&locales), super::web_draws), expected, "{locales:?}");
}
}
/// The Edit menu's Undo and Redo name the history's action in the interface language, also in
/// the languages added with #1700.
#[test]
fn undo_and_redo_are_localized_for_russian_french_and_german() {
for (language, undo, no_undo, redo, no_redo) in [
("ru", "Отменить:", "Нечего отменять", "Повторить:", "Нечего повторять"),
("fr", "Annuler", "Impossible d'annuler", "Rétablir", "Impossible de rétablir"),
("de", "Rückgängig", "Rückgängig nicht möglich", "Wiederholen", "Wiederholen nicht möglich"),
] {
let mut app = EffectcraftApp::new(effectcraft_engine::Session::default());
app.session.execute("prefs.set", json!({"key": "general.language", "value": language})).unwrap();
let entry_of = |app: &EffectcraftApp, command: &str| {
let (_, e) = effectcraft_engine::menus::entries().into_iter().find(|(_, e)| e.command == command).unwrap();
entry(app, e, e.label.clone())
};
assert_eq!(entry_of(&app, "edit.undo"), no_undo, "{language}");
assert_eq!(entry_of(&app, "edit.redo"), no_redo, "{language}");
app.session.execute("comp.new", json!({"name": "A", "duration": 1})).unwrap();
let action = app.session.history.undo.last().map(|u| u.0.clone()).unwrap();
assert_eq!(entry_of(&app, "edit.undo"), format!("{undo} {action}"), "{language}");
app.session.execute("edit.undo", json!({})).unwrap();
assert_eq!(entry_of(&app, "edit.redo"), format!("{redo} {action}"), "{language}");
}
}
#[test]
fn match_system_uses_the_system_language_where_there_is_a_catalog() {
for (locale, language) in [
@@ -7579,6 +8317,10 @@ mod tests {
(Some("cs-CZ"), "cs"),
(Some("cs_CZ.UTF-8"), "cs"),
(Some("CS_cz.UTF-8"), "cs"),
(Some("pl"), "pl"),
(Some("pl-PL"), "pl"),
(Some("pl_PL.UTF-8"), "pl"),
(Some("PL_pl.UTF-8"), "pl"),
(Some("ka"), "ka"),
(Some("ka-GE"), "ka"),
(Some("ka_GE.UTF-8"), "ka"),
@@ -7606,6 +8348,7 @@ mod tests {
assert_eq!(traditional(&app), system_language() == "zh-hant");
assert_eq!(spanish(&app), system_language() == "es");
assert_eq!(czech(&app), system_language() == "cs");
assert_eq!(polish(&app), system_language() == "pl");
app.session.execute("prefs.set", json!({"key":"general.language", "value":"ja"})).unwrap();
assert!(japanese(&app));
app.session.execute("prefs.set", json!({"key":"general.language", "value":"zh-hans"})).unwrap();
@@ -7632,6 +8375,7 @@ mod tests {
("es", SPANISH),
("pt-br", PORTUGUESE),
("cs", CZECH),
("pl", POLISH),
("ka", GEORGIAN),
] {
for (command, en, translated) in catalog {
@@ -7643,6 +8387,35 @@ mod tests {
}
}
/// The dock's tab titles are `PanelKind::title()`; the panels the Window menu lists are drawn in
/// the interface language through their `(window.panel, title)` rows. `History` and `ScriptUI
/// Panel` are in no menu and keep their English names, which is what this pins.
#[test]
fn the_dock_draws_every_panel_it_can_in_the_interface_language() {
use crate::dock::PanelKind;
const WITHOUT_A_ROW: &[&str] = &["History", "ScriptUI Panel"];
for (name, catalog, code) in [("ja", JAPANESE, "ja"), ("zh-hans", SIMPLIFIED_CHINESE, "zh-hans"), ("zh-hant", TRADITIONAL_CHINESE, "zh-hant")] {
// The dock asks both catalogs, so a title may be named by either.
ui::set_current(code);
for p in PanelKind::ALL {
let title = p.title();
let in_menu = catalog.iter().any(|(c, en, _)| *c == "window.panel" && *en == title);
let in_panel = ui::tr(title) != title;
assert_eq!(
in_menu || in_panel,
!WITHOUT_A_ROW.contains(&title),
"{name}: {title} is {}named by a catalog and {}in the exception list",
if in_menu || in_panel { "" } else { "not " },
if WITHOUT_A_ROW.contains(&title) { "" } else { "not " }
);
}
}
ui::set_current("en");
// The dock asks the catalog rather than drawing the English name.
let dock_ui = include_str!("dock_ui.rs");
assert!(dock_ui.contains(r#"crate::i18n::command_label("window.panel", title)"#), "dock_ui.rs no longer asks the catalog for the panel titles");
}
#[test]
fn ukrainian_catalog_matches_the_settings_registry() {
let codes: Vec<_> = effectcraft_engine::prefs::LANGUAGES.iter().map(|(_, code)| *code).filter(|code| !matches!(*code, "system" | "en")).collect();
@@ -7676,7 +8449,7 @@ mod tests {
app.session.execute("prefs.set", json!({"key":"general.language", "value":"ru"})).unwrap();
assert_eq!(entry(&app, &undo, "Can't Undo".into()), "Нечего отменять");
app.session.execute("prefs.set", json!({"key":"general.language", "value":"de"})).unwrap();
assert_eq!(entry(&app, &undo, "Can't Undo".into()), "Can't Undo", "no German undo wording yet: the shown label");
assert_eq!(entry(&app, &undo, "Can't Undo".into()), "Rückgängig nicht möglich");
app.session.execute("prefs.set", json!({"key":"general.language", "value":"en"})).unwrap();
assert_eq!(entry(&app, &new_comp, new_comp.label.clone()), "New Composition...");
}
+7
View File
@@ -87,6 +87,13 @@ pub(crate) fn name(id: &str, english: &str) -> String {
catalog(current()).and_then(|c| c.row(id)).map_or_else(|| english.to_string(), |(_, translated)| translated.to_string())
}
/// The number of effect-name rows `language` translates, for the row-count report; 0 when it has
/// no catalog or when it is English.
#[cfg(test)]
pub(super) fn translated_rows(language: &str) -> usize {
catalog(language).map_or(0, |c| c.rows.len())
}
/// The name to draw for an effect group the property tree stores as `stored`: the catalog name,
/// plus the instance index the engine appends to a repeat (`Gaussian Blur 2` → `高斯模糊 2`).
///
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -179,7 +179,12 @@ fn menu_nodes(
"recentScripts" => app.session.prefs.recent_scripts.get(i).map(|r| r.text().to_string()),
_ => None,
};
let r = ui.add_enabled(entry_enabled(app, e), egui::Button::new((gutter(entry_checked(app, e) == Some(true)), e.label.as_str())));
let mut b = egui::Button::new((gutter(entry_checked(app, e) == Some(true)), e.label.as_str()));
// File ▸ Scripts entries show the key a script was given.
if let Some(s) = entry_shortcut(app, e) {
b = b.shortcut_text(shortcut_text(&s));
}
let r = ui.add_enabled(entry_enabled(app, e), b);
let r = match &tip {
Some(t) => r.on_hover_text(t),
None => r,
+31 -17
View File
@@ -210,6 +210,9 @@ fn escape_dialog(app: &mut EffectcraftApp, ctx: &egui::Context) {
ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape));
if app.dialog == Some(crate::Dialog::LayerStyles) {
crate::panels::layer_styles_dialog::finish(app, false);
} else if app.dialog == Some(crate::Dialog::GradientEditor) {
// Escape is Cancel: the preview rolls back.
crate::panels::gradient_editor::finish(app, false, ctx);
} else if app.dialog == Some(crate::Dialog::Info) {
crate::panels::forms::close_info(app);
} else {
@@ -222,10 +225,37 @@ fn escape_dialog(app: &mut EffectcraftApp, ctx: &egui::Context) {
if app.dialog == Some(crate::Dialog::UnsavedChanges) {
app.dialog_state.unsaved.command = None;
}
// Escape is Cancel: a Preview that edited the project rolls back.
crate::panels::live_preview::cancel(app);
app.dialog = None;
}
}
/// Escape with a dialog open: it is Cancel. Escape while recording a shortcut cancels the
/// recording, not the editor; so does Escape in its Delete Preset prompt.
fn cancel_dialog(app: &mut EffectcraftApp, ctx: &egui::Context) {
if crate::panels::shortcut_editor::recording(app) || crate::panels::shortcut_editor::confirming(app) {
return;
}
if app.dialog == Some(crate::Dialog::Info) {
crate::panels::forms::close_info(app);
return;
}
if app.dialog == Some(crate::Dialog::GradientEditor) {
crate::panels::gradient_editor::finish(app, false, ctx);
return;
}
// Escape in Settings is Cancel: restore the settings from when it opened.
if app.dialog == Some(crate::Dialog::Settings)
&& let Some(p) = app.dialog_state.prefs_snapshot.take()
{
app.session.prefs = p;
app.session.prefs_changed();
}
crate::panels::live_preview::cancel(app);
app.dialog = None;
}
type KeyEvent = (egui::Key, egui::Modifiers, bool, Option<egui::Key>);
/// The frame's key presses, with clipboard events mapped back to Ctrl+C / X / V.
@@ -341,23 +371,7 @@ fn handle_shortcuts_impl(app: &mut EffectcraftApp, ctx: &egui::Context, tab_only
}
// Escape closes dialogs.
if key == egui::Key::Escape && app.dialog.is_some() {
// Escape while recording a shortcut cancels the recording, not the editor; so does
// Escape in its Delete Preset prompt.
if crate::panels::shortcut_editor::recording(app) || crate::panels::shortcut_editor::confirming(app) {
continue;
}
if app.dialog == Some(crate::Dialog::Info) {
crate::panels::forms::close_info(app);
continue;
}
// Escape in Settings is Cancel: restore the settings from when it opened.
if app.dialog == Some(crate::Dialog::Settings)
&& let Some(p) = app.dialog_state.prefs_snapshot.take()
{
app.session.prefs = p;
app.session.prefs_changed();
}
app.dialog = None;
cancel_dialog(app, ctx);
continue;
}
if app.dialog.is_some() {
+50 -3
View File
@@ -196,8 +196,9 @@ pub fn build(app: &EffectcraftApp) -> NativeMenu {
label: e.label.clone(),
command: e.command.clone(),
params: e.params.clone(),
shortcut: None,
accelerator: None,
// File ▸ Scripts entries show the key a script was given.
shortcut: crate::menus::entry_shortcut(app, e),
accelerator: crate::menus::entry_shortcut(app, e).as_deref().and_then(accelerator),
enabled: crate::menus::entry_enabled(app, e),
checked: crate::menus::entry_checked(app, e),
}));
@@ -243,6 +244,8 @@ pub fn build(app: &EffectcraftApp) -> NativeMenu {
"Serviços"
} else if crate::i18n::czech(app) {
"Služby"
} else if crate::i18n::polish(app) {
"Usługi"
} else if crate::i18n::georgian(app) {
"სერვისები"
} else {
@@ -360,7 +363,12 @@ pub fn state_key(app: &EffectcraftApp) -> u64 {
s.history.undo.iter().for_each(|(l, _)| l.hash(&mut h));
s.state.open_comps.hash(&mut h);
s.active_comp_id().and_then(|c| s.state.views3d.get(&c)).map(|v| format!("{:?}", v.current)).hash(&mut h);
s.shortcuts().keys.len().hash(&mut h);
// Each binding's keys: a key changed to another one gives the item a new accelerator.
let shortcuts = s.shortcuts();
for b in &shortcuts.bindables {
b.key.hash(&mut h);
shortcuts.keys.get(&b.key).hash(&mut h);
}
s.keymaps.active.hash(&mut h);
let v = &app.ui.viewer;
(v.rulers, v.guides, v.snap_guides, v.lock_guides, v.grid, v.snap_grid, v.show_layer_controls, v.pasteboard).hash(&mut h);
@@ -578,6 +586,27 @@ mod tests {
assert_eq!(translated, ["Služby", "Skrýt EffectCraft", "Skrýt ostatní", "Zobrazit vše"]);
}
#[test]
fn polish_translates_macos_predefined_items() {
if !cfg!(target_os = "macos") {
return;
}
let mut app = EffectcraftApp::new(effectcraft_engine::Session::default());
app.session.execute("prefs.set", json!({"key": "general.language", "value": "pl"})).unwrap();
fn labels(nodes: &[NativeNode], out: &mut Vec<String>) {
for node in nodes {
match node {
NativeNode::Predefined { label, .. } => out.push(label.clone()),
NativeNode::Submenu { children, .. } => labels(children, out),
_ => {}
}
}
}
let mut translated = vec![];
labels(&build(&app).menus, &mut translated);
assert_eq!(translated, ["Usługi", "Ukryj EffectCraft", "Ukryj pozostałe", "Pokaż wszystko"]);
}
#[test]
fn georgian_translates_macos_predefined_items() {
if !cfg!(target_os = "macos") {
@@ -722,6 +751,24 @@ mod tests {
assert!(spec2.items().iter().any(|i| i.command == "file.openRecent" && i.label == "Recent Test.ecproj"));
}
/// A script's key changed to another key changes the key, so the File ▸ Scripts entry gets
/// the new accelerator even though the number of bindings stays the same.
#[test]
fn a_changed_shortcut_changes_the_state_key() {
let mut app = app();
let script = json!({"name": "Rename Layers.jsx"});
let b = app.session.shortcuts().find(&effectcraft_engine::shortcuts::binding_key("file.runScript", &script)).cloned().unwrap();
app.session.keymaps.set(&b, &["Cmd+Alt+Shift+F7".to_string()]).unwrap();
app.session.shortcuts_changed();
let k0 = state_key(&app);
app.session.keymaps.set(&b, &["Cmd+Alt+Shift+F8".to_string()]).unwrap();
app.session.shortcuts_changed();
assert_ne!(state_key(&app), k0);
let spec = build(&app);
let entry = spec.items().into_iter().find(|i| i.command == "file.runScript" && i.params == script).unwrap();
assert_eq!(entry.shortcut.as_deref(), Some("Cmd+Alt+Shift+F8"));
}
/// #1630: a checked Window menu entry closes its panel; an unchecked one opens it.
#[test]
fn window_menu_entry_toggles_its_panel() {
+3 -1
View File
@@ -27,6 +27,7 @@ pub mod key_dialogs;
pub mod layer_panel;
pub mod layer_styles_dialog;
pub mod learn;
pub mod live_preview;
pub mod markers_ui;
pub mod media_panels;
pub mod misc;
@@ -35,6 +36,7 @@ pub mod paint_panels;
pub mod panel_kit;
pub mod path_vr_panels;
pub mod precomp;
pub(crate) mod preview_merge;
pub mod primitive_dialog;
pub mod project;
pub mod properties;
@@ -331,7 +333,7 @@ pub fn panel_menu_popup(app: &mut EffectcraftApp, ui: &mut egui::Ui) {
}
}
_ => {
ui.label(egui::RichText::new(panel.title()).weak());
ui.label(egui::RichText::new(crate::i18n::label(app, "window.panel", panel.title())).weak());
}
}
});
+4 -4
View File
@@ -127,7 +127,7 @@ pub fn wiggler(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
app.ui.anim_tools = st.clone();
let b = Rect::from_min_size(pos2(rect.max.x - 92.0, y), vec2(80.0, 24.0));
let enabled = app.session.state.selected_keys.len() >= 2;
if widgets::text_button(ui, b, "Apply", enabled, &t, egui::Id::unique("wg-apply")).clicked() {
if widgets::text_button(ui, b, crate::i18n::tr("Apply"), enabled, &t, egui::Id::unique("wg-apply")).clicked() {
let params = json!({
"apply": if st.wiggle_spatial { "spatial" } else { "temporal" },
"noise": if st.wiggle_smooth { "smooth" } else { "jagged" },
@@ -175,7 +175,7 @@ pub fn smoother(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
y += ROW + 8.0;
let b = Rect::from_min_size(pos2(rect.max.x - 92.0, y), vec2(80.0, 24.0));
let enabled = app.session.state.selected_keys.len() >= 3;
if widgets::text_button(ui, b, "Apply", enabled, &t, egui::Id::unique("sm-apply")).clicked() {
if widgets::text_button(ui, b, crate::i18n::tr("Apply"), enabled, &t, egui::Id::unique("sm-apply")).clicked() {
apply(app, "keys.smooth", json!({"tolerance": tol}));
}
app.auto.add("smoother.apply", b, "Apply");
@@ -375,7 +375,7 @@ pub fn mask_interpolation(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rec
changes.insert("keyframeRate".into(), json!(nr));
}
} else {
p.text(pos2(xv + 78.0, y + 9.0), Align2::LEFT_CENTER, "per second", Tokens::ui(12.0), t.text_dim);
p.text(pos2(xv + 78.0, y + 9.0), Align2::LEFT_CENTER, crate::i18n::tr("per second"), Tokens::ui(12.0), t.text_dim);
}
y += ROW;
let mut check = |app: &mut EffectcraftApp, y: f32, key: &str, text: &str, on: bool| {
@@ -454,7 +454,7 @@ pub fn mask_interpolation(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rec
}
let b = Rect::from_min_size(pos2(rect.max.x - 92.0, y), vec2(80.0, 24.0));
let enabled = app.session.state.selected_keys.len() >= 2;
if widgets::text_button(ui, b, "Apply", enabled, &t, egui::Id::unique("mi-apply")).clicked() {
if widgets::text_button(ui, b, crate::i18n::tr("Apply"), enabled, &t, egui::Id::unique("mi-apply")).clicked() {
match app.session.execute("mask.interpolate", json!({})) {
Ok(r) => app.ui.status = format!("Mask Interpolation: {} keyframes added ({} vertices)", r["keys"], r["vertices"]),
Err(e) => app.ui.status = e.to_string(),
@@ -413,12 +413,12 @@ pub fn editor(
let b1 = Rect::from_min_size(pos2(r.min.x + 24.0, r.min.y + 4.0), vec2(84.0, 22.0));
let b2 = Rect::from_min_size(pos2(b1.max.x + 8.0, b1.min.y), vec2(84.0, 22.0));
let b3 = Rect::from_min_size(pos2(b2.max.x + 8.0, b1.min.y), vec2(110.0, 22.0));
if widgets::text_button(ui, b1, "Analyze", false, &t, egui::Id::unique(("ct-analyze", g.uid))).clicked() && !busy {
if widgets::text_button(ui, b1, crate::i18n::tr("Analyze"), false, &t, egui::Id::unique(("ct-analyze", g.uid))).clicked() && !busy {
actions.push(("camera.analyze".into(), me.clone()));
}
app.auto.add(&format!("effectControls.cameraTracker.{}.analyze", g.uid), b1, "Analyze");
if running {
if widgets::text_button(ui, b2, "Cancel", false, &t, egui::Id::unique(("ct-cancel", g.uid))).clicked() {
if widgets::text_button(ui, b2, crate::i18n::tr("Cancel"), false, &t, egui::Id::unique(("ct-cancel", g.uid))).clicked() {
actions.push(("camera.cancel".into(), json!({})));
}
app.auto.add(&format!("effectControls.cameraTracker.{}.cancel", g.uid), b2, "Cancel");
@@ -426,7 +426,7 @@ pub fn editor(
let params = effectcraft_engine::camera_track::static_params(g);
let solve = ct::solve(&params);
if solve.is_some() {
if widgets::text_button(ui, b3, "Create Camera", false, &t, egui::Id::unique(("ct-camera", g.uid))).clicked() {
if widgets::text_button(ui, b3, crate::i18n::tr("Create Camera"), false, &t, egui::Id::unique(("ct-camera", g.uid))).clicked() {
actions.push(("camera.create".into(), me.clone()));
}
app.auto.add(&format!("effectControls.cameraTracker.{}.createCamera", g.uid), b3, "Create Camera");
+8 -4
View File
@@ -330,11 +330,11 @@ fn tab_body(
let cur = PIXEL_ASPECTS
.iter()
.find(|(_, v)| (v - d.pixel_aspect).abs() < 1e-3)
.map(|p| p.0.to_string())
.map(|p| tr(p.0).to_string())
.unwrap_or_else(|| format!("{:.2}", d.pixel_aspect));
let r = craft_ui::ComboBox::from_id_salt("comp-par").width(220.0).selected_text(cur).show_ui(ui, |ui| {
for (name, v) in PIXEL_ASPECTS {
if ui.selectable_label((v - d.pixel_aspect).abs() < 1e-3, *name).clicked() {
if ui.selectable_label((v - d.pixel_aspect).abs() < 1e-3, tr(name)).clicked() {
d.pixel_aspect = *v;
}
}
@@ -402,7 +402,7 @@ fn tab_body(
ui.horizontal(|ui| {
let r = crate::widgets::srgb_color_button(ui, &mut d.bg);
auto.add("dialog.comp.background", r.rect, "Background Color");
ui.label(color_name(d.bg));
ui.label(tr(color_name(d.bg)));
});
ui.end_row();
});
@@ -487,6 +487,7 @@ pub fn show(
auto: &mut Registry,
custom: &[CompPreset],
cmds: &mut Vec<(&'static str, serde_json::Value)>,
preview: Option<&mut bool>,
) -> Option<serde_json::Value> {
let mut out = None;
ui.horizontal(|ui| {
@@ -498,7 +499,7 @@ pub fn show(
ui.add_space(8.0);
ui.horizontal(|ui| {
for (tab, label, key) in [(Tab::Basic, "Basic", "basic"), (Tab::Advanced, "Advanced", "advanced"), (Tab::Renderer, "3D Renderer", "renderer")] {
let r = ui.selectable_label(d.tab == tab, label);
let r = ui.selectable_label(d.tab == tab, tr(label));
auto.add(&format!("dialog.comp.tab.{key}"), r.rect, label);
if r.clicked() {
d.tab = tab;
@@ -528,6 +529,9 @@ pub fn show(
if r.clicked() {
out = Some(serde_json::Value::Null);
}
if let Some(on) = preview {
super::live_preview::checkbox(ui, auto, on, "dialog.comp.preview");
}
});
});
out
+87 -8
View File
@@ -66,6 +66,8 @@ pub struct DialogState {
pub delete_items: super::delete_items::Pending,
/// The Gradient Editor's draft and the property it edits.
pub gradient: super::gradient_editor::Draft,
/// Preview of the settings dialog being edited (Solid, Camera, Light, Composition Settings).
pub live: super::live_preview::LivePreview,
/// The user template the Delete Template prompt asks about: its id and name.
pub delete_template: Option<(String, String)>,
}
@@ -76,6 +78,7 @@ pub fn open_new_comp(app: &mut EffectcraftApp) {
app.dialog_state.comp =
CompDraft { name: format!("Comp {n}"), tab: super::comp_settings::Tab::Basic, start_tc: None, dur_tc: None, preset_name: None, ..last };
app.dialog_state.editing_existing = false;
app.dialog_state.live = Default::default();
app.dialog = Some(Dialog::NewComp);
}
@@ -109,6 +112,7 @@ pub fn open_comp_settings_for(app: &mut EffectcraftApp, cid: ItemId) -> Result<(
app.dialog_state.editing_existing = true;
app.dialog_state.editing_comp = Some(cid);
app.dialog = Some(Dialog::CompSettings);
super::live_preview::begin(app, Dialog::CompSettings);
Ok(())
}
@@ -123,6 +127,7 @@ pub fn open_new_solid(app: &mut EffectcraftApp) -> Result<(), String> {
if d.solid_color == [0.0; 3] {
d.solid_color = [0.2, 0.45, 0.85];
}
d.live = Default::default();
app.dialog = Some(Dialog::SolidSettings);
Ok(())
}
@@ -138,6 +143,7 @@ fn open_solid_settings(app: &mut EffectcraftApp, layer: u64, item: ItemId) -> Re
(d.solid_name, d.solid_color, d.solid_size, d.solid_pixel_aspect) = (name, so.color, [so.width, so.height], so.pixel_aspect);
(d.solid_layer, d.solid_shared, d.solid_affect_all) = (Some(layer), users > 1, false);
app.dialog = Some(Dialog::SolidSettings);
super::live_preview::begin(app, Dialog::SolidSettings);
Ok(())
}
@@ -190,6 +196,31 @@ pub(crate) fn modal_vertical(ctx: &egui::Context, title: &str, size: egui::Vec2,
modal_with_scroll(ctx, title, size, t, true, body);
}
/// Where a modal dialog opens and whether it dims the window behind it. The default is the middle
/// of the window over a dimmed backdrop.
#[derive(Clone, Copy, Debug)]
pub struct Placement {
/// The opening position as a fraction of the room the dialog can move in, from the middle
/// `(0, 0)` to the bottom right `(1, 1)`.
pub start: egui::Vec2,
/// Dim the window behind the dialog.
pub dim: bool,
}
impl Default for Placement {
fn default() -> Self {
Placement { start: vec2(0.0, 0.0), dim: true }
}
}
/// [`modal`] at `place`: a dialog that edits what is behind it keeps the canvas in view.
pub(crate) fn modal_placed(ctx: &egui::Context, title: &str, size: egui::Vec2, t: &Tokens, place: Placement, body: impl FnOnce(&mut egui::Ui)) {
let max_height = (modal_space(ctx).y - 84.0).max(40.0);
modal_fit_placed(ctx, title, size, t, place, |ui, _| {
egui::ScrollArea::both().max_height(max_height).auto_shrink([false, true]).show(ui, body);
});
}
fn modal_with_scroll(ctx: &egui::Context, title: &str, size: egui::Vec2, t: &Tokens, vertical_only: bool, body: impl FnOnce(&mut egui::Ui)) {
let max_height = (modal_space(ctx).y - 84.0).max(40.0);
modal_fit(ctx, title, size, t, |ui, _| {
@@ -202,6 +233,11 @@ fn modal_with_scroll(ctx: &egui::Context, title: &str, size: egui::Vec2, t: &Tok
/// only its content: `body` gets the height left under the title, with `size` kept within the
/// window.
pub(crate) fn modal_fit(ctx: &egui::Context, title: &str, size: egui::Vec2, t: &Tokens, body: impl FnOnce(&mut egui::Ui, f32)) {
modal_fit_placed(ctx, title, size, t, Placement::default(), body);
}
/// [`modal_fit`] at `place`.
pub(crate) fn modal_fit_placed(ctx: &egui::Context, title: &str, size: egui::Vec2, t: &Tokens, place: Placement, body: impl FnOnce(&mut egui::Ui, f32)) {
let pass = ctx.cumulative_pass_nr_for(egui::ViewportId::ROOT);
ctx.data_mut(|d| d.insert_temp(modal_pass_id(), pass));
let size = size.min(modal_space(ctx));
@@ -210,10 +246,10 @@ pub(crate) fn modal_fit(ctx: &egui::Context, title: &str, size: egui::Vec2, t: &
// window, so dragging back never has to undo a part the dialog could not follow.
let offset_id = id.with("offset");
let room = ((ctx.content_rect().size() - size) * 0.5).max(vec2(0.0, 0.0));
let offset = ctx.data(|d| d.get_temp::<egui::Vec2>(offset_id)).unwrap_or_default().clamp(-room, room);
let offset = ctx.data(|d| d.get_temp::<egui::Vec2>(offset_id)).unwrap_or(room * place.start).clamp(-room, room);
egui::Modal::new(id)
.area(egui::Modal::default_area(id).anchor(Align2::CENTER_CENTER, offset))
.backdrop_color(Color32::from_black_alpha(120))
.backdrop_color(Color32::from_black_alpha(if place.dim { 120 } else { 0 }))
.frame(egui::Frame::window(&ctx.style_of(ctx.theme())).fill(t.panel_bg).inner_margin(egui::Margin::same(18)))
.show(ctx, |ui| {
ui.set_width(size.x - 36.0);
@@ -403,9 +439,15 @@ fn comp_settings(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens, exis
let mut result = None;
let custom = app.session.prefs.comp_presets.clone();
let mut cmds = Vec::new();
modal(ctx, "Composition Settings", vec2(600.0, 470.0), t, |ui| {
result = super::comp_settings::show(ui, &mut d, t, &mut app.auto, &custom, &mut cmds);
// The title keys the modal id and its drag offset; `modal_fit` translates the heading itself,
// so passing the source keeps the dialog in place when the interface language changes.
let live = existing && super::live_preview::active(app);
let place = super::live_preview::place(app);
let mut preview = app.dialog_state.live.on;
modal_placed(ctx, "Composition Settings", vec2(600.0, 470.0), t, place, |ui| {
result = super::comp_settings::show(ui, &mut d, t, &mut app.auto, &custom, &mut cmds, live.then_some(&mut preview));
});
app.dialog_state.live.on = preview;
if d.preset_name.is_some() {
// Save Preset asks for the name in its own window over the dialog, as After Effects does.
result = None;
@@ -423,11 +465,25 @@ fn comp_settings(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens, exis
}
app.dialog_state.comp = d;
match result {
Some(serde_json::Value::Null) => app.dialog = None,
Some(serde_json::Value::Null) => {
super::live_preview::cancel(app);
app.dialog = None;
}
Some(mut params) => {
if existing && let (Some(cid), Some(m)) = (app.dialog_state.editing_comp, params.as_object_mut()) {
m.insert("comp".into(), json!(cid.0));
}
// A preview already made the edit (one undo step): OK keeps it.
if existing && super::live_preview::keep(app, "comp.settings", params.clone()) {
// The Resolution is not part of the command, so the preview never applied it.
if let Some(cid) = app.dialog_state.editing_comp
&& let Err(e) = set_resolution(app, cid.0, app.dialog_state.comp.resolution)
{
app.ui.status = e.to_string();
}
app.dialog = None;
return;
}
let r = if existing { app.session.execute("comp.settings", params) } else { app.session.execute("comp.new", params) };
let cid = if existing { app.dialog_state.editing_comp.map(|c| c.0) } else { r.as_ref().ok().and_then(|v| v["comp"].as_u64()) };
let r = match cid {
@@ -442,7 +498,15 @@ fn comp_settings(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens, exis
}
app.dialog = None;
}
None => {}
None => {
if existing && let Some(cid) = app.dialog_state.editing_comp {
let mut params = super::comp_settings::params(&app.dialog_state.comp);
if let Some(m) = params.as_object_mut() {
m.insert("comp".into(), json!(cid.0));
}
super::live_preview::sync(app, "comp.settings", params);
}
}
}
}
@@ -499,8 +563,11 @@ fn solid(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
let comp_size = app.session.active_comp().map(|c| (c.width, c.height)).unwrap_or((1920, 1080));
let lock_id = egui::Id::unique("solid-lock-aspect");
let mut lock = ctx.data(|m| m.get_temp::<bool>(lock_id)).unwrap_or(true);
let live = super::live_preview::active(app);
let place = super::live_preview::place(app);
let mut preview = app.dialog_state.live.on;
let auto = &mut app.auto;
modal(ctx, "Solid Settings", vec2(480.0, if editing.is_some() { 430.0 } else { 390.0 }), t, |ui| {
modal_placed(ctx, "Solid Settings", vec2(480.0, if editing.is_some() { 430.0 } else { 390.0 }), t, place, |ui| {
ui.horizontal(|ui| {
ui.label(tr("Name:"));
let r = ui.add(egui::TextEdit::singleline(&mut name).desired_width(320.0));
@@ -586,16 +653,28 @@ fn solid(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
let r = ui.button(tr("Cancel"));
auto.add("dialog.solid.cancel", r.rect, "Cancel");
close = r.clicked();
if live {
super::live_preview::checkbox(ui, auto, &mut preview, "dialog.solid.preview");
}
});
});
ctx.data_mut(|m| m.insert_temp(lock_id, lock));
let ds = &mut app.dialog_state;
ds.live.on = preview;
(ds.solid_name, ds.solid_color, ds.solid_size, ds.solid_pixel_aspect, ds.solid_affect_all) = (name, color, size, par, affect_all);
let (command, params) = (solid_command(ds), solid_params(ds));
if ok || ui_enter(ctx) {
if let Err(e) = app.session.execute(solid_command(ds), solid_params(ds)) {
// A preview already made the edit (one undo step): OK keeps it.
if !super::live_preview::keep(app, command, params.clone())
&& let Err(e) = app.session.execute(command, params)
{
app.ui.status = e.to_string();
}
close = true;
} else if close {
super::live_preview::cancel(app);
} else {
super::live_preview::sync(app, command, params);
}
if close {
app.dialog = None;
+123 -56
View File
@@ -44,12 +44,19 @@ pub struct LightDraft {
const FALLOFFS: [&str; 3] = ["None", "Smooth", "Inverse Square Clamped"];
/// Whether a light type has Falloff and Shadows options. Ambient and Environment lights come
/// from no one place, so they have neither.
/// Whether a light type has Falloff options (distance attenuation): image-based Environment
/// lights and Ambient come from no one place, so they have none.
fn falls_off(kind: LightKind) -> bool {
!matches!(kind, LightKind::Ambient | LightKind::Environment)
}
/// Whether a light type has Shadows options in the dialog. After Effects' Light Settings shows
/// Cast Shadows (with Shadow Darkness and Diffusion) for every light but Ambient — an
/// Environment light's shadow comes from its image, not a falloff.
fn casts_shadows(kind: LightKind) -> bool {
kind != LightKind::Ambient
}
fn comp_w(app: &EffectcraftApp) -> f64 {
app.session.active_comp().map(|c| c.width as f64).unwrap_or(1920.0)
}
@@ -92,8 +99,14 @@ pub fn open_camera(app: &mut EffectcraftApp, layer: Option<u64>) -> Result<(), S
}
}
};
let editing = d.layer.is_some();
app.dialog_state.camera = d;
app.dialog = Some(Dialog::CameraSettings);
if editing {
super::live_preview::begin(app, Dialog::CameraSettings);
} else {
app.dialog_state.live = Default::default();
}
Ok(())
}
@@ -142,13 +155,19 @@ pub fn open_light(app: &mut EffectcraftApp, layer: Option<u64>) -> Result<(), St
}
}
};
let editing = d.layer.is_some();
app.dialog_state.light = d;
app.dialog = Some(Dialog::LightSettings);
if editing {
super::live_preview::begin(app, Dialog::LightSettings);
} else {
app.dialog_state.live = Default::default();
}
Ok(())
}
fn buttons(ui: &mut egui::Ui, t: &Tokens, ok: &mut bool, close: &mut bool) -> (egui::Rect, egui::Rect) {
let mut r = (egui::Rect::NOTHING, egui::Rect::NOTHING);
fn buttons(ui: &mut egui::Ui, t: &Tokens, ok: &mut bool, close: &mut bool, preview: Option<&mut bool>) -> (egui::Rect, egui::Rect, egui::Rect) {
let mut r = (egui::Rect::NOTHING, egui::Rect::NOTHING, egui::Rect::NOTHING);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let b = ui.add(egui::Button::new(egui::RichText::new(tr(" OK ")).color(Color32::WHITE)).fill(t.accent));
r.0 = b.rect;
@@ -160,6 +179,9 @@ fn buttons(ui: &mut egui::Ui, t: &Tokens, ok: &mut bool, close: &mut bool) -> (e
if c.clicked() {
*close = true;
}
if let Some(on) = preview {
r.2 = ui.checkbox(on, tr("Preview")).rect;
}
});
r
}
@@ -168,8 +190,11 @@ pub fn camera(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
let w = comp_w(app);
let mut d = app.dialog_state.camera.clone();
let (mut ok, mut close) = (false, false);
let mut rects = (egui::Rect::NOTHING, egui::Rect::NOTHING);
super::dialogs::modal(ctx, "Camera Settings", vec2(520.0, 420.0), t, |ui| {
let mut rects = (egui::Rect::NOTHING, egui::Rect::NOTHING, egui::Rect::NOTHING);
let live = super::live_preview::active(app);
let place = super::live_preview::place(app);
let mut preview = app.dialog_state.live.on;
super::dialogs::modal_placed(ctx, "Camera Settings", vec2(520.0, 420.0), t, place, |ui| {
egui::Grid::new("cam-grid").num_columns(2).spacing([14.0, 9.0]).show(ui, |ui| {
ui.label(tr("Type"));
craft_ui::ComboBox::from_id_salt("cam-type").selected_text(if d.two_node { "Two-Node Camera" } else { "One-Node Camera" }).show_ui(ui, |ui| {
@@ -222,46 +247,66 @@ pub fn camera(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
ui.end_row();
});
ui.add_space(14.0);
rects = buttons(ui, t, &mut ok, &mut close);
rects = buttons(ui, t, &mut ok, &mut close, live.then_some(&mut preview));
});
app.dialog_state.live.on = preview;
if d.lock_to_zoom {
d.focus = d.zoom;
}
app.auto.add("dialog.camera.ok", rects.0, "OK");
app.auto.add("dialog.camera.cancel", rects.1, "Cancel");
if live {
app.auto.add("dialog.camera.preview", rects.2, "Preview");
}
app.dialog_state.camera = d.clone();
let (id, p) = camera_command(&d);
if ok || super::dialogs::ui_enter(ctx) {
let mut p = json!({
"name": d.name,
"type": if d.two_node { "twoNode" } else { "oneNode" },
"zoom": d.zoom,
"dof": d.dof,
"focusDistance": d.focus,
"aperture": d.aperture,
"blurLevel": d.blur,
});
let id = match d.layer {
Some(l) => {
p["layer"] = json!(l);
"layer.cameraSettings"
}
None => "layer.newCamera",
};
if let Err(e) = app.session.execute(id, p) {
// A preview already made the edit (one undo step): OK keeps it.
if !super::live_preview::keep(app, id, p.clone())
&& let Err(e) = app.session.execute(id, p)
{
app.ui.status = e.to_string();
}
close = true;
} else if close {
super::live_preview::cancel(app);
} else {
super::live_preview::sync(app, id, p);
}
if close {
app.dialog = None;
}
}
/// The command Camera Settings' OK runs, and its parameters.
fn camera_command(d: &CameraDraft) -> (&'static str, Value) {
let mut p = json!({
"name": d.name,
"type": if d.two_node { "twoNode" } else { "oneNode" },
"zoom": d.zoom,
"dof": d.dof,
"focusDistance": d.focus,
"aperture": d.aperture,
"blurLevel": d.blur,
});
let id = match d.layer {
Some(l) => {
p["layer"] = json!(l);
"layer.cameraSettings"
}
None => "layer.newCamera",
};
(id, p)
}
pub fn light(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
let mut d = app.dialog_state.light.clone();
let (mut ok, mut close) = (false, false);
let mut rects = (egui::Rect::NOTHING, egui::Rect::NOTHING);
super::dialogs::modal(ctx, "Light Settings", vec2(480.0, 470.0), t, |ui| {
let mut rects = (egui::Rect::NOTHING, egui::Rect::NOTHING, egui::Rect::NOTHING);
let live = super::live_preview::active(app);
let place = super::live_preview::place(app);
let mut preview = app.dialog_state.live.on;
super::dialogs::modal_placed(ctx, "Light Settings", vec2(480.0, 470.0), t, place, |ui| {
egui::Grid::new("light-grid").num_columns(2).spacing([14.0, 9.0]).show(ui, |ui| {
ui.label(tr("Name"));
let r = ui.add(egui::TextEdit::singleline(&mut d.name).desired_width(220.0));
@@ -306,8 +351,11 @@ pub fn light(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut d.falloff_distance).range(0.0..=100000.0).suffix(" px")));
ui.end_row();
}
}
if casts_shadows(d.kind) {
ui.label(tr("Shadows"));
ui.checkbox(&mut d.casts_shadows, tr("Casts Shadows"));
let cb = ui.checkbox(&mut d.casts_shadows, tr("Casts Shadows"));
app.auto.add_response("dialog.light.casts", &cb, "Casts Shadows");
ui.end_row();
ui.label(tr("Shadow Darkness"));
ui.add_enabled(d.casts_shadows, crate::widgets::drag_value(egui::DragValue::new(&mut d.shadow_darkness).range(0.0..=100.0).suffix(" %")));
@@ -318,52 +366,71 @@ pub fn light(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
}
});
ui.add_space(14.0);
if falls_off(d.kind) {
if casts_shadows(d.kind) {
ui.label(egui::RichText::new(tr("Note: Shadows are only cast from layers with Cast Shadows enabled.")).font(Tokens::ui(11.0)).color(t.text_dim));
ui.add_space(8.0);
}
rects = buttons(ui, t, &mut ok, &mut close);
rects = buttons(ui, t, &mut ok, &mut close, live.then_some(&mut preview));
});
app.dialog_state.live.on = preview;
app.auto.add("dialog.light.ok", rects.0, "OK");
app.auto.add("dialog.light.cancel", rects.1, "Cancel");
if live {
app.auto.add("dialog.light.preview", rects.2, "Preview");
}
app.dialog_state.light = d.clone();
let (id, p) = light_command(&d);
if ok || super::dialogs::ui_enter(ctx) {
let kind = d.kind;
let mut p = json!({
"name": d.name,
"kind": kind.label(),
"color": [d.color[0], d.color[1], d.color[2]],
"intensity": d.intensity,
});
if kind == LightKind::Spot {
p["coneAngle"] = json!(d.cone_angle);
p["coneFeather"] = json!(d.cone_feather);
}
if falls_off(kind) {
p["falloff"] = json!(d.falloff);
p["radius"] = json!(d.radius);
p["falloffDistance"] = json!(d.falloff_distance);
p["castsShadows"] = json!(d.casts_shadows);
p["shadowDarkness"] = json!(d.shadow_darkness);
p["shadowDiffusion"] = json!(d.shadow_diffusion);
}
let id = match d.layer {
Some(l) => {
p["layer"] = json!(l);
"layer.lightSettings"
}
None => "layer.newLight",
};
if let Err(e) = app.session.execute(id, p) {
// A preview already made the edit (one undo step): OK keeps it.
if !super::live_preview::keep(app, id, p.clone())
&& let Err(e) = app.session.execute(id, p)
{
app.ui.status = e.to_string();
}
close = true;
} else if close {
super::live_preview::cancel(app);
} else {
super::live_preview::sync(app, id, p);
}
if close {
app.dialog = None;
}
}
/// The command Light Settings' OK runs, and its parameters.
fn light_command(d: &LightDraft) -> (&'static str, Value) {
let kind = d.kind;
let mut p = json!({
"name": d.name,
"kind": kind.label(),
"color": [d.color[0], d.color[1], d.color[2]],
"intensity": d.intensity,
});
if kind == LightKind::Spot {
p["coneAngle"] = json!(d.cone_angle);
p["coneFeather"] = json!(d.cone_feather);
}
if falls_off(kind) {
p["falloff"] = json!(d.falloff);
p["radius"] = json!(d.radius);
p["falloffDistance"] = json!(d.falloff_distance);
}
if casts_shadows(kind) {
p["castsShadows"] = json!(d.casts_shadows);
p["shadowDarkness"] = json!(d.shadow_darkness);
p["shadowDiffusion"] = json!(d.shadow_diffusion);
}
let id = match d.layer {
Some(l) => {
p["layer"] = json!(l);
"layer.lightSettings"
}
None => "layer.newLight",
};
(id, p)
}
/// Menu/shortcut routing: commands that open a 3D settings dialog when invoked without params.
/// Returns true when a dialog was opened.
pub fn route(app: &mut EffectcraftApp, id: &str, params: &Value) -> Result<bool, String> {
@@ -8,12 +8,12 @@ pub(super) fn warp_editor(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui:
let busy = app.session.is_warp_analyzing();
let b1 = Rect::from_min_size(pos2(r.min.x + 24.0, r.min.y + 4.0), vec2(84.0, 22.0));
let b2 = Rect::from_min_size(pos2(b1.max.x + 8.0, b1.min.y), vec2(84.0, 22.0));
if widgets::text_button(ui, b1, "Analyze", false, &t, egui::Id::unique(("warp-analyze", g.uid))).clicked() && !busy {
if widgets::text_button(ui, b1, crate::i18n::tr("Analyze"), false, &t, egui::Id::unique(("warp-analyze", g.uid))).clicked() && !busy {
actions.push(("warp.analyze".into(), json!({"layer": layer.id.0, "effect": g.uid})));
}
app.auto.add(&format!("effectControls.warp.{}.analyze", g.uid), b1, "Analyze");
if running {
if widgets::text_button(ui, b2, "Cancel", false, &t, egui::Id::unique(("warp-cancel", g.uid))).clicked() {
if widgets::text_button(ui, b2, crate::i18n::tr("Cancel"), false, &t, egui::Id::unique(("warp-cancel", g.uid))).clicked() {
actions.push(("warp.cancel".into(), json!({})));
}
app.auto.add(&format!("effectControls.warp.{}.cancel", g.uid), b2, "Cancel");
@@ -192,7 +192,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
bp.text(
pos2(body.center().x, body.min.y + 40.0),
Align2::CENTER_CENTER,
"No effects. Drag one from Effects & Presets.",
crate::i18n::tr("No effects. Drag one from Effects & Presets."),
Tokens::ui(12.0),
t.text_faint,
);
+1 -1
View File
@@ -376,7 +376,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
let sr = Rect::from_min_size(rect.min + vec2(8.0, 8.0), vec2(rect.width() - 44.0, 24.0));
let mut q = app.ui.effects_search.clone();
widgets::search_field(ui, sr, &mut q, "Search effects and presets", &t);
widgets::search_field(ui, sr, &mut q, crate::i18n::tr("Search effects and presets"), &t);
app.ui.effects_search = q.clone();
app.auto.add("effects.search", sr, "Search");
let force_scan = contents_menu(app, ui, &ctx, rect, sr, &t);
+1 -1
View File
@@ -250,7 +250,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let p = ui.painter().with_clip_rect(rect);
p.rect_filled(rect, 0.0, t.tl_bg);
let Some(rid) = root(app) else {
p.text(rect.center(), Align2::CENTER_CENTER, "(no composition)", Tokens::ui(12.0), t.text_faint);
p.text(rect.center(), Align2::CENTER_CENTER, crate::i18n::tr("(no composition)"), Tokens::ui(12.0), t.text_faint);
return;
};
// ---- toolbar: switches and direction.
+1 -1
View File
@@ -68,7 +68,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let cached: Option<((u64, i64, u64), egui::TextureHandle, [u32; 2])> = ctx.data(|d| d.get_temp(tid));
let tex = match cached {
Some((k, tex, size)) if k == key => Some((tex, size)),
_ => app.session.footage.frame(view.item, f, view.time).map(|img| {
_ => effectcraft_engine::commands::footage_panel::frame(&app.session, view.item, f, view.time).map(|img| {
let ci = egui::ColorImage::from_rgba_unmultiplied([img.width as usize, img.height as usize], &img.to_rgba8());
let tex = crate::frames::load_fitted(&ctx, "footage-panel", ci, egui::TextureOptions::LINEAR);
ctx.data_mut(|d| d.insert_temp(tid, (key, tex.clone(), [img.width, img.height])));
+130 -4
View File
@@ -233,7 +233,7 @@ fn new_comp_from_selection_form(s: &effectcraft_engine::Session) -> (String, Vec
}
/// The Interpret Footage dialog for the selected footage (or its proxy), if there is one.
fn interpret_footage_form(s: &effectcraft_engine::Session, proxy: bool) -> Option<(String, Vec<Field>)> {
fn interpret_footage_form(s: &effectcraft_engine::Session, proxy: bool, base: &mut Value) -> Option<(String, Vec<Field>)> {
let f = s.state.project_selection.first().and_then(|i| s.project.item(*i)).and_then(|it| match (&it.kind, &it.proxy) {
(_, Some(px)) if proxy => Some(px.footage.clone()),
(effectcraft_engine::project::ItemKind::Footage(f), _) if !proxy => Some(f.clone()),
@@ -286,6 +286,30 @@ fn interpret_footage_form(s: &effectcraft_engine::Session, proxy: bool) -> Optio
Field::bool("preserveRgb", "Preserve RGB", f.preserve_rgb),
Field::bool("linearLight", "Interpret As Linear Light", f.linear_light),
];
// Color ▸ Cineon Settings (DPX and Cineon files). The converted points show in the
// footage's Units, and `pointUnits` reads them back in those units when Units changes.
if f.is_cineon_file() {
use effectcraft_engine::project::CineonUnits;
let c = f.cineon;
let units: Vec<(&str, Value)> = CineonUnits::ALL.iter().map(|u| (u.label(), json!(u.id()))).collect();
fields.extend([
Field::note("cineonSettings", "Cineon Settings"),
Field::bool("logConversion", "Logarithmic Conversion", c.log_conversion),
Field::num("convertedBlack", "Converted Black Point", c.units.from_ten_bit(f64::from(c.converted_black))),
Field::num("convertedWhite", "Converted White Point", c.units.from_ten_bit(f64::from(c.converted_white))),
Field::num("tenBitBlack", "10 Bit Black Point", f64::from(c.black)),
Field::num("tenBitWhite", "10 Bit White Point", f64::from(c.white)),
Field::num("currentGamma", "Current Gamma", f64::from(c.gamma)),
Field::num("highlightRolloff", "Highlight Rolloff", f64::from(c.rolloff)),
Field::choice("cineonUnits", "Units", &units, CineonUnits::ALL.iter().position(|u| *u == c.units).unwrap_or(0)),
]);
if !base.is_object() {
*base = json!({});
}
if let Some(m) = base.as_object_mut() {
m.insert("pointUnits".into(), json!(c.units.id()));
}
}
// After Frame Rate: an image sequence's frames (missing ones show colour bars, as in
// After Effects), then Start Timecode.
let mut timing = Vec::new();
@@ -487,7 +511,7 @@ pub fn open_form(app: &mut EffectcraftApp, id: &str, p: &Value) -> bool {
let layer = comp.and_then(|c| s.state.selected_layers.first().and_then(|l| c.layer(*l)));
let t = s.time();
let lt = layer.map(|l| l.layer_time(t)).unwrap_or(t);
let base = p.clone();
let mut base = p.clone();
let (title, fields): (String, Vec<Field>) = match id {
// Photoshop files: Import Kind (Footage / Composition / – Retain Layer Sizes). Dropped
// files start at Settings ▸ Import ▸ Default Drag Import As.
@@ -651,10 +675,33 @@ pub fn open_form(app: &mut EffectcraftApp, id: &str, p: &Value) -> bool {
],
),
"file.interpretFootage" | "file.interpretProxy"
if !has(p, &["frameRate", "alpha", "loop", "pixelAspect", "colorProfile", "fields", "invertAlpha", "matteColor", "linearLight", "startFrame"]) =>
if !has(
p,
&[
"frameRate",
"alpha",
"loop",
"pixelAspect",
"colorProfile",
"fields",
"invertAlpha",
"matteColor",
"linearLight",
"startFrame",
"logConversion",
"cineonUnits",
"pointUnits",
"convertedBlack",
"convertedWhite",
"tenBitBlack",
"tenBitWhite",
"currentGamma",
"highlightRolloff",
],
) =>
{
let proxy = id == "file.interpretProxy";
let Some(form) = interpret_footage_form(s, proxy) else { return false };
let Some(form) = interpret_footage_form(s, proxy, &mut base) else { return false };
form
}
"file.projectSettings" if p.as_object().is_none_or(|m| m.is_empty()) => project_settings_form(s),
@@ -1074,4 +1121,83 @@ mod tests {
assert_eq!(r["path"], path.as_str());
assert!(std::fs::read_to_string(&path).unwrap().contains("<xmeml"));
}
#[test]
fn interpretation_menu_applies_explicit_cineon_parameters_without_a_dialog() {
use effectcraft_engine::project::{Footage, FootageKind, ItemKind, Proxy};
for command in ["file.interpretFootage", "file.interpretProxy"] {
let mut app = EffectcraftApp::new(effectcraft_engine::Session::default());
let footage = Footage { path: "/m/scan.dpx".into(), kind: FootageKind::Still, width: 4, height: 4, has_video: true, ..Default::default() };
let project = std::sync::Arc::make_mut(&mut app.session.project);
let item = project.add_item("scan.dpx", Default::default(), None, ItemKind::Footage(footage.clone()));
project.item_mut(item).unwrap().proxy = Some(Box::new(Proxy { footage, enabled: true }));
app.session.state.project_selection = vec![item];
let ctx = egui::Context::default();
for params in [
json!({"logConversion": true}),
json!({"cineonUnits": "10bit"}),
json!({"pointUnits": "10bit"}),
json!({"convertedBlack": 10.0}),
json!({"convertedWhite": 900.0}),
json!({"tenBitBlack": 90.0}),
json!({"tenBitWhite": 690.0}),
json!({"currentGamma": 2.2}),
json!({"highlightRolloff": 30.0}),
] {
crate::menus::invoke(&mut app, &ctx, command, params.clone()).unwrap();
assert!(app.dialog.is_none(), "{command}: {params}");
}
let item = app.session.project.item(item).unwrap();
let footage = if command == "file.interpretProxy" {
&item.proxy.as_ref().unwrap().footage
} else {
let ItemKind::Footage(footage) = &item.kind else { panic!("not footage") };
footage
};
assert!(footage.cineon.log_conversion);
assert_eq!(footage.cineon.gamma, 2.2);
assert_eq!(footage.cineon.converted_white, 900.0);
}
}
/// Interpret Footage shows Cineon Settings for DPX footage only. The converted points show in
/// the footage's Units, and choosing other Units reads the shown values in the old ones.
#[test]
fn interpret_footage_shows_cineon_settings_for_dpx() {
use effectcraft_engine::project::{CineonUnits, Footage, FootageKind, ItemKind};
let mut app = EffectcraftApp::new(effectcraft_engine::Session::default());
let still =
|path: &str| ItemKind::Footage(Footage { path: path.into(), kind: FootageKind::Still, width: 4, height: 4, has_video: true, ..Default::default() });
let project = std::sync::Arc::make_mut(&mut app.session.project);
let dpx = project.add_item("scan.dpx", Default::default(), None, still("/m/scan.dpx"));
let png = project.add_item("still.png", Default::default(), None, still("/m/still.png"));
let keys = |app: &EffectcraftApp| app.dialog_state.form.fields.iter().map(|f| f.key.clone()).collect::<Vec<_>>();
app.session.state.project_selection = vec![png];
assert!(open_form(&mut app, "file.interpretFootage", &json!({})));
assert!(!keys(&app).iter().any(|k| k == "logConversion"), "{:?}", keys(&app));
app.session.state.project_selection = vec![dpx];
assert!(open_form(&mut app, "file.interpretFootage", &json!({})));
for k in ["logConversion", "convertedBlack", "convertedWhite", "tenBitBlack", "tenBitWhite", "currentGamma", "highlightRolloff", "cineonUnits"] {
assert!(keys(&app).iter().any(|x| x == k), "{k}: {:?}", keys(&app));
}
// Units ▸ 8-bit with Converted White Point still showing 1023 (10-bit).
let form = &mut app.dialog_state.form;
for f in form.fields.iter_mut() {
match (f.key.as_str(), &mut f.kind) {
("cineonUnits", FieldKind::Choice { sel, .. }) => *sel = 1,
("logConversion", FieldKind::Bool(b)) => *b = true,
_ => {}
}
}
let params = form.params();
assert_eq!((params["pointUnits"].as_str(), params["cineonUnits"].as_str()), (Some("10bit"), Some("8bit")), "{params}");
app.session.execute("file.interpretFootage", params).unwrap();
let Some(ItemKind::Footage(f)) = app.session.project.item(dpx).map(|i| &i.kind) else { panic!("not footage") };
assert!(f.cineon.log_conversion && f.cineon.units == CineonUnits::EightBit, "{:?}", f.cineon);
assert!((f.cineon.converted_white - 1023.0).abs() < 1e-3, "{:?}", f.cineon);
// Opened again, the point shows in 8-bit units.
assert!(open_form(&mut app, "file.interpretFootage", &json!({})));
let white = app.dialog_state.form.fields.iter().find(|f| f.key == "convertedWhite").map(|f| f.kind.clone());
assert!(matches!(white, Some(FieldKind::Number { value, .. }) if (value - 255.0).abs() < 1e-6), "{white:?}");
}
}
+4 -4
View File
@@ -600,7 +600,7 @@ fn colorama_wheel(
p.text(
pos2(sr.min.x, sr.max.y + 9.0),
Align2::LEFT_CENTER,
"Click the ring to add a colour; Alt-click a stop (or drag it off) to remove it",
crate::i18n::tr("Click the ring to add a colour; Alt-click a stop (or drag it off) to remove it"),
Tokens::ui(10.5),
t.text_faint,
);
@@ -801,19 +801,19 @@ fn reshape_buttons(
let b1 = Rect::from_min_size(pos2(r.min.x + 24.0, r.min.y + 4.0), vec2(84.0, 22.0));
let b2 = Rect::from_min_size(pos2(b1.max.x + 8.0, b1.min.y), vec2(96.0, 22.0));
let b3 = Rect::from_min_size(pos2(b2.max.x + 8.0, b1.min.y), vec2(56.0, 22.0));
if widgets::text_button(ui, b1, "Add Point", false, &t, egui::Id::unique(("reshape-add", euid))).clicked() {
if widgets::text_button(ui, b1, crate::i18n::tr("Add Point"), false, &t, egui::Id::unique(("reshape-add", euid))).clicked() {
let mut v = pairs.clone();
v.push(add_pair(&pairs));
set_str(actions, layer, g, "correspondencePoints", format_pairs(&v), None);
}
app.auto.add(&format!("effectControls.effect.{euid}.reshape.add"), b1, "Add Point");
if widgets::text_button(ui, b2, "Remove Point", false, &t, egui::Id::unique(("reshape-remove", euid))).clicked() && !pairs.is_empty() {
if widgets::text_button(ui, b2, crate::i18n::tr("Remove Point"), false, &t, egui::Id::unique(("reshape-remove", euid))).clicked() && !pairs.is_empty() {
let mut v = pairs.clone();
v.pop();
set_str(actions, layer, g, "correspondencePoints", format_pairs(&v), None);
}
app.auto.add(&format!("effectControls.effect.{euid}.reshape.remove"), b2, "Remove Point");
if widgets::text_button(ui, b3, "Clear", false, &t, egui::Id::unique(("reshape-clear", euid))).clicked() && !pairs.is_empty() {
if widgets::text_button(ui, b3, crate::i18n::tr("Clear"), false, &t, egui::Id::unique(("reshape-clear", euid))).clicked() && !pairs.is_empty() {
set_str(actions, layer, g, "correspondencePoints", String::new(), None);
}
app.auto.add(&format!("effectControls.effect.{euid}.reshape.clear"), b3, "Clear");
+84 -12
View File
@@ -3,14 +3,18 @@
//! Opacity stops sit above the preview bar and colour stops below it. Click above or below the
//! bar to add a stop (it takes that track's value there), drag a stop to move it, edit the
//! selected stop's location, opacity or colour, and Delete removes it (never the last stop of a
//! track). The editor works on a draft: OK sets the property once with `prop.set` at the time it
//! opened (one undo step, nothing at all when nothing changed); Cancel and Escape leave it as it
//! was.
//! track). The editor works on a draft. With **Preview** on (the default, as in After Effects) each
//! edit is applied to the property at once, with `prop.set` at the time the editor opened and one
//! merged undo step, so the canvas shows the gradient as it is edited; OK keeps it, Cancel and
//! Escape roll it back to the checkpoint taken when the editor opened. With Preview off, OK sets
//! the property once (one undo step, nothing at all when nothing changed). The dialog opens at
//! the lower right of the window and does not dim it, so the canvas stays in view.
//!
//! Automation ids: `dialog.gradient.bar`, `dialog.gradient.addOpacity`, `dialog.gradient.addColor`,
//! `dialog.gradient.opacityStop.<i>`, `dialog.gradient.colorStop.<i>` (in order along the bar),
//! `dialog.gradient.location`, `dialog.gradient.opacity`, `dialog.gradient.color`,
//! `dialog.gradient.delete`, `dialog.gradient.ok`, `dialog.gradient.cancel`.
//! `dialog.gradient.delete`, `dialog.gradient.preview`, `dialog.gradient.ok`,
//! `dialog.gradient.cancel`.
use effectcraft_engine::keyframe::{Gradient, Value};
use effectcraft_engine::project::{ItemId, LayerId};
@@ -18,6 +22,8 @@ use egui::{Color32, Rect, Sense, Stroke, StrokeKind, pos2, vec2};
use serde_json::{Value as Json, json};
use crate::i18n::tr;
use crate::panels::layer_styles_dialog::{Checkpoint, checkpoint};
use crate::panels::preview_merge::{self, Applied};
use crate::theme::Tokens;
use crate::{Dialog, EffectcraftApp};
@@ -58,6 +64,14 @@ pub struct Draft {
pub baseline: Gradient,
/// Stop counts (colour, opacity) of the keyframes just before and after the edit time.
pub neighbors: Vec<(usize, usize)>,
/// Preview: edits are applied to the property as they are made.
pub preview: bool,
/// The gradient the property was last given by the preview, if it has been.
pub applied: Option<Gradient>,
/// The project as the preview left it, to tell edits made since from the preview's.
applied_at: Option<Applied>,
/// The project and undo history as the editor opened, for Cancel.
checkpoint: Option<Checkpoint>,
next_id: u64,
}
@@ -220,7 +234,10 @@ pub fn open(app: &mut EffectcraftApp, p: &Json) -> Result<Json, String> {
let before = prop.keys.iter().rev().find(|k| k.time < lt).and_then(|k| counts(&k.value));
let after = prop.keys.iter().find(|k| k.time > lt).and_then(|k| counts(&k.value));
let target = Target { comp: cid.0, layer: lid, prop: uid, time: ct.seconds() };
app.dialog_state.gradient = Draft::new(&g, Some(target), before.into_iter().chain(after).collect());
let mut draft = Draft::new(&g, Some(target), before.into_iter().chain(after).collect());
draft.preview = true;
draft.checkpoint = Some(checkpoint(app));
app.dialog_state.gradient = draft;
app.dialog = Some(Dialog::GradientEditor);
Ok(json!({"dialog": "gradientEditor"}))
}
@@ -231,6 +248,11 @@ fn commit(app: &mut EffectcraftApp, ctx: &egui::Context) {
app.dialog = None;
let d = std::mem::take(&mut app.dialog_state.gradient);
let g = d.gradient();
// The preview already set the property to this draft, in one merged undo step: OK keeps it.
if d.applied.as_ref() == Some(&g) {
app.session.history.merge_key = None;
return;
}
let Some(tg) = d.target else { return };
if g == d.baseline {
return;
@@ -241,6 +263,55 @@ fn commit(app: &mut EffectcraftApp, ctx: &egui::Context) {
}
}
/// Close the editor without keeping the preview: the project and undo history go back to how they
/// were when it opened (nothing happens when the preview never changed the property).
pub fn finish(app: &mut EffectcraftApp, ok: bool, ctx: &egui::Context) {
if ok {
// An edit made in the same frame as OK is not in the last preview yet.
sync_preview(app, ctx);
commit(app, ctx);
return;
}
let d = std::mem::take(&mut app.dialog_state.gradient);
if d.applied.is_some()
&& let Some(cp) = d.checkpoint
{
preview_merge::rollback(app, cp, d.applied_at.as_ref());
}
app.session.history.merge_key = None;
app.dialog = None;
}
/// Keep the property in step with the draft while Preview is on: apply a changed draft with one
/// merged undo step, and roll back to the checkpoint when Preview goes off or the draft is back to
/// how it opened.
fn sync_preview(app: &mut EffectcraftApp, ctx: &egui::Context) {
let d = &app.dialog_state.gradient;
let Some(tg) = d.target.clone() else { return };
let g = d.gradient();
let want = d.preview && g != d.baseline;
if want && d.applied.as_ref() != Some(&g) {
let params = json!({
"comp": tg.comp, "layer": tg.layer, "prop": tg.prop, "time": tg.time, "timeBase": "comp",
"value": Value::Gradient(g.clone()).to_json(), "merge": format!("gradient-editor-{}", tg.prop),
});
match crate::menus::invoke(app, ctx, "prop.set", params) {
Ok(_) => {
app.dialog_state.gradient.applied = Some(g);
app.dialog_state.gradient.applied_at = Some(preview_merge::applied(app));
}
Err(e) => app.ui.status = e,
}
} else if !want
&& d.applied.is_some()
&& let Some(cp) = d.checkpoint.clone()
{
let at = app.dialog_state.gradient.applied_at.take();
preview_merge::rollback(app, cp, at.as_ref());
app.dialog_state.gradient.applied = None;
}
}
pub(super) fn to_color32(c: [f32; 4]) -> Color32 {
let b = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8;
Color32::from_rgba_unmultiplied(b(c[0]), b(c[1]), b(c[2]), b(c[3]))
@@ -258,7 +329,8 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
return;
}
let mut answer: Option<bool> = None;
super::dialogs::modal(ctx, tr("Gradient Editor"), vec2(520.0, 270.0), t, |ui| {
// At the lower right, undimmed: the canvas it edits stays in view (After Effects).
super::dialogs::modal_placed(ctx, tr("Gradient Editor"), vec2(520.0, 270.0), t, super::live_preview::PLACE, |ui| {
let w = ui.available_width().max(200.0);
let (area, _) = ui.allocate_exact_size(vec2(w, 72.0), Sense::hover());
let bar = Rect::from_min_max(pos2(area.min.x + 8.0, area.min.y + 22.0), pos2(area.max.x - 8.0, area.min.y + 48.0));
@@ -416,6 +488,10 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
if r.clicked() {
answer = Some(true);
}
let mut preview = app.dialog_state.gradient.preview;
let r = ui.checkbox(&mut preview, tr("Preview"));
app.auto.add("dialog.gradient.preview", r.rect, "Preview");
app.dialog_state.gradient.preview = preview;
});
});
// Delete / Backspace remove the selected stop, but not while typing in a field.
@@ -427,12 +503,8 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
app.dialog_state.gradient.remove(track, id);
}
match answer {
Some(true) => commit(app, ctx),
Some(false) => {
app.dialog = None;
app.dialog_state.gradient = Draft::default();
}
None => {}
Some(ok) => finish(app, ok, ctx),
None => sync_preview(app, ctx),
}
}
+22 -8
View File
@@ -48,9 +48,21 @@ struct KeyPt {
t: f64,
v: f64,
sel: bool,
muted: bool,
handles: Vec<Handle>,
}
/// A graph key's fill and edge colours: muted keys stay visible and selectable, dimmed.
fn key_colors(t: &Tokens, edge: Color32, k: &KeyPt) -> (Color32, Color32) {
let fill = match (k.muted, k.sel) {
(true, true) => t.keyframe_selected.gamma_multiply(0.5),
(true, false) => Color32::from_gray(0x70),
(false, true) => t.keyframe_selected,
(false, false) => Color32::from_gray(0xe0),
};
(fill, if k.muted { edge.gamma_multiply(0.4) } else { edge })
}
struct Curve<'a> {
layer: &'a Layer,
prop: &'a Property,
@@ -153,7 +165,7 @@ fn curves<'a>(app: &EffectcraftApp, comp: &'a Comp, ectx: &EvalCtx, tm: TMap, pl
let dt = e.influence * dur * if out_side { 1.0 } else { -1.0 };
let (hv, show) = if speed { (e.speed.abs(), true) } else { (k.value.components().get(d).copied().unwrap_or(0.0) + e.speed * dt, !spatial) };
let bezier = if out_side { k.out_interp == Interp::Bezier } else { k.in_interp == Interp::Bezier };
if ks && show && (bezier || speed) {
if ks && !k.muted && show && (bezier || speed) {
handles.push(Handle { out: out_side, t: kt + dt, v: hv, speed: e.speed, dur });
}
}
@@ -162,7 +174,7 @@ fn curves<'a>(app: &EffectcraftApp, comp: &'a Comp, ectx: &EvalCtx, tm: TMap, pl
} else {
k.value.components().get(d).copied().unwrap_or(0.0)
};
keys.push(KeyPt { idx: i, time: k.time, t: kt, v, sel: ks, handles });
keys.push(KeyPt { idx: i, time: k.time, t: kt, v, sel: ks, muted: k.muted, handles });
}
out.push(Curve { layer: l, prop: pr, dim: d, color, pts, keys, editable: !spatial });
}
@@ -221,7 +233,7 @@ impl ButtonBar {
let chosen = widgets::popup_list(ui, pid, pos, r, rows, 0, |ui| {
let mut chosen = None;
for (id, label, on, tip) in &self.hidden {
let item = ui.selectable_label(*on, label.as_str()).on_hover_text(tip.as_str());
let item = ui.selectable_label(*on, crate::i18n::tr(label)).on_hover_text(crate::i18n::tr(tip));
app.auto.add(&format!("timeline.graph.more.{id}"), item.rect, tip);
if item.clicked() {
chosen = Some(id.clone());
@@ -251,14 +263,16 @@ struct BarBtn<'a> {
fn text_button(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, bar: &mut ButtonBar, y: f32, btn: BarBtn) -> bool {
let BarBtn { label, on, id, tip } = btn;
let t = app.tokens;
let w = p.layout_no_wrap(label.to_string(), Tokens::ui(11.0), t.text).size().x + 14.0;
let (shown, shown_tip) = (crate::i18n::tr(label), crate::i18n::tr(tip));
let w = p.layout_no_wrap(shown.to_string(), Tokens::ui(11.0), t.text).size().x + 14.0;
// Once one button overflows, the rest follow it into the menu (they keep their order).
if !bar.hidden.is_empty() || bar.x + w > bar.limit {
// The menu translates these English labels when it draws them; automation keeps them.
bar.hidden.push((id.to_string(), label.to_string(), on, tip.to_string()));
return bar.chosen.as_deref() == Some(id);
}
let r = Rect::from_min_size(pos2(bar.x, y - 9.0), vec2(w, 18.0));
let resp = ui.interact(r, egui::Id::unique(("graph-btn", id)), Sense::click()).on_hover_text(tip);
let resp = ui.interact(r, egui::Id::unique(("graph-btn", id)), Sense::click()).on_hover_text(shown_tip);
p.rect_filled(
r,
3.0,
@@ -270,7 +284,7 @@ fn text_button(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, b
Color32::from_rgb(0x2c, 0x2c, 0x2c)
},
);
p.text(r.center(), Align2::CENTER_CENTER, label, Tokens::ui(11.0), if on { Color32::WHITE } else { t.text });
p.text(r.center(), Align2::CENTER_CENTER, shown, Tokens::ui(11.0), if on { Color32::WHITE } else { t.text });
app.auto.add(&format!("timeline.graph.{id}"), r, tip);
bar.x += w + 4.0;
resp.clicked()
@@ -522,9 +536,9 @@ pub(crate) fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painte
}
}
// The key itself.
let col = if k.sel { t.keyframe_selected } else { Color32::from_gray(0xe0) };
let (col, edge) = key_colors(&t, c.color, k);
pc.rect_filled(Rect::from_center_size(kp, vec2(7.0, 7.0)), 0.0, col);
pc.rect_stroke(Rect::from_center_size(kp, vec2(7.0, 7.0)), 0.0, Stroke::new(1.0, c.color), StrokeKind::Outside);
pc.rect_stroke(Rect::from_center_size(kp, vec2(7.0, 7.0)), 0.0, Stroke::new(1.0, edge), StrokeKind::Outside);
let kr = Rect::from_center_size(kp, vec2(11.0, 11.0));
let kresp = ui.interact(kr, egui::Id::unique(("gkey", uid, d, k.idx)), Sense::click_and_drag());
app.auto.add(&format!("timeline.graph.key.{uid}.{d}.{}", k.idx), kr, &c.prop.name);
@@ -24,10 +24,11 @@ pub struct LayerStyleState {
checkpoint: Option<Checkpoint>,
}
/// The project and undo history as a dialog found them, for a Cancel that rolls its live edits back.
#[derive(Clone)]
struct Checkpoint {
project: std::sync::Arc<effectcraft_engine::project::Project>,
history: effectcraft_engine::history::History,
pub(crate) struct Checkpoint {
pub(crate) project: std::sync::Arc<effectcraft_engine::project::Project>,
pub(crate) history: effectcraft_engine::history::History,
}
impl std::fmt::Debug for Checkpoint {
@@ -36,7 +37,7 @@ impl std::fmt::Debug for Checkpoint {
}
}
fn checkpoint(app: &mut EffectcraftApp) -> Checkpoint {
pub(crate) fn checkpoint(app: &mut EffectcraftApp) -> Checkpoint {
app.session.history.merge_key = None;
Checkpoint { project: app.session.project.clone(), history: app.session.history.clone() }
}
@@ -258,25 +259,32 @@ pub fn finish(app: &mut EffectcraftApp, ok: bool) {
if let Some(checkpoint) = app.dialog_state.layer_style.checkpoint.take()
&& !ok
{
let changed = !std::sync::Arc::ptr_eq(&app.session.project, &checkpoint.project);
if changed {
// Snapshot only when Cancel has something to restore: opening and cancelling without
// edits must not copy the whole TextDoc.
let edited = active_editor_snapshot(&app.session);
app.session.project = checkpoint.project;
app.session.history = checkpoint.history;
app.session.sanitize_after_replacement(edited.as_ref());
app.session.bump();
} else {
// No edit was committed, so nothing can have become invalid. In particular, do not
// call sanitize_state here: its text-edit cleanup clones the whole TextDoc.
app.session.history = checkpoint.history;
}
restore(app, checkpoint);
}
app.session.history.merge_key = None;
app.dialog = None;
}
/// Put the project and undo history back as `checkpoint` has them (nothing happens when nothing
/// changed since).
pub(crate) fn restore(app: &mut EffectcraftApp, checkpoint: Checkpoint) {
let changed = !std::sync::Arc::ptr_eq(&app.session.project, &checkpoint.project);
if changed {
// Snapshot only when Cancel has something to restore: opening and cancelling without
// edits must not copy the whole TextDoc.
let edited = active_editor_snapshot(&app.session);
app.session.project = checkpoint.project;
app.session.history = checkpoint.history;
app.session.sanitize_after_replacement(edited.as_ref());
app.session.bump();
} else {
// No edit was committed, so nothing can have become invalid. In particular, do not
// call sanitize_state here: its text-edit cleanup clones the whole TextDoc.
app.session.history = checkpoint.history;
}
app.session.history.merge_key = None;
}
#[cfg(test)]
mod tests {
use super::*;
+2 -2
View File
@@ -18,7 +18,7 @@ pub fn home_tab(app: &mut EffectcraftApp, ui: &mut egui::Ui, area: Rect) {
p.text(
pos2(x, y + 26.0),
Align2::LEFT_CENTER,
"Hands-on tutorials that run inside EffectCraft. Do each step yourself, or press Show me.",
crate::i18n::tr("Hands-on tutorials that run inside EffectCraft. Do each step yourself, or press Show me."),
Tokens::ui(11.5),
t.text_faint,
);
@@ -38,7 +38,7 @@ pub fn home_tab(app: &mut EffectcraftApp, ui: &mut egui::Ui, area: Rect) {
p.circle_filled(badge.center(), 22.0, t.accent.gamma_multiply(0.25));
p.circle_stroke(badge.center(), 22.0, Stroke::new(1.5, t.accent));
p.text(badge.center() - vec2(0.0, 5.0), Align2::CENTER_CENTER, tut.steps.len().to_string(), Tokens::semibold(15.0), Color32::WHITE);
p.text(badge.center() + vec2(0.0, 10.0), Align2::CENTER_CENTER, "steps", Tokens::ui(9.0), t.text_dim);
p.text(badge.center() + vec2(0.0, 10.0), Align2::CENTER_CENTER, crate::i18n::tr("steps"), Tokens::ui(9.0), t.text_dim);
let tx = badge.max.x + 16.0;
p.text(pos2(tx, r.min.y + 22.0), Align2::LEFT_CENTER, &tut.title, Tokens::semibold(14.0), Color32::WHITE);
p.text(pos2(tx, r.min.y + 40.0), Align2::LEFT_CENTER, format!("{} min", tut.minutes), Tokens::ui(11.0), t.text_faint);
+129
View File
@@ -0,0 +1,129 @@
//! Preview for the settings dialogs that edit something that already exists (Solid Settings on a
//! solid layer, Camera Settings, Light Settings, Composition Settings): After Effects' Preview
//! checkbox. Each change of the draft is applied to the project at once, so the canvas shows it
//! while it is made; OK keeps it and Cancel and Escape take it back.
//!
//! Every step goes back to the checkpoint taken when the dialog opened (the project and its undo
//! history) and then runs the dialog's command once with the draft's parameters. So the project is
//! always "as it opened, plus one command": OK leaves exactly one undo step, whatever was tried
//! meanwhile, and Cancel only has to put the checkpoint back. With Preview off nothing is applied
//! until OK, and switching it off takes back what was applied.
//!
//! While a preview is active the dialog opens at the lower right of the window, without the dark
//! backdrop, and the window behind it is not greyed out, so the canvas stays in view.
use egui::vec2;
use serde_json::Value;
use crate::panels::dialogs::Placement;
use crate::panels::layer_styles_dialog::{Checkpoint, checkpoint};
use crate::panels::preview_merge::{self, Applied};
use crate::{Dialog, EffectcraftApp};
/// Where a previewing dialog opens: the lower right, undimmed.
pub const PLACE: Placement = Placement { start: vec2(0.75, 0.8), dim: false };
/// The state of the one dialog being previewed (only one dialog is open at a time).
#[derive(Clone, Debug, Default)]
pub struct LivePreview {
/// The dialog this belongs to; a preview left over from another one is ignored.
dialog: Option<Dialog>,
/// The Preview checkbox.
pub on: bool,
checkpoint: Option<Checkpoint>,
/// The draft's parameters when the dialog opened: nothing is applied while it equals them.
baseline: Option<Value>,
/// The command and parameters the preview last applied.
applied: Option<(&'static str, Value)>,
/// The project as that command left it, to tell edits made since from the preview's.
applied_at: Option<Applied>,
}
impl LivePreview {
fn active_for(&self, dialog: Option<Dialog>) -> bool {
self.checkpoint.is_some() && self.dialog.is_some() && self.dialog == dialog
}
}
/// Start previewing for `dialog`, which has just opened on an existing item (Preview on).
pub fn begin(app: &mut EffectcraftApp, dialog: Dialog) {
app.dialog_state.live = LivePreview { dialog: Some(dialog), on: true, checkpoint: Some(checkpoint(app)), ..Default::default() };
}
/// The dialog that is open is previewing.
pub fn active(app: &EffectcraftApp) -> bool {
app.dialog_state.live.active_for(app.dialog)
}
/// Where the open dialog goes: the lower right, undimmed, while it previews; else the middle.
pub fn place(app: &EffectcraftApp) -> Placement {
if active(app) { PLACE } else { Placement::default() }
}
/// Keep the project in step with the draft: `command` with `params` is what OK would run. The
/// first call (the draft as it opened) only notes it.
pub fn sync(app: &mut EffectcraftApp, command: &'static str, params: Value) {
if !active(app) {
return;
}
let live = &app.dialog_state.live;
if live.baseline.is_none() {
app.dialog_state.live.baseline = Some(params);
return;
}
let want = live.on && live.baseline.as_ref() != Some(&params);
let is_applied = live.applied.is_some();
if want {
if live.applied.as_ref().is_some_and(|(c, p)| *c == command && *p == params) {
return;
}
if is_applied {
put_back(app);
}
match app.session.execute(command, params.clone()) {
Ok(_) => {
app.dialog_state.live.applied = Some((command, params));
app.dialog_state.live.applied_at = Some(preview_merge::applied(app));
}
Err(e) => app.ui.status = e.to_string(),
}
} else if is_applied {
put_back(app);
}
}
/// The project as the dialog found it.
fn put_back(app: &mut EffectcraftApp) {
if let Some(cp) = app.dialog_state.live.checkpoint.clone() {
let at = app.dialog_state.live.applied_at.take();
// Only what the preview did is taken back: an edit made while the dialog was open stays.
preview_merge::rollback(app, cp, at.as_ref());
}
app.dialog_state.live.applied = None;
app.dialog_state.live.applied_at = None;
}
/// OK: the preview's result is the edit. The preview is first brought up to `command` with
/// `params`, the draft as OK found it (an edit made in the same frame as OK is not in the last
/// preview). Returns whether the project then already has the edit (the caller must not run the
/// command again); otherwise the caller runs it, as without a preview.
pub fn keep(app: &mut EffectcraftApp, command: &'static str, params: Value) -> bool {
sync(app, command, params);
let kept = active(app) && app.dialog_state.live.applied.is_some();
app.dialog_state.live = LivePreview::default();
kept
}
/// Cancel and Escape: take back whatever the preview applied.
pub fn cancel(app: &mut EffectcraftApp) {
if active(app) {
put_back(app);
}
app.dialog_state.live = LivePreview::default();
}
/// The Preview checkbox, when the open dialog previews; `id` is its automation id.
pub fn checkbox(ui: &mut egui::Ui, auto: &mut crate::automation::Registry, on: &mut bool, id: &str) {
let r = ui.checkbox(on, crate::i18n::tr("Preview"));
auto.add(id, r.rect, "Preview");
}
+6 -2
View File
@@ -11,7 +11,8 @@ use crate::{EffectcraftApp, widgets};
pub fn placeholder(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect, p: PanelKind) {
let t = app.tokens;
ui.painter().text(rect.center(), Align2::CENTER_CENTER, tr_args("{} — coming soon", &[&(p.title())]), Tokens::ui(12.0), t.text_faint);
let name = crate::i18n::label(app, "window.panel", p.title());
ui.painter().text(rect.center(), Align2::CENTER_CENTER, tr_args("{} — coming soon", &[&name]), Tokens::ui(12.0), t.text_faint);
}
/// Preview panel: transport controls, the shortcut whose options are shown, and that
@@ -42,7 +43,10 @@ pub fn preview(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
for (icon, cmd, tip) in buttons {
let r = Rect::from_min_size(pos2(x, y), vec2(bw, 28.0));
let icon = if cmd == "playback.toggle" && app.playback.playing { Icon::Pause } else { icon };
if widgets::icon_button(ui, r, icon, cmd == "playback.toggle" && app.playback.playing, &t, egui::Id::unique(("pv", cmd))).on_hover_text(tip).clicked() {
if widgets::icon_button(ui, r, icon, cmd == "playback.toggle" && app.playback.playing, &t, egui::Id::unique(("pv", cmd)))
.on_hover_text(tr(tip))
.clicked()
{
// The play button plays with the options of the shortcut shown below.
let params = if cmd == "playback.toggle" { json!({"shortcut": current.id()}) } else { json!({}) };
let _ = crate::menus::invoke(app, &ctx, cmd, params);
+204
View File
@@ -0,0 +1,204 @@
//! Taking a dialog's Preview back without taking other edits with it.
//!
//! A previewing dialog (Camera, Light, Solid, Composition Settings, the Gradient Editor) puts the
//! project and undo history back to a checkpoint when it is cancelled or Preview goes off. If the
//! project changed in the meantime for another reason (a camera analysis finishing, a script, the
//! control channel), swapping the checkpoint in would throw that edit away. This module undoes
//! only what the preview did: a three-way merge of the checkpoint (before the preview), the project
//! right after the preview (`Applied`) and the project now. Whatever the preview did not touch
//! stays as it is now; what only the preview touched goes back to the checkpoint; what both touched
//! keeps the other edit.
use std::collections::BTreeSet;
use std::sync::Arc;
use effectcraft_engine::commands::text_edit::active_editor_snapshot;
use effectcraft_engine::project::{Comp, Item, ItemId, ItemKind, Layer, LayerId, Project};
use crate::EffectcraftApp;
use crate::panels::layer_styles_dialog::{Checkpoint, restore};
/// The project right after a preview was applied, and the undo depth then.
#[derive(Clone)]
pub(crate) struct Applied {
project: Arc<Project>,
undo_len: usize,
}
impl std::fmt::Debug for Applied {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Applied").field("undo_len", &self.undo_len).finish_non_exhaustive()
}
}
/// Note the project as the preview that has just been applied left it.
pub(crate) fn applied(app: &EffectcraftApp) -> Applied {
Applied { project: app.session.project.clone(), undo_len: app.session.history.undo.len() }
}
/// Put the project back as `checkpoint` has it, minus nothing but the preview: when the project is
/// still what the preview made (or nothing says what that was) the checkpoint is simply restored.
pub(crate) fn rollback(app: &mut EffectcraftApp, checkpoint: Checkpoint, applied: Option<&Applied>) {
let Some(applied) = applied.filter(|a| !Arc::ptr_eq(&a.project, &app.session.project)) else {
restore(app, checkpoint);
return;
};
let merged = merge(&checkpoint.project, &applied.project, &app.session.project);
// One step for what was done meanwhile, from the project before the dialog: Undo takes it back.
let label = app.session.history.undo.get(applied.undo_len).map(|(label, _)| label.clone()).unwrap_or_else(|| "Edit".to_string());
let mut history = checkpoint.history;
history.undo.push((label, checkpoint.project));
history.redo.clear();
history.merge_key = None;
let edited = active_editor_snapshot(&app.session);
app.session.project = Arc::new(merged);
app.session.history = history;
app.session.sanitize_after_replacement(edited.as_ref());
app.session.bump();
}
/// `now` without the changes between `before` and `after`, keeping any other change.
fn merge(before: &Project, after: &Project, now: &Project) -> Project {
let mut merged = now.clone();
let ids: BTreeSet<ItemId> = before.items.keys().chain(after.items.keys()).chain(now.items.keys()).copied().collect();
for id in ids {
match item(before.items.get(&id), after.items.get(&id), now.items.get(&id)) {
Some(it) => {
merged.items.insert(id, it);
}
None => {
merged.items.remove(&id);
}
}
}
merged
}
/// The three-way choice for one value: untouched by the preview keeps `now`, touched only by the
/// preview goes back to `before`, touched by both is `both`.
fn pick<T: PartialEq + Clone>(before: Option<&T>, after: Option<&T>, now: Option<&T>, both: impl FnOnce() -> Option<T>) -> Option<T> {
if after == before {
now.cloned()
} else if now == after {
before.cloned()
} else {
both()
}
}
fn item(before: Option<&Item>, after: Option<&Item>, now: Option<&Item>) -> Option<Item> {
pick(before, after, now, || {
if let (Some(b), Some(a), Some(n)) = (before, after, now)
&& let (ItemKind::Comp(bc), ItemKind::Comp(ac), ItemKind::Comp(nc)) = (&b.kind, &a.kind, &n.kind)
{
let mut merged = n.clone();
// Name, folder and the like: only a preview that changed the item itself and nothing else
// is taken back; the comp's own contents are merged below.
merged.kind = ItemKind::Comp(Arc::new(comp(bc, ac, nc)));
return Some(merged);
}
now.cloned()
})
}
fn bare(c: &Comp) -> Comp {
let mut b = c.clone();
b.layers.clear();
b
}
fn layer_of(c: &Comp, id: LayerId) -> Option<&Layer> {
c.layers.iter().find(|l| l.id == id)
}
fn comp(before: &Comp, after: &Comp, now: &Comp) -> Comp {
// The comp's own settings, apart from its layers.
let mut merged = pick(Some(&bare(before)), Some(&bare(after)), Some(&bare(now)), || Some(bare(now))).unwrap_or_else(|| bare(now));
let merge_layer = |id: LayerId| pick(layer_of(before, id), layer_of(after, id), layer_of(now, id), || layer_of(now, id).cloned());
// Layers as they stand now, in their order, then the ones the preview removed put back.
let mut layers: Vec<Layer> = now.layers.iter().filter_map(|l| merge_layer(l.id)).collect();
for (at, l) in before.layers.iter().enumerate() {
if layer_of(now, l.id).is_none()
&& let Some(back) = merge_layer(l.id)
{
layers.insert(at.min(layers.len()), back);
}
}
merged.layers = layers.into();
merged
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Dialog;
use crate::panels::live_preview;
use effectcraft_engine::Session;
use serde_json::json;
fn app_with_light() -> (EffectcraftApp, u64) {
let mut s = Session::default();
s.execute("comp.new", json!({"name": "Scene", "width": 320, "height": 180, "duration": 1})).unwrap();
let light = s.execute("layer.newLight", json!({"kind": "Point"})).unwrap()["layer"].as_u64().unwrap();
(EffectcraftApp::new(s), light)
}
fn name_of(app: &EffectcraftApp, layer: u64) -> String {
app.session.active_comp().unwrap().layer(LayerId(layer)).unwrap().name.clone()
}
fn layer_count(app: &EffectcraftApp) -> usize {
app.session.active_comp().unwrap().layers.len()
}
/// The preview renames the light; a null is added meanwhile (what a finished background job
/// would do). Cancel takes the rename back and keeps the null, with one undo step for it.
#[test]
fn cancelling_a_preview_keeps_an_edit_made_while_it_was_open() {
let (mut app, light) = app_with_light();
let original = name_of(&app, light);
app.dialog = Some(Dialog::LightSettings);
live_preview::begin(&mut app, Dialog::LightSettings);
let rename = |name: &str| json!({"layer": light, "name": name});
live_preview::sync(&mut app, "layer.rename", rename(&original));
live_preview::sync(&mut app, "layer.rename", rename("Previewed"));
assert_eq!(name_of(&app, light), "Previewed");
let layers = layer_count(&app);
app.session.execute("layer.newNull", json!({})).unwrap();
live_preview::cancel(&mut app);
assert_eq!(name_of(&app, light), original, "the preview is taken back");
assert_eq!(layer_count(&app), layers + 1, "the other edit stays");
assert!(app.session.undo());
assert_eq!(layer_count(&app), layers, "and has its own undo step");
assert_eq!(name_of(&app, light), original);
}
/// With nothing else done meanwhile the checkpoint is restored as before.
#[test]
fn cancelling_an_undisturbed_preview_restores_the_checkpoint() {
let (mut app, light) = app_with_light();
let original = name_of(&app, light);
let steps = app.session.history.undo.len();
app.dialog = Some(Dialog::LightSettings);
live_preview::begin(&mut app, Dialog::LightSettings);
live_preview::sync(&mut app, "layer.rename", json!({"layer": light, "name": original}));
live_preview::sync(&mut app, "layer.rename", json!({"layer": light, "name": "Previewed"}));
live_preview::cancel(&mut app);
assert_eq!(name_of(&app, light), original);
assert_eq!(app.session.history.undo.len(), steps);
}
/// An item both the preview and another edit changed keeps the other edit.
#[test]
fn a_layer_both_edits_changed_keeps_the_other_edit() {
let (mut app, light) = app_with_light();
let original = name_of(&app, light);
app.dialog = Some(Dialog::LightSettings);
live_preview::begin(&mut app, Dialog::LightSettings);
live_preview::sync(&mut app, "layer.rename", json!({"layer": light, "name": original}));
live_preview::sync(&mut app, "layer.rename", json!({"layer": light, "name": "Previewed"}));
app.session.execute("layer.rename", json!({"layer": light, "name": "Renamed elsewhere"})).unwrap();
live_preview::cancel(&mut app);
assert_eq!(name_of(&app, light), "Renamed elsewhere");
}
}
+1 -1
View File
@@ -812,7 +812,7 @@ fn footer(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, foot:
(Icon::NewComp, "newComp", "Create a new Composition"),
] {
let r = Rect::from_min_size(pos2(x, foot.min.y + 3.0), vec2(22.0, 22.0));
let resp = widgets::icon_button(ui, r, icon, false, &t, egui::Id::unique(("pfoot", id))).on_hover_text(tip);
let resp = widgets::icon_button(ui, r, icon, false, &t, egui::Id::unique(("pfoot", id))).on_hover_text(crate::i18n::tr(tip));
app.auto.add(&format!("project.{id}"), r, tip);
resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, crate::i18n::tr(tip)));
if resp.clicked() {
+46 -4
View File
@@ -451,6 +451,22 @@ fn queue_drop(app: &EffectcraftApp, ui: &egui::Ui, rect: Rect, p: &egui::Painter
}
}
/// Elapsed and estimated remaining seconds at UI time `now`. The job reports them as frames
/// finish; between reports they count on from the last one, as a clock does, so a slow frame
/// doesn't read as a stalled render (#2593).
fn live_times(ctx: &egui::Context, pr: &effectcraft_engine::render_queue::JobState, now: f64) -> (f64, Option<f64>) {
let report = (pr.current, pr.done, pr.item_elapsed.to_bits());
let since = ctx.data_mut(|d| {
let seen = d.get_temp_mut_or_insert_with(egui::Id::unique("rq-progress-seen"), || (report, now));
if seen.0 != report {
*seen = (report, now);
}
seen.1
});
let dt = if pr.finished { 0.0 } else { (now - since).max(0.0) };
(pr.item_elapsed + dt, pr.remaining.map(|r| (r - dt).max(0.0)))
}
/// The Current Render strip inside `top`: title, elapsed / remaining time, Render, Stop, Notify
/// and the progress bar.
fn current_render_strip(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, top: Rect, rq: &RqView, actions: &mut Vec<Action>) {
@@ -461,11 +477,15 @@ fn current_render_strip(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::P
let cur_item = progress.as_ref().and_then(|p| p.current).and_then(|id| queue.iter().find(|i| i.id == id));
let cur_name = cur_item.and_then(|i| app.session.project.item(i.comp)).map(|i| i.name.clone());
if let Some(pr) = &progress {
let rem = pr.remaining.map(hms).unwrap_or_else(|| "–".into());
let (elapsed, remaining) = live_times(ui.ctx(), pr, ui.input(|i| i.time));
if rendering {
ui.ctx().request_repaint_after(std::time::Duration::from_millis(500));
}
let rem = remaining.map(hms).unwrap_or_else(|| "–".into());
p.text(
pos2(top.max.x - 12.0, top.min.y + 16.0),
Align2::RIGHT_CENTER,
format!("Elapsed: {} Est. Remain: {rem}", hms(pr.item_elapsed)),
format!("Elapsed: {} Est. Remain: {rem}", hms(elapsed)),
Tokens::ui(11.5),
t.text_dim,
);
@@ -474,7 +494,7 @@ fn current_render_strip(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::P
let render_b = Rect::from_min_size(pos2(top.max.x - 12.0 - bw, top.min.y + 34.0), vec2(bw, 26.0));
let stop_b = Rect::from_min_size(pos2(render_b.min.x - 8.0 - bw, render_b.min.y), vec2(bw, 26.0));
let can_render = !rendering && queue.iter().any(RenderQueueItem::is_queued) && !available.is_empty();
if widgets::text_button(ui, render_b, "Render", can_render, &t, egui::Id::unique("rq-render")).clicked() && can_render {
if widgets::text_button(ui, render_b, crate::i18n::tr("Render"), can_render, &t, egui::Id::unique("rq-render")).clicked() && can_render {
actions.push(("renderQueue.render", json!({"wait": false})));
}
app.auto.add("renderQueue.render", render_b, "Render");
@@ -487,7 +507,7 @@ fn current_render_strip(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::P
p.text(pos2(nb.max.x + 5.0, nb.center().y), Align2::LEFT_CENTER, tr("Notify"), Tokens::ui(11.5), t.text_dim);
app.auto.add("renderQueue.notify", nb, if notify { "Notify: on" } else { "Notify: off" });
if rendering {
if widgets::text_button(ui, stop_b, "Stop", false, &t, egui::Id::unique("rq-stop")).clicked() {
if widgets::text_button(ui, stop_b, crate::i18n::tr("Stop"), false, &t, egui::Id::unique("rq-stop")).clicked() {
actions.push(("renderQueue.stop", json!({})));
}
app.auto.add("renderQueue.stop", stop_b, "Stop");
@@ -903,4 +923,26 @@ mod tests {
assert_eq!(super::utc_stamp(u64::MAX), "584554051223-11-09 07:00:15");
assert_eq!(super::hms(3725.4), "1:02:05");
}
/// #2593: between progress reports the times count on from the last report, and a new
/// report starts the count again.
#[test]
fn render_times_tick_between_reports() {
let ctx = egui::Context::default();
let mut pr = effectcraft_engine::render_queue::JobState {
current: Some(1),
done: 2,
total: 10,
item_elapsed: 20.0,
remaining: Some(80.0),
..Default::default()
};
assert_eq!(super::live_times(&ctx, &pr, 100.0), (20.0, Some(80.0)));
assert_eq!(super::live_times(&ctx, &pr, 103.0), (23.0, Some(77.0)));
assert_eq!(super::live_times(&ctx, &pr, 300.0).1, Some(0.0), "never below zero");
(pr.done, pr.item_elapsed, pr.remaining) = (3, 31.0, Some(72.0));
assert_eq!(super::live_times(&ctx, &pr, 301.0), (31.0, Some(72.0)));
pr.finished = true;
assert_eq!(super::live_times(&ctx, &pr, 400.0), (31.0, Some(72.0)), "a finished job stands still");
}
}
+1 -1
View File
@@ -102,7 +102,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
p.text(
out_rect.center(),
Align2::CENTER_CENTER,
"JavaScript with the After Effects scripting object model — try app.project.numItems",
crate::i18n::tr("JavaScript with the After Effects scripting object model — try app.project.numItems"),
Tokens::ui(12.0),
t.text_faint,
);
+20 -8
View File
@@ -395,7 +395,11 @@ fn mouse_events(ui: &egui::Ui, origin: egui::Pos2, area: Rect, win: u32, root: &
return;
}
let mut st: MouseTrack = ui.data(|d| d.get_temp(id)).unwrap_or_default();
let (latest, events, mods, time) = ui.input(|i| (i.pointer.latest_pos(), i.events.clone(), i.modifiers, i.time));
let (latest, events, mods, time, held) = ui.input(|i| {
let held = |b: egui::PointerButton, bit: u8| if i.pointer.button_down(b) { bit } else { 0 };
let held = held(egui::PointerButton::Primary, 1) | held(egui::PointerButton::Secondary, 2) | held(egui::PointerButton::Middle, 4);
(i.pointer.latest_pos(), i.events.clone(), i.modifiers, i.time, held)
});
let ctx = ui.ctx();
let layer = ui.layer_id();
let target = |p: egui::Pos2| -> Option<&Widget> {
@@ -414,7 +418,7 @@ fn mouse_events(ui: &egui::Ui, origin: egui::Pos2, area: Rect, win: u32, root: &
w.id,
json!({
"type": ty, "x": p.x - at.x, "y": p.y - at.y, "screenX": p.x, "screenY": p.y,
"button": button, "detail": detail,
"button": button, "buttons": held, "detail": detail,
"altKey": mods.alt, "ctrlKey": mods.ctrl, "shiftKey": mods.shift, "metaKey": mods.mac_cmd,
}),
));
@@ -431,10 +435,15 @@ fn mouse_events(ui: &egui::Ui, origin: egui::Pos2, area: Rect, win: u32, root: &
}
st.hover = now_id;
}
if let Some((w, p)) = now
&& st.at != Some(p)
{
send(w, "mousemove", p, 0, 0, acts);
// While a button is held, the control it was pressed on keeps getting the moves, inside or
// outside the window, as in After Effects (#2584).
let captured = st.down.and_then(|(c, _)| root.find(c));
if let Some(p) = latest.filter(|p| st.at != Some(*p)) {
match (captured, now) {
(Some(c), _) => send(c, "mousemove", p, 0, 0, acts),
(None, Some((w, _))) => send(w, "mousemove", p, 0, 0, acts),
(None, None) => {}
}
}
st.at = latest;
// Presses and releases.
@@ -446,7 +455,9 @@ fn mouse_events(ui: &egui::Ui, origin: egui::Pos2, area: Rect, win: u32, root: &
egui::PointerButton::Secondary => 2,
_ => continue,
};
let Some(w) = target(*pos) else {
// A release goes to the control the button was pressed on, wherever the pointer is.
let pressed_on = st.down.filter(|(_, db)| !*pressed && *db == b).and_then(|(c, _)| root.find(c));
let Some(w) = pressed_on.or_else(|| target(*pos)) else {
if !pressed {
st.down = None;
}
@@ -460,7 +471,8 @@ fn mouse_events(ui: &egui::Ui, origin: egui::Pos2, area: Rect, win: u32, root: &
send(w, "mousedown", *pos, b, st.clicks, acts);
} else {
send(w, "mouseup", *pos, b, st.clicks, acts);
if st.down == Some((w.id, b)) && b == 0 && w.enabled && w.mouse.iter().any(|m| m == "click") {
let over = target(*pos).is_some_and(|t| t.id == w.id);
if over && st.down == Some((w.id, b)) && b == 0 && w.enabled && w.mouse.iter().any(|m| m == "click") {
// The click with the pointer's details, after mousedown / mouseup, in place of
// the plain click the control reported.
acts.retain(|a| !matches!(a, Act::Click(aw, ac) if *aw == win && *ac == w.id));
@@ -49,6 +49,8 @@ const PANEL_COLOR: Color32 = Color32::from_rgb(0x3f, 0xa8, 0x6c);
pub fn open(app: &mut EffectcraftApp) {
app.dialog_state.shortcuts = EditorState::default();
// Scripts copied into the Scripts folder since the table was built are listed too.
app.session.scripts_changed();
app.dialog = Some(Dialog::Shortcuts);
}
+2 -2
View File
@@ -62,14 +62,14 @@ pub fn home_tab(app: &mut EffectcraftApp, ui: &mut egui::Ui, area: Rect) {
p.text(
pos2(x, y + 26.0),
Align2::LEFT_CENTER,
"Start from a ready-made project. Opening a template makes an untitled copy.",
crate::i18n::tr("Start from a ready-made project. Opening a template makes an untitled copy."),
Tokens::ui(11.5),
t.text_faint,
);
// Save the open project as a user template.
let can_save = app.session.project.comps().next().is_some();
let sr = Rect::from_min_size(pos2(area.max.x - 210.0, y - 4.0), vec2(210.0, 28.0));
let save = widgets::text_button(ui, sr, "Save Current as Template…", false, &t, egui::Id::unique("home-templates-save")).clicked();
let save = widgets::text_button(ui, sr, crate::i18n::tr("Save Current as Template…"), false, &t, egui::Id::unique("home-templates-save")).clicked();
app.auto.add("home.templates.saveCurrent", sr, "Save Current as Template…");
if save {
if can_save {
+20
View File
@@ -654,6 +654,17 @@ struct FontSearch {
}
/// Shared font menu for Character and Properties: search, recent fonts and previews.
/// Finish a font rescan started from the font menu and report how many faces it added.
/// While the scan is still walking font folders, ask for another frame so the menu updates.
pub(crate) fn poll_font_rescan(app: &mut EffectcraftApp, ctx: &egui::Context) {
if effectcraft_engine::text::fonts::rescan_busy() {
ctx.request_repaint_after(std::time::Duration::from_millis(100));
}
if let Some(n) = effectcraft_engine::text::fonts::poll_rescan() {
app.ui.status = format!("{} +{n}", tr("Rescan Fonts"));
}
}
pub(super) fn font_picker(app: &mut EffectcraftApp, ui: &mut egui::Ui, id: egui::Id, rect: Rect, current: &str, auto: &str, enabled: bool) -> Option<String> {
let state_id = id.with("search-state");
app.auto.add(auto, rect, "Font family");
@@ -724,6 +735,15 @@ pub(super) fn font_picker(app: &mut EffectcraftApp, ui: &mut egui::Ui, id: egui:
let boxed = widgets::checkbox(ui, box_rect, only_variable, &t, id.with("variable-check"));
ui.painter().text(label_rect.left_center(), Align2::LEFT_CENTER, tr("Variable fonts only"), Tokens::ui(11.5), t.text);
app_ref.auto.add(&format!("{auto}.variable"), row.rect, "Variable fonts only");
// #1089: fonts installed after launch appear without restarting. The button is its own
// row: the popup is only 180 px wide, so a control pinned to the filter row covers
// "Variable fonts only". The scan itself runs off the UI thread (`poll_font_rescan`).
let rescan_h = 22.0;
let (resc, _) = ui.allocate_exact_size(vec2(ui.available_width(), rescan_h), egui::Sense::hover());
if widgets::text_button(ui, resc, tr("Rescan Fonts"), false, &t, id.with("rescan")).clicked() && effectcraft_engine::text::fonts::start_rescan() {
app_ref.ui.status = format!("{}…", tr("Rescan Fonts"));
}
app_ref.auto.add(&format!("{auto}.rescan"), resc, "Rescan Fonts");
let toggled = row.clicked() || boxed.clicked();
if toggled {
only_variable = !only_variable;
+10 -6
View File
@@ -1528,8 +1528,8 @@ fn shift_range(ctx: &egui::Context, cid: ItemId, comp: &Comp, rows: &[Row], sele
let anchor = ctx.data(|d| d.get_temp::<LayerId>(anchor_id(cid))).or_else(|| selected.first().copied())?;
let a = comp.layers.iter().position(|l| l.id == anchor)?;
let b = comp.layers.iter().position(|l| l.id == layer)?;
let span = comp.layers.get(a.min(b)..=a.max(b))?;
Some(span.iter().filter(|l| !l.switches.locked && rows.iter().any(|r| r.layer == l.id)).map(|l| l.id.0).collect())
let span = comp.layers.iter().skip(a.min(b)).take(a.max(b) - a.min(b) + 1);
Some(span.filter(|l| !l.switches.locked && rows.iter().any(|r| r.layer == l.id)).map(|l| l.id.0).collect())
}
/// Set on the frame the reorder drag is released.
@@ -1772,7 +1772,7 @@ fn layout(app: &mut EffectcraftApp, ui: &egui::Ui, rect: Rect, cid: ItemId, comp
fn empty_timeline(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let t = app.tokens;
let ctx = ui.ctx().clone();
ui.painter().text(rect.center(), Align2::CENTER_CENTER, "(no composition)", Tokens::ui(12.0), t.text_faint);
ui.painter().text(rect.center(), Align2::CENTER_CENTER, crate::i18n::tr("(no composition)"), Tokens::ui(12.0), t.text_faint);
// Dropping footage into an empty Timeline should create its first composition,
// just like dropping onto the Project panel's New Composition button (#649).
if let Some(crate::panels::DragPayload::Item(dragged)) = egui::DragAndDrop::payload::<crate::panels::DragPayload>(&ctx).as_deref()
@@ -2213,7 +2213,7 @@ fn header_search_and_switches(app: &mut EffectcraftApp, ui: &mut egui::Ui, g: &T
let comp = g.comp.clone();
let sr = Rect::from_min_size(pos2(tc_rect.max.x + 14.0, top + 12.0), vec2((left_w - 420.0).clamp(120.0, 220.0), 22.0));
let mut search = app.ui.timeline.search.clone();
widgets::search_field(ui, sr, &mut search, "Search", &t);
widgets::search_field(ui, sr, &mut search, crate::i18n::tr("Search"), &t);
app.ui.timeline.search = search;
app.auto.add("timeline.search", sr, "Search");
let mut bx = rect.min.x + left_w - 10.0;
@@ -3622,7 +3622,7 @@ fn waveform_row(app: &mut EffectcraftApp, g: &Rw, rc: &RowCtx<'_>) {
let RowKind::Waveform { item, .. } = &rc.row.kind else { return };
lp.rect_filled(left, 0.0, if ri.is_multiple_of(2) { t.row } else { t.row_alt });
let indent = cw.name + 6.0 + 14.0 * row.depth as f32;
lp.text(pos2(indent + 10.0, cy), Align2::LEFT_CENTER, "Waveform", Tokens::ui(12.0), t.text);
lp.text(pos2(indent + 10.0, cy), Align2::LEFT_CENTER, crate::i18n::tr("Waveform"), Tokens::ui(12.0), t.text);
let wr = Rect::from_min_max(pos2(graph_x0, r.min.y), r.max);
gp.rect_filled(wr, 0.0, t.tl_bg);
app.auto.add(&format!("timeline.layer.{}.waveform", layer.id.0), wr, "Waveform");
@@ -3632,7 +3632,7 @@ fn waveform_row(app: &mut EffectcraftApp, g: &Rw, rc: &RowCtx<'_>) {
gp.text(
pos2(tm.x(layer.in_point.seconds()).max(wr.min.x) + 6.0, wr.center().y),
Align2::LEFT_CENTER,
"Building waveform…",
crate::i18n::tr("Building waveform…"),
Tokens::ui(11.0),
t.text_faint,
);
@@ -4111,6 +4111,7 @@ fn key_context_menu(app: &mut EffectcraftApp, g: &mut Rw, rc: &RowCtx<'_>, prop:
("Keyframe Interpolation…", "keys.interpolation", json!({})),
("Keyframe Velocity…", "keys.velocity", json!({})),
("Toggle Hold Keyframe", "keys.toggleHold", json!({})),
(if k.muted { "Unmute Keyframes" } else { "Mute Keyframes" }, "keys.setMuted", json!({"muted": !k.muted})),
("Rove Across Time", "keys.interpolation", json!({"roving": !k.roving})),
("-", "", json!({})),
("Easy Ease", "keys.easyEase", json!({})),
@@ -4203,6 +4204,8 @@ fn key_paint(g: &Rw, rc: &RowCtx<'_>, k: &effectcraft_engine::keyframe::Keyframe
None if ks => t.keyframe_selected,
None => t.keyframe,
};
// Muted keys stay visible and editable but no longer drive the value.
let kcol = if k.muted { kcol.gamma_multiply(0.35) } else { kcol };
if k.roving {
// Roving keys: a small dot (their time follows their neighbours).
gp.circle_filled(c, 2.5, kcol);
@@ -5128,6 +5131,7 @@ const SHAPE_ADD_ITEMS: &[(&str, &str)] = &[
("trim", "Trim Paths"),
("twist", "Twist"),
("wiggle", "Wiggle Paths"),
("wiggler", "Wiggle Transform"),
("zigzag", "Zig Zag"),
];
+1 -1
View File
@@ -626,7 +626,7 @@ pub fn apply_dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) {
super::dialogs::modal(ctx, "Motion Tracker Apply Options", vec2(380.0, 170.0), t, |ui| {
ui.horizontal(|ui| {
ui.label(crate::i18n::tr("Apply Dimensions:"));
let labels = ["X and Y", "X only", "Y only"];
let labels = [crate::i18n::tr("X and Y"), crate::i18n::tr("X only"), crate::i18n::tr("Y only")];
let r = craft_ui::ComboBox::from_id_salt("track-apply-dims")
.selected_text(labels[d.dims.min(2)])
.show_ui(ui, |ui| {
+27 -1
View File
@@ -16,7 +16,16 @@ use crate::theme::Tokens;
use crate::{Dialog, EffectcraftApp};
/// Commands that replace or close the open project (Quit is asked when the window closes).
const CLOSING: &[&str] = &["file.newProject", "file.open", "file.openRecent", "file.closeProject", "file.revert", "file.openDemoProject", "templates.create"];
const CLOSING: &[&str] = &[
"file.newProject",
"file.open",
"file.openRecent",
"file.closeProject",
"file.revert",
"file.openDemoProject",
"templates.create",
"file.runShutdownScripts",
];
/// The command waiting on the prompt, and whether the user already answered it.
#[derive(Clone, Debug, Default)]
@@ -239,6 +248,23 @@ mod tests {
assert!(app.session.is_dirty());
}
#[test]
fn shutdown_scripts_prompt_before_discarding_unsaved_work() {
let (mut app, ctx) = app();
modified(&mut app);
let result = crate::menus::invoke(&mut app, &ctx, "file.runShutdownScripts", json!({})).unwrap();
assert_eq!(result, json!({"dialog": "unsavedChanges"}));
assert_eq!(app.session.project.comps().count(), 1);
assert!(app.session.is_dirty());
answer_with(&mut app, &ctx, Answer::Cancel);
assert_eq!(app.session.project.comps().count(), 1);
assert!(app.session.is_dirty());
crate::menus::invoke(&mut app, &ctx, "file.runShutdownScripts", json!({})).unwrap();
answer_with(&mut app, &ctx, Answer::Discard);
assert_eq!(app.session.project.comps().count(), 0);
assert!(!app.session.is_dirty() && app.dialog.is_none());
}
#[test]
fn closing_a_modified_project_asks_and_cancel_keeps_it() {
let (mut app, ctx) = app();
+3 -3
View File
@@ -9,13 +9,13 @@ pub(super) fn empty_state(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rec
let c = rect.center();
p.text(c - vec2(0.0, 40.0), Align2::CENTER_CENTER, crate::i18n::tr("Composition"), Tokens::semibold(16.0), t.text);
let b1 = Rect::from_center_size(c + vec2(0.0, 0.0), vec2(220.0, 30.0));
if widgets::text_button(ui, b1, "New Composition", true, &t, egui::Id::unique("empty-newcomp")).clicked() {
if widgets::text_button(ui, b1, crate::i18n::tr("New Composition"), true, &t, egui::Id::unique("empty-newcomp")).clicked() {
crate::panels::dialogs::open_new_comp(app);
}
app.auto.add("viewer.empty.newComp", b1, "New Composition");
let b2 = Rect::from_center_size(c + vec2(0.0, 40.0), vec2(220.0, 30.0));
if widgets::text_button(ui, b2, "New Composition From Footage", false, &t, egui::Id::unique("empty-fromfootage")).clicked() {
let _ = crate::menus::invoke(app, &ui.ctx().clone(), "file.import", json!({}));
if widgets::text_button(ui, b2, crate::i18n::tr("New Composition From Footage"), false, &t, egui::Id::unique("empty-fromfootage")).clicked() {
let _ = crate::menus::invoke(app, &ui.ctx().clone(), "file.import", json!({"newComp": true}));
}
app.auto.add("viewer.empty.import", b2, "New Composition From Footage");
// A Project item dropped on the empty viewer creates the first composition,
+25 -20
View File
@@ -360,31 +360,36 @@ pub(crate) fn motion_path(
}
let v = k.value.as_vec3();
let s = to_scr(v);
// Auto-Bezier keys show handles on a side the path leaves straight too (a two-key path's
// ends), to drag it into a curve; Linear keys (no tangents) show none.
let (tin, tout) = if k.spatial_auto {
effectcraft_engine::keyframe::spatial_handles(&pos.keys, i)
} else {
effectcraft_engine::keyframe::spatial_tangents(&pos.keys, i)
};
for (out, tg) in [(false, tin), (true, tout)] {
let has = (out && i + 1 < pos.keys.len()) || (!out && i > 0);
if !has || (tg[0] == 0.0 && tg[1] == 0.0) {
continue;
let selected = sel.contains(&KeyRef { layer: l.id, prop: pos.uid, time: k.time });
// A muted position key is off the motion path: no tangents, and a dimmed point.
let kc = if k.muted { col.gamma_multiply(if selected { 0.55 } else { 0.35 }) } else { col };
if !k.muted {
// Auto-Bezier keys show handles on a side the path leaves straight too (a two-key path's
// ends), to drag it into a curve; Linear keys (no tangents) show none.
let (tin, tout) = if k.spatial_auto {
effectcraft_engine::keyframe::spatial_handles(&pos.keys, i)
} else {
effectcraft_engine::keyframe::spatial_tangents(&pos.keys, i)
};
for (out, tg) in [(false, tin), (true, tout)] {
let has = (out && i + 1 < pos.keys.len()) || (!out && i > 0);
if !has || (tg[0] == 0.0 && tg[1] == 0.0) {
continue;
}
let h = to_scr([v[0] + tg[0], v[1] + tg[1], v[2] + tg[2]]);
painter.line_segment([s, h], Stroke::new(1.0, kc));
painter.circle_filled(h, 3.0, kc);
let side = if out { "out" } else { "in" };
app.auto.add(&format!("viewer.motionPath.{}.{i}.{side}", l.id.0), Rect::from_center_size(h, vec2(8.0, 8.0)), "Spatial tangent");
hits.push(KeyHit { layer: l.id, prop: pos.uid, time: k.time, pos: h, tangent: Some(out), value: v, p2c });
}
let h = to_scr([v[0] + tg[0], v[1] + tg[1], v[2] + tg[2]]);
painter.line_segment([s, h], Stroke::new(1.0, col));
painter.circle_filled(h, 3.0, col);
let side = if out { "out" } else { "in" };
app.auto.add(&format!("viewer.motionPath.{}.{i}.{side}", l.id.0), Rect::from_center_size(h, vec2(8.0, 8.0)), "Spatial tangent");
hits.push(KeyHit { layer: l.id, prop: pos.uid, time: k.time, pos: h, tangent: Some(out), value: v, p2c });
}
let r = Rect::from_center_size(s, vec2(7.0, 7.0));
if sel.contains(&KeyRef { layer: l.id, prop: pos.uid, time: k.time }) {
painter.rect_filled(r, 0.0, col);
if selected {
painter.rect_filled(r, 0.0, kc);
} else {
painter.rect_filled(r, 0.0, Color32::from_black_alpha(140));
painter.rect_stroke(r, 0.0, Stroke::new(1.0, col), StrokeKind::Inside);
painter.rect_stroke(r, 0.0, Stroke::new(1.0, kc), StrokeKind::Inside);
}
app.auto.add(&format!("viewer.motionPath.{}.{i}", l.id.0), r.expand(2.0), "Position keyframe");
hits.push(KeyHit { layer: l.id, prop: pos.uid, time: k.time, pos: s, tangent: None, value: v, p2c });
+1 -1
View File
@@ -602,7 +602,7 @@ pub fn popup_menu(ui: &mut Ui, id: egui::Id, pos: egui::Pos2, options: &[String]
pub fn layer_picker(
ui: &mut Ui,
rect: Rect,
layers: &[effectcraft_engine::project::Layer],
layers: &effectcraft_engine::project::Layers,
current: Option<u64>,
t: &Tokens,
id: egui::Id,