Add dialog for saving all unsaved tabs when closing app, fixes #169
This commit is contained in:
parent
4716c2242b
commit
e26175b28d
2 changed files with 140 additions and 32 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
171
src/main.rs
171
src/main.rs
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue