[meld/pathlabel: 8/10] ui.pathlabel: Update path display to support cross-pane summarisation




commit e353761e2e834843bc3db5a55021536c5b16d0d9
Author: Kai Willadsen <kai willadsen gmail com>
Date:   Sun Jan 10 09:58:46 2021 +1000

    ui.pathlabel: Update path display to support cross-pane summarisation

 meld/filediff.py     |  11 +++-
 meld/ui/pathlabel.py | 161 +++++++++++++++++++++++++++++++++++++++++++++++----
 2 files changed, 159 insertions(+), 13 deletions(-)
---
diff --git a/meld/filediff.py b/meld/filediff.py
index 46448726..e7de5c5b 100644
--- a/meld/filediff.py
+++ b/meld/filediff.py
@@ -33,7 +33,7 @@ from meld.const import (
     FileComparisonMode,
 )
 from meld.gutterrendererchunk import GutterRendererChunkLines
-from meld.iohelpers import prompt_save_filename
+from meld.iohelpers import find_shared_parent_path, prompt_save_filename
 from meld.matchers.diffutil import Differ, merged_chunk_order
 from meld.matchers.helpers import CachedSequenceMatcher
 from meld.matchers.merge import AutoMergeDiffer, Merger
@@ -1392,10 +1392,11 @@ class FileDiff(Gtk.VBox, MeldDoc):
 
     def recompute_label(self):
         self._set_save_action_sensitivity()
-        filenames = [b.data.label for b in self.textbuffer[:self.num_panes]]
+        buffers = self.textbuffer[:self.num_panes]
+        filenames = [b.data.label for b in buffers]
         shortnames = misc.shorten_names(*filenames)
 
-        for i, buf in enumerate(self.textbuffer[:self.num_panes]):
+        for i, buf in enumerate(buffers):
             if buf.get_modified():
                 shortnames[i] += "*"
             self.file_save_button[i].set_sensitive(buf.get_modified())
@@ -1403,6 +1404,10 @@ class FileDiff(Gtk.VBox, MeldDoc):
                 'document-save-symbolic' if buf.data.writable else
                 'document-save-as-symbolic')
 
+        parent_path = find_shared_parent_path([b.data.gfile for b in buffers])
+        for pathlabel in self.filelabel:
+            pathlabel.props.parent_gfile = parent_path
+
         label = self.meta.get("tablabel", "")
         if label:
             self.label_text = label
diff --git a/meld/ui/pathlabel.py b/meld/ui/pathlabel.py
index a8cf83bc..eb876283 100644
--- a/meld/ui/pathlabel.py
+++ b/meld/ui/pathlabel.py
@@ -13,31 +13,83 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+import logging
+import pathlib
 
-from gi.repository import Gdk, Gio, GObject, Gtk
+from gi.repository import Gdk, Gio, GLib, GObject, Gtk
 
 from meld.conf import _
 from meld.melddoc import open_files_external
 
+log = logging.getLogger(__name__)
+
 
 @Gtk.Template(resource_path='/org/gnome/meld/ui/path-label.ui')
 class PathLabel(Gtk.MenuButton):
 
     __gtype_name__ = 'PathLabel'
 
-    full_path_label = Gtk.Template.Child()
+    MISSING_FILE_NAME: str = _('Unnamed file')
+
+    full_path_label: Gtk.Entry = Gtk.Template.Child()
 
     custom_label = GObject.Property(type=str, nick='Custom label override')
-    gfile = GObject.Property(type=Gio.File, nick='GFile being displayed')
+
+    _gfile: Gio.File
+    _parent_gfile: Gio.File
+
+    def get_file(self) -> Gio.File:
+        return self._gfile
+
+    def set_file(self, file: Gio.File) -> None:
+        if file == self._gfile:
+            return
+
+        try:
+            self._update_paths(self._parent_gfile, file)
+        except ValueError as e:
+            log.warning(f'Error setting GFile: {str(e)}')
+
+    def get_parent_file(self) -> Gio.File:
+        return self._parent_gfile
+
+    def set_parent_file(self, parent_file: Gio.File) -> None:
+        if parent_file == self._parent_gfile:
+            return
+
+        try:
+            self._update_paths(parent_file, self._gfile)
+        except ValueError as e:
+            log.warning(f'Error setting parent GFile: {str(e)}')
+
+    gfile = GObject.Property(
+        type=Gio.File,
+        nick='File being displayed',
+        getter=get_file,
+        setter=set_file,
+    )
+
+    parent_gfile = GObject.Property(
+        type=Gio.File,
+        nick=(
+            'Parent folder of the current file being displayed that '
+            'determines where the path display will terminate'
+        ),
+        getter=get_parent_file,
+        setter=set_parent_file,
+    )
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
 
-        self.bind_property(
-            'gfile', self, 'label',
-            GObject.BindingFlags.DEFAULT | GObject.BindingFlags.SYNC_CREATE,
-            self.get_display_label,
-        )
+        self._gfile = None
+        self._parent_gfile = None
+
+        # self.bind_property(
+        #     'gfile', self, 'label',
+        #     GObject.BindingFlags.DEFAULT | GObject.BindingFlags.SYNC_CREATE,
+        #     self.get_display_label,
+        # )
         self.bind_property(
             'custom_label', self, 'label',
             GObject.BindingFlags.DEFAULT | GObject.BindingFlags.SYNC_CREATE,
@@ -74,15 +126,104 @@ class PathLabel(Gtk.MenuButton):
         elif self.gfile:
             # TODO: Ideally we'd do some cross-filename summarisation here
             # instead of just showing the basename.
-            return self.gfile.get_basename()
+            basename = self.gfile.get_basename()
+            return basename if basename else self.MISSING_FILE_NAME
         else:
-            return _('Unnamed file')
+            return self.MISSING_FILE_NAME
 
     def get_display_path(self, binding, from_value):
         if from_value:
             return from_value.get_parse_name()
         return ''
 
+    def _update_paths(self, parent: Gio.File, descendant: Gio.File) -> None:
+        # If either of the parent or the main gfiles are not set, the
+        # relationship is fine (because it's not yet established).
+        if not parent or not descendant:
+            self._parent_gfile = parent
+            self._gfile = descendant
+            return
+
+        descendant_parent = descendant.get_parent()
+        if not descendant_parent:
+            raise ValueError(
+                f'Path {descendant.get_path()} has no parent')
+
+        descendant_or_equal = bool(
+            parent.equal(descendant_parent) or
+            parent.get_relative_path(descendant_parent),
+        )
+
+        if not descendant_or_equal:
+            raise ValueError(
+                f'Path {descendant.get_path()} is not a descendant '
+                f'of {parent.get_path()}')
+
+        self._parent_gfile = parent
+        self._gfile = descendant
+
+        # When thinking about the segmentation we do here, there are
+        # four path components that we care about:
+        #
+        #  * any path components above the non-common parent
+        #  * the earliest non-common parent
+        #  * any path components between the actual filename and the
+        #    earliest non-common parent
+        #  * the actual filename
+        #
+        # This is easiest to think about with an example of comparing
+        # two files in a parallel repository structure (or similar).
+        # Let's say that you have two copies of Meld at
+        # /home/foo/checkouts/meld and /home/foo/checkouts/meld-new,
+        # and you're comparing meld/filediff.py within those checkouts.
+        # The components we want would then be (left to right):
+        #
+        #  ---------------------------------------------
+        #  | /home/foo/checkouts | /home/foo/checkouts |
+        #  | meld                | meld-new            |
+        #  | meld                | meld                |
+        #  | filediff.py         | filediff.py         |
+        #  ---------------------------------------------
+        #
+        # Of all of these, the first (the first common parent) is the
+        # *only* one that's actually guaranteed to be the same. The
+        # second will *always* be different (or won't exist if e.g.,
+        # you're comparing files in the same folder or similar). The
+        # third component can be basically anything. The fourth
+        # components will often be the same but that's not guaranteed.
+
+        base_path_str = None
+        elided_path = None
+
+        # FIXME: move all of this (and above) path segmenting logic into a
+        # unit-testable helper
+
+        relative_path_str = parent.get_relative_path(descendant_parent)
+
+        if relative_path_str:
+            relative_path = pathlib.Path(relative_path_str)
+
+            base_path_str = relative_path.parts[0]
+            if len(relative_path.parts) == 1:
+                # No directory components, so we have no elided path
+                # segment
+                elided_path = None
+            else:
+                base_path_gfile = parent.get_child(base_path_str)
+                elided_path = base_path_gfile.get_relative_path(
+                    descendant_parent)
+
+        label_segments = [
+            '…' if base_path_str else None,
+            base_path_str,
+            '…' if elided_path else None,
+            descendant.get_basename(),
+        ]
+        label_text = GLib.build_filenamev([s for s in label_segments if s])
+
+        # FIXME: this doesn't handle custom labels at all....
+        self.set_label(label_text)
+
     def action_copy_full_path(self, *args):
         if not self.gfile:
             return


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]