[meld] Add recently-used comparison support (closes bgo#652747)



commit 0fccdabd328279ffda5e4c036163e681671d7669
Author: Kai Willadsen <kai willadsen gmail com>
Date:   Tue Oct 16 05:34:49 2012 +1000

    Add recently-used comparison support (closes bgo#652747)
    
    The recent-files API provided by GTK+ doesn't actually work for Meld
    out-of-the-box, because instead of storing individual files, we need to
    stored multiple linked files. In other words, we need to store a
    comparison, not a file.
    
    This commit adds support for reading and writing a simple comparison
    record format with a new Meld-specific mime-type. These files are
    stored under the user data directory, and are managed by the new
    'recent' module. Meld creates these files for new top-level
    comparisons and inserts them into the recently-used store as proxies
    for the actual file tuples.
    
    Note that we deliberately avoid recording as recently-used comparisons
    that are invoked from other comparisons; a user may open ten quick
    file comparisons from a single VC comparison, but they probably don't
    actually want to re-open those from the recent files menu.
    
    There is also support for opening comparison files from the command
    line. This was added so that recent comparisons can be opened from the
    desktop recent files menu, but can also be used to manually open
    specified comparisons.

 Makefile               |   11 ++-
 data/meld.desktop.in   |    1 +
 data/mime/meld.xml.in  |    8 ++
 data/ui/meldapp-ui.xml |    1 +
 meld/dirdiff.py        |    9 ++
 meld/filediff.py       |    5 +
 meld/filemerge.py      |    5 +
 meld/meldapp.py        |   18 ++++-
 meld/melddoc.py        |    4 +
 meld/meldwindow.py     |   37 ++++++++
 meld/recent.py         |  215 ++++++++++++++++++++++++++++++++++++++++++++++++
 meld/vcview.py         |    4 +
 12 files changed, 316 insertions(+), 2 deletions(-)
---
diff --git a/Makefile b/Makefile
index 55bde32..eff61e1 100644
--- a/Makefile
+++ b/Makefile
@@ -12,7 +12,7 @@ SPECIALS := bin/meld meld/paths.py
 BROWSER := firefox
 
 .PHONY:all
-all: $(addsuffix .install,$(SPECIALS)) meld.desktop
+all: $(addsuffix .install,$(SPECIALS)) meld.desktop meld.xml
 	$(MAKE) -C po
 	$(MAKE) -C help
 
@@ -22,6 +22,7 @@ clean:
 		xargs -0 rm -f
 	@find ./bin -type f \( -name '*.install' \) -print0 | xargs -0 rm -f
 	@rm -f data/meld.desktop
+	@rm -f data/mime/meld.xml
 	$(MAKE) -C po clean
 	$(MAKE) -C help clean
 
@@ -60,6 +61,8 @@ install: $(addsuffix .install,$(SPECIALS)) meld.desktop
 		$(DESTDIR)$(libdir_)/meld/paths.py
 	install -m 644 data/meld.desktop \
 		$(DESTDIR)$(sharedir)/applications
+	install -m 644 data/mime/meld.xml \
+		$(DESTDIR)$(sharedir)/mime/packages/
 	$(PYTHON)    -c 'import compileall; compileall.compile_dir("$(DESTDIR)$(libdir_)",10,"$(libdir_)")'
 	$(PYTHON) -O -c 'import compileall; compileall.compile_dir("$(DESTDIR)$(libdir_)",10,"$(libdir_)")'
 	install -m 644 data/gtkrc \
@@ -88,10 +91,14 @@ install: $(addsuffix .install,$(SPECIALS)) meld.desktop
 		$(DESTDIR)$(sharedir)/icons/HighContrast/scalable/apps/meld.svg
 	$(MAKE) -C po install
 	$(MAKE) -C help install
+    update-mime-database $(DESTDIR)$(sharedir)/mime
 
 meld.desktop: data/meld.desktop.in
 	intltool-merge -d po data/meld.desktop.in data/meld.desktop
 
+meld.xml: data/meld.xml.in
+	intltool-merge -d po data/mime/meld.xml.in data/mime/meld.xml
+
 %.install: %
 	$(PYTHON) tools/install_paths \
 		libdir=$(libdir_) \
@@ -109,7 +116,9 @@ uninstall:
 		$(libdir_) \
 		$(bindir)/meld \
 		$(sharedir)/applications/meld.desktop \
+		$(sharedir)/mime/packages/meld.xml \
 		$(sharedir)/pixmaps/meld.png
 	$(MAKE) -C po uninstall
 	$(MAKE) -C help uninstall
+    update-mime-database $(DESTDIR)$(sharedir)/mime
 
diff --git a/data/meld.desktop.in b/data/meld.desktop.in
index f1b89d2..494c12c 100644
--- a/data/meld.desktop.in
+++ b/data/meld.desktop.in
@@ -7,6 +7,7 @@ Exec=meld %F
 Terminal=false
 Type=Application
 Icon=meld
+MimeType=application/x-meld-comparison
 StartupNotify=true
 Categories=GTK;Development;
 X-GNOME-Bugzilla-Bugzilla=GNOME
diff --git a/data/mime/meld.xml.in b/data/mime/meld.xml.in
new file mode 100644
index 0000000..e4c954e
--- /dev/null
+++ b/data/mime/meld.xml.in
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info";>
+   <mime-type type="application/x-meld-comparison">
+     <comment>Meld comparison description</comment>
+     <glob pattern="*.meldcmp"/>
+     <icon name="meld"/>
+   </mime-type>
+</mime-info>
diff --git a/data/ui/meldapp-ui.xml b/data/ui/meldapp-ui.xml
index ba1c00c..0b19785 100644
--- a/data/ui/meldapp-ui.xml
+++ b/data/ui/meldapp-ui.xml
@@ -7,6 +7,7 @@
       <separator/>
       <placeholder name="FileActionsPlaceholder" />
       <separator/>
+      <menuitem action="Recent" />
       <menuitem action="Close" />
       <menuitem action="Quit" />
     </menu>
diff --git a/meld/dirdiff.py b/meld/dirdiff.py
index 834187b..3db55a5 100644
--- a/meld/dirdiff.py
+++ b/meld/dirdiff.py
@@ -32,6 +32,7 @@ from . import melddoc
 from . import tree
 from . import misc
 from . import paths
+from . import recent
 from .ui import gnomeglade
 from .ui import emblemcellrenderer
 
@@ -496,6 +497,14 @@ class DirDiff(melddoc.MeldDoc, gnomeglade.Component):
         self.recursively_update( (0,) )
         self._update_diffmaps()
 
+    def get_comparison(self):
+        root = self.model.get_iter_root()
+        if root:
+            folders = self.model.value_paths(root)
+        else:
+            folders = []
+        return recent.TYPE_FOLDER, folders
+
     def recursively_update( self, path ):
         """Recursively update from tree path 'path'.
         """
diff --git a/meld/filediff.py b/meld/filediff.py
index 4a3145f..e56c521 100644
--- a/meld/filediff.py
+++ b/meld/filediff.py
@@ -40,6 +40,7 @@ from . import merge
 from . import misc
 from . import patchdialog
 from . import paths
+from . import recent
 from . import undo
 from .ui import findbar
 from .ui import gnomeglade
@@ -980,6 +981,10 @@ class FileDiff(melddoc.MeldDoc, gnomeglade.Component):
         self._connect_buffer_handlers()
         self.scheduler.add_task(self._set_files_internal(files))
 
+    def get_comparison(self):
+        files = [b.data.filename for b in self.textbuffer[:self.num_panes]]
+        return recent.TYPE_FILE, files
+
     def _load_files(self, files, textbuffers):
         self.undosequence.clear()
         yield _("[%s] Set num panes") % self.label_text
diff --git a/meld/filemerge.py b/meld/filemerge.py
index 7e5947e..420dc52 100644
--- a/meld/filemerge.py
+++ b/meld/filemerge.py
@@ -23,6 +23,7 @@ import gtk
 from . import filediff
 from . import meldbuffer
 from . import merge
+from . import recent
 
 
 class FileMerge(filediff.FileDiff):
@@ -34,6 +35,10 @@ class FileMerge(filediff.FileDiff):
         self.textview[0].set_editable(0)
         self.textview[2].set_editable(0)
 
+    def get_comparison(self):
+        comp = filediff.FileDiff.get_comparison(self)
+        return recent.TYPE_MERGE, comp[1]
+
     def _set_files_internal(self, files):
         self.textview[1].set_buffer(meldbuffer.MeldBuffer())
         for i in self._load_files(files, self.textbuffer):
diff --git a/meld/meldapp.py b/meld/meldapp.py
index 3a18b1b..1f4d57d 100644
--- a/meld/meldapp.py
+++ b/meld/meldapp.py
@@ -21,13 +21,16 @@ from __future__ import print_function
 import logging
 import optparse
 import os
+import sys
 from gettext import gettext as _
 
+import gio
 import gobject
 import gtk
 
 from . import filters
 from . import preferences
+from . import recent
 
 version = "1.7.0"
 
@@ -50,6 +53,7 @@ class MeldApp(gobject.GObject):
                                                 filters.FilterEntry.SHELL)
         self.text_filters = self._parse_filters(self.prefs.regexes,
                                                 filters.FilterEntry.REGEX)
+        self.recent_comparisons = recent.RecentFiles(sys.argv[0])
 
     def create_window(self):
         self.window = meldwindow.MeldWindow()
@@ -115,6 +119,9 @@ class MeldApp(gobject.GObject):
             help=_("Set the target file for saving a merge result"))
         parser.add_option("--auto-merge", None, action="store_true", default=False,
             help=_("Automatically merge files"))
+        parser.add_option("", "--comparison-file", action="store",
+            type="string", dest="comparison_file", default=None,
+            help=_("Load a saved comparison from a Meld comparison file"))
         parser.add_option("", "--diff", action="callback", callback=self.diff_files_callback,
                           dest="diff", default=[],
                           help=_("Creates a diff tab for up to 3 supplied files or directories."))
@@ -140,7 +147,16 @@ class MeldApp(gobject.GObject):
         for files in options.diff:
             open_paths(files)
 
-        tab = open_paths(args, options.auto_compare, options.auto_merge)
+        if options.comparison_file:
+            comparison_file_path = os.path.expanduser(options.comparison_file)
+            gio_file = gio.File(path=comparison_file_path)
+            try:
+                tab = self.window.append_recent(gio_file.get_uri())
+            except (IOError, ValueError):
+                parser.error(_("Error reading saved comparison file"))
+        elif args:
+            tab = open_paths(args, options.auto_compare, options.auto_merge)
+
         if options.label and tab:
             tab.set_labels(options.label)
 
diff --git a/meld/melddoc.py b/meld/melddoc.py
index 0bf38b9..7414e6a 100644
--- a/meld/melddoc.py
+++ b/meld/melddoc.py
@@ -62,6 +62,10 @@ class MeldDoc(gobject.GObject):
     def get_info_widgets(self):
         return self.status_info_labels
 
+    def get_comparison(self):
+        """Get the comparison type and path(s) being compared"""
+        pass
+
     def save(self):
         pass
 
diff --git a/meld/meldwindow.py b/meld/meldwindow.py
index 99cdf30..41cb1e9 100644
--- a/meld/meldwindow.py
+++ b/meld/meldwindow.py
@@ -29,6 +29,7 @@ from . import filemerge
 from . import misc
 from . import paths
 from . import preferences
+from . import recent
 from . import task
 from . import vcview
 from .ui import gnomeglade
@@ -89,6 +90,9 @@ class NewDocDialog(gnomeglade.Component):
             new_tab_idx = self.parentapp.notebook.page_num(new_tab.widget)
             self.parentapp.notebook.set_current_page(new_tab_idx)
 
+            diff_type = recent.COMPARISON_TYPES[page]
+            app.recent_comparisons.add(new_tab)
+
         self.widget.destroy()
 
 
@@ -162,6 +166,15 @@ class MeldWindow(gnomeglade.Component):
         self.actiongroup.set_translation_domain("meld")
         self.actiongroup.add_actions(actions)
         self.actiongroup.add_toggle_actions(toggleactions)
+
+        recent_action = gtk.RecentAction("Recent",  _("Open Recent"),
+                                         _("Open recent files"), None)
+        recent_action.set_show_private(True)
+        recent_action.set_filter(app.recent_comparisons.recent_filter)
+        recent_action.set_sort_type(gtk.RECENT_SORT_MRU)
+        recent_action.connect("item-activated", self.on_action_recent)
+        self.actiongroup.add_action(recent_action)
+
         self.ui = gtk.UIManager()
         self.ui.insert_action_group(self.actiongroup, 0)
         self.ui.add_ui_from_file(ui_file)
@@ -391,6 +404,16 @@ class MeldWindow(gnomeglade.Component):
     def on_menu_save_as_activate(self, menuitem):
         self.current_doc().save_as()
 
+    def on_action_recent(self, action):
+        uri = action.get_current_uri()
+        if not uri:
+            return
+        try:
+            self.append_recent(uri)
+        except (IOError, ValueError):
+            # FIXME: Need error handling, but no sensible display location
+            pass
+
     def on_menu_close_activate(self, *extra):
         i = self.notebook.get_current_page()
         if i >= 0:
@@ -672,6 +695,19 @@ class MeldWindow(gnomeglade.Component):
             doc.on_button_diff_clicked(None)
         return doc
 
+    def append_recent(self, uri):
+        comparison_type, files, flags = app.recent_comparisons.read(uri)
+        if comparison_type == recent.TYPE_MERGE:
+            tab = self.append_filemerge(files)
+        elif comparison_type == recent.TYPE_FOLDER:
+            tab = self.append_dirdiff(files)
+        elif comparison_type == recent.TYPE_VC:
+            tab = self.append_vcview(files)
+        else:  # comparison_type == recent.TYPE_FILE:
+            tab = self.append_filediff(files)
+        app.recent_comparisons.add(tab)
+        return tab
+
     def _single_file_open(self, path):
         doc = vcview.VcView(app.prefs)
         def cleanup():
@@ -694,6 +730,7 @@ class MeldWindow(gnomeglade.Component):
 
         elif len(paths) in (2, 3):
             tab = self.append_diff(paths, auto_compare, auto_merge)
+        app.recent_comparisons.add(tab)
         return tab
 
     def current_doc(self):
diff --git a/meld/recent.py b/meld/recent.py
new file mode 100644
index 0000000..a51500c
--- /dev/null
+++ b/meld/recent.py
@@ -0,0 +1,215 @@
+### Copyright (C) 2012 Kai Willadsen <kai willadsen gmail com>
+
+### 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+"""
+Recent files integration for Meld's multi-element comparisons
+
+The GTK+ recent files mechanism is designed to take only single files with a
+limited set of metadata. In Meld, we almost always need to enter pairs or
+triples of files or directories, along with some information about the
+comparison type. The solution provided by this module is to create fake
+single-file registers for multi-file comparisons, and tell the recent files
+infrastructure that that's actually what we opened.
+"""
+
+import ConfigParser
+import os
+import tempfile
+
+import gio
+import glib
+import gtk
+
+from . import misc
+
+
+TYPE_FILE = "File"
+TYPE_FOLDER = "Folder"
+TYPE_VC = "Version control"
+TYPE_MERGE = "Merge"
+COMPARISON_TYPES = (TYPE_FILE, TYPE_FOLDER, TYPE_VC, TYPE_MERGE)
+
+
+class RecentFiles(object):
+
+    recent_path = os.path.join(glib.get_user_data_dir(), "meld")
+    recent_suffix = ".meldcmp"
+
+    # Recent data
+    app_name = "Meld"
+    app_exec = "meld"
+
+    def __init__(self, exec_path=None):
+        self.recent_manager = gtk.recent_manager_get_default()
+        self.recent_filter = gtk.RecentFilter()
+        self.recent_filter.add_application(self.app_name)
+        self._stored_comparisons = []
+        # Should be argv[0] to support roundtripping in uninstalled use
+        if exec_path:
+            self.app_exec = os.path.abspath(exec_path)
+
+        if not os.path.exists(self.recent_path):
+            os.makedirs(self.recent_path)
+
+        self._clean_recent_files()
+        self._update_recent_files()
+        self.recent_manager.connect("changed", self._update_recent_files)
+
+    def add(self, tab, flags=None):
+        """Add a tab to our recently-used comparison list
+
+        The passed flags are currently ignored. In the future these are to be
+        used for extra initialisation not captured by the tab itself.
+        """
+        comp_type, paths = tab.get_comparison()
+
+        # While Meld handles comparisons including None, recording these as
+        # recently-used comparisons just isn't that sane.
+        if None in paths:
+            return
+
+        # If a (type, paths) comparison is already registered, then re-add
+        # the corresponding comparison file
+        comparison_key = (comp_type, tuple(paths))
+        if comparison_key in self._stored_comparisons:
+            gio_file = gio.File(uri=self._stored_comparisons[comparison_key])
+        else:
+            recent_path = self._write_recent_file(comp_type, paths)
+            gio_file = gio.File(path=recent_path)
+
+        if len(paths) > 1:
+            display_name = " vs. ".join(misc.shorten_names(*paths))
+        else:
+            display_name = "Comparison " + paths[0]
+        description = "%s comparison\n%s" % (comp_type, ", ".join(paths))
+
+        recent_metadata = {
+            "mime_type": "application/x-meld-comparison",
+            "app_name": self.app_name,
+            "app_exec": "%s --comparison-file %%u" % self.app_exec,
+            "display_name": display_name,
+            "description": description,
+            "is_private": True,
+        }
+        self.recent_manager.add_full(gio_file.get_uri(), recent_metadata)
+
+    def read(self, uri):
+        """Read stored comparison from URI
+
+        Returns the comparison type, the paths involved and the comparison
+        flags.
+        """
+        gio_file = gio.File(uri=uri)
+        path = gio_file.get_path()
+        if not gio_file.query_exists() or not path:
+            raise IOError("File does not exist")
+
+        config = ConfigParser.RawConfigParser()
+        config.read(path)
+
+        if not (config.has_section("Comparison") and
+                config.has_option("Comparison", "type") and
+                config.has_option("Comparison", "paths")):
+            raise ValueError("Invalid recent comparison file")
+
+        comp_type = config.get("Comparison", "type")
+        paths = tuple(config.get("Comparison", "paths").split(";"))
+        flags = tuple()
+
+        if comp_type not in COMPARISON_TYPES:
+            raise ValueError("Invalid recent comparison file")
+
+        return comp_type, paths, flags
+
+    def _write_recent_file(self, comp_type, paths):
+        # TODO: Use GKeyFile instead, and return a gio.File. This is why we're
+        # using ';' to join comparison paths.
+        with tempfile.NamedTemporaryFile(prefix='recent-',
+                                         suffix=self.recent_suffix,
+                                         dir=self.recent_path,
+                                         delete=False) as f:
+            config = ConfigParser.RawConfigParser()
+            config.add_section("Comparison")
+            config.set("Comparison", "type", comp_type)
+            config.set("Comparison", "paths", ";".join(paths))
+            config.write(f)
+            name = f.name
+        return name
+
+    def _clean_recent_files(self):
+        # Remove from RecentManager any comparisons with no existing file
+        meld_items = self._filter_items(self.recent_filter,
+                                        self.recent_manager.get_items())
+        for item in meld_items:
+            if not item.exists():
+                self.recent_manager.remove_item(item.get_uri())
+
+        meld_items = [item for item in meld_items if item.exists()]
+
+        # Remove any comparison files that are not listed by RecentManager
+        item_uris = [item.get_uri() for item in meld_items]
+        item_paths = [gio.File(uri=uri).get_path() for uri in item_uris]
+        stored = [p for p in os.listdir(self.recent_path)
+                  if p.endswith(self.recent_suffix)]
+        for path in stored:
+            file_path = os.path.abspath(os.path.join(self.recent_path, path))
+            if file_path not in item_paths:
+                os.remove(file_path)
+
+    def _update_recent_files(self, *args):
+        meld_items = self._filter_items(self.recent_filter,
+                                        self.recent_manager.get_items())
+        item_uris = [item.get_uri() for item in meld_items if item.exists()]
+        self._stored_comparisons = {}
+        for uri in item_uris:
+            try:
+                comp = self.read(uri)
+            except (IOError, ValueError):
+                continue
+            # Store and look up comparisons by type and paths, ignoring flags
+            self._stored_comparisons[comp[:2]] = uri
+
+    def _filter_items(self, recent_filter, items):
+        getters = {gtk.RECENT_FILTER_URI: "uri",
+                   gtk.RECENT_FILTER_DISPLAY_NAME: "display_name",
+                   gtk.RECENT_FILTER_MIME_TYPE: "mime_type",
+                   gtk.RECENT_FILTER_APPLICATION: "applications",
+                   gtk.RECENT_FILTER_GROUP: "group",
+                   gtk.RECENT_FILTER_AGE: "age"}
+        needed = recent_filter.get_needed()
+        attrs = [v for k, v in getters.iteritems() if needed & k]
+
+        filtered_items = []
+        for i in items:
+            filter_info = {}
+            for attr in attrs:
+                filter_info[attr] = getattr(i, "get_" + attr)()
+            if recent_filter.filter(filter_info):
+                filtered_items.append(i)
+        return filtered_items
+
+    def __str__(self):
+        items = self.recent_manager.get_items()
+        descriptions = []
+        for i in self._filter_items(self.recent_filter, items):
+            descriptions.append("%s\n%s\n" % (i.get_display_name(),
+                                              i.get_uri_display()))
+        return "\n".join(descriptions)
+
+
+if __name__ == "__main__":
+    recent = RecentFiles()
+    print recent
diff --git a/meld/vcview.py b/meld/vcview.py
index 2285927..019cd39 100644
--- a/meld/vcview.py
+++ b/meld/vcview.py
@@ -31,6 +31,7 @@ import pango
 from . import melddoc
 from . import misc
 from . import paths
+from . import recent
 from . import tree
 from . import vc
 from .ui import emblemcellrenderer
@@ -356,6 +357,9 @@ class VcView(melddoc.MeldDoc, gnomeglade.Component):
             self.scheduler.add_task(self._search_recursively_iter(root))
             self.scheduler.add_task(self.on_treeview_cursor_changed)
 
+    def get_comparison(self):
+        return recent.TYPE_VC, [self.location]
+
     def recompute_label(self):
         self.label_text = os.path.basename(self.location)
         # TRANSLATORS: This is the location of the directory the user is diffing



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