class DictWidget(Widget): def __init__(self, view, attrs): super(DictWidget, self).__init__(view, attrs) self.schema_model = attrs['schema_model'] self.keys = {} self.fields = {} self.buttons = {} self.rows = {} self.widget = gtk.Frame() self.widget.set_label(attrs.get('string', '')) self.widget.set_shadow_type(gtk.SHADOW_OUT) vbox = gtk.VBox() self.widget.add(vbox) self.table = gtk.Table(1, 3, homogeneous=False) self.table.set_col_spacings(0) self.table.set_row_spacings(0) self.table.set_border_width(0) vbox.pack_start(self.table, expand=True, fill=True) hbox = gtk.HBox() hbox.set_border_width(2) self.wid_text = PlaceholderEntry() self.wid_text.set_placeholder_text(_('Search')) self.wid_text.props.width_chars = 13 self.wid_text.connect('activate', self._sig_activate) hbox.pack_start(self.wid_text, expand=True, fill=True) if int(self.attrs.get('completion', 1)): self.wid_completion = get_completion(search=False, create=False) self.wid_completion.connect('match-selected', self._completion_match_selected) self.wid_text.set_completion(self.wid_completion) self.wid_text.connect('changed', self._update_completion) else: self.wid_completion = None self.but_add = gtk.Button() self.but_add.connect('clicked', self._sig_add) img_add = gtk.Image() img_add.set_from_stock('tryton-list-add', gtk.ICON_SIZE_SMALL_TOOLBAR) img_add.set_alignment(0.5, 0.5) self.but_add.add(img_add) self.but_add.set_relief(gtk.RELIEF_NONE) hbox.pack_start(self.but_add, expand=False, fill=False) hbox.set_focus_chain([self.wid_text]) vbox.pack_start(hbox, expand=True, fill=True) self.tooltips = Tooltips() self.tooltips.set_tip(self.but_add, _('Add value')) self.tooltips.enable() self._readonly = False self._record_id = None def _new_remove_btn(self): but_remove = gtk.Button() img_remove = gtk.Image() img_remove.set_from_stock('tryton-list-remove', gtk.ICON_SIZE_SMALL_TOOLBAR) img_remove.set_alignment(0.5, 0.5) but_remove.add(img_remove) but_remove.set_relief(gtk.RELIEF_NONE) return but_remove def _sig_activate(self, *args): if self.wid_text.get_editable(): self._sig_add() def _sig_add(self, *args): context = self.field.context_get(self.record) value = self.wid_text.get_text().decode('utf-8') domain = self.field.domain_get(self.record) def callback(result): if result: self.add_new_keys([r[0] for r in result]) self.wid_text.set_text('') win = WinSearch(self.schema_model, callback, sel_multi=True, context=context, domain=domain, new=False) win.screen.search_filter(quote(value)) win.show() def add_new_keys(self, ids): context = self.field.context_get(self.record) self.send_modified() try: new_fields = RPCExecute('model', self.schema_model, 'get_keys', ids, context=context) except RPCException: new_fields = [] focus = False for new_field in new_fields: if new_field['name'] not in self.fields: self.keys[new_field['name']] = new_field self.add_line(new_field['name']) if not focus: # Use idle add because it can be called from the callback # of WinSearch while the popup is still there gobject.idle_add( self.fields[new_field['name']].widget.grab_focus) focus = True def _sig_remove(self, button, key, modified=True): del self.fields[key] del self.buttons[key] for widget in self.rows[key]: self.table.remove(widget) widget.destroy() del self.rows[key] if modified: self.send_modified() self.set_value(self.record, self.field) def set_value(self, record, field): field.set_client(record, self.get_value()) def get_value(self): return dict( (key, widget.get_value()) for key, widget in self.fields.items()) @property def modified(self): if self.record and self.field: value = self.field.get_client(self.record) return any( widget.modified(value) for widget in self.fields.itervalues()) return False def _readonly_set(self, readonly): self._readonly = readonly self._set_button_sensitive() for widget in self.fields.values(): widget.set_readonly(readonly) self.wid_text.set_sensitive(not readonly) self.wid_text.set_editable(not readonly) def _set_button_sensitive(self): self.but_add.set_sensitive( bool(not self._readonly and self.attrs.get('create', True))) for button in self.buttons.itervalues(): button.set_sensitive( bool(not self._readonly and self.attrs.get('delete', True))) def add_line(self, key): self.fields[key] = DICT_ENTRIES[self.keys[key]['type_']](key, self) field = self.fields[key] alignment = gtk.Alignment(float(self.attrs.get('xalign', 0.0)), float(self.attrs.get('yalign', 0.5)), float(self.attrs.get('xexpand', 1.0)), float(self.attrs.get('yexpand', 1.0))) hbox = gtk.HBox() hbox.pack_start(field.widget, expand=field.expand, fill=field.fill) alignment.add(hbox) n_rows = self.table.props.n_rows self.table.resize(n_rows + 1, 3) if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL: text = _(':') + self.keys[key]['string'] else: text = self.keys[key]['string'] + _(':') label = gtk.Label(text) label.set_alignment(1., .5) self.table.attach(label, 0, 1, n_rows - 1, n_rows, xoptions=gtk.FILL, yoptions=False, xpadding=2) label.set_mnemonic_widget(field.widget) label.show() self.table.attach(alignment, 1, 2, n_rows - 1, n_rows, xoptions=gtk.FILL | gtk.EXPAND, yoptions=False, xpadding=2) alignment.show_all() remove_but = self._new_remove_btn() self.tooltips.set_tip(remove_but, _('Remove "%s"') % self.keys[key]['string']) self.table.attach(remove_but, 2, 3, n_rows - 1, n_rows, xoptions=gtk.FILL, yoptions=False, xpadding=2) remove_but.connect('clicked', self._sig_remove, key) remove_but.show_all() self.rows[key] = [label, alignment, remove_but] self.buttons[key] = remove_but def add_keys(self, keys): context = self.field.context_get(self.record) domain = self.field.domain_get(self.record) batchlen = min(10, CONFIG['client.limit']) for i in xrange(0, len(keys), batchlen): sub_keys = keys[i:i + batchlen] try: key_ids = RPCExecute('model', self.schema_model, 'search', [('name', 'in', sub_keys), domain], 0, CONFIG['client.limit'], None, context=context) except RPCException: key_ids = [] if not key_ids: continue try: values = RPCExecute('model', self.schema_model, 'get_keys', key_ids, context=context) except RPCException: values = [] if not values: continue self.keys.update({k['name']: k for k in values}) def display(self, record, field): super(DictWidget, self).display(record, field) if field is None: return record_id = record.id if record else None if record_id != self._record_id: for key in self.fields.keys(): self._sig_remove(None, key, modified=False) self._record_id = record_id value = field.get_client(record) if field else {} new_key_names = set(value.iterkeys()) - set(self.keys) if new_key_names: self.add_keys(list(new_key_names)) for key, val in sorted(value.iteritems()): if key not in self.keys: continue if key not in self.fields: self.add_line(key) widget = self.fields[key] widget.set_value(val) widget.set_readonly(self._readonly) for key in set(self.fields.keys()) - set(value.keys()): self._sig_remove(None, key, modified=False) self._set_button_sensitive() def _completion_match_selected(self, completion, model, iter_): record_id, = model.get(iter_, 1) self.add_new_keys([record_id]) self.wid_text.set_text('') completion_model = self.wid_completion.get_model() completion_model.clear() completion_model.search_text = self.wid_text.get_text() return True def _update_completion(self, widget): if not self.wid_text.get_editable(): return if not self.record: return update_completion(self.wid_text, self.record, self.field, self.schema_model)
class One2Many(Widget): expand = True def __init__(self, view, attrs): super(One2Many, self).__init__(view, attrs) self.widget = gtk.Frame() self.widget.set_shadow_type(gtk.SHADOW_NONE) self.widget.get_accessible().set_name(attrs.get('string', '')) vbox = gtk.VBox(homogeneous=False, spacing=2) self.widget.add(vbox) self._readonly = True self._required = False self._position = 0 self._length = 0 self.title_box = hbox = gtk.HBox(homogeneous=False, spacing=0) hbox.set_border_width(2) self.title = gtk.Label(attrs.get('string', '')) self.title.set_alignment(0.0, 0.5) hbox.pack_start(self.title, expand=True, fill=True) hbox.pack_start(gtk.VSeparator(), expand=False, fill=True) tooltips = common.Tooltips() but_switch = gtk.Button() tooltips.set_tip(but_switch, _('Switch')) but_switch.connect('clicked', self.switch_view) img_switch = gtk.Image() img_switch.set_from_stock('tryton-fullscreen', gtk.ICON_SIZE_SMALL_TOOLBAR) img_switch.set_alignment(0.5, 0.5) but_switch.add(img_switch) but_switch.set_relief(gtk.RELIEF_NONE) hbox.pack_start(but_switch, expand=False, fill=False) self.but_pre = gtk.Button() tooltips.set_tip(self.but_pre, _('Previous')) self.but_pre.connect('clicked', self._sig_previous) img_pre = gtk.Image() img_pre.set_from_stock('tryton-go-previous', gtk.ICON_SIZE_SMALL_TOOLBAR) img_pre.set_alignment(0.5, 0.5) self.but_pre.add(img_pre) self.but_pre.set_relief(gtk.RELIEF_NONE) hbox.pack_start(self.but_pre, expand=False, fill=False) self.label = gtk.Label('(0,0)') hbox.pack_start(self.label, expand=False, fill=False) self.but_next = gtk.Button() tooltips.set_tip(self.but_next, _('Next')) self.but_next.connect('clicked', self._sig_next) img_next = gtk.Image() img_next.set_from_stock('tryton-go-next', gtk.ICON_SIZE_SMALL_TOOLBAR) img_next.set_alignment(0.5, 0.5) self.but_next.add(img_next) self.but_next.set_relief(gtk.RELIEF_NONE) hbox.pack_start(self.but_next, expand=False, fill=False) hbox.pack_start(gtk.VSeparator(), expand=False, fill=True) self.focus_out = True self.wid_completion = None if attrs.get('add_remove'): self.wid_text = PlaceholderEntry() self.wid_text.set_placeholder_text(_('Search')) self.wid_text.set_property('width_chars', 13) self.wid_text.connect('focus-out-event', self._focus_out) hbox.pack_start(self.wid_text, expand=True, fill=True) if int(self.attrs.get('completion', 1)): access = common.MODELACCESS[attrs['relation']] self.wid_completion = get_completion( search=access['read'] and access['write'], create=attrs.get('create', True) and access['create']) self.wid_completion.connect('match-selected', self._completion_match_selected) self.wid_completion.connect('action-activated', self._completion_action_activated) self.wid_text.set_completion(self.wid_completion) self.wid_text.connect('changed', self._update_completion) self.but_add = gtk.Button() tooltips.set_tip(self.but_add, _('Add existing record')) self.but_add.connect('clicked', self._sig_add) img_add = gtk.Image() img_add.set_from_stock('tryton-list-add', gtk.ICON_SIZE_SMALL_TOOLBAR) img_add.set_alignment(0.5, 0.5) self.but_add.add(img_add) self.but_add.set_relief(gtk.RELIEF_NONE) hbox.pack_start(self.but_add, expand=False, fill=False) self.but_remove = gtk.Button() tooltips.set_tip(self.but_remove, _('Remove selected record')) self.but_remove.connect('clicked', self._sig_remove, True) img_remove = gtk.Image() img_remove.set_from_stock('tryton-list-remove', gtk.ICON_SIZE_SMALL_TOOLBAR) img_remove.set_alignment(0.5, 0.5) self.but_remove.add(img_remove) self.but_remove.set_relief(gtk.RELIEF_NONE) hbox.pack_start(self.but_remove, expand=False, fill=False) hbox.pack_start(gtk.VSeparator(), expand=False, fill=True) self.but_new = gtk.Button() tooltips.set_tip(self.but_new, _('Create a new record <F3>')) self.but_new.connect('clicked', self._sig_new) img_new = gtk.Image() img_new.set_from_stock('tryton-new', gtk.ICON_SIZE_SMALL_TOOLBAR) img_new.set_alignment(0.5, 0.5) self.but_new.add(img_new) self.but_new.set_relief(gtk.RELIEF_NONE) hbox.pack_start(self.but_new, expand=False, fill=False) self.but_open = gtk.Button() tooltips.set_tip(self.but_open, _('Edit selected record <F2>')) self.but_open.connect('clicked', self._sig_edit) img_open = gtk.Image() img_open.set_from_stock('tryton-open', gtk.ICON_SIZE_SMALL_TOOLBAR) img_open.set_alignment(0.5, 0.5) self.but_open.add(img_open) self.but_open.set_relief(gtk.RELIEF_NONE) hbox.pack_start(self.but_open, expand=False, fill=False) self.but_del = gtk.Button() tooltips.set_tip(self.but_del, _('Delete selected record <Del>')) self.but_del.connect('clicked', self._sig_remove, False) img_del = gtk.Image() img_del.set_from_stock('tryton-delete', gtk.ICON_SIZE_SMALL_TOOLBAR) img_del.set_alignment(0.5, 0.5) self.but_del.add(img_del) self.but_del.set_relief(gtk.RELIEF_NONE) hbox.pack_start(self.but_del, expand=False, fill=False) self.but_undel = gtk.Button() tooltips.set_tip(self.but_undel, _('Undelete selected record <Ins>')) self.but_undel.connect('clicked', self._sig_undelete) img_undel = gtk.Image() img_undel.set_from_stock('tryton-undo', gtk.ICON_SIZE_SMALL_TOOLBAR) img_undel.set_alignment(0.5, 0.5) self.but_undel.add(img_undel) self.but_undel.set_relief(gtk.RELIEF_NONE) hbox.pack_start(self.but_undel, expand=False, fill=False) if attrs.get('add_remove'): hbox.set_focus_chain([self.wid_text]) else: hbox.set_focus_chain([]) tooltips.enable() frame = gtk.Frame() frame.add(hbox) if attrs.get('expand_toolbar'): frame.set_shadow_type(gtk.SHADOW_NONE) else: frame.set_shadow_type(gtk.SHADOW_OUT) vbox.pack_start(frame, expand=False, fill=True) self.screen = Screen(attrs['relation'], mode=attrs.get('mode', 'tree,form').split(','), view_ids=attrs.get('view_ids', '').split(','), views_preload=attrs.get('views', {}), row_activate=self._on_activate, exclude_field=attrs.get('relation_field', None), limit=None) self.screen.pre_validate = bool(int(attrs.get('pre_validate', 0))) self.screen.signal_connect(self, 'record-message', self._sig_label) if self.attrs.get('group'): self.screen.signal_connect( self, 'current-record-changed', lambda screen, _: gobject.idle_add( self.group_sync, screen, screen.current_record)) vbox.pack_start(self.screen.widget, expand=True, fill=True) self.screen.widget.connect('key_press_event', self.on_keypress) if self.attrs.get('add_remove'): self.wid_text.connect('key_press_event', self.on_keypress) but_switch.props.sensitive = self.screen.number_of_views > 1 def _color_widget(self): if hasattr(self.screen.current_view, 'treeview'): return self.screen.current_view.treeview return super(One2Many, self)._color_widget() def on_keypress(self, widget, event): if (event.keyval == gtk.keysyms.F3) \ and self.but_new.get_property('sensitive'): self._sig_new(widget) return True if event.keyval == gtk.keysyms.F2 \ and widget == self.screen.widget: self._sig_edit(widget) return True if (event.keyval in (gtk.keysyms.Delete, gtk.keysyms.KP_Delete) and widget == self.screen.widget and self.but_del.get_property('sensitive')): self._sig_remove(widget) return True if event.keyval == gtk.keysyms.Insert and widget == self.screen.widget: self._sig_undelete(widget) return True if self.attrs.get('add_remove'): editable = self.wid_text.get_editable() activate_keys = [gtk.keysyms.Tab, gtk.keysyms.ISO_Left_Tab] if not self.wid_completion: activate_keys.append(gtk.keysyms.Return) if (widget == self.wid_text and event.keyval in activate_keys and editable and self.wid_text.get_text()): self._sig_add() self.wid_text.grab_focus() return False def destroy(self): if self.attrs.get('add_remove'): self.wid_text.disconnect_by_func(self._focus_out) self.screen.destroy() def _on_activate(self): self._sig_edit() def switch_view(self, widget): self.screen.switch_view() self.color_set(self.color_name) @property def modified(self): # JCA : Check current_view is not None return (self.screen.current_view.modified if self.screen.current_view else False) def color_set(self, name): super(One2Many, self).color_set(name) widget = self._color_widget() # if the style to apply is different from readonly then insensitive # cellrenderers should use the default insensitive color if name != 'readonly': widget.modify_text(gtk.STATE_INSENSITIVE, self.colors['text_color_insensitive']) def _readonly_set(self, value): self._readonly = value self._set_button_sensitive() self._set_label_state() def _required_set(self, value): self._required = value self._set_label_state() def _set_label_state(self): attrlist = common.get_label_attributes(self._readonly, self._required) self.title.set_attributes(attrlist) widget_class(self.title, 'readonly', self._readonly) widget_class(self.title, 'required', self._required) def _set_button_sensitive(self): access = common.MODELACCESS[self.screen.model_name] if self.record and self.field: field_size = self.record.expr_eval(self.attrs.get('size')) o2m_size = len(self.field.get_eval(self.record)) size_limit = (field_size is not None and o2m_size >= field_size >= 0) else: o2m_size = None size_limit = False has_form = ('form' in (x.view_type for x in self.screen.views) or 'form' in self.screen.view_to_load) self.but_new.set_sensitive( bool(not self._readonly and self.attrs.get('create', True) and not size_limit and access['create'] and (has_form or self.screen.current_view.editable))) self.but_del.set_sensitive( bool(not self._readonly and self.attrs.get('delete', True) and self._position and access['delete'])) self.but_undel.set_sensitive( bool(not self._readonly and not size_limit and self._position)) self.but_open.set_sensitive( bool(self._position and access['read'] and has_form)) self.but_next.set_sensitive( bool(self._position and self._position < self._length)) self.but_pre.set_sensitive(bool(self._position and self._position > 1)) if self.attrs.get('add_remove'): self.but_add.set_sensitive( bool(not self._readonly and not size_limit and access['write'] and access['read'])) self.but_remove.set_sensitive( bool(not self._readonly and self._position and access['write'] and access['read'])) self.wid_text.set_sensitive(self.but_add.get_sensitive()) self.wid_text.set_editable(self.but_add.get_sensitive()) # New button must be added to focus chain to allow keyboard only # creation when there is no existing record on form view. focus_chain = self.title_box.get_focus_chain() or [] if o2m_size == 0 and self.screen.current_view.view_type == 'form': if self.but_new not in focus_chain: focus_chain.append(self.but_new) else: if self.but_new in focus_chain: focus_chain.remove(self.but_new) self.title_box.set_focus_chain(focus_chain) def _validate(self): record = self.screen.current_record if record: fields = self.screen.current_view.get_fields() if not record.validate(fields): self.screen.display(set_cursor=True) return False if self.screen.pre_validate and not record.pre_validate(): return False return True def _sequence(self): for view in self.screen.views: if view.view_type == 'tree': sequence = view.attributes.get('sequence') if sequence: return sequence def _sig_new(self, *args): if not common.MODELACCESS[self.screen.model_name]['create']: return if not self._validate(): return if self.attrs.get('product'): self._new_product() else: self._new_single() def _new_single(self): ctx = {} ctx.update(self.field.get_context(self.record)) sequence = self._sequence() def update_sequence(): if sequence: self.screen.group.set_sequence(field=sequence) for widget in [self] + self.view.widgets[self.field_name]: if ((self.attrs.get('group') and widget.attrs.get('group') != self.attrs['group']) or not widget.visible or not hasattr(widget, 'screen')): continue if (widget.screen.current_view.view_type == 'form' or widget.screen.current_view.editable and not widget.screen.editable_open_get()): widget.screen.new() widget.screen.current_view.widget.set_sensitive(True) update_sequence() break else: field_size = self.record.expr_eval(self.attrs.get('size')) or -1 field_size -= len(self.field.get_eval(self.record)) + 1 WinForm(self.screen, lambda a: update_sequence(), new=True, many=field_size, context=ctx, title=self.attrs.get('string')) def _new_product(self): fields = self.attrs['product'].split(',') product = {} first = self.screen.new(default=False) default = first.default_get() first.set_default(default) def search_set(*args): if not fields: return make_product() field = self.screen.group.fields[fields.pop()] relation = field.attrs.get('relation') if not relation: search_set() domain = field.domain_get(first) context = field.get_context(first) def callback(result): if result: product[field.name] = result win_search = WinSearch(relation, callback, sel_multi=True, context=context, domain=domain, title=self.attrs.get('string')) win_search.win.connect('destroy', search_set) win_search.screen.search_filter() win_search.show() def make_product(first=first): if not product: self.screen.group.remove(first, remove=True) return fields = product.keys() for values in itertools.product(*product.values()): if first: record = first first = None else: record = self.screen.new(default=False) default_value = default.copy() for field, value in zip(fields, values): id_, rec_name = value default_value[field] = id_ default_value[field + '.rec_name'] = rec_name record.set_default(default_value) sequence = self._sequence() if sequence: self.screen.group.set_sequence(field=sequence) search_set() def _sig_edit(self, widget=None): if not self.but_open.props.sensitive: return if not self._validate(): return record = self.screen.current_record if record: WinForm(self.screen, lambda a: None, title=self.attrs.get('string')) def _sig_next(self, widget): if not self._validate(): return self.screen.display_next() def _sig_previous(self, widget): if not self._validate(): return self.screen.display_prev() def _sig_remove(self, widget, remove=False): access = common.MODELACCESS[self.screen.model_name] if remove: if not access['write'] or not access['read']: return else: if not access['delete']: return self.screen.remove(remove=remove) def _sig_undelete(self, button): self.screen.unremove() def _sig_add(self, *args): if not self.focus_out: return access = common.MODELACCESS[self.screen.model_name] if not access['write'] or not access['read']: return domain = self.field.domain_get(self.record) context = self.field.get_context(self.record) domain = [domain, self.record.expr_eval(self.attrs.get('add_remove'))] removed_ids = self.field.get_removed_ids(self.record) domain = ['OR', domain, ('id', 'in', removed_ids)] text = self.wid_text.get_text().decode('utf-8') self.focus_out = False sequence = self._sequence() def callback(result): self.focus_out = True if result: ids = [x[0] for x in result] self.screen.load(ids, modified=True) self.screen.display(res_id=ids[0]) if sequence: self.screen.group.set_sequence(field=sequence) self.screen.set_cursor() self.wid_text.set_text('') win = WinSearch(self.attrs['relation'], callback, sel_multi=True, context=context, domain=domain, view_ids=self.attrs.get('view_ids', '').split(','), views_preload=self.attrs.get('views', {}), new=self.but_new.get_property('sensitive'), title=self.attrs.get('string')) win.screen.search_filter(quote(text)) win.show() def _sig_label(self, screen, signal_data): self._position = signal_data[0] self._length = signal_data[1] if self._position >= 1: name = str(self._position) else: name = '_' line = '(%s/%s)' % (name, self._length) self.label.set_text(line) self._set_button_sensitive() def group_sync(self, screen, current_record): if not self.screen.widget.props.window: return if not self.view or not self.view.widgets: return if self.attrs.get('mode') == 'form': return if screen.current_record != current_record: return def is_compatbile(screen, record): return not (screen.current_view.view_type == 'form' and record is not None and screen.model_name != record.model_name) current_record = self.screen.current_record to_sync = [] for widget in self.view.widgets[self.field_name]: if (widget == self or widget.attrs.get('group') != self.attrs['group'] or not hasattr(widget, 'screen')): continue if widget.screen.current_record == current_record: continue record = current_record if not is_compatbile(widget.screen, record): record = None if not widget._validate(): def go_previous(): record = widget.screen.current_record if not is_compatbile(screen, record): record = None screen.current_record = record screen.display() gobject.idle_add(go_previous) return to_sync.append((widget, record)) for widget, record in to_sync: if (widget.screen.current_view.view_type == 'form' and record is not None and widget.screen.group.model_name == record.group.model_name): fields = dict( (name, field.attrs) for name, field in widget.screen.group.fields.iteritems()) record.group.load_fields(fields) widget.screen.current_record = record widget.display(widget.record, widget.field) def display(self, record, field): super(One2Many, self).display(record, field) self._set_button_sensitive() if field is None: self.screen.new_group() self.screen.current_record = None self.screen.parent = None self.screen.display() return False new_group = field.get_client(record) if self.attrs.get('group') and self.attrs.get('mode') == 'form': if self.screen.current_record is None: self.invisible_set(True) elif id(self.screen.group) != id(new_group): self.screen.group = new_group if (self.screen.current_view.view_type == 'tree') \ and self.screen.current_view.editable: self.screen.current_record = None domain = [] size_limit = None if record: domain = field.domain_get(record) size_limit = record.expr_eval(self.attrs.get('size')) if self._readonly: if size_limit is None: size_limit = len(self.screen.group) else: size_limit = min(size_limit, len(self.screen.group)) if self.screen.domain != domain: self.screen.domain = domain self.screen.size_limit = size_limit self.screen.display() return True def set_value(self, record, field): if (self.screen.current_view.view_type == 'form' and self.attrs.get('group') and self.screen.model_name != record.model_name): return True self.screen.current_view.set_value() if self.screen.modified(): # TODO check if required record.modified_fields.setdefault(field.name) record.signal('record-modified') return True def _completion_match_selected(self, completion, model, iter_): record_id, = model.get(iter_, 1) self.screen.load([record_id], modified=True) self.wid_text.set_text('') self.wid_text.grab_focus() completion_model = self.wid_completion.get_model() completion_model.clear() completion_model.search_text = self.wid_text.get_text() return True def _update_completion(self, widget): if self._readonly: return if not self.record: return model = self.attrs['relation'] domain = self.field.domain_get(self.record) domain = [domain, self.record.expr_eval(self.attrs.get('add_remove'))] removed_ids = self.field.get_removed_ids(self.record) domain = ['OR', domain, ('id', 'in', removed_ids)] update_completion(self.wid_text, self.record, self.field, model, domain=domain) def _completion_action_activated(self, completion, index): if index == 0: self._sig_add() self.wid_text.grab_focus() elif index == 1: self._sig_new()
class Many2Many(Widget): expand = True def __init__(self, view, attrs): super(Many2Many, self).__init__(view, attrs) self.widget = gtk.Frame() self.widget.set_shadow_type(gtk.SHADOW_NONE) self.widget.get_accessible().set_name(attrs.get('string', '')) vbox = gtk.VBox(homogeneous=False, spacing=5) self.widget.add(vbox) self._readonly = True self._required = False self._position = 0 hbox = gtk.HBox(homogeneous=False, spacing=0) hbox.set_border_width(2) self.title = gtk.Label(set_underline(attrs.get('string', ''))) self.title.set_use_underline(True) self.title.set_alignment(0.0, 0.5) hbox.pack_start(self.title, expand=True, fill=True) hbox.pack_start(gtk.VSeparator(), expand=False, fill=True) tooltips = common.Tooltips() self.wid_text = PlaceholderEntry() self.wid_text.set_placeholder_text(_('Search')) self.wid_text.set_property('width_chars', 13) self.wid_text.connect('focus-out-event', self._focus_out) self.focus_out = True hbox.pack_start(self.wid_text, expand=True, fill=True) if int(self.attrs.get('completion', 1)): self.wid_completion = get_completion( create=self.attrs.get('create', True) and common.MODELACCESS[self.attrs['relation']]['create']) self.wid_completion.connect('match-selected', self._completion_match_selected) self.wid_completion.connect('action-activated', self._completion_action_activated) self.wid_text.set_completion(self.wid_completion) self.wid_text.connect('changed', self._update_completion) else: self.wid_completion = None self.but_add = gtk.Button() tooltips.set_tip(self.but_add, _('Add existing record')) self.but_add.connect('clicked', self._sig_add) img_add = gtk.Image() img_add.set_from_stock('tryton-list-add', gtk.ICON_SIZE_SMALL_TOOLBAR) img_add.set_alignment(0.5, 0.5) self.but_add.add(img_add) self.but_add.set_relief(gtk.RELIEF_NONE) hbox.pack_start(self.but_add, expand=False, fill=False) self.but_remove = gtk.Button() tooltips.set_tip(self.but_remove, _('Remove selected record <Del>')) self.but_remove.connect('clicked', self._sig_remove) img_remove = gtk.Image() img_remove.set_from_stock('tryton-list-remove', gtk.ICON_SIZE_SMALL_TOOLBAR) img_remove.set_alignment(0.5, 0.5) self.but_remove.add(img_remove) self.but_remove.set_relief(gtk.RELIEF_NONE) hbox.pack_start(self.but_remove, expand=False, fill=False) hbox.set_focus_chain([self.wid_text]) tooltips.enable() frame = gtk.Frame() frame.add(hbox) frame.set_shadow_type(gtk.SHADOW_OUT) vbox.pack_start(frame, expand=False, fill=True) self.screen = Screen(attrs['relation'], view_ids=attrs.get('view_ids', '').split(','), mode=['tree'], views_preload=attrs.get('views', {}), row_activate=self._on_activate, limit=None) self.screen.signal_connect(self, 'record-message', self._sig_label) vbox.pack_start(self.screen.widget, expand=True, fill=True) self.title.set_mnemonic_widget( self.screen.current_view.mnemonic_widget) self.screen.widget.connect('key_press_event', self.on_keypress) self.wid_text.connect('key_press_event', self.on_keypress) def on_keypress(self, widget, event): editable = self.wid_text.get_editable() activate_keys = [gtk.keysyms.Tab, gtk.keysyms.ISO_Left_Tab] remove_keys = [gtk.keysyms.Delete, gtk.keysyms.KP_Delete] if not self.wid_completion: activate_keys.append(gtk.keysyms.Return) if widget == self.screen.widget: if event.keyval == gtk.keysyms.F3 and editable: self._sig_add() return True elif event.keyval == gtk.keysyms.F2: self._sig_edit() return True elif event.keyval in remove_keys and editable: self._sig_remove() return True elif widget == self.wid_text: if event.keyval == gtk.keysyms.F3: self._sig_new() return True elif event.keyval == gtk.keysyms.F2: self._sig_add() return True elif event.keyval in activate_keys and self.wid_text.get_text(): self._sig_add() self.wid_text.grab_focus() return False def destroy(self): self.wid_text.disconnect_by_func(self._focus_out) self.screen.destroy() def _sig_add(self, *args): if not self.focus_out: return domain = self.field.domain_get(self.record) add_remove = self.record.expr_eval(self.attrs.get('add_remove')) if add_remove: domain = [domain, add_remove] context = self.field.get_search_context(self.record) order = self.field.get_search_order(self.record) value = self.wid_text.get_text().decode('utf-8') self.focus_out = False def callback(result): self.focus_out = True if result: ids = [x[0] for x in result] self.screen.load(ids, modified=True) self.screen.display(res_id=ids[0]) self.screen.set_cursor() self.wid_text.set_text('') win = WinSearch(self.attrs['relation'], callback, sel_multi=True, context=context, domain=domain, order=order, view_ids=self.attrs.get('view_ids', '').split(','), views_preload=self.attrs.get('views', {}), new=self.attrs.get('create', True), title=self.attrs.get('string')) win.screen.search_filter(quote(value)) win.show() def _sig_remove(self, *args): self.screen.remove(remove=True) def _on_activate(self): self._sig_edit() def _get_screen_form(self): domain = self.field.domain_get(self.record) add_remove = self.record.expr_eval(self.attrs.get('add_remove')) if add_remove: domain = [domain, add_remove] context = self.field.get_context(self.record) # Remove the first tree view as mode is form only view_ids = self.attrs.get('view_ids', '').split(',')[1:] return Screen(self.attrs['relation'], domain=domain, view_ids=view_ids, mode=['form'], views_preload=self.attrs.get('views', {}), context=context) def _sig_edit(self): if not self.screen.current_record: return # Create a new screen that is not linked to the parent otherwise on the # save of the record will trigger the save of the parent screen = self._get_screen_form() screen.load([self.screen.current_record.id]) def callback(result): if result: screen.current_record.save() # Force a reload on next display self.screen.current_record.cancel() # Force a display to clear the CellCache self.screen.display() WinForm(screen, callback, title=self.attrs.get('string')) def _sig_new(self): screen = self._get_screen_form() def callback(result): self.focus_out = True if result: record = screen.current_record self.screen.load([record.id], modified=True) self.wid_text.set_text('') self.wid_text.grab_focus() self.focus_out = False WinForm(screen, callback, new=True, save_current=True, title=self.attrs.get('string'), rec_name=self.wid_text.get_text()) def _readonly_set(self, value): self._readonly = value self._set_button_sensitive() self.wid_text.set_sensitive(not value) self.wid_text.set_editable(not value) self._set_label_state() def _required_set(self, value): self._required = value self._set_label_state() def _set_label_state(self): attrlist = common.get_label_attributes(self._readonly, self._required) self.title.set_attributes(attrlist) widget_class(self.title, 'readonly', self._readonly) widget_class(self.title, 'required', self._required) def _set_button_sensitive(self): if self.record and self.field: field_size = self.record.expr_eval(self.attrs.get('size')) m2m_size = len(self.field.get_eval(self.record)) size_limit = (field_size is not None and m2m_size >= field_size >= 0) else: size_limit = False self.but_add.set_sensitive(bool(not self._readonly and not size_limit)) self.but_remove.set_sensitive( bool(not self._readonly and self._position)) def _sig_label(self, screen, signal_data): self._position = signal_data[0] self._set_button_sensitive() def display(self, record, field): super(Many2Many, self).display(record, field) if field is None: self.screen.new_group() self.screen.current_record = None self.screen.parent = None self.screen.display() return False new_group = field.get_client(record) if id(self.screen.group) != id(new_group): self.screen.group = new_group self.screen.display() return True def set_value(self, record, field): self.screen.current_view.set_value() return True def _completion_match_selected(self, completion, model, iter_): record_id, = model.get(iter_, 1) self.screen.load([record_id], modified=True) self.wid_text.set_text('') self.wid_text.grab_focus() completion_model = self.wid_completion.get_model() completion_model.clear() completion_model.search_text = self.wid_text.get_text() return True def _update_completion(self, widget): if self._readonly: return if not self.record: return model = self.attrs['relation'] update_completion(self.wid_text, self.record, self.field, model) def _completion_action_activated(self, completion, index): if index == 0: self._sig_add() self.wid_text.grab_focus() elif index == 1: self._sig_new()
class DictWidget(WidgetInterface): def __init__(self, field_name, model_name, attrs=None): super(DictWidget, self).__init__(field_name, model_name, attrs=attrs) self.schema_model = attrs['schema_model'] self.keys = {} self.fields = {} self.buttons = {} self.rows = {} self.widget = gtk.Frame(attrs.get('string', '')) self.widget.set_shadow_type(gtk.SHADOW_OUT) vbox = gtk.VBox() self.widget.add(vbox) self.table = gtk.Table(1, 3, homogeneous=False) self.table.set_col_spacings(0) self.table.set_row_spacings(0) self.table.set_border_width(0) vbox.pack_start(self.table, expand=True, fill=True) hbox = gtk.HBox() hbox.set_border_width(2) self.wid_text = PlaceholderEntry() self.wid_text.set_placeholder_text(_('Search')) self.wid_text.props.width_chars = 13 self.wid_text.connect('activate', self._sig_activate) hbox.pack_start(self.wid_text, expand=True, fill=True) self.but_add = gtk.Button() self.but_add.connect('clicked', self._sig_add) img_add = gtk.Image() img_add.set_from_stock('tryton-list-add', gtk.ICON_SIZE_SMALL_TOOLBAR) img_add.set_alignment(0.5, 0.5) self.but_add.add(img_add) self.but_add.set_relief(gtk.RELIEF_NONE) hbox.pack_start(self.but_add, expand=False, fill=False) hbox.set_focus_chain([self.wid_text]) vbox.pack_start(hbox, expand=True, fill=True) self.tooltips = Tooltips() self.tooltips.set_tip(self.but_add, _('Add value')) self.tooltips.enable() self._readonly = False self._record_id = None def _new_remove_btn(self): but_remove = gtk.Button() img_remove = gtk.Image() img_remove.set_from_stock('tryton-list-remove', gtk.ICON_SIZE_SMALL_TOOLBAR) img_remove.set_alignment(0.5, 0.5) but_remove.add(img_remove) but_remove.set_relief(gtk.RELIEF_NONE) return but_remove def _sig_activate(self, *args): if self.wid_text.get_editable(): self._sig_add() def _sig_add(self, *args): context = self.field.context_get(self.record) value = self.wid_text.get_text() domain = self.field.domain_get(self.record) dom = [('rec_name', 'ilike', '%%%s%%' % value)] if value else [] dom.append(('id', 'not in', [self.keys[f]['id'] for f in self.fields])) try: ids = RPCExecute('model', self.schema_model, 'search', domain + dom, 0, CONFIG['client.limit'], None, context=context) except RPCException: return False def callback(result): if result: self.send_modified() try: new_fields = RPCExecute('model', self.schema_model, 'get_keys', [r[0] for r in result], context=context) except RPCException: new_fields = [] for new_field in new_fields: if new_field['name'] not in self.fields: self.keys[new_field['name']] = new_field self.add_line(new_field['name']) self.wid_text.set_text('') if len(ids) != 1: WinSearch(self.schema_model, callback, sel_multi=True, ids=ids, context=context, domain=domain, new=False) else: callback([(id, None) for id in ids]) def _sig_remove(self, button, key, modified=True): del self.fields[key] del self.buttons[key] for widget in self.rows[key]: self.table.remove(widget) widget.destroy() del self.rows[key] if modified: self.send_modified() self.set_value(self.record, self.field) def set_value(self, record, field): field.set_client(record, self.get_value()) def get_value(self): return dict( (key, widget.get_value()) for key, widget in self.fields.items()) @property def modified(self): if self.record and self.field: value = self.field.get_client(self.record) return any( widget.modified(value) for widget in self.fields.itervalues()) return False def _readonly_set(self, readonly): self._readonly = readonly self._set_button_sensitive() for widget in self.fields.values(): widget.set_readonly(readonly) self.wid_text.set_editable(not readonly) def _set_button_sensitive(self): self.but_add.set_sensitive( bool(not self._readonly and self.attrs.get('create', True))) for button in self.buttons.itervalues(): button.set_sensitive( bool(not self._readonly and self.attrs.get('delete', True))) def add_line(self, key): self.fields[key] = DICT_ENTRIES[self.keys[key]['type_']](key, self) field = self.fields[key] alignment = gtk.Alignment(float(self.attrs.get('xalign', 0.0)), float(self.attrs.get('yalign', 0.5)), float(self.attrs.get('xexpand', 1.0)), float(self.attrs.get('yexpand', 1.0))) hbox = gtk.HBox() hbox.pack_start(field.widget, expand=field.expand, fill=field.fill) alignment.add(hbox) n_rows = self.table.props.n_rows self.table.resize(n_rows + 1, 3) if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL: text = _(':') + self.keys[key]['string'] else: text = self.keys[key]['string'] + _(':') label = gtk.Label(text) label.set_alignment(1., .5) self.table.attach(label, 0, 1, n_rows - 1, n_rows, xoptions=gtk.FILL, yoptions=False, xpadding=2) label.show() self.table.attach(alignment, 1, 2, n_rows - 1, n_rows, xoptions=gtk.FILL | gtk.EXPAND, yoptions=False, xpadding=2) alignment.show_all() remove_but = self._new_remove_btn() self.tooltips.set_tip(remove_but, _('Remove "%s"') % self.keys[key]['string']) self.table.attach(remove_but, 2, 3, n_rows - 1, n_rows, xoptions=gtk.FILL, yoptions=False, xpadding=2) remove_but.connect('clicked', self._sig_remove, key) remove_but.show_all() self.rows[key] = [label, alignment, remove_but] self.buttons[key] = remove_but def add_key(self, key): context = self.field.context_get(self.record) try: key_ids = RPCExecute('model', self.schema_model, 'search', [('name', '=', key)], 0, CONFIG['client.limit'], None, context=context) self.keys[key] = RPCExecute('model', self.schema_model, 'get_keys', key_ids, context=context)[0] except RPCException: pass def display(self, record, field): super(DictWidget, self).display(record, field) if field is None: return record_id = record.id if record else None if record_id != self._record_id: for key in self.fields.keys(): self._sig_remove(None, key, modified=False) self._record_id = record_id value = field.get_client(record) if field else {} for key in sorted(value.iterkeys()): val = value[key] if key not in self.keys: self.add_key(key) if key not in self.fields: self.add_line(key) widget = self.fields[key] widget.set_value(val) widget.set_readonly(self._readonly) for key in set(self.fields.keys()) - set(value.keys()): self._sig_remove(None, key, modified=False) self._set_button_sensitive()
class DictWidget(WidgetInterface): def __init__(self, field_name, model_name, attrs=None): super(DictWidget, self).__init__(field_name, model_name, attrs=attrs) self.schema_model = attrs['schema_model'] self.keys = {} self.fields = {} self.buttons = {} self.rows = {} self.widget = gtk.Frame(attrs.get('string', '')) self.widget.set_shadow_type(gtk.SHADOW_OUT) vbox = gtk.VBox() self.widget.add(vbox) self.table = gtk.Table(1, 3, homogeneous=False) self.table.set_col_spacings(0) self.table.set_row_spacings(0) self.table.set_border_width(0) vbox.pack_start(self.table, expand=True, fill=True) hbox = gtk.HBox() hbox.set_border_width(2) self.wid_text = PlaceholderEntry() self.wid_text.set_placeholder_text(_('Search')) self.wid_text.props.width_chars = 13 self.wid_text.connect('activate', self._sig_activate) hbox.pack_start(self.wid_text, expand=True, fill=True) self.but_add = gtk.Button() self.but_add.connect('clicked', self._sig_add) img_add = gtk.Image() img_add.set_from_stock('tryton-list-add', gtk.ICON_SIZE_SMALL_TOOLBAR) img_add.set_alignment(0.5, 0.5) self.but_add.add(img_add) self.but_add.set_relief(gtk.RELIEF_NONE) hbox.pack_start(self.but_add, expand=False, fill=False) hbox.set_focus_chain([self.wid_text]) vbox.pack_start(hbox, expand=True, fill=True) self.tooltips = Tooltips() self.tooltips.set_tip(self.but_add, _('Add value')) self.tooltips.enable() self._readonly = False self._record_id = None def _new_remove_btn(self): but_remove = gtk.Button() img_remove = gtk.Image() img_remove.set_from_stock('tryton-list-remove', gtk.ICON_SIZE_SMALL_TOOLBAR) img_remove.set_alignment(0.5, 0.5) but_remove.add(img_remove) but_remove.set_relief(gtk.RELIEF_NONE) return but_remove def _sig_activate(self, *args): if self.wid_text.get_editable(): self._sig_add() def _sig_add(self, *args): context = self.field.context_get(self.record) value = self.wid_text.get_text() domain = self.field.domain_get(self.record) dom = [('rec_name', 'ilike', '%%%s%%' % value)] if value else [] dom.append(('id', 'not in', [self.keys[f]['id'] for f in self.fields])) try: ids = RPCExecute('model', self.schema_model, 'search', domain + dom, 0, CONFIG['client.limit'], None, context=context) except RPCException: return False def callback(result): if result: self.send_modified() try: new_fields = RPCExecute('model', self.schema_model, 'get_keys', [r[0] for r in result], context=context) except RPCException: new_fields = [] for new_field in new_fields: if new_field['name'] not in self.fields: self.keys[new_field['name']] = new_field self.add_line(new_field['name']) self.wid_text.set_text('') if len(ids) != 1: WinSearch(self.schema_model, callback, sel_multi=True, ids=ids, context=context, domain=domain, new=False) else: callback([(id, None) for id in ids]) def _sig_remove(self, button, key, modified=True): del self.fields[key] del self.buttons[key] for widget in self.rows[key]: self.table.remove(widget) widget.destroy() del self.rows[key] if modified: self.send_modified() self.set_value(self.record, self.field) def set_value(self, record, field): field.set_client(record, self.get_value()) def get_value(self): return dict((key, widget.get_value()) for key, widget in self.fields.items()) @property def modified(self): if self.record and self.field: value = self.field.get_client(self.record) return any(widget.modified(value) for widget in self.fields.itervalues()) return False def _readonly_set(self, readonly): self._readonly = readonly self._set_button_sensitive() for widget in self.fields.values(): widget.set_readonly(readonly) self.wid_text.set_editable(not readonly) def _set_button_sensitive(self): self.but_add.set_sensitive(bool( not self._readonly and self.attrs.get('create', True))) for button in self.buttons.itervalues(): button.set_sensitive(bool( not self._readonly and self.attrs.get('delete', True))) def add_line(self, key): self.fields[key] = DICT_ENTRIES[self.keys[key]['type_']](key, self) field = self.fields[key] alignment = gtk.Alignment( float(self.attrs.get('xalign', 0.0)), float(self.attrs.get('yalign', 0.5)), float(self.attrs.get('xexpand', 1.0)), float(self.attrs.get('yexpand', 1.0))) hbox = gtk.HBox() hbox.pack_start(field.widget, expand=field.expand, fill=field.fill) alignment.add(hbox) n_rows = self.table.props.n_rows self.table.resize(n_rows + 1, 3) if gtk.widget_get_default_direction() == gtk.TEXT_DIR_RTL: text = _(':') + self.keys[key]['string'] else: text = self.keys[key]['string'] + _(':') label = gtk.Label(text) label.set_alignment(1., .5) self.table.attach(label, 0, 1, n_rows - 1, n_rows, xoptions=gtk.FILL, yoptions=False, xpadding=2) label.show() self.table.attach(alignment, 1, 2, n_rows - 1, n_rows, xoptions=gtk.FILL | gtk.EXPAND, yoptions=False, xpadding=2) alignment.show_all() remove_but = self._new_remove_btn() self.tooltips.set_tip(remove_but, _('Remove "%s"') % self.keys[key]['string']) self.table.attach(remove_but, 2, 3, n_rows - 1, n_rows, xoptions=gtk.FILL, yoptions=False, xpadding=2) remove_but.connect('clicked', self._sig_remove, key) remove_but.show_all() self.rows[key] = [label, alignment, remove_but] self.buttons[key] = remove_but def add_key(self, key): context = self.field.context_get(self.record) try: key_ids = RPCExecute('model', self.schema_model, 'search', [('name', '=', key)], 0, CONFIG['client.limit'], None, context=context) self.keys[key] = RPCExecute('model', self.schema_model, 'get_keys', key_ids, context=context)[0] except RPCException: pass def display(self, record, field): super(DictWidget, self).display(record, field) if field is None: return record_id = record.id if record else None if record_id != self._record_id: for key in self.fields.keys(): self._sig_remove(None, key, modified=False) self._record_id = record_id value = field.get_client(record) if field else {} for key in sorted(value.iterkeys()): val = value[key] if key not in self.keys: self.add_key(key) if key not in self.fields: self.add_line(key) widget = self.fields[key] widget.set_value(val) widget.set_readonly(self._readonly) for key in set(self.fields.keys()) - set(value.keys()): self._sig_remove(None, key, modified=False) self._set_button_sensitive()