# Copyright 2004-2019 Tom Rothamel # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. from __future__ import print_function from cpython.ref cimport PyObject, Py_XDECREF from libc.string cimport memset from libc.stdlib cimport calloc, free import renpy include "styleconstants.pxi" ################################################################################ # Property Functions ################################################################################ # A class that wraps a pointer to a property function. cdef class PropertyFunctionWrapper: cdef property_function function # A dictionary that maps the name of a property function into a # PropertyFunctionWrapper. cdef dict property_functions = { } cdef void register_property_function(name, property_function function): """ Registers `function` to be the property function called for the property `name`. """ cdef PropertyFunctionWrapper pfw pfw = PropertyFunctionWrapper() pfw.function = function property_functions[name] = pfw ################################################################################ # Style Management ################################################################################ # A map from style name (a tuple) to the style object with that name. styles = { } cpdef get_style(name): """ Gets the style with `name`, which must be a string. If the style doesn't exist, and it contains an underscore in it, creates a new style that has as a parent the part after the first underscore, if a parent with that name exists. """ nametuple = (name,) rv = styles.get(nametuple, None) if rv is not None: return rv start, _mid, end = name.partition("_") if not end: raise Exception("Style %r does not exist." % name) # Deal with inheritance of styles beginning with _ a bit specially, # so _foo_bar inherits from _bar, not bar. if not start: _start, _mid, end = name[1:].partition("_") if not end: raise Exception("Style %r does not exist." % name) end = "_" + end try: parent = get_style(end) except: raise Exception("Style %r does not exist." % name) rv = Style(parent, name=nametuple) styles[nametuple] = rv return rv cpdef get_or_create_style(name): """ Like get_style, but if the style doesn't exist, its hierarchy is created, eventually inheriting from default. """ nametuple = (name,) rv = styles.get(nametuple, None) if rv is not None: return rv start, _mid, end = name.partition("_") # We need both sides of the _, as we don't want to have # _foo auto-inherit from foo. if not start or not end: rv = Style("default", name=nametuple) styles[nametuple] = rv return rv parent = get_or_create_style(end) rv = Style(parent, name=nametuple) styles[nametuple] = rv return rv cpdef get_full_style(name): """ Gets the style with `name`, which must be a tuple. """ rv = styles.get(name, None) if rv is not None: return rv rv = get_style(name[0]) for i in name[1:]: rv = rv[i] return rv cpdef get_tuple_name(s): """ Gets the tuple name of a style, where `s` may be a tuple, Style, or string. If `s` is None, returns None. """ if isinstance(s, StyleCore): return s.name elif isinstance(s, tuple): return s elif s is None: return s else: return (s,) def get_text_style(style, default): """ If style exists, returns `style` + "_text". Otherwise, returns `default`. For indexed styles, the above is applied first, and then indexing is applied. """ if style is None: style = default style = get_tuple_name(style) start = style[0] rest = style[1:] rv = get_full_style((start + "_text",)) for i in rest: rv = rv[i] return rv class StyleManager(object): """ The object exported as style in the store. """ def __setattr__(self, name, value): if not isinstance(value, StyleCore): if getattr(value, "_is_style_compat", False): self.__dict__[name] = value return raise Exception("Value is not a style.") cdef StyleCore style = value name = (name,) if style.name is None: style.name = name styles[name] = value __setitem__ = __setattr__ def __getattr__(self, name): return get_style(name) __getitem__ = __getattr__ def create(self, name, parent, description=None): """ Deprecated way of creating styles. """ s = Style(parent, help=description) self[name] = s def rebuild(self): renpy.style.rebuild() def exists(self, name): """ Returns `true` if name is a style. """ return (name in styles) or ((name,) in styles) def get(self, name): """ Gets a style, which may be a name or a tuple. """ if isinstance(name, tuple): return get_full_style(name) else: return get_style(name) ################################################################################ # Style Class ################################################################################ def style_name_to_string(name): if name is None: return "anonymous style" rv = "style " + name[0] for i in name[1:]: rv += "['{}']".format(i) return rv cdef class StyleCore: def __init__(self, parent, properties=None, name=None, help=None, heavy=True): """ `parent` The parent of this style. One of: * A Style object. * A string giving the name of a style. * A tuple giving the name of an indexed style. * None, to indicate there is no parent. `properties` A map from style property to its value. `name` If given, a tuple that will be the name of this style. `help` Help information for this style. `heavy` Ignored, but retained for compatibility. """ self.prefix = "insensitive_" self.prefix_offset = INSENSITIVE_PREFIX self.properties = [ ] if properties: if not type(properties) is dict: properties = dict(properties) self.properties.append(properties) if properties and ("insensitive_child" in properties): if properties["insensitive_child"] is False: import traceback traceback.print_stack() self.parent = get_tuple_name(parent) self.name = name self.help = help def copy(self): cdef StyleCore rv rv = Style(self.parent) rv.properties = list(self.properties) return rv def __richcmp__(self, o, int op): if self is o: eq = True elif type(self) != type(o): eq = False elif self.parent != o.parent: eq = False elif self.name != o.name: eq = False elif self.properties != o.properties: eq = False else: eq = True if op == 2: # == return eq elif op == 3: # != return not eq else: return NotImplemented def __dealloc__(self): unbuild_style(self) def __getstate__(self): rv = dict( properties=self.properties, prefix=self.prefix, name=self.name, parent=self.parent) return rv def __setstate__(self, state): self.properties = state["properties"] self.name = state["name"] self.set_parent(state["parent"]) self.set_prefix(state["prefix"]) def __repr__(self): if self.parent: return "<{} is {} @ {}>".format(style_name_to_string(self.name), style_name_to_string(self.parent), hex(id(self))) else: return "<{} @ {}>".format(style_name_to_string(self.name), hex(id(self))) def __getitem__(self, name): tname = self.name + (name,) rv = styles.get(tname, None) if rv is not None: return rv if self.parent is not None: parent = self.parent + (name,) else: parent = None rv = Style(parent, name=tname) styles[tname] = rv return rv def setattr(self, property, value): # @ReservedAssignment self.properties.append({ property : value }) def delattr(self, property): # @ReservedAssignment for d in self.properties: if property in d: del d[property] def __setattr__(self, name, value): if name not in prefixed_all_properties: raise Exception("Style property {} is not known.".format(name)) self.properties.append({ name : value }) def __delattr__(self, name): self.delattr(name) def set_parent(self, parent): self.parent = get_tuple_name(parent) def clear(self): self.properties = [ ] def take(self, other): """ Takes the style properties from `other`, which may be a style name string or a style object. """ if not isinstance(other, StyleCore): other = get_style(other) self.properties = copy_properties(other.properties) def setdefault(self, **properties): """ This sets the default value of the given properties, if no more explicit values have been set. """ for p in self.properties: for k in p: if k in properties: del properties[k] if properties: self.properties.append(properties) def add_properties(self, properties): """ Adds the properties (which must be a dict) to this style. """ self.properties.append(dict(properties)) def set_prefix(self, prefix): """ Sets the style_prefix to `prefix`. """ if prefix == self.prefix: return self.prefix = prefix if prefix == "insensitive_": self.prefix_offset = INSENSITIVE_PREFIX elif prefix == "idle_": self.prefix_offset = IDLE_PREFIX elif prefix == "hover_": self.prefix_offset = HOVER_PREFIX elif prefix == "selected_insensitive_": self.prefix_offset = SELECTED_INSENSITIVE_PREFIX elif prefix == "selected_idle_": self.prefix_offset = SELECTED_IDLE_PREFIX elif prefix == "selected_hover_": self.prefix_offset = SELECTED_HOVER_PREFIX def get_offset(self): return self.prefix_offset def get_placement(self): """ Returns a tuple giving the placement of the object. """ return ( self._get(XPOS_INDEX), self._get(YPOS_INDEX), self._get(XANCHOR_INDEX), self._get(YANCHOR_INDEX), self._get(XOFFSET_INDEX), self._get(YOFFSET_INDEX), self._get(SUBPIXEL_INDEX), ) cpdef _get(StyleCore self, int index): """ Retrieves the property at `index` from this style or its parents. """ cdef PyObject *o # The current style object we're looking at. cdef StyleCore s # The style object we'll backtrack to when s has no down-parent. cdef StyleCore left # A limit to the number of styles we'll consider. cdef int limit index += self.prefix_offset if not self.built: build_style(self) s = self left = None limit = 100 while limit > 0: # If we have the style, return it. if s.cache != NULL: o = s.cache[index] if o != NULL: return o # If there is no left-parent, and we have one, store it. if left is None and s.left_parent is not None: left = s.left_parent s = s.down_parent # If no down-parent, try left. if s is None: s = left left = None # If no down-parent or left-parent, default to None. if s is None: return None limit -= 1 raise Exception("{} is too complex. Check for loops in style inheritance.".format(self)) cpdef _get_unoffset(self, int index): return self._get(index - self.prefix_offset) def _visit_window(self, pd): """ Predicts properties for a window. `pd` The function that should be called to predict a displayable. """ for i in [ INSENSITIVE_PREFIX, IDLE_PREFIX, HOVER_PREFIX, SELECTED_INSENSITIVE_PREFIX, SELECTED_IDLE_PREFIX, SELECTED_HOVER_PREFIX ]: for j in [ BACKGROUND_INDEX, CHILD_INDEX, FOREGROUND_INDEX ]: v = self._get_unoffset(i + j) if v is not None: pd(v) def _visit_bar(self, pd): """ Predicts properties for a window. `pd` The function that should be called to predict a displayable. """ for i in [ INSENSITIVE_PREFIX, IDLE_PREFIX, HOVER_PREFIX, SELECTED_INSENSITIVE_PREFIX, SELECTED_IDLE_PREFIX, SELECTED_HOVER_PREFIX ]: for j in [ FORE_BAR_INDEX, AFT_BAR_INDEX, THUMB_INDEX, THUMB_SHADOW_INDEX ]: v = self._get_unoffset(i + j) if v is not None: pd(v) def _visit_frame(self, pd): """ Predicts properties for a Frame. `pd` The function that should be called to predict a displayable. """ for i in [ INSENSITIVE_PREFIX, IDLE_PREFIX, HOVER_PREFIX, SELECTED_INSENSITIVE_PREFIX, SELECTED_IDLE_PREFIX, SELECTED_HOVER_PREFIX ]: for j in [ CHILD_INDEX ]: v = self._get_unoffset(i + j) if v is not None: pd(v) def inspect(StyleCore self): """ Inspects this style. Returns a list of (name, properties) pairs for each style, with only properties that affect the final result being in the properties list. Properties is a map from property name to value. """ cdef StyleCore s cdef StyleCore left init_inspect() if not self.built: build_style(self) rv = [ ] # Affected properties that we've seen already. seen_properties = set() def inspect_one(s): my_properties = { } for pdict in reversed(s.properties): propnames = list(pdict) propnames.sort(key=lambda pn : priority.get(pn, 100)) propnames.reverse() for propname in propnames: prop_affects = affects.get(propname, [ ]) for a in prop_affects: if a not in seen_properties: break else: continue for a in prop_affects: seen_properties.add(a) my_properties[propname] = pdict[propname] rv.append((s.name, my_properties)) s = self left = None while True: inspect_one(s) # If there is no left-parent, and we have one, store it. if left is None and s.left_parent is not None: left = s.left_parent s = s.down_parent # If no down-parent, try left. if s is None: s = left left = None # If no down-parent or left-parent, default to None. if s is None: break return rv # This will be replaced when renpy.styledata.import_style_functions is called. Style = StyleCore from renpy.styledata.stylesets import all_properties, prefix_priority, prefix_alts, property_priority # The set of all prefixed properties we know about. prefixed_all_properties = { prefix + propname for prefix in prefix_priority for propname in all_properties } ################################################################################ # Building ################################################################################ cpdef build_style(StyleCore s): cdef int cache_priorities[PREFIX_COUNT * STYLE_PROPERTY_COUNT] cdef dict d cdef PropertyFunctionWrapper pfw if s.built: return if s.building and s.name: raise Exception("{} is part of a loop of recursive styles (is one of its own parents).".format(style_name_to_string(s.name))) s.building = True try: # Find our parents. if s.parent is not None: s.down_parent = get_full_style(s.parent) build_style(s.down_parent) if s.name is not None and len(s.name) > 1: s.left_parent = get_full_style(s.name[:-1]) build_style(s.left_parent) # Build the properties cache. if not s.properties: s.cache = NULL return memset(cache_priorities, 0, sizeof(int) * PREFIX_COUNT * STYLE_PROPERTY_COUNT) s.cache = calloc(PREFIX_COUNT * STYLE_PROPERTY_COUNT, sizeof(PyObject *)) priority = 1 for d in s.properties: for k, v in d.items(): pfw = property_functions.get(k, None) if pfw is None: continue try: pfw.function(s.cache, cache_priorities, priority, v) except: renpy.game.exception_info = "While processing the {} property of {}:".format(k, style_name_to_string(s.name)) raise priority += PRIORITY_LEVELS s.built = True finally: s.building = False cpdef unbuild_style(StyleCore s): cdef int i if not s.built: return if s.cache != NULL: for 0 <= i < PREFIX_COUNT * STYLE_PROPERTY_COUNT: Py_XDECREF(s.cache[i]) free(s.cache) s.cache = NULL s.left_parent = None s.down_parent = None s.built = False s.building = False ################################################################################ # Inspect support ################################################################################ # A map from prefixed property to priority. priority = None # A map from prefixed property to the prefixed properties it affects. affects = None def init_inspect(): global priority global affects if priority is not None: return priority = { } affects = { } for prefixname, pri in prefix_priority.items(): for propname, proplist in all_properties.items(): priority[prefixname + propname] = pri + property_priority.get(propname, 0) affects[prefixname + propname] = [ a + i for a in prefix_alts[prefixname] for i in proplist ] ################################################################################ # Other functions ################################################################################ def reset(): """ Reset the style system. """ styles.clear() def build_styles(): """ Builds or rebuilds all styles. """ for i in renpy.config.build_styles_callbacks: i() for s in styles.values(): unbuild_style(s) for s in styles.values(): build_style(s) def rebuild(prepare_screens=True): """ Rebuilds all styles. """ build_styles() renpy.display.screen.prepared = False if not renpy.game.context().init_phase: renpy.display.screen.prepare_screens() renpy.exports.restart_interaction() def copy_properties(p): """ Makes a copy of the properties dict p. """ return [ dict(i) for i in p ] def backup(): """ Returns an opaque object that backs up the current styles. """ rv = { } for k, v in styles.iteritems(): rv[k] = (v.parent, copy_properties(v.properties)) return rv def restore(o): """ Restores a style backup. """ cdef StyleCore s keys = list(styles.keys()) for i in keys: if i not in o: del styles[i] for k, v in o.iteritems(): s = get_or_create_style(k[0]) for i in k[1:]: s = s[i] parent, properties = v s.clear() s.set_parent(parent) s.properties = copy_properties(properties)