Skip to content

Commit

Permalink
Add dialog for saving all unsaved tabs when closing app, fixes #169
Browse files Browse the repository at this point in the history
  • Loading branch information
jackpot51 committed May 15, 2024
1 parent 4716c22 commit e26175b
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 32 deletions.
1 change: 1 addition & 0 deletions i18n/en/cosmic_edit.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ project-search = Project search
prompt-save-changes-title = Unsaved changes
prompt-unsaved-changes = You have unsaved changes. Save?
discard = Discard changes
save-all = Save all
## Settings
settings = Settings
Expand Down
171 changes: 139 additions & 32 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut settings = Settings::default();
settings = settings.theme(config.app_theme.theme());
settings = settings.size_limits(Limits::NONE.min_width(360.0).min_height(180.0));
settings = settings.exit_on_close(false);

let flags = Flags {
config_handler,
Expand Down Expand Up @@ -228,7 +229,7 @@ pub enum Action {

impl MenuAction for Action {
type Message = Message;
fn message(&self, _entity: Option<Entity>) -> Message {
fn message(&self, entity_opt: Option<Entity>) -> Message {
match self {
Self::Todo => Message::Todo,
Self::About => Message::ToggleContextPage(ContextPage::About),
Expand All @@ -247,8 +248,8 @@ impl MenuAction for Action {
Self::Paste => Message::Paste,
Self::Quit => Message::Quit,
Self::Redo => Message::Redo,
Self::Save => Message::Save,
Self::SaveAsDialog => Message::SaveAsDialog,
Self::Save => Message::Save(entity_opt),
Self::SaveAsDialog => Message::SaveAsDialog(entity_opt),
Self::SelectAll => Message::SelectAll,
Self::TabActivate0 => Message::TabActivateJump(0),
Self::TabActivate1 => Message::TabActivateJump(1),
Expand Down Expand Up @@ -352,9 +353,11 @@ pub enum Message {
ProjectSearchValue(String),
PromptSaveChanges(segmented_button::Entity),
Quit,
QuitForce,
Redo,
Save,
SaveAsDialog,
Save(Option<segmented_button::Entity>),
SaveAll,
SaveAsDialog(Option<segmented_button::Entity>),
SaveAsResult(segmented_button::Entity, DialogResult),
SelectAll,
SystemThemeModeChange(cosmic_theme::ThemeMode),
Expand Down Expand Up @@ -403,9 +406,10 @@ impl ContextPage {
}
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[derive(Clone, Debug, Eq, PartialEq)]
enum DialogPage {
PromptSave(segmented_button::Entity),
PromptSaveClose(segmented_button::Entity),
PromptSaveQuit(Vec<segmented_button::Entity>),
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
Expand Down Expand Up @@ -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> {
if self.core.window.show_context {
match self.context_page {
Expand Down Expand Up @@ -1333,6 +1372,10 @@ impl Application for App {
Some(&self.nav_model)
}

fn on_app_exit(&mut self) -> Option<Message> {
Some(Message::Quit)
}

fn on_context_drawer(&mut self) -> Command<Message> {
// Focus correct widget
self.update_focus()
Expand Down Expand Up @@ -1402,24 +1445,27 @@ impl Application for App {
}

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;
};

let cosmic_theme::Spacing { space_xxs, .. } = self.core().system_theme().cosmic().spacing;

match dialog {
DialogPage::PromptSave(entity) => {
DialogPage::PromptSaveClose(entity) => {
// "Save" is displayed regardless if the file was already saved because the message handles
// "Save As" if necessary
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
// 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() => {
let save_as = fl!("save-as");
let save_as_button =
widget::button::suggested(save_as).on_press(Message::SaveAsDialog);
let save_as_button = widget::button::suggested(save_as)
.on_press(Message::SaveAsDialog(Some(*entity)));
Some(save_as_button)
}
_ => None,
Expand All @@ -1428,7 +1474,7 @@ impl Application for App {
// Discards unsaved changes
let discard = fl!("discard");
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"))
.body(fl!("prompt-unsaved-changes"))
Expand All @@ -1442,6 +1488,47 @@ impl Application for App {
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())
}
}
Expand Down Expand Up @@ -2064,11 +2151,16 @@ impl Application for App {
self.project_search_value = value;
}
Message::PromptSaveChanges(entity) => {
self.dialog_page_opt = Some(DialogPage::PromptSave(entity));
self.dialog_page_opt = Some(DialogPage::PromptSaveClose(entity));
}
Message::Quit => {
//TODO: prompt for save?
return window::close(window::Id::MAIN);
// Create empty dialog
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 => {
if let Some(Tab::Editor(tab)) = self.active_tab() {
Expand All @@ -2080,24 +2172,37 @@ impl Application for App {
return self.update(Message::TabChanged(self.tab_model.active()));
}
}
Message::Save => {
Message::Save(entity_opt) => {
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() {
return self.update(Message::SaveAsDialog);
return self.update(Message::SaveAsDialog(Some(entity)));
}
title_opt = Some(tab.title());
tab.save();
}

if let Some(title) = title_opt {
self.tab_model.text_set(self.tab_model.active(), title);
}
return self.update_dialogs();
}
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 => {
Message::SaveAsDialog(entity_opt) => {
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) {
let (filename, path_opt) = match &tab.path_opt {
Some(path) => (
Expand Down Expand Up @@ -2135,10 +2240,7 @@ impl Application for App {
if let Some(title) = title_opt {
self.tab_model.text_set(entity, title);
}
// Close save changes prompt only if the dialog succeeded
if self.dialog_page_opt == Some(DialogPage::PromptSave(entity)) {
self.dialog_page_opt = None;
}
return self.update_dialogs();
}
}
}
Expand Down Expand Up @@ -2177,7 +2279,7 @@ impl Application for App {
},
Message::TabActivate(entity) => {
// 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;
}

Expand Down Expand Up @@ -2216,9 +2318,9 @@ impl Application for App {
// The save prompt shouldn't be closed if `TabClose` is emitted again for
// 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
// 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
// order for `Message::Save` to save the correct tab.
return Command::batch([
Expand Down Expand Up @@ -2252,8 +2354,8 @@ impl Application for App {
self.open_tab(None);
}

// Close PromptSave dialog if open for this entity
if self.dialog_page_opt == Some(DialogPage::PromptSave(entity)) {
// Close PromptSaveClose dialog if open for this entity
if self.dialog_page_opt == Some(DialogPage::PromptSaveClose(entity)) {
self.dialog_page_opt = None;
}

Expand Down Expand Up @@ -2754,6 +2856,11 @@ impl Application for App {
event::Event::Window(id, window::Event::Focused) if id == window::Id::MAIN => {
Some(Message::Focus)
}
event::Event::Window(id, window::Event::CloseRequested)
if id == window::Id::MAIN =>
{
Some(Message::Quit)
}
_ => None,
}),
subscription::channel(
Expand Down

0 comments on commit e26175b

Please sign in to comment.