Exemple #1
0
class MeldWindow(Gtk.ApplicationWindow):

    __gtype_name__ = 'MeldWindow'

    appvbox = Template.Child("appvbox")
    folder_filter_button = Template.Child()
    text_filter_button = Template.Child()
    gear_menu_button = Template.Child("gear_menu_button")
    notebook = Template.Child("notebook")
    spinner = Template.Child("spinner")
    vc_filter_button = Template.Child()
    view_toolbar = Template.Child()

    def __init__(self):
        super().__init__()

        self.init_template()

        # Manually handle GAction additions
        actions = (
            ("close", self.action_close),
            ("new-tab", self.action_new_tab),
            ("stop", self.action_stop),
        )
        for name, callback in actions:
            action = Gio.SimpleAction.new(name, None)
            action.connect('activate', callback)
            self.add_action(action)

        state_actions = (
            (
                "fullscreen", self.action_fullscreen_change,
                GLib.Variant.new_boolean(False)
            ),
        )
        for (name, callback, state) in state_actions:
            action = Gio.SimpleAction.new_stateful(name, None, state)
            action.connect('change-state', callback)
            self.add_action(action)

        # Initialise sensitivity for important actions
        self.lookup_action('stop').set_enabled(False)

        # Fake out the spinner on Windows. See Gitlab issue #133.
        if os.name == 'nt':
            for attr in ('stop', 'hide', 'show', 'start'):
                setattr(self.spinner, attr, lambda *args: True)

        self.drag_dest_set(
            Gtk.DestDefaults.MOTION | Gtk.DestDefaults.HIGHLIGHT |
            Gtk.DestDefaults.DROP,
            None, Gdk.DragAction.COPY)
        self.drag_dest_add_uri_targets()
        self.connect(
            "drag_data_received", self.on_widget_drag_data_received)

        self.window_state = SavedWindowState()
        self.window_state.bind(self)

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

    def do_realize(self):
        Gtk.ApplicationWindow.do_realize(self)

        app = self.get_application()
        menu = app.get_menu_by_id("gear-menu")
        self.gear_menu_button.set_popover(
            Gtk.Popover.new_from_model(self.gear_menu_button, menu))

        filter_model = app.get_menu_by_id("text-filter-menu")
        self.text_filter_button.set_popover(
            Gtk.Popover.new_from_model(self.text_filter_button, filter_model))

        filter_menu = app.get_menu_by_id("folder-status-filter-menu")
        self.folder_filter_button.set_popover(
            Gtk.Popover.new_from_model(self.folder_filter_button, filter_menu))

        vc_filter_model = app.get_menu_by_id('vc-status-filter-menu')
        self.vc_filter_button.set_popover(
            Gtk.Popover.new_from_model(self.vc_filter_button, vc_filter_model))

        self.update_text_filters()
        self.update_filename_filters()
        self.settings_handlers = [
            meldsettings.connect(
                "text-filters-changed", self.update_text_filters),
            meldsettings.connect(
                "file-filters-changed", self.update_filename_filters),
        ]

        meld.ui.util.extract_accels_from_menu(menu, self.get_application())

    def update_filename_filters(self, *args):
        filter_items_model = Gio.Menu()
        for i, filt in enumerate(meldsettings.file_filters):
            name = FILE_FILTER_ACTION_FORMAT.format(i)
            filter_items_model.append(
                label=filt.label, detailed_action=f'view.{name}')
        section = Gio.MenuItem.new_section(_("Filename"), filter_items_model)
        section.set_attribute([("id", "s", "custom-filter-section")])
        app = self.get_application()
        filter_model = app.get_menu_by_id("folder-status-filter-menu")
        replace_menu_section(filter_model, section)

    def update_text_filters(self, *args):
        filter_items_model = Gio.Menu()
        for i, filt in enumerate(meldsettings.text_filters):
            name = TEXT_FILTER_ACTION_FORMAT.format(i)
            filter_items_model.append(
                label=filt.label, detailed_action=f'view.{name}')
        section = Gio.MenuItem.new_section(None, filter_items_model)
        section.set_attribute([("id", "s", "custom-filter-section")])
        app = self.get_application()
        filter_model = app.get_menu_by_id("text-filter-menu")
        replace_menu_section(filter_model, section)

    def on_widget_drag_data_received(
            self, wid, context, x, y, selection_data, info, time):
        uris = selection_data.get_uris()
        if uris:
            self.open_paths([Gio.File.new_for_uri(uri) for uri in uris])
            return True

    def on_idle(self):
        ret = self.scheduler.iteration()
        if ret and isinstance(ret, str):
            self.spinner.set_tooltip_text(ret)

        pending = self.scheduler.tasks_pending()
        if not pending:
            self.spinner.stop()
            self.spinner.hide()
            self.spinner.set_tooltip_text("")
            self.idle_hooked = None

            # On window close, this idle loop races widget destruction,
            # and so actions may already be gone at this point.
            stop_action = self.lookup_action('stop')
            if stop_action:
                stop_action.set_enabled(False)
        return pending

    def on_scheduler_runnable(self, sched):
        if not self.idle_hooked:
            self.spinner.show()
            self.spinner.start()
            self.lookup_action('stop').set_enabled(True)
            self.idle_hooked = GLib.idle_add(self.on_idle)

    @Template.Callback()
    def on_delete_event(self, *extra):
        should_cancel = False
        # 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 page in reversed(self.notebook.get_children()):
            self.notebook.set_current_page(self.notebook.page_num(page))
            response = page.on_delete_event()
            if response == Gtk.ResponseType.CANCEL:
                should_cancel = True

        should_cancel = should_cancel or self.has_pages()
        if should_cancel:
            self.should_close = True
        return should_cancel

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

    def handle_current_doc_switch(self, page):
        page.on_container_switch_out_event(self)

    @Template.Callback()
    def on_switch_page(self, notebook, page, which):
        oldidx = notebook.get_current_page()
        if oldidx >= 0:
            olddoc = notebook.get_nth_page(oldidx)
            self.handle_current_doc_switch(olddoc)

        newdoc = notebook.get_nth_page(which) if which >= 0 else None

        self.lookup_action('close').set_enabled(bool(newdoc))

        if newdoc:
            nbl = self.notebook.get_tab_label(newdoc)
            self.set_title(nbl.props.label_text)
        else:
            self.set_title("Meld")

        if hasattr(newdoc, 'scheduler'):
            self.scheduler.add_task(newdoc.scheduler)

        self.view_toolbar.foreach(self.view_toolbar.remove)
        if hasattr(newdoc, 'toolbar_actions'):
            self.view_toolbar.add(newdoc.toolbar_actions)

    @Template.Callback()
    def after_switch_page(self, notebook, page, which):
        newdoc = notebook.get_nth_page(which)
        newdoc.on_container_switch_in_event(self)

    @Template.Callback()
    def on_page_label_changed(self, notebook, label_text):
        self.set_title(label_text)

    def action_new_tab(self, action, parameter):
        self.append_new_comparison()

    def action_close(self, *extra):
        i = self.notebook.get_current_page()
        if i >= 0:
            page = self.notebook.get_nth_page(i)
            page.on_delete_event()

    def action_fullscreen_change(self, action, state):
        window_state = self.get_window().get_state()
        is_full = window_state & Gdk.WindowState.FULLSCREEN
        action.set_state(state)
        if state and not is_full:
            self.fullscreen()
        elif is_full:
            self.unfullscreen()

    def action_stop(self, *args):
        # TODO: This is the only window-level action we have that still
        # works on the "current" document like this.
        self.current_doc().action_stop()

    def page_removed(self, page, status):
        if hasattr(page, 'scheduler'):
            self.scheduler.remove_scheduler(page.scheduler)

        page_num = self.notebook.page_num(page)

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

        self.notebook.remove_page(page_num)
        # Normal switch-page handlers don't get run for removing the
        # last page from a notebook.
        if not self.has_pages():
            self.on_switch_page(self.notebook, page, -1)
            if self.should_close:
                cancelled = self.emit(
                    'delete-event', Gdk.Event.new(Gdk.EventType.DELETE))
                if not cancelled:
                    self.emit('destroy')

    def on_page_state_changed(self, page, old_state, new_state):
        if self.should_close and old_state == ComparisonState.Closing:
            # Cancel closing if one of our tabs does
            self.should_close = False

    def on_file_changed(self, srcpage, filename):
        for page in self.notebook.get_children():
            if page != srcpage:
                page.on_file_changed(filename)

    @Template.Callback()
    def on_open_recent(self, recent_selector, uri):
        try:
            self.append_recent(uri)
        except (IOError, ValueError):
            # FIXME: Need error handling, but no sensible display location
            log.exception(f'Error opening recent file {uri}')

    def _append_page(self, page, icon):
        nbl = NotebookLabel(icon_name=icon, page=page)
        self.notebook.append_page(page, nbl)
        self.notebook.child_set_property(page, 'tab-expand', True)

        # Change focus to the newly created page only if the user is on a
        # DirDiff or VcView page, or if it's a new tab page. This prevents
        # cycling through X pages when X diffs are initiated.
        if isinstance(self.current_doc(), DirDiff) or \
           isinstance(self.current_doc(), VcView) or \
           isinstance(page, NewDiffTab):
            self.notebook.set_current_page(self.notebook.page_num(page))

        if hasattr(page, 'scheduler'):
            self.scheduler.add_scheduler(page.scheduler)
        if isinstance(page, MeldDoc):
            page.file_changed_signal.connect(self.on_file_changed)
            page.create_diff_signal.connect(
                lambda obj, arg, kwargs: self.append_diff(arg, **kwargs))
            page.tab_state_changed.connect(self.on_page_state_changed)
        page.close_signal.connect(self.page_removed)

        self.notebook.set_tab_reorderable(page, True)

    def append_new_comparison(self):
        doc = NewDiffTab(self)
        self._append_page(doc, "document-new")
        self.notebook.on_label_changed(doc, _("New comparison"), None)

        def diff_created_cb(doc, newdoc):
            doc.on_delete_event()
            idx = self.notebook.page_num(newdoc)
            self.notebook.set_current_page(idx)

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

    def append_dirdiff(self, gfiles, auto_compare=False):
        dirs = [d.get_path() if d else None for d in gfiles]
        assert len(dirs) in (1, 2, 3)
        doc = DirDiff(len(dirs))
        self._append_page(doc, "folder")
        doc.set_locations(dirs)
        if auto_compare:
            doc.scheduler.add_task(doc.auto_compare)
        return doc

    def append_filediff(
            self, gfiles, *, encodings=None, merge_output=None, meta=None):
        assert len(gfiles) in (1, 2, 3)
        doc = FileDiff(len(gfiles))
        self._append_page(doc, "text-x-generic")
        doc.set_files(gfiles, encodings)
        if merge_output is not None:
            doc.set_merge_output_file(merge_output)
        if meta is not None:
            doc.set_meta(meta)
        return doc

    def append_filemerge(self, gfiles, merge_output=None):
        if len(gfiles) != 3:
            raise ValueError(
                _("Need three files to auto-merge, got: %r") %
                [f.get_parse_name() for f in gfiles])
        doc = FileMerge(len(gfiles))
        self._append_page(doc, "text-x-generic")
        doc.set_files(gfiles)
        if merge_output is not None:
            doc.set_merge_output_file(merge_output)
        return doc

    def append_diff(self, gfiles, auto_compare=False, auto_merge=False,
                    merge_output=None, meta=None):
        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:
            raise ValueError(
                _("Cannot compare a mixture of files and directories"))
        elif have_directories:
            return self.append_dirdiff(gfiles, auto_compare)
        elif auto_merge:
            return self.append_filemerge(gfiles, merge_output=merge_output)
        else:
            return self.append_filediff(
                gfiles, merge_output=merge_output, meta=meta)

    def append_vcview(self, location, auto_compare=False):
        doc = VcView()
        self._append_page(doc, "meld-version-control")
        if isinstance(location, (list, tuple)):
            location = location[0]
        doc.set_location(location.get_path())
        if auto_compare:
            doc.scheduler.add_task(doc.auto_compare)
        return doc

    def append_recent(self, uri):
        comparison_type, gfiles = recent_comparisons.read(uri)
        comparison_method = {
            RecentType.File: self.append_filediff,
            RecentType.Folder: self.append_dirdiff,
            RecentType.Merge: self.append_filemerge,
            RecentType.VersionControl: self.append_vcview,
        }
        tab = comparison_method[comparison_type](gfiles)
        self.notebook.set_current_page(self.notebook.page_num(tab))
        recent_comparisons.add(tab)
        return tab

    def _single_file_open(self, gfile):
        doc = VcView()

        def cleanup():
            self.scheduler.remove_scheduler(doc.scheduler)
        self.scheduler.add_task(cleanup)
        self.scheduler.add_scheduler(doc.scheduler)
        path = gfile.get_path()
        doc.set_location(path)
        doc.create_diff_signal.connect(
            lambda obj, arg, kwargs: self.append_diff(arg, **kwargs))
        doc.run_diff(path)

    def open_paths(self, gfiles, auto_compare=False, auto_merge=False,
                   focus=False):
        tab = None
        if len(gfiles) == 1:
            a = gfiles[0]
            if a.query_file_type(Gio.FileQueryInfoFlags.NONE, None) == \
                    Gio.FileType.DIRECTORY:
                tab = self.append_vcview(a, auto_compare)
            else:
                self._single_file_open(a)

        elif len(gfiles) in (2, 3):
            tab = self.append_diff(gfiles, auto_compare=auto_compare,
                                   auto_merge=auto_merge)
        if tab:
            recent_comparisons.add(tab)
            if focus:
                self.notebook.set_current_page(self.notebook.page_num(tab))

        return tab

    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:
            page = self.notebook.get_nth_page(index)
            if isinstance(page, MeldDoc):
                return page

        class DummyDoc:
            def __getattr__(self, a):
                return lambda *x: None
        return DummyDoc()
Exemple #2
0
class MeldWindow(Component):
    def __init__(self):
        super().__init__("meldapp.ui", "meldapp")
        self.widget.set_name("meldapp")

        actions = (
            ("FileMenu", None, _("_File")),
            ("New", Gtk.STOCK_NEW, _("_New Comparison…"), "<Primary>N",
             _("Start a new comparison"), self.on_menu_file_new_activate),
            ("Save", Gtk.STOCK_SAVE, None, None, _("Save the current file"),
             self.on_menu_save_activate),
            ("SaveAs", Gtk.STOCK_SAVE_AS, _("Save As…"), "<Primary><shift>S",
             _("Save the current file with a different name"),
             self.on_menu_save_as_activate),
            ("Close", Gtk.STOCK_CLOSE, None, None, _("Close the current file"),
             self.on_menu_close_activate),
            ("EditMenu", None, _("_Edit")),
            ("Undo", Gtk.STOCK_UNDO, None, "<Primary>Z",
             _("Undo the last action"), self.on_menu_undo_activate),
            ("Redo", Gtk.STOCK_REDO, None, "<Primary><shift>Z",
             _("Redo the last undone action"), self.on_menu_redo_activate),
            ("Cut", Gtk.STOCK_CUT, None, None, _("Cut the selection"),
             self.on_menu_cut_activate),
            ("Copy", Gtk.STOCK_COPY, None, None, _("Copy the selection"),
             self.on_menu_copy_activate),
            ("Paste", Gtk.STOCK_PASTE, None, None, _("Paste the clipboard"),
             self.on_menu_paste_activate),
            ("Find", Gtk.STOCK_FIND, _("Find…"), None, _("Search for text"),
             self.on_menu_find_activate),
            ("FindNext", None, _("Find Ne_xt"), "<Primary>G",
             _("Search forwards for the same text"),
             self.on_menu_find_next_activate),
            ("FindPrevious", None, _("Find _Previous"), "<Primary><shift>G",
             _("Search backwards for the same text"),
             self.on_menu_find_previous_activate),
            ("Replace",
             Gtk.STOCK_FIND_AND_REPLACE, _("_Replace…"), "<Primary>H",
             _("Find and replace text"), self.on_menu_replace_activate),
            ("GoToLine", None, _("Go to _Line"), "<Primary>I",
             _("Go to a specific line"), self.on_menu_go_to_line_activate),
            ("ChangesMenu", None, _("_Changes")),
            ("NextChange", Gtk.STOCK_GO_DOWN, _("Next Change"), "<Alt>Down",
             _("Go to the next change"), self.on_menu_edit_down_activate),
            ("PrevChange", Gtk.STOCK_GO_UP, _("Previous Change"), "<Alt>Up",
             _("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),
            ("ViewMenu", None, _("_View")),
            ("FileStatus", None, _("File Status")),
            ("VcStatus", None, _("Version Status")),
            ("FileFilters", None, _("File Filters")),
            ("Stop", Gtk.STOCK_STOP, None, "Escape",
             _("Stop the current action"), self.on_toolbar_stop_clicked),
            ("Refresh", Gtk.STOCK_REFRESH, None, "<Primary>R",
             _("Refresh the view"), self.on_menu_refresh_activate),
        )
        toggleactions = (
            ("Fullscreen", None, _("Fullscreen"), "F11",
             _("View the comparison in fullscreen"),
             self.on_action_fullscreen_toggled, False),
            ("ToolbarVisible", None, _("_Toolbar"), None,
             _("Show or hide the toolbar"), None, True),
        )
        self.actiongroup = Gtk.ActionGroup(name='MainActions')
        self.actiongroup.set_translation_domain("meld")
        self.actiongroup.add_actions(actions)
        self.actiongroup.add_toggle_actions(toggleactions)

        recent_action = Gtk.RecentAction(name="Recent",
                                         label=_("Open Recent"),
                                         tooltip=_("Open recent files"),
                                         stock_id=None)
        recent_action.set_show_private(True)
        recent_action.set_filter(recent_comparisons.recent_filter)
        recent_action.set_sort_type(Gtk.RecentSortType.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("meldapp-ui.xml"))

        # 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:
            from meld.meldapp import app

            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)

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

        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')
        self.toolbar.get_style_context().add_class(
            Gtk.STYLE_CLASS_PRIMARY_TOOLBAR)

        settings.bind('toolbar-visible',
                      self.actiongroup.get_action('ToolbarVisible'), 'active',
                      Gio.SettingsBindFlags.DEFAULT)
        settings.bind('toolbar-visible', self.toolbar, 'visible',
                      Gio.SettingsBindFlags.DEFAULT)
        interface_settings.bind('toolbar-style', self.toolbar, 'toolbar-style',
                                Gio.SettingsBindFlags.DEFAULT)

        # Alternate keybindings for a few commands.
        extra_accels = (
            ("<Primary>D", self.on_menu_edit_down_activate),
            ("<Primary>E", self.on_menu_edit_up_activate),
            ("<Alt>KP_Down", self.on_menu_edit_down_activate),
            ("<Alt>KP_Up", self.on_menu_edit_up_activate),
            ("F5", self.on_menu_refresh_activate),
        )

        accel_group = self.ui.get_accel_group()
        for accel, callback in extra_accels:
            keyval, mask = Gtk.accelerator_parse(accel)
            accel_group.connect(keyval, mask, 0, callback)

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

        self.appvbox.pack_start(self.menubar, False, True, 0)
        self.toolbar_holder.pack_start(self.toolbar, True, True, 0)

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

        # 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)

        # Create a secondary toolbar, just to hold our progress spinner
        toolbutton = Gtk.ToolItem()
        self.spinner = Gtk.Spinner()
        # Fake out the spinner on Windows. See Gitlab issue #133.
        if os.name == 'nt':
            for attr in ('stop', 'hide', 'show', 'start'):
                setattr(self.spinner, attr, lambda *args: True)
        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()

        self.widget.drag_dest_set(
            Gtk.DestDefaults.MOTION | Gtk.DestDefaults.HIGHLIGHT
            | Gtk.DestDefaults.DROP, None, Gdk.DragAction.COPY)
        self.widget.drag_dest_add_uri_targets()
        self.widget.connect("drag_data_received",
                            self.on_widget_drag_data_received)

        self.window_state = SavedWindowState()
        self.window_state.bind(self.widget)

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

        self.ui.ensure_update()
        self.diff_handler = None
        self.undo_handlers = tuple()
        self.widget.connect('focus_in_event', self.on_focus_change)
        self.widget.connect('focus_out_event', self.on_focus_change)

        # 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)

        builder = meld.ui.util.get_builder("shortcuts.ui")
        shortcut_window = builder.get_object("shortcuts-meld")
        self.widget.set_help_overlay(shortcut_window)

    def _on_recentmenu_map(self, recentmenu):
        for imagemenuitem in recentmenu.get_children():
            imagemenuitem.set_tooltip_text(imagemenuitem.get_label())

    def on_focus_change(self, widget, event, callback_data=None):
        for idx in range(self.notebook.get_n_pages()):
            w = self.notebook.get_nth_page(idx)
            if hasattr(w.pyobject, 'on_focus_change'):
                w.pyobject.on_focus_change()
        # Let the rest of the stack know about this event
        return False

    def on_widget_drag_data_received(self, wid, context, x, y, selection_data,
                                     info, time):
        uris = selection_data.get_uris()
        if uris:
            self.open_paths([Gio.File.new_for_uri(uri) for uri in uris])
            return True

    def on_idle(self):
        ret = self.scheduler.iteration()
        if ret and isinstance(ret, str):
            self.spinner.set_tooltip_text(ret)

        pending = self.scheduler.tasks_pending()
        if not pending:
            self.spinner.stop()
            self.spinner.hide()
            self.spinner.set_tooltip_text("")
            self.idle_hooked = None
            self.actiongroup.get_action("Stop").set_sensitive(False)
        return pending

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

    def on_delete_event(self, *extra):
        should_cancel = False
        # 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))
            response = page.on_delete_event()
            if response == Gtk.ResponseType.CANCEL:
                should_cancel = True

        should_cancel = should_cancel or self.has_pages()
        if should_cancel:
            self.should_close = True
        return should_cancel

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

    def _update_page_action_sensitivity(self):
        current_page = self.notebook.get_current_page()

        if current_page != -1:
            page = self.notebook.get_nth_page(current_page).pyobject
        else:
            page = None

        self.actiongroup.get_action("Close").set_sensitive(bool(page))
        if not isinstance(page, MeldDoc):
            for action in ("PrevChange", "NextChange", "Cut", "Copy", "Paste",
                           "Find", "FindNext", "FindPrevious", "Replace",
                           "Refresh", "GoToLine"):
                self.actiongroup.get_action(action).set_sensitive(False)
        else:
            for action in ("Find", "Refresh"):
                self.actiongroup.get_action(action).set_sensitive(True)
            is_filediff = isinstance(page, FileDiff)
            for action in ("Cut", "Copy", "Paste", "FindNext", "FindPrevious",
                           "Replace", "GoToLine"):
                self.actiongroup.get_action(action).set_sensitive(is_filediff)

    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()

    def on_switch_page(self, notebook, page, which):
        oldidx = notebook.get_current_page()
        if oldidx >= 0:
            olddoc = notebook.get_nth_page(oldidx).pyobject
            self.handle_current_doc_switch(olddoc)

        newdoc = notebook.get_nth_page(which).pyobject if which >= 0 else None
        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)

        # FileDiff handles save sensitivity; it makes no sense for other modes
        if not isinstance(newdoc, 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)

        if newdoc:
            nbl = self.notebook.get_tab_label(newdoc.widget)
            self.widget.set_title(nbl.get_label_text())
            newdoc.on_container_switch_in_event(self.ui)
        else:
            self.widget.set_title("Meld")

        if isinstance(newdoc, MeldDoc):
            self.diff_handler = newdoc.connect("next-diff-changed",
                                               self.on_next_diff_changed)
        else:
            self.diff_handler = None
        if hasattr(newdoc, 'scheduler'):
            self.scheduler.add_task(newdoc.scheduler)

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

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

    def on_page_label_changed(self, notebook, label_text):
        self.widget.set_title(label_text)

    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):
        self.append_new_comparison()

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

    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:
            page = self.notebook.get_nth_page(i).pyobject
            page.on_delete_event()

    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()

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

    def on_menu_replace_activate(self, *extra):
        self.current_doc().on_replace_activate()

    def on_menu_go_to_line_activate(self, *extra):
        self.current_doc().on_go_to_line_activate()

    def on_menu_copy_activate(self, *extra):
        widget = self.widget.get_focus()
        if isinstance(widget, Gtk.Editable):
            widget.copy_clipboard()
        elif isinstance(widget, Gtk.TextView):
            widget.emit("copy-clipboard")

    def on_menu_cut_activate(self, *extra):
        widget = self.widget.get_focus()
        if isinstance(widget, Gtk.Editable):
            widget.cut_clipboard()
        elif isinstance(widget, Gtk.TextView):
            widget.emit("cut-clipboard")

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

    def on_action_fullscreen_toggled(self, widget):
        window_state = self.widget.get_window().get_state()
        is_full = window_state & Gdk.WindowState.FULLSCREEN
        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):
        self.current_doc().next_diff(Gdk.ScrollDirection.DOWN)

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

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

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

    def page_removed(self, page, status):
        if hasattr(page, 'scheduler'):
            self.scheduler.remove_scheduler(page.scheduler)

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

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

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

    def on_page_state_changed(self, page, old_state, new_state):
        if self.should_close and old_state == ComparisonState.Closing:
            # Cancel closing if one of our tabs does
            self.should_close = False

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

    def _append_page(self, page, icon):
        nbl = NotebookLabel(icon, "", lambda b: page.on_delete_event())
        self.notebook.append_page(page.widget, nbl)

        # Change focus to the newly created page only if the user is on a
        # DirDiff or VcView page, or if it's a new tab page. This prevents
        # cycling through X pages when X diffs are initiated.
        if isinstance(self.current_doc(), DirDiff) or \
           isinstance(self.current_doc(), VcView) or \
           isinstance(page, NewDiffTab):
            self.notebook.set_current_page(self.notebook.page_num(page.widget))

        if hasattr(page, 'scheduler'):
            self.scheduler.add_scheduler(page.scheduler)
        if isinstance(page, MeldDoc):
            page.connect("file-changed", self.on_file_changed)
            page.connect(
                "create-diff",
                lambda obj, arg, kwargs: self.append_diff(arg, **kwargs))
            page.connect("state-changed", self.on_page_state_changed)
        page.connect("close", self.page_removed)

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

    def append_new_comparison(self):
        doc = NewDiffTab(self)
        self._append_page(doc, "document-new")
        self.notebook.on_label_changed(doc, _("New comparison"), None)

        def diff_created_cb(doc, newdoc):
            doc.on_delete_event()
            idx = self.notebook.page_num(newdoc.widget)
            self.notebook.set_current_page(idx)

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

    def append_dirdiff(self, gfiles, auto_compare=False):
        dirs = [d.get_path() for d in gfiles if d]
        assert len(dirs) in (1, 2, 3)
        doc = DirDiff(len(dirs))
        self._append_page(doc, "folder")
        doc.set_locations(dirs)
        if auto_compare:
            doc.scheduler.add_task(doc.auto_compare)
        return doc

    def append_filediff(self,
                        gfiles,
                        *,
                        encodings=None,
                        merge_output=None,
                        meta=None):
        assert len(gfiles) in (1, 2, 3)
        doc = FileDiff(len(gfiles))
        self._append_page(doc, "text-x-generic")
        doc.set_files(gfiles, encodings)
        if merge_output is not None:
            doc.set_merge_output_file(merge_output)
        if meta is not None:
            doc.set_meta(meta)
        return doc

    def append_filemerge(self, gfiles, merge_output=None):
        if len(gfiles) != 3:
            raise ValueError(
                _("Need three files to auto-merge, got: %r") %
                [f.get_parse_name() for f in gfiles])
        doc = FileMerge(len(gfiles))
        self._append_page(doc, "text-x-generic")
        doc.set_files(gfiles)
        if merge_output is not None:
            doc.set_merge_output_file(merge_output)
        return doc

    def append_diff(self,
                    gfiles,
                    auto_compare=False,
                    auto_merge=False,
                    merge_output=None,
                    meta=None):
        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:
            raise ValueError(
                _("Cannot compare a mixture of files and directories"))
        elif have_directories:
            return self.append_dirdiff(gfiles, auto_compare)
        elif auto_merge:
            return self.append_filemerge(gfiles, merge_output=merge_output)
        else:
            return self.append_filediff(gfiles,
                                        merge_output=merge_output,
                                        meta=meta)

    def append_vcview(self, location, auto_compare=False):
        doc = VcView()
        self._append_page(doc, "meld-version-control")
        if isinstance(location, (list, tuple)):
            location = location[0]
        doc.set_location(location.get_path())
        if auto_compare:
            doc.scheduler.add_task(doc.auto_compare)
        return doc

    def append_recent(self, uri):
        comparison_type, gfiles, flags = recent_comparisons.read(uri)
        comparison_method = {
            RecentType.File: self.append_filediff,
            RecentType.Folder: self.append_dirdiff,
            RecentType.Merge: self.append_filemerge,
            RecentType.VersionControl: self.append_vcview,
        }
        tab = comparison_method[comparison_type](gfiles)
        self.notebook.set_current_page(self.notebook.page_num(tab.widget))
        recent_comparisons.add(tab)
        return tab

    def _single_file_open(self, gfile):
        doc = VcView()

        def cleanup():
            self.scheduler.remove_scheduler(doc.scheduler)

        self.scheduler.add_task(cleanup)
        self.scheduler.add_scheduler(doc.scheduler)
        path = gfile.get_path()
        doc.set_location(path)
        doc.connect("create-diff",
                    lambda obj, arg, kwargs: self.append_diff(arg, **kwargs))
        doc.run_diff(path)

    def open_paths(self,
                   gfiles,
                   auto_compare=False,
                   auto_merge=False,
                   focus=False):
        tab = None
        if len(gfiles) == 1:
            a = gfiles[0]
            if a.query_file_type(Gio.FileQueryInfoFlags.NONE, None) == \
                    Gio.FileType.DIRECTORY:
                tab = self.append_vcview(a, auto_compare)
            else:
                self._single_file_open(a)

        elif len(gfiles) in (2, 3):
            tab = self.append_diff(gfiles,
                                   auto_compare=auto_compare,
                                   auto_merge=auto_merge)
        if tab:
            recent_comparisons.add(tab)
            if focus:
                self.notebook.set_current_page(
                    self.notebook.page_num(tab.widget))

        return tab

    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:
            page = self.notebook.get_nth_page(index).pyobject
            if isinstance(page, MeldDoc):
                return page

        class DummyDoc:
            def __getattr__(self, a):
                return lambda *x: None

        return DummyDoc()
Exemple #3
0
class MeldWindow(Component):

    def __init__(self):
        Component.__init__(self, "meldapp.ui", "meldapp")
        self.widget.set_name("meldapp")

        actions = (
            ("FileMenu", None, _("_File")),
            ("New", Gtk.STOCK_NEW, _("_New Comparison…"), "<Primary>N",
                _("Start a new comparison"),
                self.on_menu_file_new_activate),
            ("Save", Gtk.STOCK_SAVE, None, None,
                _("Save the current file"),
                self.on_menu_save_activate),
            ("SaveAs", Gtk.STOCK_SAVE_AS, _("Save As…"), "<Primary><shift>S",
                _("Save the current file with a different name"),
                self.on_menu_save_as_activate),
            ("Close", Gtk.STOCK_CLOSE, None, None,
                _("Close the current file"),
                self.on_menu_close_activate),

            ("EditMenu", None, _("_Edit")),
            ("Undo", Gtk.STOCK_UNDO, None, "<Primary>Z",
                _("Undo the last action"),
                self.on_menu_undo_activate),
            ("Redo", Gtk.STOCK_REDO, None, "<Primary><shift>Z",
                _("Redo the last undone action"),
                self.on_menu_redo_activate),
            ("Cut", Gtk.STOCK_CUT, None, None, _("Cut the selection"),
                self.on_menu_cut_activate),
            ("Copy", Gtk.STOCK_COPY, None, None, _("Copy the selection"),
                self.on_menu_copy_activate),
            ("Paste", Gtk.STOCK_PASTE, None, None, _("Paste the clipboard"),
                self.on_menu_paste_activate),
            ("Find", Gtk.STOCK_FIND, _("Find…"), None, _("Search for text"),
                self.on_menu_find_activate),
            ("FindNext", None, _("Find Ne_xt"), "<Primary>G",
                _("Search forwards for the same text"),
                self.on_menu_find_next_activate),
            ("FindPrevious", None, _("Find _Previous"), "<Primary><shift>G",
                _("Search backwards for the same text"),
                self.on_menu_find_previous_activate),
            ("Replace", Gtk.STOCK_FIND_AND_REPLACE,
                _("_Replace…"), "<Primary>H",
                _("Find and replace text"),
                self.on_menu_replace_activate),
            ("GoToLine", None, _("Go to _Line"), "<Primary>I",
                _("Go to a specific line"),
                self.on_menu_go_to_line_activate),

            ("ChangesMenu", None, _("_Changes")),
            ("NextChange", Gtk.STOCK_GO_DOWN, _("Next Change"), "<Alt>Down",
                _("Go to the next change"),
                self.on_menu_edit_down_activate),
            ("PrevChange", Gtk.STOCK_GO_UP, _("Previous Change"), "<Alt>Up",
                _("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),

            ("ViewMenu", None, _("_View")),
            ("FileStatus", None, _("File Status")),
            ("VcStatus", None, _("Version Status")),
            ("FileFilters", None, _("File Filters")),
            ("Stop", Gtk.STOCK_STOP, None, "Escape",
                _("Stop the current action"),
                self.on_toolbar_stop_clicked),
            ("Refresh", Gtk.STOCK_REFRESH, None, "<Primary>R",
                _("Refresh the view"),
                self.on_menu_refresh_activate),
        )
        toggleactions = (
            ("Fullscreen", None, _("Fullscreen"), "F11",
                _("View the comparison in fullscreen"),
                self.on_action_fullscreen_toggled, False),
            ("ToolbarVisible", None, _("_Toolbar"), None,
                _("Show or hide the toolbar"),
                None, True),
        )
        self.actiongroup = Gtk.ActionGroup(name='MainActions')
        self.actiongroup.set_translation_domain("meld")
        self.actiongroup.add_actions(actions)
        self.actiongroup.add_toggle_actions(toggleactions)

        recent_action = Gtk.RecentAction(
            name="Recent",  label=_("Open Recent"),
            tooltip=_("Open recent files"), stock_id=None)
        recent_action.set_show_private(True)
        recent_action.set_filter(recent_comparisons.recent_filter)
        recent_action.set_sort_type(Gtk.RecentSortType.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("meldapp-ui.xml"))

        # 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:
            from meld.meldapp import app

            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)

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

        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')
        self.toolbar.get_style_context().add_class(
            Gtk.STYLE_CLASS_PRIMARY_TOOLBAR)

        settings.bind('toolbar-visible',
                      self.actiongroup.get_action('ToolbarVisible'), 'active',
                      Gio.SettingsBindFlags.DEFAULT)
        settings.bind('toolbar-visible', self.toolbar, 'visible',
                      Gio.SettingsBindFlags.DEFAULT)
        interface_settings.bind('toolbar-style', self.toolbar, 'toolbar-style',
                                Gio.SettingsBindFlags.DEFAULT)

        # Add alternate keybindings for Prev/Next Change
        accels = self.ui.get_accel_group()
        (keyval, mask) = Gtk.accelerator_parse("<Primary>D")
        accels.connect(keyval, mask, 0, self.on_menu_edit_down_activate)
        (keyval, mask) = Gtk.accelerator_parse("<Primary>E")
        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)

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

        self.appvbox.pack_start(self.menubar, False, True, 0)
        self.toolbar_holder.pack_start(self.toolbar, True, True, 0)

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

        # 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)

        # Create a secondary toolbar, just to hold our progress spinner
        toolbutton = Gtk.ToolItem()
        self.spinner = Gtk.Spinner()
        # Fake out the spinner on Windows. See Gitlab issue #133.
        if os.name == 'nt':
            for attr in ('stop', 'hide', 'show', 'start'):
                setattr(self.spinner, attr, lambda *args: True)
        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()

        self.widget.drag_dest_set(
            Gtk.DestDefaults.MOTION | Gtk.DestDefaults.HIGHLIGHT |
            Gtk.DestDefaults.DROP,
            None, Gdk.DragAction.COPY)
        self.widget.drag_dest_add_uri_targets()
        self.widget.connect("drag_data_received",
                            self.on_widget_drag_data_received)

        self.window_state = SavedWindowState()
        self.window_state.bind(self.widget)

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

        self.ui.ensure_update()
        self.diff_handler = None
        self.undo_handlers = tuple()
        self.widget.connect('focus_in_event', self.on_focus_change)
        self.widget.connect('focus_out_event', self.on_focus_change)

        # 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)

        builder = meld.ui.util.get_builder("shortcuts.ui")
        shortcut_window = builder.get_object("shortcuts-meld")
        self.widget.set_help_overlay(shortcut_window)

    def _on_recentmenu_map(self, recentmenu):
        for imagemenuitem in recentmenu.get_children():
            imagemenuitem.set_tooltip_text(imagemenuitem.get_label())

    def on_focus_change(self, widget, event, callback_data=None):
        for idx in range(self.notebook.get_n_pages()):
            w = self.notebook.get_nth_page(idx)
            if hasattr(w.pyobject, 'on_focus_change'):
                w.pyobject.on_focus_change()
        # Let the rest of the stack know about this event
        return False

    def on_widget_drag_data_received(self, wid, context, x, y, selection_data,
                                     info, time):
        if len(selection_data.get_files()) != 0:
            self.open_paths(selection_data.get_files())
            return True

    def on_idle(self):
        ret = self.scheduler.iteration()
        if ret and isinstance(ret, str):
            self.spinner.set_tooltip_text(ret)

        pending = self.scheduler.tasks_pending()
        if not pending:
            self.spinner.stop()
            self.spinner.hide()
            self.spinner.set_tooltip_text("")
            self.idle_hooked = None
            self.actiongroup.get_action("Stop").set_sensitive(False)
        return pending

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

    def on_delete_event(self, *extra):
        should_cancel = False
        # 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))
            response = page.on_delete_event()
            if response == Gtk.ResponseType.CANCEL:
                should_cancel = True

        should_cancel = should_cancel or self.has_pages()
        if should_cancel:
            self.should_close = True
        return should_cancel

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

    def _update_page_action_sensitivity(self):
        current_page = self.notebook.get_current_page()

        if current_page != -1:
            page = self.notebook.get_nth_page(current_page).pyobject
        else:
            page = None

        self.actiongroup.get_action("Close").set_sensitive(bool(page))
        if not isinstance(page, MeldDoc):
            for action in ("PrevChange", "NextChange", "Cut", "Copy", "Paste",
                           "Find", "FindNext", "FindPrevious", "Replace",
                           "Refresh", "GoToLine"):
                self.actiongroup.get_action(action).set_sensitive(False)
        else:
            for action in ("Find", "Refresh"):
                self.actiongroup.get_action(action).set_sensitive(True)
            is_filediff = isinstance(page, FileDiff)
            for action in ("Cut", "Copy", "Paste", "FindNext", "FindPrevious",
                           "Replace", "GoToLine"):
                self.actiongroup.get_action(action).set_sensitive(is_filediff)

    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()

    def on_switch_page(self, notebook, page, which):
        oldidx = notebook.get_current_page()
        if oldidx >= 0:
            olddoc = notebook.get_nth_page(oldidx).pyobject
            self.handle_current_doc_switch(olddoc)

        newdoc = notebook.get_nth_page(which).pyobject if which >= 0 else None
        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)

        # FileDiff handles save sensitivity; it makes no sense for other modes
        if not isinstance(newdoc, 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)

        if newdoc:
            nbl = self.notebook.get_tab_label(newdoc.widget)
            self.widget.set_title(nbl.get_label_text())
            newdoc.on_container_switch_in_event(self.ui)
        else:
            self.widget.set_title("Meld")

        if isinstance(newdoc, MeldDoc):
            self.diff_handler = newdoc.connect("next-diff-changed",
                                               self.on_next_diff_changed)
        else:
            self.diff_handler = None
        if hasattr(newdoc, 'scheduler'):
            self.scheduler.add_task(newdoc.scheduler)

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

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

    def on_page_label_changed(self, notebook, label_text):
        self.widget.set_title(label_text)

    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):
        self.append_new_comparison()

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

    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:
            page = self.notebook.get_nth_page(i).pyobject
            page.on_delete_event()

    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()

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

    def on_menu_replace_activate(self, *extra):
        self.current_doc().on_replace_activate()

    def on_menu_go_to_line_activate(self, *extra):
        self.current_doc().on_go_to_line_activate()

    def on_menu_copy_activate(self, *extra):
        widget = self.widget.get_focus()
        if isinstance(widget, Gtk.Editable):
            widget.copy_clipboard()
        elif isinstance(widget, Gtk.TextView):
            widget.emit("copy-clipboard")

    def on_menu_cut_activate(self, *extra):
        widget = self.widget.get_focus()
        if isinstance(widget, Gtk.Editable):
            widget.cut_clipboard()
        elif isinstance(widget, Gtk.TextView):
            widget.emit("cut-clipboard")

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

    def on_action_fullscreen_toggled(self, widget):
        window_state = self.widget.get_window().get_state()
        is_full = window_state & Gdk.WindowState.FULLSCREEN
        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):
        self.current_doc().next_diff(Gdk.ScrollDirection.DOWN)

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

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

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

    def page_removed(self, page, status):
        if hasattr(page, 'scheduler'):
            self.scheduler.remove_scheduler(page.scheduler)

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

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

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

    def on_page_state_changed(self, page, old_state, new_state):
        if self.should_close and old_state == ComparisonState.Closing:
            # Cancel closing if one of our tabs does
            self.should_close = False

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

    def _append_page(self, page, icon):
        nbl = NotebookLabel(icon, "", lambda b: page.on_delete_event())
        self.notebook.append_page(page.widget, nbl)

        # Change focus to the newly created page only if the user is on a
        # DirDiff or VcView page, or if it's a new tab page. This prevents
        # cycling through X pages when X diffs are initiated.
        if isinstance(self.current_doc(), DirDiff) or \
           isinstance(self.current_doc(), VcView) or \
           isinstance(page, NewDiffTab):
            self.notebook.set_current_page(self.notebook.page_num(page.widget))

        if hasattr(page, 'scheduler'):
            self.scheduler.add_scheduler(page.scheduler)
        if isinstance(page, MeldDoc):
            page.connect("file-changed", self.on_file_changed)
            page.connect("create-diff", lambda obj, arg, kwargs:
                         self.append_diff(arg, **kwargs))
            page.connect("state-changed", self.on_page_state_changed)
        page.connect("close", self.page_removed)

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

    def append_new_comparison(self):
        doc = NewDiffTab(self)
        self._append_page(doc, "document-new")
        self.notebook.on_label_changed(doc, _("New comparison"), None)

        def diff_created_cb(doc, newdoc):
            doc.on_delete_event()
            idx = self.notebook.page_num(newdoc.widget)
            self.notebook.set_current_page(idx)

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

    def append_dirdiff(self, gfiles, auto_compare=False):
        dirs = [d.get_path() for d in gfiles if d]
        assert len(dirs) in (1, 2, 3)
        doc = DirDiff(len(dirs))
        self._append_page(doc, "folder")
        doc.set_locations(dirs)
        if auto_compare:
            doc.scheduler.add_task(doc.auto_compare)
        return doc

    def append_filediff(
            self, gfiles, *, encodings=None, merge_output=None, meta=None):
        assert len(gfiles) in (1, 2, 3)
        doc = FileDiff(len(gfiles))
        self._append_page(doc, "text-x-generic")
        doc.set_files(gfiles, encodings)
        if merge_output is not None:
            doc.set_merge_output_file(merge_output)
        if meta is not None:
            doc.set_meta(meta)
        return doc

    def append_filemerge(self, gfiles, merge_output=None):
        if len(gfiles) != 3:
            raise ValueError(
                _("Need three files to auto-merge, got: %r") %
                [f.get_parse_name() for f in gfiles])
        doc = FileMerge(len(gfiles))
        self._append_page(doc, "text-x-generic")
        doc.set_files(gfiles)
        if merge_output is not None:
            doc.set_merge_output_file(merge_output)
        return doc

    def append_diff(self, gfiles, auto_compare=False, auto_merge=False,
                    merge_output=None, meta=None):
        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:
            raise ValueError(
                _("Cannot compare a mixture of files and directories"))
        elif have_directories:
            return self.append_dirdiff(gfiles, auto_compare)
        elif auto_merge:
            return self.append_filemerge(gfiles, merge_output=merge_output)
        else:
            return self.append_filediff(
                gfiles, merge_output=merge_output, meta=meta)

    def append_vcview(self, location, auto_compare=False):
        doc = VcView()
        self._append_page(doc, "meld-version-control")
        if isinstance(location, (list, tuple)):
            location = location[0]
        doc.set_location(location.get_path())
        if auto_compare:
            doc.scheduler.add_task(doc.auto_compare)
        return doc

    def append_recent(self, uri):
        comparison_type, gfiles, flags = recent_comparisons.read(uri)
        comparison_method = {
            RecentType.File: self.append_filediff,
            RecentType.Folder: self.append_dirdiff,
            RecentType.Merge: self.append_filemerge,
            RecentType.VersionControl: self.append_vcview,
        }
        tab = comparison_method[comparison_type](gfiles)
        self.notebook.set_current_page(self.notebook.page_num(tab.widget))
        recent_comparisons.add(tab)
        return tab

    def _single_file_open(self, gfile):
        doc = VcView()

        def cleanup():
            self.scheduler.remove_scheduler(doc.scheduler)
        self.scheduler.add_task(cleanup)
        self.scheduler.add_scheduler(doc.scheduler)
        path = gfile.get_path()
        doc.set_location(path)
        doc.connect("create-diff", lambda obj, arg, kwargs:
                    self.append_diff(arg, **kwargs))
        doc.run_diff(path)

    def open_paths(self, gfiles, auto_compare=False, auto_merge=False,
                   focus=False):
        tab = None
        if len(gfiles) == 1:
            a = gfiles[0]
            if a.query_file_type(Gio.FileQueryInfoFlags.NONE, None) == \
                    Gio.FileType.DIRECTORY:
                tab = self.append_vcview(a, auto_compare)
            else:
                self._single_file_open(a)

        elif len(gfiles) in (2, 3):
            tab = self.append_diff(gfiles, auto_compare=auto_compare,
                                   auto_merge=auto_merge)
        if tab:
            recent_comparisons.add(tab)
            if focus:
                self.notebook.set_current_page(
                    self.notebook.page_num(tab.widget))

        return tab

    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:
            page = self.notebook.get_nth_page(index).pyobject
            if isinstance(page, MeldDoc):
                return page

        class DummyDoc(object):
            def __getattr__(self, a):
                return lambda *x: None
        return DummyDoc()