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