From f40b0b2e1c2383ae8e5cdd80e8bcad68f5008399 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Mon, 7 Aug 2017 19:58:00 +0200 Subject: [PATCH] Make readonly, required and invalid widget themable - add classes to the widget - provide a minimal application style - allow to load a user theme - add the profile name to the main window issue6643 review35141002 --- CHANGELOG | 2 ++ doc/usage.rst | 21 ++++++++++++++ tryton/client.py | 28 +++++++++++++++++++ tryton/common/widget_style.py | 11 +++++++- tryton/gui/main.py | 11 ++++++++ .../view_form/view/form_gtk/many2many.py | 3 ++ .../view_form/view/form_gtk/one2many.py | 3 ++ .../view_form/view/form_gtk/state_widget.py | 3 ++ .../window/view_form/view/form_gtk/widget.py | 18 ++++++++---- tryton/gui/window/view_form/view/list.py | 13 ++++----- 10 files changed, 98 insertions(+), 15 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 17de9ba294..d4e426e559 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,5 @@ +* Make readonly, required and invalid widget themable +* Load user CSS theme * Use profile name or login details in title Version 4.4.0 - 2017-05-01 diff --git a/doc/usage.rst b/doc/usage.rst index 49a264c268..cb9fe04bb8 100644 --- a/doc/usage.rst +++ b/doc/usage.rst @@ -719,6 +719,26 @@ formatted text. The tags are explain follows: * Foreground color: It is a attribute `foreground` for `span` tag, i.e. text +CSS +*** + +The client can be styled using the file `theme.css`. + +Here are the list of custom selectors: + + * `.readonly`: readonly widget or label + + * `.required`: widget or label of required field + + * `.invalid`: widget for which the field value is not valid + + * `window.profile-`: the name of the connection profile is set on the + main window + +For more information about style option see `GTK+ CSS`_ + +.. GTK+ CSS:: https://developer.gnome.org/gtk3/stable/chap-css-overview.html + Appendix ******** @@ -734,6 +754,7 @@ Configuration File ~/.config/tryton/x.y/ca_certs # Certification Authority (http://docs.python.org/library/ssl.html#ssl-certificates) ~/.config/tryton/x.y/profiles.cfg # Profile configuration ~/.config/tryton/x.y/plugins # Local user plugins directory + ~/.config.tryton/x.y/theme.css # Custom CSS theme .. note:: `~` means the home directory of the user. diff --git a/tryton/client.py b/tryton/client.py index 81250ce2f5..b50813359d 100644 --- a/tryton/client.py +++ b/tryton/client.py @@ -14,6 +14,8 @@ except ImportError: sys.modules['cdecimal'] = decimal import os +Gdk = None +Gtk = None if os.environ.get('GTK_VERSION', '2').startswith('3'): import pygtkcompat pygtkcompat.enable() @@ -213,6 +215,32 @@ if sys.platform == 'win32': return super(Dialog, self).run() gtk.Dialog = Dialog +CSS = """ +.readonly entry { + background-color: @insensitive_bg_color; +} +.required entry { + border-color: darker(@unfocused_borders); +} +.invalid entry { + border-color: @error_color; +} +""" + +if Gdk and Gtk: + screen = Gdk.Screen.get_default() + style_context = Gtk.StyleContext() + provider = Gtk.CssProvider() + provider.load_from_data(CSS) + style_context.add_provider_for_screen( + screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + theme_path = os.path.join(get_config_dir(), 'theme.css') + if os.path.exists(theme_path): + provider = Gtk.CssProvider() + provider.load_from_path(theme_path) + style_context.add_provider_for_screen( + screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_USER) + class TrytonClient(object): "Tryton client" diff --git a/tryton/common/widget_style.py b/tryton/common/widget_style.py index ab9309973e..c6d8e34a88 100644 --- a/tryton/common/widget_style.py +++ b/tryton/common/widget_style.py @@ -16,7 +16,16 @@ if gtk_version == 2: widget.modify_base( gtk.STATE_NORMAL, style.base[gtk.STATE_INSENSITIVE]) widget.modify_bg(gtk.STATE_NORMAL, style.bg[gtk.STATE_INSENSITIVE]) + + def widget_class(widget, name, value): + pass else: def set_widget_style(widget, editable): - # TODO pass + + def widget_class(widget, name, value): + style_context = widget.get_style_context() + if value: + style_context.add_class(name) + else: + style_context.remove_class(name) diff --git a/tryton/gui/main.py b/tryton/gui/main.py index b9dce72600..5f2c9db2a2 100644 --- a/tryton/gui/main.py +++ b/tryton/gui/main.py @@ -388,6 +388,17 @@ class Main(object): if value: titles.append(value) self.window.set_title(' - '.join(titles)) + try: + style_context = self.window.get_style_context() + except AttributeError: + pass + else: + for name in style_context.list_classes(): + if name.startswith('profile-'): + style_context.remove_class(name) + if CONFIG['login.profile']: + style_context.add_class( + 'profile-%s' % CONFIG['login.profile']) def _set_menu_connection(self): menu_connection = gtk.Menu() diff --git a/tryton/gui/window/view_form/view/form_gtk/many2many.py b/tryton/gui/window/view_form/view/form_gtk/many2many.py index be9b828976..51738f0370 100644 --- a/tryton/gui/window/view_form/view/form_gtk/many2many.py +++ b/tryton/gui/window/view_form/view/form_gtk/many2many.py @@ -11,6 +11,7 @@ import gettext from tryton.common.placeholder_entry import PlaceholderEntry from tryton.common.completion import get_completion, update_completion from tryton.common.domain_parser import quote +from tryton.common.widget_style import widget_class _ = gettext.gettext @@ -228,6 +229,8 @@ class Many2Many(Widget): def _set_label_state(self): attrlist = common.get_label_attributes(self._readonly, self._required) self.title.set_attributes(attrlist) + widget_class(self.title, 'readonly', self._readonly) + widget_class(self.title, 'required', self._required) def _set_button_sensitive(self): if self.record and self.field: diff --git a/tryton/gui/window/view_form/view/form_gtk/one2many.py b/tryton/gui/window/view_form/view/form_gtk/one2many.py index b3d00b8df3..838d866876 100644 --- a/tryton/gui/window/view_form/view/form_gtk/one2many.py +++ b/tryton/gui/window/view_form/view/form_gtk/one2many.py @@ -12,6 +12,7 @@ import tryton.common as common from tryton.common.placeholder_entry import PlaceholderEntry from tryton.common.completion import get_completion, update_completion from tryton.common.domain_parser import quote +from tryton.common.widget_style import widget_class _ = gettext.gettext @@ -255,6 +256,8 @@ class One2Many(Widget): def _set_label_state(self): attrlist = common.get_label_attributes(self._readonly, self._required) self.title.set_attributes(attrlist) + widget_class(self.title, 'readonly', self._readonly) + widget_class(self.title, 'required', self._required) def _set_button_sensitive(self): access = common.MODELACCESS[self.screen.model_name] diff --git a/tryton/gui/window/view_form/view/form_gtk/state_widget.py b/tryton/gui/window/view_form/view/form_gtk/state_widget.py index bc8bf3ed7c..9901206249 100644 --- a/tryton/gui/window/view_form/view/form_gtk/state_widget.py +++ b/tryton/gui/window/view_form/view/form_gtk/state_widget.py @@ -3,6 +3,7 @@ import gtk import tryton.common as common +from tryton.common.widget_style import widget_class class StateMixin(object): @@ -46,6 +47,8 @@ class Label(StateMixin, gtk.Label): or state_changes.get('readonly')) attrlist = common.get_label_attributes(readonly, required) self.set_attributes(attrlist) + widget_class(self, 'readonly', readonly) + widget_class(self, 'required', required) class VBox(StateMixin, gtk.VBox): diff --git a/tryton/gui/window/view_form/view/form_gtk/widget.py b/tryton/gui/window/view_form/view/form_gtk/widget.py index baaecef4df..bd0d29bbfe 100644 --- a/tryton/gui/window/view_form/view/form_gtk/widget.py +++ b/tryton/gui/window/view_form/view/form_gtk/widget.py @@ -8,6 +8,7 @@ import tryton.common as common from tryton.gui.window.nomodal import NoModal from tryton.common import TRYTON_ICON from tryton.common import RPCExecute, RPCException +from tryton.common.widget_style import widget_class _ = gettext.gettext @@ -98,15 +99,20 @@ class Widget(object): self.invisible_set(self.attrs.get('invisible', False)) self._required_set(False) return - readonly = self.attrs.get('readonly', - field.get_state_attrs(record).get('readonly', False)) + states = field.get_state_attrs(record) + readonly = self.attrs.get('readonly', states.get('readonly', False)) if self.view.screen.readonly: readonly = True self._readonly_set(readonly) - self.invisible_set(self.attrs.get('invisible', - field.get_state_attrs(record).get('invisible', False))) - self._required_set( - field.get_state_attrs(record).get('required', False)) + widget_class(self.widget, 'readonly', readonly) + self._required_set(not readonly and states.get('required', False)) + widget_class( + self.widget, 'required', + not readonly and states.get('required', False)) + invalid = states.get('invalid', False) + widget_class(self.widget, 'invalid', not readonly and invalid) + self.invisible_set(self.attrs.get( + 'invisible', states.get('invisible', False))) def set_value(self, record, field): pass diff --git a/tryton/gui/window/view_form/view/list.py b/tryton/gui/window/view_form/view/list.py index ff425b646f..68d7cd717e 100644 --- a/tryton/gui/window/view_form/view/list.py +++ b/tryton/gui/window/view_form/view/list.py @@ -2,7 +2,6 @@ # this repository contains the full copyright notices and license terms. import gobject import gtk -import pango import sys import json import locale @@ -17,6 +16,7 @@ from tryton.gui.window import Window from tryton.common.popup_menu import populate from tryton.common import RPCExecute, RPCException, node_attributes, Tooltips from tryton.common import domain_inversion, simplify, unique_value +from tryton.common.widget_style import widget_class from tryton.pyson import PYSONDecoder import tryton.common as common from . import View @@ -454,13 +454,10 @@ class ViewTree(View): if field and self.editable: required = field.attrs.get('required') readonly = field.attrs.get('readonly') - if (required or not readonly) and hasattr(pango, 'AttrWeight'): - attrlist = pango.AttrList() - if required: - attrlist.change(pango.AttrWeight(pango.WEIGHT_BOLD, 0, -1)) - if not readonly: - attrlist.change(pango.AttrStyle(pango.STYLE_ITALIC, 0, -1)) - label.set_attributes(attrlist) + attrlist = common.get_label_attributes(readonly, required) + label.set_attributes(attrlist) + widget_class(label, 'readonly', readonly) + widget_class(label, 'required', required) label.show() help = None if field and field.attrs.get('help'):