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:
2026-08-25 00:45:04 +08:00
parent f7b5996032
commit 476114cec0
15 changed files with 809 additions and 18 deletions
+33
View File
@@ -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);