From d4f203297264c84827103de890319d7ef9770318 Mon Sep 17 00:00:00 2001 From: Romain Failliot Date: Sat, 8 Apr 2023 14:09:53 -0400 Subject: [PATCH 1/4] feat: add default handler for SIGINT This prevents having a callstack when hitting Ctrl+C in the terminal. --- CHANGELOG.md | 1 + src/diffuse/diffuse.in | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1582c0b..e30422a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Some signals weren't properly renamed from the previous GTK3 migration (@MightyCreak) - The syntax menu wasn't working anymore (@MightyCreak) +- Properly handles SIGINT (i.e. Ctrl+C) now (@MightyCreak) ## 0.8.1 - 2023-04-07 diff --git a/src/diffuse/diffuse.in b/src/diffuse/diffuse.in index 7f405a6..b1f3742 100755 --- a/src/diffuse/diffuse.in +++ b/src/diffuse/diffuse.in @@ -22,6 +22,7 @@ import os import sys import gettext +import signal from gi.repository import Gio @@ -32,6 +33,10 @@ SYSCONFIGDIR = '@SYSCONFIGDIR@' sys.path.insert(1, PKGDATADIR) +# Quietly handle SIGINT (i.e. Ctrl+C) +signal.signal(signal.SIGINT, signal.SIG_DFL) + +# Initialize i18n gettext.bindtextdomain('diffuse', localedir=LOCALEDIR) gettext.textdomain('diffuse') From f8f0b0618ccd68d7a34ff750216f4977d8a12bab Mon Sep 17 00:00:00 2001 From: Romain Failliot Date: Sat, 8 Apr 2023 15:00:46 -0400 Subject: [PATCH 2/4] refactor: remove thin layer for Gtk.AboutDialog --- src/diffuse/constants.py | 1 + src/diffuse/dialogs.py | 34 ---------------------------------- src/diffuse/main.py | 2 +- src/diffuse/window.py | 40 ++++++++++++++++++++++++++++++++++++---- 4 files changed, 38 insertions(+), 39 deletions(-) diff --git a/src/diffuse/constants.py b/src/diffuse/constants.py index 642d8d6..91041ac 100644 --- a/src/diffuse/constants.py +++ b/src/diffuse/constants.py @@ -21,6 +21,7 @@ from gettext import gettext as _ from typing import Final APP_NAME: Final[str] = 'Diffuse' +APP_ID: Final[str] = 'io.github.mightycreak.Diffuse' COPYRIGHT: Final[str] = '''{copyright} © 2006-2019 Derrick Moser {copyright} © 2015-2023 Romain Failliot'''.format(copyright=_("Copyright")) WEBSITE: Final[str] = 'https://mightycreak.github.io/diffuse/' diff --git a/src/diffuse/dialogs.py b/src/diffuse/dialogs.py index ef74e81..ea9b88c 100644 --- a/src/diffuse/dialogs.py +++ b/src/diffuse/dialogs.py @@ -22,7 +22,6 @@ import os from gettext import gettext as _ from typing import Optional -from diffuse import constants from diffuse import utils import gi # type: ignore @@ -31,39 +30,6 @@ gi.require_version('Gtk', '3.0') from gi.repository import GObject, Gtk # type: ignore # noqa: E402 -# the about dialog -class AboutDialog(Gtk.AboutDialog): - def __init__(self, parent: Gtk.Widget) -> None: - Gtk.AboutDialog.__init__(self) - self.set_transient_for(parent) - self.set_logo_icon_name('io.github.mightycreak.Diffuse') - self.set_program_name(constants.APP_NAME) - self.set_version(constants.VERSION) - self.set_comments(_('Diffuse is a graphical tool for merging and comparing text files.')) - self.set_copyright(constants.COPYRIGHT) - self.set_website(constants.WEBSITE) - self.set_authors(['Derrick Moser ', - 'Romain Failliot ']) - self.set_translator_credits(_('translator-credits')) - license_text = [ - constants.APP_NAME + ' ' + constants.VERSION + '\n\n', - constants.COPYRIGHT + '\n\n', - '''This program is free software; you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation; either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.'''] - self.set_license(''.join(license_text)) - - # custom dialogue for picking files with widgets for specifying the encoding # and revision class FileChooserDialog(Gtk.FileChooserDialog): diff --git a/src/diffuse/main.py b/src/diffuse/main.py index 589703e..befa6b4 100644 --- a/src/diffuse/main.py +++ b/src/diffuse/main.py @@ -37,7 +37,7 @@ class DiffuseApplication(Gtk.Application): def __init__(self, sysconfigdir): super().__init__( - application_id='io.github.mightycreak.Diffuse', + application_id=constants.APP_ID, flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE | Gio.ApplicationFlags.NON_UNIQUE) self.window = None diff --git a/src/diffuse/window.py b/src/diffuse/window.py index 805658d..7f6e53a 100644 --- a/src/diffuse/window.py +++ b/src/diffuse/window.py @@ -28,7 +28,7 @@ from typing import List, Optional from urllib.parse import urlparse from diffuse import constants, utils -from diffuse.dialogs import AboutDialog, FileChooserDialog, NumericDialog, SearchDialog +from diffuse.dialogs import FileChooserDialog, NumericDialog, SearchDialog from diffuse.preferences import Preferences from diffuse.resources import theResources from diffuse.utils import LineEnding @@ -1733,9 +1733,41 @@ class DiffuseWindow(Gtk.ApplicationWindow): # callback for the about menu item def about_cb(self, widget, data): - dialog = AboutDialog(self.get_toplevel()) - dialog.run() - dialog.destroy() + authors = [ + 'Derrick Moser ', + 'Romain Failliot ' + ] + license = f'''{constants.APP_NAME} {constants.VERSION} + +{constants.COPYRIGHT} + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.''' + + dialog = Gtk.AboutDialog( + transient_for=self.get_toplevel(), + modal=True, + program_name=constants.APP_NAME, + logo_icon_name=constants.APP_ID, + version=constants.VERSION, + comments=_('Diffuse is a graphical tool for merging and comparing text files.'), + copyright=constants.COPYRIGHT, + website=constants.WEBSITE, + authors=authors, + translator_credits=_('translator-credits'), + license=license) + dialog.present() def _append_buttons(box, size, specs): From feb557d99b858e9c5070df3060a38e8b86ba268c Mon Sep 17 00:00:00 2001 From: Romain Failliot Date: Sun, 16 Apr 2023 11:34:15 -0400 Subject: [PATCH 3/4] fix: add back save_state() when shuting down --- CHANGELOG.md | 1 + src/diffuse/main.py | 7 ++++++- src/diffuse/window.py | 6 +++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e30422a..b2fdf37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Some signals weren't properly renamed from the previous GTK3 migration (@MightyCreak) - The syntax menu wasn't working anymore (@MightyCreak) - Properly handles SIGINT (i.e. Ctrl+C) now (@MightyCreak) +- Add back `save_state()` to remember window's width and height (@MightyCreak) ## 0.8.1 - 2023-04-07 diff --git a/src/diffuse/main.py b/src/diffuse/main.py index befa6b4..6454225 100644 --- a/src/diffuse/main.py +++ b/src/diffuse/main.py @@ -43,6 +43,8 @@ class DiffuseApplication(Gtk.Application): self.window = None self.sysconfigdir = sysconfigdir + self.connect('shutdown', self.on_shutdown) + self.add_main_option( 'version', ord('v'), @@ -249,7 +251,7 @@ also retrieve revisions of files from several VCSs for comparison and merging.'' # load state self.statepath = os.path.join(data_dir, 'state') - diff_window.loadState(self.statepath) + diff_window.load_state(self.statepath) # process remaining command line arguments encoding = None @@ -366,6 +368,9 @@ also retrieve revisions of files from several VCSs for comparison and merging.'' self.activate() return 0 + def on_shutdown(self, application: Gio.Application) -> None: + self.window.save_state(self.statepath) + def main(version: str, sysconfigdir: str) -> int: """The application's entry point.""" diff --git a/src/diffuse/window.py b/src/diffuse/window.py index 7f6e53a..58b1073 100644 --- a/src/diffuse/window.py +++ b/src/diffuse/window.py @@ -985,7 +985,7 @@ class DiffuseWindow(Gtk.ApplicationWindow): page.open_file(f, True) # record the window's position and size - def configure_cb(self, widget, event): + def configure_cb(self, widget: Gtk.Widget, event: Gdk.EventConfigure) -> None: # read the state directly instead of using window_maximized as the order # of configure/window_state events is undefined if (widget.get_window().get_state() & Gdk.WindowState.MAXIMIZED) == 0: @@ -1000,7 +1000,7 @@ class DiffuseWindow(Gtk.ApplicationWindow): ) # load state information that should persist across sessions - def loadState(self, statepath: str) -> None: + def load_state(self, statepath: str) -> None: if os.path.isfile(statepath): try: f = open(statepath, 'r') @@ -1030,7 +1030,7 @@ class DiffuseWindow(Gtk.ApplicationWindow): self.maximize() # save state information that should persist across sessions - def saveState(self, statepath: str) -> None: + def save_state(self, statepath: str) -> None: try: ss = [] for k, v in self.bool_state.items(): From ef3d59d87bfa9ad5f8c4253d51d4316f3cccd9e2 Mon Sep 17 00:00:00 2001 From: Romain Failliot Date: Sun, 16 Apr 2023 11:35:36 -0400 Subject: [PATCH 4/4] fix: don't save window_x/y anymore As per GNOME HIG, let the DE position the window, but continue to store the window width and height --- src/diffuse/window.py | 48 +++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/diffuse/window.py b/src/diffuse/window.py index 58b1073..78c45f6 100644 --- a/src/diffuse/window.py +++ b/src/diffuse/window.py @@ -682,24 +682,18 @@ class DiffuseWindow(Gtk.ApplicationWindow): # number of created viewers (used to label some tabs) self.viewer_count = 0 - # get monitor resolution - monitor_geometry = Gdk.Display.get_default().get_monitor(0).get_geometry() - # state information that should persist across sessions self.bool_state = { 'window_maximized': False, 'search_matchcase': False, 'search_backwards': False } - self.int_state = {'window_width': 1024, 'window_height': 768} - self.int_state['window_x'] = max( - 0, - (monitor_geometry.width - self.int_state['window_width']) / 2 - ) - self.int_state['window_y'] = max( - 0, - (monitor_geometry.height - self.int_state['window_height']) / 2 - ) + self.int_state = { + 'window_width': 1024, + 'window_height': 768, + } + + # window state signals self.connect('configure-event', self.configure_cb) self.connect('window-state-event', self.window_state_cb) @@ -989,15 +983,13 @@ class DiffuseWindow(Gtk.ApplicationWindow): # read the state directly instead of using window_maximized as the order # of configure/window_state events is undefined if (widget.get_window().get_state() & Gdk.WindowState.MAXIMIZED) == 0: - self.int_state['window_x'], self.int_state['window_y'] = widget.get_window().get_root_origin() # noqa: E501 self.int_state['window_width'] = event.width self.int_state['window_height'] = event.height # record the window's maximized state - def window_state_cb(self, window, event): - self.bool_state['window_maximized'] = ( - (event.new_window_state & Gdk.WindowState.MAXIMIZED) != 0 - ) + def window_state_cb(self, widget: Gtk.Widget, event: Gdk.EventWindowState) -> None: + is_maximized = (event.new_window_state & Gdk.WindowState.MAXIMIZED) != 0 + self.bool_state['window_maximized'] = is_maximized # load state information that should persist across sessions def load_state(self, statepath: str) -> None: @@ -1006,16 +998,22 @@ class DiffuseWindow(Gtk.ApplicationWindow): f = open(statepath, 'r') ss = utils.readlines(f) f.close() + for j, s in enumerate(ss): try: a = shlex.split(s, True) - if len(a) > 0: - if len(a) == 2 and a[0] in self.bool_state: - self.bool_state[a[0]] = (a[1] == 'True') - elif len(a) == 2 and a[0] in self.int_state: - self.int_state[a[0]] = int(a[1]) - else: - raise ValueError() + if len(a) == 0: + continue + if len(a) != 2: + raise ValueError() + + (key, value) = a + if key in self.bool_state: + self.bool_state[key] = (value == 'True') + elif key in self.int_state: + self.int_state[key] = int(value) + else: + raise ValueError() except ValueError: # this may happen if the state was written by a # different version -- don't bother the user @@ -1024,7 +1022,6 @@ class DiffuseWindow(Gtk.ApplicationWindow): # bad $HOME value? -- don't bother the user utils.logDebug(f'Error reading {statepath}.') - self.move(self.int_state['window_x'], self.int_state['window_y']) self.resize(self.int_state['window_width'], self.int_state['window_height']) if self.bool_state['window_maximized']: self.maximize() @@ -1038,6 +1035,7 @@ class DiffuseWindow(Gtk.ApplicationWindow): for k, v in self.int_state.items(): ss.append(f'{k} {v}\n') ss.sort() + f = open(statepath, 'w') f.write(f'# This state file was generated by {constants.APP_NAME} {constants.VERSION}.\n\n') # noqa: E501 for s in ss: