meldwindow.py 27.6 KB
Newer Older
1 2
# Copyright (C) 2002-2006 Stephen Kennedy <stevek@gnome.org>
# Copyright (C) 2010-2013 Kai Willadsen <kai.willadsen@gmail.com>
3
#
4 5 6 7 8 9 10 11 12 13 14 15
# 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, see <http://www.gnu.org/licenses/>.
16 17 18

import os

19
from gi.repository import Gdk
20 21
from gi.repository import Gio
from gi.repository import GLib
22
from gi.repository import Gtk
23

24
import meld.ui.util
25 26 27
from . import dirdiff
from . import filediff
from . import filemerge
28
from . import melddoc
Kai Willadsen's avatar
Kai Willadsen committed
29
from . import newdifftab
30
from . import recent
31 32 33 34 35
from . import task
from . import vcview
from .ui import gnomeglade
from .ui import notebooklabel

36
from meld.conf import _
37
from meld.recent import recent_comparisons
38
from meld.settings import interface_settings, settings
39
from meld.windowstate import SavedWindowState
40

41 42 43 44

class MeldWindow(gnomeglade.Component):

    def __init__(self):
45
        gnomeglade.Component.__init__(self, "meldapp.ui", "meldapp")
46
        self.widget.set_name("meldapp")
47 48 49

        actions = (
            ("FileMenu", None, _("_File")),
50
            ("New", Gtk.STOCK_NEW, _("_New Comparison…"), "<Primary>N",
51 52
                _("Start a new comparison"),
                self.on_menu_file_new_activate),
53
            ("Save", Gtk.STOCK_SAVE, None, None,
54 55
                _("Save the current file"),
                self.on_menu_save_activate),
56
            ("SaveAs", Gtk.STOCK_SAVE_AS, _("Save As…"), "<Primary><shift>S",
57 58
                _("Save the current file with a different name"),
                self.on_menu_save_as_activate),
59
            ("Close", Gtk.STOCK_CLOSE, None, None,
60 61
                _("Close the current file"),
                self.on_menu_close_activate),
62 63

            ("EditMenu", None, _("_Edit")),
64
            ("Undo", Gtk.STOCK_UNDO, None, "<Primary>Z",
65 66
                _("Undo the last action"),
                self.on_menu_undo_activate),
67
            ("Redo", Gtk.STOCK_REDO, None, "<Primary><shift>Z",
68 69
                _("Redo the last undone action"),
                self.on_menu_redo_activate),
70
            ("Cut", Gtk.STOCK_CUT, None, None, _("Cut the selection"),
71
                self.on_menu_cut_activate),
72
            ("Copy", Gtk.STOCK_COPY, None, None, _("Copy the selection"),
73
                self.on_menu_copy_activate),
74
            ("Paste", Gtk.STOCK_PASTE, None, None, _("Paste the clipboard"),
75
                self.on_menu_paste_activate),
76
            ("Find", Gtk.STOCK_FIND, _("Find…"), None, _("Search for text"),
77
                self.on_menu_find_activate),
78
            ("FindNext", None, _("Find Ne_xt"), "<Primary>G",
79 80
                _("Search forwards for the same text"),
                self.on_menu_find_next_activate),
81
            ("FindPrevious", None, _("Find _Previous"), "<Primary><shift>G",
82 83
                _("Search backwards for the same text"),
                self.on_menu_find_previous_activate),
84
            ("Replace", Gtk.STOCK_FIND_AND_REPLACE,
85
                _("_Replace…"), "<Primary>H",
86 87
                _("Find and replace text"),
                self.on_menu_replace_activate),
88 89

            ("ChangesMenu", None, _("_Changes")),
90
            ("NextChange", Gtk.STOCK_GO_DOWN, _("Next Change"), "<Alt>Down",
91 92
                _("Go to the next change"),
                self.on_menu_edit_down_activate),
93
            ("PrevChange", Gtk.STOCK_GO_UP, _("Previous Change"), "<Alt>Up",
94 95 96 97 98 99
                _("Go to the previous change"),
                self.on_menu_edit_up_activate),
            ("OpenExternal", None, _("Open Externally"), None,
                _("Open selected file or directory in the default external "
                  "application"),
                self.on_open_external),
100 101

            ("ViewMenu", None, _("_View")),
102 103 104
            ("FileStatus", None, _("File Status")),
            ("VcStatus", None, _("Version Status")),
            ("FileFilters", None, _("File Filters")),
105
            ("Stop", Gtk.STOCK_STOP, None, "Escape",
106 107
                _("Stop the current action"),
                self.on_toolbar_stop_clicked),
108
            ("Refresh", Gtk.STOCK_REFRESH, None, "<Primary>R",
109 110
                _("Refresh the view"),
                self.on_menu_refresh_activate),
111 112
        )
        toggleactions = (
113 114
            ("Fullscreen", None, _("Fullscreen"), "F11",
                _("View the comparison in fullscreen"),
115 116 117
                self.on_action_fullscreen_toggled, False),
            ("ToolbarVisible", None, _("_Toolbar"), None,
                _("Show or hide the toolbar"),
118
                None, True),
119
        )
120
        ui_file = gnomeglade.ui_file("meldapp-ui.xml")
121
        self.actiongroup = Gtk.ActionGroup(name='MainActions')
122 123 124
        self.actiongroup.set_translation_domain("meld")
        self.actiongroup.add_actions(actions)
        self.actiongroup.add_toggle_actions(toggleactions)
125

126 127 128
        recent_action = Gtk.RecentAction(
            name="Recent",  label=_("Open Recent"),
            tooltip=_("Open recent files"), stock_id=None)
129
        recent_action.set_show_private(True)
130
        recent_action.set_filter(recent_comparisons.recent_filter)
131
        recent_action.set_sort_type(Gtk.RecentSortType.MRU)
132 133 134
        recent_action.connect("item-activated", self.on_action_recent)
        self.actiongroup.add_action(recent_action)

135
        self.ui = Gtk.UIManager()
136 137
        self.ui.insert_action_group(self.actiongroup, 0)
        self.ui.add_ui_from_file(ui_file)
138 139 140 141

        # Manually handle shells that don't show an application menu
        gtk_settings = Gtk.Settings.get_default()
        if not gtk_settings.props.gtk_shell_shows_app_menu:
142
            from meld.meldapp import app
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170

            def make_app_action(name):
                def app_action(*args):
                    app.lookup_action(name).activate(None)
                return app_action

            app_actions = (
                ("AppMenu", None, _("_Meld")),
                ("Quit", Gtk.STOCK_QUIT, None, None, _("Quit the program"),
                 make_app_action('quit')),
                ("Preferences", Gtk.STOCK_PREFERENCES, _("Prefere_nces"), None,
                 _("Configure the application"),
                 make_app_action('preferences')),
                ("Help", Gtk.STOCK_HELP, _("_Contents"), "F1",
                 _("Open the Meld manual"), make_app_action('help')),
                ("About", Gtk.STOCK_ABOUT, None, None,
                 _("About this application"), make_app_action('about')),
            )

            app_actiongroup = Gtk.ActionGroup(name="AppActions")
            app_actiongroup.set_translation_domain("meld")
            app_actiongroup.add_actions(app_actions)
            self.ui.insert_action_group(app_actiongroup, 0)

            ui_file = gnomeglade.ui_file("appmenu-fallback.xml")
            self.ui.add_ui_from_file(ui_file)
            self.widget.set_show_menubar(False)

171 172 173 174 175
        for menuitem in ("Save", "Undo"):
            self.actiongroup.get_action(menuitem).props.is_important = True
        self.widget.add_accel_group(self.ui.get_accel_group())
        self.menubar = self.ui.get_widget('/Menubar')
        self.toolbar = self.ui.get_widget('/Toolbar')
176 177
        self.toolbar.get_style_context().add_class(
            Gtk.STYLE_CLASS_PRIMARY_TOOLBAR)
178

179 180 181 182 183
        settings.bind('toolbar-visible',
                      self.actiongroup.get_action('ToolbarVisible'), 'active',
                      Gio.SettingsBindFlags.DEFAULT)
        settings.bind('toolbar-visible', self.toolbar, 'visible',
                      Gio.SettingsBindFlags.DEFAULT)
184 185 186
        interface_settings.bind('toolbar-style', self.toolbar, 'toolbar-style',
                                Gio.SettingsBindFlags.DEFAULT)

187 188
        # Add alternate keybindings for Prev/Next Change
        accels = self.ui.get_accel_group()
189
        (keyval, mask) = Gtk.accelerator_parse("<Primary>D")
190
        accels.connect(keyval, mask, 0, self.on_menu_edit_down_activate)
191
        (keyval, mask) = Gtk.accelerator_parse("<Primary>E")
192 193 194
        accels.connect(keyval, mask, 0, self.on_menu_edit_up_activate)
        (keyval, mask) = Gtk.accelerator_parse("F5")
        accels.connect(keyval, mask, 0, self.on_menu_refresh_activate)
195

196 197 198 199
        # Initialise sensitivity for important actions
        self.actiongroup.get_action("Stop").set_sensitive(False)
        self._update_page_action_sensitivity()

200
        self.appvbox.pack_start(self.menubar, False, True, 0)
201
        self.toolbar_holder.pack_start(self.toolbar, True, True, 0)
202 203

        # Double toolbars to work around UIManager integration issues
204
        self.secondary_toolbar = Gtk.Toolbar()
205 206 207 208
        self.secondary_toolbar.get_style_context().add_class(
            Gtk.STYLE_CLASS_PRIMARY_TOOLBAR)
        self.toolbar_holder.pack_end(self.secondary_toolbar, False, True, 0)

209 210 211 212 213 214 215 216 217
        # Manually handle GAction additions
        actions = (
            ("close", self.on_menu_close_activate, None),
        )
        for (name, callback, accel) in actions:
            action = Gio.SimpleAction.new(name, None)
            action.connect('activate', callback)
            self.widget.add_action(action)

218 219 220 221 222 223 224 225
        toolbutton = Gtk.ToolItem()
        self.spinner = Gtk.Spinner()
        toolbutton.add(self.spinner)
        self.secondary_toolbar.insert(toolbutton, -1)
        # Set a minimum size because the spinner requests nothing
        self.secondary_toolbar.set_size_request(30, -1)
        self.secondary_toolbar.show_all()

226
        self.widget.drag_dest_set(
227 228
            Gtk.DestDefaults.MOTION | Gtk.DestDefaults.HIGHLIGHT |
            Gtk.DestDefaults.DROP,
229 230
            None, Gdk.DragAction.COPY)
        self.widget.drag_dest_add_uri_targets()
231 232
        self.widget.connect("drag_data_received",
                            self.on_widget_drag_data_received)
233

234 235
        self.window_state = SavedWindowState()
        self.window_state.bind(self.widget)
236 237 238 239 240 241

        self.should_close = False
        self.idle_hooked = 0
        self.scheduler = task.LifoScheduler()
        self.scheduler.connect("runnable", self.on_scheduler_runnable)

242 243
        self.ui.ensure_update()
        self.diff_handler = None
244
        self.undo_handlers = tuple()
245 246 247
        self.widget.connect('focus_in_event', self.on_focus_change)
        self.widget.connect('focus_out_event', self.on_focus_change)

248 249 250 251
        # Set tooltip on map because the recentmenu is lazily created
        rmenu = self.ui.get_widget('/Menubar/FileMenu/Recent').get_submenu()
        rmenu.connect("map", self._on_recentmenu_map)

252 253 254 255 256 257 258 259
        try:
            builder = meld.ui.util.get_builder("shortcuts.ui")
            shortcut_window = builder.get_object("shortcuts-meld")
            self.widget.set_help_overlay(shortcut_window)
        except GLib.Error:
            # GtkShortcutsWindow is new in GTK+ 3.20
            pass

260 261 262 263
    def _on_recentmenu_map(self, recentmenu):
        for imagemenuitem in recentmenu.get_children():
            imagemenuitem.set_tooltip_text(imagemenuitem.get_label())

Kai Willadsen's avatar
Kai Willadsen committed
264
    def on_focus_change(self, widget, event, callback_data=None):
265 266
        for idx in range(self.notebook.get_n_pages()):
            w = self.notebook.get_nth_page(idx)
267 268
            if hasattr(w.pyobject, 'on_focus_change'):
                w.pyobject.on_focus_change()
269 270 271
        # Let the rest of the stack know about this event
        return False

272 273
    def on_widget_drag_data_received(self, wid, context, x, y, selection_data,
                                     info, time):
274 275
        if len(selection_data.get_files()) != 0:
            self.open_paths(selection_data.get_files())
276 277 278 279
            return True

    def on_idle(self):
        ret = self.scheduler.iteration()
280
        if ret and isinstance(ret, str):
281
            self.spinner.set_tooltip_text(ret)
282 283 284

        pending = self.scheduler.tasks_pending()
        if not pending:
285 286 287
            self.spinner.stop()
            self.spinner.hide()
            self.spinner.set_tooltip_text("")
288
            self.idle_hooked = None
289
            self.actiongroup.get_action("Stop").set_sensitive(False)
290
        return pending
291 292 293

    def on_scheduler_runnable(self, sched):
        if not self.idle_hooked:
294 295
            self.spinner.show()
            self.spinner.start()
296
            self.actiongroup.get_action("Stop").set_sensitive(True)
297
            self.idle_hooked = GLib.idle_add(self.on_idle)
298 299

    def on_delete_event(self, *extra):
300
        should_cancel = False
301 302 303 304 305
        # Delete pages from right-to-left.  This ensures that if a version
        # control page is open in the far left page, it will be closed last.
        for c in reversed(self.notebook.get_children()):
            page = c.pyobject
            self.notebook.set_current_page(self.notebook.page_num(page.widget))
306
            response = page.on_delete_event()
307
            if response == Gtk.ResponseType.CANCEL:
308
                should_cancel = True
309

310 311 312 313
        should_cancel = should_cancel or self.has_pages()
        if should_cancel:
            self.should_close = True
        return should_cancel
314

315 316 317
    def has_pages(self):
        return self.notebook.get_n_pages() > 0

318 319 320
    def _update_page_action_sensitivity(self):
        current_page = self.notebook.get_current_page()

321
        if current_page != -1:
322
            page = self.notebook.get_nth_page(current_page).pyobject
323 324 325 326 327
        else:
            page = None

        self.actiongroup.get_action("Close").set_sensitive(bool(page))
        if not isinstance(page, melddoc.MeldDoc):
328 329 330 331
            for action in ("PrevChange", "NextChange", "Cut", "Copy", "Paste",
                           "Find", "FindNext", "FindPrevious", "Replace",
                           "Refresh"):
                self.actiongroup.get_action(action).set_sensitive(False)
332 333 334 335 336 337 338
        else:
            for action in ("Find", "Refresh"):
                self.actiongroup.get_action(action).set_sensitive(True)
            is_filediff = isinstance(page, filediff.FileDiff)
            for action in ("Cut", "Copy", "Paste", "FindNext", "FindPrevious",
                           "Replace"):
                self.actiongroup.get_action(action).set_sensitive(is_filediff)
339

340 341 342 343 344 345 346 347 348 349
    def handle_current_doc_switch(self, page):
        if self.diff_handler is not None:
            page.disconnect(self.diff_handler)
        page.on_container_switch_out_event(self.ui)
        if self.undo_handlers:
            undoseq = page.undosequence
            for handler in self.undo_handlers:
                undoseq.disconnect(handler)
            self.undo_handlers = tuple()

350 351 352
    def on_switch_page(self, notebook, page, which):
        oldidx = notebook.get_current_page()
        if oldidx >= 0:
353
            olddoc = notebook.get_nth_page(oldidx).pyobject
354
            self.handle_current_doc_switch(olddoc)
355

356
        newdoc = notebook.get_nth_page(which).pyobject if which >= 0 else None
357 358 359 360 361 362 363 364 365 366 367 368
        try:
            undoseq = newdoc.undosequence
            can_undo = undoseq.can_undo()
            can_redo = undoseq.can_redo()
            undo_handler = undoseq.connect("can-undo", self.on_can_undo)
            redo_handler = undoseq.connect("can-redo", self.on_can_redo)
            self.undo_handlers = (undo_handler, redo_handler)
        except AttributeError:
            can_undo, can_redo = False, False
        self.actiongroup.get_action("Undo").set_sensitive(can_undo)
        self.actiongroup.get_action("Redo").set_sensitive(can_redo)

369 370 371 372 373 374 375
        # FileDiff handles save sensitivity; it makes no sense for other modes
        if not isinstance(newdoc, filediff.FileDiff):
            self.actiongroup.get_action("Save").set_sensitive(False)
            self.actiongroup.get_action("SaveAs").set_sensitive(False)
        else:
            self.actiongroup.get_action("SaveAs").set_sensitive(True)

376 377
        if newdoc:
            nbl = self.notebook.get_tab_label(newdoc.widget)
378
            self.widget.set_title(nbl.get_label_text())
379 380 381 382
            newdoc.on_container_switch_in_event(self.ui)
        else:
            self.widget.set_title("Meld")

383
        if isinstance(newdoc, melddoc.MeldDoc):
384 385
            self.diff_handler = newdoc.connect("next-diff-changed",
                                               self.on_next_diff_changed)
386 387
        else:
            self.diff_handler = None
388 389
        if hasattr(newdoc, 'scheduler'):
            self.scheduler.add_task(newdoc.scheduler)
390

391
    def after_switch_page(self, notebook, page, which):
392
        self._update_page_action_sensitivity()
393

394 395 396
    def after_page_reordered(self, notebook, page, page_num):
        self._update_page_action_sensitivity()

397
    def on_page_label_changed(self, notebook, label_text):
398
        self.widget.set_title(label_text)
399 400 401 402 403 404 405 406 407 408 409 410

    def on_can_undo(self, undosequence, can):
        self.actiongroup.get_action("Undo").set_sensitive(can)

    def on_can_redo(self, undosequence, can):
        self.actiongroup.get_action("Redo").set_sensitive(can)

    def on_next_diff_changed(self, doc, have_prev, have_next):
        self.actiongroup.get_action("PrevChange").set_sensitive(have_prev)
        self.actiongroup.get_action("NextChange").set_sensitive(have_next)

    def on_menu_file_new_activate(self, menuitem):
411
        self.append_new_comparison()
412 413 414 415 416 417 418

    def on_menu_save_activate(self, menuitem):
        self.current_doc().save()

    def on_menu_save_as_activate(self, menuitem):
        self.current_doc().save_as()

419 420 421 422 423 424 425 426 427 428
    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

429 430 431
    def on_menu_close_activate(self, *extra):
        i = self.notebook.get_current_page()
        if i >= 0:
432
            page = self.notebook.get_nth_page(i).pyobject
433
            page.on_delete_event()
434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449

    def on_menu_undo_activate(self, *extra):
        self.current_doc().on_undo_activate()

    def on_menu_redo_activate(self, *extra):
        self.current_doc().on_redo_activate()

    def on_menu_refresh_activate(self, *extra):
        self.current_doc().on_refresh_activate()

    def on_menu_find_activate(self, *extra):
        self.current_doc().on_find_activate()

    def on_menu_find_next_activate(self, *extra):
        self.current_doc().on_find_next_activate()

450 451 452
    def on_menu_find_previous_activate(self, *extra):
        self.current_doc().on_find_previous_activate()

453 454 455 456 457
    def on_menu_replace_activate(self, *extra):
        self.current_doc().on_replace_activate()

    def on_menu_copy_activate(self, *extra):
        widget = self.widget.get_focus()
458
        if isinstance(widget, Gtk.Editable):
459
            widget.copy_clipboard()
460
        elif isinstance(widget, Gtk.TextView):
461 462 463 464
            widget.emit("copy-clipboard")

    def on_menu_cut_activate(self, *extra):
        widget = self.widget.get_focus()
465
        if isinstance(widget, Gtk.Editable):
466
            widget.cut_clipboard()
467
        elif isinstance(widget, Gtk.TextView):
468 469 470 471
            widget.emit("cut-clipboard")

    def on_menu_paste_activate(self, *extra):
        widget = self.widget.get_focus()
472
        if isinstance(widget, Gtk.Editable):
473
            widget.paste_clipboard()
474
        elif isinstance(widget, Gtk.TextView):
475 476 477
            widget.emit("paste-clipboard")

    def on_action_fullscreen_toggled(self, widget):
478 479
        window_state = self.widget.get_window().get_state()
        is_full = window_state & Gdk.WindowState.FULLSCREEN
480 481 482 483 484 485
        if widget.get_active() and not is_full:
            self.widget.fullscreen()
        elif is_full:
            self.widget.unfullscreen()

    def on_menu_edit_down_activate(self, *args):
486
        self.current_doc().next_diff(Gdk.ScrollDirection.DOWN)
487 488

    def on_menu_edit_up_activate(self, *args):
489
        self.current_doc().next_diff(Gdk.ScrollDirection.UP)
490

491 492 493
    def on_open_external(self, *args):
        self.current_doc().open_external()

494 495 496
    def on_toolbar_stop_clicked(self, *args):
        self.current_doc().stop()

497 498 499 500 501 502
    def page_removed(self, page, status):
        if hasattr(page, 'scheduler'):
            self.scheduler.remove_scheduler(page.scheduler)

        page_num = self.notebook.page_num(page.widget)

503 504 505
        if self.notebook.get_current_page() == page_num:
            self.handle_current_doc_switch(page)

506
        self.notebook.remove_page(page_num)
507 508
        # Normal switch-page handlers don't get run for removing the
        # last page from a notebook.
509
        if not self.has_pages():
510 511
            self.on_switch_page(self.notebook, page, -1)
            self._update_page_action_sensitivity()
512 513 514
            # Synchronise UIManager state; this shouldn't be necessary,
            # but upstream aren't touching UIManager bugs.
            self.ui.ensure_update()
515 516 517 518 519
            if self.should_close:
                cancelled = self.widget.emit(
                    'delete-event', Gdk.Event.new(Gdk.EventType.DELETE))
                if not cancelled:
                    self.widget.emit('destroy')
520

521 522 523 524
    def on_page_state_changed(self, page, old_state, new_state):
        if self.should_close and old_state == melddoc.STATE_CLOSING:
            # Cancel closing if one of our tabs does
            self.should_close = False
525 526 527

    def on_file_changed(self, srcpage, filename):
        for c in self.notebook.get_children():
528
            page = c.pyobject
529 530 531 532
            if page != srcpage:
                page.on_file_changed(filename)

    def _append_page(self, page, icon):
533 534
        nbl = notebooklabel.NotebookLabel(
            icon, "", lambda b: page.on_delete_event())
535
        self.notebook.append_page(page.widget, nbl)
536 537

        # Change focus to the newly created page only if the user is on a
538 539
        # DirDiff or VcView page, or if it's a new tab page. This prevents
        # cycling through X pages when X diffs are initiated.
540
        if isinstance(self.current_doc(), dirdiff.DirDiff) or \
541
           isinstance(self.current_doc(), vcview.VcView) or \
Kai Willadsen's avatar
Kai Willadsen committed
542
           isinstance(page, newdifftab.NewDiffTab):
543 544
            self.notebook.set_current_page(self.notebook.page_num(page.widget))

545 546 547 548
        if hasattr(page, 'scheduler'):
            self.scheduler.add_scheduler(page.scheduler)
        if isinstance(page, melddoc.MeldDoc):
            page.connect("file-changed", self.on_file_changed)
549 550
            page.connect("create-diff", lambda obj, arg, kwargs:
                         self.append_diff(arg, **kwargs))
551
            page.connect("state-changed", self.on_page_state_changed)
552
        page.connect("close", self.page_removed)
553

554
        self.notebook.set_tab_reorderable(page.widget, True)
555

556
    def append_new_comparison(self):
Kai Willadsen's avatar
Kai Willadsen committed
557
        doc = newdifftab.NewDiffTab(self)
558
        self._append_page(doc, "document-new")
559
        self.notebook.on_label_changed(doc, _("New comparison"), None)
560 561

        def diff_created_cb(doc, newdoc):
562
            doc.on_delete_event()
563 564 565 566
            idx = self.notebook.page_num(newdoc.widget)
            self.notebook.set_current_page(idx)

        doc.connect("diff-created", diff_created_cb)
567 568
        return doc

569 570
    def append_dirdiff(self, gfiles, auto_compare=False):
        dirs = [d.get_path() for d in gfiles if d]
Kai Willadsen's avatar
Kai Willadsen committed
571
        assert len(dirs) in (1, 2, 3)
572
        doc = dirdiff.DirDiff(len(dirs))
573 574 575
        self._append_page(doc, "folder")
        doc.set_locations(dirs)
        if auto_compare:
576
            doc.scheduler.add_task(doc.auto_compare)
577 578
        return doc

579 580 581
    def append_filediff(self, gfiles, merge_output=None, meta=None):
        assert len(gfiles) in (1, 2, 3)
        doc = filediff.FileDiff(len(gfiles))
Piotr Piastucki's avatar
Piotr Piastucki committed
582
        self._append_page(doc, "text-x-generic")
583
        doc.set_files(gfiles)
584 585
        if merge_output is not None:
            doc.set_merge_output_file(merge_output)
586 587
        if meta is not None:
            doc.set_meta(meta)
Piotr Piastucki's avatar
Piotr Piastucki committed
588 589
        return doc

590 591
    def append_filemerge(self, gfiles, merge_output=None):
        if len(gfiles) != 3:
592
            raise ValueError(
593 594 595
                _("Need three files to auto-merge, got: %r") %
                [f.get_parse_name() for f in gfiles])
        doc = filemerge.FileMerge(len(gfiles))
596
        self._append_page(doc, "text-x-generic")
597
        doc.set_files(gfiles)
598 599
        if merge_output is not None:
            doc.set_merge_output_file(merge_output)
600 601
        return doc

602
    def append_diff(self, gfiles, auto_compare=False, auto_merge=False,
603
                    merge_output=None, meta=None):
604 605 606 607 608 609 610 611 612
        have_directories = False
        have_files = False
        for f in gfiles:
            if f.query_file_type(
               Gio.FileQueryInfoFlags.NONE, None) == Gio.FileType.DIRECTORY:
                have_directories = True
            else:
                have_files = True
        if have_directories and have_files:
613 614
            raise ValueError(
                _("Cannot compare a mixture of files and directories"))
615 616
        elif have_directories:
            return self.append_dirdiff(gfiles, auto_compare)
Piotr Piastucki's avatar
Piotr Piastucki committed
617
        elif auto_merge:
618
            return self.append_filemerge(gfiles, merge_output=merge_output)
619
        else:
620
            return self.append_filediff(
621
                gfiles, merge_output=merge_output, meta=meta)
622 623

    def append_vcview(self, location, auto_compare=False):
624
        doc = vcview.VcView()
625
        self._append_page(doc, "meld-version-control")
626
        location = location[0] if isinstance(location, list) else location
627
        doc.set_location(location.get_path())
628
        if auto_compare:
629
            doc.scheduler.add_task(doc.auto_compare)
630 631
        return doc

632
    def append_recent(self, uri):
633
        comparison_type, gfiles, flags = recent_comparisons.read(uri)
634
        if comparison_type == recent.TYPE_MERGE:
635
            tab = self.append_filemerge(gfiles)
636
        elif comparison_type == recent.TYPE_FOLDER:
637
            tab = self.append_dirdiff(gfiles)
638
        elif comparison_type == recent.TYPE_VC:
639
            # Files should be a single-element iterable
640
            tab = self.append_vcview(gfiles[0])
641
        else:  # comparison_type == recent.TYPE_FILE:
642
            tab = self.append_filediff(gfiles)
643
        self.notebook.set_current_page(self.notebook.page_num(tab.widget))
644
        recent_comparisons.add(tab)
645 646
        return tab

647
    def _single_file_open(self, gfile):
648
        doc = vcview.VcView()
Kai Willadsen's avatar
Kai Willadsen committed
649

650 651 652 653
        def cleanup():
            self.scheduler.remove_scheduler(doc.scheduler)
        self.scheduler.add_task(cleanup)
        self.scheduler.add_scheduler(doc.scheduler)
654
        path = gfile.get_path()
655
        doc.set_location(path)
656 657
        doc.connect("create-diff", lambda obj, arg, kwargs:
                    self.append_diff(arg, **kwargs))
658
        doc.run_diff(path)
659

660
    def open_paths(self, gfiles, auto_compare=False, auto_merge=False,
661
                   focus=False):
662
        tab = None
663 664 665 666
        if len(gfiles) == 1:
            a = gfiles[0]
            if a.query_file_type(Gio.FileQueryInfoFlags.NONE, None) == \
                    Gio.FileType.DIRECTORY:
667
                tab = self.append_vcview(a, auto_compare)
668 669
            else:
                self._single_file_open(a)
670

671 672 673
        elif len(gfiles) in (2, 3):
            tab = self.append_diff(gfiles, auto_compare=auto_compare,
                                   auto_merge=auto_merge)
674
        if tab:
675
            recent_comparisons.add(tab)
676 677 678 679
            if focus:
                self.notebook.set_current_page(
                    self.notebook.page_num(tab.widget))

680 681
        return tab

682 683 684 685
    def current_doc(self):
        "Get the current doc or a dummy object if there is no current"
        index = self.notebook.get_current_page()
        if index >= 0:
686
            page = self.notebook.get_nth_page(index).pyobject
687 688 689
            if isinstance(page, melddoc.MeldDoc):
                return page

690
        class DummyDoc(object):
Kai Willadsen's avatar
Kai Willadsen committed
691 692
            def __getattr__(self, a):
                return lambda *x: None
693
        return DummyDoc()