sqlkit/widgets/table/columns.py

2 years ago

author
sandro
date
Thu Sep 10 19:35:03 2009 +0200
changeset 1180
230d6baaf634
parent 1053
f9602e3018b5
child 1205
b68b0c77e739
permissions
-rw-r--r--

doc: change in the Makefile to set correct download address

     1 # Copyright (C) 2009, Sandro Dentella <sandro@e-den.it>
     2 #
     3 #  This program is free software: you can redistribute it and/or modify
     4 #  it under the terms of the GNU General Public License as published by
     5 #  the Free Software Foundation, either version 3 of the License, or
     6 #  (at your option) any later version.
     7 #
     8 #  This program is distributed in the hope that it will be useful,
     9 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
    10 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11 #  GNU General Public License for more details.
    12 #
    13 #  You should have received a copy of the GNU General Public License
    14 #  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16 """
    17 Classes that implement creation of column and cell renderers
    18 This should allow to be as generic as possible while adding new renderers
    20 Each Column widget need a 'master' argument that is the SqlWidget
    21 in editing_started_cb and  edited_cb to set state variables on SqlWidget
    23 """
    26 import gtk
    27 import pango
    28 import cell_renderers
    29 from sqlkit import debug as dbg, _
    30 from sqlkit.layout import widgets
    32 class GenericColumn(object):
    33     RENDERER = gtk.CellRendererText
    34     RESIZE = True
    35     REORDER = True
    36     CLICK = True
    37     EXPAND = True
    38     MIN_DEFAULT_WIDTH = 4
    39     MAX_DEFAULT_WIDTH = 30
    41     def __init__(self, master, field_name, title):
    42         """
    43         Generic column with cell render setup
    44         """
    45         self.master = master 
    46         self.field_name = field_name
    47         self.column = self.setup_column(title)
    48         cell = self.setup_cell_renderers()
    49         self.column.set_cell_data_func(cell, self.cell_data_func_cb, 'text') 
    50         self.column.cells_data_func = [(cell, self.cell_data_func_cb, 'text') ]
    52     def setup_column(self, title):
    53         """
    54         setup column 
    55         """
    57         column = gtk.TreeViewColumn(title.replace('_',' '))
    58         column.set_data('field_name', self.field_name)  # used when reordering columns
    59         column.set_resizable(self.RESIZE)
    60         column.set_expand(self.EXPAND)
    61         column.set_reorderable(self.REORDER)
    62         column.set_clickable(self.CLICK)
    64         column.set_expand(self.is_expandable())
    65         column.connect('clicked', self.clicked_cb)
    66         self.add_header_widget(column)
    67         return column
    69     def setup_cell_renderers(self, cell_renderer=None):
    70         """
    71         setup cell renderers 
    72         """
    74         cell = cell_renderer or self.RENDERER()
    75         self.cell_set_property(cell)
    77         self.column.pack_start(cell, True)
    79         ## connect
    80         cell.connect('edited', self.edited_cb)
    81         cell.connect('editing-started', self.editing_started_cb)
    82         cell.connect('editing-canceled', self.editing_canceled_cb)
    84         return cell
    86 ### callback
    87     def edited_cb(self, cell, path, new_text):
    88         """
    89         callback on signal 'edited' on cell_renderer. 
    91         """
    93         ## NOTE: liststore.clear will trigger text_edited_cb!!!
    94         if self.master.deleting_row:
    95             return True
    97         if self.master.edited_row_model_path is None:
    98             return True
   100         if self.master.cell_entry and self.master.cell_validate():
   101             self.master.cell_entry = None
   102         else:
   103             return True
   105     def editing_started_cb(self, cell, editable, path):
   107         self.master.currently_edited_field_name = self.field_name
   108         self.master.initialize_record_editing(path)
   109         return True
   111     def cell_data_func_cb(self, column, cell, model, iter, prop_name):
   112         """
   113         function set by set_cell_data_func
   114         gets the attribute from the record and returns it
   115         """
   117         if not column.get_visible():
   118             return
   119         obj = model.get_value(iter, 0) 
   120         value = getattr(obj, self.field_name)
   121         cell.set_property(prop_name, value)        
   122         formatted_value = self.master.gui_fields[self.field_name].format_value(value)
   124         if isinstance(obj, self.master.totals.total_class):
   125             obj.set_value_and_colors(cell, formatted_value, self.field_name)
   126         else:
   127             cell.set_property(prop_name, formatted_value)        
   128             cell.set_property('foreground-set', False)
   129             cell.set_property('cell-background-set', False)
   132     def editing_canceled_cb(self, widget):
   133         """
   134         clean-up of cell_entry and restore value
   135         """
   137         if self.master.cell_entry and self.master.cell_entry.substitute:
   138             # assigning a value to liststore, triggers 'editing-canceled'
   139             self.master.cell_entry.substitute = False
   140             return
   142         self.master.set_value(self.field_name, self.master.gui_fields[self.field_name].initial_value)
   143         self.master.cell_entry = None
   145         self.master.sb(_("Editing canceled. Restoring original value"), seconds=3)
   148     def editable_connects(self, editable):
   149         """
   150         add here possible connect
   151         """
   152         pass
   154     def clicked_cb(self, treeview):
   155         """
   156         popup the menu on the header of the column
   157         """
   158         event =   gtk.gdk.Event(gtk.gdk.BUTTON_PRESS)
   159         event.button = 1
   160         self.master.menu.popup_col_menu(event, self.field_name)
   162         return True
   165     def button_press_cb(self, widget, event, column):
   166         """
   167         popup the menu on the header of the column
   168         """
   169         ## this is not working at the moment.
   170         ## click on the label inside the eventBox don't propagate the button-press-event...
   171         self.master.popup_col_menu(event, self.field_name)
   173         return True
   175 #### auxiliary
   176     def cell_set_property(self, cell):
   177         """
   178         set cell properties
   179         """
   181         if self.master.is_editable(self.field_name):
   182             cell.set_property('editable', True)
   183             cell.set_property('editable-set', True)
   184             cell.set_property('mode', gtk.CELL_RENDERER_MODE_EDITABLE)
   185         else:
   186             try:
   187                 cell.set_property('editable', False)
   188             except:
   189                 cell.set_property('sensitive', False)
   192     def add_header_widget(self, column):
   193         """
   194         Add a header that accepts tooltips and bold labels for mandatory columns
   195         """
   196         # Label
   197         label_text, help_text = self.master.label_map.get(self.field_name, (None, None))
   198         label_text = self.master.get_label(label_text or self.field_name)
   199         label = gtk.Label()
   200         label.set_markup(label_text)
   202         ## bold italic
   203         if not self.master.is_nullable(self.field_name) and not (
   204             self.master.mapper_info.fields[self.field_name]['default']):
   205             label.set_markup('<b><i>%s</i></b>' % label_text)
   207         # EventBox
   208         event_box = gtk.EventBox()
   209         event_box.add(label)
   210         event_box.show_all()
   212         # Tooltip
   213         if help_text:
   214             event_box.set_tooltip_text(help_text)
   216         # Header
   217         column.set_widget(event_box)
   218         event_box.connect('button-press-event', self.button_press_cb, column)
   221     def is_expandable(self):
   222         """
   223         make expandable only Strings with n_chars > 20 or Text
   224         """
   226         if self.master.is_text(self.field_name):
   227             return True
   228         if self.master.is_string(self.field_name):
   229             if self.master.mapper_info.fields[self.field_name]['length'] > 20:
   230                 return True
   231         return False
   233     def get_width(self):
   234         """
   235         Find the width looking in default database info or in col_width
   236         """
   238         if self.master.col_width and self.field_name in self.master.col_width:
   239             width = self.master.col_width.get(self.field_name, )
   240         else:
   241             width = self.master.gui_fields[self.field_name].length
   243             if not width or width < self.MIN_DEFAULT_WIDTH:
   244                 width = self.MIN_DEFAULT_WIDTH
   246             if width > self.MAX_DEFAULT_WIDTH:
   247                 width = self.MAX_DEFAULT_WIDTH
   249         return width
   253     def __repr__(self):
   254         return "<%s: %s >" % (self.__class__.__name__, self.field_name)
   256 class BooleanColumn(GenericColumn):
   257     RENDERER = gtk.CellRendererToggle
   258     RESIZE = False
   259     EXPAND = False
   261     def setup_cell_renderers(self, cell_renderer=None):
   262         """
   263         setup different cell_cb according to nullable value
   264         """
   266         cell = cell_renderer or self.RENDERER()
   267         self.cell_set_property(cell)
   269         self.column.pack_start(cell, True)
   270         self.column.set_data('cell', cell)
   272         ## connect
   273         cell.connect('editing-canceled', self.editing_canceled_cb) # ???
   274         cell.connect('toggled', self.toggled_signal_cb, self.master.liststore)
   276         self.column.set_cell_data_func(cell, self.cell_data_func_cb)
   277         self.column.cells_data_func = [(cell, self.cell_data_func_cb)]
   278         cell.fixed_width = True
   279         return  cell
   281     def cell_set_property(self, cell):
   283         if self.master.is_editable(self.field_name):
   284             cell.set_property('activatable', True)
   286     def toggled_signal_cb(self, cell, path, model):
   287         """
   288         callback that cicle into True/False/(inconsistent if nullable)
   289         """
   290         self.master.currently_edited_field_name = self.field_name
   291         self.master.initialize_record_editing(path)
   293         obj = self.master.liststore[path][0]
   294         status = getattr(obj, self.field_name)
   296         if self.master.is_nullable(self.field_name):
   297             # status may be True/False/None - I want to cicle
   298             if   status == True:    status = False
   299             elif status == False:   status = None
   300             elif status == None:    status = True
   301         else:
   302             status = not status
   304         self.master.gui_fields[self.field_name].set_value(status, initial=False )        
   306         return True
   308     def cell_data_func_cb(self, column, cell, model, iter, prop_name):
   309         """
   310         function set by set_cell_data_func for nullable boolean
   311         """
   313         if not column.get_visible():
   314             return
   316         obj = model.get_value(iter, 0) 
   317         value = getattr(obj, self.field_name)
   318         cell.set_property('active', value)        
   320         if isinstance(obj, self.master.totals.total_class):
   321             cell.set_property('visible', False)        
   322         else:
   323             cell.set_property('visible', True)        
   325             if value is None:
   326                 cell.set_property('inconsistent', True)
   327             else:    
   328                 cell.set_property('inconsistent', False)
   330 class VarcharColumn(GenericColumn):
   331     """
   332     A renderer that expands only large fields and that centers text if it's little
   333     Cell renderer is choosen according to VarChar or Text
   334     """
   335     __metaclass__ = dbg.LogTheMethods
   337     def setup_cell_renderers(self, cell_renderer=None):
   339         width = self.get_width()
   341         ## look for a possibly defined width
   342         ## choose cell renderer
   343         if width:
   344             cell = cell_renderers.CellRendererVarchar(width)
   345             cell.fixed_width = True
   346         else:
   347             cell = gtk.CellRendererText()
   349         GenericColumn.setup_cell_renderers(self, cell_renderer=cell)
   350         cell.set_property('ellipsize', pango.ELLIPSIZE_END)
   351         cell.set_data('width', width)
   353         self.set_alignment(cell)
   354         return cell
   356     def set_alignment(self, cell):
   357         """
   358         Set alignment of text in the cell
   359         """
   361         length = self.master.gui_fields[self.field_name].length
   362         if length:
   363             try:
   364                 ## if field has less then 4 chars, position it in the middle
   365                 if int(length) <= 4:
   366                     cell.set_property('xalign', 0.5)
   367             except:
   368                 pass
   369         return  cell
   372     def editing_started_cb(self, cell, editable, path):
   373         """
   374         prepare the editable, completion and validation
   375         """
   376         self.master.currently_edited_field_name = self.field_name
   377         self.master.cell_entry = CellEntry(editable, cell, path, self.field_name)
   378         editable.connect('remove-widget', self.remove_widget_cb)
   379         editable.connect('key-press-event', self.master.keypress_event_cb)
   380         editable.connect('focus-out-event', self.focus_out_cb, cell, path)
   381         self.editable_connects(editable)
   383         self.master.fkey_value = editable.get_text()
   385         if hasattr(self.master.field_widgets[self.field_name], 'completion'):
   386             completion = self.master.field_widgets[self.field_name].completion
   387             if completion and not self.master.is_text(self.field_name):
   388                 completion.add_completion(editable)
   389                 completion.add_callbacks(editable)
   391         ## MAX LENGTH
   392         self.master.field_widgets[self.field_name].field.set_max_length()
   394         self.master.initialize_record_editing(path)
   395         self.master.commit_inhibited = False
   396         return True
   398     def remove_widget_cb(self, widget):
   399         """
   400         If the call has invalid data we don't want to remove the widget
   401         so that we can edit the wrong data rather then loosing what was already typed
   402         """
   403         if self.master.cell_entry and not self.master.cell_entry.is_valid():
   404             widget.stop_emission('remove-widget')
   405             return True
   406         self.master.cell_entry = None
   408     def focus_out_cb(self, widget, event, cell, path):
   409         if self.master.cell_entry:
   410             widget.activate()
   412 class MultilineColumn(VarcharColumn):
   413     """
   414     A renderer able to show and edit in multiline mode. Very basic...
   415     """
   416     MIN_DEFAULT_WIDTH = 40
   419     def setup_cell_renderers(self, cell_renderer=None):
   421         width = self.get_width()
   423         cell = cell_renderers.MultilineCellRenderer(self.master)
   425         GenericColumn.setup_cell_renderers(self, cell_renderer=cell)
   426         cell.set_property('ellipsize', pango.ELLIPSIZE_END)
   427         cell.set_data('width', width)
   429         return cell
   431     def remove_widget_cb(self, widget):
   432         """
   433         If the call has invalid data we don't want to remove the widget
   434         so that we can edit the wrong data rather then loosing what was already typed
   435         """
   436         if self.master.cell_entry:
   437             self.master.cell_validate()
   439         self.master.cell_entry = None
   441     def focus_out_cb(self, widget, event, cell, path):
   443         if self.master.cell_entry:
   444             self.master.cell_validate()
   445             cell.emit("edited", cell.editor.get_data("path"), cell.editor.get_text())
   446             cell.editor.remove_widget()
   448         pass
   450 class NumericColumn(VarcharColumn):
   451     """
   452     A renderer similar to varchar but with different expand options
   453     and with right justification
   454     """
   456     def get_width(self):
   458         if self.master.col_width and self.field_name in self.master.col_width:
   459             width = self.master.col_width.get(self.field_name, )
   460         else:
   461             if self.master.gui_fields[self.field_name].type == int:
   462                 width = 8
   463             else:
   464                 width = self.master.gui_fields[self.field_name].column.type.precision
   466         return width
   468     def set_alignment(self, cell):
   470         cell.set_property('xalign', 1.0)
   472     def editable_connects(self, editable):
   473         editable.connect('key-press-event', self.master.digits_check_input_cb  )            
   476 class FKeyColumn(VarcharColumn):
   477     """
   478     A renderer that shows the value instead of the id
   479     """
   480     MIN_DEFAULT_WIDTH = 15
   482     def cell_set_property(self, cell):
   484         VarcharColumn.cell_set_property(self, cell)
   485         cell.set_property('foreground', 'navyblue')
   487     def cell_data_func_cb(self, column, cell, model, iter, prop_name):
   488         """
   489         cell_data_function used by column that have fkey
   490         """
   492         if not column.get_visible():
   493             return
   495         obj = model.get_value(iter, 0)
   497         if not obj:
   498             return
   500         if isinstance(obj, self.master.totals.total_class):
   501             cell.set_property('text', '')
   503         else:
   504             fkey_value = getattr(obj, self.field_name)
   506             if fkey_value is not None:
   507                 path = model.get_path(iter)
   508                 value = self.master.completions[self.field_name].lookup_value(fkey_value)
   509                 cell.set_property('text', value)
   510             else:
   511                 cell.set_property('text', '')
   514 class DateColumn(VarcharColumn):
   516     """
   517     Will have bindings to change the date in a faster way
   518     possibly popping a calendar
   519     """
   521     MIN_DEFAULT_WIDTH = 10
   523     def editable_connects(self, editable):
   525         pass
   526         #editable.connect('key-press-event')
   528 class DateTimeColumn(DateColumn):
   530     MIN_DEFAULT_WIDTH = 16
   532     def editable_connects(self, editable):
   534         pass
   535         #editable.connect('key-press-event')
   537 class CellEntry(object):
   538     """
   539     An object to store info on editable status
   540     Mainly needed to check if validation is required with FKey fields and m2m nested tables
   541     Completion sets valid to True, any manual edit invalidates, so that a field.validate() is
   542     triggered.
   543     Any movement (click of Tab or Down) away from a CellRenderer is inhibited if
   545       * a cell exists
   546       * the cell is not valid
   548     CellEntry is destroyed within 'remove-widget' signal callback of the editable
   550     FIXME: should probably be simplified being it only needed for FKey Columns/m2m
   551     """
   552     def __init__(self, editable, cell, path, field_name):
   554         self.entry = editable
   555         self.cell = cell
   556         self.path = path
   557         self.field_name = field_name
   558         self.valid = True
   559         self.text = editable.get_text()
   561         if isinstance(editable, gtk.Entry):
   562             # Multiline does not have 'canged' and does not need invalidating
   563             self.entry.connect('changed', self.on_editable_change)
   564         else:
   565             ## this is just a hack to force cell_validate() when browsing away
   566             self.valid = False
   568         self.substitute = False
   570     def on_editable_change(self, widget):
   571         self.valid = False
   573     def is_valid(self):
   575         valid = self.valid or self.text == self.entry.get_text()
   576         return valid
   578     def get_text(self):
   579         return self.entry.get_text()
   581     def __repr__(self):
   582         return "<CellEntry: (%s) %s>" % (self.valid, self.get_text())
   584 class ColumnMenuProxy(object):
   585     def __init__(self, master):
   586         self.master = master
   588 #### column menu
   589     def popup_col_menu(self, ev, field_name):
   591         from sqlkit.layout.misc import StockMenuItem
   592         try:
   593             self.master.widgets['M=popup'].destroy()
   594         except:
   595             pass
   597         menu = self.master.widgets['M=popup'] = gtk.Menu()
   599         ## add to filter
   600         ## TIP: menu entry to add a filter in the filter panel
   601         field_name_localized = self.master.get_label(field_name)
   602         label_str = _("Add a filter on '%s'") % field_name_localized
   603         item = StockMenuItem(label=label_str, stock='gtk-find')
   604 #        item.connect('activate', self.filter_panel.add, ev, field_name )
   605         item.connect('activate', self.master.filter_panel.add, ev, field_name, self.master )
   606         menu.append(item)
   608         ## sort
   609         if not self.master.relationship_leader:
   610             item = gtk.ImageMenuItem('gtk-sort-ascending')
   611             item.connect('activate', self.reload_sort_cb, field_name, 'ASC' )
   612             menu.append(item)
   614             item = gtk.ImageMenuItem('gtk-sort-descending')
   615             item.connect('activate', self.reload_sort_cb, field_name, 'DESC' )
   616             menu.append(item)
   618         ## hide
   619         # TIP: column menu opt
   620         item = StockMenuItem(label=_("Hide this column"), stock='gtk-close')
   621         item.connect('activate', self.hide_fields_cb, field_name )
   622         menu.append(item)
   624         ## totals
   625         if self.master.is_number(field_name) and not self.master.is_fkey(field_name):
   626             # TIP: column menu opt
   627             item = gtk.MenuItem(label=_("Create total"))
   628             item.connect('activate', self.add_total_cb, field_name )
   629             menu.append(item)
   631         menu.append(gtk.SeparatorMenuItem())
   632         ## subtotals
   633         if self.master.is_date(field_name):
   634             self.menu_add_date_total_break(menu, field_name)
   635         else:
   636             # TIP: column menu total
   637             item = gtk.MenuItem(label=_("Subtotal on %s") % (field_name_localized))
   638             item.connect('activate', self.add_subtotal_cb, field_name )
   639             menu.append(item)
   641         ## info
   642         # TIP: column menu opt
   643         item = gtk.ImageMenuItem('gtk-info')
   644         item.connect('activate', self.master.show_field_info, field_name )
   645         menu.append(item)
   647         menu.show_all()
   648         menu.popup(None, None, None, ev.button, ev.time)
   649         #menu.popup(None, None, None, 1, 1)
   651     def fill_menu_hide_fields(self):
   652         """
   653         Add menu for showing/hiding field columns
   654         """
   655         if not 'M=modify' in self.master.widgets:
   656             return
   657         menu = self.master.widgets['M=modify']
   658         # TIP: modify menu entry
   659         item_show = gtk.MenuItem(label=_("Show field"))
   660         item_hide = gtk.MenuItem(label=_("Hide field"))
   661         menu.append(item_show)
   662         menu.append(item_hide)
   664         self.menu_show = gtk.Menu()
   665         self.menu_hide = gtk.Menu()
   667         item_show.set_submenu(self.menu_show)
   668         item_hide.set_submenu(self.menu_hide)
   670         for field_name in self.master.field_list:
   671             field_name_localized = get_label(field_name, layout=self.lay_obj).replace('_', '__')
   672             m = gtk.MenuItem(field_name_localized)
   673             m.connect('activate', self.hide_fields_cb, field_name)
   674             self.menu_hide.append(m)
   676         item_hide.set_submenu(self.menu_hide)
   677         menu.show_all()
   679     def hide_fields_cb(self, menuItem, field_name):
   680         self.master.hide_fields(field_name)
   682     def menu_add_date_total_break(self, menu, field_name):
   684         def add_break_date_cb(menu_item, field_name, period):
   685             self.master.totals.add_date_break(field_name, period)
   687         periods = [('day',_('day')), ('week',_('week')), ('month',_('month')),
   688                    ('quarter',_('quarter')), ('year',_('year'))]
   690         for period, trad in periods:
   691             item = gtk.MenuItem(label=_("Subtotals by %s") % trad)
   692             item.connect('activate', add_break_date_cb, field_name, period)
   693             menu.append(item)
   695     def add_total_cb(self, menu_item, field_name):
   696         self.master.totals.add_total(field_name)
   698     def add_subtotal_cb(self, menu_item, field_name):
   699         self.master.totals.add_break(field_name)
   701     def menu_add_toggle_column(self, field_name):
   702         """
   703         Add a menu entry to show columns in the menu 'modify' or change it's
   704         visibility according to real state of the column
   705         """
   707         action_name = 'field_%s' % field_name
   708         action = self.master.ui_manager.get_action('/Main/Modify/ShowColumns/%s' % action_name)
   709         if not action:
   710             self.master.actiongroup_table.add_actions([
   711                 (action_name, None, self.master.get_label(field_name), None, None, self.master.show_column,),
   712                 ], field_name)
   714             menu_def = '''
   715               <menubar name="Main">
   716                  <menu action="Modify">
   717                     <menu action="ShowColumns" >
   718                          <menuitem action="%s" />
   719                     </menu>
   720                  </menu>
   721                </menubar>
   722             ''' % action_name
   724             self.master.ui_manager.add_ui_from_string(menu_def)
   726         else:
   727             action.set_visible(not action.get_visible())
   729         self.master.tvcolumns[field_name].set_property('visible', False)
   731     def reload_sort_cb(self, widget, field_name, direction):
   733         from sqlkit.db.utils import tables, get_description
   735         order_by = field_name
   736         if self.master.is_fkey(field_name):
   737             ftable = getattr(self.master.mapper.c, field_name).foreign_keys[0].column.table
   738             fdescr = get_description(ftable, attr='description')
   739             order_by = "%s__%s" % (field_name, fdescr)
   742         if direction == 'DESC':
   743             order_by += ' DESC'
   745         self.master.order_by = order_by
   746         self.master.reload_cb(None)

mercurial