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# Copyright (C) 2011       Gary Burton
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation; either version 2 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with this program; if not, write to the Free Software
20# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21#
22
23"""
24Module that implements the gramplet bar fuctionality.
25"""
26
27#-------------------------------------------------------------------------
28#
29# Set up logging
30#
31#-------------------------------------------------------------------------
32import logging
33LOG = logging.getLogger('.grampletbar')
34
35#-------------------------------------------------------------------------
36#
37# Python modules
38#
39#-------------------------------------------------------------------------
40import time
41import os
42import configparser
43
44#-------------------------------------------------------------------------
45#
46# GNOME modules
47#
48#-------------------------------------------------------------------------
49from gi.repository import Gtk
50
51#-------------------------------------------------------------------------
52#
53# Gramps modules
54#
55#-------------------------------------------------------------------------
56from gramps.gen.const import GRAMPS_LOCALE as glocale
57_ = glocale.translation.gettext
58from gramps.gen.const import URL_MANUAL_PAGE, VERSION_DIR
59from gramps.gen.config import config
60from gramps.gen.constfunc import win
61from ..managedwindow import ManagedWindow
62from ..display import display_help, display_url
63from .grampletpane import (AVAILABLE_GRAMPLETS,
64                           GET_AVAILABLE_GRAMPLETS,
65                           GET_GRAMPLET_LIST,
66                           get_gramplet_opts,
67                           get_gramplet_options_by_name,
68                           make_requested_gramplet,
69                           GuiGramplet)
70from .undoablebuffer import UndoableBuffer
71from ..utils import is_right_click
72from ..dialog import QuestionDialog
73
74#-------------------------------------------------------------------------
75#
76# Constants
77#
78#-------------------------------------------------------------------------
79WIKI_HELP_PAGE = URL_MANUAL_PAGE + '_-_Gramplets'
80NL = "\n"
81
82#-------------------------------------------------------------------------
83#
84# GrampletBar class
85#
86#-------------------------------------------------------------------------
87class GrampletBar(Gtk.Notebook):
88    """
89    A class which defines the graphical representation of the GrampletBar.
90    """
91    def __init__(self, dbstate, uistate, pageview, configfile, defaults):
92        Gtk.Notebook.__init__(self)
93
94        self.dbstate = dbstate
95        self.uistate = uistate
96        self.pageview = pageview
97        self.configfile = os.path.join(VERSION_DIR, "%s.ini" % configfile)
98        self.defaults = defaults
99        self.detached_gramplets = []
100        self.empty = False
101        self.close_buttons = []
102
103        self.set_group_name("grampletbar")
104        self.set_show_border(False)
105        self.set_scrollable(True)
106
107        book_button = Gtk.Button()
108        # Arrow is too small unless in a box
109        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
110        arrow = Gtk.Arrow(arrow_type=Gtk.ArrowType.DOWN,
111                                    shadow_type=Gtk.ShadowType.NONE)
112        arrow.show()
113        box.add(arrow)
114        box.show()
115        book_button.add(box)
116        book_button.set_relief(Gtk.ReliefStyle.NONE)
117        book_button.connect('clicked', self.__button_clicked)
118        book_button.set_property("tooltip-text", _("Gramplet Bar Menu"))
119        book_button.show()
120        self.set_action_widget(book_button, Gtk.PackType.END)
121
122        self.connect('page-added', self.__page_added)
123        self.connect('page-removed', self.__page_removed)
124        self.connect('create-window', self.__create_window)
125
126        config_settings, opts_list = self.__load(defaults)
127
128        opts_list.sort(key=lambda opt: opt["page"])
129        for opts in opts_list:
130            if opts["name"] in AVAILABLE_GRAMPLETS():
131                all_opts = get_gramplet_opts(opts["name"], opts)
132                gramplet = make_requested_gramplet(TabGramplet, self, all_opts,
133                                                   self.dbstate, self.uistate)
134                if gramplet:
135                    self.__add_tab(gramplet)
136
137        if len(opts_list) == 0:
138            self.empty = True
139            self.__create_empty_tab()
140
141        if config_settings[0]:
142            self.show()
143        self.set_current_page(config_settings[1])
144
145        uistate.connect('grampletbar-close-changed', self.cb_close_changed)
146
147        # Connect after gramplets added to prevent making them active
148        self.connect('switch-page', self.__switch_page)
149
150    def _get_config_setting(self, configparser, section, setting, fn=None):
151        """
152        Get a section.setting value from the config parser.
153        Takes a configparser instance, a section, a setting, and
154        optionally a post-processing function (typically int).
155
156        Always returns a value of the appropriate type.
157        """
158        value = ""
159        try:
160            value = configparser.get(section, setting)
161            value = value.strip()
162            if fn:
163                value = fn(value)
164        except:
165            if fn:
166                value = fn()
167            else:
168                value = ""
169        return value
170
171    def __load(self, defaults):
172        """
173        Load the gramplets from the configuration file.
174        """
175        retval = []
176        visible = True
177        default_page = 0
178        filename = self.configfile
179        if filename and os.path.exists(filename):
180            cp = configparser.ConfigParser()
181            try:
182                cp.read(filename, encoding='utf-8')
183            except:
184                pass
185            for sec in cp.sections():
186                if sec == "Bar Options":
187                    if "visible" in cp.options(sec):
188                        visible = self._get_config_setting(cp, sec, "visible") == "True"
189                    if "page" in cp.options(sec):
190                        default_page = self._get_config_setting(cp, sec, "page", int)
191                else:
192                    data = {}
193                    for opt in cp.options(sec):
194                        if opt.startswith("data["):
195                            temp = data.get("data", {})
196                            #temp.append(self._get_config_setting(cp, sec, opt))
197                            pos = int(opt[5:-1])
198                            temp[pos] = self._get_config_setting(cp, sec, opt)
199                            data["data"] = temp
200                        else:
201                            data[opt] = self._get_config_setting(cp, sec, opt)
202                    if "data" in data:
203                        data["data"] = [data["data"][key]
204                                        for key in sorted(data["data"].keys())]
205                    if "name" not in data:
206                        data["name"] = "Unnamed Gramplet"
207                        data["tname"] = _("Unnamed Gramplet")
208                    retval.append(data)
209        else:
210            # give defaults as currently known
211            for name in defaults:
212                if name in AVAILABLE_GRAMPLETS():
213                    retval.append(GET_AVAILABLE_GRAMPLETS(name))
214        return ((visible, default_page), retval)
215
216    def __save(self):
217        """
218        Save the gramplet configuration.
219        """
220        filename = self.configfile
221        try:
222            with open(filename, "w", encoding='utf-8') as fp:
223                fp.write(";; Gramplet bar configuration file" + NL)
224                fp.write((";; Automatically created at %s" %
225                                         time.strftime("%Y/%m/%d %H:%M:%S")) + NL + NL)
226                fp.write("[Bar Options]" + NL)
227                fp.write(("visible=%s" + NL) % self.get_property('visible'))
228                fp.write(("page=%d" + NL) % self.get_current_page())
229                fp.write(NL)
230
231                if self.empty:
232                    gramplet_list = []
233                else:
234                    gramplet_list = [self.get_nth_page(page_num)
235                                     for page_num in range(self.get_n_pages())]
236
237                for page_num, gramplet in enumerate(gramplet_list):
238                    opts = get_gramplet_options_by_name(gramplet.gname)
239                    if opts is not None:
240                        base_opts = opts.copy()
241                        for key in base_opts:
242                            if key in gramplet.__dict__:
243                                base_opts[key] = gramplet.__dict__[key]
244                        fp.write(("[%s]" + NL) % gramplet.gname)
245                        for key in base_opts:
246                            if key in ["content", "title", "tname", "row", "column",
247                                       "page", "version", "gramps"]: # don't save
248                                continue
249                            elif key == "data":
250                                if not isinstance(base_opts["data"], (list, tuple)):
251                                    fp.write(("data[0]=%s" + NL) % base_opts["data"])
252                                else:
253                                    cnt = 0
254                                    for item in base_opts["data"]:
255                                        fp.write(("data[%d]=%s" + NL) % (cnt, item))
256                                        cnt += 1
257                            else:
258                                fp.write(("%s=%s" + NL)% (key, base_opts[key]))
259                        fp.write(("page=%d" + NL) % page_num)
260                        fp.write(NL)
261
262        except IOError:
263            LOG.warning("Failed writing '%s'; gramplets not saved" % filename)
264            return
265
266    def set_active(self):
267        """
268        Called with the view is set as active.
269        """
270        if not self.empty:
271            gramplet = self.get_nth_page(self.get_current_page())
272            if gramplet and gramplet.pui:
273                gramplet.pui.active = True
274                if gramplet.pui.dirty:
275                    gramplet.pui.update()
276
277    def set_inactive(self):
278        """
279        Called with the view is set as inactive.
280        """
281        if not self.empty:
282            gramplet = self.get_nth_page(self.get_current_page())
283            if gramplet and gramplet.pui:
284                gramplet.pui.active = False
285
286    def on_delete(self):
287        """
288        Called when the view is closed.
289        """
290        list(map(self.__dock_gramplet, self.detached_gramplets))
291        if not self.empty:
292            for page_num in range(self.get_n_pages()):
293                gramplet = self.get_nth_page(page_num)
294                # this is the only place where the gui runs user code directly
295                if gramplet.pui:
296                    gramplet.pui.on_save()
297        self.__save()
298
299    def add_gramplet(self, gname):
300        """
301        Add a gramplet by name.
302        """
303        if self.has_gramplet(gname):
304            return
305        all_opts = get_gramplet_options_by_name(gname)
306        gramplet = make_requested_gramplet(TabGramplet, self, all_opts,
307                                           self.dbstate, self.uistate)
308        if not gramplet:
309            LOG.warning("Problem creating '%s'", gname)
310            return
311
312        page_num = self.__add_tab(gramplet)
313        self.set_current_page(page_num)
314
315    def remove_gramplet(self, gname):
316        """
317        Remove a gramplet by name.
318        """
319        for gramplet in self.detached_gramplets:
320            if gramplet.gname == gname:
321                self.__dock_gramplet(gramplet)
322                self.remove_page(self.page_num(gramplet))
323                return
324
325        for page_num in range(self.get_n_pages()):
326            gramplet = self.get_nth_page(page_num)
327            if gramplet.gname == gname:
328                self.remove_page(page_num)
329                return
330
331    def has_gramplet(self, gname):
332        """
333        Return True if the GrampletBar contains the gramplet, else False.
334        """
335        return gname in self.all_gramplets()
336
337    def all_gramplets(self):
338        """
339        Return a list of names of all the gramplets in the GrampletBar.
340        """
341        if self.empty:
342            return self.detached_gramplets
343        else:
344            return [gramplet.gname for gramplet in self.get_children() +
345                                                   self.detached_gramplets]
346
347    def restore(self):
348        """
349        Restore the GrampletBar to its default gramplets.
350        """
351        list(map(self.remove_gramplet, self.all_gramplets()))
352        list(map(self.add_gramplet, self.defaults))
353        self.set_current_page(0)
354
355    def __create_empty_tab(self):
356        """
357        Create an empty tab to be displayed when the GrampletBar is empty.
358        """
359        tab_label = Gtk.Label(label=_('Gramplet Bar'))
360        tab_label.show()
361        msg = _('Select the down arrow on the right corner for adding, removing or restoring gramplets.')
362        content = Gtk.Label(label=msg)
363        content.set_halign(Gtk.Align.START)
364        content.set_line_wrap(True)
365        content.set_size_request(150, -1)
366        content.show()
367        self.append_page(content, tab_label)
368        return content
369
370    def __add_tab(self, gramplet):
371        """
372        Add a tab to the notebook for the given gramplet.
373        """
374        width = -1  # Allow tab width to adjust (smaller) to sidebar
375        height = min(int(self.uistate.screen_height() * 0.20), 400)
376        gramplet.set_size_request(width, height)
377
378        label = self.__create_tab_label(gramplet)
379        page_num = self.append_page(gramplet, label)
380        return page_num
381
382    def __create_tab_label(self, gramplet):
383        """
384        Create a tab label consisting of a label and a close button.
385        """
386        tablabel = TabLabel(gramplet, self.__delete_clicked)
387
388        if hasattr(gramplet.pui, "has_data"):
389            tablabel.set_has_data(gramplet.pui.has_data)
390        else: # just a function; always show yes it has data
391            tablabel.set_has_data(True)
392
393        if config.get('interface.grampletbar-close'):
394            tablabel.use_close(True)
395        else:
396            tablabel.use_close(False)
397
398        return tablabel
399
400    def cb_close_changed(self):
401        """
402        Close button preference changed.
403        """
404        for gramplet in self.get_children():
405            tablabel = self.get_tab_label(gramplet)
406            if not isinstance(tablabel, Gtk.Label):
407                tablabel.use_close(config.get('interface.grampletbar-close'))
408
409    def __delete_clicked(self, button, gramplet):
410        """
411        Called when the delete button is clicked.
412        """
413        page_num = self.page_num(gramplet)
414        self.remove_page(page_num)
415
416    def __switch_page(self, notebook, unused, new_page):
417        """
418        Called when the user has switched to a new GrampletBar page.
419        """
420        old_page = notebook.get_current_page()
421        if old_page >= 0:
422            gramplet = self.get_nth_page(old_page)
423            if gramplet and gramplet.pui:
424                gramplet.pui.active = False
425
426        gramplet = self.get_nth_page(new_page)
427        if not self.empty:
428            if gramplet and gramplet.pui:
429                gramplet.pui.active = True
430                if gramplet.pui.dirty:
431                    gramplet.pui.update()
432
433    def __page_added(self, notebook, unused, new_page):
434        """
435        Called when a new page is added to the GrampletBar.
436        """
437        gramplet = self.get_nth_page(new_page)
438        if self.empty:
439            if isinstance(gramplet, TabGramplet):
440                self.empty = False
441                if new_page == 0:
442                    self.remove_page(1)
443                else:
444                    self.remove_page(0)
445            else:
446                return
447        gramplet.pane = self
448        label = self.__create_tab_label(gramplet)
449        self.set_tab_label(gramplet, label)
450        self.set_tab_reorderable(gramplet, True)
451        self.set_tab_detachable(gramplet, True)
452        if gramplet in self.detached_gramplets:
453            self.detached_gramplets.remove(gramplet)
454            self.reorder_child(gramplet, gramplet.page)
455
456    def __page_removed(self, notebook, unused, page_num):
457        """
458        Called when a page is removed to the GrampletBar.
459        """
460        if self.get_n_pages() == 0:
461            self.empty = True
462            self.__create_empty_tab()
463
464    def __create_window(self, grampletbar, gramplet, x_pos, y_pos):
465        """
466        Called when the user has switched to a new GrampletBar page.
467        """
468        gramplet.page = self.page_num(gramplet)
469        self.detached_gramplets.append(gramplet)
470        win = DetachedWindow(grampletbar, gramplet, x_pos, y_pos)
471        gramplet.detached_window = win
472        return win.get_notebook()
473
474    def __dock_gramplet(self, gramplet):
475        """
476        Dock a detached gramplet.
477        """
478        gramplet.detached_window.close()
479        gramplet.detached_window = None
480
481    def __button_clicked(self, button):
482        """
483        Called when the drop-down button is clicked.
484        """
485        self.menu = Gtk.Menu()
486        menu = self.menu
487
488        ag_menu = Gtk.MenuItem(label=_('Add a gramplet'))
489        nav_type = self.pageview.navigation_type()
490        skip = self.all_gramplets()
491        gramplet_list = GET_GRAMPLET_LIST(nav_type, skip)
492        gramplet_list.sort()
493        self.__create_submenu(ag_menu, gramplet_list, self.__add_clicked)
494        ag_menu.show()
495        menu.append(ag_menu)
496
497        if not (self.empty or config.get('interface.grampletbar-close')):
498            rg_menu = Gtk.MenuItem(label=_('Remove a gramplet'))
499            gramplet_list = [(gramplet.title, gramplet.gname)
500                             for gramplet in self.get_children() +
501                                             self.detached_gramplets]
502            gramplet_list.sort()
503            self.__create_submenu(rg_menu, gramplet_list,
504                                  self.__remove_clicked)
505            rg_menu.show()
506            menu.append(rg_menu)
507
508        rd_menu = Gtk.MenuItem(label=_('Restore default gramplets'))
509        rd_menu.connect("activate", self.__restore_clicked)
510        rd_menu.show()
511        menu.append(rd_menu)
512
513        menu.show_all()
514        menu.popup(None, None, cb_menu_position, button, 0, 0)
515
516    def __create_submenu(self, main_menu, gramplet_list, callback_func):
517        """
518        Create a submenu of the context menu.
519        """
520        if main_menu:
521            submenu = main_menu.get_submenu()
522            submenu = Gtk.Menu()
523            for entry in gramplet_list:
524                item = Gtk.MenuItem(label=entry[0])
525                item.connect("activate", callback_func, entry[1])
526                item.show()
527                submenu.append(item)
528            main_menu.set_submenu(submenu)
529
530    def __add_clicked(self, menu, gname):
531        """
532        Called when a gramplet is added from the context menu.
533        """
534        self.add_gramplet(gname)
535
536    def __remove_clicked(self, menu, gname):
537        """
538        Called when a gramplet is removed from the context menu.
539        """
540        self.remove_gramplet(gname)
541
542    def __restore_clicked(self, menu):
543        """
544        Called when restore defaults is clicked from the context menu.
545        """
546        QuestionDialog(
547            _("Restore to defaults?"),
548            _("The gramplet bar will be restored to contain its default "
549              "gramplets.  This action cannot be undone."),
550            _("OK"),
551            self.restore,
552            parent=self.uistate.window)
553
554    def get_config_funcs(self):
555        """
556        Return a list of configuration functions.
557        """
558        funcs = []
559        if self.empty:
560            gramplets = []
561        else:
562            gramplets = self.get_children()
563        for gramplet in gramplets + self.detached_gramplets:
564            gui_options = gramplet.make_gui_options()
565            if gui_options:
566                funcs.append(self.__build_panel(gramplet.title, gui_options))
567        return funcs
568
569    def __build_panel(self, title, gui_options):
570        """
571        Return a configuration function that returns the title of a page in
572        the Configure View dialog and a gtk container defining the page.
573        """
574        def gramplet_panel(configdialog):
575            return title, gui_options
576        return gramplet_panel
577
578#-------------------------------------------------------------------------
579#
580# TabGramplet class
581#
582#-------------------------------------------------------------------------
583class TabGramplet(Gtk.ScrolledWindow, GuiGramplet):
584    """
585    Class that handles the plugin interfaces for the GrampletBar.
586    """
587    def __init__(self, pane, dbstate, uistate, title, **kwargs):
588        """
589        Internal constructor for GUI portion of a gramplet.
590        """
591        Gtk.ScrolledWindow.__init__(self)
592        GuiGramplet.__init__(self, pane, dbstate, uistate, title, **kwargs)
593
594        self.scrolledwindow = self
595        self.textview = Gtk.TextView()
596        self.textview.set_editable(False)
597        self.textview.set_wrap_mode(Gtk.WrapMode.WORD)
598        self.buffer = UndoableBuffer()
599        self.text_length = 0
600        self.textview.set_buffer(self.buffer)
601        self.textview.connect("key-press-event", self.on_key_press_event)
602        self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
603        self.add(self.textview)
604        self.show_all()
605        self.track = []
606
607    def get_title(self):
608        return self.title
609
610    def get_container_widget(self):
611        """
612        Return the top level container widget.
613        """
614        return self
615
616#-------------------------------------------------------------------------
617#
618# DetachedWindow class
619#
620#-------------------------------------------------------------------------
621class DetachedWindow(ManagedWindow):
622    """
623    Class for showing a detached gramplet.
624    """
625    def __init__(self, grampletbar, gramplet, x_pos, y_pos):
626        """
627        Construct the window.
628        """
629        self.title = gramplet.title + " " + _("Gramplet")
630        self.grampletbar = grampletbar
631        self.gramplet = gramplet
632
633        ManagedWindow.__init__(self, gramplet.uistate, [],
634                                             self.title)
635        self.set_window(Gtk.Dialog("", gramplet.uistate.window,
636                                   Gtk.DialogFlags.DESTROY_WITH_PARENT,
637                                   (_('_Close'), Gtk.ResponseType.CLOSE)),
638                        None,
639                        self.title)
640        self.window.move(x_pos, y_pos)
641        self.window.set_default_size(gramplet.detached_width,
642                                     gramplet.detached_height)
643        self.window.add_button(_('_Help'), Gtk.ResponseType.HELP)
644        self.window.connect('response', self.handle_response)
645
646        self.notebook = Gtk.Notebook()
647        self.notebook.set_show_tabs(False)
648        self.notebook.set_show_border(False)
649        self.notebook.connect('page-added', self.page_added)
650        self.notebook.show()
651        self.window.vbox.pack_start(self.notebook, True, True, 0)
652        self.show()
653
654    def page_added(self, notebook, gramplet, page_num):
655        """
656        Called when the gramplet is added to the notebook.  This takes the
657        focus from the help button (bug #6306).
658        """
659        gramplet.grab_focus()
660
661    def handle_response(self, object, response):
662        """
663        Callback for taking care of button clicks.
664        """
665        if response == Gtk.ResponseType.CLOSE:
666            self.close()
667        elif response == Gtk.ResponseType.HELP:
668            # translated name:
669            if self.gramplet.help_url:
670                if self.gramplet.help_url.startswith("http://"):
671                    display_url(self.gramplet.help_url)
672                else:
673                    display_help(self.gramplet.help_url)
674            else:
675                display_help(WIKI_HELP_PAGE,
676                                   self.gramplet.tname.replace(" ", "_"))
677
678    def get_notebook(self):
679        """
680        Return the notebook.
681        """
682        return self.notebook
683
684    def build_menu_names(self, obj):
685        """
686        Part of the Gramps window interface.
687        """
688        return (self.title, 'Gramplet')
689
690    def get_title(self):
691        """
692        Returns the window title.
693        """
694        return self.title
695
696    def close(self, *args):
697        """
698        Dock the detached gramplet back in the GrampletBar from where it came.
699        """
700        size = self.window.get_size()
701        self.gramplet.detached_width = size[0]
702        self.gramplet.detached_height = size[1]
703        self.gramplet.detached_window = None
704        self.gramplet.reparent(self.grampletbar)
705        ManagedWindow.close(self, *args)
706
707#-------------------------------------------------------------------------
708#
709# TabLabel class
710#
711#-------------------------------------------------------------------------
712class TabLabel(Gtk.Box):
713    """
714    Create a tab label consisting of a label and a close button.
715    """
716    def __init__(self, gramplet, callback):
717        Gtk.Box.__init__(self)
718
719        self.text = gramplet.title
720        self.set_spacing(4)
721
722        self.label = Gtk.Label()
723        self.label.set_tooltip_text(gramplet.tname)
724        self.label.show()
725
726        self.closebtn = Gtk.Button()
727        image = Gtk.Image()
728        image.set_from_icon_name('window-close', Gtk.IconSize.MENU)
729        self.closebtn.connect("clicked", callback, gramplet)
730        self.closebtn.set_image(image)
731        self.closebtn.set_relief(Gtk.ReliefStyle.NONE)
732
733        self.pack_start(self.label, True, True, 0)
734        self.pack_end(self.closebtn, False, False, 0)
735
736    def set_has_data(self, has_data):
737        """
738        Set the label to indicate if the gramplet has data.
739        """
740        if has_data:
741            self.label.set_text("<b>%s</b>" % self.text)
742            self.label.set_use_markup(True)
743        else:
744            self.label.set_text(self.text)
745
746    def use_close(self, use_close):
747        """
748        Display the cose button according to user preference.
749        """
750        if use_close:
751            self.closebtn.show()
752        else:
753            self.closebtn.hide()
754
755def cb_menu_position(*args):
756    """
757    Determine the position of the popup menu.
758    """
759    # takes two argument: menu, button
760    if len(args) == 2:
761        menu = args[0]
762        button = args[1]
763    # broken introspection can't handle MenuPositionFunc annotations corectly
764    else:
765        menu = args[0]
766        button = args[3]
767    ret_val, x_pos, y_pos = button.get_window().get_origin()
768    x_pos += button.get_allocation().x
769    y_pos += button.get_allocation().y + button.get_allocation().height
770
771    return (x_pos, y_pos, False)
772