1# 2# Gramps - a GTK+/GNOME based genealogy program 3# 4# Copyright (C) 2000-2007 Donald N. Allingham 5# Copyright (C) 2011 Nick Hall 6# 7# This program is free software; you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation; either version 2 of the License, or 10# (at your option) any later version. 11# 12# This program is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with this program; if not, write to the Free Software 19# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20# 21 22""" 23GrampletView interface. 24""" 25 26#------------------------------------------------------------------------- 27# 28# Python modules 29# 30#------------------------------------------------------------------------- 31from gi.repository import Gdk 32from gi.repository import Gtk 33from gi.repository import Pango 34from xml.sax.saxutils import escape 35import time 36import os 37import configparser 38 39import logging 40 41LOG = logging.getLogger(".") 42 43#------------------------------------------------------------------------- 44# 45# Gramps modules 46# 47#------------------------------------------------------------------------- 48from gramps.gen.errors import WindowActiveError 49from gramps.gen.const import URL_MANUAL_PAGE, VERSION_DIR, COLON 50from ..editors import EditPerson, EditFamily 51from ..managedwindow import ManagedWindow 52from ..utils import is_right_click, match_primary_mask, get_link_color 53from ..uimanager import ActionGroup, valid_action_name 54from ..plug import make_gui_option 55from ..plug.quick import run_quick_report_by_name 56from ..display import display_help, display_url 57from ..glade import Glade 58from ..pluginmanager import GuiPluginManager 59from .undoablebuffer import UndoableBuffer 60from gramps.gen.const import GRAMPS_LOCALE as glocale 61_ = glocale.translation.gettext 62 63#------------------------------------------------------------------------- 64# 65# Constants 66# 67#------------------------------------------------------------------------- 68WIKI_HELP_PAGE = URL_MANUAL_PAGE + '_-_Gramplets' 69 70#------------------------------------------------------------------------- 71# 72# Globals 73# 74#------------------------------------------------------------------------- 75PLUGMAN = GuiPluginManager.get_instance() 76NL = "\n" 77 78def AVAILABLE_GRAMPLETS(): 79 return [gplug.id for gplug in PLUGMAN.get_reg_gramplets()] 80 81def GET_AVAILABLE_GRAMPLETS(name): 82 for gplug in PLUGMAN.get_reg_gramplets(): 83 if gplug.id == name: 84 return { 85 "name": gplug.id, 86 "tname": gplug.name, 87 "version": gplug.version, 88 "height": gplug.height, 89 "expand": gplug.expand, 90 "title": gplug.gramplet_title, # translated 91 "content": gplug.gramplet, 92 "detached_width": gplug.detached_width, 93 "detached_height": gplug.detached_height, 94 "state": "maximized", 95 "gramps": "0.0.0", 96 "column": -1, 97 "row": -1, 98 "page": 0, 99 "data": [], 100 "help_url": gplug.help_url, 101 "navtypes": gplug.navtypes, 102 } 103 return None 104 105def GET_GRAMPLET_LIST(nav_type, skip): 106 return [(gplug.gramplet_title, gplug.id) 107 for gplug in PLUGMAN.get_reg_gramplets() 108 if (gplug.navtypes == [] or nav_type in gplug.navtypes) 109 and gplug.name not in skip] 110 111def parse_tag_attr(text): 112 """ 113 Function used to parse markup. 114 """ 115 text = text.strip() 116 parts = text.split(" ", 1) 117 attrs = {} 118 if len(parts) == 2: 119 attr_values = parts[1].split(" ") # "name=value name=value" 120 for av in attr_values: 121 attribute, value = av.split("=", 1) 122 value = value.strip() 123 # trim off quotes: 124 if value[0] == value[-1] and value[0] in ['"', "'"]: 125 value = value[1:-1] 126 attrs[attribute.strip().lower()] = value 127 return [parts[0].upper(), attrs] 128 129def get_gramplet_opts(name, opts): 130 """ 131 Lookup the options for a given gramplet name and update 132 the options with provided dictionary, opts. 133 """ 134 if name in AVAILABLE_GRAMPLETS(): 135 data = GET_AVAILABLE_GRAMPLETS(name) 136 my_data = data.copy() 137 my_data.update(opts) 138 return my_data 139 else: 140 LOG.warning("Unknown gramplet name: '%s'", name) 141 return {} 142 143def get_gramplet_options_by_name(name): 144 """ 145 Get options by gramplet name. 146 """ 147 if name in AVAILABLE_GRAMPLETS(): 148 return GET_AVAILABLE_GRAMPLETS(name).copy() 149 else: 150 LOG.warning("Unknown gramplet name: '%s'", name) 151 return None 152 153def get_gramplet_options_by_tname(name): 154 """ 155 get options by translated name. 156 """ 157 for key in AVAILABLE_GRAMPLETS(): 158 if GET_AVAILABLE_GRAMPLETS(key)["tname"] == name: 159 return GET_AVAILABLE_GRAMPLETS(key).copy() 160 LOG.warning("Unknown gramplet name: '%s'",name) 161 return None 162 163def make_requested_gramplet(gui_class, pane, opts, dbstate, uistate): 164 """ 165 Make a GUI gramplet given its name. 166 """ 167 if opts is None: 168 return None 169 170 if "name" in opts: 171 name = opts["name"] 172 if name in AVAILABLE_GRAMPLETS(): 173 gui = gui_class(pane, dbstate, uistate, **opts) 174 if opts.get("content", None): 175 pdata = PLUGMAN.get_plugin(name) 176 module = PLUGMAN.load_plugin(pdata) 177 if module: 178 getattr(module, opts["content"])(gui) 179 else: 180 LOG.warning("Error loading gramplet '%s': " 181 "skipping content", name) 182 return gui 183 else: 184 LOG.warning("Error loading gramplet: unknown name") 185 return None 186 187def logical_true(value): 188 """ 189 Used for converting text file values to booleans. 190 """ 191 return value in ["True", True, 1, "1"] 192 193def make_callback(func, arg): 194 """ 195 Generates a callback function based off the passed arguments 196 """ 197 return lambda x, y: func(arg) 198 199 200class LinkTag(Gtk.TextTag): 201 """ 202 Class for keeping track of link data. 203 """ 204 lid = 0 205 #obtaining the theme link color once. Restart needed on theme change! 206 linkcolor = Gtk.Label(label='test') #needed to avoid label destroyed to early 207 linkcolor = get_link_color(linkcolor.get_style_context()) 208 209 def __init__(self, buffer): 210 LinkTag.lid += 1 211 Gtk.TextTag.__init__(self, name=str(LinkTag.lid)) 212 tag_table = buffer.get_tag_table() 213 self.set_property('foreground', self.linkcolor) 214 #self.set_property('underline', Pango.Underline.SINGLE) 215 try: 216 tag_table.add(self) 217 except ValueError: # tag is already in tag table 218 pass 219 220class GrampletWindow(ManagedWindow): 221 """ 222 Class for showing a detached gramplet. 223 """ 224 def __init__(self, gramplet): 225 """ 226 Constructs the window, and loads the GUI gramplet. 227 """ 228 self.title = gramplet.title + " " + _("Gramplet") 229 self.gramplet = gramplet 230 self.gramplet.scrolledwindow.set_vexpand(True) 231 self.gramplet.detached_window = self 232 # Keep track of what state it was in: 233 self.docked_state = gramplet.gstate 234 # Now detach it 235 self.gramplet.set_state("detached") 236 ManagedWindow.__init__(self, gramplet.uistate, [], 237 self.title) 238 self.set_window(Gtk.Dialog("", gramplet.uistate.window, 239 Gtk.DialogFlags.DESTROY_WITH_PARENT, 240 (_('_Close'), Gtk.ResponseType.CLOSE)), 241 None, self.title) 242 cfg_name = gramplet.gname.replace(' ', '').lower() + '-gramplet' 243 self.setup_configs('interface.' + cfg_name, 244 gramplet.detached_width, gramplet.detached_height) 245 self.window.add_button(_('_Help'), Gtk.ResponseType.HELP) 246 # add gramplet: 247 if self.gramplet.pui: 248 self.gramplet.pui.active = True 249 self.gramplet.mainframe.reparent(self.window.vbox) 250 self.window.connect('response', self.handle_response) 251 self.show() 252 # After we show, then we hide: 253 self.gramplet.gvclose.hide() 254 self.gramplet.gvstate.hide() 255 self.gramplet.gvproperties.hide() 256 if self.gramplet.titlelabel_entry: 257 self.gramplet.titlelabel_entry.hide() 258 if self.gramplet.pui: 259 for widget in self.gramplet.pui.hidden_widgets(): 260 widget.hide() 261 262 def handle_response(self, object, response): 263 """ 264 Callback for taking care of button clicks. 265 """ 266 if response == Gtk.ResponseType.CLOSE: 267 self.close() 268 elif response == Gtk.ResponseType.HELP: 269 # translated name: 270 if self.gramplet.help_url: 271 if self.gramplet.help_url.startswith("http://"): 272 display_url(self.gramplet.help_url) 273 else: 274 display_help(self.gramplet.help_url) 275 else: 276 display_help(WIKI_HELP_PAGE, 277 self.gramplet.tname.replace(" ", "_")) 278 279 def build_menu_names(self, obj): 280 """ 281 Part of the Gramps window interface. 282 """ 283 return (self.title, 'Gramplet') 284 285 def get_title(self): 286 """ 287 Returns the window title. 288 """ 289 return self.title 290 291 def close(self, *args): 292 """ 293 Dock the detached GrampletWindow back in the column from where it came. 294 """ 295 self.gramplet.scrolledwindow.set_vexpand(False) 296 self.gramplet.detached_window = None 297 self.gramplet.pane.detached_gramplets.remove(self.gramplet) 298 if self.docked_state == "minimized": 299 self.gramplet.set_state("minimized") 300 else: 301 self.gramplet.set_state("maximized") 302 pane = self.gramplet.pane 303 col = self.gramplet.column 304 stack = [] 305 for gframe in pane.columns[col]: 306 gramplet = pane.frame_map[str(gframe)] 307 if gramplet.row > self.gramplet.row: 308 pane.columns[col].remove(gframe) 309 stack.append(gframe) 310 expand = self.gramplet.gstate == "maximized" and self.gramplet.expand 311 column = pane.columns[col] 312 parent = self.gramplet.pane.get_column_frame(self.gramplet.column) 313 self.gramplet.mainframe.reparent(parent) 314 if self.gramplet.pui: 315 self.gramplet.pui.active = self.gramplet.pane.pageview.active 316 for gframe in stack: 317 gramplet = pane.frame_map[str(gframe)] 318 expand = gramplet.gstate == "maximized" and gramplet.expand 319 pane.columns[col].pack_start(gframe, expand, True, 0) 320 # Now make sure they all have the correct expand: 321 for gframe in pane.columns[col]: 322 gramplet = pane.frame_map[str(gframe)] 323 expand, fill, padding, pack = column.query_child_packing(gramplet.mainframe) 324 expand = gramplet.gstate == "maximized" and gramplet.expand 325 column.set_child_packing(gramplet.mainframe, expand, fill, padding, pack) 326 # set_image on buttons as get_image is None in first run 327 # or point to invalid adress in every other run 328 self.gramplet.gvstate.set_image(self.gramplet.xml.get_object( 329 'gvstateimage')) 330 self.gramplet.gvclose.set_image(self.gramplet.xml.get_object( 331 'gvcloseimage')) 332 self.gramplet.gvproperties.set_image(self.gramplet.xml.get_object( 333 'gvpropertiesimage')) 334 self.gramplet.gvclose.show() 335 self.gramplet.gvstate.show() 336 self.gramplet.gvproperties.show() 337 ManagedWindow.close(self, *args) 338 339#------------------------------------------------------------------------ 340 341class GuiGramplet: 342 """ 343 Class that handles the GUI representation of a Gramplet. 344 """ 345 def __init__(self, pane, dbstate, uistate, title, **kwargs): 346 """ 347 Internal constructor for GUI portion of a gramplet. 348 """ 349 self.pane = pane 350 self.view = pane.pageview 351 self.dbstate = dbstate 352 self.uistate = uistate 353 self.track = [] 354 self.title = title 355 self.detached_window = None 356 self.force_update = False 357 self.title_override = False 358 self._tags = [] 359 ########## Set defaults 360 self.gname = kwargs.get("name", "Unnamed Gramplet") 361 self.tname = kwargs.get("tname", "Unnamed Gramplet") 362 self.navtypes = kwargs.get("navtypes", []) 363 self.version = kwargs.get("version", "0.0.0") 364 self.gramps = kwargs.get("gramps", "0.0.0") 365 self.expand = logical_true(kwargs.get("expand", False)) 366 self.height = int(kwargs.get("height", 200)) 367 self.width = int(kwargs.get("width", 375)) 368 self.column = int(kwargs.get("column", -1)) 369 self.detached_height = int(kwargs.get("detached_height", 300)) 370 self.detached_width = int(kwargs.get("detached_width", 400)) 371 self.row = int(kwargs.get("row", -1)) 372 self.page = int(kwargs.get("page", -1)) 373 self.gstate = kwargs.get("state", "maximized") 374 self.data = kwargs.get("data", []) 375 self.help_url = kwargs.get("help_url", WIKI_HELP_PAGE) 376 if self.help_url == 'None': 377 self.help_url = None # to fix up the config file vers of None 378 ########## 379 self.use_markup = False 380 self.pui = None # user code 381 self.tooltips_text = None 382 383 self.link_cursor = \ 384 Gdk.Cursor.new_for_display(Gdk.Display.get_default(), 385 Gdk.CursorType.LEFT_PTR) 386 self.standard_cursor = \ 387 Gdk.Cursor.new_for_display(Gdk.Display.get_default(), 388 Gdk.CursorType.XTERM) 389 390 self.scrolledwindow = None 391 self.textview = None 392 self.buffer = None 393 394 def set_tooltip(self, tip): 395 self.tooltips_text = tip 396 self.scrolledwindow.set_tooltip_text(tip) 397 398 def undo(self): 399 self.buffer.undo() 400 self.text_length = len(self.get_text()) 401 402 def redo(self): 403 self.buffer.redo() 404 self.text_length = len(self.get_text()) 405 406 def on_key_press_event(self, widget, event): 407 """Signal handler. 408 409 Handle formatting shortcuts. 410 411 """ 412 if ((Gdk.keyval_name(event.keyval) == 'Z') and 413 match_primary_mask(event.get_state(), Gdk.ModifierType.SHIFT_MASK)): 414 self.redo() 415 return True 416 elif ((Gdk.keyval_name(event.keyval) == 'z') and 417 match_primary_mask(event.get_state())): 418 self.undo() 419 return True 420 421 return False 422 423 def append_text(self, text, scroll_to="end"): 424 enditer = self.buffer.get_end_iter() 425 start = self.buffer.create_mark(None, enditer, True) 426 self.buffer.insert(enditer, text) 427 self.text_length += len(text) 428 if scroll_to == "end": 429 enditer = self.buffer.get_end_iter() 430 end = self.buffer.create_mark(None, enditer, True) 431 self.textview.scroll_to_mark(end, 0.0, True, 0, 0) 432 elif scroll_to == "start": # beginning of this append 433 self.textview.scroll_to_mark(start, 0.0, True, 0, 0) 434 elif scroll_to == "begin": # beginning of the buffer 435 begin_iter = self.buffer.get_start_iter() 436 begin = self.buffer.create_mark(None, begin_iter, True) 437 self.textview.scroll_to_mark(begin, 0.0, True, 0, 0) 438 else: 439 raise AttributeError("no such cursor position: '%s'" % scroll_to) 440 441 def clear_text(self): 442 self.buffer.set_text('') 443 self.text_length = 0 444 445 def get_text(self): 446 start = self.buffer.get_start_iter() 447 end = self.buffer.get_end_iter() 448 return self.buffer.get_text(start, end, True) # include invisible chars 449 450 def insert_text(self, text): 451 self.buffer.insert_at_cursor(text) 452 self.text_length += len(text) 453 454 def render_text(self, text): 455 markup_pos = {"B": [], "I": [], "U": [], "A": [], "TT": []} 456 retval = "" 457 i = 0 458 r = 0 459 tag = "" 460 while i < len(text): 461 if text[i:i+2] == "</": 462 # start of ending tag 463 stop = text[i:].find(">") 464 if stop < 0: 465 retval += text[i] 466 r += 1 467 i += 1 468 else: 469 markup = text[i+2:i+stop].upper() # close tag 470 markup_pos[markup][-1].append(r) 471 i += stop + 1 472 elif text[i] == "<": 473 # start of start tag 474 stop = text[i:].find(">") 475 if stop < 0: 476 retval += text[i] 477 r += 1 478 i += 1 479 else: 480 markup, attr = parse_tag_attr(text[i+1:i+stop]) 481 markup_pos[markup].append([r, attr]) 482 i += stop + 1 483 elif text[i] == "\\": 484 retval += text[i+1] 485 r += 1 486 i += 2 487 else: 488 retval += text[i] 489 r += 1 490 i += 1 491 offset = self.text_length 492 self.append_text(retval) 493 for items in markup_pos["TT"]: 494 if len(items) == 3: 495 (a, attributes, b) = items 496 start = self.buffer.get_iter_at_offset(a + offset) 497 stop = self.buffer.get_iter_at_offset(b + offset) 498 self.buffer.apply_tag_by_name("fixed", start, stop) 499 for items in markup_pos["B"]: 500 if len(items) == 3: 501 (a, attributes, b) = items 502 start = self.buffer.get_iter_at_offset(a + offset) 503 stop = self.buffer.get_iter_at_offset(b + offset) 504 self.buffer.apply_tag_by_name("bold", start, stop) 505 for items in markup_pos["I"]: 506 if len(items) == 3: 507 (a, attributes, b) = items 508 start = self.buffer.get_iter_at_offset(a + offset) 509 stop = self.buffer.get_iter_at_offset(b + offset) 510 self.buffer.apply_tag_by_name("italic", start, stop) 511 for items in markup_pos["U"]: 512 if len(items) == 3: 513 (a, attributes, b) = items 514 start = self.buffer.get_iter_at_offset(a + offset) 515 stop = self.buffer.get_iter_at_offset(b + offset) 516 self.buffer.apply_tag_by_name("underline", start, stop) 517 for items in markup_pos["A"]: 518 if len(items) == 3: 519 (a, attributes, b) = items 520 start = self.buffer.get_iter_at_offset(a + offset) 521 stop = self.buffer.get_iter_at_offset(b + offset) 522 if "href" in attributes: 523 url = attributes["href"] 524 self.link_region(start, stop, "URL", url) # tooltip? 525 elif "wiki" in attributes: 526 url = attributes["wiki"] 527 self.link_region(start, stop, "WIKI", url) # tooltip? 528 else: 529 LOG.warning("warning: no url on link: '%s'", 530 text[start, stop]) 531 532 def link_region(self, start, stop, link_type, url): 533 link_data = (LinkTag(self.buffer), link_type, url, url) 534 self._tags.append(link_data) 535 self.buffer.apply_tag(link_data[0], start, stop) 536 537 def set_use_markup(self, value): 538 if self.use_markup == value: return 539 self.use_markup = value 540 if value: 541 self.buffer.create_tag("bold", weight=Pango.Weight.HEAVY) 542 self.buffer.create_tag("italic", style=Pango.Style.ITALIC) 543 self.buffer.create_tag("underline", 544 underline=Pango.Underline.SINGLE) 545 self.buffer.create_tag("fixed", font="monospace") 546 else: 547 tag_table = self.buffer.get_tag_table() 548 tag_table.foreach(lambda tag, data: tag_table.remove(tag)) 549 550 def set_text(self, text, scroll_to='start'): 551 self.buffer.set_text('') 552 self.text_length = 0 553 self.append_text(text, scroll_to) 554 self.buffer.reset() 555 556 def get_container_widget(self): 557 raise NotImplementedError 558 559 def add_gui_option(self, option): 560 """ 561 Add an option to the GUI gramplet. 562 """ 563 return make_gui_option(option, self.dbstate, self.uistate, self.track) 564 565 def make_gui_options(self): 566 if not self.pui: return 567 # BEGIN WORKAROUND: 568 # This is necessary because gtk doesn't redisplay these widgets 569 # correctly so we replace them with new ones 570 self.pui.save_options() 571 self.pui.update_options = {} 572 self.pui.option_order = [] 573 self.pui.build_options() 574 # END WORKAROUND 575 if len(self.pui.option_order) == 0: return 576 frame = Gtk.Frame() 577 topbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 578 hbox = Gtk.Grid() 579 hbox.set_column_spacing(5) 580 topbox.pack_start(hbox, False, False, 0) 581 row = 0 582 for item in self.pui.option_order: 583 label = Gtk.Label(label=item + COLON) 584 label.set_halign(Gtk.Align.END) 585 hbox.attach(label, 0, row, 1, 1) 586 # put Widget next to label 587 hbox.attach(self.pui.option_dict[item][0], 1, row, 1, 1) 588 row += 1 589 save_button = Gtk.Button.new_with_mnemonic(_('_Save')) 590 topbox.pack_end(save_button, False, False, 0) 591 save_button.connect('clicked', self.pui.save_update_options) 592 frame.add(topbox) 593 frame.show_all() 594 return frame 595 596 def link(self, text, link_type, data, size=None, tooltip=None): 597 buffer = self.buffer 598 iter = buffer.get_end_iter() 599 offset = buffer.get_char_count() 600 self.append_text(text) 601 start = buffer.get_iter_at_offset(offset) 602 end = buffer.get_end_iter() 603 link_data = (LinkTag(buffer), link_type, data, tooltip) 604 if size: 605 link_data[0].set_property("size-points", size) 606 self._tags.append(link_data) 607 buffer.apply_tag(link_data[0], start, end) 608 609 def on_motion(self, view, event): 610 buffer_location = view.window_to_buffer_coords(Gtk.TextWindowType.TEXT, 611 int(event.x), 612 int(event.y)) 613 iter = view.get_iter_at_location(*buffer_location) 614 if isinstance(iter, tuple): 615 iter = iter[1] 616 cursor = self.standard_cursor 617 ttip = None 618 for (tag, link_type, handle, tooltip) in self._tags: 619 if iter.has_tag(tag): 620 tag.set_property('underline', Pango.Underline.SINGLE) 621 cursor = self.link_cursor 622 ttip = tooltip 623 else: 624 tag.set_property('underline', Pango.Underline.NONE) 625 view.get_window(Gtk.TextWindowType.TEXT).set_cursor(cursor) 626 if ttip: 627 self.scrolledwindow.set_tooltip_text(ttip) 628 elif self.tooltips_text: 629 self.scrolledwindow.set_tooltip_text(self.tooltips_text) 630 return False # handle event further, if necessary 631 632 def on_button_press(self, view, event): 633 # pylint: disable-msg=W0212 634 buffer_location = view.window_to_buffer_coords(Gtk.TextWindowType.TEXT, 635 int(event.x), 636 int(event.y)) 637 iter = view.get_iter_at_location(*buffer_location) 638 if isinstance(iter, tuple): 639 iter = iter[1] 640 for (tag, link_type, handle, tooltip) in self._tags: 641 if iter.has_tag(tag): 642 if link_type == 'Person': 643 if not self.dbstate.db.has_person_handle(handle): 644 return True 645 person = self.dbstate.db.get_person_from_handle(handle) 646 if person is not None: 647 if event.button == 1: # left mouse 648 if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS: 649 try: 650 EditPerson(self.dbstate, 651 self.uistate, 652 [], person) 653 return True # handled event 654 except WindowActiveError: 655 pass 656 elif event.type == Gdk.EventType.BUTTON_PRESS: 657 self.uistate.set_active(handle, 'Person') 658 return True # handled event 659 elif is_right_click(event): 660 #FIXME: add a popup menu with options 661 try: 662 EditPerson(self.dbstate, 663 self.uistate, 664 [], person) 665 return True # handled event 666 except WindowActiveError: 667 pass 668 elif link_type == 'Surname': 669 if event.button == 1: # left mouse 670 if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS: 671 run_quick_report_by_name(self.dbstate, 672 self.uistate, 673 'samesurnames', 674 handle) 675 return True 676 elif link_type == 'Given': 677 if event.button == 1: # left mouse 678 if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS: 679 run_quick_report_by_name(self.dbstate, 680 self.uistate, 681 'samegivens_misc', 682 handle) 683 return True 684 elif link_type == 'Filter': 685 if event.button == 1: # left mouse 686 if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS: 687 run_quick_report_by_name(self.dbstate, 688 self.uistate, 689 'filterbyname', 690 handle) 691 return True 692 elif link_type == 'URL': 693 if event.button == 1: # left mouse 694 display_url(handle) 695 return True 696 elif link_type == 'WIKI': 697 if event.button == 1: # left mouse 698 handle = handle.replace(" ", "_") 699 if "#" in handle: 700 page, section = handle.split("#", 1) 701 display_help(page, section) 702 else: 703 display_help(handle) 704 return True 705 elif link_type == 'Family': 706 if not self.dbstate.db.has_family_handle(handle): 707 return True 708 family = self.dbstate.db.get_family_from_handle(handle) 709 if family is not None: 710 if event.button == 1: # left mouse 711 if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS: 712 try: 713 EditFamily(self.dbstate, 714 self.uistate, 715 [], family) 716 return True # handled event 717 except WindowActiveError: 718 pass 719 elif event.type == Gdk.EventType.BUTTON_PRESS: 720 self.uistate.set_active(handle, 'Family') 721 return True # handle event 722 elif is_right_click(event): 723 #FIXME: add a popup menu with options 724 try: 725 EditFamily(self.dbstate, 726 self.uistate, 727 [], family) 728 return True # handled event 729 except WindowActiveError: 730 pass 731 elif link_type == 'PersonList': 732 if event.button == 1: # left mouse 733 if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS: 734 run_quick_report_by_name(self.dbstate, 735 self.uistate, 736 'filterbyname', 737 'list of people', 738 handles=handle) 739 return True 740 elif link_type == 'Attribute': 741 if event.button == 1: # left mouse 742 if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS: 743 run_quick_report_by_name(self.dbstate, 744 self.uistate, 745 'attribute_match', 746 handle) 747 return True 748 else: # overzealous l10n while setting the link? 749 logging.warning( "Unknown link type %s, %s" % (link_type, RuntimeWarning)) 750 return False # did not handle event 751 752 def set_has_data(self, value): 753 if isinstance(self.pane, Gtk.Notebook): 754 if self.pane.page_num(self) != -1: 755 label = self.pane.get_tab_label(self) 756 label.set_has_data(value) 757 758class GridGramplet(GuiGramplet): 759 """ 760 Class that handles the plugin interfaces for the GrampletView. 761 """ 762 TARGET_TYPE_FRAME = 80 763 LOCAL_DRAG_TYPE = 'GRAMPLET' 764 LOCAL_DRAG_TARGET = Gtk.TargetEntry.new(LOCAL_DRAG_TYPE, 0, 765 TARGET_TYPE_FRAME) 766 767 def __init__(self, pane, dbstate, uistate, title, **kwargs): 768 """ 769 Internal constructor for GUI portion of a gramplet. 770 """ 771 GuiGramplet.__init__(self, pane, dbstate, uistate, title, 772 **kwargs) 773 774 self.xml = Glade() 775 self.gvwin = self.xml.toplevel 776 self.mainframe = self.xml.get_object('gvgramplet') 777 self.gvwin.remove(self.mainframe) 778 779 self.textview = self.xml.get_object('gvtextview') 780 self.buffer = UndoableBuffer() 781 self.text_length = 0 782 self.textview.set_buffer(self.buffer) 783 self.textview.connect("key-press-event", self.on_key_press_event) 784 #self.buffer = self.textview.get_buffer() 785 self.scrolledwindow = self.xml.get_object('gvscrolledwindow') 786 self.scrolledwindow.set_policy(Gtk.PolicyType.AUTOMATIC, 787 Gtk.PolicyType.AUTOMATIC) 788 self.vboxtop = self.xml.get_object('vboxtop') 789 self.titlelabel = self.xml.get_object('gvtitle') 790 self.titlelabel.get_children()[0].set_text("<b><i>%s</i></b>" % 791 self.title) 792 self.titlelabel.get_children()[0].set_use_markup(True) 793 self.titlelabel.connect("clicked", self.edit_title) 794 self.titlelabel_entry = None 795 self.gvclose = self.xml.get_object('gvclose') 796 self.gvclose.connect('clicked', self.close) 797 self.gvstate = self.xml.get_object('gvstate') 798 self.gvstate.connect('clicked', self.change_state) 799 self.gvproperties = self.xml.get_object('gvproperties') 800 self.gvproperties.connect('clicked', self.set_properties) 801 self.xml.get_object('gvcloseimage').set_from_icon_name('window-close', 802 Gtk.IconSize.MENU) 803 self.xml.get_object('gvstateimage').set_from_icon_name('list-remove', 804 Gtk.IconSize.MENU) 805 self.xml.get_object('gvpropertiesimage').set_from_icon_name('document-properties', 806 Gtk.IconSize.MENU) 807 808 # source: 809 drag = self.gvproperties 810 drag.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, 811 [GridGramplet.LOCAL_DRAG_TARGET], 812 Gdk.DragAction.COPY) 813 814 # default tooltip 815 msg = _("Drag Properties Button to move and click it for setup") 816 if not self.tooltips_text: 817 self.set_tooltip(msg) 818 819 def edit_title(self, widget): 820 """ 821 Edit the title in the GUI. 822 """ 823 parent = widget.get_parent() 824 widget.hide() 825 if self.titlelabel_entry is None: 826 self.titlelabel_entry = Gtk.Entry() 827 parent = widget.get_parent() 828 parent.pack_end(self.titlelabel_entry, True, True, 0) 829 self.titlelabel_entry.connect("focus-out-event", 830 self.edit_title_done) 831 self.titlelabel_entry.connect("activate", self.edit_title_done) 832 self.titlelabel_entry.connect("key-press-event", 833 self.edit_title_keypress) 834 self.titlelabel_entry.set_text(widget.get_children()[0].get_text()) 835 self.titlelabel_entry.show() 836 self.titlelabel_entry.grab_focus() 837 return True 838 839 def edit_title_keypress(self, widget, event): 840 """ 841 Edit the title, handle escape. 842 """ 843 if event.type == Gdk.EventType.KEY_PRESS: 844 if event.keyval == Gdk.KEY_Escape: 845 self.titlelabel.show() 846 widget.hide() 847 848 def edit_title_done(self, widget, event=None): 849 """ 850 Edit title in GUI, finishing callback. 851 """ 852 result = self.set_title(widget.get_text()) 853 if result: # if ok to set title to that 854 self.titlelabel.show() 855 widget.hide() 856 return False # Return False for gtk requirement 857 858 def close(self, *obj): 859 """ 860 Remove (delete) the gramplet from view. 861 """ 862 if self.gstate == "detached": 863 return 864 self.gstate = "closed" 865 self.pane.closed_gramplets.append(self) 866 self.mainframe.get_parent().remove(self.mainframe) 867 868 def detach(self): 869 """ 870 Detach the gramplet from the GrampletView, and open in own window. 871 """ 872 # hide buttons: 873 #self.set_state("detached") 874 self.pane.detached_gramplets.append(self) 875 # make a window, and attach it there 876 self.detached_window = GrampletWindow(self) 877 878 def set_state(self, state): 879 """ 880 Set the state of a gramplet. 881 """ 882 oldstate = self.gstate 883 self.gstate = state 884 if state == "minimized": 885 self.scrolledwindow.hide() 886 self.xml.get_object('gvstateimage').set_from_icon_name('list-add', 887 Gtk.IconSize.MENU) 888 column = self.mainframe.get_parent() # column 889 expand, fill, padding, pack = column.query_child_packing(self.mainframe) 890 column.set_child_packing(self.mainframe, False, fill, padding, pack) 891 else: 892 self.scrolledwindow.show() 893 self.xml.get_object('gvstateimage').set_from_icon_name('list-remove', 894 Gtk.IconSize.MENU) 895 column = self.mainframe.get_parent() # column 896 expand, fill, padding, pack = column.query_child_packing(self.mainframe) 897 column.set_child_packing(self.mainframe, 898 self.expand, 899 fill, 900 padding, 901 pack) 902 if self.pui and self.pui.dirty: 903 self.pui.update() 904 905 def change_state(self, obj): 906 """ 907 Change the state of a gramplet. 908 """ 909 if self.gstate == "detached": 910 pass # don't change if detached 911 else: 912 if self.gstate == "maximized": 913 self.set_state("minimized") 914 else: 915 self.set_state("maximized") 916 917 def set_properties(self, obj): 918 """ 919 Set the properties of a gramplet. 920 """ 921 if self.gstate == "detached": 922 pass 923 else: 924 self.detach() 925 return 926 self.expand = not self.expand 927 if self.gstate == "maximized": 928 column = self.mainframe.get_parent() # column 929 expand, fill, padding, pack = column.query_child_packing(self.mainframe) 930 column.set_child_packing(self.mainframe, self.expand, fill, 931 padding, pack) 932 def get_source_widget(self): 933 """ 934 Hack to allow us to send this object to the drop_widget 935 method as a context. 936 """ 937 return self.gvproperties 938 939 def get_container_widget(self): 940 return self.scrolledwindow 941 942 def get_title(self): 943 return self.title 944 945 def set_height(self, height): 946 self.height = height 947 self.scrolledwindow.set_size_request(-1, self.height) 948 self.set_state(self.gstate) 949 950 def get_height(self): 951 return self.height 952 953 def get_detached_height(self): 954 return self.detached_height 955 956 def get_detached_width(self): 957 return self.detached_width 958 959 def set_detached_height(self, height): 960 self.detached_height = height 961 962 def set_detached_width(self, width): 963 self.detached_width = width 964 965 def get_expand(self): 966 return self.expand 967 968 def set_expand(self, value): 969 self.expand = value 970 self.scrolledwindow.set_size_request(-1, self.height) 971 self.set_state(self.gstate) 972 973 def set_title(self, new_title, set_override=True): 974 # can't do it if already titled that way 975 if self.title == new_title: 976 return True 977 if(new_title in self.pane.gramplet_map or 978 new_title != escape(new_title)): # avoid XML specific characters 979 return False 980 if set_override: 981 self.title_override = True 982 del self.pane.gramplet_map[self.title] 983 self.title = new_title 984 if self.detached_window: 985 self.detached_window.window.set_title("%s %s - Gramps" % 986 (new_title, _("Gramplet"))) 987 self.pane.gramplet_map[self.title] = self 988 self.titlelabel.get_children()[0].set_text("<b><i>%s</i></b>" % 989 self.title) 990 self.titlelabel.get_children()[0].set_use_markup(True) 991 return True 992 993class GrampletPane(Gtk.ScrolledWindow): 994 def __init__(self, configfile, pageview, dbstate, uistate, **kwargs): 995 self._config = Configuration(self) 996 self.track = [] 997 Gtk.ScrolledWindow.__init__(self) 998 self.configfile = os.path.join(VERSION_DIR, "%s.ini" % configfile) 999 # default for new user; may be overridden in config: 1000 self.column_count = kwargs.get("column_count", 2) 1001 # width of window, if sidebar; may be overridden in config: 1002 self.pane_position = kwargs.get("pane_position", -1) 1003 self.pane_orientation = kwargs.get("pane_orientation", "horizontal") 1004 self.splitview = kwargs.get("splitview", None) 1005 self.default_gramplets = kwargs.get("default_gramplets", 1006 ["Top Surnames", "Welcome"]) 1007 self.dbstate = dbstate 1008 self.uistate = uistate 1009 self.pageview = pageview 1010 self.pane = self 1011 self._popup_xy = None 1012 self.at_popup_action = None 1013 self.at_popup_menu = None 1014 user_gramplets = self.load_gramplets() 1015 # build the GUI: 1016 msg = _("Right click to add gramplets") 1017 self.set_tooltip_text(msg) 1018 self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) 1019 self.eventb = Gtk.EventBox() 1020 self.hbox = Gtk.Box(homogeneous=True) 1021 self.eventb.add(self.hbox) 1022 self.add(self.eventb) 1023 self.set_kinetic_scrolling(True) 1024 self.set_capture_button_press(True) 1025 # Set up drag and drop 1026 self.drag_dest_set(Gtk.DestDefaults.MOTION | 1027 Gtk.DestDefaults.HIGHLIGHT | 1028 Gtk.DestDefaults.DROP, 1029 [GridGramplet.LOCAL_DRAG_TARGET], 1030 Gdk.DragAction.COPY) 1031 self.connect('drag_drop', self.drop_widget) 1032 self.eventb.connect('button-press-event', self._button_press) 1033 1034 # Create the columns: 1035 self.columns = [] 1036 for i in range(self.column_count): 1037 self.columns.append(Gtk.Box(orientation=Gtk.Orientation.VERTICAL)) 1038 self.hbox.pack_start(self.columns[-1], True, True, 0) 1039 # Load the gramplets 1040 self.gramplet_map = {} # title->gramplet 1041 self.frame_map = {} # frame->gramplet 1042 self.detached_gramplets = [] # list of detached gramplets 1043 self.closed_gramplets = [] # list of closed gramplets 1044 self.closed_opts = [] # list of closed options from ini file 1045 # get the user's gramplets from ~/.gramps/gramplets.ini 1046 # Load the user's gramplets: 1047 for name_opts in user_gramplets: 1048 if name_opts is None: 1049 continue 1050 (name, opts) = name_opts 1051 all_opts = get_gramplet_opts(name, opts) 1052 if "state" not in all_opts: 1053 all_opts["state"] = "maximized" 1054 if all_opts["state"] == "closed": 1055 self.gramplet_map[all_opts["title"]] = None # save closed name 1056 self.closed_opts.append(all_opts) 1057 continue 1058 if "title" not in all_opts: 1059 all_opts["title"] = _("Untitled Gramplet") 1060 set_override = False 1061 else: 1062 set_override = True 1063 # May have to change title 1064 g = make_requested_gramplet(GridGramplet, self, all_opts, 1065 self.dbstate, self.uistate) 1066 if g: 1067 g.title_override = set_override # to continue to override, when this is saved 1068 # make a unique title: 1069 unique = g.get_title() 1070 cnt = 1 1071 while unique in self.gramplet_map: 1072 unique = g.get_title() + ("-%d" % cnt) 1073 cnt += 1 1074 g.set_title(unique, set_override=False) 1075 self.gramplet_map[unique] = g 1076 self.frame_map[str(g.mainframe)] = g 1077 else: 1078 LOG.warning("Can't make gramplet of type '%s'.", name) 1079 self.place_gramplets() 1080 1081 def show_all(self): 1082 """ 1083 This seems to be necessary to hide the hidden 1084 parts of a collapsed gramplet on main view. 1085 """ 1086 super(GrampletPane, self).show_all() 1087 for gramplet in list(self.gramplet_map.values()): 1088 if gramplet.gstate == "minimized": 1089 gramplet.set_state("minimized") 1090 1091 def set_state_all(self): 1092 """ 1093 This seems to be necessary to hide the hidden 1094 parts of a collapsed gramplet on sidebars. 1095 """ 1096 for gramplet in list(self.gramplet_map.values()): 1097 if gramplet.gstate in ["minimized", "maximized"]: 1098 gramplet.set_state(gramplet.gstate) 1099 1100 def get_column_frame(self, column_num): 1101 if column_num < len(self.columns): 1102 return self.columns[column_num] 1103 else: 1104 return self.columns[-1] # it was too big, so select largest 1105 1106 def clear_gramplets(self): 1107 """ 1108 Detach all of the mainframe gramplets from the columns. 1109 """ 1110 gramplets = (g for g in self.gramplet_map.values() 1111 if g is not None) 1112 for gramplet in gramplets: 1113 if (gramplet.gstate == "detached" or gramplet.gstate == "closed"): 1114 continue 1115 column = gramplet.mainframe.get_parent() 1116 if column: 1117 column.remove(gramplet.mainframe) 1118 1119 def place_gramplets(self, recolumn=False): 1120 """ 1121 Place the gramplet mainframes in the columns. 1122 """ 1123 gramplets = [g for g in self.gramplet_map.values() 1124 if g is not None] 1125 # put the gramplets where they go: 1126 # sort by row 1127 gramplets.sort(key=lambda x: x.row) 1128 rows = [0] * max(self.column_count, 1) 1129 for cnt, gramplet in enumerate(gramplets): 1130 # see if the user wants this in a particular location: 1131 # and if there are that many columns 1132 if gramplet.column >= 0 and gramplet.column < self.column_count: 1133 pos = gramplet.column 1134 else: 1135 # else, spread them out: 1136 pos = cnt % self.column_count 1137 gramplet.column = pos 1138 gramplet.row = rows[gramplet.column] 1139 rows[gramplet.column] += 1 1140 if recolumn and (gramplet.gstate == "detached" or 1141 gramplet.gstate == "closed"): 1142 continue 1143 if gramplet.gstate == "minimized": 1144 self.columns[pos].pack_start(gramplet.mainframe, False, True, 0) 1145 else: 1146 self.columns[pos].pack_start(gramplet.mainframe, 1147 gramplet.expand, True, 0) 1148 # set height on gramplet.scrolledwindow here: 1149 gramplet.scrolledwindow.set_size_request(-1, gramplet.height) 1150 # Can't minimize here, because Gramps calls show_all later: 1151 #if gramplet.gstate == "minimized": # starts max, change to min it 1152 # gramplet.set_state("minimized") # minimize it 1153 # set minimized is called in page subclass hack (above) 1154 if gramplet.gstate == "detached": 1155 gramplet.detach() 1156 elif gramplet.gstate == "closed": 1157 gramplet.close() 1158 1159 def load_gramplets(self): 1160 retval = [] 1161 filename = self.configfile 1162 if filename and os.path.exists(filename): 1163 cp = configparser.ConfigParser(strict=False) 1164 try: 1165 cp.read(filename, encoding='utf-8') 1166 except Exception as err: 1167 LOG.warning("Failed to load gramplets from %s because %s", 1168 filename, str(err)) 1169 return [None] 1170 for sec in cp.sections(): 1171 if sec == "Gramplet View Options": 1172 if "column_count" in cp.options(sec): 1173 self.column_count = int(cp.get(sec, "column_count")) 1174 if "pane_position" in cp.options(sec): 1175 self.pane_position = int(cp.get(sec, "pane_position")) 1176 if "pane_orientation" in cp.options(sec): 1177 self.pane_orientation = cp.get(sec, "pane_orientation") 1178 else: 1179 data = {} 1180 for opt in cp.options(sec): 1181 if opt.startswith("data["): 1182 temp = data.get("data", {}) 1183 #temp.append(cp.get(sec, opt).strip()) 1184 pos = int(opt[5:-1]) 1185 temp[pos] = cp.get(sec, opt).strip() 1186 data["data"] = temp 1187 else: 1188 data[opt] = cp.get(sec, opt).strip() 1189 if "data" in data: 1190 data["data"] = [data["data"][key] 1191 for key in sorted(data["data"].keys())] 1192 if "name" not in data: 1193 data["name"] = "Unnamed Gramplet" 1194 data["tname"] = _("Unnamed Gramplet") 1195 retval.append((data["name"], data)) # name, opts 1196 else: 1197 # give defaults as currently known 1198 for name in self.default_gramplets: 1199 if name in AVAILABLE_GRAMPLETS(): 1200 retval.append((name, GET_AVAILABLE_GRAMPLETS(name))) 1201 return retval 1202 1203 def save(self): 1204 if len(self.frame_map) + len(self.detached_gramplets) == 0: 1205 return # something is the matter 1206 filename = self.configfile 1207 try: 1208 with open(filename, "w", encoding='utf-8') as fp: 1209 fp.write(";; Gramps gramplets file\n") 1210 fp.write(";; Automatically created at %s" % 1211 time.strftime("%Y/%m/%d %H:%M:%S\n\n")) 1212 fp.write("[Gramplet View Options]\n") 1213 fp.write("column_count=%d\n" % self.column_count) 1214 fp.write("pane_position=%d\n" % self.pane_position) 1215 fp.write("pane_orientation=%s\n\n" % self.pane_orientation) 1216 # showing gramplets: 1217 for col in range(self.column_count): 1218 row = 0 1219 for gframe in self.columns[col]: 1220 gramplet = self.frame_map[str(gframe)] 1221 opts = get_gramplet_options_by_name(gramplet.gname) 1222 if opts is not None: 1223 base_opts = opts.copy() 1224 for key in base_opts: 1225 if key in gramplet.__dict__: 1226 base_opts[key] = gramplet.__dict__[key] 1227 base_opts['state'] = gramplet.gstate 1228 fp.write("[%s]\n" % gramplet.title) # section 1229 for key in base_opts: 1230 if key == "content": continue 1231 elif key == "tname": continue 1232 elif key == "column": continue 1233 elif key == "row": continue 1234 elif key == "version": continue # code, don't save 1235 elif key == "gramps": continue # code, don't save 1236 elif key == "data": 1237 if not isinstance(base_opts["data"], (list, tuple)): 1238 fp.write("data[0]=%s\n" % base_opts["data"]) 1239 else: 1240 cnt = 0 1241 for item in base_opts["data"]: 1242 fp.write("data[%d]=%s\n" % (cnt, item)) 1243 cnt += 1 1244 else: 1245 fp.write("%s=%s\n"% (key, base_opts[key])) 1246 fp.write("column=%d\n" % col) 1247 fp.write("row=%d\n\n" % row) 1248 row += 1 1249 for gramplet in self.detached_gramplets: 1250 opts = get_gramplet_options_by_name(gramplet.gname) 1251 if opts is not None: 1252 base_opts = opts.copy() 1253 for key in base_opts: 1254 if key in gramplet.__dict__: 1255 base_opts[key] = gramplet.__dict__[key] 1256 base_opts['state'] = gramplet.gstate 1257 fp.write("[%s]\n" % gramplet.title) 1258 for key in base_opts: 1259 if key == "content": continue 1260 elif key == "title": 1261 if "title_override" in base_opts: 1262 base_opts["title"] = base_opts["title_override"] 1263 fp.write("title=%s\n" % base_opts[key]) 1264 elif key == "tname": continue 1265 elif key == "version": continue # code, don't save 1266 elif key == "gramps": continue # code, don't save 1267 elif key == "data": 1268 if not isinstance(base_opts["data"], (list, tuple)): 1269 fp.write("data[0]=%s\n" % base_opts["data"]) 1270 else: 1271 cnt = 0 1272 for item in base_opts["data"]: 1273 fp.write("data[%d]=%s\n" % (cnt, item)) 1274 cnt += 1 1275 else: 1276 fp.write("%s=%s\n" % (key, base_opts[key])) 1277 1278 except IOError as err: 1279 LOG.warning("Failed to open %s because $s; gramplets not saved", 1280 filename, str(err)) 1281 return 1282 1283 def drop_widget(self, source, context, x, y, timedata): 1284 """ 1285 This is the destination method for handling drag and drop 1286 of a gramplet onto the main scrolled window. 1287 Also used for adding new gramplets, then context should be GridGramplet 1288 """ 1289 button = None 1290 if isinstance(context, Gdk.DragContext): 1291 button = Gtk.drag_get_source_widget(context) 1292 else: 1293 button = context.get_source_widget() 1294 if button: 1295 hbox = button.get_parent() 1296 mframe = hbox.get_parent() 1297 mainframe = mframe.get_parent() # actually a vbox 1298 rect = source.get_allocation() 1299 sx, sy = rect.width, rect.height 1300 # Convert to LTR co-ordinates when using RTL locale 1301 if source.get_direction() == Gtk.TextDirection.RTL: 1302 x = sx - x 1303 # first, find column: 1304 col = 0 1305 for i in range(len(self.columns)): 1306 if x < (sx/len(self.columns) * (i + 1)): 1307 col = i 1308 break 1309 if button: 1310 fromcol = mainframe.get_parent() 1311 if fromcol: 1312 fromcol.remove(mainframe) 1313 # now find where to insert in column: 1314 stack = [] 1315 current_row = 0 1316 for gframe in self.columns[col]: 1317 gramplet = self.frame_map[str(gframe)] 1318 gramplet.row = current_row 1319 current_row += 1 1320 rect = gframe.get_allocation() 1321 if y < (rect.y + 15): # starts at 0, this allows insert before 1322 self.columns[col].remove(gframe) 1323 stack.append(gframe) 1324 maingramplet = self.frame_map.get(str(mainframe), None) 1325 maingramplet.column = col 1326 maingramplet.row = current_row 1327 current_row += 1 1328 expand = maingramplet.gstate == "maximized" and maingramplet.expand 1329 self.columns[col].pack_start(mainframe, expand, True, 0) 1330 for gframe in stack: 1331 gramplet = self.frame_map[str(gframe)] 1332 gramplet.row = current_row 1333 current_row += 1 1334 expand = gramplet.gstate == "maximized" and gramplet.expand 1335 self.columns[col].pack_start(gframe, expand, True, 0) 1336 return True 1337 1338 def set_columns(self, num): 1339 if num < 1: 1340 num = 1 1341 # clear the gramplets: 1342 self.clear_gramplets() 1343 # clear the columns: 1344 for column in self.columns: 1345 frame = column.get_parent() 1346 frame.remove(column) 1347 del column 1348 # create the new ones: 1349 self.column_count = num 1350 self.columns = [] 1351 for i in range(self.column_count): 1352 self.columns.append(Gtk.Box(orientation=Gtk.Orientation.VERTICAL)) 1353 self.columns[-1].show() 1354 self.hbox.pack_start(self.columns[-1], True, True, 0) 1355 # place the gramplets back in the new columns 1356 self.place_gramplets(recolumn=True) 1357 self.show() 1358 1359 def restore_gramplet(self, name): 1360 ############### First kind: from current session 1361 for gramplet in self.closed_gramplets: 1362 if gramplet.title == name: 1363 #gramplet.gstate = "maximized" 1364 self.closed_gramplets.remove(gramplet) 1365 if self._popup_xy is not None: 1366 self.drop_widget(self, gramplet, 1367 self._popup_xy[0], self._popup_xy[1], 0) 1368 else: 1369 self.drop_widget(self, gramplet, 0, 0, 0) 1370 gramplet.set_state("maximized") 1371 return 1372 ################ Second kind: from options 1373 for opts in self.closed_opts: 1374 if opts["title"] == name: 1375 self.closed_opts.remove(opts) 1376 g = make_requested_gramplet(GridGramplet, self, opts, 1377 self.dbstate, self.uistate) 1378 if g: 1379 self.gramplet_map[opts["title"]] = g 1380 self.frame_map[str(g.mainframe)] = g 1381 else: 1382 LOG.warning("Can't make gramplet of type '%s'.", name) 1383 if g: 1384 gramplet = g 1385 gramplet.gstate = "maximized" 1386 if gramplet.column >= 0 and gramplet.column < len(self.columns): 1387 pos = gramplet.column 1388 else: 1389 pos = 0 1390 self.columns[pos].pack_start(gramplet.mainframe, 1391 expand=gramplet.expand) 1392 # set height on gramplet.scrolledwindow here: 1393 gramplet.scrolledwindow.set_size_request(-1, gramplet.height) 1394 ## now drop it in right place 1395 if self._popup_xy is not None: 1396 self.drop_widget(self, gramplet, 1397 self._popup_xy[0], self._popup_xy[1], 0) 1398 else: 1399 self.drop_widget(self, gramplet, 0, 0, 0) 1400 1401 def add_gramplet(self, tname): 1402 all_opts = get_gramplet_options_by_tname(tname) 1403 name = all_opts["name"] 1404 if all_opts is None: 1405 LOG.warning("Unknown gramplet type: '%s'; bad " 1406 "gramplets.ini file?", name) 1407 return 1408 if "title" not in all_opts: 1409 all_opts["title"] = "Untitled Gramplet" 1410 # uniqify titles: 1411 unique = all_opts["title"] 1412 cnt = 1 1413 while unique in self.gramplet_map: 1414 unique = all_opts["title"] + ("-%d" % cnt) 1415 cnt += 1 1416 all_opts["title"] = unique 1417 if all_opts["title"] not in self.gramplet_map: 1418 g = make_requested_gramplet(GridGramplet, self, all_opts, 1419 self.dbstate, self.uistate) 1420 if g: 1421 self.gramplet_map[all_opts["title"]] = g 1422 self.frame_map[str(g.mainframe)] = g 1423 gramplet = g 1424 if gramplet.column >= 0 and gramplet.column < len(self.columns): 1425 pos = gramplet.column 1426 else: 1427 pos = 0 1428 self.columns[pos].pack_start(gramplet.mainframe, 1429 gramplet.expand, True, 0) 1430 # set height on gramplet.scrolledwindow here: 1431 gramplet.scrolledwindow.set_size_request(-1, gramplet.height) 1432 ## now drop it in right place 1433 if self._popup_xy is not None: 1434 self.drop_widget(self, gramplet, 1435 self._popup_xy[0], self._popup_xy[1], 0) 1436 else: 1437 self.drop_widget(self, gramplet, 0, 0, 0) 1438 if gramplet.pui: 1439 gramplet.pui.active = True 1440 gramplet.pui.update() 1441 else: 1442 LOG.warning("Can't make gramplet of type '%s'.", name) 1443 1444 def _button_press(self, obj, event): 1445 ui_def = ( 1446 ''' <menu id="Popup"> 1447 <submenu> 1448 <attribute name="action">win.AddGramplet</attribute> 1449 <attribute name="label" translatable="yes">Add a gramplet</attribute> 1450 %s 1451 </submenu> 1452 <submenu> 1453 <attribute name="action">win.RestoreGramplet</attribute> 1454 <attribute name="label" translatable="yes">''' 1455 '''Restore a gramplet</attribute> 1456 %s 1457 </submenu> 1458 </menu> 1459 ''') 1460 menuitem = ('<item>\n' 1461 '<attribute name="action">win.%s</attribute>\n' 1462 '<attribute name="label">%s</attribute>\n' 1463 '</item>\n') 1464 1465 if is_right_click(event): 1466 self._popup_xy = (event.x, event.y) 1467 uiman = self.uistate.uimanager 1468 actions = [] 1469 r_menuitems = '' 1470 a_menuitems = '' 1471 plugs = [gplug for gplug in PLUGMAN.get_reg_gramplets() if 1472 gplug.navtypes == [] or 'Dashboard' in gplug.navtypes] 1473 plugs.sort(key=lambda x: x.name) 1474 for plug in plugs: 1475 action_name = valid_action_name(plug.id) 1476 a_menuitems += menuitem % (action_name, escape(plug.name)) 1477 actions.append((action_name, 1478 make_callback(self.add_gramplet, plug.name))) 1479 names = [gramplet.title for gramplet in self.closed_gramplets] 1480 names.extend(opts["title"] for opts in self.closed_opts) 1481 names.sort() 1482 if len(names) > 0: 1483 for name in names: 1484 # 'name' could be non-ASCII when in non-English language 1485 # action names must be in ASCII, so use 'id' instead. 1486 action_name = valid_action_name(str(id(name))) 1487 r_menuitems += menuitem % (action_name, escape(name)) 1488 actions.append((action_name, 1489 make_callback(self.restore_gramplet, 1490 name))) 1491 1492 if self.at_popup_action: 1493 uiman.remove_ui(self.at_popup_menu) 1494 uiman.remove_action_group(self.at_popup_action) 1495 self.at_popup_action = ActionGroup('AtPopupActions', 1496 actions) 1497 uiman.insert_action_group(self.at_popup_action) 1498 self.at_popup_menu = uiman.add_ui_from_string([ 1499 ui_def % (a_menuitems, r_menuitems)]) 1500 uiman.update_menu() 1501 1502 menu = uiman.get_widget('Popup') 1503 popup_menu = Gtk.Menu.new_from_model(menu) 1504 popup_menu.attach_to_widget(obj, None) 1505 popup_menu.show_all() 1506 if Gtk.MINOR_VERSION < 22: 1507 # ToDo The following is reported to work poorly with Wayland 1508 popup_menu.popup(None, None, None, None, 1509 event.button, event.time) 1510 else: 1511 popup_menu.popup_at_pointer(event) 1512 return True 1513 return False 1514 1515 def set_inactive(self): 1516 for title in self.gramplet_map: 1517 if self.gramplet_map[title].pui: 1518 if self.gramplet_map[title].gstate != "detached": 1519 self.gramplet_map[title].pui.active = False 1520 1521 def set_active(self): 1522 for title in self.gramplet_map: 1523 if self.gramplet_map[title].pui: 1524 self.gramplet_map[title].pui.active = True 1525 if self.gramplet_map[title].pui.dirty: 1526 if self.gramplet_map[title].gstate == "maximized": 1527 self.gramplet_map[title].pui.update() 1528 1529 def on_delete(self): 1530 gramplets = (g for g in self.gramplet_map.values() 1531 if g is not None) 1532 for gramplet in gramplets: 1533 # this is the only place where the gui runs user code directly 1534 if gramplet.pui: 1535 gramplet.pui.on_save() 1536 self.save() 1537 1538 def can_configure(self): 1539 """ 1540 See :class:`.PageView` 1541 1542 :return: bool 1543 """ 1544 return True 1545 1546 def _get_configure_page_funcs(self): 1547 """ 1548 Return a list of functions that create gtk elements to use in the 1549 notebook pages of the Configure dialog 1550 1551 :return: list of functions 1552 """ 1553 def generate_pages(): 1554 return [self.config_panel] + \ 1555 [self.build_panel(gramplet) for gramplet in 1556 sorted(list(self.gramplet_map.values()), key=lambda g: g.title) 1557 if gramplet.gstate != "closed"] 1558 return generate_pages 1559 1560 def get_columns(self): 1561 return self.column_count 1562 1563 def config_panel(self, configdialog): 1564 """ 1565 Function that builds the widget in the configuration dialog 1566 """ 1567 grid = Gtk.Grid() 1568 grid.set_border_width(12) 1569 grid.set_column_spacing(6) 1570 grid.set_row_spacing(6) 1571 1572 self._config.register('Gramplet View Options.column_count', 1573 int, 1574 self.get_columns, # pane 1575 self.set_columns) # pane 1576 1577 configdialog.add_pos_int_entry(grid, 1578 _('Number of Columns'), 1579 0, 1580 'Gramplet View Options.column_count', 1581 self._config.set, 1582 config=self._config) 1583 return _('Gramplet Layout'), grid 1584 1585 def build_panel(self, gramplet): 1586 self._config.register("%s.title" % gramplet.title, 1587 str, gramplet.get_title, gramplet.set_title) 1588 self._config.register("%s.height" % gramplet.title, 1589 int, gramplet.get_height, gramplet.set_height) 1590 self._config.register("%s.detached_height" % gramplet.title, 1591 int, gramplet.get_detached_height, 1592 gramplet.set_detached_height) 1593 self._config.register("%s.detached_width" % gramplet.title, 1594 int, gramplet.get_detached_width, 1595 gramplet.set_detached_width) 1596 self._config.register("%s.expand" % gramplet.title, 1597 bool, gramplet.get_expand, gramplet.set_expand) 1598 def gramplet_panel(configdialog): 1599 configdialog.window.set_size_request(600, -1) 1600 grid = Gtk.Grid() 1601 grid.set_border_width(12) 1602 grid.set_column_spacing(6) 1603 grid.set_row_spacing(6) 1604 # Title: 1605 configdialog.add_entry(grid, 1606 _('Title'), 1607 0, 1608 "%s.title" % gramplet.title, 1609 self._config.set, 1610 config=self._config) 1611 # Expand to max height 1612 configdialog.add_checkbox(grid, 1613 _("Use maximum height available"), 1614 1, 1615 "%s.expand" % gramplet.title, 1616 config=self._config) 1617 # Height 1618 configdialog.add_pos_int_entry(grid, 1619 _('Height if not maximized'), 1620 2, 1621 "%s.height" % gramplet.title, 1622 self._config.set, 1623 config=self._config) 1624 # Options: 1625 options = gramplet.make_gui_options() 1626 if options: 1627 grid.attach(options, 1, 5, 3, 1) 1628 return gramplet.title, grid 1629 return gramplet_panel 1630 1631class Configuration: 1632 """ 1633 A config wrapper to redirect set/get to GrampletPane. 1634 """ 1635 def __init__(self, pane): 1636 self.pane = pane 1637 self.data = {} 1638 1639 def get(self, key): 1640 vtype, getter, setter = self.data[key] 1641 return getter() 1642 1643 def set(self, widget, key): 1644 """ 1645 Hooked to signal, it is widget, key. 1646 Hooked to config, it is key, widget 1647 """ 1648 if key not in self.data: 1649 widget, key = key, widget 1650 vtype, getter, setter = self.data[key] 1651 if type(widget) == vtype: 1652 setter(widget) 1653 else: 1654 try: 1655 value = vtype(widget.get_text()) 1656 except: 1657 return 1658 setter(value) 1659 1660 def register(self, key, vtype, getter, setter): 1661 """ 1662 register a key with type, getter, and setter methods. 1663 """ 1664 self.data[key] = (vtype, getter, setter) 1665