Merge lp:~openerp-commiter/openobject-client/client-image-widget into lp:openobject-client

Proposed by Nicolas DS
Status: Needs review
Proposed branch: lp:~openerp-commiter/openobject-client/client-image-widget
Merge into: lp:openobject-client
Diff against target: 723 lines (+583/-8) (has conflicts)
8 files modified
bin/widget/model/field.py (+1/-0)
bin/widget/view/common_gtk.py (+193/-0)
bin/widget/view/form_gtk/image.py (+18/-2)
bin/widget/view/form_gtk/many2one_image.py (+202/-0)
bin/widget/view/form_gtk/parser.py (+5/-5)
bin/widget/view/tree_gtk/image.py (+64/-0)
bin/widget/view/tree_gtk/many2one_image.py (+81/-0)
bin/widget/view/tree_gtk/parser.py (+19/-1)
Text conflict in bin/widget/view/form_gtk/image.py
Text conflict in bin/widget/view/tree_gtk/parser.py
To merge this branch: bzr merge lp:~openerp-commiter/openobject-client/client-image-widget
Reviewer Review Type Date Requested Status
Christophe Simonis (OpenERP) Needs Fixing
Stephane Wirtel (OpenERP) Pending
Review via email: mp+23325@code.launchpad.net

Description of the change

* Add image widget for Tree.image (and so for Form.M2M too), Tree.M2O_image, Form.M2O_image
* Add more image format support.

Pictures of those new widgets in action:
* http://zyphos.be/openobject/img_widget_tree.jpg (Tree.image)
* http://zyphos.be/openobject/M2O_img_widget_tree.jpg (Tree.many2one_image)
* http://zyphos.be/openobject/M2O_img_widget_form.jpg (Form.many2one_image)

See branch description for more details.

To post a comment you must log in.
Revision history for this message
Christophe Simonis (OpenERP) (kangol) wrote :

I don't get it. I don't see why you need to define a new widget ? Is keeping widget="image" not enough, and just handle the case where this is display in tree view? Beeing displayed into a x2many [1] is not a special case.

[1] this is a one2many, not a many2one. And only for this error, the merge is in "Needs Fixing"

review: Needs Fixing
Revision history for this message
Nicolas DS (zyphos) wrote :

In GTK client all widget are separated by view. And the "Tree.image" does NOT exist for now in GTK client.

This branch is keeping widget="image" for image in Tree view, Form view and many2many in Form view (of course it is a tree view embedded in a screen view that acts as a form widget.).

In GTK client, "Tree view" widget are a lot different from "Form" one indeed:
- You need to define a new kind of cell renderer for special content. (It's not as easy as Form view)
- GTK treeview widget is asking a LOT of time to refresh row and column. (2 times to show the widget and then each time the mouse pointer get over, get out of any cell.)

In any refresh with the standard widget you need to: get the image data, compute the pixbuf (GTK image), then rescale the pixbuf to fit into screen. Those operations are very CPU/bandwidth intensive, that's why those widget need to be cached (for 120sec defined in bin/widget/view/common_gtk.py @line 137).

The problem with caching is the fact that if the image change in DB, the client will see the change maximum 120 sec further, and not in real time. Real time is still working for the basic "Form.image" widget.

At first I tried to use the widget="image" for many2one, but it's impossible in widget definition in openobject to know where type of field data come from.
Indeed widget python code is called from:
client/bin/widget/view/form_gtk/parser.py @line 416
widget_act = widgets_type[type][0](self.window, self.parent, model, fields[name])

and "type" is not alway the original field type, indeed:
@line 401
type = attrs.get('widget', fields[name]['type'])

and the many2one image widget needs to search for a binary field to get the data from.

I don't understand your one2many thing, I didn't made any one2many widget, if you look into source you will see that many2one_image is inheriting from the many2one class. So even if the many2one original class is a one2many I am lost.

The only fixing I see could be a different caching time or avoiding this caching time. But the best solution would be to create some kind of "push on field content change" from server to the client. But this feature needs a persistent connection to the server.
http://en.wikipedia.org/wiki/Push_technology

Or another one is to store hash of binary fields to quickly see if the image content has changed without eating lot of resource (CPU/bandwidth).

Don't forget to have a look at branch description for more information how this branch is working:
https://code.launchpad.net/~openerp-commiter/openobject-client/client-image-widget

Thank you for your review.

Revision history for this message
Christophe Simonis (OpenERP) (kangol) wrote :

After seeing you example module, I see what you try to do with the many2one_image.
You are doing it wrong. You have to create a related field on your object and use it with widget="image"

I modify your code (untested):
http://sprunge.us/MhFQ?python
http://sprunge.us/EfKA?xml

review: Needs Fixing
Revision history for this message
Nicolas DS (zyphos) wrote :

You are right for the Tree.many2one_image widget !
I must admit that I never used field.related before ;)
It acts like the one I created.

But for the Form.many2one_image the purpose isn't the same.

The Form.many2one_image represent a M2O object, so you can change the M2O relation directly, with your solution you will directly change the showed image itself and not the M2O relation.

1214. By Nicolas DS

[MERGE] from official trunk GTK client

1215. By Nicolas DS

[IMP] get_pixbuf and get_thumbnail support multi id query

1216. By Nicolas DS

[FIX] image-widget: Typos + Making code more robust

1217. By Nicolas DS

[REF] Form.M2O_image widget

1218. By Nicolas DS

[IMP] Added clear button for the form.M2O_image widget

1219. By Nicolas DS

[IMP] form.M2O_image code clean up

Unmerged revisions

1219. By Nicolas DS

[IMP] form.M2O_image code clean up

1218. By Nicolas DS

[IMP] Added clear button for the form.M2O_image widget

1217. By Nicolas DS

[REF] Form.M2O_image widget

1216. By Nicolas DS

[FIX] image-widget: Typos + Making code more robust

1215. By Nicolas DS

[IMP] get_pixbuf and get_thumbnail support multi id query

1214. By Nicolas DS

[MERGE] from official trunk GTK client

1213. By Nicolas DS

[FIX] Bug due to model field change

1212. By Nicolas DS

[FIX] M2O image find and get

1211. By Nicolas DS

[FIX] Case when M2O relation is empty

1210. By Nicolas DS

[MERGE] From trunk

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bin/widget/model/field.py'
2--- bin/widget/model/field.py 2010-10-05 07:05:05 +0000
3+++ bin/widget/model/field.py 2010-10-06 13:46:15 +0000
4@@ -496,6 +496,7 @@
5 'integer' : IntegerField,
6 'float' : FloatField,
7 'many2one' : M2OField,
8+ 'many2one_image' : M2OField,
9 'many2many' : M2MField,
10 'one2many' : O2MField,
11 'reference' : ReferenceField,
12
13=== added file 'bin/widget/view/common_gtk.py'
14--- bin/widget/view/common_gtk.py 1970-01-01 00:00:00 +0000
15+++ bin/widget/view/common_gtk.py 2010-10-06 13:46:15 +0000
16@@ -0,0 +1,193 @@
17+# -*- coding: utf-8 -*-
18+##############################################################################
19+#
20+# OpenERP, Open Source Management Solution
21+# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
22+#
23+# This program is free software: you can redistribute it and/or modify
24+# it under the terms of the GNU Affero General Public License as
25+# published by the Free Software Foundation, either version 3 of the
26+# License, or (at your option) any later version.
27+#
28+# This program is distributed in the hope that it will be useful,
29+# but WITHOUT ANY WARRANTY; without even the implied warranty of
30+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31+# GNU Affero General Public License for more details.
32+#
33+# You should have received a copy of the GNU Affero General Public License
34+# along with this program. If not, see <http://www.gnu.org/licenses/>.
35+#
36+##############################################################################
37+
38+import collections
39+import base64
40+import time
41+import gtk
42+import rpc
43+import common
44+
45+NOIMAGE = file(common.terp_path_pixmaps("noimage.png"), 'rb').read()
46+
47+def make_pixbuf(data):
48+ ''' Generate a pixbuf (gnome image) from a image file data
49+ Warning this processing is slow and should be cached'''
50+ if not data:
51+ data = NOIMAGE
52+ else:
53+ data = base64.decodestring(data)
54+ pixbuf = None
55+ loader = gtk.gdk.PixbufLoader()
56+ try:
57+ loader.write(data, len(data))
58+ pixbuf = loader.get_pixbuf()
59+ except:
60+ pass
61+ if not pixbuf:
62+ loader.close()
63+ loader = gtk.gdk.PixbufLoader('png')
64+ loader.write(NOIMAGE, len(NOIMAGE))
65+ pixbuf = loader.get_pixbuf()
66+
67+ loader.close()
68+ return pixbuf
69+
70+def thumbnail_pixbuf(pixbuf, to_width, to_height):
71+ ''' Make a thumbnail of the image according to dimensions provided
72+ Warning this processing is slow and should be cached'''
73+ img_height = pixbuf.get_height()
74+ if img_height > to_height:
75+ height = to_height
76+ else:
77+ height = img_height
78+
79+ img_width = pixbuf.get_width()
80+ if img_width > to_width:
81+ width = to_width
82+ else:
83+ width = img_width
84+
85+ if (img_width / width) < (img_height / height):
86+ width = float(img_width) / float(img_height) * float(height)
87+ else:
88+ height = float(img_height) / float(img_width) * float(width)
89+
90+ return pixbuf.scale_simple(int(width), int(height), gtk.gdk.INTERP_BILINEAR)
91+
92+class CacheDict(collections.MutableMapping):
93+ ''' Act like a dict but with timeout
94+ @param timeout: timeout in seconds
95+ '''
96+ def __init__(self, timeout=30):
97+ self.cache = {}
98+ self.timeout = int(timeout)
99+ self.last_check = time.time()
100+
101+ def __setitem__(self, key, value):
102+ if not self.timeout:
103+ return
104+ self.check_timeout()
105+ self.cache[key] = (value, time.time())
106+
107+ def __getitem__(self, key):
108+ if key not in self.cache:
109+ raise KeyError
110+ return self.cache[key][0]
111+
112+ def __delitem__(self, key):
113+ if key not in self.cache:
114+ raise KeyError
115+ del self.cache[key]
116+
117+ def keys(self):
118+ return self.cache.keys()
119+
120+ def __iter__(self):
121+ for key in self.cache:
122+ yield key
123+
124+ def __len__(self):
125+ return len(self.cache)
126+
127+ def check_timeout(self):
128+ ''' Remove expired cache '''
129+ now = time.time()
130+ if now < self.last_check + self.timeout / 10:
131+ return
132+ ct = now - self.timeout # Critical time
133+ key2del = [k for k in self.cache if self.cache[k][1] < ct]
134+ for key in key2del:
135+ self.cache.pop(key)
136+
137+CACHE_TIME_OUT = 120 # In second
138+pixbuf_cache = CacheDict(CACHE_TIME_OUT)
139+
140+
141+def get_pixbuf(model, field, ids):
142+ ''' Retrieve the data and convert to pixbuf if possible
143+ @param model: The model that the image come from. (Ie: res.images)
144+ @param field: The column of the model. (Ie: img)
145+ @param ids: Id of records row in database (int or list of int)
146+ @return pixbuf or dict of pixbuf
147+ '''
148+ result = {}
149+ is_int = isinstance(ids, int)
150+ if is_int:
151+ ids = [ids]
152+ cache_ids = dict([(id, "%s[%s][%d]" % (model, field, id)) for id in ids])
153+ ids2query = []
154+ for id in ids:
155+ if cache_ids[id] in pixbuf_cache:
156+ result[id] = pixbuf_cache[cache_ids[id]]
157+ else:
158+ ids2query.append(id)
159+
160+ if ids2query:
161+ datas = rpc.session.rpc_exec_auth('/object', 'execute', model, 'read',
162+ ids2query, ['id', field])
163+ for res in datas:
164+ id = res['id']
165+ pixbuf = make_pixbuf(res[field])
166+ pixbuf_cache[cache_ids[id]] = pixbuf
167+ result[id] = pixbuf
168+
169+ # Complete result table
170+ for id in ids:
171+ if id not in result:
172+ result[id] = None
173+
174+ if is_int:
175+ return result[0]
176+ return result
177+
178+def get_thumbnail(model, field, ids, to_width, to_height):
179+ ''' Resize Image to fit width, height cache result to be used later.
180+ @param model: The model that the image come from. (Ie: res.images)
181+ @param field: The column of the resource. (Ie: img)
182+ @param ids: Id of records row in database. (int or list of int)
183+ @param to_width: Maximum width in pixel.
184+ @param to_height: Maximum height in pixel.
185+ @return pixbuf or dict of pixbuf
186+ '''
187+ if not ids:
188+ return None
189+ result = {}
190+ is_int = isinstance(ids, int)
191+ if is_int:
192+ ids = [ids]
193+
194+ ids2query = []
195+ cache_ids = dict([(id, "%s[%s][%d]-%dx%d" % (model, field, id, to_width, to_height)) for id in ids])
196+ for id in ids:
197+ if cache_ids[id] in pixbuf_cache:
198+ result[id] = pixbuf_cache[cache_ids[id]]
199+ else:
200+ ids2query.append(id)
201+ if ids2query:
202+ pixbufs = get_pixbuf(model, field, ids2query)
203+ for id, pixbuf in pixbufs.iteritems():
204+ thumbnail = thumbnail_pixbuf(pixbuf, to_width, to_height)
205+ pixbuf_cache[cache_ids[id]] = thumbnail
206+ result[id] = thumbnail
207+ if is_int:
208+ return result[ids[0]]
209+ return result
210
211=== modified file 'bin/widget/view/form_gtk/image.py'
212--- bin/widget/view/form_gtk/image.py 2010-09-08 06:30:55 +0000
213+++ bin/widget/view/form_gtk/image.py 2010-10-06 13:46:15 +0000
214@@ -31,9 +31,13 @@
215 import interface
216 import tempfile
217 import urllib
218+<<<<<<< TREE
219
220 NOIMAGE = file(common.terp_path_pixmaps("noimage.png"), 'rb').read()
221 REQUIRED_IMG = file(common.terp_path_pixmaps("image_required.png"), 'rb').read()
222+=======
223+from widget.view import common_gtk
224+>>>>>>> MERGE-SOURCE
225
226
227 class image_wid(interface.widget_interface):
228@@ -106,9 +110,16 @@
229
230 filter_image = gtk.FileFilter()
231 filter_image.set_name(_('Images'))
232- for mime in ("image/png", "image/jpeg", "image/gif"):
233+ supported_mime = ('image/bmp', 'image/gif', 'image/jpeg', 'image/tiff',
234+ 'image/x-cmu-raster', 'image/x-icon',
235+ 'image/x-portable-anymap', 'image/x-xbitmap',
236+ 'image/x-xpixmap', 'image/png')
237+ supported_pat = ('*.ani', '*.bmp', '*.gif', '*.ico', '*.jpe', '*.jpeg',
238+ '*.jpg', '*.pcx', '*.png', '*.pnm', '*.ras', '*.tga',
239+ '*.tif', '*.tiff', '*.xbm', '*.xpm')
240+ for mime in supported_mime:
241 filter_image.add_mime_type(mime)
242- for pat in ("*.png", "*.jpg", "*.gif", "*.tif", "*.xpm"):
243+ for pat in supported_pat:
244 filter_image.add_pattern(pat)
245
246 filename = common.file_selection(_('Open...'), parent=self._window, preview=True,
247@@ -164,6 +175,7 @@
248 self.update_img()
249
250 def update_img(self):
251+<<<<<<< TREE
252 if not self._value:
253 if self._set_required_img:
254 data = REQUIRED_IMG
255@@ -207,6 +219,10 @@
256 height = float(img_height) / float(img_width) * float(width)
257
258 scaled = pixbuf.scale_simple(int(width), int(height), gtk.gdk.INTERP_BILINEAR)
259+=======
260+ pixbuf = common_gtk.make_pixbuf(self._value)
261+ scaled = common_gtk.thumbnail_pixbuf(pixbuf, self.width, self.height)
262+>>>>>>> MERGE-SOURCE
263 self.image.set_from_pixbuf(scaled)
264
265 def display(self, model, model_field):
266
267=== added file 'bin/widget/view/form_gtk/many2one_image.py'
268--- bin/widget/view/form_gtk/many2one_image.py 1970-01-01 00:00:00 +0000
269+++ bin/widget/view/form_gtk/many2one_image.py 2010-10-06 13:46:15 +0000
270@@ -0,0 +1,202 @@
271+# -*- coding: utf-8 -*-
272+##############################################################################
273+#
274+# OpenERP, Open Source Management Solution
275+# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
276+#
277+# This program is free software: you can redistribute it and/or modify
278+# it under the terms of the GNU Affero General Public License as
279+# published by the Free Software Foundation, either version 3 of the
280+# License, or (at your option) any later version.
281+#
282+# This program is distributed in the hope that it will be useful,
283+# but WITHOUT ANY WARRANTY; without even the implied warranty of
284+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
285+# GNU Affero General Public License for more details.
286+#
287+# You should have received a copy of the GNU Affero General Public License
288+# along with this program. If not, see <http://www.gnu.org/licenses/>.
289+#
290+##############################################################################
291+
292+import gobject
293+import gtk
294+
295+import interface
296+import common
297+
298+import widget
299+from widget.screen import Screen
300+
301+from modules.gui.window.win_search import win_search
302+import rpc
303+
304+import service
305+
306+import many2one
307+from widget.view import common_gtk
308+
309+class many2one_image(many2one.many2one):
310+ ''' Binary many2one image form widget
311+ Ie:
312+ <field name="img_id" widget="many2one_image" fieldname="img"/>
313+
314+ img_field (optional): field of the many2one relation to show in widget
315+ Otherwise it will take the first binary field.
316+ '''
317+ def __init__(self, window, parent, model, attrs={}):
318+ interface.widget_interface.__init__(self, window, parent, model, attrs)
319+
320+ self.widget = gtk.VBox(spacing=3)
321+
322+ self.image = gtk.Image()
323+ self.height = int(attrs.get('img_height', 100))
324+ self.width = int(attrs.get('img_width', 300))
325+ self.model_field = False
326+
327+ self.widget.pack_start(self.image, expand=True, fill=True)
328+
329+ alignment = gtk.Alignment(xalign=0.5, yalign=0.5)
330+ hbox = gtk.HBox()
331+
332+ self.but_open = many2one.Button('gtk-open', self.sig_edit,
333+ _('Open this resource'))
334+ hbox.pack_start(self.but_open, padding=2, expand=False, fill=False)
335+
336+ self.but_find = many2one.Button('gtk-find', self.sig_find,
337+ _('Search a resource'))
338+ hbox.pack_start(self.but_find, padding=2, expand=False, fill=False)
339+
340+ self.but_remove = many2one.Button('gtk-clear', self.sig_remove,
341+ _('Clear'))
342+ hbox.pack_start(self.but_remove, padding=2, expand=False, fill=False)
343+
344+ alignment.add(hbox)
345+ self.widget.pack_start(alignment, expand=False, fill=False)
346+
347+ self.value_on_field = ''
348+ self.ok = True
349+ self._readonly = False
350+ self.model_type = attrs['relation']
351+ self._menu_loaded = False
352+ self._menu_entries.append((None, None, None))
353+ self._menu_entries.append((_('Action'), lambda x: self.click_and_action('client_action_multi'), 0))
354+ self._menu_entries.append((_('Report'), lambda x: self.click_and_action('client_print_multi'), 0))
355+
356+ def _readonly_set(self, value):
357+ self._readonly = value
358+ #self.but_new.set_sensitive(not value)
359+
360+ def _color_widget(self):
361+ return self.image
362+
363+ def sig_find(self, widget, event=None, leave=True):
364+ self.ok = False
365+ if not self._readonly:
366+ domain = self.model_field.domain_get(self._view.model)
367+ context = self.model_field.context_get(self._view.model)
368+ #self.wid_text.grab_focus()
369+ name_search = ''
370+ relation = self.attrs['relation']
371+ ids = rpc.session.rpc_exec_auth('/object', 'execute', relation,
372+ 'name_search', name_search, domain,
373+ 'ilike', context)
374+ if (len(ids) == 1) and leave:
375+ self.model_field.set_client(self._view.model, ids[0],
376+ force_change=True)
377+ self.display(self._view.model, self.model_field)
378+ self.ok = True
379+ return True
380+
381+ win = win_search(self.attrs['relation'], sel_multi=False,
382+ ids=map(lambda x: x[0], ids), context=context,
383+ domain=domain, parent=self._window)
384+ ids = win.go()
385+ if ids:
386+ name = rpc.session.rpc_exec_auth('/object', 'execute', relation,
387+ 'name_get', [ids[0]],
388+ rpc.session.context)[0]
389+ self.model_field.set_client(self._view.model, name,
390+ force_change=True)
391+ self.display(self._view.model, self.model_field)
392+ self.ok = True
393+
394+ def sig_edit(self, widget, event=None, leave=False):
395+ self.ok = False
396+ if not leave:
397+ domain = self.model_field.domain_get(self._view.model)
398+ context = self.model_field.context_get(self._view.model)
399+ m2o_value = self.model_field.get(self._view.model, False)
400+ relation_record_id = self.model_field.get(self._view.model)
401+ dia = many2one.dialog(self.attrs['relation'],
402+ relation_record_id,
403+ attrs=self.attrs, window=self._window,
404+ domain=domain, context=context)
405+ ok, value = dia.run()
406+ if ok:
407+ self.model_field.set_client(self._view.model, value,
408+ force_change=True)
409+ dia.destroy()
410+ self.display(self._view.model, self.model_field)
411+ self.ok = True
412+
413+ def sig_new(self, *args):
414+ domain = self.model_field.domain_get(self._view.model)
415+ context = self.model_field.context_get(self._view.model)
416+ dia = many2one.dialog(self.attrs['relation'], attrs=self.attrs,
417+ window=self._window, domain=domain,
418+ context=context)
419+ ok, value = dia.run()
420+ if ok:
421+ self.model_field.set_client(self._view.model, value)
422+ self.display(self._view.model, self.model_field)
423+ dia.destroy()
424+
425+ def sig_remove(self, widget=None):
426+ self.model_field.set_client(self._view.model, False)
427+ self.display(self._view.model, False)
428+
429+ def sig_key_press(self, widget, event, *args):
430+ if event.keyval == gtk.keysyms.F1:
431+ self.sig_new(widget, event)
432+ elif event.keyval == gtk.keysyms.F2:
433+ self.sig_activate(widget, event)
434+ elif event.keyval == gtk.keysyms.F3:
435+ self.sig_edit(widget, event)
436+ elif event.keyval == gtk.keysyms.Tab:
437+ self.sig_activate(widget, event, leave=True)
438+ return True
439+ return False
440+
441+ def display(self, model, model_field):
442+ if not model_field:
443+ self.ok = False
444+ return False
445+ self.model_field = model_field
446+ interface.widget_interface.display(self, model, model_field)
447+ self.ok = False
448+ # retrieve image data
449+ m2o_value = model.value.get(model_field.name, False)
450+ relation_record_id = m2o_value and m2o_value[0]
451+ relation = model_field.attrs['relation']
452+ img_field_name = model_field.attrs.get('fieldname', False)
453+ if not img_field_name:
454+ # if not img_field is set get the first binary field
455+ fields = rpc.session.rpc_exec_auth('/object', 'execute', relation,
456+ 'fields_get')
457+ for field_name in fields:
458+ if fields[field_name]['type'] == 'binary':
459+ img_field_name = field_name
460+ break
461+
462+ if not img_field_name:
463+ return
464+
465+ thumbnail = common_gtk.get_thumbnail(relation, img_field_name,
466+ relation_record_id, self.width,
467+ self.height)
468+ self.image.set_from_pixbuf(thumbnail)
469+ self.ok = True
470+
471+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
472+
473
474=== modified file 'bin/widget/view/form_gtk/parser.py'
475--- bin/widget/view/form_gtk/parser.py 2010-09-23 09:18:56 +0000
476+++ bin/widget/view/form_gtk/parser.py 2010-10-06 13:46:15 +0000
477@@ -462,11 +462,9 @@
478 continue
479
480 fields[name]['name'] = name
481- if 'saves' in attrs:
482- fields[name]['saves'] = attrs['saves']
483-
484- if 'filename' in attrs:
485- fields[name]['filename'] = attrs['filename']
486+ for attr in ['saves', 'filename', 'fieldname']:
487+ if attr in attrs:
488+ fields[name][attr] = attrs[attr]
489
490 if fields[name]['type'] == 'many2one' and 'search_mode' in attrs:
491 fields[name]['search_mode'] = attrs['search_mode']
492@@ -907,6 +905,7 @@
493 #import one2many
494 import many2many
495 import many2one
496+import many2one_image
497 import selection
498 import one2many_list
499 import picture
500@@ -937,6 +936,7 @@
501 'one2many_list': (one2many_list.one2many_list, 1, True, True),
502 'many2many': (many2many.many2many, 1, True, True),
503 'many2one': (many2one.many2one, 1, False, False),
504+ 'many2one_image': (many2one_image.many2one_image, 1, False, False),
505 'email' : (url.email, 1, False, False),
506 'url' : (url.url, 1, False, False),
507 'callto' : (url.callto, 1, False, False),
508
509=== added file 'bin/widget/view/tree_gtk/image.py'
510--- bin/widget/view/tree_gtk/image.py 1970-01-01 00:00:00 +0000
511+++ bin/widget/view/tree_gtk/image.py 2010-10-06 13:46:15 +0000
512@@ -0,0 +1,64 @@
513+# -*- coding: utf-8 -*-
514+##############################################################################
515+#
516+# OpenERP, Open Source Management Solution
517+# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
518+#
519+# This program is free software: you can redistribute it and/or modify
520+# it under the terms of the GNU Affero General Public License as
521+# published by the Free Software Foundation, either version 3 of the
522+# License, or (at your option) any later version.
523+#
524+# This program is distributed in the hope that it will be useful,
525+# but WITHOUT ANY WARRANTY; without even the implied warranty of
526+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
527+# GNU Affero General Public License for more details.
528+#
529+# You should have received a copy of the GNU Affero General Public License
530+# along with this program. If not, see <http://www.gnu.org/licenses/>.
531+#
532+##############################################################################
533+
534+
535+import gtk
536+
537+import service
538+import common
539+from widget.view import common_gtk
540+
541+class Image(object):
542+ def __init__(self, field_name, treeview=None, attrs=None, window=None):
543+ self.field_name = field_name
544+ self.attrs = attrs or {}
545+ self.renderer = gtk.CellRendererPixbuf()
546+ self.editable = attrs.get('editable',False)
547+ self.height = int(attrs.get('img_height', 33))
548+ self.width = int(attrs.get('img_width', 100))
549+ self.treeview = treeview
550+ if not window:
551+ window = service.LocalService('gui.main').window
552+ self.window = window
553+ self.pixbuf_cache = {} # warning caching consequence is memory eated.
554+
555+ def setter(self, column, cell, store, iter):
556+ model_record = store.get_value(iter, 0)
557+ resource = model_record.resource
558+ resource_id = model_record.id
559+ field_name = column.name
560+ thumbnail = common_gtk.get_thumbnail(resource, field_name,
561+ resource_id, self.width,
562+ self.height)
563+
564+ cell.set_property('pixbuf', thumbnail)
565+
566+ def open_remote(self, model, create, changed=False, text=None):
567+ raise NotImplementedError
568+
569+ def _get_data(self, model_record):
570+ return model_record[self.field_name].get_client(model_record)
571+
572+ def get_textual_value(self, model):
573+ return ''
574+
575+ def value_from_text(self, model, text):
576+ return text
577\ No newline at end of file
578
579=== added file 'bin/widget/view/tree_gtk/many2one_image.py'
580--- bin/widget/view/tree_gtk/many2one_image.py 1970-01-01 00:00:00 +0000
581+++ bin/widget/view/tree_gtk/many2one_image.py 2010-10-06 13:46:15 +0000
582@@ -0,0 +1,81 @@
583+# -*- coding: utf-8 -*-
584+##############################################################################
585+#
586+# OpenERP, Open Source Management Solution
587+# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
588+#
589+# This program is free software: you can redistribute it and/or modify
590+# it under the terms of the GNU Affero General Public License as
591+# published by the Free Software Foundation, either version 3 of the
592+# License, or (at your option) any later version.
593+#
594+# This program is distributed in the hope that it will be useful,
595+# but WITHOUT ANY WARRANTY; without even the implied warranty of
596+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
597+# GNU Affero General Public License for more details.
598+#
599+# You should have received a copy of the GNU Affero General Public License
600+# along with this program. If not, see <http://www.gnu.org/licenses/>.
601+#
602+##############################################################################
603+
604+
605+import gtk
606+
607+import service
608+import common
609+from widget.view import common_gtk
610+import rpc
611+
612+class many2one_image(object):
613+ ''' Binary many2one image tree widget
614+ Ie:
615+ <field name="img_id" widget="many2one_image" fieldname="img"/>
616+
617+ fieldname (optional): field of the many2one relation to show in widget
618+ Otherwise it will take the first binary field.
619+ '''
620+ def __init__(self, field_name, treeview=None, attrs=None, window=None):
621+ self.field_name = field_name
622+ self.attrs = attrs or {}
623+ self.renderer = gtk.CellRendererPixbuf()
624+ self.editable = attrs.get('editable',False)
625+ self.height = int(attrs.get('img_height', 33))
626+ self.width = int(attrs.get('img_width', 100))
627+ self.treeview = treeview
628+ if not window:
629+ window = service.LocalService('gui.main').window
630+ self.window = window
631+
632+ self.img_field_name = self.attrs.get('fieldname', False)
633+ if not self.img_field_name:
634+ # if not img_field is set get the first binary field
635+ fields = rpc.session.rpc_exec_auth('/object', 'execute', self.attrs['relation'], 'fields_get')
636+ for field_name in fields:
637+ if fields[field_name]['type'] == 'binary':
638+ self.img_field_name = field_name
639+ break
640+
641+ def setter(self, column, cell, store, iter):
642+ if not self.img_field_name:
643+ return
644+
645+ model_record = store.get_value(iter, 0)
646+ m2o_value = model_record.value[self.field_name]
647+ relation_record_id = m2o_value and m2o_value[0]
648+ relation = self.attrs['relation']
649+
650+ thumbnail = common_gtk.get_thumbnail(relation, self.img_field_name,
651+ relation_record_id, self.width,
652+ self.height)
653+
654+ cell.set_property('pixbuf', thumbnail)
655+
656+ def open_remote(self, model, create, changed=False, text=None):
657+ raise NotImplementedError
658+
659+ def get_textual_value(self, model):
660+ return ''
661+
662+ def value_from_text(self, model, text):
663+ return text
664\ No newline at end of file
665
666=== modified file 'bin/widget/view/tree_gtk/parser.py'
667--- bin/widget/view/tree_gtk/parser.py 2010-09-30 06:52:47 +0000
668+++ bin/widget/view/tree_gtk/parser.py 2010-10-06 13:46:15 +0000
669@@ -131,7 +131,7 @@
670 if node.tag == 'field':
671 handler_id = False
672 fname = str(node_attrs['name'])
673- if fields[fname]['type'] in ('image', 'binary'):
674+ if fields[fname]['type'] in ('binary', ):
675 continue # not showed types
676 if fname == 'sequence':
677 treeview.sequence = True
678@@ -178,6 +178,7 @@
679 'integer': (60, 170),
680 'float': (80, 300),
681 'float_time': (80,150),
682+<<<<<<< TREE
683 'date': (70, False),
684 'datetime': (145, 145),
685 'selection': (90, 250),
686@@ -186,6 +187,17 @@
687 'many2many': (50, False),
688 'boolean': (20, 80),
689 'progressbar':(150, 200)
690+=======
691+ 'date': (70,100),
692+ 'datetime': (145,145),
693+ 'selection': (90,250),
694+ 'char': (100,False),
695+ 'one2many': (50,False),
696+ 'many2many': (50,False),
697+ 'boolean': (20,80),
698+ 'progressbar':(150,200),
699+ 'image': (20, 100)
700+>>>>>>> MERGE-SOURCE
701 }
702
703 if col._type not in twidth:
704@@ -697,6 +709,10 @@
705 return 0
706 #gobject.type_register(CellRendererButton)
707
708+
709+import image
710+import many2one_image
711+
712 CELLTYPES = {
713 'char': Char,
714 'many2one': M2O,
715@@ -711,6 +727,8 @@
716 'boolean': Boolean,
717 'progressbar': ProgressBar,
718 'button': CellRendererButton,
719+ 'image': image.Image,
720+ 'many2one_image': many2one_image.many2one_image,
721 }
722
723 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: