# # Gramps - a GTK+/GNOME based genealogy program # # Copyright (C) 2000-2007 Donald N. Allingham # Copyright (C) 2011 Nick Hall # Copyright (C) 2011 Gary Burton # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # $Id$ """ Module that implements the sidebar and bottombar fuctionality. """ #------------------------------------------------------------------------- # # Python modules # #------------------------------------------------------------------------- from gen.ggettext import gettext as _ import time import os #------------------------------------------------------------------------- # # GNOME modules # #------------------------------------------------------------------------- import gtk gtk.rc_parse_string(""" style "tab-button-style" { GtkWidget::focus-padding = 0 GtkWidget::focus-line-width = 0 xthickness = 0 ythickness = 0 } widget "*.tab-button" style "tab-button-style" """) #------------------------------------------------------------------------- # # Gramps modules # #------------------------------------------------------------------------- import ConfigParser import const import ManagedWindow import GrampsDisplay from gui.widgets.grampletpane import (AVAILABLE_GRAMPLETS, GET_AVAILABLE_GRAMPLETS, get_gramplet_opts, get_gramplet_options_by_name, make_requested_gramplet, GuiGramplet) from gui.widgets.undoablebuffer import UndoableBuffer from ListModel import ListModel, NOSORT #------------------------------------------------------------------------- # # Constants # #------------------------------------------------------------------------- WIKI_HELP_PAGE = const.URL_MANUAL_PAGE + '_-_Gramplets' NL = "\n" #------------------------------------------------------------------------- # # GrampsBar class # #------------------------------------------------------------------------- class GrampsBar(gtk.Notebook): """ A class which defines the graphical representation of the GrampsBar. """ def __init__(self, dbstate, uistate, pageview, configfile, defaults): gtk.Notebook.__init__(self) self.dbstate = dbstate self.uistate = uistate self.pageview = pageview self.configfile = os.path.join(const.VERSION_DIR, "%s.ini" % configfile) self.detached_gramplets = [] self.empty = False self.set_group_id(1) self.set_show_border(False) self.set_scrollable(True) self.connect('switch-page', self.__switch_page) self.connect('page-added', self.__page_added) self.connect('page-removed', self.__page_removed) self.connect('create-window', self.__create_window) self.connect('button-press-event', self.__button_press) config_settings, opts_list = self.__load(defaults) opts_list.sort(key=lambda opt: opt["page"]) for opts in opts_list: all_opts = get_gramplet_opts(opts["name"], opts) gramplet = make_requested_gramplet(TabGramplet, self, all_opts, self.dbstate, self.uistate) self.__add_tab(gramplet) if len(opts_list) == 0: self.empty = True self.__create_empty_tab() if config_settings[0]: self.show() self.set_current_page(config_settings[1]) def __load(self, defaults): """ Load the gramplets from the configuration file. """ retval = [] visible = False default_page = 0 filename = self.configfile if filename and os.path.exists(filename): cp = ConfigParser.ConfigParser() cp.read(filename) for sec in cp.sections(): if sec == "Bar Options": if "visible" in cp.options(sec): visible = cp.get(sec, "visible") == "True" if "page" in cp.options(sec): default_page = int(cp.get(sec, "page")) else: data = {"title": sec} for opt in cp.options(sec): if opt.startswith("data["): temp = data.get("data", {}) #temp.append(cp.get(sec, opt).strip()) pos = int(opt[5:-1]) temp[pos] = cp.get(sec, opt).strip() data["data"] = temp else: data[opt] = cp.get(sec, opt).strip() if "data" in data: data["data"] = [data["data"][key] for key in sorted(data["data"].keys())] if "name" not in data: data["name"] = "Unnamed Gramplet" data["tname"] = _("Unnamed Gramplet") retval.append(data) else: # give defaults as currently known for name in defaults: if name in AVAILABLE_GRAMPLETS(): retval.append(GET_AVAILABLE_GRAMPLETS(name)) return ((visible, default_page), retval) def __save(self): """ Save the gramplet configuration. """ filename = self.configfile try: fp = open(filename, "w") except IOError: print "Failed writing '%s'; gramplets not saved" % filename return fp.write(";; Gramps bar configuration file" + NL) fp.write((";; Automatically created at %s" % time.strftime("%Y/%m/%d %H:%M:%S")) + NL + NL) fp.write("[Bar Options]" + NL) fp.write(("visible=%s" + NL) % self.get_property('visible')) fp.write(("page=%d" + NL) % self.get_current_page()) fp.write(NL) if self.empty: gramplet_list = [] else: gramplet_list = [self.get_nth_page(page_num) for page_num in range(self.get_n_pages())] for page_num, gramplet in enumerate(gramplet_list): opts = get_gramplet_options_by_name(gramplet.gname) if opts is not None: base_opts = opts.copy() for key in base_opts: if key in gramplet.__dict__: base_opts[key] = gramplet.__dict__[key] fp.write(("[%s]" + NL) % gramplet.title) for key in base_opts: if key in ["content", "title", "row", "column", "page", "version", "gramps"]: # don't save continue elif key == "data": if not isinstance(base_opts["data"], (list, tuple)): fp.write(("data[0]=%s" + NL) % base_opts["data"]) else: cnt = 0 for item in base_opts["data"]: fp.write(("data[%d]=%s" + NL) % (cnt, item)) cnt += 1 else: fp.write(("%s=%s" + NL)% (key, base_opts[key])) fp.write(("page=%d" + NL) % page_num) fp.write(NL) fp.close() def set_active(self): """ Called with the view is set as active. """ if not self.empty: gramplet = self.get_nth_page(self.get_current_page()) if gramplet and gramplet.pui: gramplet.pui.active = True if gramplet.pui.dirty: gramplet.pui.update() def set_inactive(self): """ Called with the view is set as inactive. """ if not self.empty: gramplet = self.get_nth_page(self.get_current_page()) if gramplet and gramplet.pui: gramplet.pui.active = False def on_delete(self): """ Called when the view is closed. """ map(self.__dock_gramplet, self.detached_gramplets) if not self.empty: for page_num in range(self.get_n_pages()): gramplet = self.get_nth_page(page_num) # this is the only place where the gui runs user code directly if gramplet.pui: gramplet.pui.on_save() self.__save() def add_gramplet(self, gname): """ Add a gramplet by name. """ if self.has_gramplet(gname): return all_opts = get_gramplet_options_by_name(gname) gramplet = make_requested_gramplet(TabGramplet, self, all_opts, self.dbstate, self.uistate) if not gramplet: print "Problem creating ", gname return page_num = self.__add_tab(gramplet) self.set_current_page(page_num) def remove_gramplet(self, gname): """ Remove a gramplet by name. """ for gramplet in self.detached_gramplets: if gramplet.gname == gname: self.__dock_gramplet(gramplet) self.remove_page(self.page_num(gramplet)) return for page_num in range(self.get_n_pages()): gramplet = self.get_nth_page(page_num) if gramplet.gname == gname: self.remove_page(page_num) return def has_gramplet(self, gname): """ Return True if the GrampsBar contains the gramplet, else False. """ return gname in self.all_gramplets() def all_gramplets(self): """ Return a list of names of all the gramplets in the GrampsBar. """ if self.empty: return self.detached_gramplets else: return [gramplet.gname for gramplet in self.get_children() + self.detached_gramplets] def __create_empty_tab(self): """ Create an empty tab to be displayed when the GrampsBar is empty. """ tab_label = gtk.Label(_('Gramps Bar')) tab_label.show() msg = _('Right-click to the right of the tab to add a gramplet.') content = gtk.Label(msg) content.show() self.append_page(content, tab_label) return content def __add_clicked(self): """ Called when the add button is clicked. """ skip = self.all_gramplets() names = [name for name in AVAILABLE_GRAMPLETS() if name not in skip] gramplet_list = [(GET_AVAILABLE_GRAMPLETS(name)["tname"], name) for name in names] gramplet_list.sort() dialog = ChooseGrampletDialog(_("Select Gramplet"), gramplet_list) name = dialog.run() if name: self.add_gramplet(name) def __add_tab(self, gramplet): """ Add a tab to the notebook for the given gramplet. """ gramplet.set_size_request(gramplet.width, gramplet.height) page_num = self.append_page(gramplet) return page_num def __create_tab_label(self, gramplet): """ Create a tab label consisting of a label and a close button. """ hbox = gtk.HBox(False, 4) label = gtk.Label(gramplet.title) label.set_tooltip_text(gramplet.tname) closebtn = gtk.Button() image = gtk.Image() image.set_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU) closebtn.connect("clicked", self.__delete_clicked, gramplet) closebtn.set_image(image) closebtn.set_relief(gtk.RELIEF_NONE) # The next three lines adjust the close button to the correct size. closebtn.set_name('tab-button') size = gtk.icon_size_lookup_for_settings(closebtn.get_settings(), gtk.ICON_SIZE_MENU) closebtn.set_size_request(size[0] + 2, size[1] + 2) hbox.pack_start(label, True, True) hbox.pack_end(closebtn, False, False) hbox.show_all() return hbox def __delete_clicked(self, button, gramplet): """ Called when the delete button is clicked. """ page_num = self.page_num(gramplet) self.remove_page(page_num) def __switch_page(self, notebook, unused, new_page): """ Called when the user has switched to a new GrampsBar page. """ old_page = notebook.get_current_page() if old_page >= 0: gramplet = self.get_nth_page(old_page) if gramplet and gramplet.pui: gramplet.pui.active = False gramplet = self.get_nth_page(new_page) if not self.empty: if gramplet and gramplet.pui: gramplet.pui.active = True if gramplet.pui.dirty: gramplet.pui.update() def __page_added(self, notebook, unused, new_page): """ Called when a new page is added to the GrampsBar. """ gramplet = self.get_nth_page(new_page) if self.empty: if isinstance(gramplet, TabGramplet): self.remove_page(0) self.empty = False else: return label = self.__create_tab_label(gramplet) self.set_tab_label(gramplet, label) self.set_tab_reorderable(gramplet, True) self.set_tab_detachable(gramplet, True) if gramplet in self.detached_gramplets: self.detached_gramplets.remove(gramplet) self.reorder_child(gramplet, gramplet.page) def __page_removed(self, notebook, unused, page_num): """ Called when a page is removed to the GrampsBar. """ if self.get_n_pages() == 0: self.empty = True self.__create_empty_tab() def __create_window(self, grampsbar, gramplet, x_pos, y_pos): """ Called when the user has switched to a new GrampsBar page. """ gramplet.page = self.page_num(gramplet) self.detached_gramplets.append(gramplet) win = DetachedWindow(grampsbar, gramplet, x_pos, y_pos) gramplet.detached_window = win return win.get_notebook() def __dock_gramplet(self, gramplet): """ Dock a detached gramplet. """ gramplet.detached_window.close() gramplet.detached_window = None def __button_press(self, widget, event): """ Called when a button is pressed in the tabs section of the GrampsBar. """ if event.type == gtk.gdk.BUTTON_PRESS and event.button == 3: # TODO: We will probably want a context menu here. self.__add_clicked() def get_config_funcs(self): """ Return a list of configuration functions. """ funcs = [] if self.empty: gramplets = [] else: gramplets = self.get_children() for gramplet in gramplets + self.detached_gramplets: gui_options = gramplet.make_gui_options() if gui_options: funcs.append(self.__build_panel(gramplet, gui_options)) return funcs def __build_panel(self, title, gui_options): """ Return a configuration function that returns the title of a page in the Configure View dialog and a gtk container defining the page. """ def gramplet_panel(configdialog): return title, gui_options return gramplet_panel #------------------------------------------------------------------------- # # TabGramplet class # #------------------------------------------------------------------------- class TabGramplet(gtk.ScrolledWindow, GuiGramplet): """ Class that handles the plugin interfaces for the GrampletBar. """ def __init__(self, pane, dbstate, uistate, title, **kwargs): """ Internal constructor for GUI portion of a gramplet. """ gtk.ScrolledWindow.__init__(self) GuiGramplet.__init__(self, pane, dbstate, uistate, title, **kwargs) self.scrolledwindow = self self.textview = gtk.TextView() self.buffer = UndoableBuffer() self.text_length = 0 self.textview.set_buffer(self.buffer) self.textview.connect("key-press-event", self.on_key_press_event) self.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) self.add(self.textview) self.show_all() self.track = [] def get_container_widget(self): """ Return the top level container widget. """ return self #------------------------------------------------------------------------- # # DetachedWindow class # #------------------------------------------------------------------------- class DetachedWindow(ManagedWindow.ManagedWindow): """ Class for showing a detached gramplet. """ def __init__(self, grampsbar, gramplet, x_pos, y_pos): """ Construct the window. """ self.title = gramplet.title + " " + _("Gramplet") self.grampsbar = grampsbar self.gramplet = gramplet ManagedWindow.ManagedWindow.__init__(self, gramplet.uistate, [], self.title) self.set_window(gtk.Dialog("", gramplet.uistate.window, gtk.DIALOG_DESTROY_WITH_PARENT, (gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE)), None, self.title) self.window.move(x_pos, y_pos) self.window.set_size_request(gramplet.detached_width, gramplet.detached_height) self.window.add_button(gtk.STOCK_HELP, gtk.RESPONSE_HELP) self.window.connect('response', self.handle_response) self.notebook = gtk.Notebook() self.notebook.set_show_tabs(False) self.notebook.set_show_border(False) self.notebook.show() self.window.vbox.add(self.notebook) self.show() def handle_response(self, object, response): """ Callback for taking care of button clicks. """ if response in [gtk.RESPONSE_CLOSE, gtk.STOCK_CLOSE]: self.close() elif response == gtk.RESPONSE_HELP: # translated name: if self.gramplet.help_url: if self.gramplet.help_url.startswith("http://"): GrampsDisplay.url(self.gramplet.help_url) else: GrampsDisplay.help(self.gramplet.help_url) else: GrampsDisplay.help(WIKI_HELP_PAGE, self.gramplet.tname.replace(" ", "_")) def get_notebook(self): """ Return the notebook. """ return self.notebook def build_menu_names(self, obj): """ Part of the GRAMPS window interface. """ return (self.title, 'Gramplet') def get_title(self): """ Returns the window title. """ return self.title def close(self, *args): """ Dock the detached gramplet back in the GrampsBar from where it came. """ size = self.window.get_size() self.gramplet.detached_width = size[0] self.gramplet.detached_height = size[1] self.gramplet.detached_window = None self.gramplet.reparent(self.grampsbar) ManagedWindow.ManagedWindow.close(self, *args) #------------------------------------------------------------------------- # # Choose Gramplet Dialog # #------------------------------------------------------------------------- class ChooseGrampletDialog(object): """ A dialog to choose a gramplet """ def __init__(self, title, names): self.title = title self.names = names self.namelist = None self.namemodel = None self.top = self._create_dialog() def run(self): """ Run the dialog and return the result. """ self._populate_model() response = self.top.run() result = None if response == gtk.RESPONSE_OK: store, iter_ = self.namemodel.get_selected() if iter_: result = store.get_value(iter_, 1) self.top.destroy() return result def _populate_model(self): """ Populate the model. """ self.namemodel.clear() for name in self.names: self.namemodel.add(name) def _create_dialog(self): """ Create a dialog box to organize tags. """ # pylint: disable-msg=E1101 title = _("%(title)s - Gramps") % {'title': self.title} top = gtk.Dialog(title) top.set_default_size(400, 350) top.set_modal(True) top.set_has_separator(False) top.vbox.set_spacing(5) label = gtk.Label('<span size="larger" weight="bold">%s</span>' % self.title) label.set_use_markup(True) top.vbox.pack_start(label, 0, 0, 5) box = gtk.HBox() top.vbox.pack_start(box, 1, 1, 5) name_titles = [(_('Name'), NOSORT, 200), ('', NOSORT, 200)] self.namelist = gtk.TreeView() self.namemodel = ListModel(self.namelist, name_titles) slist = gtk.ScrolledWindow() slist.add_with_viewport(self.namelist) slist.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) box.pack_start(slist, 1, 1, 5) bbox = gtk.VButtonBox() bbox.set_layout(gtk.BUTTONBOX_START) bbox.set_spacing(6) top.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK) top.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) box.pack_start(bbox, 0, 0, 5) top.show_all() return top