yoda: add "Always use this app" toggle to OpenWith dialog

The 'Open with...' dialog let you pick an app but never remembered your
choice — you'd see the same dialog again next time. The infrastructure
was already there (MimeAppCache::set_default writes to mimeapps.list),
just never wired to the UI.

Adds a toggler below the app list labelled 'Always use this app for this
file type' (EN) / 'Toujours utiliser cette application pour ce type de
fichier' (FR). When enabled, after spawning the selected app, the
default handler for the file's mime type is persisted via
self.mime_app_cache.set_default(mime, app.id).

Implementation:
- DialogPage::OpenWith gains a set_default: bool field (defaulted false)
- Message::OpenWithToggleDefault(bool) + handler mutates the dialog state
- DialogComplete handler for OpenWith calls set_default after a clean
  spawn when the flag is set
- Dialog rendering adds a .control(widget::row) with label + toggler,
  between the scrollable list and the action buttons
- i18n strings added: en/fr open-with-set-default
This commit is contained in:
Lionel DARNIS 2026-04-23 20:18:21 +02:00
parent a025fd6380
commit e8d62ae43d
3 changed files with 37 additions and 2 deletions

View file

@ -96,6 +96,7 @@ save-file = Save file
## Open With Dialog ## Open With Dialog
open-with-title = How do you want to open "{$name}"? open-with-title = How do you want to open "{$name}"?
open-with-set-default = Always use this app for this file type
browse-store = Browse {$store} browse-store = Browse {$store}
other-apps = Other applications other-apps = Other applications
related-apps = Related applications related-apps = Related applications

View file

@ -92,6 +92,7 @@ save-file = Enregistrer fichier
## Open With Dialog ## Open With Dialog
open-with-title = Comment souhaitez-vous ouvrir "{ $name }"? open-with-title = Comment souhaitez-vous ouvrir "{ $name }"?
open-with-set-default = Toujours utiliser cette application pour ce type de fichier
browse-store = Parcourir { $store } browse-store = Parcourir { $store }
## Permanently delete Dialog ## Permanently delete Dialog

View file

@ -407,6 +407,7 @@ pub enum Message {
OpenWithBrowse, OpenWithBrowse,
OpenWithDialog(Option<Entity>), OpenWithDialog(Option<Entity>),
OpenWithSelection(usize), OpenWithSelection(usize),
OpenWithToggleDefault(bool),
#[cfg(all(feature = "wayland", feature = "desktop-applet"))] #[cfg(all(feature = "wayland", feature = "desktop-applet"))]
Overlap(window::Id, OverlapNotifyEvent), Overlap(window::Id, OverlapNotifyEvent),
Paste(Option<Entity>), Paste(Option<Entity>),
@ -577,6 +578,9 @@ pub enum DialogPage {
mime: mime_guess::Mime, mime: mime_guess::Mime,
selected: usize, selected: usize,
store_opt: Option<MimeApp>, store_opt: Option<MimeApp>,
/// When true, the chosen app is written to mimeapps.list as the
/// default handler for `mime` after it spawns.
set_default: bool,
}, },
PermanentlyDelete { PermanentlyDelete {
paths: Box<[PathBuf]>, paths: Box<[PathBuf]>,
@ -3232,6 +3236,7 @@ impl Application for App {
path, path,
mime, mime,
selected, selected,
set_default,
.. ..
} => { } => {
let available_apps = self.get_apps_for_mime(&mime); let available_apps = self.get_apps_for_mime(&mime);
@ -3250,6 +3255,11 @@ impl Application for App {
None, None,
); );
} }
// Yoda: persist as default if the user asked for it.
if set_default {
self.mime_app_cache
.set_default(mime.clone(), app.id.clone());
}
} }
Err(err) => { Err(err) => {
log::warn!( log::warn!(
@ -3872,6 +3882,7 @@ impl Application for App {
.and_then(|mime| { .and_then(|mime| {
self.mime_app_cache.get(&mime).first().cloned() self.mime_app_cache.get(&mime).first().cloned()
}), }),
set_default: false,
}, },
Some(CONFIRM_OPEN_WITH_BUTTON_ID.clone()), Some(CONFIRM_OPEN_WITH_BUTTON_ID.clone()),
); );
@ -3883,6 +3894,13 @@ impl Application for App {
*selected = index; *selected = index;
} }
} }
Message::OpenWithToggleDefault(enabled) => {
if let Some(DialogPage::OpenWith { set_default, .. }) =
self.dialog_pages.front_mut()
{
*set_default = enabled;
}
}
Message::Paste(entity_opt) => { Message::Paste(entity_opt) => {
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) if let Some(tab) = self.tab_model.data_mut::<Tab>(entity)
@ -5110,6 +5128,7 @@ impl Application for App {
.and_then(|mime| { .and_then(|mime| {
self.mime_app_cache.get(&mime).first().cloned() self.mime_app_cache.get(&mime).first().cloned()
}), }),
set_default: false,
}, },
None, None,
); );
@ -5953,7 +5972,7 @@ impl Application for App {
mime, mime,
selected, selected,
store_opt, store_opt,
.. set_default,
} => { } => {
let name = match path.file_name() { let name = match path.file_name() {
Some(file_name) => file_name.to_str(), Some(file_name) => file_name.to_str(),
@ -6038,7 +6057,21 @@ impl Application for App {
} else { } else {
Length::Shrink Length::Shrink
} }
})); }))
// Yoda: let the user make this choice stick. A plain row
// instead of settings::item::builder because the latter
// returns a section Item, not an Element usable in .control().
.control(
widget::row::with_children([
widget::text::body(fl!("open-with-set-default")).into(),
widget::space::horizontal().into(),
widget::toggler(*set_default)
.on_toggle(Message::OpenWithToggleDefault)
.into(),
])
.spacing(space_s)
.align_y(Alignment::Center),
);
if let Some(app) = store_opt { if let Some(app) = store_opt {
dialog = dialog.tertiary_action( dialog = dialog.tertiary_action(