project file: format dialog hands to the SYSTEM dialog (extension-limited)

- 导出工程文件: the in-app dialog only picks the format (OTIO / OVE /
  FCPXML); OK asks the system save dialog with the suggested file name
  carrying the format's extension, then exports
- 打开项目: the system open dialog restricts the choosable files to
  .ove/.ovexml/.otio/.fcpxml (PathPromptOptions.allowed_extensions
  plumbed through gpui; Linux portal glob filter)
This commit is contained in:
2026-09-02 12:00:39 +08:00
parent 22a17442ac
commit fe017aa060
5 changed files with 45 additions and 31 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
name: CD
kiname: CD
on:
push:
+38 -29
View File
@@ -424,6 +424,8 @@ pub struct OakApp<E: AppEngine> {
plugin_progress_rx: Mutex<mpsc::Receiver<crate::oakui::ofx::PluginProgressEvent>>,
/// The library row pending an export save dialog (manager 导出).
pending_export: Option<String>,
/// The suggested file stem for the export-project system save dialog.
export_suggested_name: String,
/// The panel that most recently took focus (the target of
/// [`Route::FocusedPanel`](crate::actions::Route::FocusedPanel)
/// commands; `None` before any panel is focused).
@@ -843,6 +845,7 @@ impl<E: AppEngine> OakApp<E> {
export: None,
pending_drop: None,
pending_export: None,
export_suggested_name: String::new(),
plugin_progress_open: false,
plugin_progress_rx: Mutex::new(plugin_progress_rx),
focused_panel: None,
@@ -2018,6 +2021,17 @@ impl<E: AppEngine> OakApp<E> {
directories: false,
multiple: action == FileAction::ImportFootage,
prompt: Some(prompt.into()),
allowed_extensions: if action == FileAction::Open {
// Project files only: OVE / OTIO / FCPXML.
vec![
"ove".to_string(),
"ovexml".to_string(),
"otio".to_string(),
"fcpxml".to_string(),
]
} else {
Vec::new()
},
});
cx.spawn(async move |this, cx| {
if let Ok(Ok(Some(paths))) = receiver.await {
@@ -2615,8 +2629,9 @@ impl<E: AppEngine> OakApp<E> {
}
/// Opens the 导出工程文件 dialog: choose the project file format
/// (OTIO / OVE / FCPXML) and the output path; the OK button runs the
/// engine's export_project_path with the picked extension.
/// (OTIO / OVE / FCPXML); the OK button then asks the SYSTEM save
/// dialog for the output path (the suggested file name carries the
/// chosen extension).
fn open_export_project_dfd(&mut self, cx: &mut Context<Self>) {
if !matches!(self.modal, ModalState::None) {
return;
@@ -2633,15 +2648,11 @@ impl<E: AppEngine> OakApp<E> {
.map(|name| name.to_string_lossy().into_owned())
})
.unwrap_or_else(|| "untitled".to_string());
self.export_suggested_name = suggested;
self.spawn_modal(cx, move |window, app| {
let content = app.new(|cx| {
crate::dialogs::ExportProjectDialogContent::new(window, cx)
});
// Default selection is OTIO: pre-fill the pathway with the
// correct extension, the OK handler applies the chosen one.
content.update(app, |content, cx| {
content.set_path(format!("{suggested}.otio"), cx)
});
let modal = app.new(|cx| {
Modal::new(
modal_ids::EXPORT_PROJECT_DFD,
@@ -3128,35 +3139,33 @@ impl<E: AppEngine> OakApp<E> {
modal_ids::EXPORT_PROJECT_DFD => {
if let ModalState::ExportProjectDfd { content, .. } = &self.modal {
if *button == 0 {
// OK: export with the chosen format the path
// gets the format's extension appended.
// OK: keep the chosen format; the SYSTEM save
// dialog then picks the output path (the
// suggested name carries the format's
// extension).
let format = content.read(cx).format(cx);
let ext = match format {
crate::dialogs::PROJECT_FORMAT_OVE => "ove",
crate::dialogs::PROJECT_FORMAT_FCPXML => "fcpxml",
_ => "otio",
};
let path = content.read(cx).path(cx).to_string();
let path = if path.to_ascii_lowercase().ends_with(".otio")
|| path.to_ascii_lowercase().ends_with(".ove")
|| path.to_ascii_lowercase().ends_with(".fcpxml")
{
// Stem swap: the entered name already has
// an extension — replace it.
let stem = path
.split('.')
.next()
.unwrap_or(&path)
.to_string();
format!("{stem}.{ext}")
} else {
format!("{path}.{ext}")
};
let engine = self.engine.clone();
engine.update(cx, |engine, cx| {
engine.export_project_path(PathBuf::from(path), cx)
});
let suggested = self.export_suggested_name.clone();
let receiver = cx.prompt_for_new_path(
std::path::Path::new("."),
Some(&format!("{suggested}.{ext}")),
);
self.close_modal(cx);
cx.spawn(async move |this, cx| {
if let Ok(Ok(Some(path))) = receiver.await {
this.update(cx, |this, cx| {
let engine = this.engine.clone();
engine.update(cx, |engine, cx| {
engine.export_project_path(path, cx)
});
});
}
})
.detach();
} else {
self.close_modal(cx);
}
+4
View File
@@ -585,6 +585,7 @@ impl PreferencesContent {
directories: false,
multiple: false,
prompt: Some(i18n::tr("preferences.color.browse").into()),
allowed_extensions: Vec::new(),
});
cx.spawn(async move |this, cx| {
let Ok(Ok(Some(paths))) = receiver.await else {
@@ -623,6 +624,7 @@ impl PreferencesContent {
directories: true,
multiple: false,
prompt: Some(i18n::tr("preferences.cache.browse").into()),
allowed_extensions: Vec::new(),
});
cx.spawn(async move |this, cx| {
let Ok(Ok(Some(paths))) = receiver.await else {
@@ -2095,6 +2097,7 @@ impl<E: crate::oakui::engine::AppEngine> Render for ProjectPropertiesContent<E>
directories: false,
multiple: false,
prompt: None,
allowed_extensions: Vec::new(),
});
cx.spawn(async move |this, cx| {
if let Ok(Ok(Some(paths))) = receiver.await {
@@ -2569,6 +2572,7 @@ impl KeyboardTabContent {
directories: false,
multiple: false,
prompt: Some(i18n::tr("preferences.keyboard.import").into()),
allowed_extensions: Vec::new(),
});
cx.spawn(async move |this, cx| {
let Ok(Ok(Some(paths))) = receiver.await else {
@@ -142,6 +142,7 @@ impl<E: AppEngine> ProjectExplorerPanel<E> {
directories: false,
multiple: false,
prompt: Some(crate::i18n::tr("project.context.replace_footage").into()),
allowed_extensions: Vec::new(),
});
cx.spawn(async move |this, cx| {
if let Ok(Ok(Some(paths))) = receiver.await {
+1 -1
Submodule gpui updated: 209c593da6...a934be5bc6