Skip to content

Commit fac8985

Browse files
committed
Add unsaved changes dialog and update host editor UI
- Introduced a new unsaved changes dialog to handle unsaved changes on window close. - Updated host editor UI to remove save and revert buttons, replacing them with a banner for unsaved changes. - Adjusted resource files to include the new dialog and updated UI components accordingly.
1 parent 56a59bd commit fac8985

File tree

6 files changed

+68
-135
lines changed

6 files changed

+68
-135
lines changed

data/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ blueprint_files = files(
1111
'ui/key_picker_dialog.blp',
1212
'ui/keyboard_shortcuts_dialog.blp',
1313
'ui/welcome_view.blp',
14+
'ui/unsaved_changes_dialog.blp',
1415
)
1516

1617
bp_gen = generator(blueprint_compiler,

data/ssh-studio.gresource.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<file alias="ui/key_picker_dialog.ui" preprocess="xml-stripblanks">key_picker_dialog.ui</file>
1212
<file alias="ui/keyboard_shortcuts_dialog.ui" preprocess="xml-stripblanks">keyboard_shortcuts_dialog.ui</file>
1313
<file alias="ui/welcome_view.ui" preprocess="xml-stripblanks">welcome_view.ui</file>
14+
<file alias="ui/unsaved_changes_dialog.ui" preprocess="xml-stripblanks">unsaved_changes_dialog.ui</file>
1415
<file>ssh-studio.css</file>
1516
<file alias="media/icon_256.png">icon_256.png</file>
1617
<file alias="icons/256x256/apps/io.github.BuddySirJava.SSH-Studio.png">icon_256.png</file>

data/ui/host_editor.blp

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -77,33 +77,13 @@ template $HostEditor: Gtk.Box {
7777
child: Adw.Banner unsaved_banner {
7878
revealed: false;
7979
title: _("You have unsaved changes.");
80+
button-label: _("Save");
81+
use-markup: false;
82+
styles [
83+
"banner",
84+
]
8085
};
8186

82-
[overlay]
83-
Box {
84-
halign: end;
85-
valign: start;
86-
spacing: 8;
87-
margin-top: 6;
88-
margin-end: 12;
89-
90-
Button save_button {
91-
label: _("Save");
92-
visible: false;
93-
styles [
94-
"suggested-action",
95-
]
96-
}
97-
98-
Button revert_button {
99-
label: _("Revert");
100-
visible: false;
101-
styles [
102-
"flat",
103-
"destructive-action",
104-
]
105-
}
106-
}
10787
}
10888

10989
Adw.ViewStack viewstack {

data/ui/unsaved_changes_dialog.blp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using Gtk 4.0;
2+
using Adw 1;
3+
4+
Adw.AlertDialog unsaved_changes_dialog {
5+
heading: _("Unsaved Changes");
6+
body: _("You have unsaved changes that will be lost if you quit.\n\nWhat would you like to do?");
7+
close-response: "cancel";
8+
}
9+
10+

src/ui/host_editor.py

Lines changed: 1 addition & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,6 @@ class HostEditor(Gtk.Box):
6464
copy_row = Gtk.Template.Child()
6565
test_row = Gtk.Template.Child()
6666
unsaved_banner = Gtk.Template.Child()
67-
save_button = Gtk.Template.Child()
68-
revert_button = Gtk.Template.Child()
6967

7068
__gsignals__ = {
7169
"host-changed": (GObject.SignalFlags.RUN_LAST, None, (object,)),
@@ -113,12 +111,6 @@ def __init__(self):
113111
if getattr(self, "unsaved_banner", None):
114112
self.unsaved_banner.set_revealed(False)
115113
self.unsaved_banner.set_sensitive(False)
116-
if getattr(self, "save_button", None):
117-
self.save_button.set_visible(False)
118-
self.save_button.set_sensitive(False)
119-
if getattr(self, "revert_button", None):
120-
self.revert_button.set_visible(False)
121-
self.revert_button.set_sensitive(False)
122114
except Exception:
123115
pass
124116

@@ -415,16 +407,6 @@ def _connect_buttons(self):
415407
self.unsaved_banner.connect("button-clicked", lambda *_: self._on_save_clicked(None))
416408
except Exception:
417409
pass
418-
try:
419-
if getattr(self, "save_button", None) is not None:
420-
self.save_button.connect("clicked", self._on_save_clicked)
421-
except Exception:
422-
pass
423-
try:
424-
if getattr(self, "revert_button", None) is not None:
425-
self.revert_button.connect("clicked", self._on_revert_clicked)
426-
except Exception:
427-
pass
428410

429411
def _connect_header_buttons(self):
430412
"""Connect header bar button signals."""
@@ -622,12 +604,6 @@ def _safe_set_entry_text(row, text):
622604
if getattr(self, "unsaved_banner", None):
623605
self.unsaved_banner.set_revealed(False)
624606
self.unsaved_banner.set_sensitive(False)
625-
if getattr(self, "save_button", None):
626-
self.save_button.set_visible(False)
627-
self.save_button.set_sensitive(False)
628-
if getattr(self, "revert_button", None):
629-
self.revert_button.set_visible(False)
630-
self.revert_button.set_sensitive(False)
631607
except Exception:
632608
pass
633609

@@ -1737,65 +1713,16 @@ def _on_save_clicked(self, button):
17371713
except Exception:
17381714
pass
17391715

1740-
def _on_revert_clicked(self, button):
1741-
"""Reverts the current host's changes to its last loaded state by reloading the configuration."""
1742-
if not self.current_host:
1743-
return
1744-
1745-
if not hasattr(self, "original_host_state") or not self.original_host_state:
1746-
return
1747-
self.is_loading = True
1748-
self.current_host.patterns = copy.deepcopy(self.original_host_state.patterns)
1749-
self.current_host.options = copy.deepcopy(self.original_host_state.options)
1750-
self.current_host.raw_lines = copy.deepcopy(self.original_host_state.raw_lines)
1751-
1752-
self._sync_fields_from_host()
1753-
1754-
buffer = self.raw_text_view.get_buffer()
1755-
if hasattr(self, "_raw_changed_handler_id"):
1756-
buffer.handler_block(self._raw_changed_handler_id)
1757-
self._programmatic_raw_update = True
1758-
buffer.set_text("\n".join(self.current_host.raw_lines))
1759-
if hasattr(self, "_raw_changed_handler_id"):
1760-
buffer.handler_unblock(self._raw_changed_handler_id)
1761-
self._programmatic_raw_update = False
1762-
self.original_raw_content = "\n".join(self.current_host.raw_lines)
1763-
self.is_loading = False
1764-
1765-
self._ensure_buffer_initialized()
1766-
if self.buffer is not None:
1767-
self.buffer.remove_all_tags(
1768-
self.buffer.get_start_iter(), self.buffer.get_end_iter()
1769-
)
1770-
self.emit("host-changed", self.current_host)
1771-
try:
1772-
if getattr(self, "save_banner", None):
1773-
self.save_banner.set_revealed(False)
1774-
self.save_banner.set_sensitive(False)
1775-
if getattr(self, "revert_banner", None):
1776-
self.revert_banner.set_revealed(False)
1777-
self.revert_banner.set_sensitive(False)
1778-
except Exception:
1779-
pass
1780-
self._show_message(_(f"Reverted changes for {self.current_host.patterns[0]}"))
1781-
self._touched_options.clear()
1782-
self._update_button_sensitivity()
17831716

17841717
def _update_button_sensitivity(self):
1785-
"""Updates the sensitivity of save and revert buttons based on dirty state and validity."""
1718+
"""Updates the sensitivity of banner based on dirty state and validity."""
17861719
is_dirty = self.is_host_dirty()
17871720
field_errors = self._collect_field_errors()
17881721
is_valid = not bool(field_errors)
17891722
try:
17901723
if getattr(self, "unsaved_banner", None):
17911724
self.unsaved_banner.set_revealed(is_dirty)
17921725
self.unsaved_banner.set_sensitive(is_dirty and is_valid)
1793-
if getattr(self, "save_button", None):
1794-
self.save_button.set_visible(is_dirty)
1795-
self.save_button.set_sensitive(is_dirty and is_valid)
1796-
if getattr(self, "revert_button", None):
1797-
self.revert_button.set_visible(is_dirty)
1798-
self.revert_button.set_sensitive(is_dirty)
17991726
except Exception:
18001727
pass
18011728

src/ui/main_window.py

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
gi.require_version("Gtk", "4.0")
44
gi.require_version("Adw", "1")
5-
from gi.repository import Gtk, Gio, Gdk, Adw
5+
from gi.repository import Gtk, Gio, Gdk, Adw, GLib
66
from pathlib import Path
77
from gettext import gettext as _
88
import sys
99
from .host_list import HostList
1010
from .host_editor import HostEditor
1111
from .welcome_view import WelcomeView
12+
from gi.repository import Gio as _Gio
1213

1314

1415
@Gtk.Template(resource_path="/io/github/BuddySirJava/SSH-Studio/ui/main_window.ui")
@@ -52,6 +53,7 @@ def __init__(self, app):
5253
pass
5354

5455
self.connect("notify::has-focus", self._on_window_focus_changed)
56+
self.connect("close-request", self._on_close_request)
5557
self._show_welcome_view()
5658

5759
try:
@@ -155,10 +157,6 @@ def _setup_split_view(self):
155157

156158
def _connect_signals(self):
157159
"""Connect all the signal handlers."""
158-
try:
159-
self.save_button.connect("clicked", self._on_save_clicked)
160-
except Exception:
161-
self.save_button = None
162160

163161
self.host_list.connect("host-selected", self._on_host_selected)
164162
self.host_list.connect("host-added", self._on_host_added)
@@ -171,14 +169,6 @@ def _connect_signals(self):
171169
)
172170
self.host_editor.connect("show-toast", self._on_show_toast)
173171

174-
try:
175-
if (
176-
hasattr(self.host_editor, "save_button")
177-
and self.host_editor.save_button is not None
178-
):
179-
self.host_editor.save_button.set_sensitive(False)
180-
except Exception:
181-
pass
182172

183173
self.host_list.search_entry.connect("search-changed", self._on_search_changed)
184174

@@ -394,6 +384,50 @@ def _on_window_focus_changed(self, window, param):
394384
self.host_list.search_bar.set_visible(False)
395385
self.host_list.filter_hosts("")
396386

387+
def _on_close_request(self, window):
388+
"""Handle window close request - check for unsaved changes."""
389+
if hasattr(self.host_editor, 'is_host_dirty') and self.host_editor.is_host_dirty():
390+
return self._show_unsaved_changes_dialog()
391+
return False
392+
393+
def _show_unsaved_changes_dialog(self):
394+
"""Show alert dialog asking user what to do with unsaved changes."""
395+
# Load from blueprint resource
396+
builder = Gtk.Builder.new_from_resource("/io/github/BuddySirJava/SSH-Studio/ui/unsaved_changes_dialog.ui")
397+
dialog = builder.get_object("unsaved_changes_dialog")
398+
dialog.set_close_response("cancel")
399+
dialog.add_response("discard", _("Discard Changes"))
400+
dialog.add_response("cancel", _("Cancel"))
401+
dialog.add_response("save", _("Save & Quit"))
402+
dialog.set_response_appearance("discard", Adw.ResponseAppearance.DESTRUCTIVE)
403+
dialog.set_response_appearance("save", Adw.ResponseAppearance.SUGGESTED)
404+
dialog.set_default_response("save")
405+
406+
def on_response(dialog, response):
407+
if response == "save":
408+
try:
409+
if hasattr(self.host_editor, 'unsaved_banner') and self.host_editor.unsaved_banner:
410+
self.host_editor._on_save_clicked(None)
411+
dialog.close()
412+
GLib.timeout_add(200, self._delayed_close)
413+
except Exception as e:
414+
self.show_toast(_(f"Failed to save: {e}"))
415+
elif response == "discard":
416+
dialog.close()
417+
self.destroy()
418+
else:
419+
dialog.close()
420+
return True
421+
422+
dialog.connect("response", on_response)
423+
dialog.present(self)
424+
return True
425+
426+
def _delayed_close(self):
427+
"""Close the window after a short delay to allow save to complete."""
428+
self.destroy()
429+
return False
430+
397431
def on_status_bar_close_clicked(self, button):
398432
pass
399433

@@ -417,25 +451,19 @@ def _on_save_clicked(self, button):
417451

418452
self.host_list.load_hosts(self.parser.config.hosts)
419453
self.is_dirty = False
420-
if self.save_button is not None:
421-
self.save_button.set_sensitive(False)
422454
self._update_status(_("Configuration saved successfully"))
423455
except Exception as e:
424456
self._show_error(f"Failed to save configuration: {e}")
425457

426458
def _write_and_reload(self, show_status: bool = False):
427-
"""Write the config to disk and reload UI without showing validation dialogs.
428-
Disables the save button afterward.
429-
"""
459+
"""Write the config to disk and reload UI without showing validation dialogs."""
430460
if not self.parser:
431461
return
432462
try:
433463
self.parser.write(backup=True)
434464
self.parser.parse()
435465
self.host_list.load_hosts(self.parser.config.hosts)
436466
self.is_dirty = False
437-
if self.save_button is not None:
438-
self.save_button.set_sensitive(False)
439467
if show_status:
440468
self._update_status(_("Configuration saved"))
441469
except Exception as e:
@@ -485,16 +513,12 @@ def _on_host_added(self, host_list, host):
485513

486514
self.parser.config.add_host(host)
487515
self.is_dirty = True
488-
if self.save_button is not None:
489-
self.save_button.set_sensitive(True)
490516

491517
def undo_add():
492518
try:
493519
if host in self.parser.config.hosts:
494520
self.parser.config.remove_host(host)
495521
self.is_dirty = self.parser.config.is_dirty()
496-
if self.save_button is not None:
497-
self.save_button.set_sensitive(self.is_dirty)
498522
self.host_list.load_hosts(self.parser.config.hosts)
499523
try:
500524
if not self.parser.config.hosts:
@@ -540,27 +564,17 @@ def undo_delete():
540564
self.host_editor.current_host = None
541565
self.host_editor._clear_all_fields()
542566
self._set_host_editor_visible(False)
543-
if self.save_button is not None:
544-
self.save_button.set_sensitive(False)
545567
self.is_dirty = False
546568

547569
else:
548570
self.host_list.select_host(self.parser.config.hosts[0])
549571

550572
def _on_host_changed(self, editor, host):
551573
self.is_dirty = self.parser.config.is_dirty()
552-
if self.save_button is not None:
553-
if self.save_button is not None:
554-
self.save_button.set_sensitive(self.is_dirty)
555574

556575
def _on_editor_validity_changed(self, editor, is_valid: bool):
557-
if self.save_button is not None:
558-
if not is_valid:
559-
if self.save_button is not None:
560-
self.save_button.set_sensitive(False)
561-
else:
562-
if self.save_button is not None:
563-
self.save_button.set_sensitive(self.is_dirty)
576+
# The banner handles its own sensitivity based on validity
577+
pass
564578

565579
def _on_show_toast(self, editor, message: str):
566580
"""Handle show-toast signal from host editor."""

0 commit comments

Comments
 (0)