ergo: editable lists don't loose focus when user tab a non editable column
[openerp-client.git] / bin / widget / view / tree_gtk / parser.py
blob73489f9bec2a81d71ff5414255479ca9ba3fb540
1 ##############################################################################
3 # Copyright (c) 2004-2008 TINY SPRL. (http://tiny.be) All Rights Reserved.
5 # $Id$
7 # WARNING: This program as such is intended to be used by professional
8 # programmers who take the whole responsability of assessing all potential
9 # consequences resulting from its eventual inadequacies and bugs
10 # End users who are looking for a ready-to-use solution with commercial
11 # garantees and support are strongly adviced to contract a Free Software
12 # Service Company
14 # This program is Free Software; you can redistribute it and/or
15 # modify it under the terms of the GNU General Public License
16 # as published by the Free Software Foundation; either version 2
17 # of the License, or (at your option) any later version.
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
24 # You should have received a copy of the GNU General Public License
25 # along with this program; if not, write to the Free Software
26 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
28 ##############################################################################
30 import re
31 import locale
32 import gtk
33 from gtk import glade
34 import math
36 import tools
37 from rpc import RPCProxy
38 from editabletree import EditableTreeView
39 from decoratedtree import DecoratedTreeView
40 from widget.view import interface
42 import time
44 from widget.view.form_gtk.many2one import dialog as M2ODialog
45 from modules.gui.window.win_search import win_search
47 import common
48 import rpc
49 import datetime as DT
50 import service
52 def send_keys(renderer, editable, position, treeview):
53 editable.connect('key_press_event', treeview.on_keypressed)
54 editable.editing_done_id = editable.connect('editing_done', treeview.on_editing_done)
55 if isinstance(editable, gtk.ComboBoxEntry):
56 editable.connect('changed', treeview.on_editing_done)
58 def sort_model(column, treeview):
59 model = treeview.get_model()
60 model.sort(column.name)
62 class parser_tree(interface.parser_interface):
63 def parse(self, model, root_node, fields):
64 dict_widget = {}
65 attrs = tools.node_attributes(root_node)
66 on_write = attrs.get('on_write', '')
67 editable = attrs.get('editable', False)
68 if editable:
69 treeview = EditableTreeView(editable)
70 else:
71 treeview = DecoratedTreeView(editable)
72 treeview.colors = dict()
73 self.treeview = treeview
74 for color_spec in attrs.get('colors', '').split(';'):
75 if color_spec:
76 colour, test = color_spec.split(':')
77 treeview.colors[colour] = test
78 treeview.set_property('rules-hint', True)
79 if not self.title:
80 self.title = attrs.get('string', 'Unknown')
82 for node in root_node.childNodes:
83 node_attrs = tools.node_attributes(node)
84 if node.localName == 'field':
85 fname = str(node_attrs['name'])
86 for boolean_fields in ('readonly', 'required'):
87 if boolean_fields in node_attrs:
88 node_attrs[boolean_fields] = bool(int(node_attrs[boolean_fields]))
89 fields[fname].update(node_attrs)
90 node_attrs.update(fields[fname])
91 cell = Cell(fields[fname]['type'])(fname, treeview, node_attrs,
92 self.window)
93 treeview.cells[fname] = cell
94 renderer = cell.renderer
95 if editable and not node_attrs.get('readonly', False):
96 if isinstance(renderer, gtk.CellRendererToggle):
97 renderer.set_property('activatable', True)
98 else:
99 renderer.set_property('editable', True)
100 renderer.connect_after('editing-started', send_keys, treeview)
101 # renderer.connect_after('editing-canceled', self.editing_canceled)
102 else:
103 if isinstance(renderer, gtk.CellRendererToggle):
104 renderer.set_property('activatable', False)
106 col = gtk.TreeViewColumn(fields[fname]['string'], renderer)
107 col.name = fname
108 col._type = fields[fname]['type']
109 col.set_cell_data_func(renderer, cell.setter)
110 col.set_clickable(True)
111 twidth = {
112 'integer': 60,
113 'float': 80,
114 'float_time': 80,
115 'date': 70,
116 'datetime': 120,
117 'selection': 90,
118 'char': 100,
119 'one2many': 50,
120 'many2many': 50,
121 'boolean': 20,
123 if 'width' in fields[fname]:
124 width = int(fields[fname]['width'])
125 else:
126 width = twidth.get(fields[fname]['type'], 100)
127 col.set_min_width(width)
128 col.connect('clicked', sort_model, treeview)
129 col.set_resizable(True)
130 #col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
131 visval = eval(fields[fname].get('invisible', 'False'), {'context':self.screen.context})
132 col.set_visible(not visval)
133 n = treeview.append_column(col)
134 if 'sum' in fields[fname] and fields[fname]['type'] \
135 in ('integer', 'float', 'float_time'):
136 label = gtk.Label()
137 label.set_use_markup(True)
138 label_str = fields[fname]['sum'] + ': '
139 label_bold = bool(int(fields[fname].get('sum_bold', 0)))
140 if label_bold:
141 label.set_markup('<b>%s</b>' % label_str)
142 else:
143 label.set_markup(label_str)
144 label_sum = gtk.Label()
145 label_sum.set_use_markup(True)
146 dict_widget[n] = (fname, label, label_sum,
147 fields.get('digits', (16,2))[1], label_bold)
148 return treeview, dict_widget, [], on_write
150 class UnsettableColumn(Exception):
151 pass
153 class Cell(object):
154 def __new__(self, type):
155 klass = CELLTYPES.get(type, CELLTYPES['char'])
156 return klass
159 class Char(object):
160 def __init__(self, field_name, treeview=None, attrs=None, window=None):
161 self.field_name = field_name
162 self.attrs = attrs or {}
163 self.renderer = gtk.CellRendererText()
164 self.treeview = treeview
165 if not window:
166 window = service.LocalService('gui.main').window
167 self.window = window
169 def setter(self, column, cell, store, iter):
170 model = store.get_value(iter, 0)
171 text = self.get_textual_value(model)
172 cell.set_property('text', text)
173 color = self.get_color(model)
174 cell.set_property('foreground', str(color))
175 if self.attrs['type'] in ('float', 'integer', 'boolean'):
176 align = 1
177 else:
178 align = 0
179 if self.treeview.editable:
180 field = model[self.field_name]
181 if not field.get_state_attrs(model).get('valid', True):
182 cell.set_property('background', common.colors.get('invalid', 'white'))
183 elif bool(int(field.get_state_attrs(model).get('required', 0))):
184 cell.set_property('background', common.colors.get('required', 'white'))
185 cell.set_property('xalign', align)
187 def get_color(self, model):
188 to_display = ''
189 for color, expr in self.treeview.colors.items():
190 if model.expr_eval(expr, check_load=False):
191 to_display = color
192 break
193 return to_display or 'black'
195 def open_remote(self, model, create, changed=False, text=None):
196 raise NotImplementedError
198 def get_textual_value(self, model):
199 return model[self.field_name].get_client(model) or ''
201 def value_from_text(self, model, text):
202 return text
204 class Int(Char):
206 def value_from_text(self, model, text):
207 return int(text)
209 def get_textual_value(self, model):
210 return locale.format('%d',
211 model[self.field_name].get_client(model) or 0, True)
213 class Boolean(Int):
215 def __init__(self, *args):
216 super(Boolean, self).__init__(*args)
217 self.renderer = gtk.CellRendererToggle()
218 self.renderer.connect('toggled', self._sig_toggled)
220 def get_textual_value(self, model):
221 return model[self.field_name].get_client(model) or 0
223 def setter(self, column, cell, store, iter):
224 model = store.get_value(iter, 0)
225 value = self.get_textual_value(model)
226 cell.set_active(bool(value))
228 def _sig_toggled(self, renderer, path):
229 store = self.treeview.get_model()
230 model = store.get_value(store.get_iter(path), 0)
231 field = model[self.field_name]
232 if not field.get_state_attrs(model).get('readonly', False):
233 value = model[self.field_name].get_client(model)
234 model[self.field_name].set_client(model, int(not value))
235 self.treeview.set_cursor(path)
236 return True
239 class GenericDate(Char):
241 def get_textual_value(self, model):
242 value = model[self.field_name].get_client(model)
243 if not value:
244 return ''
245 date = time.strptime(value, self.server_format)
246 return time.strftime(self.display_format, date)
248 def value_from_text(self, model, text):
249 if not text:
250 return False
251 try:
252 dt = time.strptime(text, self.display_format)
253 except:
254 try:
255 dt = list(time.localtime())
256 dt[2] = int(text)
257 dt = tuple(dt)
258 except:
259 return False
260 return time.strftime(self.server_format, dt)
262 if not hasattr(locale, 'nl_langinfo'):
263 locale.nl_langinfo = lambda *a: '%x'
265 if not hasattr(locale, 'D_FMT'):
266 locale.D_FMT = None
268 class Date(GenericDate):
269 server_format = '%Y-%m-%d'
270 display_format = locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y')
272 class Datetime(GenericDate):
273 server_format = '%Y-%m-%d %H:%M:%S'
274 display_format = locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y')+' %H:%M:%S'
276 def get_textual_value(self, model):
277 value = model[self.field_name].get_client(model)
278 if not value:
279 return ''
280 date = time.strptime(value, self.server_format)
281 if 'tz' in rpc.session.context:
282 try:
283 import pytz
284 lzone = pytz.timezone(rpc.session.context['tz'])
285 szone = pytz.timezone(rpc.session.timezone)
286 dt = DT.datetime(date[0], date[1], date[2], date[3], date[4], date[5], date[6])
287 sdt = szone.localize(dt, is_dst=True)
288 ldt = sdt.astimezone(lzone)
289 date = ldt.timetuple()
290 except:
291 pass
292 return time.strftime(self.display_format, date)
294 def value_from_text(self, model, text):
295 if not text:
296 return False
297 try:
298 date = time.strptime(text, self.display_format)
299 except:
300 try:
301 dt = list(time.localtime())
302 dt[2] = int(text)
303 date = tuple(dt)
304 except:
305 return False
306 if 'tz' in rpc.session.context:
307 try:
308 import pytz
309 lzone = pytz.timezone(rpc.session.context['tz'])
310 szone = pytz.timezone(rpc.session.timezone)
311 dt = DT.datetime(date[0], date[1], date[2], date[3], date[4], date[5], date[6])
312 ldt = lzone.localize(dt, is_dst=True)
313 sdt = ldt.astimezone(szone)
314 date = sdt.timetuple()
315 except:
316 pass
317 return time.strftime(self.server_format, date)
319 class Float(Char):
320 def get_textual_value(self, model):
321 interger, digit = self.attrs.get('digits', (16,2) )
322 return locale.format('%.'+str(digit)+'f',
323 model[self.field_name].get_client(model) or 0.0, True)
325 def value_from_text(self, model, text):
326 try:
327 return locale.atof(text)
328 except:
329 return 0.0
331 from mx.DateTime import DateTimeDelta
333 class FloatTime(Char):
334 def get_textual_value(self, model):
335 val = model[self.field_name].get_client(model)
336 t = '%02d:%02d' % (math.floor(abs(val)),round(abs(val)%1+0.01,2) * 60)
337 if val<0:
338 t = '-'+t
339 return t
341 def value_from_text(self, model, text):
342 try:
343 if text and ':' in text:
344 return round(int(text.split(':')[0]) + int(text.split(':')[1]) / 60.0,2)
345 else:
346 return locale.atof(text)
347 except:
348 pass
349 return 0.0
351 class M2O(Char):
353 def value_from_text(self, model, text):
354 if not text:
355 return False
357 relation = model[self.field_name].attrs['relation']
358 rpc = RPCProxy(relation)
360 domain = model[self.field_name].domain_get(model)
361 context = model[self.field_name].context_get(model)
363 names = rpc.name_search(text, domain, 'ilike', context)
364 if len(names) != 1:
365 return self.search_remote(relation, [x[0] for x in names],
366 domain=domain, context=context)[0]
367 return names[0]
369 def open_remote(self, model, create=True, changed=False, text=None):
370 modelfield = model.mgroup.mfields[self.field_name]
371 relation = modelfield.attrs['relation']
373 domain=modelfield.domain_get(model)
374 context=modelfield.context_get(model)
375 if create:
376 id = None
377 elif not changed:
378 id = modelfield.get(model)
379 else:
380 rpc = RPCProxy(relation)
382 names = rpc.name_search(text, domain, 'ilike', context)
383 if len(names) == 1:
384 return True, names[0]
385 searched = self.search_remote(relation, [x[0] for x in names], domain=domain, context=context)
386 if searched[0]:
387 return True, searched
388 return False, False
389 dia = M2ODialog(relation, id, domain=domain, context=context,
390 window=self.window)
391 ok, value = dia.run()
392 dia.destroy()
393 if ok:
394 return True, value
395 else:
396 return False, False
398 def search_remote(self, relation, ids=[], domain=[], context={}):
399 rpc = RPCProxy(relation)
401 win = win_search(relation, sel_multi=False, ids=ids, context=context, domain=domain)
402 found = win.go()
403 if found:
404 return rpc.name_get([found[0]], context)[0]
405 else:
406 return False, None
409 class O2M(Char):
410 def get_textual_value(self, model):
411 return '( '+str(len(model[self.field_name].get_client(model).models)) + ' )'
413 def value_from_text(self, model, text):
414 raise UnsettableColumn('Can not set column of type o2m')
417 class M2M(Char):
418 def get_textual_value(self, model):
419 value = model[self.field_name].get_client(model)
420 if value:
421 return '(%s)' % len(value)
422 else:
423 return '(0)'
425 def value_from_text(self, model, text):
426 if not text:
427 return []
428 if not (text[0]<>'('):
429 return model[self.field_name].get(model)
430 relation = model[self.field_name].attrs['relation']
431 rpc = RPCProxy(relation)
432 domain = model[self.field_name].domain_get(model)
433 context = model[self.field_name].context_get(model)
434 names = rpc.name_search(text, domain, 'ilike', context)
435 ids = [x[0] for x in names]
436 win = win_search(relation, sel_multi=True, ids=ids, context=context, domain=domain)
437 found = win.go()
438 return found or []
440 def open_remote(self, model, create=True, changed=False, text=None):
441 modelfield = model[self.field_name]
442 relation = modelfield.attrs['relation']
444 rpc = RPCProxy(relation)
445 context = model[self.field_name].context_get(model)
446 domain = model[self.field_name].domain_get(model)
447 if create:
448 if text and len(text) and text[0]<>'(':
449 domain.append(('name','=',text))
450 ids = rpc.search(domain)
451 if ids and len(ids)==1:
452 return True, ids
453 else:
454 ids = model[self.field_name].get_client(model)
455 win = win_search(relation, sel_multi=True, ids=ids, context=context, domain=domain)
456 found = win.go()
457 if found:
458 return True, found
459 else:
460 return False, None
462 class Selection(Char):
464 def __init__(self, *args):
465 super(Selection, self).__init__(*args)
466 self.renderer = gtk.CellRendererCombo()
467 selection_data = gtk.ListStore(str, str)
468 for x in self.attrs.get('selection', []):
469 selection_data.append(x)
470 self.renderer.set_property('model', selection_data)
471 self.renderer.set_property('text-column', 1)
473 def get_textual_value(self, model):
474 selection = dict(model[self.field_name].attrs['selection'])
475 return selection.get(model[self.field_name].get(model), '')
477 def value_from_text(self, model, text):
478 selection = model[self.field_name].attrs['selection']
479 res = False
480 for val, txt in selection:
481 if txt[:len(text)].lower() == text.lower():
482 if len(txt) == len(text):
483 return val
484 res = val
485 return res
487 CELLTYPES = {
488 'char': Char,
489 'many2one': M2O,
490 'date': Date,
491 'one2many': O2M,
492 'many2many': M2M,
493 'selection': Selection,
494 'float': Float,
495 'float_time': FloatTime,
496 'integer': Int,
497 'datetime': Datetime,
498 'boolean': Boolean,
501 # vim:noexpandtab: