Commit 0fccdabd authored by Kai Willadsen's avatar Kai Willadsen

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.
parent b831bb7e
......@@ -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
......@@ -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
......
<?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>
......@@ -7,6 +7,7 @@
<separator/>
<placeholder name="FileActionsPlaceholder" />
<separator/>
<menuitem action="Recent" />
<menuitem action="Close" />
<menuitem action="Quit" />
</menu>
......
......@@ -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'.
"""
......
......@@ -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
......
......@@ -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):
......
......@@ -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)
......
......@@ -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
......
......@@ -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):
......
### 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
......@@ -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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment