# coding: utf-8
import re
import copy
import collections
import inflection
import qstylizer.descriptor.prop
import qstylizer.descriptor.subcontrol
import qstylizer.descriptor.pseudostate
import qstylizer.descriptor.pseudoprop
import qstylizer.descriptor.qclass
import qstylizer.descriptor.stylerule
QPROPERTIES = qstylizer.descriptor.prop.PropParent.get_attr_options()
QSUBCONTROLS = qstylizer.descriptor.subcontrol.SubControlParent.get_attr_options()
QPSEUDOSTATES = qstylizer.descriptor.pseudostate.PseudoStateParent.get_attr_options()
QPSEUDOPROPS = qstylizer.descriptor.pseudoprop.PseudoPropParent.get_attr_options()
QCLASSES = qstylizer.descriptor.qclass.ClassStyleParent.get_attr_options()
class StyleRule(
collections.OrderedDict, qstylizer.descriptor.prop.PropParent,
qstylizer.descriptor.pseudoprop.PseudoPropParent
):
"""StyleRule Object.
A dictionary containing nested Styles and property:value pairs.
Example structure::
,
"background-color": ,
"indicator": ,
"hover": ,
"border":
} />
} />
} />
Output format::
{
: ;
: ;
...
}
Stylesheet output::
QCheckBox {
color: red;
background-color: black;
}
QCheckBox::indicator {
border: 1px solid green;
}
QCheckBox::indicator:hover {
background-color: green;
border: 0px transparent black;
}
"""
_split_regex = "\*|\[[A-Za-z0-9='\"]+\]|\W*\w*"
@classmethod
def split_selector(cls, selector):
"""Split the selector based on the _split_regex.
Return a list of each component.
Example::
name = "QObject::subcontrol:pseudostate"
return value = ["QObject", "::subcontrol", ":pseudostate"]
:param name: String name
"""
selector = selector.replace("-", "_")
return re.findall(cls._split_regex, selector)[:-1]
def __init__(self, name=None, value=None, parent=None):
"""Initialize the StyleRule dictionary.
.. note:: All public variables will be put into ordered dictionary.
:param name: The name of the StyleRule
:param value: The property value
:param parent: The parent StyleRule
"""
super(StyleRule, self).__init__()
self._name = self._sanitize_key(name) if name else None
self._parent = parent
self._attributes = self.get_attributes()
self._attr_options = self.get_attr_options()
self._value = self._sanitize_value(value)
self._child_rules = collections.OrderedDict()
@staticmethod
def _sanitize_key(key):
"""Strip the key of colons and replace underscores with dashes.
:param key: A string variable
"""
key = str(key)
if key and key[0] not in ["Q", "#", "[", " "] and key != inflection.camelize(key) and not key.startswith("qproperty-"):
key = inflection.underscore(key)
return (
key
.replace("not_", "!")
.replace(":", "")
.replace("_", "-")
)
@staticmethod
def _sanitize_value(value):
"""Strip the value of any semi-colons.
:param value: A value of any type
"""
try:
if type(value) in [str, unicode]:
return value.replace(";", "")
except NameError:
if type(value) in [str, bytes]:
return value.replace(";", "")
return value
def find_or_create_child_rule(self, name):
"""Find or create a child rule from a string key.
If the key rule already exists, return the rule.
If there is a comma in requested key, return a StyleRuleList object.
Otherwise create rules from the style rule names in the key and return
the top level rule or property.
:param name: The dictionary key
"""
value = self.find_child_rule(name)
if value is not None:
return value
if "," in name:
rule_list = self.create_child_rule_list(name)
return rule_list
return self.create_child_rules(name)
def find_child_rule(self, key):
"""Find rule from key.
Return the sanitized key's hash value in the ordered dict.
"""
key = self._sanitize_key(key)
return self.get(key)
def create_child_rule_list(self, name):
"""Create a StyleRuleList object and add it to ordered dict.
:param name: String name
"""
rule_list = StyleRuleList(name=name, parent=self)
self.set_child_rule(name, rule_list)
return rule_list
def create_child_rules(self, selector):
"""Create child rules from selector string.
Split the selector into individual components based on the _split_regex
and recursively build the StyleRule hierarchy looping through
the components.
If selector is "QClass::subcontrol:pseudostate",
curr_name is "QClass" and remaining is "::subcontrol:pseudostate"
:param name: String to split
"""
curr_name = self.split_selector(selector)[0]
remaining = selector.split(curr_name, 1)[-1].replace("-", "_")
rule = self.find_child_rule(curr_name)
if rule is None:
rule = self.create_child_rule(curr_name)
if remaining and remaining != curr_name:
return rule.find_or_create_child_rule(remaining)
return rule
def create_child_rule(self, name):
"""Create child rule from name.
Determine subclass from name, create an instance of the subclass,
then add it to ordered dict.
:param name: String name
"""
class_ = rule_class(name)
rule = class_(name=name, parent=self)
self.set_child_rule(name, rule)
return rule
def set_child_rule(self, key, value, **kwargs):
"""Set rule in ordered dictionary."""
key = self._sanitize_key(key)
if not isinstance(value, StyleRule):
value = self._sanitize_value(value)
value = PropRule(name=key, value=value, parent=self)
self._add_child_rule(value)
return super(StyleRule, self).__setitem__(key, value, **kwargs)
def _add_child_rule(self, rule):
"""Add a rule to the _child_rules dictionary.
:param rule: A StyleRule object.
"""
if rule.selector not in self._child_rules:
self._child_rules[rule.selector] = rule
if self._parent is not None:
self._parent._add_child_rule(rule)
@property
def selector(self):
"""Get the selector.
Example::
Object::subcontrol:pseudostate
"""
if self._parent is None:
return self.name if self.name else ""
return self._parent.selector + self.scope_operator + self.name
@property
def name(self):
"""Return the name of the StyleRule (eg. "QCheckBox").
Strip off the scope operator if it exists in name.
"""
if (self._name and self.scope_operator and
self._name.startswith(self.scope_operator)):
return self._name.split(self.scope_operator, 1)[-1]
return self._name
@name.setter
def name(self, name):
self._name = self._sanitize_key(name)
@property
def parent(self):
return self._parent
@property
def scope_operator(self):
"""Get the scope operator.
The scope operator is the "::" or ":" that is printed in front
of the name of the StyleRule in the selector.
Subclasses are expected to define the scope operator or else it will
try to guess it based on its position in the hierarchy.
"""
if self.is_top_level():
return ""
elif self.is_leaf():
return ":"
return "::"
def is_leaf(self):
"""Determine if StyleRule is a leaf.
StyleRule is a leaf its child rules dictionary contains only PropRules.
"""
for rule in self._child_rules.values():
if not isinstance(rule, PropRule):
return False
return True
def is_top_level(self):
"""Determine if StyleRule is top level.
StyleRule is top level if its parent is of the StyleSheet class.
"""
return isinstance(self._parent, StyleSheet)
def _to_string_recursive(self):
"""Convert all child rules into a single stirng in css format.
Loop through all of the rules and generate a stylesheet string.
"""
stylesheet = self.toString(recursive=False)
for rule in self._child_rules.values():
stylesheet += rule.toString(recursive=False)
return stylesheet
def _to_string(self, recursive=False):
"""Convert to a single string in css format.
:param recursive: Output all of the sub-style rules.
"""
if recursive:
return self._to_string_recursive()
rule_template = "{selector} {{\n{properties}}}\n"
prop_template = " {}: {};\n"
properties = ""
sheet = ""
selector = self.selector
for key, rule in self.items():
if rule.value is not None:
properties += prop_template.format(key, rule.value)
if properties:
sheet = rule_template.format(**locals())
return sheet
def toString(self, *args, **kwargs):
"""Convert to a single string in css format.
Use camelcase for function name to match PyQt/PySide.
"""
return self._to_string(*args, **kwargs)
def _set_values(self, *args, **kwargs):
"""Set property values in the style rule."""
for key, value in kwargs.items():
if "-" in key:
self.__getitem__(key).setValue(value)
else:
self.__getattribute__(key).setValue(value)
def update(self, *args, **kwargs):
if isinstance(args[0], StyleRule):
for key, child_rule in args[0]._child_rules.items():
rule = self.find_or_create_child_rule(key)
for k, v in child_rule.items():
if isinstance(v, StyleRule):
v = copy.deepcopy(v)
v._parent = rule
rule[k] = v
def setValues(self, *args, **kwargs):
"""Set property values in the style rule.
Use camelcase for function name to match PyQt/PySide.
"""
self._set_values(*args, **kwargs)
def _set_value(self, value):
"""Set property value."""
self._value = self._sanitize_value(value)
def setValue(self, value):
"""Set property value.
Use camelcase for function name to match PyQt/PySide.
"""
self._set_value(value)
@property
def value(self):
return self._value
def __getitem__(self, key):
"""Override the retrieving of a value from dictionary.
Find or create rule in the key's hash location.
:param key: The dictionary key
"""
return self.find_or_create_child_rule(key)
def __delattr__(self, name):
"""Override the deletion of an attribute.
If attribute starts with an underscore, delete the attribute from
the object's __dict__ otherwise delete it from the ordered dict.
:param name: String name of attribute to delete
"""
if name in self.__dict__:
return super(StyleRule, self).__delattr__(name)
return self.__delitem__(name)
def __setattr__(self, name, val):
"""Override the setting of an attribute.
If name is in the pre-defined attributes, call the attribute's
descriptor's __set__ function. Otherwise, add it to ordered dict as-is.
:param name: The attribute name
:param val: The value to set
"""
if name.startswith("_"):
return super(StyleRule, self).__setattr__(name, val)
elif name in self._attributes:
return self._attributes[name].__set__(self, val)
return self.set_child_rule(name, val)
def __setitem__(self, key, value, **kwargs):
"""Override the setting of an attribute in ordered dict.
If key is in pre-defined attributes, call attribute's descriptor's
__set__ function. Otherwise add the value to ordered dict as-is.
:param key: The hash key of the ordered dict
:param value: The value to map to hash key
"""
if key in self._attr_options:
if "-" in key:
key = key.replace("-", "_")
key = inflection.camelize(key)
key = key[0].lower() + key[1:]
try:
return self._attributes[key].__set__(self, value)
except KeyError:
pass
return self.set_child_rule(key, value, **kwargs)
def __deepcopy__(self, memo):
"""Override deepcopy.
Make a deepcopy of all member attributes as well as all rule rules
in ordered dictionary.
"""
cls = self.__class__
result = cls.__new__(cls)
result._name = self._name
result._value = self._value
result._parent = None
result._child_rules = collections.OrderedDict()
memo[id(self)] = result
for k, v in self.__dict__.items():
if k in ("_child_rules",):
continue
setattr(result, k, copy.deepcopy(v, memo))
result.clear()
for k, v in self.items():
if isinstance(v, StyleRule):
v = copy.deepcopy(v, memo)
v._parent = result
result.set_child_rule(k, v)
result._parent = self._parent
return result
def __repr__(self, *args, **kwargs):
"""Set the representation to look like xml syntax."""
value_attr = ""
name_attr = ""
if self.value is not None:
value_attr = "value={0!r} ".format(self.value)
if self.name is not None:
name_attr = "name={0!r} ".format(self.name)
return "<{0} {1}{2}/>".format(
self.__class__.__name__, name_attr, value_attr
)
def __str__(self):
"""Call toString if StyleRule is cast to string."""
return self.toString()
class StyleSheet(StyleRule, qstylizer.descriptor.qclass.ClassStyleParent):
"""The StyleSheet definition.
Contains descriptors for all class and property options.
"""
def is_global_scope(self):
"""Determine if stylesheet is global scope.
A StyleSheet is global scope if it has no rules.
Resulting string should contain no brackets.
::
background-color: red;
border: none;
"""
return self.is_leaf()
def _to_string_recursive(self):
"""Convert all child rules into a single stirng in css format.
Loop through all of the rules and generate a stylesheet string.
"""
stylesheet = self.toString(recursive=False)
for key, rule in self._child_rules.items():
if key == "*":
continue
stylesheet += rule.toString(recursive=False)
return stylesheet
def _to_string(self, recursive=True):
"""Return the selector and properties as a single string.
:param recursive: Loop through all rules to generate a stylesheet.
"""
if recursive:
return self._to_string_recursive()
rule_template = "{selector} {{\n{properties}}}\n"
prop_template = " {}: {};\n"
selector = self.selector
if self.is_global_scope():
rule_template = "{properties}"
prop_template = "{}: {};\n"
else:
selector = "*"
properties = ""
sheet = ""
for key, value in self.items():
# Output the "*" property values if applicable.
if key == "*":
for global_key, global_value in self.__getitem__("*").items():
if not isinstance(global_value, StyleRule):
properties += prop_template.format(
global_key, global_value
)
elif global_value.value is not None:
properties += prop_template.format(
global_key, global_value.value
)
if not isinstance(value, StyleRule):
properties += prop_template.format(key, value)
elif value.value is not None:
properties += prop_template.format(key, value.value)
if properties:
sheet = rule_template.format(**locals())
return sheet
@property
def name(self):
"""Return the name of the StyleSheet."""
return self._name
class ClassRule(
StyleRule, qstylizer.descriptor.subcontrol.SubControlParent,
qstylizer.descriptor.pseudostate.PseudoStateParent
):
"""The ClassRule definition.
Example class rule name: "QCheckBox".
Contains descriptors for all subcontrols and pseudostates.
"""
class ObjectRule(ClassRule):
"""The ObjectRule definition.
Example object rule name: "#objectName".
Inherits from ClassRule. Only difference is "#" is the scope operator.
"""
@property
def scope_operator(self):
return "#"
class ChildClassRule(ClassRule):
"""The ChildClassRule definition.
Example object rule name: " QFrame".
Inherits from ClassRule.
::
QWidget QFrame {
property: value
}
"""
@property
def scope_operator(self):
return " "
class ObjectPropRule(ClassRule):
"""The ObjectPropRule definition.
Example object property rule name: "[echoMode="2"]".
"""
@property
def scope_operator(self):
return ""
class StyleRuleList(StyleRule):
"""The StyleRuleList definition.
Example rule list name: "QCheckBox, QComboBox".
"""
@staticmethod
def _sanitize_key(key):
"""Strip the key of newlines only."""
return str(key).replace("\n", "")
def _create_child_rules_in_parent(self, name, val):
"""Find or create value in parent StyleRule
Will loop through all components in name separated by a comma and set the
property in each of the rules in the parent StyleRule.
:param name: The attribute name
:param val: The value
"""
rule_names = self.name.split(",")
for rule_name in rule_names:
self._parent.find_or_create_child_rule(rule_name).__setattr__(
name, val
)
return None
@property
def scope_operator(self):
return ""
def __setattr__(self, name, val):
"""Override the setting of an attribute.
:param name: The attribute name
:param val: The value to set
"""
if name.startswith("_"):
return super(StyleRule, self).__setattr__(name, val)
return self._create_child_rules_in_parent(name, val)
def __setitem__(self, key, value, **kwargs):
"""Override the setting of a value in ordered dict.
:param key: The hash key of the ordered dict
:param value: The value to map to hash key
"""
return self._create_child_rules_in_parent(key, value)
@property
def name(self):
"""Return the name with no spaces."""
return self._name.replace(" ", "")
class SubControlRule(StyleRule, qstylizer.descriptor.pseudostate.PseudoStateParent):
"""The SubControlRule definition.
Example subcontrol name: "::indicator".
"""
@property
def scope_operator(self):
return "::"
class PseudoStateRule(SubControlRule):
"""The PseudoStateRule definition.
Example pseudostate name: ":hover".
"""
@property
def scope_operator(self):
return ":"
class PseudoPropRule(PseudoStateRule):
"""The PseudoPropRule definition.
The PseudoPropRule covers PseudoStates and properties that have the same
name like "top", "bottom", "left", and "right".
It is basically a PseudoStateRule that also stores a property value.
In the following example, *top* is the PseudoPropRule.
.. code-block:: python
>>> css.QWidget.tab.top = "0"
>>> css.QWidget.tab.top.color = "green"
>>> print(css.toString())
QWidget::tab {
top: 0;
}
QWidget::tab:top {
color: green;
}
"""
class PropRule(StyleRule):
"""The PropRule definition.
Example prop rule name: "background-color".
"""
def rule_class(name):
"""Determine StyleRule subclass from string name.
:param name: name of type string
"""
if "!" in name:
name = name.replace("!", "")
name = name[0].lower() + name[1:]
class_ = StyleRule
if name.startswith("::") or name in QSUBCONTROLS:
class_ = SubControlRule
elif name.startswith(":") or name in QPSEUDOSTATES:
class_ = PseudoStateRule
elif name.startswith("#"):
class_ = ObjectRule
elif name.startswith(" "):
class_ = ChildClassRule
elif name in QCLASSES or name.startswith("Q"):
class_ = ClassRule
elif "=" in name:
class_ = ObjectPropRule
return class_