1# -*- coding: UTF-8 -*- 2 3# Copyright 2011 Jiří Janoušek <janousek.jiri@gmail.com> 4# Copyright 2014-2018 Jaap Karssenberg <jaap.karssenberg@gmail.com> 5 6 7from gi.repository import Gtk 8from gi.repository import GObject 9from gi.repository import Gdk 10 11import zim.errors 12 13from zim.plugins import PluginManager, InsertedObjectTypeExtension 14from zim.insertedobjects import InsertedObjectType 15 16from zim.gui.widgets import ScrolledTextView, ScrolledWindow, widget_set_css 17 18 19# Constants for grab-focus-cursor and release-focus-cursor 20POSITION_BEGIN = 1 21POSITION_END = 2 22 23 24class InsertedObjectWidget(Gtk.EventBox): 25 '''Base class & contained for custom object widget 26 27 We derive from a C{Gtk.EventBox} because we want to re-set the 28 default cursor for the area of the object widget. For this the 29 widget needs it's own window for drawing. 30 31 @signal: C{link-clicked (link)}: To be emitted when the user clicks a link 32 @signal: C{link-enter (link)}: To be emitted when the mouse pointer enters a link 33 @signal: C{link-leave (link)}: To be emitted when the mouse pointer leaves a link 34 @signal: C{grab-cursor (position)}: emitted when embedded widget 35 should grab focus, position can be either POSITION_BEGIN or POSITION_END 36 @signal: C{release-cursor (position)}: emitted when the embedded 37 widget wants to give back focus to the embedding TextView 38 ''' 39 40 # define signals we want to use - (closure type, return type and arg types) 41 __gsignals__ = { 42 'link-clicked': (GObject.SignalFlags.RUN_LAST, None, (object,)), 43 'link-enter': (GObject.SignalFlags.RUN_LAST, None, (object,)), 44 'link-leave': (GObject.SignalFlags.RUN_LAST, None, (object,)), 45 46 'grab-cursor': (GObject.SignalFlags.RUN_LAST, None, (int,)), 47 'release-cursor': (GObject.SignalFlags.RUN_LAST, None, (int,)), 48 } 49 50 expand = True 51 52 def __init__(self, widget_style=None): 53 GObject.GObject.__init__(self) 54 self._has_cursor = False 55 self._vbox = Gtk.VBox() 56 Gtk.EventBox.add(self, self._vbox) 57 if widget_style == 'inline': 58 self._vbox.set_name('zim-inserted-object-inline') 59 else: 60 self.set_border_width(3) 61 widget_set_css(self._vbox, 'zim-inserted-object', 'border: 1px solid #ccc') 62 # Choosen #ccc because it should give contract with both light and 63 # dark theme, but less than the text color itself 64 # Can be overruled in user css is really conflicts with theme 65 66 def add(self, widget): 67 '''Add a widget to the object''' 68 self._vbox.pack_start(widget, True, True, 0) 69 70 def add_header(self, widget): 71 '''Add an header widget on top of the object''' 72 widget.get_style_context().add_class(Gtk.STYLE_CLASS_BACKGROUND) 73 widget_set_css(widget, 'zim-inserted-object-head', 'border-bottom: 1px solid #ccc') 74 self._vbox.pack_start(widget, True, True, 0) 75 self._vbox.reorder_child(widget, 0) 76 77 def remove(self, widget): 78 self._vbox.remove(widget) 79 80 def do_realize(self): 81 Gtk.EventBox.do_realize(self) 82 window = self.get_parent_window() 83 window.set_cursor(Gdk.Cursor.new(Gdk.CursorType.ARROW)) 84 85 def set_textview_wrap_width(self, width): 86 if self.expand: 87 self.set_size_request(width, -1) 88 89 def has_cursor(self): 90 '''Returns True if this object has an internal cursor. Will be 91 used by the TextView to determine if the cursor should go 92 "into" the object or just jump from the position before to the 93 position after the object. If True the embedded widget is 94 expected to support grab_cursor() and use release_cursor(). 95 ''' 96 return self._has_cursor 97 98 def set_has_cursor(self, has_cursor): 99 '''See has_cursor()''' 100 self._has_cursor = has_cursor 101 102 def grab_cursor(self, position): 103 '''Emits the grab-cursor signal''' 104 self.emit('grab-cursor', position) 105 106 def release_cursor(self, position): 107 '''Emits the release-cursor signal''' 108 self.emit('release-cursor', position) 109 110 def do_button_press_event(self, event): 111 if Gdk.Event.triggers_context_menu(event) \ 112 and event.type == Gdk.EventType.BUTTON_PRESS: 113 self._do_popup_menu(event) 114 return True # Prevent propagating event to parent textview 115 116 def do_button_release_event(self, event): 117 return True # Prevent propagating event to parent textview 118 119 def do_popup_menu(self): 120 # See https://developer.gnome.org/gtk3/stable/gtk-migrating-checklist.html#checklist-popup-menu 121 self._do_popup_menu(None) 122 123 def _do_popup_menu(self, event): 124 menu = Gtk.Menu() 125 try: 126 self.populate_popup(menu) 127 except NotImplementedError: 128 return False 129 else: 130 menu.show_all() 131 132 if event is not None: 133 button = event.button 134 event_time = event.time 135 else: 136 button = 0 137 event_time = Gtk.get_current_event_time() 138 139 menu.attach_to_widget(self) 140 menu.popup(None, None, None, None, button, event_time) 141 142 def populate_popup(self, menu): 143 raise NotImplementedError 144 145 def edit_object(self): 146 raise NotImplementedError 147 148 149class TextViewWidget(InsertedObjectWidget): 150 151 def __init__(self, buffer): 152 InsertedObjectWidget.__init__(self) 153 self.set_has_cursor(True) 154 self.buffer = buffer 155 self._init_view() 156 self._init_signals() 157 158 def _init_view(self): 159 win, self.view = ScrolledTextView(monospace=True, 160 hpolicy=Gtk.PolicyType.AUTOMATIC, vpolicy=Gtk.PolicyType.NEVER, shadow=Gtk.ShadowType.NONE) 161 self.view.set_buffer(self.buffer) 162 self.view.set_editable(True) 163 self.add(win) 164 165 def _init_signals(self): 166 # Hook up integration with pageview cursor movement 167 self.view.connect('move-cursor', self.on_move_cursor) 168 self.connect('parent-set', self.on_parent_set) 169 self.parent_notify_h = None 170 171 def set_editable(self, editable): 172 self.view.set_editable(editable) 173 self.view.set_cursor_visible(editable) 174 175 def on_parent_set(self, widget, old_parent): 176 if old_parent and self.parent_notify_h: 177 old_parent.disconnect(self.parent_notify_h) 178 self.parent_notify_h = None 179 parent = self.get_parent() 180 if parent: 181 self.set_editable(parent.get_editable()) 182 self.parent_notify_h = parent.connect('notify::editable', self.on_parent_notify) 183 184 def on_parent_notify(self, widget, prop, *args): 185 self.set_editable(self.get_parent().get_editable()) 186 187 def do_grab_cursor(self, position): 188 # Emitted when we are requesed to capture the cursor 189 begin, end = self.buffer.get_bounds() 190 if position == POSITION_BEGIN: 191 self.buffer.place_cursor(begin) 192 else: 193 self.buffer.place_cursor(end) 194 self.view.grab_focus() 195 196 def on_move_cursor(self, view, step_size, count, extend_selection): 197 # If you try to move the cursor out of the sourceview 198 # release the cursor to the parent textview 199 buffer = view.get_buffer() 200 iter = buffer.get_iter_at_mark(buffer.get_insert()) 201 if (iter.is_start() or iter.is_end()) \ 202 and not extend_selection: 203 if iter.is_start() and count < 0: 204 self.release_cursor(POSITION_BEGIN) 205 return None 206 elif iter.is_end() and count > 0: 207 self.release_cursor(POSITION_END) 208 return None 209 210 return None # let parent handle this signal 211 212 213class ImageFileWidget(InsertedObjectWidget): 214 215 expand = False 216 217 def __init__(self, file, widget_style=None): 218 InsertedObjectWidget.__init__(self, widget_style=widget_style) 219 self.file = file 220 if file.exists(): 221 self.image = Gtk.Image.new_from_file(file.path) 222 else: 223 self.image = Gtk.Image() 224 self.image.set_property('margin', 1) # seperate line and content 225 self.add(self.image) 226 227 # TODO: setup file monitor to reload on changed -- update it in "set_file" 228 229 # TODO: shrink image when larger than width -- have "shrink" class property 230 # implement set_textview_wrap_width() for this here 231 232 def set_file(self, file): 233 self.file = file 234 if self.file.exists(): 235 self.image.set_from_file(file.path) 236 else: 237 self.image.clear() 238 239 240def _find_plugin(name): 241 plugins = PluginManager() 242 for plugin_name in plugins.list_installed_plugins(): 243 try: 244 klass = plugins.get_plugin_class(plugin_name) 245 for objtype in klass.discover_classes(InsertedObjectTypeExtension): 246 if objtype.name == name: 247 activatable = klass.check_dependencies_ok() 248 return (plugin_name, klass.plugin_info['name'], activatable, klass) 249 except: 250 continue 251 return None 252 253 254class UnkownObjectWidget(TextViewWidget): 255 256 def __init__(self, buffer): 257 TextViewWidget.__init__(self, buffer) 258 #~ self.view.set_editable(False) # object knows best how to manage content 259 # TODO set background grey ? 260 261 type = buffer.object_attrib.get('type') 262 plugin_info = _find_plugin(type) if type else None 263 if plugin_info: 264 header = self._add_load_plugin_bar(plugin_info) 265 self.add_header(header) 266 else: 267 label = Gtk.Label( 268 _("No plugin available to display objects of type: %s") % type # T: Label for object manager 269 ) 270 self.add_header(label) 271 272 def _add_load_plugin_bar(self, plugin_info): 273 key, name, activatable, klass = plugin_info 274 275 hbox = Gtk.HBox(False, 5) 276 label = Gtk.Label(label=_("Plugin \"%s\" is required to display this object") % name) 277 # T: Label for object manager - "%s" is the plugin name 278 hbox.pack_start(label, True, True, 0) 279 280 button = Gtk.Button(_("Enable plugin")) # T: Label for object manager 281 button.set_relief(Gtk.ReliefStyle.NONE) 282 hbox.pack_end(button, False, False, 0) 283 284 if activatable: 285 # Plugin can be enabled 286 def load_plugin(button): 287 PluginManager().load_plugin(key) 288 button.connect("clicked", load_plugin) 289 else: 290 button.set_sensitive(False) 291 292 return hbox 293 294 295class UnkownObjectBuffer(Gtk.TextBuffer): 296 297 def __init__(self, attrib, data): 298 Gtk.TextBuffer.__init__(self) 299 self.object_attrib = attrib 300 self.set_text(data) 301 302 def get_object_data(self): 303 attrib = self.object_attrib.copy() 304 start, end = self.get_bounds() 305 data = start.get_text(end) 306 return attrib, data 307 308 309class UnknownInsertedObject(InsertedObjectType): 310 311 name = "unknown" 312 313 label = _('Unkown Object') # T: label for inserted object 314 315 def parse_attrib(self, attrib): 316 # Overrule base class checks since we don't know what this object is 317 attrib.setdefault('type', self.name) 318 return attrib 319 320 def model_from_data(self, notebook, page, attrib, data): 321 return UnkownObjectBuffer(attrib, data) 322 323 def data_from_model(self, buffer): 324 return buffer.get_object_data() 325 326 def create_widget(self, buffer): 327 return UnkownObjectWidget(buffer) 328 329 330class UnkownImage(object): 331 332 def __init__(self, file, attrib, data): 333 self.file = file 334 self.object_attrib = attrib 335 self.object_data = data 336 337 def get_object_data(self): 338 return self.object_attrib.copy(), self.object_data 339 340 def connect(self, signal, handler): 341 assert signal == 'changed' 342 pass 343 344 def __getattr__(self, name): 345 return getattr(self.file, name) 346 347 348class UnknownInsertedImageObject(InsertedObjectType): 349 350 name = "unknown-image" 351 352 label = _('Unkown Image type') # T: label for inserted object 353 354 def parse_attrib(self, attrib): 355 # Overrule base class checks since we don't know what this object is 356 attrib.setdefault('type', self.name) 357 return attrib 358 359 def model_from_data(self, notebook, page, attrib, data): 360 file = notebook.resolve_file(attrib['src'], page) 361 return UnkownImage(file, attrib, data) 362 363 def data_from_model(self, model): 364 return model.get_object_data() 365 366 def create_widget(self, model): 367 return ImageFileWidget(model) 368 369 370 371class InsertedObjectUI(object): 372 373 def __init__(self, uimanager, pageview): 374 self.uimanager = uimanager 375 self.pageview = pageview 376 self.insertedobjects = PluginManager().insertedobjects 377 self._ui_id = None 378 self._actiongroup = None 379 self.add_ui() 380 self.insertedobjects.connect('changed', self.on_changed) 381 382 def on_changed(self, o): 383 self.uimanager.remove_ui(self._ui_id) 384 self.uimanager.remove_action_group(self._actiongroup) 385 self._ui_id = None 386 self._actiongroup = None 387 self.add_ui() 388 389 def add_ui(self): 390 assert self._ui_id is None 391 assert self._actiongroup is None 392 393 self._actiongroup = self.get_actiongroup() 394 ui_xml = self.get_ui_xml() 395 396 self.uimanager.insert_action_group(self._actiongroup, 0) 397 self._ui_id = self.uimanager.add_ui_from_string(ui_xml) 398 399 def get_actiongroup(self): 400 actions = [ 401 ('insert_' + obj.name, obj.verb_icon, obj.label, '', None, self._action_handler) 402 for obj in self.insertedobjects.values() 403 ] 404 group = Gtk.ActionGroup('inserted_objects') 405 group.add_actions(actions) 406 return group 407 408 def get_ui_xml(self): 409 menulines = [] 410 for obj in self.insertedobjects.values(): 411 name = 'insert_' + obj.name 412 menulines.append("<menuitem action='%s'/>\n" % name) 413 return """\ 414 <ui> 415 <menubar name='menubar'> 416 <menu action='insert_menu'> 417 <placeholder name='plugin_items'> 418 %s 419 </placeholder> 420 </menu> 421 </menubar> 422 </ui> 423 """ % ( 424 ''.join(menulines), 425 ) 426 427 def _action_handler(self, action): 428 try: 429 name = action.get_name()[7:] # len('insert_') = 7 430 otype = self.insertedobjects[name] 431 pageview = self.pageview 432 notebook = pageview.notebook 433 page = pageview.page 434 try: 435 model = otype.new_model_interactive(self.pageview, notebook, page) 436 except ValueError: 437 return # dialog cancelled 438 self.pageview.insert_object_model(otype, model) 439 except: 440 zim.errors.exception_handler( 441 'Exception during action: %s' % name) 442