app: project properties dialog (File > Project Properties…)
The menu item was a placeholder print; it now opens a real dialog (the C++ ProjectPropertiesDialog): - Per-project OCIO config override with a 浏览… picker: validated on OK (an invalid config keeps the dialog open with the error shown, like the C++ accept()), persisted in the project settings, applied to the display color pipeline on accept and on project open, and reverted to the app default when the project closes. oak-render gains set_up_default_config_from for the explicit-path load. - Disk-cache location (default / alongside the project / custom path): persisted through the OVE serializer (cachesetting/customcachepath round-trip the settings map, clamped on load) and honored by the thumbnail writer — the first live consumer of Project::cache_path. - PathField gains an enabled state (the custom path field follows the combo selection). The C++ color tab's Default Input Color Space and Reference Space combos are intentionally absent: the Rust render pipeline has no consumer for them today (decode performs no input transfer conversion), so showing them would be dead settings. Tests: dialog opens, OK applies the cache location, an invalid OCIO config keeps the dialog open with the error row. i18n keys for all eight packs.
This commit is contained in:
@@ -238,6 +238,15 @@
|
||||
"ofx.color.invalid": "Ungültige Hexadezimalfarbe"
|
||||
"dialog.cancel": "Abbrechen"
|
||||
"dialog.close": "Schließen"
|
||||
"dialog.ok": "OK"
|
||||
"projprops.title": "Projekteigenschaften"
|
||||
"projprops.name": "Projektname"
|
||||
"projprops.ocio_config": "OpenColorIO-Konfiguration"
|
||||
"projprops.browse": "Durchsuchen…"
|
||||
"projprops.cache.location": "Speicherort für den Festplatten-Cache"
|
||||
"projprops.cache.default": "Standardort verwenden"
|
||||
"projprops.cache.alongside": "Zusammen mit dem Projekt speichern"
|
||||
"projprops.cache.custom": "Benutzerdefinierten Speicherort verwenden:"
|
||||
"file.open.title": "Projekt öffnen"
|
||||
"file.import_footage.title": "Material importieren"
|
||||
"preferences.title": "Einstellungen"
|
||||
|
||||
@@ -238,6 +238,15 @@
|
||||
"ofx.color.invalid": "Invalid hex color"
|
||||
"dialog.cancel": "Cancel"
|
||||
"dialog.close": "Close"
|
||||
"dialog.ok": "OK"
|
||||
"projprops.title": "Project Properties"
|
||||
"projprops.name": "Project Name"
|
||||
"projprops.ocio_config": "OpenColorIO Configuration"
|
||||
"projprops.browse": "Browse…"
|
||||
"projprops.cache.location": "Disk Cache Location"
|
||||
"projprops.cache.default": "Use Default Location"
|
||||
"projprops.cache.alongside": "Store Alongside Project"
|
||||
"projprops.cache.custom": "Use Custom Location:"
|
||||
"file.open.title": "Open Project"
|
||||
"file.import_footage.title": "Import Footage"
|
||||
"preferences.title": "Preferences"
|
||||
|
||||
@@ -238,6 +238,15 @@
|
||||
"ofx.color.invalid": "Color hexadecimal no válido"
|
||||
"dialog.cancel": "Cancelar"
|
||||
"dialog.close": "Cerrar"
|
||||
"dialog.ok": "Aceptar"
|
||||
"projprops.title": "Propiedades del proyecto"
|
||||
"projprops.name": "Nombre del proyecto"
|
||||
"projprops.ocio_config": "Configuración de OpenColorIO"
|
||||
"projprops.browse": "Examinar…"
|
||||
"projprops.cache.location": "Ubicación de la caché de disco"
|
||||
"projprops.cache.default": "Usar ubicación predeterminada"
|
||||
"projprops.cache.alongside": "Almacenar junto al proyecto"
|
||||
"projprops.cache.custom": "Usar ubicación personalizada:"
|
||||
"file.open.title": "Abrir proyecto"
|
||||
"file.import_footage.title": "Importar material"
|
||||
"preferences.title": "Preferencias"
|
||||
|
||||
@@ -238,6 +238,15 @@
|
||||
"ofx.color.invalid": "Couleur hexadécimale invalide"
|
||||
"dialog.cancel": "Annuler"
|
||||
"dialog.close": "Fermer"
|
||||
"dialog.ok": "OK"
|
||||
"projprops.title": "Propriétés du projet"
|
||||
"projprops.name": "Nom du projet"
|
||||
"projprops.ocio_config": "Configuration OpenColorIO"
|
||||
"projprops.browse": "Parcourir…"
|
||||
"projprops.cache.location": "Emplacement du cache disque"
|
||||
"projprops.cache.default": "Utiliser l'emplacement par défaut"
|
||||
"projprops.cache.alongside": "Stocker à côté du projet"
|
||||
"projprops.cache.custom": "Utiliser un emplacement personnalisé :"
|
||||
"file.open.title": "Ouvrir le projet"
|
||||
"file.import_footage.title": "Importer des médias"
|
||||
"preferences.title": "Préférences"
|
||||
|
||||
@@ -238,6 +238,15 @@
|
||||
"ofx.color.invalid": "無効な16進カラーです"
|
||||
"dialog.cancel": "キャンセル"
|
||||
"dialog.close": "閉じる"
|
||||
"dialog.ok": "OK"
|
||||
"projprops.title": "プロジェクト設定"
|
||||
"projprops.name": "プロジェクト名"
|
||||
"projprops.ocio_config": "OpenColorIO設定"
|
||||
"projprops.browse": "参照…"
|
||||
"projprops.cache.location": "ディスクキャッシュの場所"
|
||||
"projprops.cache.default": "デフォルトの場所を使用"
|
||||
"projprops.cache.alongside": "プロジェクトと同じ場所に保存"
|
||||
"projprops.cache.custom": "カスタムの場所を使用:"
|
||||
"file.open.title": "プロジェクトを開く"
|
||||
"file.import_footage.title": "素材を読み込み"
|
||||
"preferences.title": "環境設定"
|
||||
|
||||
@@ -238,6 +238,15 @@
|
||||
"ofx.color.invalid": "Cor hexadecimal inválida"
|
||||
"dialog.cancel": "Cancelar"
|
||||
"dialog.close": "Fechar"
|
||||
"dialog.ok": "OK"
|
||||
"projprops.title": "Propriedades do projeto"
|
||||
"projprops.name": "Nome do projeto"
|
||||
"projprops.ocio_config": "Configuração do OpenColorIO"
|
||||
"projprops.browse": "Procurar…"
|
||||
"projprops.cache.location": "Localização do cache de disco"
|
||||
"projprops.cache.default": "Usar localização padrão"
|
||||
"projprops.cache.alongside": "Armazenar junto ao projeto"
|
||||
"projprops.cache.custom": "Usar localização personalizada:"
|
||||
"file.open.title": "Abrir projeto"
|
||||
"file.import_footage.title": "Importar mídia"
|
||||
"preferences.title": "Preferências"
|
||||
|
||||
@@ -238,6 +238,15 @@
|
||||
"ofx.color.invalid": "Недопустимый шестнадцатеричный цвет"
|
||||
"dialog.cancel": "Отмена"
|
||||
"dialog.close": "Закрыть"
|
||||
"dialog.ok": "ОК"
|
||||
"projprops.title": "Свойства проекта"
|
||||
"projprops.name": "Имя проекта"
|
||||
"projprops.ocio_config": "Конфигурация OpenColorIO"
|
||||
"projprops.browse": "Обзор…"
|
||||
"projprops.cache.location": "Расположение дискового кэша"
|
||||
"projprops.cache.default": "Использовать расположение по умолчанию"
|
||||
"projprops.cache.alongside": "Хранить рядом с проектом"
|
||||
"projprops.cache.custom": "Использовать пользовательское расположение:"
|
||||
"file.open.title": "Открыть проект"
|
||||
"file.import_footage.title": "Импорт медиафайлов"
|
||||
"preferences.title": "Настройки"
|
||||
|
||||
@@ -238,6 +238,15 @@
|
||||
"ofx.color.invalid": "无效的十六进制颜色"
|
||||
"dialog.cancel": "取消"
|
||||
"dialog.close": "关闭"
|
||||
"dialog.ok": "确定"
|
||||
"projprops.title": "项目属性"
|
||||
"projprops.name": "项目名称"
|
||||
"projprops.ocio_config": "OpenColorIO 配置"
|
||||
"projprops.browse": "浏览…"
|
||||
"projprops.cache.location": "磁盘缓存位置"
|
||||
"projprops.cache.default": "使用默认位置"
|
||||
"projprops.cache.alongside": "随工程文件存储"
|
||||
"projprops.cache.custom": "使用自定义位置:"
|
||||
"file.open.title": "打开项目"
|
||||
"file.import_footage.title": "导入素材"
|
||||
"preferences.title": "偏好设置"
|
||||
|
||||
+227
-1
@@ -123,6 +123,8 @@ mod modal_ids {
|
||||
pub const PLUGIN_PROGRESS: usize = 10;
|
||||
/// The action search dialog (Help > Search Actions…, the `/` key).
|
||||
pub const ACTION_SEARCH: usize = 11;
|
||||
/// The project properties dialog (File > Project Properties…).
|
||||
pub const PROJECT_PROPERTIES: usize = 12;
|
||||
}
|
||||
|
||||
/// What a picked platform-dialog path should do.
|
||||
@@ -178,6 +180,12 @@ enum ModalState<E: AppEngine> {
|
||||
modal: Entity<Modal>,
|
||||
content: Entity<crate::dialogs::ActionSearchContent>,
|
||||
},
|
||||
/// The project properties dialog (File > Project Properties…; the
|
||||
/// per-project OCIO config override and the disk-cache location).
|
||||
ProjectProperties {
|
||||
modal: Entity<Modal>,
|
||||
content: Entity<crate::dialogs::ProjectPropertiesContent<E>>,
|
||||
},
|
||||
}
|
||||
|
||||
/// A running export: the session the tick loop drains for progress.
|
||||
@@ -197,7 +205,8 @@ impl<E: AppEngine> ModalState<E> {
|
||||
| ModalState::ManagerRename { modal, .. }
|
||||
| ModalState::ManagerDelete { modal, .. }
|
||||
| ModalState::Proxy { modal, .. }
|
||||
| ModalState::ActionSearch { modal, .. } => Some(modal.clone()),
|
||||
| ModalState::ActionSearch { modal, .. }
|
||||
| ModalState::ProjectProperties { modal, .. } => Some(modal.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1062,6 +1071,7 @@ impl<E: AppEngine> OakApp<E> {
|
||||
self.rebuild_menu_bar(cx);
|
||||
}
|
||||
A::ProxySettings => self.open_proxy_dialog(cx),
|
||||
A::ProjectProperties => self.open_project_properties(cx),
|
||||
// The multicam source-switch hotkeys are scoped to the Multicam
|
||||
// panel (the focused-panel route handles them there); a fall-through
|
||||
// from any other focused panel is a silent no-op.
|
||||
@@ -2000,6 +2010,46 @@ impl<E: AppEngine> OakApp<E> {
|
||||
});
|
||||
}
|
||||
|
||||
/// Opens the project properties dialog (File > Project Properties…; the
|
||||
/// C++ `ProjectPropertiesDialog`): the read-only project name, the
|
||||
/// per-project OCIO config override and the disk-cache location. The OK
|
||||
/// button applies through the content's `commit` (an invalid OCIO config
|
||||
/// keeps the dialog open), Escape / Cancel discard without applying.
|
||||
fn open_project_properties(&mut self, cx: &mut Context<Self>) {
|
||||
if !matches!(self.modal, ModalState::None) {
|
||||
return;
|
||||
}
|
||||
let engine = self.engine.clone();
|
||||
let project_name = self
|
||||
.engine
|
||||
.read(cx)
|
||||
.project()
|
||||
.map(|p| p.name.clone())
|
||||
.unwrap_or_default();
|
||||
self.spawn_modal(cx, move |window, app| {
|
||||
let content =
|
||||
app.new(|cx| crate::dialogs::ProjectPropertiesContent::new(engine, window, cx));
|
||||
let modal = app.new(|cx| {
|
||||
Modal::new(
|
||||
modal_ids::PROJECT_PROPERTIES,
|
||||
ModalOptions::new(
|
||||
format!("{} — {project_name}", crate::i18n::tr("projprops.title")),
|
||||
px(560.0),
|
||||
)
|
||||
.with_button(DialogButton::primary(crate::i18n::tr("dialog.ok")))
|
||||
.with_button(DialogButton::new(
|
||||
crate::i18n::tr("dialog.cancel"),
|
||||
gpui_widgets::dialog::DialogButtonRole::Secondary,
|
||||
)),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.with_content(content.clone())
|
||||
});
|
||||
ModalState::ProjectProperties { modal, content }
|
||||
});
|
||||
}
|
||||
|
||||
/// Opens the export dialog.
|
||||
fn open_export_dialog(&mut self, cx: &mut Context<Self>) {
|
||||
if self.engine.read(cx).current_sequence().is_none() {
|
||||
@@ -2194,6 +2244,25 @@ impl<E: AppEngine> OakApp<E> {
|
||||
}
|
||||
}
|
||||
}
|
||||
modal_ids::PROJECT_PROPERTIES => {
|
||||
if let ModalState::ProjectProperties { content, .. } = &self.modal {
|
||||
let content = content.clone();
|
||||
if *button == 0 {
|
||||
// OK: apply the settings; an invalid OCIO config
|
||||
// keeps the dialog open with the error shown.
|
||||
match content.update(cx, |dialog, cx| dialog.commit(cx)) {
|
||||
Ok(()) => self.close_modal(cx),
|
||||
Err(err) => {
|
||||
content.update(cx, |dialog, cx| {
|
||||
dialog.set_error(Some(err), cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.close_modal(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
ModalEvent::Dismissed { control } => match *control {
|
||||
@@ -2213,6 +2282,9 @@ impl<E: AppEngine> OakApp<E> {
|
||||
// Escape closes the proxy dialog without applying (the
|
||||
// Close button is the apply path, like the C++ accept()).
|
||||
modal_ids::PROXY => self.close_modal(cx),
|
||||
// Escape / backdrop close the project properties dialog
|
||||
// without applying (only OK commits).
|
||||
modal_ids::PROJECT_PROPERTIES => self.close_modal(cx),
|
||||
_ => self.close_modal(cx),
|
||||
},
|
||||
}
|
||||
@@ -3064,6 +3136,160 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Opening File → Project Properties… must not crash: the content is
|
||||
/// built with the engine and the modal is layered onto the shell (the
|
||||
/// same deferred-build path as Preferences).
|
||||
#[gpui::test]
|
||||
async fn project_properties_dialog_opens_without_crashing(cx: &mut TestAppContext) {
|
||||
let _guard = crate::actions::shortcuts_test_lock().lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner());
|
||||
crate::i18n::set_language_code("en-US");
|
||||
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(size(px(1600.0), px(900.0)), |window, cx| {
|
||||
OakApp::<MockEngine>::new(window, None, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let root = window.root(cx).expect("app root");
|
||||
|
||||
cx.update(|app| {
|
||||
root.update(
|
||||
app,
|
||||
|app, cx| app.on_menu(ActionId::ProjectProperties.menu_id(), cx),
|
||||
)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
// Force a draw so render-time panics in the dialog content surface.
|
||||
cx.update_window(window.into(), |_root, window, cx| {
|
||||
window.draw(cx).clear();
|
||||
})
|
||||
.expect("window is still open");
|
||||
|
||||
let has_modal =
|
||||
cx.read(|app| matches!(root.read(app).modal, ModalState::ProjectProperties { .. }));
|
||||
assert!(
|
||||
has_modal,
|
||||
"project properties modal should be shown after the menu action"
|
||||
);
|
||||
}
|
||||
|
||||
/// The dialog's OK button applies the chosen cache location to the
|
||||
/// engine: choosing 自定义位置 + a path, then clicking OK, lands in
|
||||
/// `project_cache_location()` as `(2, path)` and closes the dialog.
|
||||
#[gpui::test]
|
||||
async fn project_properties_commit_applies_cache_location(cx: &mut TestAppContext) {
|
||||
let _guard = crate::actions::shortcuts_test_lock().lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner());
|
||||
let (window, root) = mock_shell(cx);
|
||||
|
||||
cx.update(|app| {
|
||||
root.update(
|
||||
app,
|
||||
|app, cx| app.on_menu(ActionId::ProjectProperties.menu_id(), cx),
|
||||
)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let content = cx.read(|app| match &root.read(app).modal {
|
||||
ModalState::ProjectProperties { content, .. } => content.clone(),
|
||||
_ => panic!("project properties modal should be open"),
|
||||
});
|
||||
|
||||
// Choose 自定义位置 and type a path.
|
||||
cx.update(|app| {
|
||||
content.update(app, |dialog, cx| {
|
||||
dialog.select_cache_setting(2, cx);
|
||||
dialog.set_custom_cache_path("/tmp/oak-cache", cx);
|
||||
})
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
// Click the OK button (the modal's primary button).
|
||||
cx.update_window(window.into(), |_root, window, cx| {
|
||||
window.draw(cx).clear();
|
||||
})
|
||||
.expect("window is still open");
|
||||
cx.run_until_parked();
|
||||
let mut vcx = VisualTestContext::from_window(window.into(), cx);
|
||||
let ok = vcx.debug_bounds("dialog-button-0").expect("OK button rendered");
|
||||
vcx.simulate_click(ok.center(), gpui::Modifiers::none());
|
||||
drop(vcx);
|
||||
cx.run_until_parked();
|
||||
|
||||
assert!(
|
||||
cx.read(|app| matches!(root.read(app).modal, ModalState::None)),
|
||||
"OK closes the dialog"
|
||||
);
|
||||
let location = cx.read(|app| root.read(app).engine.read(app).project_cache_location());
|
||||
assert_eq!(location, (2, "/tmp/oak-cache".to_string()));
|
||||
}
|
||||
|
||||
/// A bogus OCIO config keeps the dialog open: clicking OK runs the
|
||||
/// engine's validation, which rejects the path, shows the error label
|
||||
/// under the OCIO row and leaves the modal on screen.
|
||||
#[gpui::test]
|
||||
async fn project_properties_invalid_ocio_keeps_the_dialog_open(cx: &mut TestAppContext) {
|
||||
let _guard = crate::actions::shortcuts_test_lock().lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner());
|
||||
let (window, root) = mock_shell(cx);
|
||||
|
||||
cx.update(|app| {
|
||||
root.update(
|
||||
app,
|
||||
|app, cx| app.on_menu(ActionId::ProjectProperties.menu_id(), cx),
|
||||
)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let content = cx.read(|app| match &root.read(app).modal {
|
||||
ModalState::ProjectProperties { content, .. } => content.clone(),
|
||||
_ => panic!("project properties modal should be open"),
|
||||
});
|
||||
|
||||
// Type a path that cannot load as an OCIO config.
|
||||
cx.update(|app| {
|
||||
content.update(app, |dialog, cx| {
|
||||
dialog.set_ocio_config_path("/nonexistent/ocio/config.ocio", cx);
|
||||
})
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
// Click OK: the commit fails, the dialog stays open, the error shows.
|
||||
cx.update_window(window.into(), |_root, window, cx| {
|
||||
window.draw(cx).clear();
|
||||
})
|
||||
.expect("window is still open");
|
||||
cx.run_until_parked();
|
||||
let mut vcx = VisualTestContext::from_window(window.into(), cx);
|
||||
let ok = vcx.debug_bounds("dialog-button-0").expect("OK button rendered");
|
||||
vcx.simulate_click(ok.center(), gpui::Modifiers::none());
|
||||
drop(vcx);
|
||||
cx.run_until_parked();
|
||||
|
||||
let still_open =
|
||||
cx.read(|app| matches!(root.read(app).modal, ModalState::ProjectProperties { .. }));
|
||||
assert!(still_open, "an invalid OCIO config keeps the dialog open");
|
||||
let error = cx.read(|app| match &root.read(app).modal {
|
||||
ModalState::ProjectProperties { content, .. } => content.read(app).error().cloned(),
|
||||
_ => None,
|
||||
});
|
||||
let error = error.expect("the commit error is recorded");
|
||||
assert!(
|
||||
error.to_lowercase().contains("ocio"),
|
||||
"the error mentions the config: {error}"
|
||||
);
|
||||
|
||||
// The error label renders under the OCIO row.
|
||||
cx.update_window(window.into(), |_root, window, cx| {
|
||||
window.draw(cx).clear();
|
||||
})
|
||||
.expect("window is still open");
|
||||
let mut vcx = VisualTestContext::from_window(window.into(), cx);
|
||||
assert!(
|
||||
vcx.debug_bounds("projprops-error").is_some(),
|
||||
"the OCIO error label renders"
|
||||
);
|
||||
drop(vcx);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Keyboard shortcuts (M12 P5c)
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
@@ -204,7 +204,10 @@ impl PreferencesContent {
|
||||
// --- 缓存 Cache: the disk cache directory --------------------------
|
||||
let cache_dir = cx.new(|cx| {
|
||||
let editor = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx));
|
||||
PathField { editor }
|
||||
PathField {
|
||||
editor,
|
||||
enabled: true,
|
||||
}
|
||||
});
|
||||
let configured_cache = config_get_string(CONFIG_KEY_DISK_CACHE_PATH);
|
||||
cache_dir.update(cx, |field, cx| field.set_path(configured_cache, cx));
|
||||
@@ -319,7 +322,10 @@ impl PreferencesContent {
|
||||
.detach();
|
||||
let display_icc_path = cx.new(|cx| {
|
||||
let editor = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx));
|
||||
PathField { editor }
|
||||
PathField {
|
||||
editor,
|
||||
enabled: true,
|
||||
}
|
||||
});
|
||||
let configured_icc = config_get_string(CONFIG_KEY_CUSTOM_ICC);
|
||||
display_icc_path.update(cx, |field, cx| field.set_path(configured_icc, cx));
|
||||
@@ -719,6 +725,9 @@ impl Render for PreferencesContent {
|
||||
/// A text field with the same shape as the file dialog's path field.
|
||||
pub struct PathField {
|
||||
editor: Entity<EditableTextState>,
|
||||
/// Whether the field accepts input (disabled fields dim and drop the
|
||||
/// text-input handler, like the checkbox disabled state).
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
impl PathField {
|
||||
@@ -735,12 +744,29 @@ impl PathField {
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Whether the field accepts input (disabled fields are read-only and
|
||||
/// render dimmed).
|
||||
pub fn set_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {
|
||||
if self.enabled == enabled {
|
||||
return;
|
||||
}
|
||||
self.enabled = enabled;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// The input state the field starts in (chainable after construction).
|
||||
pub fn with_enabled(mut self, enabled: bool) -> Self {
|
||||
self.enabled = enabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for PathField {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let colors = cx.default_colors().clone();
|
||||
let weak = self.editor.downgrade();
|
||||
let enabled = self.enabled;
|
||||
div()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
@@ -748,10 +774,11 @@ impl Render for PathField {
|
||||
.bg(colors.background)
|
||||
.px_2()
|
||||
.py_1()
|
||||
.opacity(if enabled { 1.0 } else { 0.45 })
|
||||
.child(
|
||||
text_input("gpui-widgets-export-path", cx)
|
||||
.state(weak)
|
||||
.accepts_input(true),
|
||||
.accepts_input(enabled),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -790,7 +817,10 @@ impl ExportDialogContent {
|
||||
|
||||
let path = cx.new(|cx| {
|
||||
let editor = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx));
|
||||
PathField { editor }
|
||||
PathField {
|
||||
editor,
|
||||
enabled: true,
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
@@ -986,7 +1016,10 @@ impl<E: crate::oakui::engine::AppEngine> ProxyDialogContent<E> {
|
||||
|
||||
let ffmpeg_path = cx.new(|cx| {
|
||||
let editor = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx));
|
||||
PathField { editor }
|
||||
PathField {
|
||||
editor,
|
||||
enabled: true,
|
||||
}
|
||||
});
|
||||
ffmpeg_path.update(cx, |field, cx| {
|
||||
field.set_path(config_get_string(CONFIG_KEY_FFMPEG_PATH), cx)
|
||||
@@ -1273,6 +1306,239 @@ impl<E: crate::oakui::engine::AppEngine> Render for ProxyDialogContent<E> {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Project properties (the C++ File > Project Properties dialog)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// The project-properties dialog content (the C++ `ProjectPropertiesDialog`):
|
||||
/// the read-only project name, the per-project OCIO config override and the
|
||||
/// disk-cache location. Apply happens through [`Self::commit`], which the
|
||||
/// host runs on the OK button — an invalid OCIO config keeps the dialog open
|
||||
/// with the error shown under the OCIO row.
|
||||
pub struct ProjectPropertiesContent<E: crate::oakui::engine::AppEngine> {
|
||||
engine: Entity<E>,
|
||||
ocio_config: Entity<PathField>,
|
||||
cache_location: Entity<ComboBox>,
|
||||
custom_cache_path: Entity<PathField>,
|
||||
/// The cache location selected in the combo (0 = default location,
|
||||
/// 1 = alongside the project, 2 = custom path; see
|
||||
/// [`crate::oakui::engine::AppEngine::project_cache_location`]).
|
||||
cache_setting: i32,
|
||||
/// The commit error shown under the OCIO row (an invalid config keeps
|
||||
/// the dialog open, like the C++ accept()).
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
impl<E: crate::oakui::engine::AppEngine> ProjectPropertiesContent<E> {
|
||||
/// Builds the content seeded from the engine's current project state.
|
||||
pub fn new(engine: Entity<E>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let ocio_config = cx.new(|cx| {
|
||||
let editor = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx));
|
||||
PathField {
|
||||
editor,
|
||||
enabled: true,
|
||||
}
|
||||
});
|
||||
let configured_ocio = engine.read(cx).project_ocio_config();
|
||||
ocio_config.update(cx, |field, cx| field.set_path(configured_ocio, cx));
|
||||
|
||||
let cache_options = vec![
|
||||
ComboBoxOption::new(0, i18n::tr("projprops.cache.default")),
|
||||
ComboBoxOption::new(1, i18n::tr("projprops.cache.alongside")),
|
||||
ComboBoxOption::new(2, i18n::tr("projprops.cache.custom")),
|
||||
];
|
||||
let cache_location = cx.new(|cx| ComboBox::new(30, cache_options, window, cx));
|
||||
let (cache_setting, custom_path) = engine.read(cx).project_cache_location();
|
||||
cache_location.update(cx, |combo, cx| {
|
||||
combo.set_selected(Some(cache_setting as usize), cx)
|
||||
});
|
||||
// The custom-path field follows the combo selection live.
|
||||
cx.subscribe(&cache_location, |this, _combo, event: &ComboBoxEvent, cx| {
|
||||
let ComboBoxEvent::Selected { value } = event;
|
||||
let setting = *value as i32;
|
||||
this.cache_setting = setting;
|
||||
this.custom_cache_path.update(cx, |field, cx| {
|
||||
field.set_enabled(setting == 2, cx)
|
||||
});
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
|
||||
let custom_cache_path = cx.new(|cx| {
|
||||
let editor = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx));
|
||||
PathField { editor, enabled: cache_setting == 2 }
|
||||
});
|
||||
custom_cache_path.update(cx, |field, cx| field.set_path(custom_path, cx));
|
||||
|
||||
Self {
|
||||
engine,
|
||||
ocio_config,
|
||||
cache_location,
|
||||
custom_cache_path,
|
||||
cache_setting,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The OCIO config path currently entered.
|
||||
pub fn ocio_config_path(&self, cx: &App) -> SharedString {
|
||||
self.ocio_config.read(cx).path(cx)
|
||||
}
|
||||
|
||||
/// Replaces the OCIO config path (the 浏览… picker and tests).
|
||||
pub fn set_ocio_config_path(&mut self, path: impl Into<SharedString>, cx: &mut Context<Self>) {
|
||||
let path = path.into();
|
||||
self.ocio_config
|
||||
.update(cx, |field, cx| field.set_path(path, cx));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// The custom disk-cache path currently entered.
|
||||
pub fn custom_cache_path(&self, cx: &App) -> SharedString {
|
||||
self.custom_cache_path.read(cx).path(cx)
|
||||
}
|
||||
|
||||
/// Replaces the custom disk-cache path.
|
||||
pub fn set_custom_cache_path(&mut self, path: impl Into<SharedString>, cx: &mut Context<Self>) {
|
||||
let path = path.into();
|
||||
self.custom_cache_path
|
||||
.update(cx, |field, cx| field.set_path(path, cx));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Selects the cache location option (0 = default, 1 = alongside,
|
||||
/// 2 = custom) and enables the custom-path field accordingly — the
|
||||
/// combo's own event path, exposed for tests.
|
||||
pub fn select_cache_setting(&mut self, setting: i32, cx: &mut Context<Self>) {
|
||||
let setting = setting.clamp(0, 2);
|
||||
self.cache_setting = setting;
|
||||
self.cache_location
|
||||
.update(cx, |combo, cx| combo.set_selected(Some(setting as usize), cx));
|
||||
self.custom_cache_path.update(cx, |field, cx| {
|
||||
field.set_enabled(setting == 2, cx)
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Applies the edited settings (the C++ `accept()`): validates and
|
||||
/// applies the OCIO config override first — an invalid config keeps the
|
||||
/// dialog open — then the disk-cache location. Ok clears the error row.
|
||||
pub fn commit(&mut self, cx: &mut Context<Self>) -> Result<(), String> {
|
||||
let ocio = self.ocio_config_path(cx).to_string();
|
||||
self.engine
|
||||
.update(cx, |engine, cx| engine.set_project_ocio_config(ocio, cx))?;
|
||||
let custom = self.custom_cache_path(cx).to_string();
|
||||
let setting = self.cache_setting;
|
||||
let path = if setting == 2 { custom } else { String::new() };
|
||||
self.engine
|
||||
.update(cx, |engine, cx| engine.set_project_cache_location(setting, path, cx));
|
||||
self.set_error(None, cx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The error shown under the OCIO row after a rejected commit.
|
||||
pub fn set_error(&mut self, msg: Option<String>, cx: &mut Context<Self>) {
|
||||
self.error = msg;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// The commit error currently shown (`None` while the last commit
|
||||
/// applied cleanly).
|
||||
pub fn error(&self) -> Option<&String> {
|
||||
self.error.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: crate::oakui::engine::AppEngine> Render for ProjectPropertiesContent<E> {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let colors = cx.default_colors().clone();
|
||||
let project_name = self
|
||||
.engine
|
||||
.read(cx)
|
||||
.project()
|
||||
.map(|p| p.name.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
// The 浏览… button picks an OCIO config through the platform file
|
||||
// dialog and fills the path field (the resolve is async, so the
|
||||
// picker's receiver is drained in a spawned task).
|
||||
let ocio_field = self.ocio_config.clone();
|
||||
let browse = div()
|
||||
.id("projprops-browse")
|
||||
.px_3()
|
||||
.py_1()
|
||||
.rounded_md()
|
||||
.bg(colors.background)
|
||||
.border_1()
|
||||
.border_color(colors.border)
|
||||
.text_color(colors.text)
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(move |_this, _event: &gpui::ClickEvent, _window, cx| {
|
||||
let receiver = cx.prompt_for_paths(PathPromptOptions {
|
||||
files: true,
|
||||
directories: false,
|
||||
multiple: false,
|
||||
prompt: None,
|
||||
});
|
||||
cx.spawn(async move |this, cx| {
|
||||
if let Ok(Ok(Some(paths))) = receiver.await {
|
||||
if let Some(path) = paths.first() {
|
||||
let path = path.to_string_lossy().into_owned();
|
||||
let _ = this.update(cx, |this, cx| this.set_ocio_config_path(path, cx));
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}))
|
||||
.child(i18n::tr("projprops.browse"));
|
||||
|
||||
let ocio_row = div()
|
||||
.flex()
|
||||
.gap_2()
|
||||
.child(ocio_field)
|
||||
.child(browse);
|
||||
|
||||
let custom_row = form_row(
|
||||
&colors,
|
||||
i18n::tr("projprops.cache.custom").into(),
|
||||
self.custom_cache_path.clone(),
|
||||
);
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.w_full()
|
||||
.child(form_row(
|
||||
&colors,
|
||||
i18n::tr("projprops.name").into(),
|
||||
div().text_color(colors.text).child(project_name),
|
||||
))
|
||||
.child(
|
||||
form_row(
|
||||
&colors,
|
||||
i18n::tr("projprops.ocio_config").into(),
|
||||
ocio_row,
|
||||
)
|
||||
.child(if let Some(error) = &self.error {
|
||||
div()
|
||||
.debug_selector(|| "projprops-error".into())
|
||||
.text_color(gpui::rgb(0xe5484d))
|
||||
.text_xs()
|
||||
.child(error.clone())
|
||||
} else {
|
||||
div()
|
||||
}),
|
||||
)
|
||||
.child(form_row(
|
||||
&colors,
|
||||
i18n::tr("projprops.cache.location").into(),
|
||||
self.cache_location.clone(),
|
||||
))
|
||||
.child(custom_row)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Preferences: the tabbed host (General + Keyboard)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -795,6 +795,39 @@ pub trait AppEngine:
|
||||
let _ = cx;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Project properties (the C++ File > Project Properties dialog):
|
||||
// the per-project OCIO config override and the disk-cache location.
|
||||
// Defaults cover engines without a project surface.
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
/// The project's OCIO config override ("" = the app default config).
|
||||
fn project_ocio_config(&self) -> String {
|
||||
String::new()
|
||||
}
|
||||
|
||||
/// Validates and applies the OCIO config override: the file must load
|
||||
/// as an OCIO config (Err keeps the dialog open, like the C++ accept()),
|
||||
/// then the display color pipeline reloads and every rendered frame is
|
||||
/// invalidated. Empty path restores the app default.
|
||||
fn set_project_ocio_config(&mut self, path: String, cx: &mut Context<Self>) -> Result<(), String> {
|
||||
let _ = (path, cx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The project's disk-cache location: `(setting, custom_path)` with
|
||||
/// setting 0 = default location, 1 = alongside the project file,
|
||||
/// 2 = custom path (the C++ `Project::CacheSetting`).
|
||||
fn project_cache_location(&self) -> (i32, String) {
|
||||
(0, String::new())
|
||||
}
|
||||
|
||||
/// Sets the disk-cache location; later cache writes (thumbnails and
|
||||
/// any future per-project caches) land in the new location.
|
||||
fn set_project_cache_location(&mut self, setting: i32, custom_path: String, cx: &mut Context<Self>) {
|
||||
let _ = (setting, custom_path, cx);
|
||||
}
|
||||
|
||||
/// The footage rows the proxy dialog's footage mode lists (every
|
||||
/// footage node in the open project).
|
||||
fn proxy_rows(&self) -> Vec<ProxyFootageRow> {
|
||||
|
||||
@@ -539,6 +539,10 @@ pub struct MockEngine {
|
||||
proxy_custom: HashMap<u64, crate::oakui::engine::ProxyParamsUi>,
|
||||
/// The demo's global "Use Proxy Media" switch.
|
||||
use_proxy: bool,
|
||||
/// The demo project's OCIO config override (the 项目属性 color tab).
|
||||
ocio_config: String,
|
||||
/// The demo project's disk-cache location (setting, custom path).
|
||||
cache_location: (i32, String),
|
||||
/// The demo multicam graph: a real oaknode project whose source
|
||||
/// sequence's video tracks are the angles. Created lazily so the demo
|
||||
/// panel shows a genuine graph behind its synthetic frames — and the
|
||||
@@ -806,6 +810,8 @@ impl MockEngine {
|
||||
proxy_enabled: HashMap::new(),
|
||||
proxy_custom: HashMap::new(),
|
||||
use_proxy: true,
|
||||
ocio_config: String::new(),
|
||||
cache_location: (0, String::new()),
|
||||
multicam_graph: Mutex::new(None),
|
||||
multicam_frames: Mutex::new(HashMap::new()),
|
||||
};
|
||||
@@ -1903,6 +1909,39 @@ impl AppEngine for MockEngine {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn project_ocio_config(&self) -> String {
|
||||
self.ocio_config.clone()
|
||||
}
|
||||
|
||||
fn set_project_ocio_config(&mut self, path: String, cx: &mut Context<Self>) -> Result<(), String> {
|
||||
let trimmed = path.trim().to_string();
|
||||
// Validate like the real engine (a bogus path keeps the dialog open).
|
||||
if !trimmed.is_empty() {
|
||||
oak_render::color::set_up_default_config_from(Some(&trimmed))
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
self.ocio_config = trimmed;
|
||||
cx.notify();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn project_cache_location(&self) -> (i32, String) {
|
||||
self.cache_location.clone()
|
||||
}
|
||||
|
||||
fn set_project_cache_location(&mut self, setting: i32, custom_path: String, cx: &mut Context<Self>) {
|
||||
let setting = setting.clamp(0, 2);
|
||||
self.cache_location = (
|
||||
setting,
|
||||
if setting == 2 {
|
||||
custom_path.trim().to_string()
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn proxy_rows(&self) -> Vec<crate::oakui::engine::ProxyFootageRow> {
|
||||
// Every explorer footage entry gets a row; audio-only entries keep
|
||||
// `can_generate` off (the proxy pipeline is video-only).
|
||||
|
||||
@@ -444,6 +444,10 @@ struct ThumbnailState {
|
||||
pending: HashSet<u64>,
|
||||
}
|
||||
|
||||
/// The project settings key of the OCIO config override (the 项目属性
|
||||
/// color tab).
|
||||
pub const PROJECT_SETTING_OCIO_CONFIG: &str = "ocioconfig";
|
||||
|
||||
/// The shared directory holding generated footage thumbnails.
|
||||
fn thumbnail_dir() -> PathBuf {
|
||||
std::env::temp_dir().join("oak-thumbnails")
|
||||
@@ -451,13 +455,18 @@ fn thumbnail_dir() -> PathBuf {
|
||||
|
||||
/// The PNG path of a footage's thumbnail: an FNV-1a hash of the media
|
||||
/// filename, so the same file always hits the same cached PNG.
|
||||
fn thumbnail_path(filename: &str) -> PathBuf {
|
||||
/// `cache_dir` is the project's cache-location override (the 项目属性
|
||||
/// disk-cache setting); `None` uses the shared default directory.
|
||||
fn thumbnail_path(cache_dir: Option<&str>, filename: &str) -> PathBuf {
|
||||
let mut h: u64 = 0xcbf29ce484222325;
|
||||
for b in filename.as_bytes() {
|
||||
h ^= u64::from(*b);
|
||||
h = h.wrapping_mul(0x100000001b3);
|
||||
}
|
||||
thumbnail_dir().join(format!("{h:016x}.png"))
|
||||
let dir = cache_dir
|
||||
.map(|d| PathBuf::from(d).join("thumbnails"))
|
||||
.unwrap_or_else(thumbnail_dir);
|
||||
dir.join(format!("{h:016x}.png"))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1920,18 +1929,22 @@ impl RealEngine {
|
||||
});
|
||||
}
|
||||
|
||||
/// Renders `identity`'s footage first frame into the shared thumbnail
|
||||
/// Renders `identity`'s footage first frame into the thumbnail
|
||||
/// directory and returns the PNG path (`None` when the entry is not a
|
||||
/// renderable footage or the render fails).
|
||||
/// renderable footage or the render fails). The directory follows the
|
||||
/// project's disk-cache location when it overrides the default (the
|
||||
/// 项目属性 disk-cache setting's live consumer).
|
||||
fn render_thumbnail(project: &ProjectRef, identity: u64) -> Option<PathBuf> {
|
||||
let node = graphops::id_of(identity)?;
|
||||
let filename = {
|
||||
let (filename, cache_dir) = {
|
||||
let guard = graphops::lock(project);
|
||||
graphops::footage_behavior(&guard.graph, node)
|
||||
let filename = graphops::footage_behavior(&guard.graph, node)
|
||||
.map(|f| f.filename.clone())
|
||||
.filter(|f| !f.is_empty())?
|
||||
.filter(|f| !f.is_empty())?;
|
||||
let dir = (guard.cache_location_setting != 0).then(|| guard.cache_path());
|
||||
(filename, dir)
|
||||
};
|
||||
let path = thumbnail_path(&filename);
|
||||
let path = thumbnail_path(cache_dir.as_deref(), &filename);
|
||||
if path.exists() {
|
||||
return Some(path);
|
||||
}
|
||||
@@ -2531,6 +2544,10 @@ impl RealEngine {
|
||||
self.refresh_sequence_info();
|
||||
self.rebuild_timeline();
|
||||
|
||||
// The project's stored OCIO override (if any) drives the display
|
||||
// color pipeline from here on.
|
||||
Self::apply_project_color_config(Some(&project));
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -2573,6 +2590,31 @@ impl RealEngine {
|
||||
name: UNTITLED.into(),
|
||||
path: PathBuf::new(),
|
||||
};
|
||||
// The dropped project's OCIO override leaves with it: the display
|
||||
// color pipeline returns to the app default config.
|
||||
Self::apply_project_color_config(None);
|
||||
}
|
||||
|
||||
/// Applies a project's stored OCIO override (`Some(project)`) or
|
||||
/// restores the app default config (`None`, or a project without an
|
||||
/// override). A config that fails to load falls back to the app default
|
||||
/// (the file may have moved since the project was saved).
|
||||
fn apply_project_color_config(project: Option<&ProjectRef>) {
|
||||
let stored = project.and_then(|p| {
|
||||
graphops::lock(p)
|
||||
.settings
|
||||
.get(PROJECT_SETTING_OCIO_CONFIG)
|
||||
.cloned()
|
||||
});
|
||||
let applied = match stored.as_deref() {
|
||||
None => oak_render::color::set_up_default_config(),
|
||||
Some(path) => oak_render::color::set_up_default_config_from(Some(path)),
|
||||
};
|
||||
if let Err(e) = applied {
|
||||
println!("[real engine] project OCIO config apply failed: {e}");
|
||||
let _ = oak_render::color::set_up_default_config();
|
||||
}
|
||||
super::displaycolor::invalidate();
|
||||
}
|
||||
|
||||
/// Refreshes the cached `Sequence` (name / format / length) from the
|
||||
@@ -4345,6 +4387,78 @@ impl AppEngine for RealEngine {
|
||||
self.invalidate_preview_frames(cx);
|
||||
}
|
||||
|
||||
/// The project's OCIO config override (the 项目属性 color tab; "" = the
|
||||
/// app default config).
|
||||
fn project_ocio_config(&self) -> String {
|
||||
self.project_ref()
|
||||
.map(|p| {
|
||||
graphops::lock(p)
|
||||
.settings
|
||||
.get(PROJECT_SETTING_OCIO_CONFIG)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn set_project_ocio_config(&mut self, path: String, cx: &mut Context<Self>) -> Result<(), String> {
|
||||
let Some(project) = self.project.clone() else {
|
||||
return Err("no project open".to_string());
|
||||
};
|
||||
let trimmed = path.trim().to_string();
|
||||
// Validate first — the dialog stays open on Err (the C++ accept()
|
||||
// refuses an invalid config the same way). Applying is the
|
||||
// process-wide color config reload plus a full frame invalidation.
|
||||
if trimmed.is_empty() {
|
||||
oak_render::color::set_up_default_config().map_err(|e| e.to_string())?;
|
||||
} else {
|
||||
oak_render::color::set_up_default_config_from(Some(&trimmed))
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
{
|
||||
let mut guard = graphops::lock(&project);
|
||||
if trimmed.is_empty() {
|
||||
guard.settings.remove(PROJECT_SETTING_OCIO_CONFIG);
|
||||
} else {
|
||||
guard
|
||||
.settings
|
||||
.insert(PROJECT_SETTING_OCIO_CONFIG.to_string(), trimmed);
|
||||
}
|
||||
guard.modified = true;
|
||||
}
|
||||
super::displaycolor::invalidate();
|
||||
self.invalidate_rendered_frames();
|
||||
cx.notify();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn project_cache_location(&self) -> (i32, String) {
|
||||
self.project_ref()
|
||||
.map(|p| {
|
||||
let g = graphops::lock(p);
|
||||
// Clamp: the dialog's combo indexes by this value.
|
||||
(g.cache_location_setting.clamp(0, 2), g.custom_cache_path.clone())
|
||||
})
|
||||
.unwrap_or((0, String::new()))
|
||||
}
|
||||
|
||||
fn set_project_cache_location(&mut self, setting: i32, custom_path: String, cx: &mut Context<Self>) {
|
||||
let Some(project) = self.project.clone() else {
|
||||
return;
|
||||
};
|
||||
{
|
||||
let mut guard = graphops::lock(&project);
|
||||
guard.cache_location_setting = setting.clamp(0, 2);
|
||||
guard.custom_cache_path = if guard.cache_location_setting == 2 {
|
||||
custom_path.trim().to_string()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
guard.modified = true;
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn proxy_rows(&self) -> Vec<super::engine::ProxyFootageRow> {
|
||||
let Some(project) = self.project.as_ref() else {
|
||||
return Vec::new();
|
||||
|
||||
@@ -307,8 +307,27 @@ pub fn save(project: &Project) -> crate::error::Result<String> {
|
||||
let mut keys: Vec<&String> = project.settings.keys().collect();
|
||||
keys.sort();
|
||||
for key in keys {
|
||||
// The cache location lives on the struct fields; any settings-map
|
||||
// copies (from a load) are skipped — the fields win on save.
|
||||
if key == crate::project::SETTING_CACHE_LOCATION
|
||||
|| key == crate::project::SETTING_CACHE_PATH
|
||||
{
|
||||
continue;
|
||||
}
|
||||
writer.text_element(key, project.settings.get(key).unwrap_or(&String::new()));
|
||||
}
|
||||
// Persist the cache location as settings entries (the C++
|
||||
// k_cache_location_setting_key / k_cache_path_key); defaults add no
|
||||
// noise to the file.
|
||||
if project.cache_location_setting != 0 {
|
||||
writer.text_element(
|
||||
crate::project::SETTING_CACHE_LOCATION,
|
||||
&project.cache_location_setting.to_string(),
|
||||
);
|
||||
}
|
||||
if !project.custom_cache_path.is_empty() {
|
||||
writer.text_element(crate::project::SETTING_CACHE_PATH, &project.custom_cache_path);
|
||||
}
|
||||
writer.end_element(); // settings
|
||||
|
||||
writer.end_element(); // project
|
||||
@@ -604,6 +623,20 @@ fn load_project_body(reader: &mut dyn XmlRead, project: &mut Project) -> crate::
|
||||
}
|
||||
}
|
||||
|
||||
// The cache location arrives as settings entries (see the save side).
|
||||
if let Some(setting) = project
|
||||
.settings
|
||||
.get(crate::project::SETTING_CACHE_LOCATION)
|
||||
.and_then(|s| s.parse::<i32>().ok())
|
||||
{
|
||||
// Clamp to the known CacheSetting range: a hand-edited file must not
|
||||
// poison the dialog's combo selection.
|
||||
project.cache_location_setting = setting.clamp(0, 2);
|
||||
}
|
||||
if let Some(path) = project.settings.get(crate::project::SETTING_CACHE_PATH) {
|
||||
project.custom_cache_path = path.clone();
|
||||
}
|
||||
|
||||
// Rebuild the timeline structure: the custom segments carry packed
|
||||
// references that only resolve now that every node is live.
|
||||
resolve_timeline_refs(&mut project.graph, &id_map);
|
||||
|
||||
@@ -412,10 +412,18 @@ pub fn default_config() -> Option<std::sync::Arc<SafeConfig>> {
|
||||
/// Load the process-wide default config from $OCIO or the bundled config
|
||||
/// (C++ `ColorManager::SetUpDefaultConfig`).
|
||||
pub fn set_up_default_config() -> Result<()> {
|
||||
let config = match std::env::var("OCIO") {
|
||||
Ok(path) if !path.is_empty() => ocio_rs::Config::from_file(&path)
|
||||
.map_err(|e| Error::Failed(format!("load $OCIO config: {e}")))?,
|
||||
_ => ocio_rs::Config::create_from_builtin_config("default")
|
||||
let env = std::env::var("OCIO").ok().filter(|p| !p.is_empty());
|
||||
set_up_default_config_from(env.as_deref())
|
||||
}
|
||||
|
||||
/// Load the process-wide default config from an explicit path (`None` =
|
||||
/// the bundled default config). The project-properties OCIO override and
|
||||
/// the `$OCIO` startup path both land here.
|
||||
pub fn set_up_default_config_from(path: Option<&str>) -> Result<()> {
|
||||
let config = match path {
|
||||
Some(path) => ocio_rs::Config::from_file(path)
|
||||
.map_err(|e| Error::Failed(format!("load OCIO config \"{path}\": {e}")))?,
|
||||
None => ocio_rs::Config::create_from_builtin_config("default")
|
||||
.or_else(|_| ocio_rs::Config::create_from_builtin_config("ocio-2.2-default"))
|
||||
.map_err(|e| Error::Failed(format!("load bundled config: {e}")))?,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user