Package Camelot :: Package camelot :: Package view :: Module forms |
[frames] | no frames]

# Source Code for Module Camelot.camelot.view.forms

      1  #  ============================================================================
      2  #
      3  #  Copyright (C) 2007-2008 Conceptive Engineering bvba. All rights reserved.
      4  #  www.conceptive.be / project-camelot@conceptive.be
      5  #
      6  #  This file is part of the Camelot Library.
      7  #
      8  #  This file may be used under the terms of the GNU General Public
      9  #  License version 2.0 as published by the Free Software Foundation
     10  #  and appearing in the file LICENSE.GPL included in the packaging of
     11  #  this file.  Please review the following information to ensure GNU
     12  #  General Public Licensing requirements will be met:
     13  #  http://www.trolltech.com/products/qt/opensource.html
     14  #
     15  #  If you are unsure which license is appropriate for your use, please
     16  #  review the following information:
     17  #  http://www.trolltech.com/products/qt/licensing.html or contact
     18  #  project-camelot@conceptive.be.
     19  #
     20  #  This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
     21  #  WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
     22  #
     23  #  For use of this library in commercial applications, please contact
     24  #  project-camelot@conceptive.be
     25  #
     26  #  ============================================================================
     27
     28  """Classes to layout fields on a form.  These are mostly used for specifying the
     29  form_display attribute in Admin classes, but they can be used on their own as
     30  well.  Form classes can be used recursive.
     31  """
     32
     33  import logging
     34  logger = logging.getLogger( 'camelot.view.forms' )
     35
     36  from camelot.view.model_thread import gui_function
     37
     38 -class Form( object ):
     39      """Base Form class to put fields on a form.  A form can be converted to a
     40      QT widget by calling its render method.  The base form uses the QFormLayout
     41      to render a form::
     42
     43      class Admin(EntityAdmin):
     44        form_display = Form(['title', 'short_description', 'director', 'release_date'])
     45
     46      .. image:: ../_static/form/form.png
     47      """
     48
     49 -    def __init__( self, content, scrollbars = False, columns = 1  ):
     50          """
     51          :param content: a list with the field names and forms to render
     52          :param columns: the number of columns in which to order the fields.
     53
     54          eg : with 2 columns, the fields ['street', 'city', 'country'] will
     55          be ordered as :
     56
     57          +-------------+--------------+
     58          | street      | city         |
     59          +-------------+--------------+
     60          | country     |              |
     61          +-------------+--------------+
     62
     63          """
     64          assert isinstance( content, list )
     65          self._content = content
     66          self._scrollbars = scrollbars
     67          self._columns = columns
     68
     69 -    def get_fields( self ):
     70          """:return: the fields, visible in this form"""
     71          return [field for field in self._get_fields_from_form()]
     72
     73 -    def _get_fields_from_form( self ):
     74          for field in self._content:
     75              if isinstance( field, Form ):
     76                  for nested_field in  field._get_fields_from_form():
     77                      yield nested_field
     78              else:
     79                  assert isinstance( field, ( str, unicode ) )
     80                  yield field;
     81
     82
     83 -    def removeField( self, original_field ):
     84          """Remove a field from the form, This function can be used to modify
     85          inherited forms.
     86
     87          :param original_field: the name of the field to be removed
     88          :return: True if the field was found and removed
     89          """
     90          for c in self._content:
     91              if isinstance( c, Form ):
     92                  c.removeField( original_field )
     93              if original_field in self._content:
     94                  self._content.remove( original_field )
     95                  return True
     96              if original_field in self._fields:
     97                  self._fields.remove( original_field )
     98                  return True
     99          return False
    100
    101 -    def replaceField( self, original_field, new_field ):
    102          """Replace a field on this form with another field.  This function can be used to
    103          modify inherited forms.
    104
    105          :param original_field : the name of the field to be replace
    106          :param new_field : the name of the new field
    107          :return: True if the original field was found and replaced.
    108          """
    109          for i, c in enumerate( self._content ):
    110              if isinstance( c, Form ):
    111                  c.replaceField( original_field, new_field )
    112              elif c == original_field:
    113                  self._content[i] = new_field
    114                  return True
    115          return False
    116
    117 -    def add_field( self, new_field ):
    118          self._content.append( new_field )
    119
    120 -    def __unicode__( self ):
    121          return 'Form(%s)' % ( u','.join( unicode( c ) for c in self._content ) )
    122
    123      @gui_function
    124 -    def render( self, widgets, parent = None, nomargins = False):
    125          """:param widgets: a dictionary mapping each field in this form to a tuple
    126          of (label, widget editor)
    127
    128          :return: a QWidget into which the form is rendered
    129          """
    130          logger.debug( 'rendering %s' % self.__class__.__name__ )
    131          from camelot.view.controls.editors.wideeditor import WideEditor
    132
    133          from PyQt4 import QtGui
    134          form_layout = QtGui.QGridLayout()
    135
    136          # where 1 column in the form is a label and a field, so two columns in the grid
    137          columns = min(self._columns, len(self._content))
    138          # make sure all columns have the same width
    139          if columns > 1:
    140              for i in range(columns*2):
    141                  form_layout.setColumnStretch(i, 1)
    142
    143          row_span = 1
    144
    145          class cursor(object):
    146
    147              def __init__(self):
    148                  self.row = 0
    149                  self.col = 0
    150
    151              def next_row(self):
    152                  self.row = self.row + 1
    153                  self.col = 0
    154
    155              def next_col(self):
    156                  self.col = self.col + 2
    157                  if self.col >= columns * 2:
    158                      self.next_row()
    159
    160              def next_empty_row(self):
    161                  if self.col!=0:
    162                      self.next_row()
    163
    164              def __str__(self):
    165                  return '%s,%s'%(self.row, self.col)
    166
    167          c = cursor()
    168          for field in self._content:
    169              if isinstance( field, Form ):
    170                  c.next_empty_row()
    171                  col_span = 2 * columns
    172                  f = field.render( widgets, parent, True )
    173                  if isinstance( f, QtGui.QLayout ):
    174                      form_layout.addLayout( f, c.row, c.col, row_span, col_span )
    175                  else:
    176                      form_layout.addWidget( f, c.row, c.col, row_span, col_span )
    177                  c.next_row()
    178              elif field in widgets:
    179                  label, editor = widgets[field]
    180                  if isinstance( editor, ( WideEditor, ) ):
    181                      c.next_empty_row()
    182                      col_span = 2 * columns
    183                      if label:
    184                          form_layout.addWidget( label, c.row, c.col, row_span, col_span )
    185                          c.next_row()
    186                      form_layout.addWidget( editor, c.row, c.col, row_span, col_span )
    187                      c.next_row()
    188                  else:
    189                      col_span = 1
    190                      if label:
    191                          form_layout.addWidget( label, c.row, c.col, row_span, col_span )
    192                      form_layout.addWidget( editor, c.row, c.col + 1, row_span, col_span )
    193                      c.next_col()
    194              else:
    195                  logger.warning('ProgrammingError : widgets should contain a widget for field %s'%unicode(field))
    196
    197          if self._content and form_layout.count():
    198              # get last item in the layout
    199              last_item = form_layout.itemAt( form_layout.count() - 1 )
    200
    201              # if last item does not contain a widget, 0 is returned
    202              # which is fine with the isinstance test
    203              w = last_item.widget()
    204
    205              # add stretch only if last item is not expandable
    206              last_size_policy = w.sizePolicy()
    207              if last_size_policy.verticalPolicy()==QtGui.QSizePolicy.Expanding:
    208                  pass
    209              else:
    210                  form_layout.setRowStretch( form_layout.rowCount(), 1 )
    211
    212          form_widget = QtGui.QWidget( parent )
    213
    214          # fix embedded forms
    215          if nomargins:
    216              form_layout.setContentsMargins( 0, 0, 0, 0 )
    217
    218          form_widget.setSizePolicy( QtGui.QSizePolicy.Expanding,
    219                                     QtGui.QSizePolicy.Expanding )
    220          form_widget.setLayout( form_layout )
    221
    222          if self._scrollbars:
    223              scroll_area = QtGui.QScrollArea( parent )
    224              scroll_area.setWidget( form_widget )
    225              scroll_area.setWidgetResizable( True )
    226              scroll_area.setFrameStyle( QtGui.QFrame.NoFrame )
    227              return scroll_area
    228
    229          return form_widget
    230
    231
    232 -class Label( Form ):
    233      """Render a label with a QLabel"""
    234
    235 -    def __init__( self, label, alignment='left', style=None):
    236          """
    237          :param label : string to be displayed in the label
    238          :param alignment : alignment of text in the label. values that make sense 'left', 'right' or 'center'
    239          :param style : string of cascading stylesheet instructions
    240          """
    241          super( Label, self ).__init__( [] )
    242          self.label = label
    243          self.alignment = alignment
    244          self.style = style
    245
    246      @gui_function
    247 -    def render( self, widgets, parent = None, nomargins = False ):
    248          from PyQt4 import QtGui
    249          if self.style:
    250              widget = QtGui.QLabel( '<p align="%s" style="%s">%s</p>' % (self.alignment, self.style,unicode(self.label)) )
    251          else:
    252              widget = QtGui.QLabel( '<p align="%s">%s</p>' % (self.alignment,unicode(self.label)) )
    253          return widget
    254
    255 -class TabForm( Form ):
    256      """
    257    Render forms within a QTabWidget::
    258
    259      from = TabForm([('First tab', ['title', 'short_description']),
    260                      ('Second tab', ['director', 'release_date'])])
    261
    262    .. image:: ../_static/form/tab_form.png
    263      """
    264
    265      NORTH = 'North'
    266      SOUTH = 'South'
    267      WEST = 'West'
    268      EAST = 'East'
    269
    270 -    def __init__( self, tabs, position=NORTH ):
    271          """
    272          :param tabs: a list of tuples of (tab_label, tab_form)
    273          :param position: the position of the tabs with respect to the pages
    274          """
    275          assert isinstance( tabs, list )
    276          assert position in [self.NORTH, self.SOUTH, self.WEST, self.EAST]
    277          self.position = position
    278          for tab in tabs:
    279              assert isinstance( tab, tuple )
    280          self.tabs = [( tab_label, structure_to_form( tab_form ) ) for tab_label, tab_form in tabs]
    281          super( TabForm, self ).__init__( sum( ( tab_form.get_fields()
    282                                        for tab_label, tab_form in self.tabs ), [] ) )
    283
    284 -    def __unicode__( self ):
    285          return 'TabForm { %s\n        }' % ( u'\n  '.join( '%s : %s' % ( label, unicode( form ) ) for label, form in self.tabs ) )
    286
    287 -    def add_tab_at_index( self, tab_label, tab_form, index ):
    288          """Add a tab to the form at the specified index
    289
    290    :param tab_label: the name to the tab
    291    :param tab_form: the form to display in the tab or a list of field names.
    292    :param index: the position of tab in the tabs list.
    293          """
    294          tab_form = structure_to_form( tab_form )
    295          self.tabs.insert( index, ( tab_label, tab_form ) )
    296          self._content.extend( [tab_form] )
    297
    298 -    def add_tab( self, tab_label, tab_form ):
    299          """Add a tab to the form
    300
    301      :param tab_label: the name of the tab
    302      :param tab_form: the form to display in the tab or a list of field names.
    303          """
    304          tab_form = structure_to_form( tab_form )
    305          self.tabs.append( ( tab_label, tab_form ) )
    306          self._content.extend( [tab_form] )
    307
    308 -    def get_tab( self, tab_label ):
    309          """Get the tab form of associated with a tab_label, use this function to
    310          modify the underlying tab_form in case of inheritance
    311
    312      :param tab_label : a label of a tab as passed in the construction method
    313      :return: the tab_form corresponding to tab_label
    314          """
    315          for label, form in self.tabs:
    316              if label == tab_label:
    317                  return form
    318
    319 -    def replaceField( self, original_field, new_field ):
    320          for _label, form in self.tabs:
    321              if form.replaceField( original_field, new_field ):
    322                  return True
    323          return False
    324
    325 -    def _get_fields_from_form( self ):
    326          for _label, form in self.tabs:
    327              for field in form._get_fields_from_form():
    328                  yield field
    329
    330      @gui_function
    331 -    def render( self, widgets, parent = None, nomargins = False ):
    332          logger.debug( 'rendering %s' % self.__class__.__name__ )
    333          from PyQt4 import QtGui
    334          widget = QtGui.QTabWidget( parent )
    335          widget.setTabPosition(getattr(QtGui.QTabWidget, self.position))
    336          for tab_label, tab_form in self.tabs:
    337              widget.addTab( tab_form.render( widgets, widget ), unicode(tab_label) )
    338          return widget
    339
    340
    341 -class HBoxForm( Form ):
    342      """
    343    Render different forms in a horizontal box::
    344
    345      form = forms.HBoxForm([['title', 'short_description'], ['director', 'release_date']])
    346
    347    .. image:: ../_static/form/hbox_form.png
    348
    349    """
    350
    351 -    def __init__( self, columns ):
    352          """:param columns: a list of forms to display in the different columns
    353          of the horizontal box"""
    354          assert isinstance( columns, list )
    355          self.columns = [structure_to_form( col ) for col in columns]
    356          super( HBoxForm, self ).__init__( sum( ( column_form.get_fields()
    357                                        for column_form in self.columns ), [] ) )
    358
    359 -    def __unicode__( self ):
    360          return 'HBoxForm [ %s\n         ]' % ( '         \n'.join( [unicode( form ) for form in self.columns] ) )
    361
    362 -    def replaceField( self, original_field, new_field ):
    363          for form in self.columns:
    364              if form.replaceField( original_field, new_field ):
    365                  return True
    366          return False
    367
    368 -    def _get_fields_from_form( self ):
    369          for form in self.columns:
    370              for field in form._get_fields_from_form():
    371                  yield field
    372
    373      @gui_function
    374 -    def render( self, widgets, parent = None, nomargins = False ):
    375          logger.debug( 'rendering %s' % self.__class__.__name__ )
    376          from PyQt4 import QtGui
    377          widget = QtGui.QWidget( parent )
    378          form_layout = QtGui.QHBoxLayout()
    379          for form in self.columns:
    380              f = form.render( widgets, widget, nomargins )
    381              if isinstance( f, QtGui.QLayout ):
    382                  form_layout.addLayout( f )
    383              else:
    384                  form_layout.addWidget( f )
    385          widget.setLayout( form_layout )
    386          return widget
    387
    388 -class VBoxForm( Form ):
    389      """
    390    Render different forms or widgets in a vertical box::
    391
    392      form = forms.VBoxForm([['title', 'short_description'], ['director', 'release_date']])
    393
    394    .. image:: ../_static/form/vbox_form.png
    395    """
    396
    397 -    def __init__( self, rows ):
    398          """:param rows: a list of forms to display in the different columns
    399          of the horizontal box
    400          """
    401          assert isinstance( rows, list )
    402          self.rows = [structure_to_form( row ) for row in rows]
    403          super( VBoxForm, self ).__init__( sum( ( row_form.get_fields() for row_form in self.rows ), [] ) )
    404
    405 -    def replaceField( self, original_field, new_field ):
    406          for form in self.rows:
    407              if form.replaceField( original_field, new_field ):
    408                  return True
    409          return False
    410
    411 -    def _get_fields_from_form( self ):
    412          for form in self.rows:
    413              for field in form._get_fields_from_form():
    414                  yield field
    415
    416 -    def __unicode__( self ):
    417          return 'VBoxForm [ %s\n         ]' % ( '         \n'.join( [unicode( form ) for form in self.rows] ) )
    418
    419      @gui_function
    420 -    def render( self, widgets, parent = None, nomargins = False ):
    421          logger.debug( 'rendering %s' % self.__class__.__name__ )
    422          from PyQt4 import QtGui
    423          widget = QtGui.QWidget( parent )
    424          form_layout = QtGui.QVBoxLayout()
    425          for form in self.rows:
    426              f = form.render( widgets, widget, nomargins )
    427              if isinstance( f, QtGui.QLayout ):
    428                  form_layout.addLayout( f )
    429              else:
    430                  form_layout.addWidget( f )
    431          widget.setLayout( form_layout )
    432          return widget
    433
    434 -class ColumnSpan( Form ):
    435 -    def __init__(self, field=None, num=2):
    436          self.num = num
    437          self.field = field
    438          super( ColumnSpan, self ).__init__( [field] )
    439
    440 -class GridForm( Form ):
    441      """Put different fields into a grid, without a label.  Row or column labels can be added
    442    using the Label form::
    443
    444      GridForm([['title', 'short_description'], ['director','release_date']])
    445
    446    .. image:: ../_static/form/grid_form.png
    447    """
    448
    449 -    def __init__( self, grid, nomargins = False ):
    450          """:param grid: A list for each row in the grid, containing a list with all fields that should be put in that row
    451          """
    452          assert isinstance( grid, list )
    453          self._grid = grid
    454          self._nomargins = nomargins
    455          fields = []
    456          for row in grid:
    457              assert isinstance( row, list )
    458              fields.extend( row )
    459          super( GridForm, self ).__init__( fields )
    460
    461 -    def append_row(self, row):
    462          """:param row: the list of fields that should come in the additional row
    463          use this method to modify inherited grid forms"""
    464          assert isinstance( row, list )
    465          self._content.extend(row)
    466          self._grid.append(row)
    467
    468 -    def append_column(self, column):
    469          """:param column: the list of fields that should come in the additional column
    470          use this method to modify inherited grid forms"""
    471          assert isinstance( column, list )
    472          self._content.extend(column)
    473          for row, additional_field in zip(self._grid, column):
    474              row.append(additional_field)
    475
    476      @gui_function
    477 -    def render( self, widgets, parent = None, nomargins = False ):
    478          from PyQt4 import QtGui
    479          widget = QtGui.QWidget( parent )
    480          grid_layout = QtGui.QGridLayout()
    481          for i, row in enumerate( self._grid ):
    482              skip = 0
    483              for j, field in enumerate( row ):
    484                  num = 1
    485                  if isinstance( field, ColumnSpan ):
    486                      num = field.num
    487                      field = field.field
    488
    489                  if isinstance( field, Form ):
    490                      grid_layout.addWidget( field.render( widgets, parent ), i, j + skip, 1, num )
    491                      skip += num - 1
    492                  else:
    493                      _label, editor = widgets[field]
    494                      grid_layout.addWidget( editor, i, j + skip, 1, num )
    495                      skip += num - 1
    496
    497          widget.setLayout( grid_layout )
    498          if nomargins:
    499              grid_layout.setContentsMargins( 0, 0, 0, 0 )
    500
    501          return widget
    502
    503 -class WidgetOnlyForm( Form ):
    504      """Renders a single widget without its label, typically a one2many widget"""
    505
    506 -    def __init__( self, field ):
    507          assert isinstance( field, ( str, unicode ) )
    508          super( WidgetOnlyForm, self ).__init__( [field] )
    509
    510      @gui_function
    511 -    def render( self, widgets, parent = None, nomargins = False ):
    512          logger.debug( 'rendering %s' % self.__class__.__name__ )
    513          _label, editor = widgets[self.get_fields()[0]]
    514          return editor
    515
    516 -class GroupBoxForm( Form ):
    517      """
    518    Renders a form within a QGroupBox::
    519
    520      class Admin(EntityAdmin):
    521        form_display = GroupBoxForm('Movie', ['title', 'short_description'])
    522
    523    .. image:: ../_static/form/group_box_form.png
    524    """
    525
    526 -    def __init__( self, title, content, scrollbars=None, min_width=None, min_height=None, columns=1 ):
    527          self.title = title
    528          self.min_width = min_width
    529          self.min_height = min_height
    530          if isinstance(content, Form):
    531              content = [content]
    532          Form.__init__( self, content, scrollbars, columns=columns )
    533
    534      @gui_function
    535 -    def render( self, widgets, parent = None, nomargins = False ):
    536          from PyQt4 import QtGui
    537          widget = QtGui.QGroupBox( unicode(self.title), parent )
    538          layout = QtGui.QVBoxLayout()
    539          if self.min_width and self.min_height:
    540              widget.setMinimumSize ( self.min_width, self.min_height )
    541          widget.setLayout( layout )
    542          form = Form.render( self, widgets, widget, nomargins )
    543          layout.addWidget( form )
    544          return widget
    545
    546 -def structure_to_form( structure ):
    547      """Convert a python data structure to a form, using the following rules :
    548
    549     * if structure is an instance of Form, return structure
    550     * if structure is a list, create a Form from this list
    551
    552    This function is mainly used in the Admin class to construct forms out of
    553    the form_display attribute
    554      """
    555      if isinstance( structure, Form ):
    556          return structure
    557      return Form( structure )
    558