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() 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', lambda *a: 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) hbox.pack_start(gtk.VSeparator(), expand=False, fill=True) 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) 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) 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) 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)) self.screen.pre_validate = bool(int(attrs.get('pre_validate', 0))) self.screen.signal_connect(self, 'record-message', self._sig_label) 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 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): self.screen.destroy() def _on_activate(self): self._sig_edit() def switch_view(self, widget): self.screen.switch_view() @property def modified(self): return self.screen.current_view.modified 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) 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 self.but_new.set_sensitive( bool(not self._readonly and self.attrs.get('create', True) and not size_limit and access['create'])) 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'])) 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()) # 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): self.view.set_value() 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 _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.context_get(self.record)) sequence = None for view in self.screen.views: if view.view_type == 'tree': sequence = view.attributes.get('sequence') if sequence: break def update_sequence(): if sequence: self.screen.group.set_sequence(field=sequence) if self.screen.current_view.editable: self.screen.new() self.screen.current_view.widget.set_sensitive(True) update_sequence() 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.context_get(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) search_set() def _sig_edit(self, widget=None): if not common.MODELACCESS[self.screen.model_name]['read']: 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 self.view.set_value() domain = self.field.domain_get(self.record) context = self.field.context_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)] text = self.wid_text.get_text().decode('utf-8') self.focus_out = False sequence = None if self.screen.current_view.view_type == 'tree': sequence = self.screen.current_view.attributes.get('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 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 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.screen.domain != domain: self.screen.domain = domain self.screen.size_limit = size_limit self.screen.display() return True def set_value(self, record, field): 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 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.get_context(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.get_context(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) 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.get_context(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 ScreenContainer(object): def __init__(self, tab_domain): self.viewport = gtk.Viewport() self.viewport.set_shadow_type(gtk.SHADOW_NONE) self.vbox = gtk.VBox(spacing=3) self.alternate_viewport = gtk.Viewport() self.alternate_viewport.set_shadow_type(gtk.SHADOW_NONE) self.alternate_view = False self.search_window = None self.search_table = None self.last_search_text = '' self.tab_domain = tab_domain or [] self.tab_counter = [] tooltips = common.Tooltips() self.filter_vbox = gtk.VBox(spacing=0) self.filter_vbox.set_border_width(0) hbox = gtk.HBox(homogeneous=False, spacing=0) self.search_entry = PlaceholderEntry() self.search_entry.set_placeholder_text(_('Search')) self.search_entry.set_alignment(0.0) self.search_entry.set_icon_from_stock(gtk.ENTRY_ICON_PRIMARY, 'tryton-find') self.search_entry.set_icon_tooltip_text(gtk.ENTRY_ICON_PRIMARY, _('Open filters')) self.completion = gtk.EntryCompletion() self.completion.set_model(gtk.ListStore(str)) self.completion.set_text_column(0) self.completion.props.inline_selection = True self.completion.props.popup_set_width = False self.completion.set_match_func(lambda *a: True) self.completion.connect('match-selected', self.match_selected) self.search_entry.connect('activate', self.activate) self.search_entry.set_completion(self.completion) self.search_entry.connect('key-press-event', self.key_press) self.search_entry.connect('focus-in-event', self.focus_in) self.search_entry.connect('icon-press', self.icon_press) hbox.pack_start(self.search_entry, expand=True, fill=True) def popup(widget): menu = widget._menu for child in menu.get_children(): menu.remove(child) if not widget.props.active: menu.popdown() return def menu_position(menu, data=None): widget_allocation = widget.get_allocation() if hasattr(widget.window, 'get_root_coords'): x, y = widget.window.get_root_coords( widget_allocation.x, widget_allocation.y) else: x, y = widget.window.get_origin() x += widget_allocation.x y += widget_allocation.y return (x, y + widget_allocation.height, False) for id_, name, domain in self.bookmarks(): menuitem = gtk.MenuItem(name) menuitem.connect('activate', self.bookmark_activate, domain) menu.add(menuitem) menu.show_all() menu.popup(None, None, menu_position, 0, 0) def deactivate(menuitem, togglebutton): togglebutton.props.active = False but_bookmark = gtk.ToggleButton() self.but_bookmark = but_bookmark tooltips.set_tip(but_bookmark, _('Show bookmarks of filters')) img_bookmark = gtk.Image() img_bookmark.set_from_stock('tryton-bookmark', gtk.ICON_SIZE_SMALL_TOOLBAR) img_bookmark.set_alignment(0.5, 0.5) but_bookmark.add(img_bookmark) but_bookmark.set_relief(gtk.RELIEF_NONE) menu = gtk.Menu() menu.set_property('reserve-toggle-size', False) menu.connect('deactivate', deactivate, but_bookmark) but_bookmark._menu = menu but_bookmark.connect('toggled', popup) hbox.pack_start(but_bookmark, expand=False, fill=False) but_prev = gtk.Button() self.but_prev = but_prev tooltips.set_tip(but_prev, _('Previous')) but_prev.connect('clicked', self.search_prev) img_prev = gtk.Image() img_prev.set_from_stock('tryton-go-previous', gtk.ICON_SIZE_SMALL_TOOLBAR) img_prev.set_alignment(0.5, 0.5) but_prev.add(img_prev) but_prev.set_relief(gtk.RELIEF_NONE) hbox.pack_start(but_prev, expand=False, fill=False) but_next = gtk.Button() self.but_next = but_next tooltips.set_tip(but_next, _('Next')) but_next.connect('clicked', self.search_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) but_next.add(img_next) but_next.set_relief(gtk.RELIEF_NONE) hbox.pack_start(but_next, expand=False, fill=False) hbox.show_all() hbox.set_focus_chain([self.search_entry]) self.filter_vbox.pack_start(hbox, expand=False, fill=False) hseparator = gtk.HSeparator() hseparator.show() self.filter_vbox.pack_start(hseparator, expand=False, fill=False) if self.tab_domain: self.notebook = gtk.Notebook() try: self.notebook.props.homogeneous = True except AttributeError: # No more supported by GTK+3 pass self.notebook.set_scrollable(True) for name, domain, count in self.tab_domain: hbox = gtk.HBox(spacing=3) label = gtk.Label('_' + name) label.set_use_underline(True) hbox.pack_start(label, expand=True, fill=True) counter = gtk.Label() hbox.pack_start(counter, expand=False, fill=True) hbox.show_all() self.notebook.append_page(gtk.VBox(), hbox) self.tab_counter.append(counter) self.filter_vbox.pack_start(self.notebook, expand=True, fill=True) self.notebook.show_all() # Set the current page before connecting to switch-page to not # trigger the search a second times. self.notebook.set_current_page(0) self.notebook.get_nth_page(0).pack_end(self.viewport) self.notebook.connect('switch-page', self.switch_page) self.notebook.connect_after('switch-page', self.switch_page_after) filter_expand = True else: self.notebook = None self.vbox.pack_end(self.viewport) filter_expand = False self.vbox.pack_start(self.filter_vbox, expand=filter_expand, fill=True) self.but_next.set_sensitive(False) self.but_prev.set_sensitive(False) tooltips.enable() def destroy(self): if self.search_window: self.search_window.hide() def widget_get(self): return self.vbox def set_screen(self, screen): self.screen = screen self.but_bookmark.set_sensitive(bool(list(self.bookmarks()))) self.bookmark_match() def show_filter(self): if self.filter_vbox: self.filter_vbox.show() if self.notebook: self.notebook.set_show_tabs(True) if self.viewport in self.vbox.get_children(): self.vbox.remove(self.viewport) self.notebook.get_nth_page( self.notebook.get_current_page()).pack_end(self.viewport) def hide_filter(self): if self.filter_vbox: self.filter_vbox.hide() if self.notebook: self.notebook.set_show_tabs(False) if self.viewport not in self.vbox.get_children(): self.notebook.get_nth_page( self.notebook.get_current_page()).remove(self.viewport) self.vbox.pack_end(self.viewport) def set(self, widget): viewport1 = self.viewport viewport2 = self.alternate_viewport if self.alternate_view: viewport1, viewport2 = viewport2, viewport1 if viewport1.get_child(): viewport1.remove(viewport1.get_child()) if widget == viewport2.get_child(): viewport2.remove(widget) viewport1.add(widget) viewport1.show_all() def update(self): res = self.screen.search_complete(self.get_text()) model = self.completion.get_model() model.clear() for r in res: model.append([r.strip()]) def get_text(self): return self.search_entry.get_text().decode('utf-8') def set_text(self, value): self.search_entry.set_text(value) self.bookmark_match() def bookmarks(self): for id_, name, domain in common.VIEW_SEARCH[self.screen.model_name]: if self.screen.domain_parser.stringable(domain): yield id_, name, domain def bookmark_activate(self, menuitem, domain): self.set_text(self.screen.domain_parser.string(domain)) self.do_search() def bookmark_match(self): icon_stock = self.search_entry.get_icon_stock(gtk.ENTRY_ICON_SECONDARY) current_text = self.get_text() if current_text: current_domain = self.screen.domain_parser.parse(current_text) self.search_entry.set_icon_activatable(gtk.ENTRY_ICON_SECONDARY, bool(current_text)) self.search_entry.set_icon_sensitive(gtk.ENTRY_ICON_SECONDARY, bool(current_text)) for id_, name, domain in self.bookmarks(): text = self.screen.domain_parser.string(domain) domain = self.screen.domain_parser.parse(text.decode('utf-8')) if (text == current_text or domain == current_domain): if icon_stock != 'tryton-star': self.search_entry.set_icon_from_stock( gtk.ENTRY_ICON_SECONDARY, 'tryton-star') self.search_entry.set_icon_tooltip_text( gtk.ENTRY_ICON_SECONDARY, _('Remove this bookmark')) return id_ if icon_stock != 'tryton-unstar': self.search_entry.set_icon_from_stock(gtk.ENTRY_ICON_SECONDARY, 'tryton-unstar') if current_text: self.search_entry.set_icon_tooltip_text(gtk.ENTRY_ICON_SECONDARY, _('Bookmark this filter')) elif self.search_entry.get_icon_tooltip_text(gtk.ENTRY_ICON_SECONDARY): self.search_entry.set_icon_tooltip_text(gtk.ENTRY_ICON_SECONDARY, None) def search_next(self, widget=None): self.screen.search_next(self.get_text()) def search_prev(self, widget=None): self.screen.search_prev(self.get_text()) def switch_page(self, notebook, page, page_num): current_page = notebook.get_nth_page(notebook.get_current_page()) current_page.remove(self.viewport) new_page = notebook.get_nth_page(page_num) new_page.pack_end(self.viewport) def switch_page_after(self, notebook, page, page_num): self.do_search() notebook.grab_focus() self.screen.count_tab_domain() def get_tab_domain(self): if not self.notebook: return [] idx = self.notebook.get_current_page() if idx < 0: return [] return self.tab_domain[idx][1] def set_tab_counter(self, count, idx=None): if not self.tab_counter or not self.notebook: return if idx is None: idx = self.notebook.get_current_page() if idx < 0: return label = self.tab_counter[idx] tooltip = common.Tooltips() if count is None: label.set_label('') tooltip.set_tip(label, '') else: tooltip.set_tip(label, '%d' % count) fmt = '(%d)' if count > 99: fmt = '(%d+)' count = 99 label.set_label(fmt % count) def match_selected(self, completion, model, iter): def callback(): if not self.search_entry.props.window: return self.update() self.search_entry.emit('changed') gobject.idle_add(callback) def activate(self, widget): if not self.search_entry.get_selection_bounds(): self.do_search(widget) def do_search(self, widget=None): self.screen.search_filter(self.get_text()) def set_cursor(self, new=False, reset_view=True): if self.filter_vbox: self.search_entry.grab_focus() def key_press(self, widget, event): def keypress(): if not self.search_entry.props.window: return self.update() self.bookmark_match() gobject.idle_add(keypress) def icon_press(self, widget, icon_pos, event): if icon_pos == gtk.ENTRY_ICON_PRIMARY: self.search_box(widget) elif icon_pos == gtk.ENTRY_ICON_SECONDARY: icon_stock = self.search_entry.get_icon_stock(icon_pos) model_name = self.screen.model_name if icon_stock == 'tryton-unstar': text = self.get_text() if not text: return name = common.ask(_('Bookmark Name:')) if not name: return domain = self.screen.domain_parser.parse(text) common.VIEW_SEARCH.add(model_name, name, domain) self.set_text(self.screen.domain_parser.string(domain)) elif icon_stock == 'tryton-star': id_ = self.bookmark_match() common.VIEW_SEARCH.remove(model_name, id_) # Refresh icon and bookmark button self.bookmark_match() self.but_bookmark.set_sensitive(bool(list(self.bookmarks()))) def focus_in(self, widget, event): self.update() self.search_entry.emit('changed') def search_box(self, widget): def window_hide(window, *args): window.hide() self.search_entry.grab_focus() def key_press(widget, event): if event.keyval == gtk.keysyms.Escape: window_hide(widget) return True return False def search(): self.search_window.hide() text = '' for label, entry in self.search_table.fields: if isinstance(entry, gtk.ComboBox): value = quote(entry.get_active_text()) or None elif isinstance(entry, (Dates, Selection)): value = entry.get_value() else: value = quote(entry.get_text()) or None if value is not None: text += quote(label) + ': ' + value + ' ' self.set_text(text) self.do_search() # Store text after doing the search # because domain parser could simplify the text self.last_search_text = self.get_text() if not self.search_window: self.search_window = gtk.Window() self.search_window.set_transient_for(widget.get_toplevel()) self.search_window.set_type_hint( gtk.gdk.WINDOW_TYPE_HINT_POPUP_MENU) self.search_window.set_destroy_with_parent(True) self.search_window.set_decorated(False) self.search_window.set_deletable(False) self.search_window.connect('delete-event', window_hide) self.search_window.connect('key-press-event', key_press) self.search_window.connect('focus-out-event', window_hide) def toggle_window_hide(combobox, shown): if combobox.props.popup_shown: self.search_window.handler_block_by_func(window_hide) else: self.search_window.handler_unblock_by_func(window_hide) vbox = gtk.VBox() fields = [ f for f in self.screen.domain_parser.fields.itervalues() if f.get('searchable', True) ] self.search_table = gtk.Table(rows=len(fields), columns=2) self.search_table.set_homogeneous(False) self.search_table.set_border_width(5) self.search_table.set_row_spacings(2) self.search_table.set_col_spacings(2) # Fill table with fields self.search_table.fields = [] for i, field in enumerate(fields): label = gtk.Label(field['string']) label.set_alignment(0.0, 0.0) self.search_table.attach(label, 0, 1, i, i + 1, yoptions=gtk.FILL) yoptions = False if field['type'] == 'boolean': if hasattr(gtk, 'ComboBoxText'): entry = gtk.ComboBoxText() else: entry = gtk.combo_box_new_text() entry.connect('notify::popup-shown', toggle_window_hide) entry.append_text('') selections = (_('True'), _('False')) for selection in selections: entry.append_text(selection) elif field['type'] == 'selection': selections = tuple(x[1] for x in field['selection']) entry = Selection(selections) yoptions = gtk.FILL | gtk.EXPAND elif field['type'] in ('date', 'datetime', 'time'): date_format = self.screen.context.get('date_format', '%x') if field['type'] == 'date': entry = Dates(date_format) elif field['type'] in ('datetime', 'time'): time_format = PYSONDecoder({}).decode(field['format']) if field['type'] == 'time': entry = Times(time_format) elif field['type'] == 'datetime': entry = DateTimes(date_format, time_format) entry.connect_activate(lambda *a: search()) else: entry = gtk.Entry() entry.connect('activate', lambda *a: search()) label.set_mnemonic_widget(entry) self.search_table.attach(entry, 1, 2, i, i + 1, yoptions=yoptions) self.search_table.fields.append((field['string'], entry)) scrolled = gtk.ScrolledWindow() scrolled.add_with_viewport(self.search_table) scrolled.set_shadow_type(gtk.SHADOW_NONE) scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) vbox.pack_start(scrolled, expand=True, fill=True) find_button = gtk.Button(_('Find')) find_button.connect('clicked', lambda *a: search()) find_img = gtk.Image() find_img.set_from_stock('tryton-find', gtk.ICON_SIZE_SMALL_TOOLBAR) find_button.set_image(find_img) hbuttonbox = gtk.HButtonBox() hbuttonbox.set_spacing(5) hbuttonbox.pack_start(find_button) hbuttonbox.set_layout(gtk.BUTTONBOX_END) vbox.pack_start(hbuttonbox, expand=False, fill=True) self.search_window.add(vbox) vbox.show_all() new_size = map( sum, zip(self.search_table.size_request(), scrolled.size_request())) self.search_window.set_default_size(*new_size) parent = widget.get_toplevel() widget_x, widget_y = widget.translate_coordinates(parent, 0, 0) widget_allocation = widget.get_allocation() # Resize the window to not be out of the parent width, height = self.search_window.get_default_size() allocation = parent.get_allocation() delta_width = allocation.width - (widget_x + width) delta_height = allocation.height - (widget_y + widget_allocation.height + height) if delta_width < 0: width += delta_width if delta_height < 0: height += delta_height self.search_window.resize(width, height) # Move the window under the button if hasattr(widget.window, 'get_root_coords'): x, y = widget.window.get_root_coords(widget_allocation.x, widget_allocation.y) else: x, y = widget.window.get_origin() self.search_window.move(x, y + widget_allocation.height) self.search_window.show() self.search_window.grab_focus() if self.last_search_text.strip() != self.get_text().strip(): for label, entry in self.search_table.fields: if isinstance(entry, gtk.ComboBox): entry.set_active(-1) elif isinstance(entry, Dates): entry.set_values(None, None) elif isinstance(entry, Selection): entry.treeview.get_selection().unselect_all() else: entry.set_text('') if self.search_table.fields: self.search_table.fields[0][1].grab_focus()
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(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() 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', lambda *a: 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.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.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_context(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, 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._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 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._position = 0 self._length = 0 self.title_box = hbox = gtk.HBox(homogeneous=False, spacing=0) hbox.set_border_width(2) label = gtk.Label(attrs.get("string", "")) label.set_alignment(0.0, 0.5) hbox.pack_start(label, expand=True, fill=True) hbox.pack_start(gtk.VSeparator(), expand=False, fill=True) tooltips = common.Tooltips() 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", lambda *a: 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) hbox.pack_start(gtk.VSeparator(), expand=False, fill=True) 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) 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) 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, readonly=self.attrs.get("readonly", False), exclude_field=attrs.get("relation_field", 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): 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): return self.screen.current_view.modified 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() 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()) # 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 _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.context_get(self.record)) sequence = None for view in self.screen.views: if view.view_type == "tree": sequence = view.attributes.get("sequence") if sequence: break 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) 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.context_get(first) def callback(result): if result: product[field.name] = result win_search = WinSearch(relation, callback, sel_multi=True, context=context, domain=domain) 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) 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) 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.context_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)] text = self.wid_text.get_text().decode("utf-8") self.focus_out = False sequence = None if self.screen.current_view.view_type == "tree": sequence = self.screen.current_view.attributes.get("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"), ) 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 readonly = False domain = [] size_limit = None if record: readonly = field.get_state_attrs(record).get("readonly", False) domain = field.domain_get(record) size_limit = record.expr_eval(self.attrs.get("size")) if self.screen.get_domain() != domain: self.screen.domain = domain self.screen.group.readonly = readonly 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._position = 0 hbox = gtk.HBox(homogeneous=False, spacing=0) hbox.set_border_width(2) label = gtk.Label(attrs.get('string', '')) label.set_alignment(0.0, 0.5) hbox.pack_start(label, 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', lambda *a: 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) 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'], view_ids=attrs.get('view_ids', '').split(','), mode=['tree'], views_preload=attrs.get('views', {}), row_activate=self._on_activate) self.screen.signal_connect(self, 'record-message', self._sig_label) vbox.pack_start(self.screen.widget, expand=True, fill=True) self.screen.widget.connect('key_press_event', self.on_keypress) self.wid_text.connect('key_press_event', self.on_keypress) def _color_widget(self): if hasattr(self.screen.current_view, 'treeview'): return self.screen.current_view.treeview return super(Many2Many, self)._color_widget() 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.screen.destroy() def color_set(self, name): super(Many2Many, 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 _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.context_get(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, view_ids=self.attrs.get('view_ids', '').split(','), views_preload=self.attrs.get('views', {}), new=self.attrs.get('create', True)) 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 _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 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.context_get(self.record) screen = Screen(self.attrs['relation'], domain=domain, view_ids=self.attrs.get('view_ids', '').split(','), mode=['form'], views_preload=self.attrs.get('views', {}), readonly=self.attrs.get('readonly', False), context=context) 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) def _sig_new(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.context_get(self.record) screen = Screen(self.attrs['relation'], domain=domain, view_ids=self.attrs.get('view_ids', '').split(','), mode=['form'], views_preload=self.attrs.get('views', {}), context=context) 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) def _readonly_set(self, value): self._readonly = value self._set_button_sensitive() self.wid_text.set_sensitive(not value) 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(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(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) 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) if not key_ids: continue values = RPCExecute('model', self.schema_model, 'get_keys', key_ids, context=context) if not values: continue except RPCException: pass 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 ScreenContainer(object): def __init__(self, tab_domain): self.viewport = gtk.Viewport() self.viewport.set_shadow_type(gtk.SHADOW_NONE) self.vbox = gtk.VBox(spacing=3) self.alternate_viewport = gtk.Viewport() self.alternate_viewport.set_shadow_type(gtk.SHADOW_NONE) self.alternate_view = False self.search_window = None self.search_table = None self.last_search_text = '' self.tab_domain = tab_domain or [] tooltips = common.Tooltips() self.filter_vbox = gtk.VBox(spacing=0) self.filter_vbox.set_border_width(0) hbox = gtk.HBox(homogeneous=False, spacing=0) self.filter_button = gtk.ToggleButton() self.filter_button.set_use_underline(True) self.filter_button.set_label(_('F_ilters')) self.filter_button.set_relief(gtk.RELIEF_NONE) self.filter_button.set_alignment(0.0, 0.5) self.filter_button.connect('toggled', self.search_box) hbox.pack_start(self.filter_button, expand=False, fill=False) self.search_entry = PlaceholderEntry() self.search_entry.set_placeholder_text(_('Search')) self.search_entry.set_alignment(0.0) self.completion = gtk.EntryCompletion() self.completion.set_model(gtk.ListStore(str)) self.completion.set_text_column(0) self.completion.props.inline_selection = True self.completion.props.popup_set_width = False self.completion.set_match_func(lambda *a: True) self.completion.connect('match-selected', self.match_selected) self.search_entry.connect('activate', self.activate) self.search_entry.set_completion(self.completion) self.search_entry.connect('key-press-event', self.key_press) self.search_entry.connect('focus-in-event', self.focus_in) self.search_entry.connect('icon-press', self.icon_press) hbox.pack_start(self.search_entry, expand=True, fill=True) def popup(widget): menu = widget._menu for child in menu.children(): menu.remove(child) if not widget.props.active: menu.popdown() return def menu_position(menu): x, y = widget.window.get_origin() widget_allocation = widget.get_allocation() return ( widget_allocation.x + x, widget_allocation.y + widget_allocation.height + y, False ) for id_, name, domain in self.bookmarks(): menuitem = gtk.MenuItem(name) menuitem.connect('activate', self.bookmark_activate, domain) menu.add(menuitem) menu.show_all() menu.popup(None, None, menu_position, 0, 0) def deactivate(menuitem, togglebutton): togglebutton.props.active = False but_bookmark = gtk.ToggleButton() self.but_bookmark = but_bookmark tooltips.set_tip(but_bookmark, _('Show bookmarks of filters')) img_bookmark = gtk.Image() img_bookmark.set_from_stock('tryton-bookmark', gtk.ICON_SIZE_SMALL_TOOLBAR) img_bookmark.set_alignment(0.5, 0.5) but_bookmark.add(img_bookmark) but_bookmark.set_relief(gtk.RELIEF_NONE) menu = gtk.Menu() menu.set_property('reserve-toggle-size', False) menu.connect('deactivate', deactivate, but_bookmark) but_bookmark._menu = menu but_bookmark.connect('toggled', popup) hbox.pack_start(but_bookmark, expand=False, fill=False) but_prev = gtk.Button() self.but_prev = but_prev tooltips.set_tip(but_prev, _('Previous')) but_prev.connect('clicked', self.search_prev) img_prev = gtk.Image() img_prev.set_from_stock('tryton-go-previous', gtk.ICON_SIZE_SMALL_TOOLBAR) img_prev.set_alignment(0.5, 0.5) but_prev.add(img_prev) but_prev.set_relief(gtk.RELIEF_NONE) hbox.pack_start(but_prev, expand=False, fill=False) but_next = gtk.Button() self.but_next = but_next tooltips.set_tip(but_next, _('Next')) but_next.connect('clicked', self.search_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) but_next.add(img_next) but_next.set_relief(gtk.RELIEF_NONE) hbox.pack_start(but_next, expand=False, fill=False) hbox.show_all() hbox.set_focus_chain([self.search_entry]) self.filter_vbox.pack_start(hbox, expand=False, fill=False) hseparator = gtk.HSeparator() hseparator.show() self.filter_vbox.pack_start(hseparator, expand=False, fill=False) if self.tab_domain: self.notebook = gtk.Notebook() self.notebook.props.homogeneous = True self.notebook.set_scrollable(True) for name, domain in self.tab_domain: label = gtk.Label('_' + name) label.set_use_underline(True) self.notebook.append_page(gtk.VBox(), label) self.filter_vbox.pack_start(self.notebook, expand=True, fill=True) self.notebook.show_all() # Set the current page before connecting to switch-page to not # trigger the search a second times. self.notebook.set_current_page(0) self.notebook.get_nth_page(0).pack_end(self.viewport) self.notebook.connect('switch-page', self.switch_page) self.notebook.connect_after('switch-page', self.switch_page_after) filter_expand = True else: self.notebook = None self.vbox.pack_end(self.viewport) filter_expand = False self.vbox.pack_start(self.filter_vbox, expand=filter_expand, fill=True) self.but_next.set_sensitive(False) self.but_prev.set_sensitive(False) tooltips.enable() def widget_get(self): return self.vbox def set_screen(self, screen): self.screen = screen self.but_bookmark.set_sensitive(bool(list(self.bookmarks()))) self.bookmark_match() def show_filter(self): if self.filter_vbox: self.filter_vbox.show() if self.notebook: self.notebook.set_show_tabs(True) if self.viewport in self.vbox.get_children(): self.vbox.remove(self.viewport) self.notebook.get_nth_page(self.notebook.get_current_page() ).pack_end(self.viewport) def hide_filter(self): if self.filter_vbox: self.filter_vbox.hide() if self.filter_button and self.filter_button.get_active(): self.filter_button.set_active(False) self.filter_button.toggled() if self.notebook: self.notebook.set_show_tabs(False) if self.viewport not in self.vbox.get_children(): self.notebook.get_nth_page(self.notebook.get_current_page() ).remove(self.viewport) self.vbox.pack_end(self.viewport) def set(self, widget): if self.alternate_view: if self.alternate_viewport.get_child(): self.alternate_viewport.remove( self.alternate_viewport.get_child()) if widget == self.viewport.get_child(): self.viewport.remove(self.viewport.get_child()) self.alternate_viewport.add(widget) self.alternate_viewport.show_all() return if self.viewport.get_child(): self.viewport.remove(self.viewport.get_child()) self.viewport.add(widget) self.viewport.show_all() def update(self): res = self.screen.search_complete(self.get_text()) model = self.completion.get_model() model.clear() for r in res: model.append([r.strip()]) def get_text(self): return self.search_entry.get_text().decode('utf-8') def set_text(self, value): self.search_entry.set_text(value) self.bookmark_match() def bookmarks(self): for id_, name, domain in common.VIEW_SEARCH[self.screen.model_name]: if self.screen.domain_parser.stringable(domain): yield id_, name, domain def bookmark_activate(self, menuitem, domain): self.set_text(self.screen.domain_parser.string(domain)) self.do_search() def bookmark_match(self): current_text = self.get_text() current_domain = self.screen.domain_parser.parse(current_text) self.search_entry.set_icon_activatable(gtk.ENTRY_ICON_SECONDARY, bool(current_text)) self.search_entry.set_icon_sensitive(gtk.ENTRY_ICON_SECONDARY, bool(current_text)) icon_stock = self.search_entry.get_icon_stock(gtk.ENTRY_ICON_SECONDARY) for id_, name, domain in self.bookmarks(): text = self.screen.domain_parser.string(domain) domain = self.screen.domain_parser.parse(text.decode('utf-8')) if (text == current_text or domain == current_domain): if icon_stock != 'tryton-star': self.search_entry.set_icon_from_stock( gtk.ENTRY_ICON_SECONDARY, 'tryton-star') self.search_entry.set_icon_tooltip_text( gtk.ENTRY_ICON_SECONDARY, _('Remove this bookmark')) return id_ if icon_stock != 'tryton-unstar': self.search_entry.set_icon_from_stock(gtk.ENTRY_ICON_SECONDARY, 'tryton-unstar') if current_text: self.search_entry.set_icon_tooltip_text(gtk.ENTRY_ICON_SECONDARY, _('Bookmark this filter')) else: self.search_entry.set_icon_tooltip_text(gtk.ENTRY_ICON_SECONDARY, None) def search_next(self, widget=None): self.screen.search_next(self.get_text()) def search_prev(self, widget=None): self.screen.search_prev(self.get_text()) def switch_page(self, notebook, page, page_num): current_page = notebook.get_nth_page(notebook.get_current_page()) current_page.remove(self.viewport) new_page = notebook.get_nth_page(page_num) new_page.pack_end(self.viewport) def switch_page_after(self, notebook, page, page_num): self.do_search() notebook.grab_focus() def get_tab_domain(self): if not self.notebook: return [] idx = self.notebook.get_current_page() if idx < 0: return [] ctx, domain = self.tab_domain[idx][1] decoder = PYSONDecoder(ctx) return decoder.decode(domain) def match_selected(self, completion, model, iter): def callback(): if not self.search_entry.props.window: return self.update() self.search_entry.emit('changed') gobject.idle_add(callback) def activate(self, widget): if not self.search_entry.get_selection_bounds(): self.do_search(widget) def do_search(self, widget=None): self.screen.search_filter(self.get_text()) def set_cursor(self, new=False, reset_view=True): if self.filter_vbox: self.search_entry.grab_focus() def key_press(self, widget, event): def keypress(): if not self.search_entry.props.window: return self.update() self.bookmark_match() gobject.idle_add(keypress) def icon_press(self, widget, icon_pos, event): if icon_pos == 1: icon_stock = self.search_entry.get_icon_stock(icon_pos) model_name = self.screen.model_name if icon_stock == 'tryton-unstar': text = self.get_text() if not text: return name = common.ask(_('Bookmark Name:')) if not name: return domain = self.screen.domain_parser.parse(text) common.VIEW_SEARCH.add(model_name, name, domain) self.set_text(self.screen.domain_parser.string(domain)) elif icon_stock == 'tryton-star': id_ = self.bookmark_match() common.VIEW_SEARCH.remove(model_name, id_) # Refresh icon and bookmark button self.bookmark_match() self.but_bookmark.set_sensitive(bool(list(self.bookmarks()))) def focus_in(self, widget, event): self.update() self.search_entry.emit('changed') def search_box(self, button): def key_press(window, event): if event.keyval == gtk.keysyms.Escape: button.set_active(False) window.hide() def search(): button.set_active(False) self.search_window.hide() text = '' for label, entry in self.search_table.fields: if isinstance(entry, gtk.ComboBox): value = quote(entry.get_active_text()) or None elif isinstance(entry, (Dates, Selection)): value = entry.get_value() else: value = quote(entry.get_text()) or None if value is not None: text += quote(label) + ': ' + value + ' ' self.set_text(text) self.do_search() # Store text after doing the search # because domain parser could simplify the text self.last_search_text = self.get_text() if not self.search_window: self.search_window = gtk.Window() self.search_window.set_transient_for(button.get_toplevel()) self.search_window.set_type_hint( gtk.gdk.WINDOW_TYPE_HINT_POPUP_MENU) self.search_window.set_destroy_with_parent(True) self.search_window.set_title('coog') self.search_window.set_icon(TRYTON_ICON) self.search_window.set_decorated(False) # set_deletable is False on tryton master repo # But this is not working on each graphical environnement # Further more, setting theses windows deletable does not seems # to bring any trouble. self.search_window.set_deletable(True) self.search_window.connect('delete-event', lambda *a: True) self.search_window.connect('key-press-event', key_press) vbox = gtk.VBox() fields = [f for f in self.screen.domain_parser.fields.itervalues() if f.get('searchable', True)] self.search_table = gtk.Table(rows=len(fields), columns=2) self.search_table.set_homogeneous(False) self.search_table.set_border_width(5) self.search_table.set_row_spacings(2) self.search_table.set_col_spacings(2) # Fill table with fields self.search_table.fields = [] for i, field in enumerate(fields): label = gtk.Label(field['string']) label.set_alignment(0.0, 0.0) self.search_table.attach(label, 0, 1, i, i + 1, yoptions=gtk.FILL) yoptions = False if field['type'] == 'boolean': if hasattr(gtk, 'ComboBoxText'): entry = gtk.ComboBoxText() else: entry = gtk.combo_box_new_text() entry.append_text('') selections = (_('True'), _('False')) for selection in selections: entry.append_text(selection) elif field['type'] == 'selection': selections = tuple(x[1] for x in field['selection']) entry = Selection(selections) yoptions = gtk.FILL | gtk.EXPAND elif field['type'] in ('date', 'datetime', 'time'): date_format = self.screen.context.get('date_format', '%x') if field['type'] == 'date': entry = Dates(date_format) elif field['type'] in ('datetime', 'time'): time_format = PYSONDecoder({}).decode(field['format']) if field['type'] == 'time': entry = Times(time_format) elif field['type'] == 'datetime': entry = DateTimes(date_format, time_format) entry.connect_activate(lambda *a: search()) else: entry = gtk.Entry() entry.connect('activate', lambda *a: search()) label.set_mnemonic_widget(entry) self.search_table.attach(entry, 1, 2, i, i + 1, yoptions=yoptions) self.search_table.fields.append((field['string'], entry)) scrolled = gtk.ScrolledWindow() scrolled.add_with_viewport(self.search_table) scrolled.set_shadow_type(gtk.SHADOW_NONE) scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) vbox.pack_start(scrolled, expand=True, fill=True) find_button = gtk.Button(_('Find')) find_button.connect('clicked', lambda *a: search()) find_img = gtk.Image() find_img.set_from_stock('tryton-find', gtk.ICON_SIZE_SMALL_TOOLBAR) find_button.set_image(find_img) hbuttonbox = gtk.HButtonBox() hbuttonbox.set_spacing(5) hbuttonbox.pack_start(find_button) hbuttonbox.set_layout(gtk.BUTTONBOX_END) vbox.pack_start(hbuttonbox, expand=False, fill=True) self.search_window.add(vbox) vbox.show_all() new_size = map(sum, zip(self.search_table.size_request(), scrolled.size_request())) self.search_window.set_default_size(*new_size) parent = button.get_toplevel() button_x, button_y = button.translate_coordinates(parent, 0, 0) button_allocation = button.get_allocation() # Resize the window to not be out of the parent width, height = self.search_window.get_default_size() allocation = parent.get_allocation() delta_width = allocation.width - (button_x + width) delta_height = allocation.height - (button_y + button_allocation.height + height) if delta_width < 0: width += delta_width if delta_height < 0: height += delta_height self.search_window.resize(width, height) # Move the window under the button x, y = button.window.get_origin() self.search_window.move( x + button_allocation.x, y + button_allocation.y + button_allocation.height) from tryton.gui.main import Main page = Main.get_main().get_page() if button.get_active(): if page and self.search_window not in page.dialogs: page.dialogs.append(self.search_window) self.search_window.show() if self.last_search_text.strip() != self.get_text().strip(): for label, entry in self.search_table.fields: if isinstance(entry, gtk.ComboBox): entry.set_active(-1) elif isinstance(entry, Dates): entry.set_values(None, None) elif isinstance(entry, Selection): entry.treeview.get_selection().unselect_all() else: entry.set_text('') if self.search_table.fields: self.search_table.fields[0][1].grab_focus() else: self.search_window.hide() if page and self.search_window in page.dialogs: page.dialogs.remove(self.search_window)
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 Many2Many(WidgetInterface): def __init__(self, field_name, model_name, attrs=None): super(Many2Many, self).__init__(field_name, model_name, attrs=attrs) self.widget = gtk.VBox(homogeneous=False, spacing=5) self._readonly = True self._position = 0 hbox = gtk.HBox(homogeneous=False, spacing=0) hbox.set_border_width(2) label = gtk.Label(attrs.get('string', '')) label.set_alignment(0.0, 0.5) hbox.pack_start(label, 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', lambda *a: 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() 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) self.widget.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) self.screen.signal_connect(self, 'record-message', self._sig_label) self.widget.pack_start(self.screen.widget, expand=True, fill=True) self.screen.widget.connect('key_press_event', self.on_keypress) self.wid_text.connect('key_press_event', self.on_keypress) def _color_widget(self): if hasattr(self.screen.current_view, 'widget_tree'): return self.screen.current_view.widget_tree return super(Many2Many, self)._color_widget() def grab_focus(self): return self.wid_text.grab_focus() def on_keypress(self, widget, event): 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 event.keyval == gtk.keysyms.F3: self._sig_add() return True if event.keyval == gtk.keysyms.F2 \ and widget == self.screen.widget: self._sig_edit() return True if event.keyval in (gtk.keysyms.Delete, gtk.keysyms.KP_Delete) \ and widget == self.screen.widget: self._sig_remove() return True 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): self.screen.destroy() self.widget.destroy() del self.widget def color_set(self, name): super(Many2Many, 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 _sig_add(self, *args, **kwargs): if not self.focus_out: return domain = self.field.domain_get(self.record) context = self.field.context_get(self.record) value = self.wid_text.get_text() self.focus_out = False try: if value: dom = [('rec_name', 'ilike', '%' + value + '%'), domain] else: dom = domain ids = RPCExecute('model', self.attrs['relation'], 'search', dom, 0, CONFIG['client.limit'], None, context=context) except RPCException: self.focus_out = True return 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('') if len(ids) != 1 or not value or kwargs.get('win_search', False): WinSearch(self.attrs['relation'], callback, sel_multi=True, ids=ids, context=context, domain=domain, view_ids=self.attrs.get('view_ids', '').split(','), views_preload=self.attrs.get('views', {}), new=self.attrs.get('create', True)) else: callback([(i, None) for i in ids]) def _sig_remove(self, *args): self.screen.remove(remove=True) def _on_activate(self): self._sig_edit() def _sig_edit(self): if self.screen.current_record: def callback(result): if result: self.screen.current_record.save() else: self.screen.current_record.cancel() WinForm(self.screen, callback) def _readonly_set(self, value): self._readonly = value self._set_button_sensitive() 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.wid_text.set_sensitive(not self._readonly) 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 = True 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.save_tree_state() 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(win_search=True) self.wid_text.grab_focus() elif index == 1: model = self.attrs['relation'] domain = self.field.domain_get(self.record) context = self.field.context_get(self.record) screen = Screen(model, domain, context=context, mode=['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)
class One2Many(WidgetInterface): def __init__(self, field_name, model_name, attrs=None): super(One2Many, self).__init__(field_name, model_name, attrs=attrs) self.widget = gtk.VBox(homogeneous=False, spacing=2) self._readonly = True self._position = 0 self._length = 0 hbox = gtk.HBox(homogeneous=False, spacing=0) hbox.set_border_width(2) label = gtk.Label(attrs.get('string', '')) label.set_alignment(0.0, 0.5) hbox.pack_start(label, expand=True, fill=True) hbox.pack_start(gtk.VSeparator(), expand=False, fill=True) tooltips = common.Tooltips() 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', lambda *a: self._focus_out()) hbox.pack_start(self.wid_text, expand=True, fill=True) if int(self.attrs.get('completion', 1)): self.wid_completion = get_completion() 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) hbox.pack_start(gtk.VSeparator(), expand=False, fill=True) 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) 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) 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) frame.set_shadow_type(gtk.SHADOW_OUT) self.widget.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)) self.screen.pre_validate = bool(int(attrs.get('pre_validate', 0))) self.screen.signal_connect(self, 'record-message', self._sig_label) self.widget.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, 'widget_tree'): return self.screen.current_view.widget_tree return super(One2Many, self)._color_widget() def grab_focus(self): return self.screen.widget.grab_focus() 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): 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): return self.screen.current_view.modified 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() 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: size_limit = False self.but_new.set_sensitive(bool( not self._readonly and self.attrs.get('create', True) and not size_limit and access['create'])) 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'])) 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.wid_text.set_sensitive(not self._readonly) 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'])) def _validate(self): self.view.set_value() 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 _sig_new(self, widget=None): if not common.MODELACCESS[self.screen.model_name]['create']: return if not self._validate(): return ctx = {} ctx.update(self.field.context_get(self.record)) sequence = None if self.screen.current_view.view_type == 'tree': sequence = self.screen.current_view.widget_tree.sequence def update_sequence(): if sequence: self.screen.group.set_sequence(field=sequence) if self.screen.current_view.editable: self.screen.new() self.screen.current_view.widget.set_sensitive(True) update_sequence() 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) def _sig_edit(self, widget=None): if not common.MODELACCESS[self.screen.model_name]['read']: return if not self._validate(): return record = self.screen.current_record if record: WinForm(self.screen, lambda a: None) 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, **kwargs): if not self.focus_out: return access = common.MODELACCESS[self.screen.model_name] if not access['write'] or not access['read']: return self.view.set_value() domain = self.field.domain_get(self.record) context = self.field.context_get(self.record) domain = [domain, self.record.expr_eval(self.attrs.get('add_remove'))] removed_ids = self.field.get_removed_ids(self.record) self.focus_out = False try: if self.wid_text.get_text(): dom = [('rec_name', 'ilike', '%' + self.wid_text.get_text() + '%'), ['OR', domain, ('id', 'in', removed_ids)]] else: dom = ['OR', domain, ('id', 'in', removed_ids)] ids = RPCExecute('model', self.attrs['relation'], 'search', dom, 0, CONFIG['client.limit'], None, context=context) except RPCException: self.focus_out = True return False sequence = None if self.screen.current_view.view_type == 'tree': sequence = self.screen.current_view.widget_tree.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('') if len(ids) != 1 or kwargs.get('win_search', False): WinSearch(self.attrs['relation'], callback, sel_multi=True, ids=ids, 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')) else: callback([(i, None) for i in ids]) 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 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 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 readonly = False domain = [] size_limit = None if record: readonly = field.get_state_attrs(record).get('readonly', False) domain = field.domain_get(record) size_limit = record.expr_eval(self.attrs.get('size')) if self.screen.domain != domain: self.screen.domain = domain if not self.screen.group.readonly and readonly: self.screen.group.readonly = readonly self.screen.size_limit = size_limit self.screen.display() return True def set_value(self, record, field): self.screen.save_tree_state() 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(win_search=True) self.wid_text.grab_focus() elif index == 1: self._sig_new()
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._position = 0 self._length = 0 self.title_box = hbox = gtk.HBox(homogeneous=False, spacing=0) hbox.set_border_width(2) label = gtk.Label(attrs.get('string', '')) label.set_alignment(0.0, 0.5) hbox.pack_start(label, expand=True, fill=True) hbox.pack_start(gtk.VSeparator(), expand=False, fill=True) tooltips = common.Tooltips() 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', lambda *a: 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) hbox.pack_start(gtk.VSeparator(), expand=False, fill=True) 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) 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) 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) 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, readonly=self.attrs.get('readonly', False), exclude_field=attrs.get('relation_field', None)) self.screen.pre_validate = bool(int(attrs.get('pre_validate', 0))) self.screen.signal_connect(self, 'record-message', self._sig_label) 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 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): self.screen.destroy() def _on_activate(self): self._sig_edit() def switch_view(self, widget): self.screen.switch_view() @property def modified(self): return self.screen.current_view.modified def _readonly_set(self, value): self._readonly = value self._set_button_sensitive() 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 self.but_new.set_sensitive(bool( not self._readonly and self.attrs.get('create', True) and not size_limit and access['create'])) 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'])) 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()) # 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): self.view.set_value() 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 _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.context_get(self.record)) sequence = None for view in self.screen.views: if view.view_type == 'tree': sequence = view.attributes.get('sequence') if sequence: break def update_sequence(): if sequence: self.screen.group.set_sequence(field=sequence) if self.screen.current_view.editable: self.screen.new() self.screen.current_view.widget.set_sensitive(True) update_sequence() 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) 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.context_get(first) def callback(result): if result: product[field.name] = result win_search = WinSearch(relation, callback, sel_multi=True, context=context, domain=domain) 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) search_set() def _sig_edit(self, widget=None): if not common.MODELACCESS[self.screen.model_name]['read']: return if not self._validate(): return record = self.screen.current_record if record: WinForm(self.screen, lambda a: None) 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 self.view.set_value() domain = self.field.domain_get(self.record) context = self.field.context_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)] text = self.wid_text.get_text().decode('utf-8') self.focus_out = False sequence = None if self.screen.current_view.view_type == 'tree': sequence = self.screen.current_view.attributes.get('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')) 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 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 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 readonly = False domain = [] size_limit = None if record: readonly = field.get_state_attrs(record).get('readonly', False) domain = field.domain_get(record) size_limit = record.expr_eval(self.attrs.get('size')) if self.screen.domain != domain: self.screen.domain = domain self.screen.group.readonly = readonly self.screen.size_limit = size_limit self.screen.display() return True def set_value(self, record, field): 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(WidgetInterface): def __init__(self, field_name, model_name, attrs=None): super(Many2Many, self).__init__(field_name, model_name, attrs=attrs) self.widget = gtk.VBox(homogeneous=False, spacing=5) self._readonly = True self._position = 0 hbox = gtk.HBox(homogeneous=False, spacing=0) hbox.set_border_width(2) label = gtk.Label(attrs.get('string', '')) label.set_alignment(0.0, 0.5) hbox.pack_start(label, 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', lambda *a: 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() 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) self.widget.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) self.screen.signal_connect(self, 'record-message', self._sig_label) self.widget.pack_start(self.screen.widget, expand=True, fill=True) self.screen.widget.connect('key_press_event', self.on_keypress) self.wid_text.connect('key_press_event', self.on_keypress) def _color_widget(self): if hasattr(self.screen.current_view, 'widget_tree'): return self.screen.current_view.widget_tree return super(Many2Many, self)._color_widget() def grab_focus(self): return self.wid_text.grab_focus() def on_keypress(self, widget, event): 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 event.keyval == gtk.keysyms.F3: self._sig_add() return True if event.keyval == gtk.keysyms.F2 \ and widget == self.screen.widget: self._sig_edit() return True if event.keyval in (gtk.keysyms.Delete, gtk.keysyms.KP_Delete) \ and widget == self.screen.widget: self._sig_remove() return True 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): self.screen.destroy() self.widget.destroy() del self.widget def color_set(self, name): super(Many2Many, 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 _sig_add(self, *args, **kwargs): if not self.focus_out: return domain = self.field.domain_get(self.record) context = self.field.context_get(self.record) value = self.wid_text.get_text() self.focus_out = False try: if value: dom = [('rec_name', 'ilike', '%' + value + '%'), domain] else: dom = domain ids = RPCExecute('model', self.attrs['relation'], 'search', dom, 0, CONFIG['client.limit'], None, context=context) except RPCException: self.focus_out = True return 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('') if len(ids) != 1 or not value or kwargs.get('win_search', False): WinSearch(self.attrs['relation'], callback, sel_multi=True, ids=ids, context=context, domain=domain, view_ids=self.attrs.get('view_ids', '').split(','), views_preload=self.attrs.get('views', {}), new=self.attrs.get('create', True)) else: callback([(i, None) for i in ids]) def _sig_remove(self, *args): self.screen.remove(remove=True) def _on_activate(self): self._sig_edit() def _sig_edit(self): if self.screen.current_record: def callback(result): if result: self.screen.current_record.save() else: self.screen.current_record.cancel() WinForm(self.screen, callback) def _readonly_set(self, value): self._readonly = value self._set_button_sensitive() 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.wid_text.set_sensitive(not self._readonly) 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.save_tree_state() 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(win_search=True) self.wid_text.grab_focus() elif index == 1: model = self.attrs['relation'] domain = self.field.domain_get(self.record) context = self.field.context_get(self.record) screen = Screen(model, domain, context=context, mode=['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)
class One2Many(WidgetInterface): def __init__(self, field_name, model_name, attrs=None): super(One2Many, self).__init__(field_name, model_name, attrs=attrs) self.widget = gtk.VBox(homogeneous=False, spacing=2) self._readonly = True self._position = 0 self._length = 0 hbox = gtk.HBox(homogeneous=False, spacing=0) hbox.set_border_width(2) label = gtk.Label(attrs.get('string', '')) label.set_alignment(0.0, 0.5) hbox.pack_start(label, expand=True, fill=True) hbox.pack_start(gtk.VSeparator(), expand=False, fill=True) tooltips = common.Tooltips() 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', lambda *a: self._focus_out()) hbox.pack_start(self.wid_text, expand=True, fill=True) if int(self.attrs.get('completion', 1)): self.wid_completion = get_completion() 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) hbox.pack_start(gtk.VSeparator(), expand=False, fill=True) 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) 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) 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) frame.set_shadow_type(gtk.SHADOW_OUT) self.widget.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)) self.screen.pre_validate = bool(int(attrs.get('pre_validate', 0))) self.screen.signal_connect(self, 'record-message', self._sig_label) self.widget.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, 'widget_tree'): return self.screen.current_view.widget_tree return super(One2Many, self)._color_widget() def grab_focus(self): return self.screen.widget.grab_focus() 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): 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): return self.screen.current_view.modified 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() 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: size_limit = False self.but_new.set_sensitive(bool( not self._readonly and self.attrs.get('create', True) and not size_limit and access['create'])) 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'])) 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.wid_text.set_sensitive(not self._readonly) 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'])) def _validate(self): self.view.set_value() record = self.screen.current_record if record: fields = self.screen.current_view.get_fields() if not record.validate(fields): self.screen.display() return False if self.screen.pre_validate and not record.pre_validate(): return False return True def _sig_new(self, widget=None): if not common.MODELACCESS[self.screen.model_name]['create']: return if not self._validate(): return ctx = {} ctx.update(self.field.context_get(self.record)) sequence = None if self.screen.current_view.view_type == 'tree': sequence = self.screen.current_view.widget_tree.sequence def update_sequence(): if sequence: self.screen.group.set_sequence(field=sequence) if (self.screen.current_view.view_type == 'form') \ or self.screen.editable_get(): self.screen.new() self.screen.current_view.widget.set_sensitive(True) update_sequence() 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) def _sig_edit(self, widget=None): if not common.MODELACCESS[self.screen.model_name]['read']: return if not self._validate(): return record = self.screen.current_record if record: WinForm(self.screen, lambda a: None) 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, **kwargs): if not self.focus_out: return access = common.MODELACCESS[self.screen.model_name] if not access['write'] or not access['read']: return self.view.set_value() domain = self.field.domain_get(self.record) context = self.field.context_get(self.record) domain = domain[:] domain.extend(self.record.expr_eval(self.attrs.get('add_remove'))) removed_ids = self.field.get_removed_ids(self.record) self.focus_out = False try: if self.wid_text.get_text(): dom = [('rec_name', 'ilike', '%' + self.wid_text.get_text() + '%'), ['OR', domain, ('id', 'in', removed_ids)]] else: dom = ['OR', domain, ('id', 'in', removed_ids)] ids = RPCExecute('model', self.attrs['relation'], 'search', dom, 0, CONFIG['client.limit'], None, context=context) except RPCException: self.focus_out = True return False sequence = None if self.screen.current_view.view_type == 'tree': sequence = self.screen.current_view.widget_tree.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('') if len(ids) != 1 or kwargs.get('win_search', False): WinSearch(self.attrs['relation'], callback, sel_multi=True, ids=ids, 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')) else: callback([(i, None) for i in ids]) 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 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 = True self.screen.display() return False new_group = field.get_client(record) if id(self.screen.group) != id(new_group): self.screen.group = new_group if (self.screen.current_view.view_type == 'tree') \ and self.screen.editable_get(): self.screen.current_record = None readonly = False domain = [] size_limit = None if record: readonly = field.get_state_attrs(record).get('readonly', False) domain = field.domain_get(record) size_limit = record.expr_eval(self.attrs.get('size')) if self.screen.domain != domain: self.screen.domain = domain if not self.screen.group.readonly and readonly: self.screen.group.readonly = readonly self.screen.size_limit = size_limit self.screen.display() return True def set_value(self, record, field): self.screen.save_tree_state() 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[:] domain.extend(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(win_search=True) self.wid_text.grab_focus() elif index == 1: self._sig_new()