1# Copyright (C) 2002-2006 Stephen Kennedy <stevek@gnome.org>
2# Copyright (C) 2011-2013 Kai Willadsen <kai.willadsen@gmail.com>
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation, either version 2 of the License, or (at
7# your option) any later version.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12# General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17import enum
18import logging
19import pipes
20import shlex
21import string
22import subprocess
23import sys
24
25from gi.repository import Gdk
26from gi.repository import Gio
27from gi.repository import GLib
28from gi.repository import GObject
29from gi.repository import Gtk
30
31from meld.conf import _
32from meld.recent import RecentType
33from meld.settings import settings
34from meld.task import FifoScheduler
35
36log = logging.getLogger(__name__)
37
38
39def make_custom_editor_command(path, line=0):
40    custom_command = settings.get_string('custom-editor-command')
41    fmt = string.Formatter()
42    replacements = [tok[1] for tok in fmt.parse(custom_command)]
43
44    if not any(replacements):
45        return [custom_command, path]
46    elif not all(r in (None, 'file', 'line') for r in replacements):
47        log.error("Unsupported fields found", )
48        return [custom_command, path]
49    else:
50        cmd = custom_command.format(file=pipes.quote(path), line=line)
51    return shlex.split(cmd)
52
53
54class ComparisonState(enum.IntEnum):
55    # TODO: Consider use-cases for states in gedit-enum-types.c
56    Normal = 0
57    Closing = 1
58    SavingError = 2
59
60
61class LabeledObjectMixin(GObject.GObject):
62    __gsignals__ = {
63        'label-changed': (
64            GObject.SignalFlags.RUN_FIRST, None,
65            (GObject.TYPE_STRING, GObject.TYPE_STRING)),
66    }
67
68    label_text = _("untitled")
69    tooltip_text = None
70
71    def label_changed(self):
72        self.emit("label-changed", self.label_text, self.tooltip_text)
73
74
75class MeldDoc(LabeledObjectMixin, GObject.GObject):
76    """Base class for documents in the meld application.
77    """
78
79    __gsignals__ = {
80        'file-changed':         (GObject.SignalFlags.RUN_FIRST, None,
81                                 (GObject.TYPE_STRING,)),
82        'create-diff':          (GObject.SignalFlags.RUN_FIRST, None,
83                                 (GObject.TYPE_PYOBJECT,
84                                  GObject.TYPE_PYOBJECT)),
85        'status-changed':       (GObject.SignalFlags.RUN_FIRST, None,
86                                 (GObject.TYPE_PYOBJECT,)),
87        'current-diff-changed': (GObject.SignalFlags.RUN_FIRST, None,
88                                 ()),
89        'next-diff-changed':    (GObject.SignalFlags.RUN_FIRST, None,
90                                 (bool, bool)),
91        'close': (GObject.SignalFlags.RUN_FIRST, None, (bool,)),
92        'state-changed': (GObject.SignalFlags.RUN_FIRST, None, (int, int)),
93    }
94
95    def __init__(self):
96        super().__init__()
97        self.scheduler = FifoScheduler()
98        self.num_panes = 0
99        self.main_actiongroup = None
100        self._state = ComparisonState.Normal
101
102    @property
103    def state(self):
104        return self._state
105
106    @state.setter
107    def state(self, value):
108        if value == self._state:
109            return
110        self.emit('state-changed', self._state, value)
111        self._state = value
112
113    def get_comparison(self) -> RecentType:
114        """Get the comparison type and URI(s) being compared"""
115        pass
116
117    def save(self):
118        pass
119
120    def save_as(self):
121        pass
122
123    def stop(self):
124        if self.scheduler.tasks_pending():
125            self.scheduler.remove_task(self.scheduler.get_current_task())
126
127    def _open_files(self, selected, line=0):
128        query_attrs = ",".join((Gio.FILE_ATTRIBUTE_STANDARD_TYPE,
129                                Gio.FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE))
130
131        def os_open(path, uri):
132            if not path:
133                return
134            if sys.platform == "win32":
135                subprocess.Popen(["start", path], shell=True)
136            elif sys.platform == "darwin":
137                subprocess.Popen(["open", path])
138            else:
139                Gtk.show_uri(Gdk.Screen.get_default(), uri,
140                             Gtk.get_current_event_time())
141
142        def open_cb(source, result, *data):
143            info = source.query_info_finish(result)
144            file_type = info.get_file_type()
145            path, uri = source.get_path(), source.get_uri()
146            if file_type == Gio.FileType.DIRECTORY:
147                os_open(path, uri)
148            elif file_type == Gio.FileType.REGULAR:
149                content_type = info.get_content_type()
150                # FIXME: Content types are broken on Windows with current gio
151                if Gio.content_type_is_a(content_type, "text/plain") or \
152                        sys.platform == "win32":
153                    if settings.get_boolean('use-system-editor'):
154                        gfile = Gio.File.new_for_path(path)
155                        if sys.platform == "win32":
156                            handler = gfile.query_default_handler(None)
157                            result = handler.launch([gfile], None)
158                        else:
159                            uri = gfile.get_uri()
160                            Gio.AppInfo.launch_default_for_uri(
161                                uri, None)
162                    else:
163                        editor = make_custom_editor_command(path, line)
164                        if editor:
165                            # TODO: If the editor is badly set up, this fails
166                            # silently
167                            subprocess.Popen(editor)
168                        else:
169                            os_open(path, uri)
170                else:
171                    os_open(path, uri)
172            else:
173                # TODO: Add some kind of 'failed to open' notification
174                pass
175
176        for f in [Gio.File.new_for_path(s) for s in selected]:
177            f.query_info_async(query_attrs, 0, GLib.PRIORITY_LOW, None,
178                               open_cb, None)
179
180    def open_external(self):
181        pass
182
183    def on_refresh_activate(self, *extra):
184        pass
185
186    def on_find_activate(self, *extra):
187        pass
188
189    def on_find_next_activate(self, *extra):
190        pass
191
192    def on_find_previous_activate(self, *extra):
193        pass
194
195    def on_replace_activate(self, *extra):
196        pass
197
198    def on_file_changed(self, filename):
199        pass
200
201    def set_labels(self, lst):
202        pass
203
204    def on_container_switch_in_event(self, uimanager):
205        """Called when the container app switches to this tab.
206        """
207        self.ui_merge_id = uimanager.add_ui_from_file(self.ui_file)
208        uimanager.insert_action_group(self.actiongroup, -1)
209        self.popup_menu = uimanager.get_widget("/Popup")
210        action_groups = uimanager.get_action_groups()
211        self.main_actiongroup = [
212            a for a in action_groups if a.get_name() == "MainActions"][0]
213        uimanager.ensure_update()
214        if hasattr(self, "focus_pane") and self.focus_pane:
215            self.scheduler.add_task(self.focus_pane.grab_focus)
216
217    def on_container_switch_out_event(self, uimanager):
218        """Called when the container app switches away from this tab.
219        """
220        uimanager.remove_action_group(self.actiongroup)
221        uimanager.remove_ui(self.ui_merge_id)
222        self.main_actiongroup = None
223        self.popup_menu = None
224        self.ui_merge_id = None
225
226    def on_delete_event(self):
227        """Called when the docs container is about to close.
228
229        A doc normally returns Gtk.ResponseType.OK, but may instead return
230        Gtk.ResponseType.CANCEL to request that the container not delete it.
231        """
232        return Gtk.ResponseType.OK
233