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'):