camelot.view.forms — Camelot documentation

### Navigation

- index
- modules |
- Camelot documentation »
- Module code »

# Source code for camelot.view.forms

    #  ============================================================================
    #
    #  Copyright (C) 2007-2011 Conceptive Engineering bvba. All rights reserved.
    #  www.conceptive.be / project-camelot@conceptive.be
    #
    #  This file is part of the Camelot Library.
    #
    #  This file may be used under the terms of the GNU General Public
    #  License version 2.0 as published by the Free Software Foundation
    #  and appearing in the file license.txt included in the packaging of
    #  this file.  Please review this information to ensure GNU
    #  General Public Licensing requirements will be met.
    #
    #  If you are unsure which license is appropriate for your use, please
    #  visit www.python-camelot.com or contact project-camelot@conceptive.be
    #
    #  This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
    #  WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
    #
    #  For use of this library in commercial applications, please contact
    #  project-camelot@conceptive.be
    #
    #  ============================================================================

    """Classes to layout fields on a form.  These are mostly used for specifying the
    form_display attribute in Admin classes, but they can be used on their own as
    well.  Form classes can be used recursive.
    """

    import logging
    logger = logging.getLogger( 'camelot.view.forms' )

    from PyQt4 import QtCore, QtGui

    from camelot.view.model_thread import gui_function

    class Form( list ):
        """Base Form class to put fields on a form.  The base class of a form is
    a list.  So the form itself is nothing more than a list of field names or
    sub-forms.  A form can thus be manipulated using the list's method such as
    append or insert.

    A form can be converted to a
    QT widget by calling its render method.  The base form uses the QFormLayout
    to render a form::

        class Admin(EntityAdmin):
          form_display = Form(['title', 'short_description', 'director', 'release_date'])

    ..image:: /_static/form/form.png
    """

        def __init__( self, content, scrollbars = False, columns = 1  ):
            """
            :param content: an iterable with the field names and forms to render
            :param columns: the number of columns in which to order the fields.

            eg : with 2 columns, the fields ['street', 'city', 'country'] will
            be ordered as :

            +-------------+--------------+
            | street      | city         |
            +-------------+--------------+
            | country     |              |
            +-------------+--------------+

            """
            super(Form, self).__init__( content )
            self._scrollbars = scrollbars
            self._columns = columns

        def get_fields( self ):
            """:return: the fields, visible in this form"""
            return [field for field in self._get_fields_from_form()]

        def _get_fields_from_form( self ):
            for field in self:
                if isinstance( field, Form ):
                    for nested_field in  field._get_fields_from_form():
                        yield nested_field
                else:
                    assert isinstance( field, ( str, unicode ) )
                    yield field;

        def remove_field( self, original_field ):
            """Remove a field from the form, This function can be used to modify
            inherited forms.

            :param original_field: the name of the field to be removed
            :return: True if the field was found and removed
            """
            for c in self:
                if isinstance( c, Form ):
                    c.remove_field( original_field )
                if original_field in self:
                    self.remove( original_field )
                    return True
            return False

        def replace_field( self, original_field, new_field ):
            """Replace a field on this form with another field.  This function can be used to
            modify inherited forms.

            :param original_field : the name of the field to be replace
            :param new_field : the name of the new field
            :return: True if the original field was found and replaced.
            """
            for i, c in enumerate( self ):
                if isinstance( c, Form ):
                    c.replace_field( original_field, new_field )
                elif c == original_field:
                    self[i] = new_field
                    return True
            return False

        def add_field( self, new_field ):
            self.append( new_field )

        def __unicode__( self ):
            return 'Form(%s)' % ( u','.join( unicode( c ) for c in self ) )

        def render_ooxml( self, obj, delegates ):
            """Generator for lines of text in Office Open XML representing this form, using tables
            :param obj: the object or entity that will be rendered
            :param delegates: a dictionary mapping field names to their delegate
            """
            yield '<w:tbl>'
            yield '    <w:tblPr>'
            yield '      <w:tblW w:w="0" w:type="auto"/>'
            yield '      <w:tblBorders>'
            yield '          <w:top w:val="single" w:sz="4" wx:bdrwidth="10" w:space="0" w:color="auto"/>'
            yield '          <w:left w:val="single" w:sz="4" wx:bdrwidth="10" w:space="0" w:color="auto"/>'
            yield '          <w:bottom w:val="single" w:sz="4" wx:bdrwidth="10" w:space="0" w:color="auto"/>'
            yield '          <w:right w:val="single" w:sz="4" wx:bdrwidth="10" w:space="0" w:color="auto"/>'
            yield '          <w:insideH w:val="single" w:sz="4" wx:bdrwidth="10" w:space="0" w:color="auto"/>'
            yield '          <w:insideV w:val="single" w:sz="4" wx:bdrwidth="10" w:space="0" w:color="auto"/>'
            yield '        </w:tblBorders>'
            yield '      <w:tblLook w:val="04A0"/>'
            yield '    </w:tblPr>'
            yield '    <w:tblGrid>'
            yield '      <w:gridCol w:w="4811"/>'
            yield '      <w:gridCol w:w="4811"/>'
            yield '    </w:tblGrid>'
            yield '    <w:tr>'
            for index, field in enumerate(self):
                if index % self._columns == 0 and index != 0:
                    yield '    </w:tr><w:tr>'
                yield '<w:tc>'
                yield '  <w:tcPr>'
                yield '    <w:tcW w:w="4811" w:type="dxa"/>'
                yield '    <w:shd w:val="clear" w:color="auto" w:fill="auto"/>'
                yield '  </w:tcPr>'
                yield '<w:p>'
                lines = []
                if isinstance(field, Label):
                    lines = field.render_ooxml()
                elif isinstance(field, Form):
                    lines = field.render_ooxml( obj, delegates )
                elif isinstance(field, basestring):
                    delegate = delegates[field]
                    value = getattr(obj, field)
                    lines = delegate.render_ooxml(value)
                for line in lines:
                    yield line
                yield '</w:p>'
                yield '</w:tc>'
            yield '</w:tr>'
            yield '</w:tbl>'

        @gui_function
        def render( self, widgets, parent = None, toplevel = False):
            """
            :param widgets: a :class:`camelot.view.controls.formview.FormEditors` object
                that is able to create the widgets for this form
            :param parent: the :class:`QtGui.QWidget` in which the form is placed
            :param toplevel: a :keyword:`boolean` indicating if this form is toplevel,
                or a child form of another form.  A toplevel form will be expanding,
                while a non toplevel form is only expanding if it contains other
                expanding elements.

            :return: a QWidget into which the form is rendered
            """
            logger.debug( 'rendering %s' % (self.__class__.__name__) )
            from camelot.view.controls.editors.wideeditor import WideEditor
            form_widget = QtGui.QWidget( parent )
            form_layout = QtGui.QGridLayout()
            # where 1 column in the form is a label and a field, so two columns in the grid
            columns = min(self._columns, len(self))
            # make sure all columns have the same width
            for i in range(columns*2):
                if i%2:
                    form_layout.setColumnStretch(i, 1)

            row_span = 1

            class cursor(object):

                def __init__(self):
                    self.row = 0
                    self.col = 0

                def next_row(self):
                    self.row = self.row + 1
                    self.col = 0

                def next_col(self):
                    self.col = self.col + 2
                    if self.col >= columns * 2:
                        self.next_row()

                def next_empty_row(self):
                    if self.col!=0:
                        self.next_row()

                def __str__(self):
                    return '%s,%s'%(self.row, self.col)

            c = cursor()

            has_vertical_expanding_row = False
            for field in self:
                size_policy = None
                if isinstance( field, Form ):
                    c.next_empty_row()
                    col_span = 2 * columns
                    f = field.render( widgets, parent, False )
                    if isinstance( f, QtGui.QLayout ):
                        #
                        # this should maybe be recursive ??
                        #
                        for layout_item_index in range( f.count() ):
                            layout_item = f.itemAt( layout_item_index )
                            layout_item_widget = layout_item.widget()
                            if layout_item_widget and layout_item_widget.sizePolicy().verticalPolicy() == QtGui.QSizePolicy.Expanding:
                                has_vertical_expanding_row = True
                        form_layout.addLayout( f, c.row, c.col, row_span, col_span )
                    else:
                        form_layout.addWidget( f, c.row, c.col, row_span, col_span )
                        size_policy = f.sizePolicy()
                    c.next_row()
                else:
                    editor = widgets.create_editor( field, form_widget )
                    if editor != None:
                        if isinstance( editor, ( WideEditor, ) ):
                            c.next_empty_row()
                            col_span = 2 * columns
                            label = widgets.create_label( field, editor, form_widget )
                            if label:
                                form_layout.addWidget( label, c.row, c.col, row_span, col_span )
                                c.next_row()
                            form_layout.addWidget( editor, c.row, c.col, row_span, col_span )
                            c.next_row()
                        else:
                            col_span = 1
                            label = widgets.create_label( field, editor, form_widget )
                            if label:
                                form_layout.addWidget( label, c.row, c.col, row_span, col_span )
                            form_layout.addWidget( editor, c.row, c.col + 1, row_span, col_span )
                            c.next_col()
                        size_policy = editor.sizePolicy()
                    else:
                        logger.warning('ProgrammingError : widgets should contain a widget for field %s'%unicode(field))
                if size_policy and size_policy.verticalPolicy() == QtGui.QSizePolicy.Expanding:
                    has_vertical_expanding_row = True

            if (not has_vertical_expanding_row) and toplevel and form_layout.rowCount():
                form_layout.setRowStretch( form_layout.rowCount(), 1 )

            # fix embedded forms
            if not toplevel:
                form_layout.setContentsMargins( 0, 0, 0, 0 )

            if toplevel or has_vertical_expanding_row:
                form_widget.setSizePolicy( QtGui.QSizePolicy.Expanding,
                                           QtGui.QSizePolicy.Expanding )
            form_widget.setLayout( form_layout )

            if self._scrollbars:
                scroll_area = QtGui.QScrollArea( parent )
                # we should inherit parent's background color
                scroll_area.setWidget( form_widget )
                scroll_area.setWidgetResizable( True )
                scroll_area.setFrameStyle( QtGui.QFrame.NoFrame )
                return scroll_area

            logger.debug( 'end rendering %s' % self.__class__.__name__ )

            return form_widget

    class Label( Form ):
        """Render a label with a QLabel"""

        def __init__( self, label, alignment='left', style=None):
            """
            :param label : string to be displayed in the label
            :param alignment : alignment of text in the label. values that make sense 'left', 'right' or 'center'
            :param style : string of cascading stylesheet instructions
            """
            super( Label, self ).__init__( [] )
            self.label = label
            self.alignment = alignment
            self.style = style

        def render_ooxml( self ):
            """Generator for label text in Office Open XML representing this form"""
            yield '<w:r>'
            yield '  <w:t>%s</w:t>' % self.label
            yield '</w:r>'

        @gui_function
        def render( self, widgets, parent = None, toplevel = False ):
            from PyQt4 import QtGui
            if self.style:
                widget = QtGui.QLabel( '<p align="%s" style="%s">%s</p>' % (self.alignment, self.style,unicode(self.label)) )
            else:
                widget = QtGui.QLabel( '<p align="%s">%s</p>' % (self.alignment,unicode(self.label)) )
            return widget

    class DelayedTabWidget( QtGui.QTabWidget ):
        """Helper class for :class:`TabForm` to delay the creation of tabs to
    the moment the tab is shown.
        """

        def __init__( self,  widgets, tabs, parent = None ):
            super( DelayedTabWidget, self ).__init__( parent )
            self._widgets = widgets
            self._forms = []
            #
            # keep track for each of the tabs wether they are expanding,
            #
            self._vertical_expanding = [False] * len(tabs)
            #
            # set dummy tab widgets
            #
            for tab_label, tab_form in tabs:
                self._forms.append( tab_form )
                tab_widget = QtGui.QWidget( self )
                self.addTab( tab_widget, unicode(tab_label) )
            #
            # render the first tab and continue rendering until we have
            # a tab with an expanding size policy, because then we know
            # the widget itself should be expanding, and we can delay
            # the rendering of the other tabs
            #
            for i in range( len(tabs) ):
                self.render_tab( i )
                if sum(self._vertical_expanding):
                    self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
                    #
                    # if one of the tabs is expanding, the others should have spacer
                    # items to stretch
                    #
                    for j, vertical_expanding_of_widget in zip(range(i), self._vertical_expanding):
                        if vertical_expanding_of_widget == False:
                            tab_widget = self.widget( j )
                            tab_widget.layout().addStretch( 1 )
                    break
            self.currentChanged.connect( self.render_tab )

        @QtCore.pyqtSlot( int )
        def render_tab( self, index ):
            """
            Render the tab at index
            """
            tab_widget = self.widget( index )
            layout = tab_widget.layout()
            if layout != None:
                # this tab has been rendered before
                return
            layout = QtGui.QVBoxLayout( tab_widget )
            tab_form = self._forms[index]
            tab_form_widget = tab_form.render( self._widgets, tab_widget, False )
            layout.addWidget( tab_form_widget )
            tab_widget.setLayout( layout )
            size_policy = tab_form_widget.sizePolicy()
            if size_policy.verticalPolicy() == QtGui.QSizePolicy.Expanding:
                self._vertical_expanding[index] = True
            else:
                self._vertical_expanding[index] = False
            #
            # if other widgets are expanding, and this one isn't, add some stretch
            #
            if self._vertical_expanding[index] == False and sum(self._vertical_expanding):
                tab_widget.layout().addStretch( 1 )

    class TabForm( Form ):
        """
    Render forms within a QTabWidget::

        from = TabForm([('First tab', ['title', 'short_description']),
                        ('Second tab', ['director', 'release_date'])])

    .. image:: /_static/form/tab_form.png
        """

        NORTH = 'North'
        SOUTH = 'South'
        WEST = 'West'
        EAST = 'East'

        def __init__( self, tabs, position=NORTH ):
            """
            :param tabs: a list of tuples of (tab_label, tab_form)
            :param position: the position of the tabs with respect to the pages
            """
            assert isinstance( tabs, list )
            assert position in [self.NORTH, self.SOUTH, self.WEST, self.EAST]
            self.position = position
            for tab in tabs:
                assert isinstance( tab, tuple )
            self.tabs = [( tab_label, structure_to_form( tab_form ) ) for tab_label, tab_form in tabs]
            super( TabForm, self ).__init__( sum( ( tab_form.get_fields()
                                          for tab_label, tab_form in self.tabs ), [] ) )

        def __unicode__( self ):
            return 'TabForm { %s\n        }' % ( u'\n  '.join( '%s : %s' % ( label, unicode( form ) ) for label, form in self.tabs ) )

        def add_tab_at_index( self, tab_label, tab_form, index ):
            """Add a tab to the form at the specified index

      :param tab_label: the name to the tab
      :param tab_form: the form to display in the tab or a list of field names.
      :param index: the position of tab in the tabs list.
            """
            tab_form = structure_to_form( tab_form )
            self.tabs.insert( index, ( tab_label, tab_form ) )
            self.extend( [tab_form] )

        def add_tab( self, tab_label, tab_form ):
            """Add a tab to the form

        :param tab_label: the name of the tab
        :param tab_form: the form to display in the tab or a list of field names.
            """
            tab_form = structure_to_form( tab_form )
            self.tabs.append( ( tab_label, tab_form ) )
            self.extend( [tab_form] )

        def get_tab( self, tab_label ):
            """Get the tab form of associated with a tab_label, use this function to
            modify the underlying tab_form in case of inheritance

        :param tab_label : a label of a tab as passed in the construction method
        :return: the tab_form corresponding to tab_label
            """
            for label, form in self.tabs:
                if label == tab_label:
                    return form

        def replace_field( self, original_field, new_field ):
            super(TabForm, self).replace_field( original_field, new_field )
            for _label, form in self.tabs:
                if form.replace_field( original_field, new_field ):
                    return True
            return False

        def remove_field( self, original_field ):
            super(TabForm, self).remove_field( original_field )
            for _label, form in self.tabs:
                if form.remove_field( original_field ):
                    return True
            return False

        def _get_fields_from_form( self ):
            for _label, form in self.tabs:
                for field in form._get_fields_from_form():
                    yield field

        @gui_function
        def render( self, widgets, parent = None, toplevel = False ):
            logger.debug( 'rendering %s' % self.__class__.__name__ )
            widget = DelayedTabWidget( widgets, self.tabs, parent )
            widget.setTabPosition( getattr(QtGui.QTabWidget, self.position) )
            return widget

    class HBoxForm( Form ):
        """
      Render different forms in a horizontal box::

        form = forms.HBoxForm([['title', 'short_description'], ['director', 'release_date']])

      .. image:: /_static/form/hbox_form.png

      """

        def __init__( self, columns, scrollbars=False ):
            """:param columns: a list of forms to display in the different columns
            of the horizontal box"""
            assert isinstance( columns, list )
            self.columns = [structure_to_form( col ) for col in columns]
            super( HBoxForm, self ).__init__( sum( ( column_form.get_fields()
                                          for column_form in self.columns ), [] ), scrollbars=scrollbars )

        def __unicode__( self ):
            return 'HBoxForm [ %s\n         ]' % ( '         \n'.join( [unicode( form ) for form in self.columns] ) )

        def replace_field( self, original_field, new_field ):
            for form in self.columns:
                if form.replace_field( original_field, new_field ):
                    return True
            return False

        def _get_fields_from_form( self ):
            for form in self.columns:
                for field in form._get_fields_from_form():
                    yield field

        @gui_function
        def render( self, widgets, parent = None, toplevel = False ):
            logger.debug( 'rendering %s' % self.__class__.__name__ )
            widget = QtGui.QWidget( parent )
            form_layout = QtGui.QHBoxLayout()
            for form in self.columns:
                f = form.render( widgets, widget, False )
                if isinstance( f, QtGui.QLayout ):
                    form_layout.addLayout( f )
                else:
                    form_layout.addWidget( f )
            widget.setLayout( form_layout )
            return widget

    class VBoxForm( Form ):
        """
      Render different forms or widgets in a vertical box::

        form = forms.VBoxForm([['title', 'short_description'], ['director', 'release_date']])

      .. image:: /_static/form/vbox_form.png
      """

        def __init__( self, rows ):
            """:param rows: a list of forms to display in the different columns
            of the horizontal box
            """
            assert isinstance( rows, list )
            self.rows = [structure_to_form( row ) for row in rows]
            super( VBoxForm, self ).__init__( sum( ( row_form.get_fields() for row_form in self.rows ), [] ) )

        def replace_field( self, original_field, new_field ):
            for form in self.rows:
                if form.replace_field( original_field, new_field ):
                    return True
            return False

        def _get_fields_from_form( self ):
            for form in self.rows:
                for field in form._get_fields_from_form():
                    yield field

        def __unicode__( self ):
            return 'VBoxForm [ %s\n         ]' % ( '         \n'.join( [unicode( form ) for form in self.rows] ) )

        @gui_function
        def render( self, widgets, parent = None, toplevel = False ):
            logger.debug( 'rendering %s' % self.__class__.__name__ )
            widget = QtGui.QWidget( parent )
            form_layout = QtGui.QVBoxLayout()
            for form in self.rows:
                f = form.render( widgets, widget, False )
                if isinstance( f, QtGui.QLayout ):
                    form_layout.addLayout( f )
                else:
                    form_layout.addWidget( f )
            widget.setLayout( form_layout )
            return widget

    class ColumnSpan( Form ):

        def __init__(self, field=None, num=2):
            self.num = num
            self.field = field
            super( ColumnSpan, self ).__init__( [field] )

    class GridForm( Form ):
        """Put different fields into a grid, without a label.  Row or column labels can be added
      using the Label form::

        GridForm([['title', 'short_description'], ['director','release_date']])

      .. image:: /_static/form/grid_form.png
      """

        def __init__( self, grid, nomargins = False ):
            """:param grid: A list for each row in the grid, containing a list with all fields that should be put in that row
            """
            assert isinstance( grid, list )
            self._grid = grid
            self._nomargins = nomargins
            fields = []
            for row in grid:
                assert isinstance( row, list )
                fields.extend( row )
            super( GridForm, self ).__init__( fields )

        def append_row(self, row):
            """:param row: the list of fields that should come in the additional row
            use this method to modify inherited grid forms"""
            assert isinstance( row, list )
            self.extend(row)
            self._grid.append(row)

        def append_column(self, column):
            """:param column: the list of fields that should come in the additional column
            use this method to modify inherited grid forms"""
            assert isinstance( column, list )
            self.extend(column)
            for row, additional_field in zip(self._grid, column):
                row.append(additional_field)

        @gui_function
        def render( self, widgets, parent = None, toplevel = False ):
            widget = QtGui.QWidget( parent )
            grid_layout = QtGui.QGridLayout()
            for i, row in enumerate( self._grid ):
                skip = 0
                for j, field in enumerate( row ):
                    num = 1
                    if isinstance( field, ColumnSpan ):
                        num = field.num
                        field = field.field

                    if isinstance( field, Form ):
                        grid_layout.addWidget( field.render( widgets, parent ), i, j + skip, 1, num )
                        skip += num - 1
                    else:
                        editor = widgets.create_editor( field, widget )
                        grid_layout.addWidget( editor, i, j + skip, 1, num )
                        skip += num - 1

            widget.setLayout( grid_layout )
            if not toplevel:
                grid_layout.setContentsMargins( 0, 0, 0, 0 )

            return widget

    class WidgetOnlyForm( Form ):
        """Renders a single widget without its label, typically a one2many widget"""

        def __init__( self, field ):
            assert isinstance( field, ( str, unicode ) )
            super( WidgetOnlyForm, self ).__init__( [field] )

        @gui_function
        def render( self, widgets, parent = None, toplevel = False ):
            logger.debug( 'rendering %s' % self.__class__.__name__ )
            editor = widgets.create_editor( self.get_fields()[0], parent )
            return editor

    class GroupBoxForm( Form ):
        """
      Renders a form within a QGroupBox::

        class Admin(EntityAdmin):
          form_display = GroupBoxForm('Movie', ['title', 'short_description'])

      .. image:: /_static/form/group_box_form.png
      """

        def __init__( self, title, content, scrollbars=None, min_width=None, min_height=None, columns=1 ):
            self.title = title
            self.min_width = min_width
            self.min_height = min_height
            if isinstance(content, Form):
                content = [content]
            Form.__init__( self, content, scrollbars, columns=columns )

        @gui_function
        def render( self, widgets, parent = None, toplevel = False ):
            widget = QtGui.QGroupBox( unicode(self.title), parent )
            layout = QtGui.QVBoxLayout()
            if self.min_width and self.min_height:
                widget.setMinimumSize ( self.min_width, self.min_height )
            widget.setLayout( layout )
            form = Form.render( self, widgets, widget, False )
            layout.addWidget( form )
            return widget

    def structure_to_form( structure ):
        """Convert a python data structure to a form, using the following rules :

       * if structure is an instance of Form, return structure
       * if structure is a list, create a Form from this list

      This function is mainly used in the Admin class to construct forms out of
      the form_display attribute
        """
        if isinstance( structure, Form ):
            return structure
        return Form( structure )

### Navigation

- index
- modules |
- Camelot documentation »
- Module code »

Comments
blog comments powered by Disqus