diff --git a/tryton/gui/window/view_form/model/__init__.py b/tryton/gui/window/view_form/model/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tryton/gui/window/view_form/model/field.py b/tryton/gui/window/view_form/model/field.py new file mode 100644 index 0000000000..fefe4b3987 --- /dev/null +++ b/tryton/gui/window/view_form/model/field.py @@ -0,0 +1,369 @@ +from tryton.rpc import RPCProxy +import tryton.rpc as rpc + +class ModelField(object): + ''' + get: return the values to write to the server + get_client: return the value for the client widget (form_gtk) + set: save the value from the server + set_client: save the value from the widget + ''' + + def __new__(cls, ctype): + klass = TYPES.get(ctype, CharField) + return klass + + +class CharField(object): + + def __init__(self, parent, attrs): + self.parent = parent + self.attrs = attrs + self.name = attrs['name'] + self.internal = False + self.default_attrs = {} + + def sig_changed(self, model): + if self.get_state_attrs(model).get('readonly', False): + return + if self.attrs.get('on_change', False): + model.on_change(self.attrs['on_change']) + if self.attrs.get('change_default', False): + model.cond_default(self.attrs['name'], self.get(model)) + + def domain_get(self, model): + dom = self.attrs.get('domain', '[]') + return model.expr_eval(dom) + + def context_get(self, model, check_load=True, eval_context=True): + context = {} + context.update(self.parent.context) + if eval_context: + field_context_str = self.attrs.get('context', '{}') or '{}' + field_context = model.expr_eval('dict(%s)' % field_context_str, + check_load=check_load) + context.update(field_context) + return context + + def validate(self, model): + res = True + if bool(int(self.get_state_attrs(model).get('required', 0))): + if not model.value[self.name]: + res = False + self.get_state_attrs(model)['valid'] = res + return res + + def set(self, model, value, test_state=True, modified=False): + model.value[self.name] = value + if modified: + model.modified = True + model.modified_fields.setdefault(self.name) + return True + + def get(self, model, check_load=True, readonly=True, modified=False): + return model.value.get(self.name, False) or False + + def set_client(self, model, value, test_state=True, force_change=False): + internal = model.value.get(self.name, False) + self.set(model, value, test_state) + if (internal or False) != (model.value.get(self.name, False) or False): + model.modified = True + model.modified_fields.setdefault(self.name) + self.sig_changed(model) + model.signal('record-changed', model) + + def get_client(self, model): + return model.value[self.name] or False + + def set_default(self, model, value): + res = self.set(model, value) + if self.attrs.get('on_change', False): + model.on_change(self.attrs['on_change']) + return res + + def get_default(self, model): + return self.get(model) + + def create(self, model): + return False + + def state_set(self, model, state='draft'): + state_changes = dict(self.attrs.get('states', {}).get(state, [])) + for key in ('readonly', 'required'): + if key in state_changes: + self.get_state_attrs(model)[key] = state_changes[key] + else: + self.get_state_attrs(model)[key] = self.attrs[key] + if 'value' in state_changes: + self.set(model, state_changes['value'], test_state=False, + modified=True) + + def get_state_attrs(self, model): + if self.name not in model.state_attrs: + model.state_attrs[self.name] = self.attrs.copy() + return model.state_attrs[self.name] + + +class SelectionField(CharField): + + def set(self, model, value, test_state=True, modified=False): + if value in [sel[0] for sel in self.attrs['selection']]: + super(SelectionField, self).set(model, value, test_state, modified) + + +class FloatField(CharField): + + def validate(self, model): + self.get_state_attrs(model)['valid'] = True + return True + + def set_client(self, model, value, test_state=True, force_change=False): + internal = model.value[self.name] + self.set(model, value, test_state) + if abs(float(internal or 0.0) - float(model.value[self.name] or 0.0)) \ + >= (10.0**(-int(self.attrs.get('digits', (12,4))[1]))): + if not self.get_state_attrs(model).get('readonly', False): + model.modified = True + model.modified_fields.setdefault(self.name) + self.sig_changed(model) + model.signal('record-changed', model) + + +class IntegerField(CharField): + + def get(self, model, check_load=True, readonly=True, modified=False): + return model.value.get(self.name, 0) or 0 + + def get_client(self, model): + return model.value[self.name] or 0 + + def validate(self, model): + self.get_state_attrs(model)['valid'] = True + return True + + +class M2OField(CharField): + ''' + internal = (id, name) + ''' + + def create(self, model): + return False + + def get(self, model, check_load=True, readonly=True, modified=False): + if model.value[self.name]: + return model.value[self.name][0] or False + return False + + def get_client(self, model): + #model._check_load() + if model.value[self.name]: + return model.value[self.name][1] + return False + + def set(self, model, value, test_state=False, modified=False): + if value and isinstance(value, (int, str, unicode, long)): + rpc2 = RPCProxy(self.attrs['relation']) + result = rpc2.name_get([value], rpc.session.context) + model.value[self.name] = result[0] + else: + model.value[self.name] = value + if modified: + model.modified = True + model.modified_fields.setdefault(self.name) + + def set_client(self, model, value, test_state=False, force_change=False): + internal = model.value[self.name] + self.set(model, value, test_state) + if internal != model.value[self.name]: + model.modified = True + model.modified_fields.setdefault(self.name) + self.sig_changed(model) + model.signal('record-changed', model) + elif force_change: + self.sig_changed(model) + + +class M2MField(CharField): + ''' + internal = [id] + ''' + + def __init__(self, parent, attrs): + super(M2MField, self).__init__(parent, attrs) + + def create(self, model): + return [] + + def get(self, model, check_load=True, readonly=True, modified=False): + return [(6, 0, model.value[self.name] or [])] + + def get_client(self, model): + return model.value[self.name] or [] + + def set(self, model, value, test_state=False, modified=False): + model.value[self.name] = value or [] + if modified: + model.modified = True + model.modified_fields.setdefault(self.name) + + def set_client(self, model, value, test_state=False, force_change=False): + internal = model.value[self.name] + self.set(model, value, test_state, modified=False) + if set(internal) != set(value): + model.modified = True + model.modified_fields.setdefault(self.name) + self.sig_changed(model) + model.signal('record-changed', model) + + def get_default(self, model): + return self.get_client(model) + + +class O2MField(CharField): + ''' + internal = ModelRecordGroup of the related objects + ''' + + def __init__(self, parent, attrs): + super(O2MField, self).__init__(parent, attrs) + self.context = {} + + def create(self, model): + from group import ModelRecordGroup + mod = ModelRecordGroup(resource=self.attrs['relation'], + fields={}, parent=model) + mod.signal_connect(mod, 'model-changed', self._model_changed) + return mod + + def _model_changed(self, group, model): + model.parent.modified = True + model.parent.modified_fields.setdefault(self.name) + self.sig_changed(model.parent) + self.parent.signal('record-changed', model) + + def get_client(self, model): + return model.value[self.name] + + def get(self, model, check_load=True, readonly=True, modified=False): + if not model.value[self.name]: + return [] + result = [] + for model2 in model.value[self.name].models: + if (modified and not model2.is_modified()) or \ + (not model2.id and not model2.is_modified()): + continue + if model2.id: + result.append((1, model2.id, + model2.get(check_load=check_load, get_readonly=readonly))) + else: + result.append((0, 0, + model2.get(check_load=check_load, get_readonly=readonly))) + for rm_id in model.value[self.name].model_removed: + result.append((2, rm_id, False)) + return result + + def set(self, model, value, test_state=False, modified=False): + from group import ModelRecordGroup + mod = ModelRecordGroup(resource=self.attrs['relation'], + fields={}, parent=model) + mod.signal_connect(mod, 'model-changed', self._model_changed) + model.value[self.name] = mod + #self.internal.signal_connect(self.internal, 'model-changed', + # self._model_changed) + model.value[self.name].pre_load(value, display=False) + #self.internal.signal_connect(self.internal, 'model-changed', + # self._model_changed) + + def set_client(self, model, value, test_state=False, force_change=False): + self.set(model, value, test_state=test_state) + model.signal('record-changed', model) + + def set_default(self, model, value): + from group import ModelRecordGroup + fields = {} + if value and len(value): + context = self.context_get(model) + rpc2 = RPCProxy(self.attrs['relation']) + fields = rpc2.fields_get(value[0].keys(), context) + + model.value[self.name] = ModelRecordGroup( + resource=self.attrs['relation'], fields=fields, parent=model) + model.value[self.name].signal_connect(model.value[self.name], + 'model-changed', self._model_changed) + mod = None + for record in (value or []): + mod = model.value[self.name].model_new(default=False) + mod.set_default(record) + model.value[self.name].model_add(mod) + model.value[self.name].current_model = mod + #mod.signal('record-changed') + return True + + def get_default(self, model): + res = [x.get_default() for x in model.value[self.name].models or []] + return res + + def validate(self, model): + res = True + for model2 in model.value[self.name].models: + if not model2.validate(): + if not model2.is_modified(): + model.value[self.name].models.remove(model2) + else: + res = False + if not super(O2MField, self).validate(model): + res = False + self.get_state_attrs(model)['valid'] = res + return res + + +class ReferenceField(CharField): + + def get_client(self, model): + if model.value[self.name]: + return model.value[self.name] + return False + + def get(self, model, check_load=True, readonly=True, modified=False): + if model.value[self.name]: + return '%s,%d' % (model.value[self.name][0], + model.value[self.name][1][0]) + return False + + def set_client(self, model, value, test_state=False, force_change=False): + internal = model.value[self.name] + model.value[self.name] = value + if (internal or False) != (model.value[self.name] or False): + model.modified = True + model.modified_fields.setdefault(self.name) + self.sig_changed(model) + model.signal('record-changed', model) + + def set(self, model, value, test_state=False, modified=False): + if not value: + model.value[self.name] = False + return + ref_model, ref_id = value.split(',') + rpc2 = RPCProxy(ref_model) + result = rpc2.name_get([ref_id], rpc.session.context) + if result: + model.value[self.name] = ref_model, result[0] + else: + model.value[self.name] = False + if modified: + model.modified = True + model.modified_fields.setdefault(self.name) + +TYPES = { + 'char' : CharField, + 'float_time': FloatField, + 'integer' : IntegerField, + 'float' : FloatField, + 'many2one' : M2OField, + 'many2many' : M2MField, + 'one2many' : O2MField, + 'reference' : ReferenceField, + 'selection': SelectionField, + 'boolean': IntegerField, +} diff --git a/tryton/gui/window/view_form/model/group.py b/tryton/gui/window/view_form/model/group.py new file mode 100644 index 0000000000..203ed1cb90 --- /dev/null +++ b/tryton/gui/window/view_form/model/group.py @@ -0,0 +1,282 @@ +from tryton.rpc import RPCProxy +import tryton.rpc as rpc +from record import ModelRecord +import field +from tryton.signal_event import SignalEvent + + +class ModelList(list): + def __init__(self, screen): + super(ModelList, self).__init__() + self.lock_signal = False + self.__screen = screen + + def insert(self, pos, obj): + super(ModelList, self).insert(pos, obj) + if not self.lock_signal: + self.__screen.signal('record-changed', ('record-added', pos)) + + def append(self, obj): + super(ModelList, self).append(obj) + if not self.lock_signal: + self.__screen.signal('record-changed', ('record-added', -1)) + + def remove(self, obj): + idx = self.index(obj) + super(ModelList, self).remove(obj) + if not self.lock_signal: + self.__screen.signal('record-changed', ('record-removed', idx)) + + def clear(self): + while self: + self.pop() + if not self.lock_signal: + self.__screen.signal('record-changed', + ('record-removed', len(self))) + + def __setitem__(self, key, value): + super(ModelList, self).__setitem__(key, value) + if not self.lock_signal: + self.__screen.signal('record-changed', ('record-changed', key)) + + +class ModelRecordGroup(SignalEvent): + + def __init__(self, resource, fields, ids=None, parent=None, context=None): + super(ModelRecordGroup, self).__init__() + self.parent = parent + self._context = context or {} + self._context.update(rpc.session.context) + self.resource = resource + self.rpc = RPCProxy(resource) + self.fields = fields + self.mfields = {} + ModelRecordGroup.mfields_load(fields.keys(), self) + self.models = ModelList(self) + self.current_idx = None + self.load(ids) + self.model_removed = [] + self.on_write = '' + + @staticmethod + def mfields_load(fkeys, models): + for fname in fkeys: + fvalue = models.fields[fname] + modelfield = field.ModelField(fvalue['type']) + fvalue['name'] = fname + models.mfields[fname] = modelfield(models, fvalue) + + def save(self): + for model in self.models: + saved = model.save() + self.writen(saved) + + def writen(self, edited_id): + if not self.on_write: + return + new_ids = getattr(self.rpc, self.on_write)(edited_id, self.context) + model_idx = self.models.index(self[edited_id]) + result = False + for new_id in new_ids: + cont = False + for model in self.models: + if model.id == new_id: + cont = True + model.reload() + if cont: + continue + newmod = ModelRecord(self.resource, new_id, + parent=self.parent, group=self) + newmod.reload() + if not result: + result = newmod + new_index = min(model_idx, len(self.models)-1) + self.model_add(newmod, new_index) + return result + + def pre_load(self, ids, display=True): + if not ids: + return True + if len(ids)>10: + self.models.lock_signal = True + for obj_id in ids: + newmod = ModelRecord(self.resource, obj_id, + parent=self.parent, group=self) + self.model_add(newmod) + if display: + self.signal('model-changed', newmod) + if len(ids)>10: + self.models.lock_signal = False + self.signal('record-cleared') + return True + + def load_for(self, values): + if len(values)>10: + self.models.lock_signal = True + for value in values: + newmod = ModelRecord(self.resource, value['id'], + parent=self.parent, group=self) + newmod.set(value) + self.models.append(newmod) + newmod.signal_connect(self, 'record-changed', self._record_changed) + if len(values)>10: + self.models.lock_signal = False + self.signal('record-cleared') + + def load(self, ids, display=True): + if not ids: + return True + if not self.fields: + return self.pre_load(ids, display) + ctx = rpc.session.context.copy() + ctx.update(self.context) + values = self.rpc.read(ids, self.fields.keys(), ctx) + if not values: + return False + newmod = False + self.load_for(values) + if newmod and display: + self.signal('model-changed', newmod) + self.current_idx = 0 + return True + + def clear(self): + self.models.clear() + self.model_removed = [] + + def _get_context(self): + ctx = {} + ctx.update(self._context) + return ctx + context = property(_get_context) + + def model_add(self, model, position=-1): + #TODO To be checked + if not model.mgroup is self: + fields = {} + for i in model.mgroup.fields: + fields[model.mgroup.fields[i]['name']] = \ + model.mgroup.fields[i] + self.add_fields(fields, self) + self.add_fields(self.fields, model.mgroup) + model.mgroup = self + + if position == -1: + self.models.append(model) + else: + self.models.insert(position, model) + self.current_idx = position + model.parent = self.parent + model.signal_connect(self, 'record-changed', self._record_changed) + return model + + def model_new(self, default=True, domain=None, context=None): + newmod = ModelRecord(self.resource, None, group=self, + parent=self.parent, new=True) + newmod.signal_connect(self, 'record-changed', self._record_changed) + if default: + ctx = {} + ctx.update(context or {}) + ctx.update(self.context) + newmod.default_get(domain, ctx) + self.signal('model-changed', newmod) + return newmod + + def model_remove(self, model): + idx = self.models.index(model) + self.models.remove(model) + if model.parent: + model.parent.modified = True + if self.models: + self.current_idx = min(idx, len(self.models)-1) + else: + self.current_idx = None + + def _record_changed(self, model, signal_data): + self.signal('model-changed', model) + + def prev(self): + if self.models and self.current_idx is not None: + self.current_idx = (self.current_idx - 1) % len(self.models) + elif self.models: + self.current_idx = 0 + else: + return None + return self.models[self.current_idx] + + def next(self): + if self.models and self.current_idx is not None: + self.current_idx = (self.current_idx + 1) % len(self.models) + elif self.models: + self.current_idx = 0 + else: + return None + return self.models[self.current_idx] + + def remove(self, model): + idx = self.models.index(model) + if self.models[idx].id: + self.model_removed.append(self.models[idx].id) + if model.parent: + model.parent.modified = True + self.models.remove(self.models[idx]) + + def add_fields_custom(self, fields, models): + to_add = [] + for field_add in fields.keys(): + if not field_add in models.fields: + models.fields[field_add] = fields[field_add] + models.fields[field_add]['name'] = field_add + to_add.append(field_add) + else: + models.fields[field_add].update(fields[field_add]) + ModelRecordGroup.mfields_load(to_add, models) + for fname in to_add: + for model in models.models: + model.value[fname] = self.mfields[fname].create(model) + return to_add + + def add_fields(self, fields, models, context=None): + if context is None: + context = {} + to_add = self.add_fields_custom(fields, models) + models = models.models + if not len(models): + return True + + old = [] + new = [] + for model in models: + if model.id: + old.append(model.id) + else: + new.append(model) + ctx = context.copy() + if len(old) and len(to_add): + ctx.update(rpc.session.context) + ctx.update(self.context) + values = self.rpc.read(old, to_add, ctx) + if values: + for value in values: + value_id = value['id'] + if 'id' not in to_add: + del value['id'] + self[value_id].set(value, signal=False) + if len(new) and len(to_add): + ctx.update(self.context) + values = self.rpc.default_get(to_add, ctx) + for field_to_add in to_add: + if field_to_add not in values: + values[field_to_add] = False + for mod in new: + mod.set_default(values) + + def __iter__(self): + return iter(self.models) + + def get_by_id(self, m_id): + for model in self.models: + if model.id == m_id: + return model + + __getitem__ = get_by_id diff --git a/tryton/gui/window/view_form/model/record.py b/tryton/gui/window/view_form/model/record.py new file mode 100644 index 0000000000..ef8cbd1cd3 --- /dev/null +++ b/tryton/gui/window/view_form/model/record.py @@ -0,0 +1,238 @@ +import re +import time +from tryton.rpc import RPCProxy +import tryton.rpc as rpc +from tryton.signal_event import SignalEvent +import field + + +class EvalEnvironment(object): + + def __init__(self, parent): + self.parent = parent + + def __getattr__(self, item): + if item == 'parent' and self.parent.parent: + return EvalEnvironment(self.parent.parent) + if item == "current_date": + return time.strftime('%Y-%m-%d') + if item == "time": + return time + return self.parent.get(includeid=True)[item] + + +class ModelRecord(SignalEvent): + + def __init__(self, resource, obj_id, group=None, parent=None, new=False ): + super(ModelRecord, self).__init__() + self.resource = resource + self.rpc = RPCProxy(self.resource) + self.id = obj_id + self._loaded = False + self.parent = parent + self.mgroup = group + self.value = {} + self.state_attrs = {} + self.modified = False + self.modified_fields = {} + self.read_time = time.time() + for key, val in self.mgroup.mfields.items(): + self.value[key] = val.create(self) + if (new and val.attrs['type']=='one2many') \ + and (val.attrs.get('mode','tree,form').startswith('form')): + mod = self.value[key].model_new() + self.value[key].model_add(mod) + + def __getitem__(self, name): + return self.mgroup.mfields.get(name, False) + + def __repr__(self): + return '' % (self.id, self.resource) + + def is_modified(self): + return self.modified + + def fields_get(self): + return self.mgroup.mfields + + def _check_load(self): + if not self._loaded: + self.reload() + return True + return False + + def get(self, get_readonly=True, includeid=False, check_load=True, + get_modifiedonly=False): + if check_load: + self._check_load() + value = [] + for name, mfield in self.mgroup.mfields.items(): + if (get_readonly or \ + not mfield.get_state_attrs(self).get('readonly', False)) \ + and (not get_modifiedonly \ + or (mfield.name in self.modified_fields \ + or isinstance(mfield, mfield.O2MField))): + value.append((name, mfield.get(self, readonly=get_readonly, + modified=get_modifiedonly))) + value = dict(value) + if includeid: + value['id'] = self.id + return value + + def cancel(self): + self._loaded = False + self.reload() + + def save(self, force_reload=True): + self._check_load() + if not self.id: + value = self.get(get_readonly=False) + self.id = self.rpc.create(value, self.context_get()) + else: + if not self.is_modified(): + return self.id + value = self.get(get_readonly=False, get_modifiedonly=True) + context = self.context_get() + context = context.copy() + #XXX must compute delta on server side + context['read_delta'] = time.time() - self.read_time + if not rpc.session.rpc_exec_auth('/object', 'execute', + self.resource, 'write', [self.id], value, context): + return False + self._loaded = False + if force_reload: + self.reload() + return self.id + + def default_get(self, domain=None, context=None): + if domain is None: + domain = [] + if len(self.mgroup.fields): + val = self.rpc.default_get(self.mgroup.fields.keys(), context) + for clause in domain: + if clause[0] in self.mgroup.fields and clause[1] == '=': + val[clause[0]] = clause[2] + self.set_default(val) + + def name_get(self): + name = self.rpc.name_get([self.id], rpc.session.context)[0] + return name + + def validate_set(self): + change = self._check_load() + for fname in self.mgroup.mfields: + mfield = self.mgroup.mfields[fname] + change = change or \ + not mfield.get_state_attrs(self).get('valid', True) + mfield.get_state_attrs(self)['valid'] = True + if change: + self.signal('record-changed') + return change + + def validate(self): + self._check_load() + res = True + for fname in self.mgroup.mfields: + if not self.mgroup.mfields[fname].validate(self): + res = False + return res + + def _get_invalid_fields(self): + res = [] + for fname, mfield in self.mgroup.mfields.items(): + if not mfield.get_state_attrs(self).get('valid', True): + res.append((fname, mfield.attrs['string'])) + return dict(res) + invalid_fields = property(_get_invalid_fields) + + def context_get(self): + return self.mgroup.context + + def get_default(self): + self._check_load() + value = dict([(name, mfield.get_default(self)) + for name, mfield in self.mgroup.mfields.items()]) + return value + + def set_default(self, val): + for fieldname, value in val.items(): + if fieldname not in self.mgroup.mfields: + continue + self.mgroup.mfields[fieldname].set_default(self, value) + self._loaded = True + self.signal('record-changed') + + def set(self, val, modified=False, signal=True): + later = {} + for fieldname, value in val.items(): + if fieldname not in self.mgroup.mfields: + continue + if isinstance(self.mgroup.mfields[fieldname], field.O2MField): + later[fieldname] = value + continue + self.mgroup.mfields[fieldname].set(self, value, modified=modified) + for fieldname, value in later.items(): + self.mgroup.mfields[fieldname].set(self, value, modified=modified) + self._loaded = True + self.modified = modified + if not self.modified: + self.modified_fields = {} + if signal: + self.signal('record-changed') + + def reload(self): + if not self.id: + return + ctx = rpc.session.context.copy() + ctx.update(self.context_get()) + res = self.rpc.read([self.id], self.mgroup.mfields.keys(), ctx) + if res: + value = res[0] + self.read_time = time.time() + self.set(value) + + def expr_eval(self, dom, check_load=True): + if not isinstance(dom, basestring): + return dom + if check_load: + self._check_load() + ctx = {} + for name, mfield in self.mgroup.mfields.items(): + ctx[name] = mfield.get(self, check_load=check_load) + + ctx['current_date'] = time.strftime('%Y-%m-%d') + ctx['time'] = time + ctx['context'] = self.context_get() + ctx['active_id'] = self.id + if self.parent: + ctx['parent'] = EvalEnvironment(self.parent) + val = eval(dom, ctx) + return val + + #XXX Shoud use changes of attributes (ro, ...) + def on_change(self, callback): + match = re.match('^(.*?)\((.*)\)$', callback) + if not match: + raise Exception, 'ERROR: Wrong on_change trigger: %s' % callback + func_name = match.group(1) + arg_names = [n.strip() for n in match.group(2).split(',')] + args = [self.expr_eval(arg) for arg in arg_names] + ids = self.id and [self.id] or [] + response = getattr(self.rpc, func_name)(ids, *args) + if response: + self.set(response.get('value', {}), modified=True) + if 'domain' in response: + for fieldname, value in response['domain'].items(): + if fieldname not in self.mgroup.mfields: + continue + self.mgroup.mfields[fieldname].attrs['domain'] = value + self.signal('record-changed') + + def cond_default(self, field_name, value): + ir_values = RPCProxy('ir.values') + values = ir_values.get('default', '%s=%s' % (field_name, value), + [(self.resource, False)], False, {}) + data = {} + for index, fname, value in values: + data[fname] = value + self.set_default(data) diff --git a/tryton/gui/window/view_form/screen/screen.py b/tryton/gui/window/view_form/screen/screen.py index 5d54f53f9a..f98b5e8f29 100644 --- a/tryton/gui/window/view_form/screen/screen.py +++ b/tryton/gui/window/view_form/screen/screen.py @@ -2,9 +2,9 @@ import xml.dom.minidom from tryton.rpc import RPCProxy import tryton.rpc as rpc -from widget.model.group import ModelRecordGroup -from widget.view.screen_container import screen_container -import widget_search +from tryton.gui.window.view_form.model.group import ModelRecordGroup +from tryton.gui.window.view_form.view.screen_container import ScreenContainer +from tryton.gui.window.view_form.widget_search import Form from tryton.signal_event import SignalEvent from tryton.common import node_attributes @@ -60,7 +60,7 @@ class Screen(SignalEvent): context=self.context) self.models_set(models) self.current_model = None - self.screen_container = screen_container() + self.screen_container = ScreenContainer() self.filter_widget = None self.widget = self.screen_container.widget_get() self.__current_view = 0 @@ -83,7 +83,7 @@ class Screen(SignalEvent): view_form = rpc.session.rpc_exec_auth('/object', 'execute', self.name, 'fields_view_get', False, 'form', self.context) - self.filter_widget = widget_search.form(view_form['arch'], + self.filter_widget = Form(view_form['arch'], view_form['fields'], self.name, self.window, self.domain, (self, self.search_filter)) self.screen_container.add_filter(self.filter_widget.widget, @@ -239,7 +239,7 @@ class Screen(SignalEvent): dom = xml.dom.minidom.parseString(arch) _parse_fields(dom, fields) - from widget.view.widget_parse import widget_parse + from tryton.gui.window.view_form.view.widget_parse import widget_parse models = self.models.models if self.current_model and (self.current_model not in models): models = models + [self.current_model] diff --git a/tryton/gui/window/view_form/view/__init__.py b/tryton/gui/window/view_form/view/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tryton/gui/window/view_form/view/form_gtk/__init__.py b/tryton/gui/window/view_form/view/form_gtk/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tryton/gui/window/view_form/view/form_gtk/interface.py b/tryton/gui/window/view_form/view/form_gtk/interface.py new file mode 100644 index 0000000000..23802cc465 --- /dev/null +++ b/tryton/gui/window/view_form/view/form_gtk/interface.py @@ -0,0 +1,177 @@ +import gtk +from gtk import glade +import tryton.rpc as rpc +from tryton.common import warning, COLORS +from tryton.config import GLADE, TRYTON_ICON +import gettext + +_ = gettext.gettext + +_ATTRS_BOOLEAN = { + 'required': False, + 'readonly': False +} + +def field_pref_set(field, name, model, value, dependance=None, window=None): + win_gl = glade.XML(GLADE, 'win_field_pref', gettext.textdomain()) + if dependance is None: + dependance = [] + win = win_gl.get_widget('win_field_pref') + win.set_transient_for(window) + win.set_icon(TRYTON_ICON) + ent = win_gl.get_widget('ent_field') + ent.set_text(name) + ent = win_gl.get_widget('ent_domain') + ent.set_text(model) + ent = win_gl.get_widget('ent_value') + ent.set_text((value and str(value)) or '/') + + radio = win_gl.get_widget('radio_user_pref') + + vbox = win_gl.get_widget('pref_vbox') + widgets = {} + addwidget = False + for (fname, fvalue, rname, rvalue) in dependance: + if rvalue: + addwidget = True + widget = gtk.CheckButton(fname+' = '+str(rname)) + widgets[(fvalue, rvalue)] = widget + vbox.pack_start(widget) + if not len(dependance) or not addwidget: + vbox.pack_start(gtk.Label(_('Always applicable !'))) + vbox.show_all() + + res = win.run() + + deps = False + for val in widgets.keys(): + if widgets[val].get_active(): + deps = val[0] + '=' + str(val[1]) + break + window.present() + win.destroy() + if res == gtk.RESPONSE_OK: + rpc.session.rpc_exec_auth('/object', 'execute', 'ir.values', 'set', + 'default', deps, field, [(model,False)], value, True, False, + False, radio.get_active(), True) + return True + return False + + +class WidgetInterface(object): + + def __init__(self, window, parent=None, model=None, attrs=None): + if attrs is None: + attrs = {} + self.parent = parent + self._window = window + self._view = None + self.attrs = attrs + for key, val in _ATTRS_BOOLEAN.items(): + self.attrs[key] = attrs.get(key, False) not in ('False', '0', False) + self.default_readonly = self.attrs.get('readonly', False) + self._menu_entries = [ + (_('Set to default value'), + lambda x: self._menu_sig_default_get(), 1), + (_('Set as default'), + lambda x: self._menu_sig_default_set(), 1), + ] + self.widget = None + + def destroy(self): + pass + + def _menu_sig_default_get(self): + try: + if self._view.modelfield.get_state_attrs(self._view.model)\ + .get('readonly', False): + return False + model = self._view.modelfield.parent.resource + res = rpc.session.rpc_exec_auth_try('/object', 'execute', model, + 'default_get', [self.attrs['name']]) + self._view.modelfield.set(self._view.model, + res.get(self.attrs['name'], False)) + self.display(self._view.model, self._view.modelfield) + except: + warning(_('You can not set to the default value here!'), + _('Operation not permited')) + return False + + def sig_activate(self, widget=None): + # emulate a focus_out so that the onchange is called if needed + self._focus_out() + + def _readonly_set(self, readonly): + pass + + def _color_widget(self): + return self.widget + + def color_set(self, name): + widget = self._color_widget() + colormap = widget.get_colormap() + colour = colormap.alloc_color(COLORS.get(name,'white')) + widget.modify_bg(gtk.STATE_ACTIVE, colour) + widget.modify_fg(gtk.STATE_NORMAL, gtk.gdk.color_parse("black")) + widget.modify_base(gtk.STATE_NORMAL, colour) + widget.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("black")) + widget.modify_text(gtk.STATE_INSENSITIVE, gtk.gdk.color_parse("black")) + + def _menu_sig_default_set(self): + deps = [] + wid = self._view.view_form.widgets + for wname, wview in self._view.view_form.widgets.items(): + if wview.modelfield.attrs.get('change_default', False): + value = wview.modelfield.get(self._view.model) + deps.append((wname, wname, value, value)) + value = self._view.modelfield.get_default(self._view.model) + model = self._view.modelfield.parent.resource + field_pref_set(self._view.widget_name, + self.attrs.get('string', self._view.widget_name), model, + value, deps, window=self._window) + + def _menu_open(self, obj, event): + if event.button == 3: + menu = gtk.Menu() + for stock_id, callback, sensitivity in self._menu_entries: + if stock_id: + item = gtk.ImageMenuItem(stock_id) + if callback: + item.connect("activate", callback) + item.set_sensitive(sensitivity) + else: + item = gtk.SeparatorMenuItem() + item.show() + menu.append(item) + menu.popup(None, None, None, event.button, event.time) + return True + + def _focus_in(self): + pass + + def _focus_out(self): + if not self._view.modelfield: + return False + self.set_value(self._view.model, self._view.modelfield) + + def display(self, model, modelfield): + if not modelfield: + self._readonly_set(self.attrs.get('readonly', False)) + return + self._readonly_set(modelfield.get_state_attrs(model).\ + get('readonly', False)) + if modelfield.get_state_attrs(model).get('readonly', False): + self.color_set('readonly') + elif not modelfield.get_state_attrs(model).get('valid', True): + self.color_set('invalid') + elif modelfield.get_state_attrs(model).get('required', False): + self.color_set('required') + else: + self.color_set('normal') + + def sig_changed(self): + if self.attrs.get('on_change', False): + self._view.view_form.screen.on_change(self.attrs['on_change']) + + def set_value(self, model, model_field): + pass diff --git a/tryton/gui/window/view_form/view/form_gtk/many2one.py b/tryton/gui/window/view_form/view/form_gtk/many2one.py new file mode 100644 index 0000000000..94ef81d6d7 --- /dev/null +++ b/tryton/gui/window/view_form/view/form_gtk/many2one.py @@ -0,0 +1,381 @@ +import gobject +import gtk +import gettext +from interface import WidgetInterface +import tryton.common as common +from tryton.gui.window.view_form.screen import Screen +from tryton.gui.window.win_search import win_search +import tryton.rpc as rpc +from tryton.action import Action + +_ = gettext.gettext + + +class Dialog(object): + + def __init__(self, model, obj_id=None, attrs=None, domain=None, + context=None, window=None): + if attrs is None: + attrs = {} + if domain is None: + domain = [] + if context is None: + context = {} + + self.dia = gtk.Dialog(_('Tryton - Link'), window, + gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT) + self.window = window + if ('string' in attrs) and attrs['string']: + self.dia.set_title(self.dia.get_title() + ' - ' + attrs['string']) + self.dia.set_property('default-width', 760) + self.dia.set_property('default-height', 500) + self.dia.set_position(gtk.WIN_POS_CENTER_ON_PARENT) + self.dia.set_icon(common.TINYERP_ICON) + + self.accel_group = gtk.AccelGroup() + self.dia.add_accel_group(self.accel_group) + + self.but_cancel = self.dia.add_button(gtk.STOCK_CANCEL, + gtk.RESPONSE_CANCEL) + self.but_cancel.add_accelerator('clicked', self.accel_group, + gtk.keysyms.Escape, gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE) + + self.but_ok = self.dia.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK) + self.but_ok.add_accelerator('clicked', self.accel_group, + gtk.keysyms.Return, gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE) + + scroll = gtk.ScrolledWindow() + scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + scroll.set_placement(gtk.CORNER_TOP_LEFT) + scroll.set_shadow_type(gtk.SHADOW_NONE) + self.dia.vbox.pack_start(scroll, expand=True, fill=True) + + viewport = gtk.Viewport() + viewport.set_shadow_type(gtk.SHADOW_NONE) + scroll.add(viewport) + + self.screen = Screen(model, domain=domain, context=context, + window=self.dia, view_type=['form']) + if obj_id: + self.screen.load([obj_id]) + else: + self.screen.new() + viewport.add(self.screen.widget) + i, j = self.screen.screen_container.size_get() + viewport.set_size_request(i, j + 30) + self.dia.show_all() + self.screen.display() + + def run(self): + while True: + res = self.dia.run() + if res == gtk.RESPONSE_OK: + if self.screen.current_model.validate() \ + and self.screen.save_current(): + return (True, self.screen.current_model.name_get()) + else: + self.screen.display() + else: + break + return (False, False) + + def destroy(self): + self.window.present() + self.dia.destroy() + +class Many2One(WidgetInterface): + + def __init__(self, window, parent, model, attrs=None): + if attrs is None: + attrs = {} + WidgetInterface.__init__(self, window, parent, model, attrs) + + self.widget = gtk.HBox(spacing=3) + self.widget.set_property('sensitive', True) + self.widget.connect('focus-in-event', lambda x, y: self._focus_in()) + self.widget.connect('focus-out-event', lambda x, y: self._focus_out()) + + self.wid_text = gtk.Entry() + self.wid_text.set_property('width-chars', 13) + self.wid_text.connect('key_press_event', self.sig_key_press) + self.wid_text.connect('button_press_event', self._menu_open) + self.wid_text.connect_after('changed', self.sig_changed) + self.wid_text.connect_after('activate', self.sig_activate) + self.wid_text_focus_out_id = \ + self.wid_text.connect_after('focus-out-event', + self.sig_activate, True) + self.widget.pack_start(self.wid_text, expand=True, fill=True) + + self.but_new = gtk.Button() + img_new = gtk.Image() + img_new.set_from_stock('gtk-new', gtk.ICON_SIZE_BUTTON) + self.but_new.set_image(img_new) + self.but_new.set_relief(gtk.RELIEF_NONE) + self.but_new.connect('clicked', self.sig_new) + self.but_new.set_alignment(0.5, 0.5) + self.but_new.set_property('can-focus', False) + self.widget.pack_start(self.but_new, expand=False, fill=False) + + self.but_open = gtk.Button() + img_find = gtk.Image() + img_find.set_from_stock('gtk-find', gtk.ICON_SIZE_BUTTON) + img_open = gtk.Image() + img_open.set_from_stock('gtk-open', gtk.ICON_SIZE_BUTTON) + self.but_open.set_image(img_find) + self.but_open.set_relief(gtk.RELIEF_NONE) + self.but_open.connect('clicked', self.sig_edit) + self.but_open.set_alignment(0.5, 0.5) + self.but_open.set_property('can-focus', False) + self.widget.pack_start(self.but_open, padding=2, expand=False, + fill=False) + + self.tooltips = gtk.Tooltips() + self.tooltips.set_tip(self.but_new, _('Create a new resource')) + self.tooltips.set_tip(self.but_open, _('Open a resource')) + self.tooltips.enable() + + self.activate = True + self._readonly = False + self.model_type = attrs['relation'] + self._menu_loaded = False + self._menu_entries = [] + self._menu_entries.append((None, None, None)) + self._menu_entries.append((_('Action'), + lambda x: self.click_and_action('client_action_multi'),0)) + self._menu_entries.append((_('Report'), + lambda x: self.click_and_action('client_print_multi'),0)) + + + self.completion = gtk.EntryCompletion() + self.liststore = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING) + if attrs.get('completion', False): + ids = rpc.session.rpc_exec_auth('/object', 'execute', + self.attrs['relation'], 'name_search', '', [], 'ilike', {}) + if ids: + self.load_completion(ids) + + def _focus_out(self): + return WidgetInterface._focus_out(self) + + def _focus_in(self): + return WidgetInterface._focus_in(self) + + def load_completion(self, ids): + self.completion.set_match_func(self.match_func, None) + self.completion.connect("match-selected", self.on_completion_match) + self.wid_text.set_completion(self.completion) + self.completion.set_model(self.liststore) + self.completion.set_text_column(0) + for i, word in enumerate(ids): + if word[1][0] == '[': + i = word[1].find(']') + str1 = word[1][1:i] + str2 = word[1][i+2:] + self.liststore.append([("%s %s" % (str1, str2)), str2]) + else: + self.liststore.append([word[1], word[1]]) + + def match_func(self, completion, key_string, iter, data): + model = self.completion.get_model() + modelstr = model[iter][0].lower() + return modelstr.startswith(key_string) + + def on_completion_match(self, completion, model, iter): + name = model[iter][1] + domain = self._view.modelfield.domain_get(self._view.model) + context = self._view.modelfield.context_get(self._view.model) + ids = rpc.session.rpc_exec_auth('/object', 'execute', + self.attrs['relation'], 'name_search', name, domain, 'ilike', + context) + if len(ids)==1: + self._view.modelfield.set_client(self._view.model, ids[0]) + self.display(self._view.model, self._view.modelfield) + self.activate = True + else: + win = win_search(self.attrs['relation'], sel_multi=False, + ids = [x[0] for x in ids], context=context, + domain=domain, window=self._window) + ids = win.go() + if ids: + name = rpc.session.rpc_exec_auth('/object', 'execute', + self.attrs['relation'], 'name_get', [ids[0]], + rpc.session.context)[0] + self._view.modelfield.set_client(self._view.model, name) + return True + + + + def _readonly_set(self, value): + self._readonly = value + self.wid_text.set_editable(not value) + self.but_new.set_sensitive(not value) + + def _color_widget(self): + return self.wid_text + + def _menu_sig_pref(self, obj): + self._menu_sig_default_set() + + def _menu_sig_default(self, obj): + rpc.session.rpc_exec_auth('/object', 'execute', + self.attrs['model'], 'default_get', [self.attrs['name']]) + + def sig_activate(self, widget, event=None, leave=False): + self.activate = False + value = self._view.modelfield.get(self._view.model) + + self.wid_text.disconnect(self.wid_text_focus_out_id) + if value: + if not leave: + domain = self._view.modelfield.domain_get(self._view.model) + dia = Dialog(self.attrs['relation'], + self._view.modelfield.get(self._view.model), + attrs=self.attrs, window=self._window, domain=domain) + res, value = dia.run() + if res: + self._view.modelfield.set_client(self._view.model, value, + force_change=True) + dia.destroy() + else: + if not self._readonly and ( self.wid_text.get_text() or not leave): + domain = self._view.modelfield.domain_get(self._view.model) + context = self._view.modelfield.context_get(self._view.model) + self.wid_text.grab_focus() + + ids = rpc.session.rpc_exec_auth('/object', 'execute', + self.attrs['relation'], 'name_search', + self.wid_text.get_text(), domain, 'ilike', context) + if len(ids)==1: + self._view.modelfield.set_client(self._view.model, ids[0], + force_change=True) + self.wid_text_focus_out_id = \ + self.wid_text.connect_after('focus-out-event', + self.sig_activate, True) + self.display(self._view.model, self._view.modelfield) + self.activate = True + return True + + win = win_search(self.attrs['relation'], sel_multi=False, + ids = [x[0] for x in ids], context=context, + domain=domain, parent=self._window) + ids = win.go() + if ids: + name = rpc.session.rpc_exec_auth('/object', 'execute', + self.attrs['relation'], 'name_get', [ids[0]], + rpc.session.context)[0] + self._view.modelfield.set_client(self._view.model, name, + force_change=True) + self.wid_text_focus_out_id = \ + self.wid_text.connect_after('focus-out-event', + self.sig_activate, True) + self.display(self._view.model, self._view.modelfield) + self.activate = True + + def sig_new(self, *args): + self.wid_text.disconnect(self.wid_text_focus_out_id) + domain = self._view.modelfield.domain_get(self._view.model) + dia = Dialog(self.attrs['relation'], attrs=self.attrs, + window=self._window, domain=domain) + res, value = dia.run() + if res: + self._view.modelfield.set_client(self._view.model, value) + self.display(self._view.model, self._view.modelfield) + dia.destroy() + self.wid_text_focus_out_id = \ + self.wid_text.connect_after('focus-out-event', + self.sig_activate, True) + sig_edit = sig_activate + + def sig_key_press(self, widget, event, *args): + if event.keyval == gtk.keysyms.F1: + self.sig_new(widget, event) + elif event.keyval==gtk.keysyms.F2: + self.sig_activate(widget, event) + elif event.keyval == gtk.keysyms.Tab: + if self._view.modelfield.get(self._view.model) or \ + not self.wid_text.get_text(): + return False + self.sig_activate(widget, event, leave=True) + return True + return False + + def sig_changed(self, *args): + if self.activate: + if self._view.modelfield.get(self._view.model): + self._view.modelfield.set_client(self._view.model, False) + self.display(self._view.model, self._view.modelfield) + return False + + def set_value(self, model, model_field): + pass # No update of the model, the model is updated in real time ! + + def display(self, model, model_field): + if not model_field: + self.activate = False + self.wid_text.set_text('') + return False + WidgetInterface.display(self, model, model_field) + self.activate = False + res = model_field.get_client(model) + self.wid_text.set_text((res and str(res)) or '') + img = gtk.Image() + if res: + img.set_from_stock('gtk-open', gtk.ICON_SIZE_BUTTON) + self.but_open.set_image(img) + self.tooltips.set_tip(self.but_open, _('Open a resource')) + else: + img.set_from_stock('gtk-find', gtk.ICON_SIZE_BUTTON) + self.but_open.set_image(img) + self.tooltips.set_tip(self.but_open, _('Search a resource')) + self.activate = True + + def _menu_open(self, obj, event): + if event.button == 3: + value = self._view.modelfield.get(self._view.model) + if not self._menu_loaded: + resrelate = rpc.session.rpc_exec_auth('/object', 'execute', + 'ir.values', 'get', 'action', 'client_action_relate', + [(self.model_type, False)], False, rpc.session.context) + resrelate = [x[2] for x in resrelate] + self._menu_entries.append((None, None, None)) + for i in resrelate: + i['string'] = i['name'] + fct = lambda action: lambda x: self.click_and_relate(action) + self._menu_entries.append(('... ' + i['name'], fct(i), 0)) + self._menu_loaded = True + + menu = gtk.Menu() + for stock_id, callback, sensitivity in self._menu_entries: + if stock_id: + item = gtk.ImageMenuItem(stock_id) + if callback: + item.connect("activate", callback) + item.set_sensitive(bool(sensitivity or value)) + else: + item = gtk.SeparatorMenuItem() + item.show() + menu.append(item) + menu.popup(None, None, None, event.button, event.time) + return True + return False + + def click_and_relate(self, action): + data = {} + context = {} + act = action.copy() + obj_id = self._view.modelfield.get(self._view.model) + if not obj_id: + common.message(_('You must select a record to use the relation !')) + return False + screen = Screen(self.attrs['relation']) + screen.load([obj_id]) + act['domain'] = screen.current_model.expr_eval(act['domain'], + check_load=False) + act['context'] = str(screen.current_model.expr_eval(act['context'], + check_load=False)) + return Action._exec_action(act, data, context) + + def click_and_action(self, atype): + obj_id = self._view.modelfield.get(self._view.model) + return Action.exec_keyword(atype, {'model': self.model_type, + 'id': obj_id or False, 'ids': [obj_id], 'report_type': 'pdf'}) diff --git a/tryton/gui/window/view_form/view/interface.py b/tryton/gui/window/view_form/view/interface.py new file mode 100644 index 0000000000..06b4a751ec --- /dev/null +++ b/tryton/gui/window/view_form/view/interface.py @@ -0,0 +1,26 @@ +"Interfaces" + + +class ParserInterface(object): + + def __init__(self, window, parent=None, attrs=None, screen=None): + self.window = window + self.parent = parent + self.attrs = attrs + self.title = None + self.buttons = {} + self.screen = screen + + +class ParserView(object): + + def __init__(self, window, screen, widget, children=None, buttons=None, + toolbar=None): + self.window = window + self.screen = screen + self.widget = widget + self.children = children + if buttons is None: + buttons = [] + self.buttons = buttons + self.toolbar = toolbar diff --git a/tryton/gui/window/view_form/view/screen_container.py b/tryton/gui/window/view_form/view/screen_container.py new file mode 100644 index 0000000000..198e903af4 --- /dev/null +++ b/tryton/gui/window/view_form/view/screen_container.py @@ -0,0 +1,72 @@ +import gtk +import gettext + +_ = gettext.gettext + +class ScreenContainer(object): + + def __init__(self): + self.old_widget = False + self.scrolledwindow = gtk.ScrolledWindow() + self.scrolledwindow.set_shadow_type(gtk.SHADOW_NONE) + self.scrolledwindow.set_policy(gtk.POLICY_AUTOMATIC, + gtk.POLICY_AUTOMATIC) + self.viewport = gtk.Viewport() + self.viewport.set_shadow_type(gtk.SHADOW_NONE) + self.vbox = gtk.VBox() + self.vbox.pack_end(self.scrolledwindow) + self.filter_vbox = None + self.button = None + + def widget_get(self): + return self.vbox + + def add_filter(self, widget, fnct, clear_fnct): + self.filter_vbox = gtk.VBox(spacing=1) + self.filter_vbox.set_border_width(1) + label = gtk.Label(_('Search')) + label.set_alignment(0.0, 0.5) + label.show() + self.filter_vbox.pack_start(label, expand=True, fill=False) + hseparator = gtk.HSeparator() + hseparator.show() + self.filter_vbox.pack_start(hseparator, expand=True, fill=False) + self.filter_vbox.pack_start(widget, expand=True, fill=True) + hbuttonbox = gtk.HButtonBox() + hbuttonbox.set_spacing(5) + hbuttonbox.set_layout(gtk.BUTTONBOX_END) + button_clear = gtk.Button(stock=gtk.STOCK_CLEAR) + button_clear.connect('clicked', clear_fnct) + hbuttonbox.pack_start(button_clear, expand=False, fill=False) + self.button = gtk.Button(stock=gtk.STOCK_FIND) + self.button.connect('clicked', fnct) + self.button.set_property('can_default', True) + hbuttonbox.pack_start(self.button, expand=False, fill=False) + hbuttonbox.show_all() + self.filter_vbox.pack_start(hbuttonbox, expand=False, fill=False) + hseparator = gtk.HSeparator() + hseparator.show() + self.filter_vbox.pack_start(hseparator, expand=True, fill=False) + self.vbox.pack_start(self.filter_vbox, expand=False, fill=True) + + def show_filter(self): + if self.filter_vbox: + self.filter_vbox.show() + + def hide_filter(self): + if self.filter_vbox: + self.filter_vbox.hide() + + def set(self, widget): + if self.viewport.get_child(): + self.viewport.remove(self.viewport.get_child()) + if self.scrolledwindow.get_child(): + self.scrolledwindow.remove(self.scrolledwindow.get_child()) + if not isinstance(widget, gtk.TreeView): + self.viewport.add(widget) + widget = self.viewport + self.scrolledwindow.add(widget) + self.scrolledwindow.show_all() + + def size_get(self): + return self.scrolledwindow.get_child().size_request() diff --git a/tryton/gui/window/view_form/view/tree_gtk/__init__.py b/tryton/gui/window/view_form/view/tree_gtk/__init__.py new file mode 100644 index 0000000000..95bf6b3381 --- /dev/null +++ b/tryton/gui/window/view_form/view/tree_gtk/__init__.py @@ -0,0 +1 @@ +from parser import * diff --git a/tryton/gui/window/view_form/view/tree_gtk/editabletree.py b/tryton/gui/window/view_form/view/tree_gtk/editabletree.py new file mode 100644 index 0000000000..4d1cebdc63 --- /dev/null +++ b/tryton/gui/window/view_form/view/tree_gtk/editabletree.py @@ -0,0 +1,244 @@ +import gtk +import parser +import gettext + +_ = gettext.gettext + + +class EditableTreeView(gtk.TreeView): + leaving_model_events = (gtk.keysyms.Up, gtk.keysyms.Down, + gtk.keysyms.Return, gtk.keysyms.KP_Enter) + leaving_events = leaving_model_events + (gtk.keysyms.Tab, + gtk.keysyms.ISO_Left_Tab) + + def __init__(self, position): + super(EditableTreeView, self).__init__() + self.editable = position + self.cells = {} + self.colors = dict() + + def on_quit_cell(self, current_model, fieldname, value): + modelfield = current_model[fieldname] + if hasattr(modelfield, 'editabletree_entry'): + del modelfield.editabletree_entry + cell = self.cells[fieldname] + + # The value has not changed ... do nothing. + if value == cell.get_textual_value(current_model): + return + + try: + real_value = cell.value_from_text(current_model, value) + modelfield.set_client(current_model, real_value) + except parser.UnsettableColumn: + return + + # And now the on_change stuff ... only 3 lines are enough. + #callback = modelfield.attrs.get('on_change', '') + #if callback: + # current_model.on_change(callback) + + # And finally the conditional default + #if modelfield.attrs.get('change_default', False): + # current_model.cond_default(fieldname, real_value) + + def on_open_remote(self, current_model, fieldname, create, value): + modelfield = current_model[fieldname] + cell = self.cells[fieldname] + if value != cell.get_textual_value(current_model) or not value: + changed = True + else: + changed = False + try: + valid, value = cell.open_remote(current_model, create, + changed, value) + if valid: + modelfield.set_client(current_model, value) + except NotImplementedError: + pass + return cell.get_textual_value(current_model) + + def on_create_line(self): + model = self.get_model() + if self.editable == 'top': + method = model.prepend + else: + method = model.append + ctx = self.screen.context.copy() + if self.screen.current_model.parent: + ctx.update(self.screen.current_model.parent.expr_eval( + self.screen.default_get)) + new_model = model.model_group.model_new(domain=self.screen.domain, + context=ctx) + res = method(new_model) + return res + + def __next_column(self, col): + cols = self.get_columns() + current = cols.index(col) + idx = (current + 1) % len(cols) + return cols[idx] + + def __prev_column(self, col): + cols = self.get_columns() + current = cols.index(col) + idx = (current - 1) % len(cols) + return cols[idx] + + def set_cursor(self, path, focus_column=None, start_editing=False): + if focus_column and (focus_column._type in ('many2one','many2many')): + self.warn('misc-message', + _('Relation Field: F1: New F2: Open/Search')) + elif focus_column and (focus_column._type in ('boolean')): + start_editing = False + else: + self.warn('misc-message', '') + return super(EditableTreeView, self).set_cursor(path, focus_column, + start_editing) + + def set_value(self): + path, column = self.get_cursor() + store = self.get_model() + if not path or not column: + return True + model = store.get_value(store.get_iter(path), 0) + modelfield = model[column.name] + if hasattr(modelfield, 'editabletree_entry'): + entry = modelfield.editabletree_entry + if isinstance(entry, gtk.Entry): + txt = entry.get_text() + else: + txt = entry.get_active_text() + self.on_quit_cell(model, column.name, txt) + return True + + def on_keypressed(self, entry, event): + path, column = self.get_cursor() + store = self.get_model() + model = store.get_value(store.get_iter(path), 0) + + if event.keyval in self.leaving_events: + shift_pressed = bool(gtk.gdk.SHIFT_MASK & event.state) + if isinstance(entry, gtk.Entry): + txt = entry.get_text() + elif isinstance(entry, gtk.ComboBoxEntry) \ + and shift_pressed \ + and event.keyval != gtk.keysyms.ISO_Left_Tab: + model = entry.get_property('model') + txt = entry.get_active_text() + idx = 0 + for idx, line in enumerate(model): + if line[1] == txt: + break + if event.keyval == gtk.keysyms.Up: + entry.set_active((idx - 1) % 3) + elif event.keyval == gtk.keysyms.Down: + entry.set_active((idx + 1) % 3) + return True + else: + txt = entry.get_active_text() + entry.disconnect(entry.editing_done_id) + self.on_quit_cell(model, column.name, txt) + entry.editing_done_id = entry.connect('editing_done', + self.on_editing_done) + if event.keyval in self.leaving_model_events: + if model.validate() and self.screen.tree_saves: + obj_id = model.save() + if not obj_id: + return True + else: + res = store.saved(obj_id) + if res: + self.screen.display(res.id) + + elif self.screen.tree_saves: + invalid_fields = model.invalid_fields + col = None + for col in self.get_columns(): + if col.name in invalid_fields: + break + self.set_cursor(path, col, True) + self.warn('misc-message', + _('Warning; field "%s" is required !') % \ + invalid_fields[col.name]) + return True + + if event.keyval == gtk.keysyms.Tab: + new_col = self.__next_column(column) + self.set_cursor(path, new_col, True) + elif event.keyval == gtk.keysyms.ISO_Left_Tab: + new_col = self.__prev_column(column) + self.set_cursor(path, new_col, True) + elif event.keyval == gtk.keysyms.Up: + self._key_up(path, store, column) + elif event.keyval == gtk.keysyms.Down: + self._key_down(path, store, column) + elif event.keyval in (gtk.keysyms.Return, gtk.keysyms.KP_Enter): + if self.editable == 'top': + new_path = self._key_up(path, store, column) + else: + new_path = self._key_down(path, store, column) + col = self.get_columns()[0] + self.set_cursor(new_path, col, True) + elif event.keyval == gtk.keysyms.Escape: + if model.id is None: + store.remove(store.get_iter(path)) + self.screen.current_model = False + if not path[0]: + self.screen.current_model = False + self.screen.display() + self.set_cursor(path, column, False) + elif event.keyval in (gtk.keysyms.F1, gtk.keysyms.F2): + if isinstance(entry, gtk.Entry): + value = entry.get_text() + else: + value = entry.get_active_text() + entry.disconnect(entry.editing_done_id) + newval = self.on_open_remote(model, column.name, + create=(event.keyval==gtk.keysyms.F1), + value=value) + if isinstance(entry, gtk.Entry): + entry.set_text(newval) + else: + entry.set_active_text(value) + entry.editing_done_id = entry.connect('editing_done', + self.on_editing_done) + self.set_cursor(path, column, True) + else: + modelfield = model[column.name] + if isinstance(entry, gtk.Entry): + entry.set_max_length(int(modelfield.attrs.get('size', 0))) + # store in the model the entry widget to get the value in set_value + modelfield.editabletree_entry = entry + model.modified = True + model.modified_fields.setdefault(column.name) + return False + + return True + + def _key_down(self, path, store, column): + if path[0] == len(store) - 1 and self.editable == 'bottom': + self.on_create_line() + new_path = (path[0] + 1) % len(store) + self.set_cursor(new_path, column, True) + return new_path + + def _key_up(self, path, store, column): + if path[0] == 0 and self.editable == 'top': + self.on_create_line() + new_path = 0 + else: + new_path = (path[0] - 1) % len(store) + self.set_cursor(new_path, column, True) + return new_path + + def on_editing_done(self, entry): + path, column = self.get_cursor() + if not path: + return True + store = self.get_model() + model = store.get_value(store.get_iter(path), 0) + if isinstance(entry, gtk.Entry): + self.on_quit_cell(model, column.name, entry.get_text()) + elif isinstance(entry, gtk.ComboBoxEntry): + self.on_quit_cell(model, column.name, entry.get_active_text()) diff --git a/tryton/gui/window/view_form/view/tree_gtk/parser.py b/tryton/gui/window/view_form/view/tree_gtk/parser.py new file mode 100644 index 0000000000..5338081f92 --- /dev/null +++ b/tryton/gui/window/view_form/view/tree_gtk/parser.py @@ -0,0 +1,477 @@ +import locale +import gtk +import math + +from tryton.rpc import RPCProxy +from editabletree import EditableTreeView +from tryton.gui.window.view_form.view.interface import ParserInterface + +import time + +from tryton.gui.window.view_form.view.form_gtk.many2one import Dialog \ + as M2ODialog +from tryton.gui.window.win_search import win_search + +import tryton.rpc as rpc +import datetime as DT +from tryton.common import DT_FORMAT, DHM_FORMAT, COLORS, node_attributes + +if not hasattr(locale, 'nl_langinfo'): + locale.nl_langinfo = lambda *a: '%x' + +if not hasattr(locale, 'D_FMT'): + locale.D_FMT = None + +def send_keys(renderer, editable, position, treeview): + editable.connect('key_press_event', treeview.on_keypressed) + editable.editing_done_id = editable.connect('editing_done', + treeview.on_editing_done) + +def sort_model(column, treeview): + model = treeview.get_model() + model.sort(column.name) + +class ParserTree(ParserInterface): + + def __init__(self, window, parent=None, attrs=None, screen=None): + super(ParserTree, self).__init__(window, parent, attrs, screen) + self.treeview = None + + def parse(self, model, root_node, fields): + dict_widget = {} + attrs = node_attributes(root_node) + on_write = attrs.get('on_write', '') + editable = attrs.get('editable', False) + if editable: + treeview = EditableTreeView(editable) + else: + treeview = gtk.TreeView() + treeview.editable = editable + treeview.cells = {} + self.treeview = treeview + for color_spec in attrs.get('colors', '').split(';'): + if color_spec: + colour, test = color_spec.split(':') + treeview.colors[colour] = test + treeview.set_property('rules-hint', True) + if not self.title: + self.title = attrs.get('string', 'Unknown') + + for node in root_node.childNodes: + node_attrs = node_attributes(node) + if node.localName == 'field': + fname = str(node_attrs['name']) + for boolean_fields in ('readonly', 'required'): + if boolean_fields in node_attrs: + node_attrs[boolean_fields] = \ + bool(int(node_attrs[boolean_fields])) + fields[fname].update(node_attrs) + node_attrs.update(fields[fname]) + cell = CELLTYPES.get(fields[fname]['type'])(fname, treeview, + node_attrs, self.window) + treeview.cells[fname] = cell + renderer = cell.renderer + if editable and not node_attrs.get('readonly', False): + if isinstance(renderer, gtk.CellRendererToggle): + renderer.set_property('activatable', True) + else: + renderer.set_property('editable', True) + renderer.connect_after('editing-started', send_keys, + treeview) + else: + if isinstance(renderer, gtk.CellRendererToggle): + renderer.set_property('activatable', False) + + col = gtk.TreeViewColumn(fields[fname]['string'], renderer) + col.name = fname + col._type = fields[fname]['type'] + col.set_cell_data_func(renderer, cell.setter) + col.set_clickable(True) + twidth = { + 'integer': 60, + 'float': 80, + 'float_time': 80, + 'date': 70, + 'datetime': 120, + 'selection': 90, + 'char': 100, + 'one2many': 50, + 'many2many': 50, + 'boolean': 20, + } + if 'width' in fields[fname]: + width = int(fields[fname]['width']) + else: + width = twidth.get(fields[fname]['type'], 100) + col.set_min_width(width) + col.connect('clicked', sort_model, treeview) + col.set_resizable(True) + #col.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) + col.set_visible(not fields[fname].get('invisible', False)) + i = treeview.append_column(col) + if 'sum' in fields[fname] and fields[fname]['type'] \ + in ('integer', 'float', 'float_time'): + label = gtk.Label() + label.set_use_markup(True) + label_str = fields[fname]['sum'] + ': ' + label_bold = bool(int(fields[fname].get('sum_bold', 0))) + if label_bold: + label.set_markup('%s' % label_str) + else: + label.set_markup(label_str) + label_sum = gtk.Label() + label_sum.set_use_markup(True) + dict_widget[i] = (fname, label, label_sum, + fields.get('digits', (16,2))[1], label_bold) + return treeview, dict_widget, [], on_write + + +class Char(object): + + def __init__(self, field_name, treeview=None, attrs=None, window=None): + self.field_name = field_name + self.attrs = attrs or {} + self.renderer = gtk.CellRendererText() + self.treeview = treeview + self.window = window + + def setter(self, column, cell, store, iter): + model = store.get_value(iter, 0) + text = self.get_textual_value(model) + cell.set_property('text', text) + color = self.get_color(model) + cell.set_property('foreground', str(color)) + if self.attrs['type'] in ('float', 'integer', 'boolean'): + align = 1 + else: + align = 0 + if self.treeview.editable: + field = model[self.field_name] + if not field.get_state_attrs(model).get('valid', True): + cell.set_property('background', + COLORS.get('invalid', 'white')) + elif bool(int(field.get_state_attrs(model).get('required', 0))): + cell.set_property('background', + COLORS.get('required', 'white')) + cell.set_property('xalign', align) + + def get_color(self, model): + to_display = '' + for color, expr in self.treeview.colors.items(): + if model.expr_eval(expr, check_load=False): + to_display = color + break + return to_display or 'black' + + def open_remote(self, model, create, changed=False, text=None): + raise NotImplementedError + + def get_textual_value(self, model): + return model[self.field_name].get_client(model) or '' + + def value_from_text(self, model, text): + return text + +class Int(Char): + + def value_from_text(self, model, text): + return int(text) + + def get_textual_value(self, model): + return locale.format('%d', + model[self.field_name].get_client(model) or 0, True) + +class Boolean(Int): + + def __init__(self, *args): + super(Boolean, self).__init__(*args) + self.renderer = gtk.CellRendererToggle() + self.renderer.connect('toggled', self._sig_toggled) + + def get_textual_value(self, model): + return model[self.field_name].get_client(model) or 0 + + def setter(self, column, cell, store, iter): + model = store.get_value(iter, 0) + value = self.get_textual_value(model) + cell.set_active(bool(value)) + + def _sig_toggled(self, renderer, path): + store = self.treeview.get_model() + model = store.get_value(store.get_iter(path), 0) + field = model[self.field_name] + if not field.get_state_attrs(model).get('readonly', False): + value = model[self.field_name].get_client(model) + model[self.field_name].set_client(model, int(not value)) + self.treeview.set_cursor(path) + return True + + +class Date(Char): + server_format = DT_FORMAT + display_format = locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y') + + def get_textual_value(self, model): + value = model[self.field_name].get_client(model) + if not value: + return '' + date = time.strptime(value, self.server_format) + return time.strftime(self.display_format, date) + + def value_from_text(self, model, text): + if not text: + return False + try: + date = time.strptime(text, self.display_format) + except: + try: + date = list(time.localtime()) + date[2] = int(text) + date = tuple(date) + except: + return False + return time.strftime(self.server_format, date) + + +class Datetime(Date): + server_format = DHM_FORMAT + display_format = locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y') + \ + ' %H:%M:%S' + + def get_textual_value(self, model): + value = model[self.field_name].get_client(model) + if not value: + return '' + date = time.strptime(value, self.server_format) + if 'tz' in rpc.session.context: + try: + import pytz + lzone = pytz.timezone(rpc.session.context['tz']) + szone = pytz.timezone(rpc.session.timezone) + datetime = DT.datetime(date[0], date[1], date[2], date[3], + date[4], date[5], date[6]) + sdt = szone.localize(datetime, is_dst=True) + ldt = sdt.astimezone(lzone) + date = ldt.timetuple() + except: + pass + return time.strftime(self.display_format, date) + + def value_from_text(self, model, text): + if not text: + return False + try: + date = time.strptime(text, self.display_format) + except: + try: + datetime = list(time.localtime()) + datetime[2] = int(text) + date = tuple(datetime) + except: + return False + if 'tz' in rpc.session.context: + try: + import pytz + lzone = pytz.timezone(rpc.session.context['tz']) + szone = pytz.timezone(rpc.session.timezone) + datetime = DT.datetime(date[0], date[1], date[2], date[3], + date[4], date[5], date[6]) + ldt = lzone.localize(datetime, is_dst=True) + sdt = ldt.astimezone(szone) + date = sdt.timetuple() + except: + pass + return time.strftime(self.server_format, date) + +class Float(Char): + def get_textual_value(self, model): + digit = self.attrs.get('digits', (16, 2))[1] + return locale.format('%.'+str(digit)+'f', + model[self.field_name].get_client(model) or 0.0, True) + + def value_from_text(self, model, text): + try: + return locale.atof(text) + except: + return 0.0 + + +class FloatTime(Char): + + def get_textual_value(self, model): + val = model[self.field_name].get_client(model) + value = '%02d:%02d' % (math.floor(abs(val)), + round(abs(val) % 1 + 0.01, 2) * 60) + if val < 0: + value = '-' + value + return value + + def value_from_text(self, model, text): + try: + if text and ':' in text: + return round(int(text.split(':')[0]) + \ + int(text.split(':')[1]) / 60.0,2) + else: + return locale.atof(text) + except: + pass + return 0.0 + +class M2O(Char): + + def value_from_text(self, model, text): + if not text: + return False + + relation = model[self.field_name].attrs['relation'] + rpc_relation = RPCProxy(relation) + + domain = model[self.field_name].domain_get(model) + context = model[self.field_name].context_get(model) + + names = rpc_relation.name_search(text, domain, 'ilike', context) + if len(names) != 1: + return self.search_remote(relation, [x[0] for x in names], + domain=domain, context=context)[0] + return names[0] + + def open_remote(self, model, create=True, changed=False, text=None): + modelfield = model.mgroup.mfields[self.field_name] + relation = modelfield.attrs['relation'] + + domain = modelfield.domain_get(model) + context = modelfield.context_get(model) + if create: + obj_id = None + elif not changed: + obj_id = modelfield.get(model) + else: + rpc_relation = RPCProxy(relation) + + names = rpc_relation.name_search(text, domain, 'ilike', context) + if len(names) == 1: + return True, names[0] + searched = self.search_remote(relation, [x[0] for x in names], + domain=domain, context=context) + if searched[0]: + return True, searched + return False, False + dia = M2ODialog(relation, obj_id, domain=domain, context=context, + window=self.window) + res, value = dia.run() + dia.destroy() + if res: + return True, value + else: + return False, False + + def search_remote(self, relation, ids=None, domain=None, context=None): + rpc_relation = RPCProxy(relation) + + win = win_search(relation, sel_multi=False, ids=ids, context=context, + domain=domain) + found = win.go() + if found: + return rpc_relation.name_get([found[0]], context)[0] + else: + return False, None + +class UnsettableColumn(Exception): + + def __init__(self): + Exception.__init__() + + +class O2M(Char): + + def get_textual_value(self, model): + return '( ' + str(len(model[self.field_name].\ + get_client(model).models)) + ' )' + + def value_from_text(self, model, text): + raise UnsettableColumn('Can not set column of type o2m') + + +class M2M(Char): + + def get_textual_value(self, model): + value = model[self.field_name].get_client(model) + if value: + return '(%s)' % len(value) + else: + return '(0)' + + def value_from_text(self, model, text): + if not text: + return [] + if not (text[0] != '('): + return model[self.field_name].get(model) + relation = model[self.field_name].attrs['relation'] + rpc_relation = RPCProxy(relation) + domain = model[self.field_name].domain_get(model) + context = model[self.field_name].context_get(model) + names = rpc_relation.name_search(text, domain, 'ilike', context) + ids = [x[0] for x in names] + win = win_search(relation, sel_multi=True, ids=ids, context=context, + domain=domain) + found = win.go() + return found or [] + + def open_remote(self, model, create=True, changed=False, text=None): + modelfield = model[self.field_name] + relation = modelfield.attrs['relation'] + + rpc_relation = RPCProxy(relation) + context = model[self.field_name].context_get(model) + domain = model[self.field_name].domain_get(model) + if create: + if text and len(text) and text[0] != '(': + domain.append(('name', '=', text)) + ids = rpc_relation.search(domain) + if ids and len(ids)==1: + return True, ids + else: + ids = model[self.field_name].get_client(model) + win = win_search(relation, sel_multi=True, ids=ids, context=context, + domain=domain) + found = win.go() + if found: + return True, found + else: + return False, None + +class Selection(Char): + + def __init__(self, *args): + super(Selection, self).__init__(*args) + self.renderer = gtk.CellRendererCombo() + selection_data = gtk.ListStore(str, str) + for i in self.attrs.get('selection', []): + selection_data.append(i) + self.renderer.set_property('model', selection_data) + self.renderer.set_property('text-column', 1) + + def get_textual_value(self, model): + selection = dict(model[self.field_name].attrs['selection']) + return selection.get(model[self.field_name].get(model), '') + + def value_from_text(self, model, text): + selection = model[self.field_name].attrs['selection'] + for val, txt in selection: + if txt == text: + return val + return False + +CELLTYPES = { + 'char': Char, + 'many2one': M2O, + 'date': Date, + 'one2many': O2M, + 'many2many': M2M, + 'selection': Selection, + 'float': Float, + 'float_time': FloatTime, + 'integer': Int, + 'datetime': Datetime, + 'boolean': Boolean, +} diff --git a/tryton/gui/window/view_form/view/widget_parse.py b/tryton/gui/window/view_form/view/widget_parse.py new file mode 100644 index 0000000000..6d33a7595d --- /dev/null +++ b/tryton/gui/window/view_form/view/widget_parse.py @@ -0,0 +1,46 @@ +from interface import ParserInterface +#import form_gtk +import tree_gtk +#import graph_gtk +#import calendar_gtk +#from form import ViewForm +from list import ViewList +#from graph import ViewGraph +#from calendar import ViewCalendar + +PARSERS = { +# 'form': form_gtk.parser_form, + 'tree': tree_gtk.parser_tree, +# 'graph': graph_gtk.parser_graph, +# 'calendar': calendar_gtk.parser_calendar, +} + +PARSERS2 = { +# 'form': ViewForm, + 'tree': ViewList, +# 'graph': ViewGraph, +# 'calendar': ViewCalendar, +} + + +class WidgetParse(ParserInterface): + + def parse(self, screen, root_node, fields, toolbar=None): + widget = None + for node in root_node.childNodes: + if not node.nodeType == node.ELEMENT_NODE: + continue + if node.localName in PARSERS: + widget = PARSERS[node.localName](self.window, self.parent, + self.attrs, screen) + wid, child, buttons, on_write = widget.parse(screen.resource, + node, fields) + screen.set_on_write(on_write) + res = PARSERS2[node.localName](self.window, screen, wid, child, + buttons, toolbar) + res.title = widget.title + widget = res + break + else: + pass + return widget diff --git a/tryton/gui/window/view_form/widget_search/__init__.py b/tryton/gui/window/view_form/widget_search/__init__.py new file mode 100644 index 0000000000..5db6452a7f --- /dev/null +++ b/tryton/gui/window/view_form/widget_search/__init__.py @@ -0,0 +1 @@ +from form import * diff --git a/tryton/gui/window/view_form/widget_search/calendar.py b/tryton/gui/window/view_form/widget_search/calendar.py new file mode 100644 index 0000000000..480d535ca2 --- /dev/null +++ b/tryton/gui/window/view_form/widget_search/calendar.py @@ -0,0 +1,124 @@ +import time +import datetime as DT +import gtk +import gettext +import locale +from interface import Interface +from tryton.common import DT_FORMAT + +_ = gettext.gettext + +if not hasattr(locale, 'nl_langinfo'): + locale.nl_langinfo = lambda *a: '%x' + +if not hasattr(locale, 'D_FMT'): + locale.D_FMT = None + + +class Calendar(Interface): + + def __init__(self, name, parent, attrs=None): + super(Calendar, self).__init__(self, name, parent, attrs) + + tooltips = gtk.Tooltips() + self.widget = gtk.HBox(spacing=3) + + self.entry1 = gtk.Entry() + self.entry1.set_property('width-chars', 10) + self.entry1.set_property('activates_default', True) + tooltips.set_tip(self.entry1, _('Start date')) + self.widget.pack_start(self.entry1, expand=False, fill=True) + + self.eb1 = gtk.EventBox() + tooltips.set_tip(self.eb1, _('Open the calendar widget')) + self.eb1.set_events(gtk.gdk.BUTTON_PRESS) + self.eb1.connect('button_press_event', self.cal_open, self.entry1, + parent) + img = gtk.Image() + img.set_from_stock('gtk-zoom-in', gtk.ICON_SIZE_MENU) + img.set_alignment(0.5, 0.5) + self.eb1.add(img) + self.widget.pack_start(self.eb1, expand=False, fill=False) + + self.widget.pack_start(gtk.Label('-'), expand=False, fill=False) + + self.entry2 = gtk.Entry() + self.entry2.set_property('width-chars', 10) + self.entry2.set_property('activates_default', True) + tooltips.set_tip(self.entry2, _('End date')) + self.widget.pack_start(self.entry2, expand=False, fill=True) + + self.eb2 = gtk.EventBox() + tooltips.set_tip(self.eb2, _('Open the calendar widget')) + self.eb2.set_events(gtk.gdk.BUTTON_PRESS) + self.eb2.connect('button_press_event', self.cal_open, self.entry2, + parent) + img = gtk.Image() + img.set_from_stock('gtk-zoom-in', gtk.ICON_SIZE_MENU) + img.set_alignment(0.5, 0.5) + self.eb2.add(img) + self.widget.pack_start(self.eb2, expand=False, fill=False) + + tooltips.enable() + + def _date_get(self, value): + try: + date = time.strptime(value, + locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y')) + except: + return False + return time.strftime(DT_FORMAT, date) + + def _value_get(self): + res = [] + val = self.entry1.get_text() + if val: + res.append((self.name, '>=', self._date_get(val))) + val = self.entry2.get_text() + if val: + res.append((self.name, '<=', self._date_get(val))) + return res + + def _value_set(self, value): + pass + + value = property(_value_get, _value_set, None, + _('The content of the widget or ValueError if not valid')) + + def cal_open(self, widget, event, dest, parent=None): + win = gtk.Dialog(_('Tiny ERP - Date selection'), parent, + gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OK, gtk.RESPONSE_OK)) + + cal = gtk.Calendar() + cal.display_options(gtk.CALENDAR_SHOW_HEADING | \ + gtk.CALENDAR_SHOW_DAY_NAMES | \ + gtk.CALENDAR_SHOW_WEEK_NUMBERS) + cal.connect('day-selected-double-click', + lambda *x: win.response(gtk.RESPONSE_OK)) + win.vbox.pack_start(cal, expand=True, fill=True) + win.show_all() + + try: + val = self._date_get(dest.get_text()) + if val: + cal.select_month(int(val[5:7])-1, int(val[0:4])) + cal.select_day(int(val[8:10])) + except ValueError: + pass + + response = win.run() + if response == gtk.RESPONSE_OK: + year, month, day = cal.get_date() + date = DT.date(year, month+1, day) + dest.set_text(date.strftime( + locale.nl_langinfo(locale.D_FMT).replace('%y', '%Y'))) + win.destroy() + + def clear(self): + self.value = '' + + def sig_activate(self, fct): + self.entry1.connect_after('activate', fct) + self.entry2.connect_after('activate', fct) diff --git a/tryton/gui/window/view_form/widget_search/char.py b/tryton/gui/window/view_form/widget_search/char.py new file mode 100644 index 0000000000..91e023b163 --- /dev/null +++ b/tryton/gui/window/view_form/widget_search/char.py @@ -0,0 +1,41 @@ +import gtk +import gettext +from interface import Interface + +_ = gettext.gettext + + +class Char(Interface): + + def __init__(self, name, parent, attrs=None): + if attrs is None: + attrs = {} + super(Char, self).__init__(self, name, parent, attrs) + + self.widget = gtk.Entry() + self.widget.set_max_length(int(attrs.get('size', 16))) + self.widget.set_width_chars(5) + self.widget.set_property('activates_default', True) + + def _value_get(self): + value = self.widget.get_text() + if value: + return [(self.name, self.attrs.get('comparator', 'ilike'), value)] + else: + return [] + + def _value_set(self, value): + self.widget.set_text(value) + + value = property(_value_get, _value_set, None, + _('The content of the widget or ValueError if not valid')) + + def clear(self): + self.value = '' + + def _readonly_set(self, value): + self.widget.set_editable(not value) + self.widget.set_sensitive(not value) + + def sig_activate(self, fct): + self.widget.connect_after('activate', fct) diff --git a/tryton/gui/window/view_form/widget_search/checkbox.py b/tryton/gui/window/view_form/widget_search/checkbox.py new file mode 100644 index 0000000000..ec47297a3f --- /dev/null +++ b/tryton/gui/window/view_form/widget_search/checkbox.py @@ -0,0 +1,32 @@ +import gtk +import gettext +from interface import Interface + +_ = gettext.gettext + + +class CheckBox(Interface): + + def __init__(self, name, parent, attrs=None): + super(CheckBox, self).__init__(name, parent, attrs) + + self.widget = gtk.combo_box_entry_new_text() + self.widget.append_text('') + self.widget.append_text(_('Yes')) + self.widget.append_text(_('No')) + + self.entry = self.widget.child + self.entry.set_property('activates_default', True) + self.entry.set_editable(False) + + def _value_get(self): + val = self.entry.get_text() + if val: + return [(self.name, '=', int(val == _('Yes')))] + return [] + + def _value_set(self, value): + pass + + value = property(_value_get, _value_set, None, + _('The content of the widget or ValueError if not valid')) diff --git a/tryton/gui/window/view_form/widget_search/form.py b/tryton/gui/window/view_form/widget_search/form.py new file mode 100644 index 0000000000..cff2792460 --- /dev/null +++ b/tryton/gui/window/view_form/widget_search/form.py @@ -0,0 +1,309 @@ +import gtk +from xml.parsers import expat +import sys +import gettext + +_ = gettext.gettext + + +class _container(object): + + def __init__(self, max_width): + self.cont = [] + self.max_width = max_width + self.width = {} + self.count = 0 + self.col = 0 + + def new(self, col=8): + self.col = col+1 + table = gtk.Table(1, col) + table.set_homogeneous(False) + table.set_col_spacings(3) + table.set_row_spacings(0) + table.set_border_width(1) + self.cont.append( (table, 1, 0) ) + + def get(self): + return self.cont[-1][0] + + def pop(self): + return self.cont.pop()[0] + + def newline(self): + (table, i, j) = self.cont[-1] + if i > 0: + self.cont[-1] = (table, 1, j+1) + table.resize(j + 1, self.col) + + def wid_add(self, widget, length=1, name=None, expand=False, ypadding=0): + self.count += 1 + (table, i, j) = self.cont[-1] + if length > self.col: + length = self.col + if length + i > self.col: + self.newline() + (table, i, j) = self.cont[-1] + if name: + vbox = gtk.VBox(homogeneous=False, spacing=1) + label = gtk.Label(name) + label.set_alignment(0.0, 0.5) + vbox.pack_start(label, expand=False) + vbox.pack_start(widget, expand=expand, fill=True) + wid = vbox + else: + wid = widget + yopt = False + if expand: + yopt = yopt | gtk.EXPAND |gtk.FILL + table.attach(wid, i, i+length, j, j+1, + yoptions=yopt, xoptions=gtk.FILL|gtk.EXPAND, + ypadding=ypadding, xpadding=5) + self.cont[-1] = (table, i+length, j) + width = 750 + if widget: + width = widget.size_request()[0] + self.width[('%d.%d') % (i, j)] = width + return wid + + +class Parse(object): + + def __init__(self, parent, fields, model=''): + self.fields = fields + self.parent = parent + self.model = model + self.col = 8 + self.focusable = None + self.add_widget_end = [] + self.container = None + self.button_param = gtk.Button() + self.spin_limit = gtk.SpinButton(climb_rate=1, digits=0) + self.spin_offset = gtk.SpinButton(climb_rate=1, digits=0) + self.title = 'Form' + self.notebooks = [] + self.dict_widget = {} + self.hb_param = None + + def _psr_start(self, name, attrs): + + if name in ('form','tree'): + self.title = attrs.get('string', self.title) + self.container.new(self.col) + elif name=='field': + val = attrs.get('select', False) \ + or self.fields[str(attrs['name'])].get('select', False) + if val: + if int(val) <= 1: + self.add_widget(attrs, val) + else: + self.add_widget_end.append((attrs, val)) + + def add_widget(self, attrs, val): + ftype = attrs.get('widget', self.fields[str(attrs['name'])]['ftype']) + self.fields[str(attrs['name'])].update(attrs) + self.fields[str(attrs['name'])]['model']=self.model + if ftype not in WIDGETS_TYPE: + return False + widget_act = WIDGETS_TYPE[ftype][0](str(attrs['name']), self.parent, + self.fields[attrs['name']]) + if 'string' in self.fields[str(attrs['name'])]: + label = self.fields[str(attrs['name'])]['string']+' :' + else: + label = None + size = WIDGETS_TYPE[ftype][1] + if not self.focusable: + self.focusable = widget_act.widget + wid = self.container.wid_add(widget_act.widget, size, label, + int(self.fields[str(attrs['name'])].get('expand',0))) + if int(val) <= 1: + wid.show() + self.dict_widget[str(attrs['name'])] = (widget_act, wid, int(val)) + + def add_parameters(self): + hb_param = gtk.HBox(spacing=3) + hb_param.pack_start(gtk.Label(_('Limit :')), expand=False, fill=False) + + self.spin_limit.set_numeric(False) + self.spin_limit.set_adjustment(gtk.Adjustment(value=80, lower=1, + upper=sys.maxint, step_incr=10, page_incr=100, page_size=100)) + self.spin_limit.set_property('visible', True) + + hb_param.pack_start(self.spin_limit, expand=False, fill=False) + + hb_param.pack_start(gtk.Label(_('Offset :')), expand=False, fill=False) + + self.spin_offset.set_numeric(False) + self.spin_offset.set_adjustment(gtk.Adjustment(value=0, lower=0, + upper=sys.maxint, step_incr=80, page_incr=100, page_size=100)) + + hb_param.pack_start(self.spin_offset, expand=False, fill=False) + + return hb_param + + def _psr_end(self, name): + pass + + def _psr_char(self, name): + pass + + def parse(self, xml_data, max_width): + psr = expat.ParserCreate() + psr.StartElementHandler = self._psr_start + psr.EndElementHandler = self._psr_end + psr.CharacterDataHandler = self._psr_char + self.container = _container(max_width) + + psr.Parse(xml_data) + for i in self.add_widget_end: + self.add_widget(*i) + self.add_widget_end = [] + + img = gtk.Image() + img.set_from_stock('gtk-add', gtk.ICON_SIZE_BUTTON) + self.button_param.set_image(img) + self.button_param.set_relief(gtk.RELIEF_NONE) + self.button_param.set_alignment(0.5, 0.5) + table = self.container.get() + table.attach(self.button_param, 0, 1, 0, 1, + yoptions=gtk.FILL, xoptions=gtk.FILL, ypadding=2, xpadding=0) + + self.hb_param = self.container.wid_add(self.add_parameters(), l=8, + name=_('Parameters :')) + + + return (self.dict_widget, self.container.pop()) + + +class Form(object): + + def __init__(self, xml, fields, model=None, parent=None, domain=None, + call=None): + if domain is None: + domain = [] + parser = Parse(parent, fields, model=model) + self.parent = parent + self.fields = fields + self.model = model + self.parser = parser + self.call = call + #get the size of the window and the limite / decalage Hbox element + width = 640 + if self.parent: + width = self.parent.size_request()[0] + (self.widgets, self.widget) = parser.parse(xml, width) + self.widget.show_all() + self.hb_param = parser.hb_param + self.button_param = parser.button_param + self.button_param.connect('clicked', self.toggle) + self.spin_limit = parser.spin_limit + self.spin_limit.connect('value-changed', self.limit_changed) + self.spin_limit.set_activates_default(True) + self.spin_offset = parser.spin_offset + self.spin_offset.set_activates_default(True) + self.focusable = parser.focusable + self.id = 0 + self.name = parser.title + self._hide = True + self.hide() + for i in domain: + if i[0] in self.widgets: + if i[1] == '=': + self.widgets[i[0]][0]._readonly_set(True) + for i in self.widgets.values(): + i[0].sig_activate(self.sig_activate) + self.spin_limit.connect_after('activate', self.sig_activate) + self.spin_offset.connect_after('activate', self.sig_activate) + + def clear(self, widget): + self.id = 0 + for i in self.widgets.values(): + i[0].clear() + + def show(self): + for i, widget, value in self.widgets.values(): + if value >= 2: + widget.show() + self.hb_param.show() + self._hide = False + + def hide(self): + for i, widget, value in self.widgets.values(): + if value >= 2: + widget.hide() + self.hb_param.hide() + self._hide = True + + def toggle(self, widget): + img = gtk.Image() + if self._hide: + self.show() + img.set_from_stock('gtk-remove', gtk.ICON_SIZE_BUTTON) + widget.set_image(img) + else: + self.hide() + img.set_from_stock('gtk-add', gtk.ICON_SIZE_BUTTON) + widget.set_image(img) + + def limit_changed(self, widget): + self.spin_offset.set_increments(step=self.spin_limit.get_value(), + page=100) + + def set_limit(self, value): + return self.spin_limit.set_value(value) + + def get_limit(self): + return self.spin_limit.get_value() + + def get_offset(self): + return self.spin_offset.get_value() + + def sig_activate(self, *args): + if self.call: + obj, fct = self.call + fct(obj) + + def _value_get(self): + res = [] + for i in self.widgets: + res += self.widgets[i][0].value + return res + + def _value_set(self, value): + for i in value: + if i in self.widgets: + self.widgets[i][0].value = value[i] + + value = property(_value_get, _value_set, None, + _('The content of the form or excpetion if not valid')) + +import calendar +import spinbutton +import spinint +import selection +import char +import checkbox +import reference + +WIDGETS_TYPE = { + 'date': (calendar.Calendar, 2), + 'datetime': (calendar.Calendar, 2), + 'float': (spinbutton.SpinButton, 2), + 'integer': (spinint.SpinInt, 2), + 'selection': (selection.Selection, 2), + 'many2one_selection': (selection.Selection, 2), + 'char': (char.Char, 2), + 'boolean': (checkbox.CheckBox, 2), + 'reference': (reference.Reference, 2), + 'text': (char.Char, 2), + 'email': (char.Char, 2), + 'url': (char.Char, 2), + 'many2one': (char.Char, 2), + 'one2many': (char.Char, 2), + 'one2many_form': (char.Char, 2), + 'one2many_list': (char.Char, 2), + 'many2many_edit': (char.Char, 2), + 'many2many': (char.Char, 2), + 'callto': (char.Char, 2), + 'sip': (char.Char, 2), +} diff --git a/tryton/gui/window/view_form/widget_search/interface.py b/tryton/gui/window/view_form/widget_search/interface.py new file mode 100644 index 0000000000..2b010c4365 --- /dev/null +++ b/tryton/gui/window/view_form/widget_search/interface.py @@ -0,0 +1,34 @@ +import gettext + +_ = gettext.gettext + + +class Interface(object): + "Interface for search widget" + + def __init__(self, name, parent, attrs=None): + if attrs is None: + attrs = {} + self._value = None + self.parent = parent + self.name = name + self.model = attrs.get('model', None) + self.attrs = attrs + + def clear(self): + self.value = '' + + def _value_get(self): + return self._value + + def _value_set(self, value): + self._value = value + + value = property(_value_get, _value_set, None, + _('The content of the widget or excpetion if not valid')) + + def _readonly_set(self, value): + pass + + def sig_activate(self, fct): + pass diff --git a/tryton/gui/window/view_form/widget_search/reference.py b/tryton/gui/window/view_form/widget_search/reference.py new file mode 100644 index 0000000000..c582bf0677 --- /dev/null +++ b/tryton/gui/window/view_form/widget_search/reference.py @@ -0,0 +1,56 @@ +import gtk +import gettext +from interface import Interface + +_ = gettext.gettext + + +class Reference(Interface): + + def __init__(self, name, parent, attrs=None): + if attrs is None: + attrs = {} + super(Reference, self).__init__(name, parent, attrs) + self.widget = gtk.combo_box_entry_new_text() + self.widget.child.set_editable(False) + self.set_popdown(attrs.get('selection', [])) + self._selection = {} + + def get_model(self): + res = self.widget.child.get_text() + return self._selection.get(res, False) + + def set_popdown(self, selection): + model = self.widget.get_model() + model.clear() + lst = [] + for (i, j) in selection: + name = str(j) + if type(i) == type(1): + name += ' ('+str(i)+')' + lst.append(name) + self._selection[name] = i + self.widget.append_text('') + for name in lst: + self.widget.append_text(name) + return lst + + def _value_get(self): + if self.get_model(): + return [(self.name, 'like', self.get_model() + ',')] + else: + return [] + + def _value_set(self, value): + if value == False: + value = '' + for sel in self._selection: + if self._selection[sel] == value: + self.widget.child.set_text(sel) + + + value = property(_value_get, _value_set, None, + _('The content of the widget or ValueError if not valid')) + + def clear(self): + self.value = '' diff --git a/tryton/gui/window/view_form/widget_search/selection.py b/tryton/gui/window/view_form/widget_search/selection.py new file mode 100644 index 0000000000..3bb34c081b --- /dev/null +++ b/tryton/gui/window/view_form/widget_search/selection.py @@ -0,0 +1,56 @@ +import gtk +from interface import Interface + +class Selection(Interface): + + def __init__(self, name, parent, attrs=None): + if attrs is None: + attrs = {} + super(Selection, self).__init__(self, name, parent, attrs) + + self.widget = gtk.combo_box_entry_new_text() + self.widget.child.set_editable(False) + self._selection = {} + if 'selection' in attrs: + self.set_popdown(attrs.get('selection', [])) + + def set_popdown(self, selection): + model = self.widget.get_model() + model.clear() + self._selection = {} + lst = [] + for (i, j) in selection: + name = str(j) + if type(i) == type(1): + name += ' (' + str(i) + ')' + lst.append(name) + self._selection[name] = i + self.widget.append_text('') + for name in lst: + self.widget.append_text(name) + return lst + + def _value_get(self): + model = self.widget.get_model() + index = self.widget.get_active() + if index >= 0: + res = self._selection.get(model[index][0], False) + if res: + return [(self.name, '=', res)] + return [] + + def _value_set(self, value): + if value == False: + value = '' + for sel in self._selection: + if self._selection[sel] == value: + self.widget.child.set_text(sel) + + def clear(self): + self.value = '' + + value = property(_value_get, _value_set, None, + 'The content of the widget or ValueError if not valid') + + def _readonly_set(self, value): + self.widget.set_sensitive(not value) diff --git a/tryton/gui/window/view_form/widget_search/spinbutton.py b/tryton/gui/window/view_form/widget_search/spinbutton.py new file mode 100644 index 0000000000..690e6646c4 --- /dev/null +++ b/tryton/gui/window/view_form/widget_search/spinbutton.py @@ -0,0 +1,64 @@ +import gtk +import sys +import gettext +from interface import Interface + +_ = gettext.gettext + + +class SpinButton(Interface): + + def __init__(self, name, parent, attrs=None): + if attrs is None: + attrs = {} + super(SpinButton, self).__init__(self, name, parent, attrs) + + self.widget = gtk.HBox(spacing=3) + + adj1 = gtk.Adjustment(0.0, -sys.maxint, sys.maxint, 1.0, 5.0, 5.0) + self.spin1 = gtk.SpinButton(adj1, 1.0, + digits=int(attrs.get('digits', (14, 2))[1])) + self.spin1.set_numeric(True) + self.spin1.set_activates_default(True) + self.widget.pack_start(self.spin1, expand=False, fill=True) + + self.widget.pack_start(gtk.Label('-'), expand=False, fill=False) + + adj2 = gtk.Adjustment(0.0, -sys.maxint, sys.maxint, 1.0, 5.0, 5.0) + self.spin2 = gtk.SpinButton(adj2, 1.0, + digits=int(attrs.get('digits', (14, 2))[1])) + self.spin2.set_numeric(True) + self.spin2.set_activates_default(True) + self.widget.pack_start(self.spin2, expand=False, fill=True) + + def _value_get(self): + res = [] + self.spin1.update() + self.spin2.update() + if self.spin1.get_value() > self.spin2.get_value(): + if self.spin2.get_value() != 0.0: + res.append((self.name, '>=', self.spin2.get_value())) + res.append((self.name, '<=', self.spin1.get_value())) + else: + res.append((self.name, '>=', self.spin1.get_value())) + elif self.spin2.get_value() > self.spin1.get_value(): + res.append((self.name, '<=', self.spin2.get_value())) + res.append((self.name, '>=', self.spin1.get_value())) + elif (self.spin2.get_value() == self.spin1.get_value()) \ + and (self.spin1.get_value() != 0.0): + res.append((self.name, '=', self.spin1.get_value())) + return res + + def _value_set(self, value): + self.spin1.set_value(value) + self.spin2.set_value(value) + + value = property(_value_get, _value_set, None, + _('The content of the widget or ValueError if not valid')) + + def clear(self): + self.value = 0.00 + + def sig_activate(self, fct): + self.spin1.connect_after('activate', fct) + self.spin2.connect_after('activate', fct) diff --git a/tryton/gui/window/view_form/widget_search/spinint.py b/tryton/gui/window/view_form/widget_search/spinint.py new file mode 100644 index 0000000000..e73b903bf8 --- /dev/null +++ b/tryton/gui/window/view_form/widget_search/spinint.py @@ -0,0 +1,56 @@ +import gtk +import gettext +import sys +from interface import Interface + +_ = gettext.gettext + + +class SpinInt(Interface): + + def __init__(self, name, parent, attrs=None): + super(Spinint, self).__init__(name, parent, attrs=attrs) + self.widget = gtk.HBox(spacing=3) + adj1 = gtk.Adjustment(0.0, 0.0, sys.maxint, 1.0, 5.0, 5.0) + self.spin1 = gtk.SpinButton(adj1, 1, digits=0) + self.spin1.set_numeric(True) + self.spin1.set_activates_default(True) + self.widget.pack_start(self.spin1, expand=False, fill=True) + self.widget.pack_start(gtk.Label('-'), expand=False, fill=False) + adj2 = gtk.Adjustment(0.0, 0.0, sys.maxint, 1.0, 5.0, 5.0) + self.spin2 = gtk.SpinButton(adj2, 1, digits=0) + self.spin2.set_numeric(True) + self.spin2.set_activates_default(True) + self.widget.pack_start(self.spin2, expand=False, fill=True) + + def _value_get(self): + res = [] + self.spin1.update() + self.spin2.update() + if self.spin1.get_value_as_int() > self.spin2.get_value_as_int(): + if self.spin2.get_value_as_int() != 0: + res.append((self.name, '>=', self.spin2.get_value_as_int())) + res.append((self.name, '<=', self.spin1.get_value_as_int())) + else: + res.append((self.name, '>=', self.spin1.get_value_as_int())) + elif self.spin2.get_value_as_int() > self.spin1.get_value_as_int(): + res.append((self.name, '<=', self.spin2.get_value_as_int())) + res.append((self.name, '>=', self.spin1.get_value_as_int())) + elif (self.spin2.get_value_as_int() == self.spin1.get_value_as_int()) \ + and (self.spin1.get_value_as_int() != 0): + res.append((self.name, '=', self.spin1.get_value_as_int())) + return res + + def _value_set(self, value): + self.spin1.set_value(value) + self.spin2.set_value(value) + + value = property(_value_get, _value_set, None, + _('The content of the widget or ValueError if not valid')) + + def clear(self): + self.value = 0.0 + + def sig_activate(self, fct): + self.spin1.connect_after('activate', fct) + self.spin2.connect_after('activate', fct)