1394 lines
35 KiB
Python
1394 lines
35 KiB
Python
# Copyright 2004-2019 Tom Rothamel <pytom@bishoujo.us>
|
|
#
|
|
# 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
|
|
import renpy.display
|
|
import time
|
|
import collections
|
|
|
|
import datetime
|
|
|
|
# Profiling ####################################################################
|
|
|
|
profile_log = renpy.log.open("profile_screen", developer=True, append=False, flush=False)
|
|
|
|
# A map from screen name to ScreenProfile object.
|
|
profile = { }
|
|
|
|
|
|
class ScreenProfile(renpy.object.Object):
|
|
"""
|
|
:doc: profile_screen
|
|
:name: renpy.profile_screen
|
|
|
|
"""
|
|
|
|
def __init__(self, name, predict=False, show=False, update=False, request=False, time=False, debug=False, const=False):
|
|
"""
|
|
Requests screen profiling for the screen named `name`, which
|
|
must be a string.
|
|
|
|
Apart from `name`, all arguments must be supplied as keyword
|
|
arguments. This function takes three groups of arguments.
|
|
|
|
|
|
The first group of arguments determines when profiling occurs.
|
|
|
|
`predict`
|
|
If true, profiling occurs when the screen is being predicted.
|
|
|
|
`show`
|
|
If true, profiling occurs when the screen is first shown.
|
|
|
|
`update`
|
|
If true, profiling occurs when the screen is updated.
|
|
|
|
`request`
|
|
If true, profiling occurs when requested by pressing F8.
|
|
|
|
The second group of arguments controls what profiling output is
|
|
produced when profiling occurs.
|
|
|
|
`time`
|
|
If true, Ren'Py will log the amount of time it takes to evaluate
|
|
the screen.
|
|
|
|
`debug`
|
|
If true, Ren'Py will log information as to how screens are
|
|
evaluated, including:
|
|
|
|
* Which displayables Ren'Py considers constant.
|
|
* Which arguments, if any, needed to be evaluated.
|
|
* Which displayables were reused.
|
|
|
|
Producing and saving this debug information takes a noticeable
|
|
amount of time, and so the `time` output should not be considered
|
|
reliable if `debug` is set.
|
|
|
|
The last group of arguments controls what output is produced once
|
|
per Ren'Py run.
|
|
|
|
`const`
|
|
Displays the variables in the screen that are marked as const and
|
|
not-const.
|
|
|
|
All profiling output will be logged to profile_screen.txt in the game
|
|
directory.
|
|
"""
|
|
|
|
self.predict = predict
|
|
self.show = show
|
|
self.update = update
|
|
self.request = request
|
|
|
|
self.time = time
|
|
self.debug = debug
|
|
|
|
self.const = const
|
|
|
|
if name is not None:
|
|
if isinstance(name, basestring):
|
|
name = tuple(name.split())
|
|
profile[name] = self
|
|
|
|
|
|
def get_profile(name):
|
|
"""
|
|
Returns the profile object for the screen with `name`, or a default
|
|
profile object if none exists.
|
|
|
|
`name`
|
|
A string or tuple.
|
|
"""
|
|
|
|
if isinstance(name, basestring):
|
|
name = tuple(name.split())
|
|
|
|
if name in profile:
|
|
return profile[name]
|
|
else:
|
|
return ScreenProfile(None)
|
|
|
|
# Cache ########################################################################
|
|
|
|
|
|
# A map from screen name to a list of ScreenCache objects. We ensure the cache
|
|
# does not exceed config.screen_cache_size for each screen.
|
|
predict_cache = collections.defaultdict(list)
|
|
|
|
|
|
class ScreenCache(object):
|
|
"""
|
|
Represents an entry in the screen cache. Upon creation, puts itself into
|
|
the screen cache.
|
|
"""
|
|
|
|
def __init__(self, screen, args, kwargs, cache):
|
|
|
|
if screen.ast is None:
|
|
return
|
|
|
|
self.args = args
|
|
self.kwargs = kwargs
|
|
self.cache = cache
|
|
|
|
pc = predict_cache[screen]
|
|
|
|
pc.append(self)
|
|
|
|
if len(pc) > renpy.config.screen_cache_size:
|
|
pc.pop(0)
|
|
|
|
|
|
cache_put = ScreenCache
|
|
|
|
|
|
def cache_get(screen, args, kwargs):
|
|
"""
|
|
Returns the cache to use when `screen` is accessed with `args` and
|
|
`kwargs`.
|
|
"""
|
|
|
|
if screen.ast is None:
|
|
return { }
|
|
|
|
pc = predict_cache[screen]
|
|
|
|
if not pc:
|
|
return { }
|
|
|
|
for sc in pc:
|
|
|
|
# Reuse w/ same arguments.
|
|
if sc.args == args and sc.args == kwargs:
|
|
pc.remove(sc)
|
|
break
|
|
else:
|
|
|
|
# Reuse the oldest.
|
|
sc = pc.pop(0)
|
|
|
|
return sc.cache
|
|
|
|
|
|
# Screens #####################################################################
|
|
|
|
class Screen(renpy.object.Object):
|
|
"""
|
|
A screen is a collection of widgets that are displayed together.
|
|
This class stores information about the screen.
|
|
"""
|
|
|
|
sensitive = "True"
|
|
|
|
def __init__(self,
|
|
name,
|
|
function,
|
|
modal="False",
|
|
zorder="0",
|
|
tag=None,
|
|
predict=None,
|
|
variant=None,
|
|
parameters=False,
|
|
location=None,
|
|
layer="screens",
|
|
sensitive="True"):
|
|
|
|
# The name of this screen.
|
|
if isinstance(name, basestring):
|
|
name = tuple(name.split())
|
|
|
|
self.name = name
|
|
|
|
if (variant is None) or isinstance(variant, basestring):
|
|
variant = [ variant ]
|
|
|
|
for v in variant:
|
|
screens[name[0], v] = self
|
|
screens_by_name[name[0]][v] = self
|
|
|
|
# A function that can be called to display the screen.
|
|
self.function = function
|
|
|
|
# If this is a SL2 screen, the SLScreen node at the root of this
|
|
# screen.
|
|
if isinstance(function, renpy.sl2.slast.SLScreen): # @UndefinedVariable
|
|
self.ast = function
|
|
else:
|
|
self.ast = None
|
|
|
|
# Expression: Are we modal? (A modal screen ignores screens under it.)
|
|
self.modal = modal
|
|
|
|
# Expression: Our zorder.
|
|
self.zorder = zorder
|
|
|
|
# The tag associated with the screen.
|
|
self.tag = tag or name[0]
|
|
|
|
# Can this screen be predicted?
|
|
if predict is None:
|
|
predict = renpy.config.predict_screens
|
|
|
|
self.predict = predict
|
|
|
|
# True if this screen takes parameters via _args and _kwargs.
|
|
self.parameters = parameters
|
|
|
|
# The location (filename, linenumber) of this screen.
|
|
self.location = location
|
|
|
|
# The layer the screen will be shown on.
|
|
self.layer = layer
|
|
|
|
# Is this screen sensitive? An expression.
|
|
self.sensitive = sensitive
|
|
|
|
global prepared
|
|
global analyzed
|
|
|
|
prepared = False
|
|
analyzed = False
|
|
|
|
|
|
# Phases we can be in.
|
|
PREDICT = 0 # Predicting the screen before it is shown.
|
|
SHOW = 1 # Showing the screen for the first time.
|
|
UPDATE = 2 # Showing the screen for the second and later times.
|
|
HIDE = 3 # After the screen has been hid with "hide screen" (or the end of call screen).
|
|
OLD = 4 # A copy of the screen in the old side of a transition.
|
|
|
|
phase_name = [
|
|
"PREDICT",
|
|
"SHOW",
|
|
"UPDATE",
|
|
"HIDE",
|
|
"OLD",
|
|
]
|
|
|
|
|
|
class ScreenDisplayable(renpy.display.layout.Container):
|
|
"""
|
|
A screen is a collection of widgets that are displayed together. This
|
|
class is responsible for managing the display of a screen.
|
|
"""
|
|
|
|
nosave = [
|
|
'screen',
|
|
'child',
|
|
'children',
|
|
'transforms',
|
|
'widgets',
|
|
'old_widgets',
|
|
'hidden_widgets',
|
|
'old_transforms',
|
|
'cache',
|
|
'miss_cache',
|
|
'profile',
|
|
'phase',
|
|
'use_cache' ]
|
|
|
|
restarting = False
|
|
hiding = False
|
|
transient = False
|
|
|
|
def after_setstate(self):
|
|
self.screen = get_screen_variant(self.screen_name[0])
|
|
self.child = None
|
|
self.children = [ ]
|
|
self.transforms = { }
|
|
self.widgets = { }
|
|
self.old_widgets = None
|
|
self.old_transforms = None
|
|
self.hidden_widgets = { }
|
|
self.cache = { }
|
|
self.phase = UPDATE
|
|
self.use_cache = { }
|
|
self.miss_cache = { }
|
|
|
|
self.profile = profile.get(self.screen_name, None)
|
|
|
|
def __init__(self, screen, tag, layer, widget_properties={}, scope={}, transient=False, **properties):
|
|
|
|
super(ScreenDisplayable, self).__init__(**properties)
|
|
|
|
# Stash the properties, so we can re-create the screen.
|
|
self.properties = properties
|
|
|
|
# The screen, and it's name. (The name is used to look up the
|
|
# screen on save.)
|
|
self.screen = screen
|
|
self.screen_name = screen.name
|
|
|
|
self._location = self.screen.location
|
|
|
|
# The profile object that determines when we profile.
|
|
self.profile = profile.get(self.screen_name, None)
|
|
|
|
# The tag and layer screen was displayed with.
|
|
self.tag = tag
|
|
self.layer = layer
|
|
|
|
# The scope associated with this statement. This is passed in
|
|
# as keyword arguments to the displayable.
|
|
self.scope = renpy.python.RevertableDict(scope)
|
|
|
|
# The child associated with this screen.
|
|
self.child = None
|
|
|
|
# Widget properties given to this screen the last time it was
|
|
# shown.
|
|
self.widget_properties = widget_properties
|
|
|
|
# A map from name to the widget with that name.
|
|
self.widgets = { }
|
|
|
|
# The persistent cache.
|
|
self.cache = { }
|
|
|
|
if tag and layer:
|
|
old_screen = get_screen(tag, layer)
|
|
else:
|
|
old_screen = None
|
|
|
|
# A map from name to the transform with that name. (This is
|
|
# taken from the old version of the screen, if it exists.
|
|
if old_screen is not None:
|
|
self.transforms = old_screen.transforms
|
|
else:
|
|
self.transforms = { }
|
|
|
|
# A map from a (screen name, id) pair to cache. This is for use
|
|
# statements with the id parameter.
|
|
if old_screen is not None:
|
|
self.use_cache = old_screen.use_cache
|
|
else:
|
|
self.use_cache = { }
|
|
|
|
# A version of the cache that's used when we have a screen that is
|
|
# being displayed with the same tag with a cached copy of the screen
|
|
# we want to display.
|
|
self.miss_cache = { }
|
|
|
|
# What widgets and transforms were the last time this screen was
|
|
# updated. Used to communicate with the ui module, and only
|
|
# valid during an update - not used at other times.
|
|
self.old_widgets = None
|
|
self.old_transforms = None
|
|
|
|
# Should we transfer data from the old_screen? This becomes
|
|
# true once this screen finishes updating for the first time,
|
|
# and also while we're using something.
|
|
self.old_transfers = (old_screen and old_screen.screen_name == self.screen_name)
|
|
|
|
# The current transform event, and the last transform event to
|
|
# be processed.
|
|
self.current_transform_event = None
|
|
|
|
# A dict-set of widgets (by id) that have been hidden from us.
|
|
self.hidden_widgets = { }
|
|
|
|
# Are we restarting or hiding?
|
|
self.restarting = False
|
|
self.hiding = False
|
|
|
|
# Is this a transient screen?
|
|
self.transient = transient
|
|
|
|
# Modal and zorder.
|
|
self.modal = renpy.python.py_eval(self.screen.modal, locals=self.scope)
|
|
self.zorder = renpy.python.py_eval(self.screen.zorder, locals=self.scope)
|
|
|
|
# The lifecycle phase we are in - one of PREDICT, SHOW, UPDATE, or HIDE.
|
|
self.phase = PREDICT
|
|
|
|
def __unicode__(self):
|
|
return "Screen {}".format(" ".join(self.screen_name))
|
|
|
|
def visit(self):
|
|
return [ self.child ]
|
|
|
|
def visit_all(self, callback, seen=None):
|
|
callback(self)
|
|
|
|
try:
|
|
push_current_screen(self)
|
|
self.child.visit_all(callback, seen=None)
|
|
finally:
|
|
pop_current_screen()
|
|
|
|
def per_interact(self):
|
|
renpy.display.render.redraw(self, 0)
|
|
self.update()
|
|
|
|
def set_transform_event(self, event):
|
|
super(ScreenDisplayable, self).set_transform_event(event)
|
|
self.current_transform_event = event
|
|
|
|
def find_focusable(self, callback, focus_name):
|
|
|
|
hiding = (self.phase == OLD) or (self.phase == HIDE)
|
|
|
|
try:
|
|
push_current_screen(self)
|
|
|
|
if self.child and not hiding:
|
|
self.child.find_focusable(callback, focus_name)
|
|
finally:
|
|
pop_current_screen()
|
|
|
|
def copy(self):
|
|
rv = ScreenDisplayable(self.screen, self.tag, self.layer, self.widget_properties, self.scope, **self.properties)
|
|
rv.transforms = self.transforms.copy()
|
|
rv.widgets = self.widgets.copy()
|
|
rv.old_transfers = True
|
|
rv.child = self.child
|
|
|
|
return rv
|
|
|
|
def _handles_event(self, event):
|
|
if self.child is None:
|
|
|
|
if self.transient:
|
|
return False
|
|
|
|
self.update()
|
|
|
|
return self.child._handles_event(event)
|
|
|
|
def _hide(self, st, at, kind):
|
|
|
|
if self.phase == HIDE:
|
|
hid = self
|
|
else:
|
|
|
|
if (self.child is not None) and (not self.child._handles_event(kind)):
|
|
return None
|
|
|
|
updated_screens.discard(self)
|
|
self.update()
|
|
|
|
if self.screen is None:
|
|
return None
|
|
|
|
if self.child is None:
|
|
return None
|
|
|
|
if not self.child._handles_event(kind):
|
|
return None
|
|
|
|
if self.screen.ast is not None:
|
|
self.screen.ast.copy_on_change(self.cache.get(0, {}))
|
|
|
|
hid = self.copy()
|
|
|
|
for i in self.child.children:
|
|
i.set_transform_event(kind)
|
|
|
|
hid.phase = HIDE
|
|
|
|
rv = None
|
|
|
|
old_child = hid.child
|
|
|
|
if not isinstance(old_child, renpy.display.layout.MultiBox):
|
|
return None
|
|
|
|
renpy.ui.detached()
|
|
hid.child = renpy.ui.default_fixed(focus="_screen_" + "_".join(self.screen_name))
|
|
hid.children = [ hid.child ]
|
|
renpy.ui.close()
|
|
|
|
for d in old_child.children:
|
|
c = d._hide(st, at, kind)
|
|
|
|
if c is not None:
|
|
renpy.display.render.redraw(c, 0)
|
|
hid.child.add(c)
|
|
|
|
rv = hid
|
|
|
|
if hid is not None:
|
|
renpy.display.render.redraw(hid, 0)
|
|
|
|
return rv
|
|
|
|
def _in_current_store(self):
|
|
|
|
if self.screen is None:
|
|
return self
|
|
|
|
if self.child is None:
|
|
return self
|
|
|
|
if not renpy.config.transition_screens:
|
|
return self
|
|
|
|
if self.screen.ast is not None:
|
|
self.screen.ast.copy_on_change(self.cache.get(0, {}))
|
|
|
|
rv = self.copy()
|
|
rv.phase = OLD
|
|
rv.child = self.child._in_current_store()
|
|
|
|
return rv
|
|
|
|
def update(self):
|
|
|
|
if self in updated_screens:
|
|
return
|
|
|
|
updated_screens.add(self)
|
|
|
|
if self.screen is None:
|
|
self.child = renpy.display.layout.Null()
|
|
return { }
|
|
|
|
# Do not update if restarting or hiding.
|
|
if self.restarting or (self.phase == HIDE) or (self.phase == OLD):
|
|
if not self.child:
|
|
self.child = renpy.display.layout.Null()
|
|
|
|
return self.widgets
|
|
|
|
profile = False
|
|
debug = False
|
|
|
|
if self.profile:
|
|
|
|
if self.phase == UPDATE and self.profile.update:
|
|
profile = True
|
|
elif self.phase == SHOW and self.profile.show:
|
|
profile = True
|
|
elif self.phase == PREDICT and self.profile.predict:
|
|
profile = True
|
|
|
|
if renpy.display.interface.profile_once and self.profile.request:
|
|
profile = True
|
|
|
|
if profile:
|
|
profile_log.write("%s %s %s",
|
|
phase_name[self.phase],
|
|
" ".join(self.screen_name),
|
|
datetime.datetime.now().strftime("%H:%M:%S.%f"))
|
|
|
|
start = time.time()
|
|
|
|
if self.profile.debug:
|
|
debug = True
|
|
|
|
# Cycle widgets and transforms.
|
|
self.old_widgets = self.widgets
|
|
self.old_transforms = self.transforms
|
|
self.widgets = { }
|
|
self.transforms = { }
|
|
|
|
push_current_screen(self)
|
|
|
|
old_ui_screen = renpy.ui.screen
|
|
renpy.ui.screen = self
|
|
|
|
# The name of the root screen of this screen.
|
|
NAME = 0
|
|
|
|
old_cache = self.cache.get(NAME, None)
|
|
|
|
# Evaluate the screen.
|
|
try:
|
|
|
|
renpy.ui.detached()
|
|
self.child = renpy.ui.default_fixed(focus="_screen_" + "_".join(self.screen_name))
|
|
self.children = [ self.child ]
|
|
|
|
self.scope["_scope"] = self.scope
|
|
self.scope["_name"] = NAME
|
|
self.scope["_debug"] = debug
|
|
|
|
self.screen.function(**self.scope)
|
|
|
|
renpy.ui.close()
|
|
|
|
finally:
|
|
del self.scope["_scope"]
|
|
|
|
renpy.ui.screen = old_ui_screen
|
|
pop_current_screen()
|
|
|
|
# Finish up.
|
|
self.old_widgets = None
|
|
self.old_transforms = None
|
|
self.old_transfers = True
|
|
|
|
if self.miss_cache:
|
|
self.miss_cache.clear()
|
|
|
|
# Deal with the case where the screen version changes.
|
|
if (self.cache.get(NAME, None) is not old_cache) and (self.current_transform_event is None) and (self.phase == UPDATE):
|
|
self.current_transform_event = "update"
|
|
|
|
if self.current_transform_event:
|
|
|
|
for i in self.child.children:
|
|
i.set_transform_event(self.current_transform_event)
|
|
|
|
self.current_transform_event = None
|
|
|
|
if profile:
|
|
end = time.time()
|
|
|
|
if self.profile.time:
|
|
profile_log.write("* %.2f ms", 1000 * (end - start))
|
|
|
|
if self.profile.debug:
|
|
profile_log.write("\n")
|
|
|
|
return self.widgets
|
|
|
|
def render(self, w, h, st, at):
|
|
|
|
if not self.child:
|
|
self.update()
|
|
|
|
if self.phase == SHOW:
|
|
self.phase = UPDATE
|
|
|
|
try:
|
|
push_current_screen(self)
|
|
child = renpy.display.render.render(self.child, w, h, st, at)
|
|
finally:
|
|
pop_current_screen()
|
|
|
|
rv = renpy.display.render.Render(w, h)
|
|
rv.focus_screen = self
|
|
|
|
hiding = (self.phase == OLD) or (self.phase == HIDE)
|
|
|
|
if self.screen is None:
|
|
sensitive = False
|
|
else:
|
|
sensitive = renpy.python.py_eval(self.screen.sensitive, locals=self.scope)
|
|
|
|
rv.blit(child, (0, 0), focus=sensitive and not hiding, main=not hiding)
|
|
rv.modal = self.modal and not hiding
|
|
|
|
return rv
|
|
|
|
def get_placement(self):
|
|
if not self.child:
|
|
self.update()
|
|
|
|
return self.child.get_placement()
|
|
|
|
def event(self, ev, x, y, st):
|
|
|
|
if (self.phase == OLD) or (self.phase == HIDE):
|
|
return
|
|
|
|
if not self.screen:
|
|
return None
|
|
|
|
if not renpy.python.py_eval(self.screen.sensitive, locals=self.scope):
|
|
ev = renpy.display.interface.time_event
|
|
|
|
try:
|
|
push_current_screen(self)
|
|
|
|
rv = self.child.event(ev, x, y, st)
|
|
finally:
|
|
pop_current_screen()
|
|
|
|
if rv is not None:
|
|
return rv
|
|
|
|
if self.modal:
|
|
raise renpy.display.layout.IgnoreLayers()
|
|
|
|
def get_phase_name(self):
|
|
return phase_name[self.phase]
|
|
|
|
|
|
# The name of the screen that is currently being displayed, or
|
|
# None if no screen is being currently displayed.
|
|
_current_screen = None
|
|
|
|
# The stack of old current screens.
|
|
current_screen_stack = [ ]
|
|
|
|
|
|
def push_current_screen(screen):
|
|
global _current_screen
|
|
current_screen_stack.append(_current_screen)
|
|
_current_screen = screen
|
|
|
|
|
|
def pop_current_screen():
|
|
global _current_screen
|
|
_current_screen = current_screen_stack.pop()
|
|
|
|
|
|
# A map from (screen_name, variant) tuples to screen.
|
|
screens = { }
|
|
|
|
# A map from screen name to map from variant to screen.
|
|
screens_by_name = collections.defaultdict(dict)
|
|
|
|
# The screens that were updated during the current interaction.
|
|
updated_screens = set()
|
|
|
|
|
|
def get_screen_variant(name, candidates=None):
|
|
"""
|
|
Get a variant screen object for `name`.
|
|
|
|
`candidates`
|
|
A list of candidate variants.
|
|
"""
|
|
|
|
if candidates is None:
|
|
candidates = renpy.config.variants
|
|
|
|
for i in candidates:
|
|
rv = screens.get((name, i), None)
|
|
if rv is not None:
|
|
return rv
|
|
|
|
return None
|
|
|
|
|
|
def get_all_screen_variants(name):
|
|
"""
|
|
Gets all variants of the screen with `name`.
|
|
|
|
Returns a list of (`variant`, `screen`) tuples, in no particular
|
|
order.
|
|
"""
|
|
|
|
rv = [ ]
|
|
|
|
for k, v in screens.iteritems():
|
|
if k[0] == name:
|
|
rv.append((k[1], v))
|
|
|
|
return rv
|
|
|
|
|
|
# Have all screens been analyzed?
|
|
analyzed = False
|
|
|
|
# Have the screens been prepared?
|
|
prepared = False
|
|
|
|
# Caches for sort_screens.
|
|
sorted_screens = [ ]
|
|
screens_at_sort = { }
|
|
|
|
# The list of screens that participate in a use cycle.
|
|
use_cycle = [ ]
|
|
|
|
|
|
def sort_screens():
|
|
"""
|
|
Produces a list of SL2 screens in topologically sorted order.
|
|
"""
|
|
|
|
global use_cycle
|
|
global sorted_screens
|
|
global screens_at_sort
|
|
|
|
if screens_at_sort == screens:
|
|
return sorted_screens
|
|
|
|
# For each screen, the set of screens it uses.
|
|
depends = collections.defaultdict(set)
|
|
|
|
# For each screen, the set of screens that use it.
|
|
reverse = collections.defaultdict(set)
|
|
|
|
names = { i[0] for i in screens }
|
|
|
|
for k, v in screens.items():
|
|
|
|
name = k[0]
|
|
|
|
# Ensure name exists.
|
|
depends[name]
|
|
|
|
if not v.ast:
|
|
continue
|
|
|
|
def callback(uses):
|
|
|
|
if uses not in names:
|
|
return
|
|
|
|
depends[name].add(uses)
|
|
reverse[uses].add(name)
|
|
|
|
v.ast.used_screens(callback)
|
|
|
|
rv = [ ]
|
|
|
|
workset = { k for k, v in depends.items() if not len(v) }
|
|
|
|
while workset:
|
|
name = workset.pop()
|
|
rv.append(name)
|
|
|
|
for i in reverse[name]:
|
|
d = depends[i]
|
|
d.remove(name)
|
|
|
|
if not d:
|
|
workset.add(i)
|
|
|
|
del reverse[name]
|
|
|
|
# Store the use cycle for later reporting.
|
|
use_cycle = reverse.keys()
|
|
use_cycle.sort()
|
|
|
|
sorted_screens = rv
|
|
screens_at_sort = dict(screens)
|
|
|
|
return rv
|
|
|
|
|
|
def sorted_variants():
|
|
"""
|
|
Produces a list of screen variants in topological order.
|
|
"""
|
|
|
|
rv = [ ]
|
|
|
|
for name in sort_screens():
|
|
rv.extend(screens_by_name[name].values())
|
|
|
|
return rv
|
|
|
|
|
|
def analyze_screens():
|
|
"""
|
|
Analyzes all screens.
|
|
"""
|
|
|
|
global analyzed
|
|
|
|
if analyzed:
|
|
return
|
|
|
|
for s in sorted_variants():
|
|
if s.ast is None:
|
|
continue
|
|
|
|
s.ast.analyze_screen()
|
|
|
|
analyzed = True
|
|
|
|
|
|
def prepare_screens():
|
|
"""
|
|
Prepares all screens for use.
|
|
"""
|
|
|
|
global prepared
|
|
|
|
if prepared:
|
|
return
|
|
|
|
predict_cache.clear()
|
|
|
|
old_predicting = renpy.display.predict.predicting
|
|
renpy.display.predict.predicting = True
|
|
|
|
try:
|
|
|
|
if not analyzed:
|
|
analyze_screens()
|
|
|
|
for s in sorted_variants():
|
|
if s.ast is None:
|
|
continue
|
|
|
|
s.ast.unprepare_screen()
|
|
s.ast.prepare_screen()
|
|
|
|
prepared = True
|
|
|
|
finally:
|
|
renpy.display.predict.predicting = old_predicting
|
|
|
|
if renpy.config.developer and use_cycle:
|
|
raise Exception("The following screens use each other in a loop: " + ", ".join(use_cycle) +". This is not allowed.")
|
|
|
|
|
|
def define_screen(*args, **kwargs):
|
|
"""
|
|
:doc: screens
|
|
:args: (name, function, modal="False", zorder="0", tag=None, variant=None)
|
|
|
|
Defines a screen with `name`, which should be a string.
|
|
|
|
`function`
|
|
The function that is called to display the screen. The
|
|
function is called with the screen scope as keyword
|
|
arguments. It should ignore additional keyword arguments.
|
|
|
|
The function should call the ui functions to add things to the
|
|
screen.
|
|
|
|
`modal`
|
|
A string that, when evaluated, determines of the created
|
|
screen should be modal. A modal screen prevents screens
|
|
underneath it from receiving input events.
|
|
|
|
`zorder`
|
|
A string that, when evaluated, should be an integer. The integer
|
|
controls the order in which screens are displayed. A screen
|
|
with a greater zorder number is displayed above screens with a
|
|
lesser zorder number.
|
|
|
|
`tag`
|
|
The tag associated with this screen. When the screen is shown,
|
|
it replaces any other screen with the same tag. The tag
|
|
defaults to the name of the screen.
|
|
|
|
`predict`
|
|
If true, this screen can be loaded for image prediction. If false,
|
|
it can't. Defaults to true.
|
|
|
|
`variant`
|
|
String. Gives the variant of the screen to use.
|
|
|
|
"""
|
|
|
|
Screen(*args, **kwargs)
|
|
|
|
|
|
def get_screen_layer(name):
|
|
"""
|
|
Returns the layer that the screen with `name` is part of.
|
|
"""
|
|
|
|
if not isinstance(name, basestring):
|
|
name = name[0]
|
|
|
|
screen = get_screen_variant(name)
|
|
|
|
if screen is None:
|
|
return "screens"
|
|
else:
|
|
return screen.layer
|
|
|
|
|
|
def get_screen(name, layer=None):
|
|
"""
|
|
:doc: screens
|
|
|
|
Returns the ScreenDisplayable with the given `name` on layer. `name`
|
|
is first interpreted as a tag name, and then a screen name. If the
|
|
screen is not showing, returns None.
|
|
|
|
This can also take a list of names, in which case the first screen
|
|
that is showing is returned.
|
|
|
|
This function can be used to check if a screen is showing::
|
|
|
|
if renpy.get_screen("say"):
|
|
text "The say screen is showing."
|
|
else:
|
|
text "The say screen is hidden."
|
|
|
|
"""
|
|
|
|
if layer is None:
|
|
layer = get_screen_layer(name)
|
|
|
|
if isinstance(name, basestring):
|
|
name = (name, )
|
|
|
|
sl = renpy.exports.scene_lists()
|
|
|
|
for tag in name:
|
|
|
|
sd = sl.get_displayable_by_tag(layer, tag)
|
|
if sd is not None:
|
|
return sd
|
|
|
|
for tag in name:
|
|
|
|
sd = sl.get_displayable_by_name(layer, (tag, ))
|
|
if sd is not None:
|
|
return sd
|
|
|
|
return None
|
|
|
|
|
|
def has_screen(name):
|
|
"""
|
|
Returns true if a screen with the given name exists.
|
|
"""
|
|
|
|
if not isinstance(name, tuple):
|
|
name = tuple(name.split())
|
|
|
|
if not name:
|
|
return False
|
|
|
|
if get_screen_variant(name[0]):
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
def show_screen(_screen_name, *_args, **kwargs):
|
|
"""
|
|
:doc: screens
|
|
|
|
The programmatic equivalent of the show screen statement.
|
|
|
|
Shows the named screen. This takes the following keyword arguments:
|
|
|
|
`_screen_name`
|
|
The name of the screen to show.
|
|
`_layer`
|
|
The layer to show the screen on.
|
|
`_zorder`
|
|
The zorder to show the screen on. If not specified, defaults to
|
|
the zorder associated with the screen. It that's not specified,
|
|
it is 0 by default.
|
|
`_tag`
|
|
The tag to show the screen with. If not specified, defaults to
|
|
the tag associated with the screen. It that's not specified,
|
|
defaults to the name of the screen.
|
|
`_widget_properties`
|
|
A map from the id of a widget to a property name -> property
|
|
value map. When a widget with that id is shown by the screen,
|
|
the specified properties are added to it.
|
|
`_transient`
|
|
If true, the screen will be automatically hidden at the end of
|
|
the current interaction.
|
|
|
|
Non-keyword arguments, and keyword arguments that do not begin with
|
|
an underscore, are passed to the screen.
|
|
"""
|
|
|
|
_layer = kwargs.pop("_layer", None)
|
|
_tag = kwargs.pop("_tag", None)
|
|
_widget_properties = kwargs.pop("_widget_properties", {})
|
|
_transient = kwargs.pop("_transient", False)
|
|
_zorder = kwargs.pop("_zorder", None)
|
|
|
|
name = _screen_name
|
|
|
|
if not isinstance(name, tuple):
|
|
name = tuple(name.split())
|
|
|
|
screen = get_screen_variant(name[0])
|
|
|
|
if screen is None:
|
|
raise Exception("Screen %s is not known.\n" % (name[0],))
|
|
|
|
if _layer is None:
|
|
_layer = get_screen_layer(name)
|
|
|
|
if _tag is None:
|
|
_tag = screen.tag
|
|
|
|
scope = { }
|
|
|
|
if screen.parameters:
|
|
scope["_kwargs" ] = kwargs
|
|
scope["_args"] = _args
|
|
else:
|
|
scope.update(kwargs)
|
|
|
|
d = ScreenDisplayable(screen, _tag, _layer, _widget_properties, scope, transient=_transient)
|
|
|
|
if _zorder is None:
|
|
_zorder = d.zorder
|
|
|
|
old_d = get_screen(_tag, _layer)
|
|
|
|
if old_d and old_d.cache:
|
|
d.cache = old_d.cache
|
|
d.miss_cache = cache_get(screen, _args, kwargs)
|
|
d.phase = UPDATE
|
|
else:
|
|
d.cache = cache_get(screen, _args, kwargs)
|
|
d.phase = SHOW
|
|
|
|
sls = renpy.display.core.scene_lists()
|
|
|
|
sls.add(_layer, d, _tag, zorder=_zorder, transient=_transient, keep_st=True, name=name)
|
|
|
|
|
|
def predict_screen(_screen_name, *_args, **kwargs):
|
|
"""
|
|
Predicts the displayables that make up the given screen.
|
|
|
|
`_screen_name`
|
|
The name of the screen to show.
|
|
`_widget_properties`
|
|
A map from the id of a widget to a property name -> property
|
|
value map. When a widget with that id is shown by the screen,
|
|
the specified properties are added to it.
|
|
|
|
Keyword arguments not beginning with underscore (_) are used to
|
|
initialize the screen's scope.
|
|
"""
|
|
|
|
_layer = kwargs.pop("_layer", None)
|
|
_tag = kwargs.pop("_tag", None)
|
|
_widget_properties = kwargs.pop("_widget_properties", {})
|
|
_transient = kwargs.pop("_transient", False)
|
|
|
|
name = _screen_name
|
|
|
|
if renpy.config.debug_image_cache:
|
|
renpy.display.ic_log.write("Predict screen %s", name)
|
|
|
|
if not isinstance(name, tuple):
|
|
name = tuple(name.split())
|
|
|
|
screen = get_screen_variant(name[0])
|
|
|
|
if screen is None:
|
|
return
|
|
|
|
if _layer is None:
|
|
_layer = get_screen_layer(name)
|
|
|
|
scope = { }
|
|
scope["_scope"] = scope
|
|
|
|
if screen.parameters:
|
|
scope["_kwargs" ] = kwargs
|
|
scope["_args"] = _args
|
|
else:
|
|
scope.update(kwargs)
|
|
|
|
try:
|
|
|
|
if screen is None:
|
|
raise Exception("Screen %s is not known.\n" % (name[0],))
|
|
|
|
if not screen.predict:
|
|
return
|
|
|
|
d = ScreenDisplayable(screen, None, None, _widget_properties, scope)
|
|
d.cache = cache_get(screen, _args, kwargs)
|
|
d.update()
|
|
cache_put(screen, _args, kwargs, d.cache)
|
|
|
|
renpy.display.predict.displayable(d)
|
|
|
|
except:
|
|
if renpy.config.debug_image_cache:
|
|
import traceback
|
|
|
|
print("While predicting screen", _screen_name)
|
|
traceback.print_exc()
|
|
|
|
finally:
|
|
del scope["_scope"]
|
|
|
|
renpy.ui.reset()
|
|
|
|
|
|
def hide_screen(tag, layer=None):
|
|
"""
|
|
:doc: screens
|
|
|
|
The programmatic equivalent of the hide screen statement.
|
|
|
|
Hides the screen with `tag` on `layer`.
|
|
"""
|
|
|
|
if layer is None:
|
|
layer = get_screen_layer((tag,))
|
|
|
|
screen = get_screen(tag, layer)
|
|
|
|
if screen is not None:
|
|
renpy.exports.hide(screen.tag, layer=layer)
|
|
|
|
|
|
def use_screen(_screen_name, *_args, **kwargs):
|
|
|
|
_name = kwargs.pop("_name", ())
|
|
_scope = kwargs.pop("_scope", { })
|
|
|
|
name = _screen_name
|
|
|
|
if not isinstance(name, tuple):
|
|
name = tuple(name.split())
|
|
|
|
screen = get_screen_variant(name[0])
|
|
|
|
if screen is None:
|
|
raise Exception("Screen %r is not known." % (name,))
|
|
|
|
old_transfers = _current_screen.old_transfers
|
|
_current_screen.old_transfers = True
|
|
|
|
if screen.parameters:
|
|
scope = { }
|
|
scope["_kwargs"] = kwargs
|
|
scope["_args"] = _args
|
|
else:
|
|
scope = _scope.copy()
|
|
scope.update(kwargs)
|
|
|
|
scope["_scope"] = scope
|
|
scope["_name"] = (_name, name)
|
|
|
|
try:
|
|
screen.function(**scope)
|
|
finally:
|
|
del scope["_scope"]
|
|
|
|
_current_screen.old_transfers = old_transfers
|
|
|
|
|
|
def current_screen():
|
|
return _current_screen
|
|
|
|
|
|
def get_widget(screen, id, layer=None): # @ReservedAssignment
|
|
"""
|
|
:doc: screens
|
|
|
|
From the `screen` on `layer`, returns the widget with
|
|
`id`. Returns None if the screen doesn't exist, or there is no
|
|
widget with that id on the screen.
|
|
"""
|
|
|
|
if isinstance(screen, ScreenDisplayable):
|
|
screen = screen.screen_name
|
|
|
|
if screen is None:
|
|
screen = current_screen()
|
|
else:
|
|
if layer is None:
|
|
layer = get_screen_layer(screen)
|
|
|
|
screen = get_screen(screen, layer)
|
|
|
|
if not isinstance(screen, ScreenDisplayable):
|
|
return None
|
|
|
|
if screen.child is None:
|
|
screen.update()
|
|
|
|
rv = screen.widgets.get(id, None)
|
|
return rv
|
|
|
|
|
|
def get_widget_properties(id, screen=None, layer=None): # @ReservedAssignment
|
|
"""
|
|
:doc: screens
|
|
|
|
Returns the properties for the widget with `id` in the `screen`
|
|
on `layer`. If `screen` is None, returns the properties for the
|
|
current screen. This can be used from Python or property code inside
|
|
a screen.
|
|
|
|
Note that this returns a dictionary containing the widget properties,
|
|
and so to get an individual property, the dictionary must be accessed.
|
|
"""
|
|
|
|
if screen is None:
|
|
s = current_screen()
|
|
else:
|
|
if layer is None:
|
|
layer = get_screen_layer(screen)
|
|
|
|
s = get_screen(screen, layer)
|
|
|
|
if s is None:
|
|
return { }
|
|
|
|
rv = s.widget_properties.get(id, None)
|
|
|
|
if rv is None:
|
|
rv = { }
|
|
|
|
return rv
|
|
|
|
|
|
def before_restart():
|
|
"""
|
|
This is called before Ren'Py restarts to put the screens into restart
|
|
mode, which prevents crashes due to variables being used that are no
|
|
longer defined.
|
|
"""
|
|
|
|
for k, layer in renpy.display.interface.old_scene.iteritems():
|
|
if k is None:
|
|
continue
|
|
|
|
for i in layer.children:
|
|
if isinstance(i, ScreenDisplayable):
|
|
i.restarting = True
|
|
|
|
|
|
def show_overlay_screens(suppress_overlay):
|
|
"""
|
|
Called from interact to show or hide the overlay screens.
|
|
"""
|
|
|
|
show = not suppress_overlay
|
|
|
|
if renpy.store._overlay_screens is None:
|
|
show = show
|
|
elif renpy.store._overlay_screens is True:
|
|
show = True
|
|
else:
|
|
show = False
|
|
|
|
if show:
|
|
|
|
for i in renpy.config.overlay_screens:
|
|
if get_screen(i) is None:
|
|
show_screen(i)
|
|
|
|
else:
|
|
|
|
for i in renpy.config.overlay_screens:
|
|
if get_screen(i) is not None:
|
|
hide_screen(i)
|
|
|
|
|
|
def per_frame():
|
|
"""
|
|
Called from interact once per frame to invalidate screens we want to
|
|
update once per frame.
|
|
"""
|
|
|
|
for i in renpy.config.per_frame_screens:
|
|
s = get_screen(i)
|
|
|
|
if s is None:
|
|
continue
|
|
|
|
updated_screens.discard(s)
|
|
renpy.display.render.invalidate(s)
|
|
s.update()
|