diff --git a/src/main.py b/src/main.py index 621f954..6ff7aca 100644 --- a/src/main.py +++ b/src/main.py @@ -46,6 +46,7 @@ from diffuse.dialogs import AboutDialog, FileChooserDialog, NumericDialog, Searc from diffuse.preferences import Preferences from diffuse.resources import Resources from diffuse.vcs.vcs_registry import VcsRegistry +from diffuse.widgets import ScrolledWindow # avoid some dictionary lookups when string.whitespace is used in loops # this is sorted based upon frequency to speed up code for stripping whitespace @@ -178,111 +179,6 @@ def convert_to_format(s, format): s += '\r' return s -# utility method to step advance an adjustment -def step_adjustment(adj, delta): - v = adj.get_value() + delta - # clamp to the allowed range - v = max(v, int(adj.get_lower())) - v = min(v, int(adj.get_upper() - adj.get_page_size())) - adj.set_value(v) - -# This is a replacement for Gtk.ScrolledWindow as it forced expose events to be -# handled immediately after changing the viewport position. This could cause -# the application to become unresponsive for a while as it processed a large -# queue of keypress and expose event pairs. -class ScrolledWindow(Gtk.Grid): - scroll_directions = set((Gdk.ScrollDirection.UP, - Gdk.ScrollDirection.DOWN, - Gdk.ScrollDirection.LEFT, - Gdk.ScrollDirection.RIGHT)) - - def __init__(self, hadj, vadj): - Gtk.Grid.__init__(self) - self.position = (0, 0) - self.scroll_count = 0 - self.partial_redraw = False - - self.hadj, self.vadj = hadj, vadj - vport = Gtk.Viewport.new() - darea = Gtk.DrawingArea.new() - darea.add_events(Gdk.EventMask.SCROLL_MASK) - self.darea = darea - # replace darea's queue_draw_area with our own so we can tell when - # to disable/enable our scrolling optimisation - self.darea_queue_draw_area = darea.queue_draw_area - darea.queue_draw_area = self.redraw_region - vport.add(darea) - darea.show() - self.attach(vport, 0, 0, 1, 1) - vport.set_vexpand(True) - vport.set_hexpand(True) - vport.show() - - self.vbar = bar = Gtk.Scrollbar.new(Gtk.Orientation.VERTICAL, vadj) - self.attach(bar, 1, 0, 1, 1) - bar.show() - - self.hbar = bar = Gtk.Scrollbar.new(Gtk.Orientation.HORIZONTAL, hadj) - self.attach(bar, 0, 1, 1, 1) - bar.show() - - # listen to our signals - hadj.connect('value-changed', self.value_changed_cb) - vadj.connect('value-changed', self.value_changed_cb) - darea.connect('configure-event', self.configure_cb) - darea.connect('scroll-event', self.scroll_cb) - darea.connect('draw', self.draw_cb) - - # updates the adjustments to match the new widget size - def configure_cb(self, widget, event): - w, h = event.width, event.height - for adj, d in (self.hadj, w), (self.vadj, h): - v = adj.get_value() - if v + d > adj.get_upper(): - adj.set_value(max(0, adj.get_upper() - d)) - adj.set_page_size(d) - adj.set_page_increment(d) - - # update the vertical adjustment when the mouse's scroll wheel is used - def scroll_cb(self, widget, event): - d = event.direction - if d in self.scroll_directions: - delta = 100 - if d in (Gdk.ScrollDirection.UP, Gdk.ScrollDirection.LEFT): - delta = -delta - vertical = (d in (Gdk.ScrollDirection.UP, Gdk.ScrollDirection.DOWN)) - if event.state & Gdk.ModifierType.SHIFT_MASK: - vertical = not vertical - if vertical: - adj = self.vadj - else: - adj = self.hadj - step_adjustment(adj, delta) - - def value_changed_cb(self, widget): - old_x, old_y = self.position - pos_x = int(self.hadj.get_value()) - pos_y = int(self.vadj.get_value()) - self.position = (pos_x, pos_y) - if self.darea.get_window() is not None: - # window.scroll() although visually nice, is slow, revert to - # queue_draw() if scroll a lot without seeing an expose event - if self.scroll_count < 2 and not self.partial_redraw: - self.scroll_count += 1 - self.darea.get_window().scroll(old_x - pos_x, old_y - pos_y) - else: - self.partial_redraw = False - self.darea.queue_draw() - - def draw_cb(self, widget, cr): - self.scroll_count = 0 - - # replacement for darea.queue_draw_area that notifies us when a partial - # redraw happened - def redraw_region(self, x, y, w, h): - self.partial_redraw = True - self.darea_queue_draw_area(x, y, w, h) - # Enforcing manual alignment is accomplished by dividing the lines of text into # sections that are matched independently. 'blocks' is an array of integers # describing how many lines (including null lines for spacing) that are in each @@ -2770,9 +2666,9 @@ class FileDiffViewer(Gtk.Grid): def diffmap_scroll_cb(self, widget, event): delta = 100 if event.direction == Gdk.ScrollDirection.UP: - step_adjustment(self.vadj, -delta) + utils.step_adjustment(self.vadj, -delta) elif event.direction == Gdk.ScrollDirection.DOWN: - step_adjustment(self.vadj, delta) + utils.step_adjustment(self.vadj, delta) # redraws the overview map when a portion is exposed def diffmap_draw_cb(self, widget, cr): diff --git a/src/meson.build b/src/meson.build index 8a1514f..eccac61 100644 --- a/src/meson.build +++ b/src/meson.build @@ -37,6 +37,7 @@ diffuse_sources = [ 'preferences.py', 'resources.py', 'utils.py', + 'widgets.py', ] install_data(diffuse_sources, install_dir: moduledir) diff --git a/src/utils.py b/src/utils.py index dec2319..7fca234 100644 --- a/src/utils.py +++ b/src/utils.py @@ -264,6 +264,14 @@ def null_to_empty(s): s = '' return s +# utility method to step advance an adjustment +def step_adjustment(adj, delta): + v = adj.get_value() + delta + # clamp to the allowed range + v = max(v, int(adj.get_lower())) + v = min(v, int(adj.get_upper() - adj.get_page_size())) + adj.set_value(v) + # use the program's location as a starting place to search for supporting files # such as icon and help documentation @@ -279,9 +287,9 @@ lang = locale.getdefaultlocale()[0] if isWindows(): # gettext looks for the language using environment variables which # are normally not set on Windows so we try setting it for them - for v in 'LC_ALL', 'LC_CTYPE', 'LANG', 'LANGUAGE': - if v in os.environ: - lang = os.environ[v] + for lang_env in 'LC_ALL', 'LC_CTYPE', 'LANG', 'LANGUAGE': + if lang_env in os.environ: + lang = os.environ[lang_env] # remove any additional languages, encodings, or modifications for c in ':.@': lang = lang.split(c)[0] diff --git a/src/widgets.py b/src/widgets.py new file mode 100644 index 0000000..a2aa5a7 --- /dev/null +++ b/src/widgets.py @@ -0,0 +1,124 @@ +# Diffuse: a graphical tool for merging and comparing text files. +# +# Copyright (C) 2019 Derrick Moser +# Copyright (C) 2021 Romain Failliot +# +# 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. + +# pylint: disable=wrong-import-position +import gi +gi.require_version('Gtk', '3.0') +gi.require_version('Gdk', '3.0') +from gi.repository import Gtk, Gdk +# pylint: enable=wrong-import-position + +from diffuse import utils + +# This is a replacement for Gtk.ScrolledWindow as it forced expose events to be +# handled immediately after changing the viewport position. This could cause +# the application to become unresponsive for a while as it processed a large +# queue of keypress and expose event pairs. +class ScrolledWindow(Gtk.Grid): + scroll_directions = set((Gdk.ScrollDirection.UP, + Gdk.ScrollDirection.DOWN, + Gdk.ScrollDirection.LEFT, + Gdk.ScrollDirection.RIGHT)) + + def __init__(self, hadj, vadj): + Gtk.Grid.__init__(self) + self.position = (0, 0) + self.scroll_count = 0 + self.partial_redraw = False + + self.hadj, self.vadj = hadj, vadj + vport = Gtk.Viewport.new() + darea = Gtk.DrawingArea.new() + darea.add_events(Gdk.EventMask.SCROLL_MASK) + self.darea = darea + # replace darea's queue_draw_area with our own so we can tell when + # to disable/enable our scrolling optimisation + self.darea_queue_draw_area = darea.queue_draw_area + darea.queue_draw_area = self.redraw_region + vport.add(darea) + darea.show() + self.attach(vport, 0, 0, 1, 1) + vport.set_vexpand(True) + vport.set_hexpand(True) + vport.show() + + self.vbar = Gtk.Scrollbar.new(Gtk.Orientation.VERTICAL, vadj) + self.attach(self.vbar, 1, 0, 1, 1) + self.vbar.show() + + self.hbar = Gtk.Scrollbar.new(Gtk.Orientation.HORIZONTAL, hadj) + self.attach(self.hbar, 0, 1, 1, 1) + self.hbar.show() + + # listen to our signals + hadj.connect('value-changed', self.value_changed_cb) + vadj.connect('value-changed', self.value_changed_cb) + darea.connect('configure-event', self.configure_cb) + darea.connect('scroll-event', self.scroll_cb) + darea.connect('draw', self.draw_cb) + + # updates the adjustments to match the new widget size + def configure_cb(self, widget, event): + w, h = event.width, event.height + for adj, d in (self.hadj, w), (self.vadj, h): + v = adj.get_value() + if v + d > adj.get_upper(): + adj.set_value(max(0, adj.get_upper() - d)) + adj.set_page_size(d) + adj.set_page_increment(d) + + # update the vertical adjustment when the mouse's scroll wheel is used + def scroll_cb(self, widget, event): + d = event.direction + if d in self.scroll_directions: + delta = 100 + if d in (Gdk.ScrollDirection.UP, Gdk.ScrollDirection.LEFT): + delta = -delta + vertical = (d in (Gdk.ScrollDirection.UP, Gdk.ScrollDirection.DOWN)) + if event.state & Gdk.ModifierType.SHIFT_MASK: + vertical = not vertical + if vertical: + adj = self.vadj + else: + adj = self.hadj + utils.step_adjustment(adj, delta) + + def value_changed_cb(self, widget): + old_x, old_y = self.position + pos_x = int(self.hadj.get_value()) + pos_y = int(self.vadj.get_value()) + self.position = (pos_x, pos_y) + if self.darea.get_window() is not None: + # window.scroll() although visually nice, is slow, revert to + # queue_draw() if scroll a lot without seeing an expose event + if self.scroll_count < 2 and not self.partial_redraw: + self.scroll_count += 1 + self.darea.get_window().scroll(old_x - pos_x, old_y - pos_y) + else: + self.partial_redraw = False + self.darea.queue_draw() + + def draw_cb(self, widget, cr): + self.scroll_count = 0 + + # replacement for darea.queue_draw_area that notifies us when a partial + # redraw happened + def redraw_region(self, x, y, w, h): + self.partial_redraw = True + self.darea_queue_draw_area(x, y, w, h)