2 years ago
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)