project file: export dialog with OTIO/OVE/FCPXML format choice
文件 → 导出工程文件… now opens an in-app dialog (format dropdown: OpenTimelineIO / Oak / FCPXML + output path) instead of the bare platform save prompt; OK routes the chosen extension through the engine's export_project_path. Adds project.export.format i18n across the 8 packs.
This commit is contained in:
@@ -374,6 +374,7 @@
|
||||
"actionsearch.no_actions": "Keine Aktionen verfügbar"
|
||||
"export.title": "Sequenz exportieren"
|
||||
"export.format": "Format"
|
||||
"project.export.format": "Projektformat"
|
||||
"export.format.placeholder": "Format auswählen…"
|
||||
"export.path": "Ausgabepfad"
|
||||
"export.run": "Exportieren"
|
||||
|
||||
@@ -372,6 +372,7 @@
|
||||
"actionsearch.no_actions": "No actions available"
|
||||
"export.title": "Export Sequence"
|
||||
"export.format": "Format"
|
||||
"project.export.format": "Project Format"
|
||||
"export.format.placeholder": "Select a format…"
|
||||
"export.path": "Output path"
|
||||
"export.run": "Export"
|
||||
|
||||
@@ -374,6 +374,7 @@
|
||||
"actionsearch.no_actions": "No hay acciones disponibles"
|
||||
"export.title": "Exportar secuencia"
|
||||
"export.format": "Formato"
|
||||
"project.export.format": "Formato del proyecto"
|
||||
"export.format.placeholder": "Seleccionar un formato…"
|
||||
"export.path": "Ruta de salida"
|
||||
"export.run": "Exportar"
|
||||
|
||||
@@ -374,6 +374,7 @@
|
||||
"actionsearch.no_actions": "Aucune action disponible"
|
||||
"export.title": "Exporter la séquence"
|
||||
"export.format": "Format"
|
||||
"project.export.format": "Format du projet"
|
||||
"export.format.placeholder": "Sélectionner un format…"
|
||||
"export.path": "Chemin de sortie"
|
||||
"export.run": "Exporter"
|
||||
|
||||
@@ -374,6 +374,7 @@
|
||||
"actionsearch.no_actions": "利用可能なアクションがありません"
|
||||
"export.title": "シーケンスを書き出し"
|
||||
"export.format": "フォーマット"
|
||||
"project.export.format": "プロジェクト形式"
|
||||
"export.format.placeholder": "フォーマットを選択…"
|
||||
"export.path": "出力先"
|
||||
"export.run": "書き出し"
|
||||
|
||||
@@ -374,6 +374,7 @@
|
||||
"actionsearch.no_actions": "Nenhuma ação disponível"
|
||||
"export.title": "Exportar sequência"
|
||||
"export.format": "Formato"
|
||||
"project.export.format": "Formato do projeto"
|
||||
"export.format.placeholder": "Selecionar um formato…"
|
||||
"export.path": "Caminho de saída"
|
||||
"export.run": "Exportar"
|
||||
|
||||
@@ -374,6 +374,7 @@
|
||||
"actionsearch.no_actions": "Нет доступных действий"
|
||||
"export.title": "Экспорт последовательности"
|
||||
"export.format": "Формат"
|
||||
"project.export.format": "Формат проекта"
|
||||
"export.format.placeholder": "Выберите формат…"
|
||||
"export.path": "Путь вывода"
|
||||
"export.run": "Экспортировать"
|
||||
|
||||
@@ -372,6 +372,7 @@
|
||||
"actionsearch.no_actions": "没有可用动作"
|
||||
"export.title": "导出序列"
|
||||
"export.format": "格式"
|
||||
"project.export.format": "项目格式"
|
||||
"export.format.placeholder": "选择格式…"
|
||||
"export.path": "输出路径"
|
||||
"export.run": "导出"
|
||||
|
||||
@@ -140,6 +140,8 @@ mod modal_ids {
|
||||
pub const MULTICAM_WIZARD: usize = 17;
|
||||
/// The rename dialog (project explorer 重命名).
|
||||
pub const RENAME: usize = 18;
|
||||
/// The project export dialog (文件 → 导出工程文件…).
|
||||
pub const EXPORT_PROJECT_DFD: usize = 19;
|
||||
}
|
||||
|
||||
/// What a picked platform-dialog path should do.
|
||||
@@ -239,6 +241,12 @@ enum ModalState<E: AppEngine> {
|
||||
content: Entity<crate::dialogs::RenameContent>,
|
||||
entry_id: u64,
|
||||
},
|
||||
/// The project export dialog (文件 → 导出工程文件…): choose the
|
||||
/// project format (OTIO / OVE / FCPXML) + the output path.
|
||||
ExportProjectDfd {
|
||||
modal: Entity<Modal>,
|
||||
content: Entity<crate::dialogs::ExportProjectDialogContent>,
|
||||
},
|
||||
}
|
||||
|
||||
/// A running export: the session the tick loop drains for progress.
|
||||
@@ -266,6 +274,7 @@ impl<E: AppEngine> ModalState<E> {
|
||||
| ModalState::DropSequenceChoice { modal, .. }
|
||||
| ModalState::MulticamWizard { modal, .. }
|
||||
| ModalState::EntryRename { modal, .. } => Some(modal.clone()),
|
||||
| ModalState::ExportProjectDfd { modal, .. } => Some(modal.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1014,9 +1023,8 @@ impl<E: AppEngine> OakApp<E> {
|
||||
A::OpenProject => self.open_file_dialog(FileAction::Open, cx),
|
||||
A::OpenFromLibrary | A::ProjectManager => self.show_project_manager(cx),
|
||||
A::Import => self.open_file_dialog(FileAction::ImportFootage, cx),
|
||||
A::SaveProject | A::SaveProjectAs => {
|
||||
self.open_file_dialog(FileAction::ExportProjectFile, cx)
|
||||
}
|
||||
// The 导出工程文件… dialog: choose OTIO / OVE / FCPXML + path.
|
||||
A::SaveProject | A::SaveProjectAs => self.open_export_project_dfd(cx),
|
||||
A::CloseProject => self
|
||||
.engine
|
||||
.update(cx, |engine, cx| engine.close_project(cx)),
|
||||
@@ -2542,6 +2550,55 @@ 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.
|
||||
fn open_export_project_dfd(&mut self, cx: &mut Context<Self>) {
|
||||
if !matches!(self.modal, ModalState::None) {
|
||||
return;
|
||||
}
|
||||
let current = self
|
||||
.engine
|
||||
.read(cx)
|
||||
.project()
|
||||
.map(|p| p.path.clone())
|
||||
.filter(|p| !p.as_os_str().is_empty());
|
||||
let suggested = current
|
||||
.and_then(|path| {
|
||||
path.file_stem()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
})
|
||||
.unwrap_or_else(|| "untitled".to_string());
|
||||
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,
|
||||
ModalOptions::new(
|
||||
crate::i18n::tr("menu.file.export_project"),
|
||||
px(480.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::ExportProjectDfd { modal, content }
|
||||
});
|
||||
}
|
||||
|
||||
/// Opens the multicam wizard (窗口 → 多机位向导): pick the angle
|
||||
/// footage, choose the sync mode, then create the multicam sequence.
|
||||
fn open_multicam_wizard(&mut self, cx: &mut Context<Self>) {
|
||||
@@ -2946,6 +3003,43 @@ 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.
|
||||
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)
|
||||
});
|
||||
self.close_modal(cx);
|
||||
} else {
|
||||
self.close_modal(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
modal_ids::SEQUENCE_PROPERTIES => {
|
||||
if let ModalState::SequenceProperties { content, .. } = &self.modal {
|
||||
let content = content.clone();
|
||||
|
||||
@@ -3857,6 +3857,101 @@ impl Render for RenameContent {
|
||||
}
|
||||
}
|
||||
|
||||
/// The project export dialog (文件 → 导出工程文件…): choose the project
|
||||
/// file format (OTIO / OVE / FCPXML) and the output path. The host reads
|
||||
/// [`Self::format`] + [`Self::path`] on OK.
|
||||
pub struct ExportProjectDialogContent {
|
||||
format: Entity<ComboBox>,
|
||||
path: Entity<PathField>,
|
||||
/// (format id, display name) in dropdown order.
|
||||
formats: Vec<(i32, String)>,
|
||||
}
|
||||
|
||||
/// The project-file format ids (mirror the engine's serializer dispatch;
|
||||
/// the extension is derived on the host).
|
||||
pub const PROJECT_FORMAT_OTIO: i32 = 0;
|
||||
pub const PROJECT_FORMAT_OVE: i32 = 1;
|
||||
pub const PROJECT_FORMAT_FCPXML: i32 = 2;
|
||||
|
||||
impl ExportProjectDialogContent {
|
||||
/// Builds the dialog (OTIO default, empty path).
|
||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let formats: Vec<(i32, String)> = vec![
|
||||
(PROJECT_FORMAT_OTIO, "OpenTimelineIO (.otio)".to_string()),
|
||||
(PROJECT_FORMAT_OVE, "Oak (.ove)".to_string()),
|
||||
(PROJECT_FORMAT_FCPXML, "Final Cut Pro XML (.fcpxml)".to_string()),
|
||||
];
|
||||
let options = formats
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (_, name))| ComboBoxOption::new(i, name.clone()))
|
||||
.collect();
|
||||
let format = cx.new(|cx| {
|
||||
ComboBox::new(4, options, window, cx)
|
||||
.with_placeholder(i18n::tr("project.export.format"))
|
||||
});
|
||||
format.update(cx, |combo, cx| combo.set_selected(Some(PROJECT_FORMAT_OTIO as usize), cx));
|
||||
let path = cx.new(|cx| {
|
||||
let editor = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx));
|
||||
PathField { editor, enabled: true }
|
||||
});
|
||||
Self { format, path, formats }
|
||||
}
|
||||
|
||||
/// The selected project format id.
|
||||
pub fn format(&self, cx: &App) -> i32 {
|
||||
let Some(selected) = self.format.read(cx).selected() else {
|
||||
return PROJECT_FORMAT_OTIO;
|
||||
};
|
||||
self.formats
|
||||
.get(selected)
|
||||
.map(|(id, _)| *id)
|
||||
.unwrap_or(PROJECT_FORMAT_OTIO)
|
||||
}
|
||||
|
||||
/// The format's file extension (no dot).
|
||||
pub fn extension(&self, cx: &App) -> &'static str {
|
||||
match self.format(cx) {
|
||||
PROJECT_FORMAT_OVE => "ove",
|
||||
PROJECT_FORMAT_FCPXML => "fcpxml",
|
||||
_ => "otio",
|
||||
}
|
||||
}
|
||||
|
||||
/// The output path currently entered.
|
||||
pub fn path(&self, cx: &App) -> SharedString {
|
||||
self.path.read(cx).path(cx)
|
||||
}
|
||||
|
||||
/// Pre-fills the output path (the suggested file name).
|
||||
pub fn set_path(&mut self, path: impl Into<SharedString>, cx: &mut Context<Self>) {
|
||||
let path = path.into();
|
||||
self.path.update(cx, |field, cx| field.set_path(path, cx));
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ExportProjectDialogContent {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let colors = cx.default_colors().clone();
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.w_full()
|
||||
.child(form_row(
|
||||
&colors,
|
||||
i18n::tr("project.export.format").into(),
|
||||
self.format.clone(),
|
||||
))
|
||||
.child(form_row(
|
||||
&colors,
|
||||
i18n::tr("export.path").into(),
|
||||
self.path.clone(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user