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

Source Code for Module screenlets.options.base

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