screenlets.options.base

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