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