Package screenlets :: Package options :: Module base
[hide private]
[frames] | no frames]

Source Code for Module screenlets.options.base

  1  # 
  2  # Copyright (C) 2011 Martin Owens (DoctorMO) <doctormo@gmail.com> 
  3  # 
  4  # This program is free software; you can redistribute it and/or modify 
  5  # it under the terms of the GNU General Public License as published by 
  6  # the Free Software Foundation; either version 3 of the License, or 
  7  # (at your option) any later version. 
  8  # 
  9  # This program is distributed in the hope that it will be useful, 
 10  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 11  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 12  # GNU General Public License for more details. 
 13  # 
 14  # You should have received a copy of the GNU General Public License 
 15  # along with this program; if not, write to the Free Software 
 16  # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 
 17  # 
 18  """ 
 19  Base classes and basic mechanics for all screenlet options. 
 20  """ 
 21   
 22  import screenlets 
 23  from screenlets.options import _ 
 24   
 25  import os 
 26  import gtk, gobject 
 27  import xml.dom.minidom 
 28  from xml.dom.minidom import Node 
 29   
 30  # ----------------------------------------------------------------------- 
 31  # Option-classes and subclasses 
 32  # ----------------------------------------------------------------------- 
 33   
 34  OPT_ATTRS = [ 'default', 'label', 'desc', 'choices' ] 
 35   
36 -class Option(gobject.GObject):
37 """An Option stores information about a certain object-attribute. It doesn't 38 carry information about the value or the object it belongs to - it is only a 39 one-way data-storage for describing how to handle attributes.""" 40 __gsignals__ = dict(option_changed=(gobject.SIGNAL_RUN_FIRST, 41 gobject.TYPE_NONE, (gobject.TYPE_OBJECT,))) 42 default = None 43 label = None 44 desc = None 45 hidden = False 46 disabled = False 47 realtime = True 48 protected = False 49 widget = None 50
51 - def __init__ (self, group, name, *attr, **args):
52 """Creates a new Option with the given information.""" 53 super(Option, self).__init__() 54 if name == None: 55 raise ValueError("Option widget %s must have name." % str(type(self)) ) 56 self.name = name 57 self.group = group 58 # To maintain compatability, we parse out the 3 attributes and 59 # Move into known arguments. 60 for i in range(len(attr)): 61 args.setdefault(OPT_ATTRS[i], attr[i]) 62 # This should allow any of the class options to be set on init. 63 for name in args.keys(): 64 if hasattr(self, name): 65 # Replaces class variables (defaults) with object vars 66 setattr(self, name, args[name])
67 68 # XXX for groups (TODO: OptionGroup) 69 # XXX callback to be notified when this option changes 70 # XXX real-time update? 71 # XXX protected from get/set through service 72
73 - def on_import(self, strvalue):
74 """Callback - called when an option gets imported from a string. 75 This function MUST return the string-value converted to the required 76 type!""" 77 return strvalue.replace("\\n", "\n")
78
79 - def on_export(self, value):
80 """Callback - called when an option gets exported to a string. The 81 value-argument needs to be converted to a string that can be imported 82 by the on_import-handler. This handler MUST return the value 83 converted to a string!""" 84 return str(value).replace("\n", "\\n")
85
86 - def generate_widget(self):
87 """This should generate all the required widgets for display.""" 88 raise NotImplementedError, "Generating Widget should be done in child"
89
90 - def set_value(self, value):
91 """Set the true/false value to the checkbox widget""" 92 raise NotImplementedError, "Can't update the widget and local value"
93
94 - def has_changed(self):
95 """Executed when the widget event kicks off.""" 96 return self.emit("option_changed", self)
97 98
99 -def create_option_from_node (node, groupname):
100 """Create an Option from an XML-node with option-metadata.""" 101 #print "TODO OPTION: " + str(cn) 102 otype = node.getAttribute("type") 103 oname = node.getAttribute("name") 104 ohidden = node.getAttribute("hidden") 105 options = { 106 'default' : None, 107 'info' : '', 108 'label' : '', 109 'min' : None, 110 'max' : None, 111 'increment' : 1, 112 'choices' : '', 113 'digits' : None, 114 } 115 if otype and oname: 116 # parse children of option-node and save all useful attributes 117 for attr in node.childNodes: 118 if attr.nodeType == Node.ELEMENT_NODE and attr.nodeName in options.keys(): 119 options[attr.nodeName] = attr.firstChild.nodeValue 120 # if we have all needed values, create the Option 121 if options['default']: 122 # create correct classname here 123 cls = otype[0].upper() + otype.lower()[1:] + 'Option' 124 #print 'Create: ' +cls +' / ' + oname + ' ('+otype+')' 125 # and build new instance (we use on_import for setting default val) 126 clsobj = getattr(__import__(__name__), cls) 127 opt = clsobj(groupname, oname, default=None, 128 label=options['label'], desc=options['info']) 129 opt.default = opt.on_import(options['default']) 130 # set values to the correct types 131 if cls == 'IntOption': 132 if options['min']: 133 opt.min = int(options['min']) 134 if options['max']: 135 opt.max = int(options['max']) 136 if options['increment']: 137 opt.increment = int(options['increment']) 138 elif cls == 'FloatOption': 139 if options['digits']: 140 opt.digits = int(options['digits']) 141 if options['min']: 142 opt.min = float(options['min']) 143 if options['min']: 144 opt.max = float(options['max']) 145 if options['increment']: 146 opt.increment = float(options['increment']) 147 elif cls == 'StringOption': 148 if options['choices']: 149 opt.choices = options['choices'] 150 return opt 151 return None
152 153
154 -class EditableOptions(object):
155 """The EditableOptions can be inherited from to allow objects to export 156 editable options for editing them with the OptionsEditor-class. 157 NOTE: This could use some improvement and is very poorly coded :) ...""" 158
159 - def __init__ (self):
160 self.__options__ = [] 161 self.__options_groups__ = {} 162 # This is a workaround to remember the order of groups 163 self.__options_groups_ordered__ = []
164
165 - def add_option (self, option, callback=None, realtime=True):
166 """Add an editable option to this object. Editable Options can be edited 167 and configured using the OptionsDialog. The optional callback-arg can be 168 used to set a callback that gets notified when the option changes its 169 value.""" 170 #print "Add option: "+option.name 171 # if option already editable (i.e. initialized), return 172 for o in self.__options__: 173 if o.name == option.name: 174 return False 175 self.__dict__[option.name] = option.default 176 # set auto-update (TEMPORARY?) 177 option.realtime = realtime 178 # add option to group (output error if group is undefined) 179 try: 180 self.__options_groups__[option.group]['options'].append(option) 181 except: 182 print "Options: Error - group %s not defined." % option.group 183 return False 184 # now add the option 185 self.__options__.append(option) 186 # if callback is set, add callback 187 if callback: 188 option.connect("option_changed", callback) 189 option.connect("option_changed", self.callback_value_changed) 190 return True
191 192
193 - def add_options_group (self, name, group_info):
194 """Add a new options-group to this Options-object""" 195 self.__options_groups__[name] = {'label':name, 196 'info':group_info, 'options':[]} 197 self.__options_groups_ordered__.append(name)
198 #print self.options_groups 199
200 - def disable_option(self, name):
201 """Disable the inputs for a certain Option.""" 202 for o in self.__options__: 203 if o.name == name: 204 o.disabled = True 205 return True 206 return False
207
208 - def enable_option(self, name):
209 """Enable the inputs for a certain Option.""" 210 for o in self.__options__: 211 if o.name == name: 212 o.disabled = False 213 return True 214 return False
215
216 - def export_options_as_list(self):
217 """Returns all editable options within a list (without groups) 218 as key/value tuples.""" 219 lst = [] 220 for o in self.__options__: 221 lst.append((o.name, o.on_export(getattr(self, o.name)))) 222 return lst
223
224 - def callback_value_changed(self, sender, option):
225 """Called when a widget has updated and this needs calling.""" 226 if hasattr(self, option.name): 227 return setattr(self, option.name, option.value) 228 raise KeyError, "Callback tried to set an option that wasn't defined."
229
230 - def get_option_by_name (self, name):
231 """Returns an option in this Options by it's name (or None). 232 TODO: this gives wrong results in childclasses ... maybe access 233 as class-attribute??""" 234 for o in self.__options__: 235 if o.name == name: 236 return o 237 return None
238
239 - def remove_option (self, name):
240 """Remove an option from this Options.""" 241 for o in self.__options__: 242 if o.name == name: 243 del o 244 return True 245 return True
246
247 - def add_options_from_file (self, filename):
248 """This function creates options from an XML-file with option-metadata. 249 TODO: make this more reusable and place it into module (once the groups 250 are own objects)""" 251 # create xml document 252 try: 253 doc = xml.dom.minidom.parse(filename) 254 except: 255 raise Exception('Invalid XML in metadata-file (or file missing): "%s".' % filename) 256 # get rootnode 257 root = doc.firstChild 258 if not root or root.nodeName != 'screenlet': 259 raise Exception('Missing or invalid rootnode in metadata-file: "%s".' % filename) 260 # ok, let's check the nodes: this one should contain option-groups 261 groups = [] 262 for node in root.childNodes: 263 # we only want element-nodes 264 if node.nodeType == Node.ELEMENT_NODE: 265 #print node 266 if node.nodeName != 'group' or not node.hasChildNodes(): 267 # we only allow groups in the first level (groups need children) 268 raise Exception('Error in metadata-file "%s" - only <group>-tags allowed in first level. Groups must contain at least one <info>-element.' % filename) 269 else: 270 # ok, create a new group and parse its elements 271 group = {} 272 group['name'] = node.getAttribute("name") 273 if not group['name']: 274 raise Exception('No name for group defined in "%s".' % filename) 275 group['info'] = '' 276 group['options'] = [] 277 # check all children in group 278 for on in node.childNodes: 279 if on.nodeType == Node.ELEMENT_NODE: 280 if on.nodeName == 'info': 281 # info-node? set group-info 282 group['info'] = on.firstChild.nodeValue 283 elif on.nodeName == 'option': 284 # option node? parse option node 285 opt = create_option_from_node (on, group['name']) 286 # ok? add it to list 287 if opt: 288 group['options'].append(opt) 289 else: 290 raise Exception('Invalid option-node found in "%s".' % filename) 291 292 # create new group 293 if len(group['options']): 294 self.add_options_group(group['name'], group['info']) 295 for o in group['options']: 296 self.add_option(o)
297 # add group to list 298 #groups.append(group) 299 300
301 -class OptionsDialog(gtk.Dialog):
302 """A dynamic options-editor for editing Screenlets which are implementing 303 the EditableOptions-class.""" 304 305 __shown_object = None 306
307 - def __init__ (self, width, height):
308 # call gtk.Dialog.__init__ 309 super(OptionsDialog, self).__init__( 310 _("Edit Options"), flags=gtk.DIALOG_DESTROY_WITH_PARENT | 311 gtk.DIALOG_NO_SEPARATOR, 312 buttons = (#gtk.STOCK_REVERT_TO_SAVED, gtk.RESPONSE_APPLY, 313 gtk.STOCK_CLOSE, gtk.RESPONSE_OK)) 314 # set size 315 self.resize(width, height) 316 self.set_keep_above(True) # to avoid confusion 317 self.set_border_width(10) 318 # create attribs 319 self.page_about = None 320 self.page_options = None 321 self.page_themes = None 322 self.vbox_editor = None 323 self.hbox_about = None 324 self.infotext = None 325 self.infoicon = None 326 # create theme-list 327 self.liststore = gtk.ListStore(object) 328 self.tree = gtk.TreeView(model=self.liststore) 329 # create/add outer notebook 330 self.main_notebook = gtk.Notebook() 331 self.main_notebook.show() 332 self.vbox.add(self.main_notebook) 333 # create/init notebook pages 334 self.create_about_page() 335 self.create_themes_page() 336 self.create_options_page()
337 338 # "public" functions 339
340 - def reset_to_defaults(self):
341 """Reset all entries for the currently shown object to their default 342 values (the values the object has when it is first created). 343 NOTE: This function resets ALL options, so BE CARFEUL!""" 344 if self.__shown_object: 345 for o in self.__shown_object.__options__: 346 # set default value 347 setattr(self.__shown_object, o.name, o.default)
348
349 - def set_info(self, name, info, copyright='', version='', icon=None):
350 """Update the "About"-page with the given information.""" 351 # convert infotext (remove EOLs and TABs) 352 info = info.replace("\n", "") 353 info = info.replace("\t", " ") 354 # create markup 355 markup = '\n<b><span size="xx-large">' + name + '</span></b>' 356 if version: 357 markup += ' <span size="large"><b>' + version + '</b></span>' 358 markup += '\n\n'+info+'\n<span size="small">\n'+copyright+'</span>' 359 self.infotext.set_markup(markup) 360 # icon? 361 if icon: 362 # remove old icon 363 if self.infoicon: 364 self.infoicon.destroy() 365 # set new icon 366 self.infoicon = icon 367 self.infoicon.set_alignment(0.0, 0.10) 368 self.infoicon.show() 369 self.hbox_about.pack_start(self.infoicon, 0, 1, 10) 370 else: 371 self.infoicon.hide()
372
373 - def show_options_for_object (self, obj):
374 """Update the OptionsEditor to show the options for the given Object. 375 The Object needs to be an EditableOptions-subclass. 376 NOTE: This needs heavy improvement and should use OptionGroups once 377 they exist""" 378 self.__shown_object = obj 379 # create notebook for groups 380 notebook = gtk.Notebook() 381 self.vbox_editor.add(notebook) 382 for group in obj.__options_groups_ordered__: 383 group_data = obj.__options_groups__[group] 384 # create box for tab-page 385 page = gtk.VBox() 386 page.set_border_width(10) 387 if group_data['info'] != '': 388 info = gtk.Label(group_data['info']) 389 info.show() 390 info.set_alignment(0, 0) 391 page.pack_start(info, 0, 0, 7) 392 sep = gtk.HSeparator() 393 sep.show() 394 #page.pack_start(sep, 0, 0, 5) 395 # create VBox for inputs 396 box = gtk.VBox() 397 box.show() 398 box.set_border_width(5) 399 # add box to page 400 page.add(box) 401 page.show() 402 # add new notebook-page 403 label = gtk.Label(group_data['label']) 404 label.show() 405 notebook.append_page(page, label) 406 # and create inputs 407 for option in group_data['options']: 408 if option.hidden == False: 409 name = getattr(obj, option.name) 410 if name == None: 411 raise ValueError("Option %s has no name" % str(type(obj))) 412 w = self.generate_widget( option, name ) 413 if w: 414 box.pack_start(w, 0, 0) 415 w.show() 416 notebook.show() 417 # show/hide themes tab, depending on whether the screenlet uses themes 418 if obj.uses_theme and obj.theme_name != '': 419 self.show_themes_for_screenlet(obj) 420 else: 421 self.page_themes.hide()
422
423 - def generate_widget(self, option, value):
424 """Generate the required widgets and add the label.""" 425 widget = option.generate_widget(value) 426 hbox = gtk.HBox() 427 label = gtk.Label() 428 label.set_alignment(0.0, 0.0) 429 label.set_label(option.label) 430 label.set_size_request(180, 28) 431 label.show() 432 hbox.pack_start(label, 0, 1) 433 if widget: 434 if option.disabled: 435 widget.set_sensitive(False) 436 label.set_sensitive(False) 437 widget.set_tooltip_text(option.desc) 438 widget.show() 439 # check if needs Apply-button 440 if option.realtime == False: 441 but = gtk.Button(_('Apply'), gtk.STOCK_APPLY) 442 but.show() 443 but.connect("clicked", option.has_changed) 444 b = gtk.HBox() 445 b.show() 446 b.pack_start(widget, 0, 0) 447 b.pack_start(but, 0, 0) 448 hbox.pack_start(b, 0, 0) 449 else: 450 hbox.pack_start(widget, 0, 0) 451 return hbox
452 453
454 - def show_themes_for_screenlet (self, obj):
455 """Update the Themes-page to display the available themes for the 456 given Screenlet-object.""" 457 dircontent = [] 458 screenlets.utils.refresh_available_screenlet_paths() 459 # now check all paths for themes 460 for path in screenlets.SCREENLETS_PATH: 461 p = path + '/' + obj.get_short_name() + '/themes' 462 print p 463 #p = '/usr/local/share/screenlets/Clock/themes' # TEMP!!! 464 try: 465 dc = os.listdir(p) 466 for d in dc: 467 dircontent.append({'name':d, 'path':p+'/'}) 468 except: 469 print "Path %s not found." % p 470 # list with found themes 471 found_themes = [] 472 # check all themes in path 473 for elem in dircontent: 474 # load themes with the same name only once 475 if found_themes.count(elem['name']): 476 continue 477 found_themes.append(elem['name']) 478 # build full path of theme.conf 479 theme_conf = elem['path'] + elem['name'] + '/theme.conf' 480 # if dir contains a theme.conf 481 if os.access(theme_conf, os.F_OK): 482 # load it and create new list entry 483 ini = screenlets.utils.IniReader() 484 if ini.load(theme_conf): 485 # check for section 486 if ini.has_section('Theme'): 487 # get metainfo from theme 488 th_fullname = ini.get_option('name', 489 section='Theme') 490 th_info = ini.get_option('info', 491 section='Theme') 492 th_version = ini.get_option('version', 493 section='Theme') 494 th_author = ini.get_option('author', 495 section='Theme') 496 # create array from metainfo and add it to liststore 497 info = [elem['name'], th_fullname, th_info, th_author, 498 th_version] 499 self.liststore.append([info]) 500 else: 501 # no theme section in theme.conf just add theme-name 502 self.liststore.append([[elem['name'], '-', '-', '-', '-']]) 503 else: 504 # no theme.conf in dir? just add theme-name 505 self.liststore.append([[elem['name'], '-', '-', '-', '-']]) 506 # is it the active theme? 507 if elem['name'] == obj.theme_name: 508 # select it in tree 509 print "active theme is: %s" % elem['name'] 510 sel = self.tree.get_selection() 511 if sel: 512 it = self.liststore.get_iter_from_string(\ 513 str(len(self.liststore)-1)) 514 if it: 515 sel.select_iter(it)
516 # UI-creation 517
518 - def create_about_page (self):
519 """Create the "About"-tab.""" 520 self.page_about = gtk.HBox() 521 # create about box 522 self.hbox_about = gtk.HBox() 523 self.hbox_about.show() 524 self.page_about.add(self.hbox_about) 525 # create icon 526 self.infoicon = gtk.Image() 527 self.infoicon.show() 528 self.page_about.pack_start(self.infoicon, 0, 1, 10) 529 # create infotext 530 self.infotext = gtk.Label() 531 self.infotext.use_markup = True 532 self.infotext.set_line_wrap(True) 533 self.infotext.set_alignment(0.0, 0.0) 534 self.infotext.show() 535 self.page_about.pack_start(self.infotext, 1, 1, 5) 536 # add page 537 self.page_about.show() 538 self.main_notebook.append_page(self.page_about, gtk.Label(_('About ')))
539
540 - def create_options_page (self):
541 """Create the "Options"-tab.""" 542 self.page_options = gtk.HBox() 543 # create vbox for options-editor 544 self.vbox_editor = gtk.VBox(spacing=3) 545 self.vbox_editor.set_border_width(5) 546 self.vbox_editor.show() 547 self.page_options.add(self.vbox_editor) 548 # show/add page 549 self.page_options.show() 550 self.main_notebook.append_page(self.page_options, gtk.Label(_('Options ')))
551
552 - def create_themes_page (self):
553 """Create the "Themes"-tab.""" 554 self.page_themes = gtk.VBox(spacing=5) 555 self.page_themes.set_border_width(10) 556 # create info-text list 557 txt = gtk.Label(_('Themes allow you to easily switch the appearance of your Screenlets. On this page you find a list of all available themes for this Screenlet.')) 558 txt.set_size_request(450, -1) 559 txt.set_line_wrap(True) 560 txt.set_alignment(0.0, 0.0) 561 txt.show() 562 self.page_themes.pack_start(txt, False, True) 563 # create theme-selector list 564 self.tree.set_headers_visible(False) 565 self.tree.connect('cursor-changed', self.__tree_cursor_changed) 566 self.tree.show() 567 col = gtk.TreeViewColumn('') 568 cell = gtk.CellRendererText() 569 col.pack_start(cell, True) 570 #cell.set_property('foreground', 'black') 571 col.set_cell_data_func(cell, self.__render_cell) 572 self.tree.append_column(col) 573 # wrap tree in scrollwin 574 sw = gtk.ScrolledWindow() 575 sw.set_shadow_type(gtk.SHADOW_IN) 576 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) 577 sw.add(self.tree) 578 sw.show() 579 # add vbox and add tree/buttons 580 vbox = gtk.VBox() 581 vbox.pack_start(sw, True, True) 582 vbox.show() 583 # show/add page 584 self.page_themes.add(vbox) 585 self.page_themes.show() 586 self.main_notebook.append_page(self.page_themes, gtk.Label(_('Themes ')))
587
588 - def __render_cell(self, tvcolumn, cell, model, iter):
589 """Callback for rendering the cells in the theme-treeview.""" 590 # get attributes-list from Treemodel 591 attrib = model.get_value(iter, 0) 592 593 # set colors depending on state 594 col = '555555' 595 name_uc = attrib[0][0].upper() + attrib[0][1:] 596 # create markup depending on info 597 if attrib[1] == '-' and attrib[2] == '-': 598 mu = '<b><span weight="ultrabold" size="large">' + name_uc + \ 599 '</span></b> (' + _('no info available') + ')' 600 else: 601 if attrib[1] == None : attrib[1] = '-' 602 if attrib[2] == None : attrib[2] = '-' 603 if attrib[3] == None : attrib[3] = '-' 604 if attrib[4] == None : attrib[4] = '-' 605 mu = '<b><span weight="ultrabold" size="large">' + name_uc + \ 606 '</span></b> v' + attrib[4] + '\n<small><span color="#555555' +\ 607 '">' + attrib[2].replace('\\n', '\n') + \ 608 '</span></small>\n<i><small>by '+str(attrib[3])+'</small></i>' 609 # set markup 610 cell.set_property('markup', mu)
611 612 # UI-callbacks 613
614 - def __tree_cursor_changed (self, treeview):
615 """Callback for handling selection changes in the Themes-treeview.""" 616 sel = self.tree.get_selection() 617 if sel: 618 s = sel.get_selected() 619 if s: 620 it = s[1] 621 if it: 622 attribs = self.liststore.get_value(it, 0) 623 if attribs and self.__shown_object: 624 #print attribs 625 # set theme in Screenlet (if not already active) 626 if self.__shown_object.theme_name != attribs[0]: 627 self.__shown_object.theme_name = attribs[0]
628