From 9692af5e113fd21a95af3237b5a3d729940ba154 Mon Sep 17 00:00:00 2001 From: Doug Blank Date: Wed, 26 Dec 2007 08:26:33 +0000 Subject: [PATCH] Substantial improvements to gadgets svn: r9587 --- src/DataViews/_MyGrampsView.py | 385 +++++++++++++++++++++++++++------ src/plugins/DefaultGadgets.py | 5 +- 2 files changed, 317 insertions(+), 73 deletions(-) diff --git a/src/DataViews/_MyGrampsView.py b/src/DataViews/_MyGrampsView.py index d93991886..90f44346a 100644 --- a/src/DataViews/_MyGrampsView.py +++ b/src/DataViews/_MyGrampsView.py @@ -32,19 +32,27 @@ import gobject import traceback import time import pango +import os import Errors import const import PageView import ManagedWindow +import ConfigParser +import Utils AVAILABLE_GADGETS = [] +GADGET_FILENAME = os.path.join(const.HOME_DIR,"gadgets.ini") +NL = "\n" debug = False def register_gadget(data_dict): global AVAILABLE_GADGETS - AVAILABLE_GADGETS.append(data_dict) + base_opts = {"state":"maximized", + "column": -1, "row": -1} + base_opts.update(data_dict) + AVAILABLE_GADGETS.append(base_opts) def register(**data): if "type" in data: @@ -59,6 +67,14 @@ def get_gadget_opts(name, opts): return my_data return {} +def get_gadget_options_by_name(name): + if debug: print "name:", name + for opts in AVAILABLE_GADGETS: + gname = opts.get("name", None) + if gname == name: + return opts.copy() + return None + def make_requested_gadget(viewpage, name, opts, dbstate, uistate): for data in AVAILABLE_GADGETS: if data.get("name", None) == name: @@ -78,11 +94,6 @@ class LinkTag(gtk.TextTag): self.set_property('underline', pango.UNDERLINE_SINGLE) tag_table.add(self) -# notes on building a filter: src/Filters/_FilterParser.py -# f = GenericFilterFactory('Person')() -# r = Rules.Person.HasNameOf -# f.add_rule(r) - class GadgetWindow(ManagedWindow.ManagedWindow): def __init__(self, gadget): self.title = gadget.title + " Gadget" @@ -94,9 +105,8 @@ class GadgetWindow(ManagedWindow.ManagedWindow): None, self.title) self.window.set_size_request(400,300) self.window.connect('response', self.close) - self.gadget.state = "windowed" self.gadget.mainframe.reparent(self.window.vbox) - self.window.show_all() + self.window.show() def build_menu_names(self, obj): return (self.title, 'Gadget') @@ -105,9 +115,20 @@ class GadgetWindow(ManagedWindow.ManagedWindow): return self.title def close(self, *args): + """ + Closes the detached GadgetWindow. + """ + self.gadget.gvclose.show() + self.gadget.gvstate.show() + self.gadget.gvproperties.show() + self.gadget.viewpage.detached_frames.remove(self.gadget) self.gadget.state = "maximized" self.gadget.mainframe.reparent(self.gadget.parent) + # FIXME: need to pack as it was, not just stick it in ManagedWindow.ManagedWindow.close(self, *args) + # The save can stay here because GRAMPS doesn't run this method + # on exit, apparently: + self.gadget.viewpage.save() #------------------------------------------------------------------------ @@ -133,20 +154,6 @@ class Gadget(object): def active_changed(self, handle): pass - def on_save(self): - pass - - def _on_save(self): - # save data - pass - - def on_load(self): - pass - - def _on_load(self): - # load data - pass - def _db_changed(self, db): if debug: print "%s is _connecting" % self.gui.title self.dbstate.db = db @@ -176,6 +183,9 @@ class Gadget(object): priority=gobject.PRIORITY_LOW) def background(self): # return false finishes + """ + Generator which will be run in the background. + """ if debug: print "%s dummy" % self.gui.title yield False @@ -189,6 +199,9 @@ class Gadget(object): self._idle_id = 0 def _updater(self): + """ + Runs the generator. + """ if debug: print "%s _updater" % self.gui.title try: retval = self._generator.next() @@ -241,6 +254,19 @@ class Gadget(object): except Errors.WindowActiveError: pass else: + # Create filters on the fly: + #f = GenericFilterFactory('Person')() + #r = Rules.Person.HasNameOf + #rule = r([person.get_primary_name.get_surname()]) + #f.add_rule(rule) + #filter_info = (False, f) + #model = self.make_model(self.dbstate.db, 0, + # search=filter_info) + #self.list.set_model(self.model) + #self.dirty = False + #self.uistate.show_filter_results(self.dbstate, + # self.model.displayed, + # self.model.total) self.gui.dbstate.change_active_person(person) return True # handled event return False # did not handle event @@ -264,6 +290,8 @@ class Gadget(object): def set_text(self, text): self.gui.buffer.set_text(text) +def logical_true(value): + return value in ["True", True, 1, "1"] class GuiGadget: """ @@ -278,10 +306,11 @@ class GuiGadget: self.uistate = uistate self.title = title ########## Set defaults - self.expand = kwargs.get("expand", False) - self.height = kwargs.get("height", 200) - self.column = kwargs.get("column", -1) - self.row = kwargs.get("row", -1) + self.name = kwargs.get("name", "Unnamed Gadget") + self.expand = logical_true(kwargs.get("expand", False)) + self.height = int(kwargs.get("height", 200)) + self.column = int(kwargs.get("column", -1)) + self.row = int(kwargs.get("row", -1)) self.state = kwargs.get("state", "maximized") ########## self.xml = gtk.glade.XML(const.GLADE_FILE, 'gvgadget', "gramps") @@ -292,10 +321,12 @@ class GuiGadget: self.titlelabel = self.xml.get_widget('gvtitle') self.titlelabel.set_text("%s" % self.title) self.titlelabel.set_use_markup(True) - self.xml.get_widget('gvclose').connect('clicked', self.close) - self.xml.get_widget('gvstate').connect('clicked', self.change_state) - self.xml.get_widget('gvproperties').connect('clicked', - self.set_properties) + self.gvclose = self.xml.get_widget('gvclose') + self.gvclose.connect('clicked', self.close) + self.gvstate = self.xml.get_widget('gvstate') + self.gvstate.connect('clicked', self.change_state) + self.gvproperties = self.xml.get_widget('gvproperties') + self.gvproperties.connect('clicked', self.set_properties) self.xml.get_widget('gvcloseimage').set_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_MENU) self.xml.get_widget('gvstateimage').set_from_stock(gtk.STOCK_REMOVE, @@ -304,27 +335,32 @@ class GuiGadget: gtk.ICON_SIZE_MENU) # source: - drag = self.xml.get_widget('gvproperties') + drag = self.gvproperties drag.drag_source_set(gtk.gdk.BUTTON1_MASK, [GuiGadget.LOCAL_DRAG_TARGET], gtk.gdk.ACTION_COPY) def close(self, obj): + if self.state == "windowed": + return del self.viewpage.gadget_map[self.title] del self.viewpage.frame_map[str(self.mainframe)] self.mainframe.destroy() + self.viewpage.save() - def detach(self): + def detach(self, save=True): + # hide buttons: + self.gvclose.hide() + self.gvstate.hide() + self.gvproperties.hide() # keep a pointer to old parent frame: self.parent = self.mainframe.get_parent() + self.viewpage.detached_frames.append(self) # make a window, and attach it there self.detached_window = GadgetWindow(self) - - def attach(self): - # do this if you close window - # move this widget to old parent frame - # remove window - pass + self.state = "windowed" + if save: + self.viewpage.save() def set_state(self, state): self.state = state @@ -343,6 +379,7 @@ class GuiGadget: column = self.mainframe.get_parent() # column expand,fill,padding,pack = column.query_child_packing(self.mainframe) column.set_child_packing(self.mainframe,self.expand,fill,padding,pack) + self.viewpage.save() def change_state(self, obj): if self.state == "windowed": @@ -359,7 +396,7 @@ class GuiGadget: else: self.detach() return - # FIXME: how to control expand/not expand and detached + # FIXME: add control for expand AND detach self.expand = not self.expand if self.state == "maximized": column = self.mainframe.get_parent() # column @@ -375,6 +412,26 @@ class GuiGadget: def set_text(self, text): self.buffer.set_text(text) + def get_source_widget(self): + """ + Hack to allow us to send this object to the drop_widget + method as a context. + """ + return self.gvproperties + + +class MyScrolledWindow(gtk.ScrolledWindow): + def show_all(self): + # first show them all: + gtk.ScrolledWindow.show_all(self) + # Hack to get around show_all that shows hidden items + # do once, the first time showing + if self.viewpage: + gadgets = [g for g in self.viewpage.gadget_map.values()] + self.viewpage = None + for gadget in gadgets: + if gadget.state == "minimized": + gadget.set_state("minimized") class MyGrampsView(PageView.PageView): """ @@ -386,7 +443,7 @@ class MyGrampsView(PageView.PageView): Creates a MyGrampsView, with the current dbstate and uistate """ PageView.PageView.__init__(self, _('My Gramps'), dbstate, uistate) - self.column_count = 3 + self._popup_xy = None def change_db(self, event): """ @@ -399,9 +456,13 @@ class MyGrampsView(PageView.PageView): Builds the container widget for the interface. Must be overridden by the the base class. Returns a gtk container widget. """ - frame = gtk.ScrolledWindow() + # load the user's gadgets and set columns, etc + user_gadgets = self.load_gadgets() + # build the GUI: + frame = MyScrolledWindow() + frame.viewpage = self frame.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) - hbox = gtk.HBox(homogeneous=True) + self.hbox = gtk.HBox(homogeneous=True) # FIXME: issue when window is scrolled down; drops in wrong place # Set up drag and drop frame.drag_dest_set(gtk.DEST_DEFAULT_MOTION | @@ -410,32 +471,21 @@ class MyGrampsView(PageView.PageView): [('GADGET', 0, 80)], gtk.gdk.ACTION_COPY) frame.connect('drag_drop', self.drop_widget) + frame.connect('button-press-event', self._button_press) - frame.add_with_viewport(hbox) + frame.add_with_viewport(self.hbox) # Create the columns: self.columns = [] for i in range(self.column_count): self.columns.append(gtk.VBox()) - hbox.pack_start(self.columns[-1],expand=True) + self.hbox.pack_start(self.columns[-1],expand=True) # Load the gadgets self.gadget_map = {} # title->gadget self.frame_map = {} # frame->gadget - # FIXME - # get the user's gadgets from .gramps - # and/or from open database + self.detached_frames = [] # list of detached mainframes + # get the user's gadgets from ~/.gramps/gadgets.ini # Load the user's gadgets: - for (name, opts) in [ - ('Stats Gadget', {}), - ('Top Surnames Gadget', {}), - #('Families Gadget', {}), - #('Families Gadget', {"title": "My Peeps"}), - ('Hello World Gadget', {}), - ('Shell Gadget', {}), - ('Python Gadget', {}), - ('TODO Gadget', {}), - ('Log Gadget', {}), - #('Events Gadget', {}), - ]: + for (name, opts) in user_gadgets: all_opts = get_gadget_opts(name, opts) if "title" not in all_opts: all_opts["title"] = "Untitled Gadget" @@ -456,31 +506,119 @@ class MyGrampsView(PageView.PageView): print "Can't make gadget of type '%s'." % name else: print "Ignoring duplicate named gadget '%s'." % all_opts["title"] + self.place_gadgets() + return frame + + def clear_gadgets(self): + """ + Detach all of the mainframe gadgets from the columns. + """ + gadgets = [g for g in self.gadget_map.values()] + for gadget in gadgets: + column = gadget.mainframe.get_parent() + column.remove(gadget.mainframe) + + def place_gadgets(self): + """ + Place the gadget mainframes in the columns. + """ + gadgets = [g for g in self.gadget_map.values()] # put the gadgets where they go: + # sort by row + gadgets.sort(lambda a, b: cmp(a.row, b.row)) cnt = 0 - for gadget in self.gadget_map.values(): + for gadget in gadgets: # see if the user wants this in a particular location: # and if there are that many columns - if gadget.column >= 0 and gadget.column < len(self.columns): + if gadget.column >= 0 and gadget.column < self.column_count: pos = gadget.column else: # else, spread them out: - pos = cnt % len(self.columns) - # to make as big as possible, set to True: - # GTK BUG: can't minimize: - #if gadget.state == "minimized": # starts max, change to min it - # self.columns[pos].pack_start(gadget.mainframe, expand=False) - #else: + pos = cnt % self.column_count self.columns[pos].pack_start(gadget.mainframe, expand=gadget.expand) + gadget.column = pos # set height on gadget.scrolledwindow here: gadget.scrolledwindow.set_size_request(-1, gadget.height) - # GTK BUG: can't minimize: + # Can't minimize here, because GRAMPS calls show_all later: #if gadget.state == "minimized": # starts max, change to min it - #gadget.set_state("minimized") # minimize it + # gadget.set_state("minimized") # minimize it + if gadget.state == "windowed": + gadget.detach(save=False) # don't save on this change cnt += 1 - return frame + + def load_gadgets(self): + self.column_count = 3 + retval = [] + filename = GADGET_FILENAME + if filename and os.path.exists(filename): + cp = ConfigParser.ConfigParser() + cp.read(filename) + for sec in cp.sections(): + if sec == "My Gramps View Options": + if "column_count" in cp.options(sec): + self.column_count = int(cp.get(sec, "column_count")) + else: + data = {} + for opt in cp.options(sec): + data[opt] = cp.get(sec, opt).strip() + if "name" not in data: + data["name"] = "Unnamed Gadget" + retval.append((data["name"], data)) # name, opts + else: + # give defaults as currently known + for item in AVAILABLE_GADGETS: + retval.append((item["name"], item)) + return retval + + def save(self, *args): + if debug: print "saving" + filename = GADGET_FILENAME + fp = open(filename, "w") + fp.write(";; Gramps gadgets file" + NL) + fp.write((";; Automatically created at %s" % time.strftime("%Y/%m/%d %H:%M:%S")) + NL + NL) + fp.write("[My Gramps View Options]" + NL) + fp.write(("column_count=%d" + NL + NL) % self.column_count) + for col in range(self.column_count): + row = 0 + for gframe in self.columns[col]: + gadget = self.frame_map[str(gframe)] + opts = get_gadget_options_by_name(gadget.name) + if opts != None: + base_opts = opts.copy() + for key in base_opts: + if key in gadget.__dict__: + base_opts[key] = gadget.__dict__[key] + fp.write(("[%s]" + NL) % gadget.title) + for key in base_opts: + if key == "content": continue + if key == "title": continue + if key == "column": continue + if key == "row": continue + fp.write(("%s=%s" + NL)% (key, base_opts[key])) + fp.write(("column=%d" + NL) % col) + fp.write(("row=%d" + NL) % row) + fp.write(NL) + row += 1 + for gadget in self.detached_frames: + opts = get_gadget_options_by_name(gadget.name) + if opts != None: + base_opts = opts.copy() + for key in base_opts: + if key in gadget.__dict__: + base_opts[key] = gadget.__dict__[key] + fp.write(("[%s]" + NL) % gadget.title) + for key in base_opts: + if key == "content": continue + if key == "title": continue + fp.write(("%s=%s" + NL)% (key, base_opts[key])) + fp.write(NL) + fp.close() def drop_widget(self, source, context, x, y, timedata): + """ + This is the destination method for handling drag and drop + of a gadget onto the main scrolled window. + """ button = context.get_source_widget() hbox = button.get_parent() mainframe = hbox.get_parent() @@ -502,6 +640,7 @@ class MyGrampsView(PageView.PageView): self.columns[col].remove(gframe) stack.append(gframe) maingadget = self.frame_map[str(mainframe)] + maingadget.column = col if maingadget.state == "maximized": expand = maingadget.expand else: @@ -514,6 +653,7 @@ class MyGrampsView(PageView.PageView): else: expand = False self.columns[col].pack_start(gframe, expand=expand) + self.save() return True def define_actions(self): @@ -522,7 +662,77 @@ class MyGrampsView(PageView.PageView): View. The user typically defines self.action_list and self.action_toggle_list in this function. """ - return '' + self.action = gtk.ActionGroup(self.title + "/Gadgets") + self.action.add_actions([('AddGadget',gtk.STOCK_ADD,_("_Add a gadget")), + ('Columns1',None,_("Set columns to 1"), + None,None, + lambda obj:self.set_columns(1)), + ('Columns2',None,_("Set columns to 2"), + None,None, + lambda obj:self.set_columns(2)), + ('Columns3',None,_("Set columns to 3"), + None,None, + lambda obj:self.set_columns(3)), + ]) + self._add_action_group(self.action) + + def set_columns(self, num): + # clear the gadgets: + self.clear_gadgets() + # clear the columns: + for column in self.columns: + frame = column.get_parent() + frame.remove(column) + del column + # create the new ones: + self.column_count = num + self.columns = [] + for i in range(self.column_count): + self.columns.append(gtk.VBox()) + self.columns[-1].show() + self.hbox.pack_start(self.columns[-1],expand=True) + # place the gadgets back in the new columns + self.place_gadgets() + self.widget.show() + self.save() + + def add_gadget(self, obj): + name = obj.get_child().get_label() + all_opts = get_gadget_options_by_name(name) + if "title" not in all_opts: + all_opts["title"] = "Untitled Gadget" + # uniqify titles: + unique = all_opts["title"] + cnt = 1 + while unique in self.gadget_map: + unique = all_opts["title"] + ("-%d" % cnt) + cnt += 1 + all_opts["title"] = unique + if all_opts["title"] not in self.gadget_map: + g = make_requested_gadget(self, name, all_opts, + self.dbstate, self.uistate) + if g: + self.gadget_map[all_opts["title"]] = g + self.frame_map[str(g.mainframe)] = g + else: + print "Can't make gadget of type '%s'." % name + else: + print "Ignoring duplicate named gadget '%s'." % all_opts["title"] + + if g: + gadget = g + if gadget.column >= 0 and gadget.column < len(self.columns): + pos = gadget.column + else: + pos = 0 + self.columns[pos].pack_start(gadget.mainframe, expand=gadget.expand) + # set height on gadget.scrolledwindow here: + gadget.scrolledwindow.set_size_request(-1, gadget.height) + ## now drop it in right place + if self._popup_xy != None: + self.drop_widget(self.widget, gadget, + self._popup_xy[0], self._popup_xy[1], 0) + self.save() def get_stock(self): """ @@ -534,3 +744,36 @@ class MyGrampsView(PageView.PageView): def build_tree(self): return + def ui_definition(self): + return """ + + + + + + + + + + """ + + def _button_press(self, obj, event): + if event.type == gtk.gdk.BUTTON_PRESS and event.button == 3: + self._popup_xy = (event.x, event.y) + menu = self.uistate.uimanager.get_widget('/Popup') + qr_menu = self.uistate.uimanager.get_widget('/Popup/AddGadget').get_submenu() + if qr_menu == None: + qr_menu = gtk.Menu() + names = [] + for opts in AVAILABLE_GADGETS: + names.append(opts["name"]) + names.sort() + for name in names: + Utils.add_menuitem(qr_menu, name, + None, self.add_gadget) + self.uistate.uimanager.get_widget('/Popup/AddGadget').set_submenu(qr_menu) + if menu: + menu.popup(None, None, None, event.button, event.time) + return True + return False + diff --git a/src/plugins/DefaultGadgets.py b/src/plugins/DefaultGadgets.py index 6fb80eb3b..2ff9b9d38 100644 --- a/src/plugins/DefaultGadgets.py +++ b/src/plugins/DefaultGadgets.py @@ -124,8 +124,9 @@ class TopSurnamesGadget(Gadget): ### All done! self.set_text("") for (count, surname) in surname_sort: - self.append_text(" %s, %d%%\n" % - (surname, int((float(count)/total) * 100))) + self.append_text(" %d. %s, %d%% (%d)\n" % + (line + 1, surname, + int((float(count)/total) * 100), count)) line += 1 if line >= 10: break