Add dialog for saving all unsaved tabs when closing app, fixes #169

This commit is contained in:
Jeremy Soller 2024-05-15 10:20:40 -06:00
parent 4716c2242b
commit e26175b28d
No known key found for this signature in database
GPG key ID: D02FD439211AF56F
2 changed files with 140 additions and 32 deletions

View file

@ -31,6 +31,7 @@ project-search = Project search
prompt-save-changes-title = Unsaved changes prompt-save-changes-title = Unsaved changes
prompt-unsaved-changes = You have unsaved changes. Save? prompt-unsaved-changes = You have unsaved changes. Save?
discard = Discard changes discard = Discard changes
save-all = Save all
## Settings ## Settings
settings = Settings settings = Settings

View file

@ -169,6 +169,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut settings = Settings::default(); let mut settings = Settings::default();
settings = settings.theme(config.app_theme.theme()); settings = settings.theme(config.app_theme.theme());
settings = settings.size_limits(Limits::NONE.min_width(360.0).min_height(180.0)); settings = settings.size_limits(Limits::NONE.min_width(360.0).min_height(180.0));
settings = settings.exit_on_close(false);
let flags = Flags { let flags = Flags {
config_handler, config_handler,
@ -228,7 +229,7 @@ pub enum Action {
impl MenuAction for Action { impl MenuAction for Action {
type Message = Message; type Message = Message;
fn message(&self, _entity: Option<Entity>) -> Message { fn message(&self, entity_opt: Option<Entity>) -> Message {
match self { match self {
Self::Todo => Message::Todo, Self::Todo => Message::Todo,
Self::About => Message::ToggleContextPage(ContextPage::About), Self::About => Message::ToggleContextPage(ContextPage::About),
@ -247,8 +248,8 @@ impl MenuAction for Action {
Self::Paste => Message::Paste, Self::Paste => Message::Paste,
Self::Quit => Message::Quit, Self::Quit => Message::Quit,
Self::Redo => Message::Redo, Self::Redo => Message::Redo,
Self::Save => Message::Save, Self::Save => Message::Save(entity_opt),
Self::SaveAsDialog => Message::SaveAsDialog, Self::SaveAsDialog => Message::SaveAsDialog(entity_opt),
Self::SelectAll => Message::SelectAll, Self::SelectAll => Message::SelectAll,
Self::TabActivate0 => Message::TabActivateJump(0), Self::TabActivate0 => Message::TabActivateJump(0),
Self::TabActivate1 => Message::TabActivateJump(1), Self::TabActivate1 => Message::TabActivateJump(1),
@ -352,9 +353,11 @@ pub enum Message {
ProjectSearchValue(String), ProjectSearchValue(String),
PromptSaveChanges(segmented_button::Entity), PromptSaveChanges(segmented_button::Entity),
Quit, Quit,
QuitForce,
Redo, Redo,
Save, Save(Option<segmented_button::Entity>),
SaveAsDialog, SaveAll,
SaveAsDialog(Option<segmented_button::Entity>),
SaveAsResult(segmented_button::Entity, DialogResult), SaveAsResult(segmented_button::Entity, DialogResult),
SelectAll, SelectAll,
SystemThemeModeChange(cosmic_theme::ThemeMode), SystemThemeModeChange(cosmic_theme::ThemeMode),
@ -403,9 +406,10 @@ impl ContextPage {
} }
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
enum DialogPage { enum DialogPage {
PromptSave(segmented_button::Entity), PromptSaveClose(segmented_button::Entity),
PromptSaveQuit(Vec<segmented_button::Entity>),
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
@ -653,6 +657,41 @@ impl App {
} }
} }
fn update_dialogs(&mut self) -> Command<Message> {
match self.dialog_page_opt {
Some(DialogPage::PromptSaveClose(entity)) => {
if let Some(Tab::Editor(tab)) = self.tab_model.data::<Tab>(entity) {
if !tab.changed() {
// Tab has been saved, close it (which also closes this dialog)
return self.update(Message::TabCloseForce(entity));
}
} else {
// Tab no longer found, close dialog
self.dialog_page_opt = None;
}
}
Some(DialogPage::PromptSaveQuit(ref _entities)) => {
let mut unsaved = Vec::new();
for entity in self.tab_model.iter() {
if let Some(Tab::Editor(tab)) = self.tab_model.data::<Tab>(entity) {
if tab.changed() {
unsaved.push(entity);
}
}
}
if unsaved.is_empty() {
// All tabs have been saved, we can exit
return self.update(Message::QuitForce);
} else {
// Update dialog
self.dialog_page_opt = Some(DialogPage::PromptSaveQuit(unsaved));
}
}
None => {}
}
Command::none()
}
fn update_focus(&self) -> Command<Message> { fn update_focus(&self) -> Command<Message> {
if self.core.window.show_context { if self.core.window.show_context {
match self.context_page { match self.context_page {
@ -1333,6 +1372,10 @@ impl Application for App {
Some(&self.nav_model) Some(&self.nav_model)
} }
fn on_app_exit(&mut self) -> Option<Message> {
Some(Message::Quit)
}
fn on_context_drawer(&mut self) -> Command<Message> { fn on_context_drawer(&mut self) -> Command<Message> {
// Focus correct widget // Focus correct widget
self.update_focus() self.update_focus()
@ -1402,24 +1445,27 @@ impl Application for App {
} }
fn dialog(&self) -> Option<Element<Self::Message>> { fn dialog(&self) -> Option<Element<Self::Message>> {
let Some(dialog) = self.dialog_page_opt else { let Some(ref dialog) = self.dialog_page_opt else {
return None; return None;
}; };
let cosmic_theme::Spacing { space_xxs, .. } = self.core().system_theme().cosmic().spacing;
match dialog { match dialog {
DialogPage::PromptSave(entity) => { DialogPage::PromptSaveClose(entity) => {
// "Save" is displayed regardless if the file was already saved because the message handles // "Save" is displayed regardless if the file was already saved because the message handles
// "Save As" if necessary // "Save As" if necessary
let save = fl!("save"); let save = fl!("save");
let save_button = widget::button::suggested(save).on_press(Message::Save); let save_button =
widget::button::suggested(save).on_press(Message::Save(Some(*entity)));
// "Save As" is only shown if the file has been saved previously // "Save As" is only shown if the file has been saved previously
// Rationale: The user may want to save the modified buffer as a new file // Rationale: The user may want to save the modified buffer as a new file
let save_as_button = match self.tab_model.data(entity) { let save_as_button = match self.tab_model.data(*entity) {
Some(Tab::Editor(tab)) if tab.path_opt.is_some() => { Some(Tab::Editor(tab)) if tab.path_opt.is_some() => {
let save_as = fl!("save-as"); let save_as = fl!("save-as");
let save_as_button = let save_as_button = widget::button::suggested(save_as)
widget::button::suggested(save_as).on_press(Message::SaveAsDialog); .on_press(Message::SaveAsDialog(Some(*entity)));
Some(save_as_button) Some(save_as_button)
} }
_ => None, _ => None,
@ -1428,7 +1474,7 @@ impl Application for App {
// Discards unsaved changes // Discards unsaved changes
let discard = fl!("discard"); let discard = fl!("discard");
let discard_button = let discard_button =
widget::button::destructive(discard).on_press(Message::TabCloseForce(entity)); widget::button::destructive(discard).on_press(Message::TabCloseForce(*entity));
let mut dialog = widget::dialog(fl!("prompt-save-changes-title")) let mut dialog = widget::dialog(fl!("prompt-save-changes-title"))
.body(fl!("prompt-unsaved-changes")) .body(fl!("prompt-unsaved-changes"))
@ -1442,6 +1488,47 @@ impl Application for App {
dialog.secondary_action(discard_button) dialog.secondary_action(discard_button)
}; };
Some(dialog.into())
}
DialogPage::PromptSaveQuit(entities) => {
let mut can_save_all = true;
let mut column = widget::column::with_capacity(entities.len()).spacing(space_xxs);
for entity in entities.iter() {
if let Some(Tab::Editor(tab)) = self.tab_model.data::<Tab>(*entity) {
let mut row = widget::row::with_capacity(3).align_items(Alignment::Center);
row = row.push(widget::text(tab.title()));
row = row.push(widget::horizontal_space(Length::Fill));
if let Some(path) = &tab.path_opt {
row = row.push(
widget::button::standard(fl!("save"))
.on_press(Message::Save(Some(*entity))),
);
//TODO row = row.push(widget::text(format!("{}", path.display())));
} else {
row = row.push(
widget::button::standard(fl!("save-as"))
.on_press(Message::SaveAsDialog(Some(*entity))),
);
can_save_all = false;
}
column = column.push(row);
}
}
let mut save_button = widget::button::suggested(fl!("save-all"));
if can_save_all {
save_button = save_button.on_press(Message::SaveAll);
}
let discard_button =
widget::button::destructive(fl!("discard")).on_press(Message::QuitForce);
let dialog = widget::dialog(fl!("prompt-save-changes-title"))
.body(fl!("prompt-unsaved-changes"))
.icon(icon::from_name("dialog-warning-symbolic").size(64))
.control(column)
.primary_action(save_button)
.secondary_action(discard_button);
Some(dialog.into()) Some(dialog.into())
} }
} }
@ -2064,11 +2151,16 @@ impl Application for App {
self.project_search_value = value; self.project_search_value = value;
} }
Message::PromptSaveChanges(entity) => { Message::PromptSaveChanges(entity) => {
self.dialog_page_opt = Some(DialogPage::PromptSave(entity)); self.dialog_page_opt = Some(DialogPage::PromptSaveClose(entity));
} }
Message::Quit => { Message::Quit => {
//TODO: prompt for save? // Create empty dialog
return window::close(window::Id::MAIN); self.dialog_page_opt = Some(DialogPage::PromptSaveQuit(Vec::new()));
// This update will get the actual list of unsaved tabs
return self.update_dialogs();
}
Message::QuitForce => {
process::exit(0);
} }
Message::Redo => { Message::Redo => {
if let Some(Tab::Editor(tab)) = self.active_tab() { if let Some(Tab::Editor(tab)) = self.active_tab() {
@ -2080,24 +2172,37 @@ impl Application for App {
return self.update(Message::TabChanged(self.tab_model.active())); return self.update(Message::TabChanged(self.tab_model.active()));
} }
} }
Message::Save => { Message::Save(entity_opt) => {
let mut title_opt = None; let mut title_opt = None;
if let Some(Tab::Editor(tab)) = self.active_tab_mut() { let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
if let Some(Tab::Editor(tab)) = self.tab_model.data_mut::<Tab>(entity) {
if tab.path_opt.is_none() { if tab.path_opt.is_none() {
return self.update(Message::SaveAsDialog); return self.update(Message::SaveAsDialog(Some(entity)));
} }
title_opt = Some(tab.title()); title_opt = Some(tab.title());
tab.save(); tab.save();
} }
if let Some(title) = title_opt { if let Some(title) = title_opt {
self.tab_model.text_set(self.tab_model.active(), title); self.tab_model.text_set(self.tab_model.active(), title);
} }
return self.update_dialogs();
} }
Message::SaveAsDialog => { Message::SaveAll => {
let entities: Vec<_> = self.tab_model.iter().collect();
for entity in entities {
if let Some(Tab::Editor(tab)) = self.tab_model.data_mut::<Tab>(entity) {
if tab.path_opt.is_none() {
log::warn!("{} has no path when doing save all", tab.title());
}
tab.save();
}
}
return self.update_dialogs();
}
Message::SaveAsDialog(entity_opt) => {
if self.dialog_opt.is_none() { if self.dialog_opt.is_none() {
let entity = self.tab_model.active(); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
if let Some(Tab::Editor(tab)) = self.tab_model.data::<Tab>(entity) { if let Some(Tab::Editor(tab)) = self.tab_model.data::<Tab>(entity) {
let (filename, path_opt) = match &tab.path_opt { let (filename, path_opt) = match &tab.path_opt {
Some(path) => ( Some(path) => (
@ -2135,10 +2240,7 @@ impl Application for App {
if let Some(title) = title_opt { if let Some(title) = title_opt {
self.tab_model.text_set(entity, title); self.tab_model.text_set(entity, title);
} }
// Close save changes prompt only if the dialog succeeded return self.update_dialogs();
if self.dialog_page_opt == Some(DialogPage::PromptSave(entity)) {
self.dialog_page_opt = None;
}
} }
} }
} }
@ -2177,7 +2279,7 @@ impl Application for App {
}, },
Message::TabActivate(entity) => { Message::TabActivate(entity) => {
// Close save changes dialog if switching to a different tab for consistency // Close save changes dialog if switching to a different tab for consistency
if self.dialog_page_opt != Some(DialogPage::PromptSave(entity)) { if self.dialog_page_opt != Some(DialogPage::PromptSaveClose(entity)) {
self.dialog_page_opt = None; self.dialog_page_opt = None;
} }
@ -2216,9 +2318,9 @@ impl Application for App {
// The save prompt shouldn't be closed if `TabClose` is emitted again for // The save prompt shouldn't be closed if `TabClose` is emitted again for
// the same tab. // the same tab.
// //
// `PromptSave` for a different tab other than `entity` counts as // `PromptSaveClose` for a different tab other than `entity` counts as
// a different dialog // a different dialog
// Ex. If tab 2 and 3 both have unsaved changes and `PromptSave` is // Ex. If tab 2 and 3 both have unsaved changes and `PromptSaveClose` is
// emitted for tab 2, closing tab 3 should open the dialog for tab 3 in // emitted for tab 2, closing tab 3 should open the dialog for tab 3 in
// order for `Message::Save` to save the correct tab. // order for `Message::Save` to save the correct tab.
return Command::batch([ return Command::batch([
@ -2252,8 +2354,8 @@ impl Application for App {
self.open_tab(None); self.open_tab(None);
} }
// Close PromptSave dialog if open for this entity // Close PromptSaveClose dialog if open for this entity
if self.dialog_page_opt == Some(DialogPage::PromptSave(entity)) { if self.dialog_page_opt == Some(DialogPage::PromptSaveClose(entity)) {
self.dialog_page_opt = None; self.dialog_page_opt = None;
} }
@ -2754,6 +2856,11 @@ impl Application for App {
event::Event::Window(id, window::Event::Focused) if id == window::Id::MAIN => { event::Event::Window(id, window::Event::Focused) if id == window::Id::MAIN => {
Some(Message::Focus) Some(Message::Focus)
} }
event::Event::Window(id, window::Event::CloseRequested)
if id == window::Id::MAIN =>
{
Some(Message::Quit)
}
_ => None, _ => None,
}), }),
subscription::channel( subscription::channel(