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