diff --git a/tryton/common/__init__.py b/tryton/common/__init__.py index a2e17ce29b..c32ac23aa5 100644 --- a/tryton/common/__init__.py +++ b/tryton/common/__init__.py @@ -2,17 +2,17 @@ # this repository contains the full copyright notices and license terms. from . import timedelta from .common import ( - COLORS, - COLOR_RGB, COLOR_SCHEMES, FORMAT_ERROR, MODELACCESS, MODELHISTORY, + COLOR_RGB, COLOR_SCHEMES, COLORS, FORMAT_ERROR, MODELACCESS, MODELHISTORY, MODELNAME, TRYTON_ICON, VIEW_SEARCH, IconFactory, Login, Logout, RPCContextReload, RPCException, RPCExecute, RPCProgress, Tooltips, apply_label_attributes, ask, check_version, concurrency, data2pixbuf, date_format, ellipsize, error, file_open, file_selection, file_write, - filter_domain, generateColorscheme, get_align, get_hostname, get_port, - get_sensible_widget, get_toplevel_window, hex2rgb, highlight_rgb, humanize, - idle_add, mailto, message, node_attributes, process_exception, - resize_pixbuf, selection, setup_window, slugify, sur, sur_3b, - timezoned_date, to_xml, untimezoned_date, url_open, userwarning, warning) + filter_domain, generateColorscheme, get_align, + get_gdk_backend, get_hostname, get_port, get_sensible_widget, + get_toplevel_window, hex2rgb, highlight_rgb, humanize, idle_add, mailto, + message, node_attributes, process_exception, resize_pixbuf, selection, + setup_window, slugify, sur, sur_3b, timezoned_date, to_xml, + untimezoned_date, url_open, userwarning, warning) from .domain_inversion import ( concat, domain_inversion, eval_domain, extract_reference_models, filter_leaf, inverse_leaf, localize_domain, merge, @@ -57,6 +57,7 @@ filter_leaf, generateColorscheme, get_align, + get_gdk_backend, get_hostname, get_port, get_sensible_widget, diff --git a/tryton/common/common.py b/tryton/common/common.py index 6150228a64..e295bdf181 100644 --- a/tryton/common/common.py +++ b/tryton/common/common.py @@ -1395,3 +1395,19 @@ def wrapper(*args, **kwargs): def setup_window(window): if sys.platform == 'darwin': window.set_mnemonic_modifier(Gdk.ModifierType.CONTROL_MASK) + + +def get_gdk_backend(): + if sys.platform == 'darwin': + return 'macos' + elif sys.platform == 'win32': + return 'win32' + else: + dm = Gdk.DisplayManager.get() + default = dm.props.default_display + dm_class_name = default.__class__.__name__ + if 'X11' in dm_class_name: + return 'x11' + elif 'Wayland' in dm_class_name: + return 'wayland' + return 'x11' diff --git a/tryton/gui/window/view_form/screen/screen.py b/tryton/gui/window/view_form/screen/screen.py index 8c97edec37..5b82a69977 100644 --- a/tryton/gui/window/view_form/screen/screen.py +++ b/tryton/gui/window/view_form/screen/screen.py @@ -38,7 +38,6 @@ class Screen: # Width of tree columns per model # It is shared with all connection but it is the price for speed. tree_column_width = collections.defaultdict(lambda: {}) - tree_column_optional = {} def __init__(self, model_name, **attributes): context = attributes.get('context', {}) diff --git a/tryton/gui/window/view_form/view/form_gtk/dictionary.py b/tryton/gui/window/view_form/view/form_gtk/dictionary.py index 187c2a949a..df6d938d92 100644 --- a/tryton/gui/window/view_form/view/form_gtk/dictionary.py +++ b/tryton/gui/window/view_form/view/form_gtk/dictionary.py @@ -482,10 +482,10 @@ def __init__(self, view, attrs): else: self.wid_text = None + self.tooltips = Tooltips() if not no_command: - self.tooltips = Tooltips() self.tooltips.set_tip(self.but_add, _('Add value')) - self.tooltips.enable() + self.tooltips.enable() self._readonly = False self._record_id = None diff --git a/tryton/gui/window/view_form/view/form_gtk/sourceeditor.py b/tryton/gui/window/view_form/view/form_gtk/sourceeditor.py index c808cbb184..0ddc6b0b90 100644 --- a/tryton/gui/window/view_form/view/form_gtk/sourceeditor.py +++ b/tryton/gui/window/view_form/view/form_gtk/sourceeditor.py @@ -140,6 +140,56 @@ def __init__(self, view, attrs): check_btn.connect('clicked', self.check_code) toolbar.insert(check_btn, -1) + self.replacing = False + self.replacements = None + self.search_band = Gtk.HBox() + self.search_band.connect('key-press-event', self._hide_search) + self.search_entry = Gtk.Entry() + self.search_entry.props.max_width_chars = 40 + self.search_entry.props.placeholder_text = "Search" + self.search_entry.connect('activate', self.do_search) + self.search_entry.set_icon_from_icon_name( + Gtk.EntryIconPosition.PRIMARY, 'system-search-symbolic') + self.replace_entry = Gtk.Entry() + self.replace_entry.props.max_width_chars = 40 + self.replace_entry.props.placeholder_text = "Replace" + self.replace_entry.connect('activate', self.do_replace) + replace = Gtk.Button.new_with_label("Replace") + replace.connect('clicked', self.do_replace) + replace_all = Gtk.Button.new_with_label("Replace All") + replace_all.connect('clicked', self.do_replace_all) + self.occurrence_label = Gtk.Label() + prev_button = Gtk.Button.new_from_icon_name( + 'go-previous-symbolic', Gtk.IconSize.BUTTON) + prev_button.connect('clicked', self.prev_search_entry) + next_button = Gtk.Button.new_from_icon_name( + 'go-next-symbolic', Gtk.IconSize.BUTTON) + next_button.connect('clicked', self.next_search_entry) + # Required because Tabbing from the next button to the replace entry + # deselects the text in the TextView + next_button.connect('key-press-event', self._go_replace) + self.search_band.pack_start( + self.search_entry, expand=False, fill=True, padding=2) + self.search_band.pack_start( + prev_button, expand=False, fill=True, padding=2) + self.search_band.pack_start( + next_button, expand=False, fill=True, padding=2) + self.search_band.pack_start( + self.occurrence_label, expand=True, fill=True, padding=2) + self.search_band.pack_start( + self.replace_entry, expand=False, fill=True, padding=2) + self.search_band.pack_start( + replace, expand=False, fill=True, padding=2) + self.search_band.pack_start( + replace_all, expand=False, fill=True, padding=2) + self.search_settings = GtkSource.SearchSettings() + self.search_settings.props.wrap_around = True + self.search_context = GtkSource.SearchContext.new( + self.sourcebuffer, self.search_settings) + self.search_context.connect( + 'notify::occurrences-count', self.update_occurrences) + self.sourcebuffer.connect('mark-set', self._mark_cb) + self.error_store = Gtk.ListStore( GObject.TYPE_INT, GObject.TYPE_STRING, GObject.TYPE_STRING) @@ -167,6 +217,7 @@ def __init__(self, view, attrs): vbox.pack_start(toolbar, expand=False, fill=True, padding=0) vbox.pack_start(sc_editor, expand=True, fill=True, padding=0) + vbox.pack_start(self.search_band, expand=False, fill=True, padding=0) vbox.pack_start(sc_error, expand=True, fill=True, padding=0) vbox.show_all() @@ -224,6 +275,7 @@ def cell_setter(column, cell, store, iter, data): else: self.widget = vbox + self.search_band.hide() self.tree_data = [] self.known_funcs = set() @@ -280,6 +332,7 @@ def display(self): self.tree_data = [] self.model.clear() self.known_funcs.clear() + self.search_band_hide() self.check_code() def populate_tree(self, tree_data, parent=None): @@ -379,7 +432,15 @@ def focus_line(self, selection): def _test_check(self, sourceview, event): if Gdk.keyval_name(event.keyval) == 'F7': self.check_code(None) - sourceview.emit_stop_by_name('key-press-event') + sourceview.stop_emission_by_name('key-press-event') + elif (Gdk.keyval_name(event.keyval) == 'f' + and event.state & Gdk.ModifierType.CONTROL_MASK): + if self.search_band.is_visible(): + self.search_band_hide() + else: + self.search_band.show_all() + self.search_entry.grab_focus() + sourceview.stop_emission_by_name('key-press-event') def _clear_marks(self, sourcebuffer): tag_table = sourcebuffer.get_tag_table() @@ -403,3 +464,161 @@ def _clear_marks(self, sourcebuffer): def tree_display_tooltip(self, treeview, x, y, keyboard_mode, tooltip): return False + + def search_band_hide(self): + self.search_entry.set_text('') + self.replace_entry.set_text('') + self.occurrence_label.set_text('') + self.replacements = None + self.search_settings.props.search_text = '' + self.search_context.props.highlight = False + insert_mark = self.sourcebuffer.get_insert() + self.sourcebuffer.move_mark_by_name( + 'selection_bound', + self.sourcebuffer.get_iter_at_mark(insert_mark)) + self.search_band.hide() + self.grab_focus() + + def do_search(self, entry, forward=True): + self.replacements = None + searched_text = entry.get_text() + if searched_text: + self.search_settings.props.search_text = searched_text + self.search_context.props.highlight = True + if forward: + self.next_search_entry(None) + else: + self.prev_search_entry(None) + else: + self.search_settings.props.search_text = '' + self.search_context.props.highlight = False + + def prev_search_entry(self, button): + self.replacements = None + if not self.search_settings.props.search_text: + self.do_search(self.search_entry) + + selection = self.sourcebuffer.get_selection_bounds() + if not selection: + insert_mark = self.sourcebuffer.get_insert() + start = self.sourcebuffer.get_iter_at_mark(insert_mark) + else: + start = selection[0] + + self.search_context.backward_async( + start, None, self._backward_search_finished) + + def _backward_search_finished(self, context, task): + success, start, stop, wrap = context.backward_finish2(task) + if not success: + return + self.sourcebuffer.select_range(start, stop) + insert_mark = self.sourcebuffer.get_insert() + self.sourceview.scroll_mark_onscreen(insert_mark) + + if self.replacing: + self.replacing = False + GLib.idle_add(self.do_replace) + + def next_search_entry(self, button): + self.replacements = None + if not self.search_settings.props.search_text: + self.do_search(self.search_entry) + + selection = self.sourcebuffer.get_selection_bounds() + if not selection: + insert_mark = self.sourcebuffer.get_insert() + start = self.sourcebuffer.get_iter_at_mark(insert_mark) + else: + start = selection[1] + + self.search_context.forward_async( + start, None, self._forward_search_finished) + + def _forward_search_finished(self, context, task): + success, start, stop, wrap = context.forward_finish2(task) + if not success: + return + self.sourcebuffer.select_range(start, stop) + insert_mark = self.sourcebuffer.get_insert() + self.sourceview.scroll_mark_onscreen(insert_mark) + + if self.replacing: + self.replacing = False + GLib.idle_add(self.do_replace) + + def _go_replace(self, widget, event): + if (Gdk.keyval_name(event.keyval) == 'Tab' + and not event.state & Gdk.ModifierType.MODIFIER_MASK): + self.replace_entry.grab_focus_without_selecting() + return True + + def _hide_search(self, widget, event): + if Gdk.keyval_name(event.keyval) in {'Escape'}: + self.search_band_hide() + + def do_replace(self, *args): + self.replacements = None + replacement_text = self.replace_entry.get_text() + selection_bounds = self.sourcebuffer.get_selection_bounds() + if not replacement_text: + return + if (not self.search_settings.props.search_text + or not selection_bounds): + self.replacing = True + self.do_search(self.search_entry) + return + + replacement_text_length = self.replace_entry.get_buffer().get_bytes() + start, end = selection_bounds + self.search_context.replace( + start, end, replacement_text, replacement_text_length) + self.replacing = False + + selection_bound = self.sourcebuffer.get_selection_bound() + end = self.sourcebuffer.get_iter_at_mark(selection_bound) + self.search_context.forward_async( + end, None, self._forward_search_finished) + + def do_replace_all(self, *args): + searched_text = self.search_entry.get_text() + replacement_text = self.replace_entry.get_text() + if not replacement_text or not searched_text: + return + + self.search_settings.props.search_text = searched_text + start, _ = self.sourcebuffer.get_bounds() + self.search_context.forward_async( + start, None, self._replace_all_search_finished) + + def _replace_all_search_finished(self, context, task): + success, start, stop, wrap = context.forward_finish2(task) + if not success: + return + + replacement_text = self.replace_entry.get_text() + replacement_text_length = self.replace_entry.get_buffer().get_bytes() + self.replacements = context.replace_all( + replacement_text, replacement_text_length) + + def update_occurrences(self, context, param): + count = context.get_occurrences_count() + if self.sourcebuffer.get_has_selection(): + start, end = self.sourcebuffer.get_selection_bounds() + position = context.get_occurrence_position(start, end) + else: + position = -1 + + if self.replacements is not None: + text = f"{self.replacements} occurrence(s) replaced" + elif count == -1: + text = "" + elif position == -1: + text = f"{count} occurrence(s)" + else: + text = f"{position} / {count} occurrence(s)" + self.occurrence_label.set_text(text) + + def _mark_cb(self, buffer, iter_, mark): + if mark.get_name() in {'insert', 'selection_bound'}: + GLib.idle_add(self.update_occurrences, self.search_context, None) diff --git a/tryton/gui/window/view_form/view/list.py b/tryton/gui/window/view_form/view/list.py index 4813e2a0f9..6d4ee0d088 100644 --- a/tryton/gui/window/view_form/view/list.py +++ b/tryton/gui/window/view_form/view/list.py @@ -16,7 +16,7 @@ COLOR_RGB, FORMAT_ERROR, RPCException, RPCExecute, Tooltips, domain_inversion, node_attributes, simplify, unique_value) from tryton.common.cellrendererbutton import CellRendererButton -from tryton.common.popup_menu import populate, popup +from tryton.common.popup_menu import populate from tryton.config import CONFIG from tryton.gui.window import Window from tryton.pyson import PYSONDecoder @@ -571,23 +571,6 @@ def __init__(self, view_id, screen, xml, children_field, self.display() - if self.optionals: - if self.draggable: - column = self.treeview.get_columns()[0] - else: - column = Gtk.TreeViewColumn() - column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) - column.name = None - column._type = 'optional' - self.treeview.insert_column(column, 0) - image = Gtk.Image() - image.set_from_pixbuf(common.IconFactory.get_pixbuf('tryton-menu')) - image.show() - column.set_widget(image) - column.set_fixed_width(25) - column.set_clickable(True) - column.connect('clicked', self.optional_menu) - # Add last column if necessary after display for updated visible for column in self.treeview.get_columns(): if column.get_expand() and column.get_visible(): @@ -599,30 +582,6 @@ def __init__(self, view_id, screen, xml, children_field, column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) self.treeview.append_column(column) - def optional_menu(self, column): - def toggle(menuitem, column): - column.set_visible(menuitem.get_active()) - self.save_optional() - - widget = column.get_widget() - menu = Gtk.Menu() - for optional in self.optionals: - menuitem = Gtk.CheckMenuItem(label=optional.get_title()) - menuitem.set_active(optional.get_visible()) - menuitem.connect('toggled', toggle, optional) - menu.add(menuitem) - popup(menu, widget) - - def save_optional(self): - fields = {c.name: not c.get_visible() for c in self.optionals} - try: - RPCExecute( - 'model', 'ir.ui.view_tree_optional', 'set_optional', - self.view_id, fields) - except RPCException: - pass - self.screen.tree_column_optional[self.view_id] = fields - def get_column_widget(self, column): 'Return the widget of the column' idx = [c for c in self.treeview.get_columns() @@ -1110,7 +1069,8 @@ def __select_changed(self, tree_sel): def do_selection_changed(): previous_record = self.record if (previous_record - and previous_record not in previous_record.group): + and (previous_record not in previous_record.group + or previous_record.destroyed)): previous_record = None # Because do_selection_changed is call through an idle_add it can @@ -1164,9 +1124,12 @@ def pre_validate(): GLib.idle_add(pre_validate) self.update_sum() - # Delay the switch to the record so that focus-out event of the mixed - # widget can be triggered - GLib.idle_add(do_selection_changed) + if self.screen._multiview_form: + tree, *forms = self.screen._multiview_form.widget_groups[\ + self.screen._multiview_group] + for form in forms: + form.set_value() + do_selection_changed() def set_value(self): if self.editable: @@ -1212,20 +1175,14 @@ def display(self, force=False): domain.append(tab_domain) domain = simplify(domain) decoder = PYSONDecoder(self.screen.context) - tree_column_optional = self.screen.tree_column_optional.get( - self.view_id, {}) for column in self.treeview.get_columns(): name = column.name if not name: continue widget = self.get_column_widget(column) widget.set_editable() - if column.name in tree_column_optional: - optional = tree_column_optional[column.name] - else: - optional = bool(int(widget.attrs.get('optional', '0'))) invisible = decoder.decode(widget.attrs.get('tree_invisible', '0')) - if invisible or optional: + if invisible: column.set_visible(False) elif name == self.screen.exclude_field: column.set_visible(False) diff --git a/tryton/gui/window/view_form/view/list_gtk/widget.py b/tryton/gui/window/view_form/view/list_gtk/widget.py index ea18437bec..20073c1da9 100644 --- a/tryton/gui/window/view_form/view/list_gtk/widget.py +++ b/tryton/gui/window/view_form/view/list_gtk/widget.py @@ -10,7 +10,8 @@ from gi.repository import Gdk, GLib, Gtk import tryton.common as common -from tryton.common import data2pixbuf, file_open, file_selection, file_write +from tryton.common import ( + data2pixbuf, file_open, file_selection, file_write, get_gdk_backend) from tryton.common.cellrendererbutton import CellRendererButton from tryton.common.cellrendererclickablepixbuf import ( CellRendererClickablePixbuf) @@ -1165,10 +1166,12 @@ def set_editable(self): def editing_started(self, cell, editable, path): super(Selection, self).editing_started(cell, editable, path) record, field = self._get_record_field_from_path(path) - # Combobox does not emit remove-widget when focus is changed - self.editable.connect( - 'editing-done', - lambda *a: self.editable.emit('remove-widget')) + gdk_backend = get_gdk_backend() + if gdk_backend == 'x11': + # Combobox does not emit remove-widget when focus is changed + self.editable.connect( + 'editing-done', + lambda *a: self.editable.emit('remove-widget')) selection_shortcuts(editable) @@ -1176,6 +1179,12 @@ def set_value(*a): return self.set_value(editable, record, field) editable.get_child().connect('activate', set_value) editable.get_child().connect('focus-out-event', set_value) + + if gdk_backend != 'x11': + def remove_entry(entry, event): + editable.emit('editing-done') + editable.emit('remove-widget') + editable.get_child().connect('focus-out-event', remove_entry) editable.connect('changed', set_value) self.update_selection(record, field) diff --git a/tryton/gui/window/view_form/view/screen_container.py b/tryton/gui/window/view_form/view/screen_container.py index 3268622955..b91e72dc5c 100644 --- a/tryton/gui/window/view_form/view/screen_container.py +++ b/tryton/gui/window/view_form/view/screen_container.py @@ -564,6 +564,10 @@ def activate(self, widget): self.do_search(widget) def do_search(self, widget=None): + searched_text = self.get_text() + if searched_text != self.last_search_text: + self.last_search_text = searched_text + self.screen.offset = 0 self.screen.search_filter(self.get_text()) def set_cursor(self, new=False, reset_view=True): diff --git a/tryton/gui/window/wizard.py b/tryton/gui/window/wizard.py index f401a740f1..b62e951dcd 100644 --- a/tryton/gui/window/wizard.py +++ b/tryton/gui/window/wizard.py @@ -298,7 +298,7 @@ def end(self, callback=None): def set_cursor(self): if self.screen: - self.screen.set_cursor() + self.screen.set_cursor(reset_view=False) class WizardDialog(Wizard, NoModal):