# Copyright 2017 Tom Rothamel # Permission to use, copy, modify, and/or distribute this software for # non-commerical purposes is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # # For the purpose of this license, when using this software to develop a # another software program, this program is being used commerically if # payment is required to distribute that program, to use that program, or # to access any feature in that program, or if the program presents # advertising to its user. # # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH # REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY # AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM # LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE # OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR # PERFORMANCE OF THIS SOFTWARE. # TODO: # # - Allow the creator to specify a list of tags they are interested in, # disabling the auto-detection. init offset = -1101 default persistent._director_bottom = False init python in director: from store import Action, config import store # A set of tags that will not be show to the user. tag_blacklist = { "black", "text", "vtext", "side" } # A set of tags that should only be used with the scene statement. scene_tags = { "bg" } # The set of tags that should only be used with the show statement. show_tags = set() # Should we try to filter out tags that have other tags as their # prefix (as are used alot in layerimages)? blacklist_prefixed_tags = True # A list of transforms to use. transforms = [ "left", "center", "right" ] # A list of transitions to use. transitions = [ "dissolve", "pixellate" ] # A list of audio channels we know about. audio_channels = [ "music", "sound", "audio" ] # The name of the voice channel. voice_channel = "voice" # A list of audio patterns to use for channels that do not have a # more specific list of patterns already defined. audio_patterns = [ "*.opus", "*.ogg", "*.mp3" ] # A map from channel name to the audio patterns to use for that channel. audio_channel_patterns = { } # A map from channel name to the audio files available on that channel. audio_files = { } # The spacing between a non-display line and a display line, or vice # versa. spacing = 1 # The spacing between two display lines. director_spacing = 0 # The spacing between two non-display lines. other_spacing = 0 # The maximum height of viewports containing scrolling information. viewport_height = 280 # Is the director enabled? Used by the tutorial to protect itself. enable = True state = renpy.session.get("director", None) # A list of statements we find too uninteresting to present to the # creator. UNINTERESTING_NODES = ( renpy.ast.Translate, renpy.ast.EndTranslate, ) # Nodes that are only interesting when on a line by itself. ALONE_NODES = ( renpy.ast.Label, renpy.ast.Pass, ) def audio_code_to_filename(channel, fn): return fn def audio_filename_to_code(channel, fn): return fn def audio_filename_to_display(channel, fn): return fn def is_play(n): return isinstance(n, renpy.ast.UserStatement) and n.get_name().startswith("play ") def is_queue(n): return isinstance(n, renpy.ast.UserStatement) and n.get_name().startswith("queue ") def is_stop(n): return isinstance(n, renpy.ast.UserStatement) and n.get_name().startswith("stop ") def is_voice(n): return isinstance(n, renpy.ast.UserStatement) and (n.get_name() == "voice") def is_interesting(n): if isinstance(n, ( renpy.ast.Show, renpy.ast.Hide, renpy.ast.Scene, renpy.ast.With, )): return True if is_play(n) or is_queue(n) or is_stop(n) or is_voice(n): return True return False # Initialize the state object if it doesn't exist. if state is None: state = store.NoRollback() # Is the directory currently active. state.active = False # Should the director screen be shown. state.show_director = False # The list of lines we've seen recently. state.lines = [ ] # The mode we're in. state.mode = "lines" # The filename and linenumber of the line we're editing, state.filename = "" state.linenumber = 0 # What tags are currently showing? state.showing = set() # What kind of statement is this? state.kind = None state.original_kind = None # The tag we're updating. state.tag = "" state.original_tag = "" # The attributes of the image we're updating. state.attributes = [ ] state.original_attributes = [ ] # The transforms applied to the image. state.transforms = [ ] state.original_transforms = [ ] # A list of image tags the current image is behind. state.behind = [ ] state.original_behind = [ ] # The transition in a with statement. state.transition = None state.original_transition = None # The audio channel. state.channel = None state.original_channel = None # The audio file. state.audio = None state.original_audio = None # NOTE: Remember to add new states to the Cancel function. # Has the new line been added to ast. state.added_statement = None # Are we changing the script? (Does the node needed to be removed?) state.change = False # The position of the first line. state.old_pos = None renpy.session["director"] = state def interact_base(): """ This is called by interact to update our data structures. """ if renpy.game.interface.trans_pause: return False show_director = False # Update the line log. lines = renpy.get_line_log() renpy.clear_line_log() if lines: pos = (lines[0].filename, lines[0].line) if pos != state.old_pos: state.old_pos = pos renpy.hide_screen("director") # Update state.line to the current line. state.line = renpy.get_filename_line() # State.lines is the list of lines we've just seen, along with # the actions used to edit those lines. for lle in lines[-30:]: if isinstance(lle.node, renpy.ast.Say): state.lines = [ ] break for lle in lines[-30:]: filename = lle.filename line = lle.line node = lle.node if isinstance(node, UNINTERESTING_NODES): continue if isinstance(node, ALONE_NODES): if [ i for i in renpy.scriptedit.nodes_on_line(filename, line) if not isinstance(i, ALONE_NODES) ]: continue if filename.startswith("renpy/"): show_director = False continue else: show_director = True text = renpy.scriptedit.get_line_text(filename, line) if text is None: text = "" text = text.strip() short_fn = filename.rpartition('/')[2] pos = "{}:{:04d}".format(short_fn, line) if lle.abnormal: add_action = None else: add_action = AddStatement(lle) if is_interesting(node): change_action = ChangeStatement(lle, node) else: change_action = None state.lines.append(( pos, text, add_action, change_action, )) return show_director def interact(): """ This is called once per interaction, to update the list of lines being displayed, and also to show or hide the director as appropriate. """ show_director = interact_base() # Show the director screen. if show_director and state.show_director and (renpy.context_nesting_level() == 0): if not renpy.get_screen("director"): renpy.show_screen("director") else: if renpy.get_screen("director"): renpy.hide_screen("director") def line_log_callback(lle): """ This annotates a line log entry with more information. """ lle.showing = set(renpy.get_showing_tags()) def init(): """ This is called once at game start, to reconfigured Ren'Py to support the ID. """ config.clear_lines = False config.line_log = True config.line_log_callbacks = [ line_log_callback ] config.start_interact_callbacks.append(interact) if state.active: init() def command(): """ This can be used to invoke the ID from the command line. """ if not state.active: state.active = True init() return True renpy.arguments.register_command("director", command) def get_scene_show_hide_statement(): if state.kind == "scene" and state.tag is None: return "scene" if state.tag is None: return None if state.kind == "hide": attributes = state.attributes else: if state.change and (state.attributes == state.original_attributes): attributes = state.attributes else: attributes = get_image_attributes() if attributes is None: return None rv = [ state.kind ] rv.append(state.tag) rv.extend(attributes) rv = " ".join(rv) if state.transforms: rv += " at " + ", ".join(state.transforms) if state.behind: rv += " behind " + ",".join(state.behind) return rv def quote_audio(): """ Returns the quoted audio filename. """ if state.audio is None: return None if state.channel is not None: audio = audio_filename_to_code(state.channel, state.audio) else: audio = state.audio return "'" + audio.replace("\\", "\\\\").replace("'", "\\'") + "'" def get_play_queue_statement(): if state.channel is None: return None if state.audio is None: return None return "{} {} {}".format(state.kind, state.channel, quote_audio()) def get_stop_statement(): if state.channel is None: return None return "{} {}".format(state.kind, state.channel) def get_voice_statement(): if state.audio is None: return None return "voice {}".format(quote_audio()) def get_statement(): """ If a statement is defined enough to implement, returns the text that can be added to the AST. Otherwise, returns None. """ if state.kind is None: rv = None elif state.kind == "with": rv = "with {}".format(state.transition) elif state.kind in ("play", "queue"): rv = get_play_queue_statement() elif state.kind == "stop": rv = get_stop_statement() elif state.kind == "voice": rv = get_voice_statement() else: rv = get_scene_show_hide_statement() return rv def update_ast(force=False): """ Updates the abstract syntax tree to match the current state, forcing a rollback if something significant has changed. This always forces an interaction restart. """ statement = get_statement() if (state.added_statement == statement) and (not force): renpy.restart_interaction() return linenumber = state.linenumber if statement: renpy.scriptedit.add_to_ast_before(statement, state.filename, linenumber) linenumber += 1 if state.added_statement is not None: renpy.scriptedit.remove_from_ast(state.filename, linenumber) state.added_statement = statement renpy.rollback(checkpoints=0, force=True, greedy=True) def dump_script(): for i in range(0, 100): key = ('game/script.rpy', i) if key in renpy.scriptedit.lines: print(key, renpy.scriptedit.lines[key].text) def pick_tag(): """ If there is only one valid tag, choose it and move to attribute mode. """ tags = get_tags() if state.mode != "tag": return if len(tags) != 1: return state.tag = tags[0] if state.kind == "hide": return state.mode = "attributes" def find_statement(filename, line, delta, limit=10): """ Tries to find a statement near `line`. If it can't find it on `line` itself, it searches forward (if `delta`) is positive or back (if `delta`) is negative. Returns the line number and nodes for the statement, or (None, None) if the statement is not found after searching `limit` lines. """ for _i in range(limit): nodes = renpy.scriptedit.nodes_on_line(filename, line) if nodes: return line, nodes line += delta return None, None def needs_space(filename, line): """ Returns True if there should be a space between (filename, line-1) and (filename, line). """ previous, previous_nodes = find_statement(filename, line-1, -1) if previous is None: return None next, next_nodes = find_statement(filename, line, 1) if next is None: return None def display(nodes): for n in nodes: if is_interesting(n): return True return False previous_display = display(previous_nodes) next_display = display(next_nodes) if previous_display ^ next_display: return spacing elif previous_display: return director_spacing else: return other_spacing def is_spacing(filename, line): line = renpy.scriptedit.get_full_text(filename, line) if line is None: return False return not line.strip() def adjust_spacing_before(filename, line): """ Adjusts the spacing between (filename, line) and the line before it. """ line, _ = find_statement(filename, line, 1) if line is None: return space = needs_space(filename, line) if space is None: return previous, _ = find_statement(filename, line - 1, -1) blanks = [ ] for i in range(previous + 1, line): if not is_spacing(filename, i): break blanks.append(i) delta_space = space - len(blanks) if delta_space > 0: for _ in range(delta_space): renpy.scriptedit.adjust_ast_linenumbers(filename, previous + 1, 1) renpy.scriptedit.insert_line_before('', filename, previous + 1) elif delta_space < 0: blanks.reverse() blanks = blanks[space:] for i in blanks: renpy.scriptedit.adjust_ast_linenumbers(filename, i, -1) renpy.scriptedit.remove_line(filename, i) def add_spacing(filename, line): adjust_spacing_before(filename, line + 1) adjust_spacing_before(filename, line) def remove_spacing(filename, line): adjust_spacing_before(filename, line) # Screen support functions ################################################# def get_tags(): """ Returns a list of tags that are valid for the current statement kind. """ if state.kind == "scene": rv = [ i for i in renpy.get_available_image_tags() if not i.startswith("_") if i not in tag_blacklist if i in scene_tags ] elif state.kind == "show": if show_tags: rv = [ i for i in renpy.get_available_image_tags() if not i.startswith("_") if i not in tag_blacklist if i in show_tags ] else: rv = [ i for i in renpy.get_available_image_tags() if not i.startswith("_") if i not in tag_blacklist if i not in scene_tags ] elif state.kind == "hide": rv = [ i for i in renpy.get_available_image_tags() if i in state.showing if not i.startswith("_") if i not in tag_blacklist if i not in scene_tags ] else: rv = [ ] rv.sort(key=component_key) return rv def get_attributes(): """ Returns a list of attributes that are valid for the current tag. """ if not state.tag: return [ ] return renpy.get_ordered_image_attributes(state.tag, [ ], component_key) def get_transforms(): """ Returns a list of transforms that are valid for the current tag. """ return transforms def get_image_attributes(): """ Returns the list of attributes in the current image, or None if no image is known. """ if state.tag is None: return None return renpy.check_image_attributes(state.tag, state.attributes) def get_ordered_attributes(): """ Returns the list of attributes in the order they appear in the current image (if known), or in the order they were selected otherwise. """ attrs = get_image_attributes() if attrs is not None: return attrs return state.attributes def get_behind_tags(exclude=None): """ Get a list of tags the current tag can be placed behind. """ rv = [ ] for t in state.showing: if t in scene_tags: continue if t == exclude: continue rv.append(t) return rv # Actions ################################################################## class Start(Action): """ This action starts the director and displays the lines screen. """ def __call__(self): if not state.show_director: if store._menu: return None if not config.developer: return None if not enable: renpy.notify(_("The interactive director is not enabled here.")) return None if state.show_director: return None state.show_director = True state.mode = "lines" if state.active: renpy.show_screen("director") renpy.restart_interaction() return # renpy.session["compile"] = True renpy.session["_greedy_rollback"] = True state.active = True store._reload_game() def get_sensitive(self): return (not state.active) or (not state.show_director) class Stop(Action): """ This hides the director interface. """ def __call__(self): state.show_director = False if renpy.get_screen("director"): renpy.hide_screen("director") renpy.restart_interaction() class AddStatement(Action): """ An action that adds a new statement before `filename`:`linenumber`. """ def __init__(self, lle): self.lle = lle self.sensitive = renpy.scriptedit.can_add_before(self.lle.filename, self.lle.line) def get_sensitive(self): return self.sensitive def __call__(self): state.filename = self.lle.filename state.linenumber = self.lle.line state.showing = self.lle.showing state.kind = None state.mode = "kind" state.tag = None state.original_tag = None state.attributes = [ ] state.original_attributes = [ ] state.transforms = [ ] state.original_transforms = [ ] state.transition = None state.original_transition = None state.behind = [ ] state.original_behind = [ ] state.channel = None state.original_channel = None state.audio = None state.original_audio = None state.added_statement = None state.change = False update_ast() def is_scene_show_hide_editable(node): """ Return true if a Scene, Show, or Hide ATL node is editable, or False otherwise. """ if not node.imspec: return True if node.imspec[1]: # expression return False if node.imspec[2]: # tag return False if node.imspec[4]: # layer return False if node.imspec[5]: # zorder return False if getattr(node, "atl", None): return False return True class ChangeStatement(Action): """ An action that changes the statement at `filename`:`linenumber`. `node` Should be the AST node corresponding to that statement. """ def __init__(self, lle, node): self.lle = lle self.tag = None self.attributes = [ ] self.transforms = [ ] self.transition = None self.behind = [ ] self.channel = None self.audio = None self.sensitive = True def audio(n): self.sensitive = False name = n.parsed[0] p = n.parsed[1] self.kind = name[0] self.channel = p["channel"] or name[1] if "file" in p: try: self.audio = eval(p["file"]) except: return self.audio = audio_code_to_filename(self.channel, self.audio) else: self.audio = None if p.get("loop", None): return fadeout = p.get("fadeout", None) if (fadeout is not None) and (fadeout != "None"): return if p.get("if_changed", False): return if p.get("fadein", "0") != "0": return self.sensitive = True if isinstance(node, renpy.ast.Show): self.kind = "show" self.tag = node.imspec[0][0] self.attributes = list(node.imspec[0][1:]) self.transforms = list(node.imspec[3]) self.behind = list(node.imspec[6]) self.sensitive = is_scene_show_hide_editable(node) elif isinstance(node, renpy.ast.Scene): self.kind = "scene" # The scene statement does not need to show an image. if node.imspec is not None: self.tag = node.imspec[0][0] self.attributes = list(node.imspec[0][1:]) self.transforms = list(node.imspec[3]) self.behind = list(node.imspec[6]) else: self.tag = None self.attributes = [ ] self.transforms = [ ] self.behind = [ ] self.sensitive = is_scene_show_hide_editable(node) elif isinstance(node, renpy.ast.Hide): self.kind = "hide" self.tag = node.imspec[0][0] self.attributes = list(node.imspec[0][1:]) self.transforms = list(node.imspec[3]) self.sensitive = is_scene_show_hide_editable(node) elif isinstance(node, renpy.ast.With): self.kind = "with" self.transition = node.expr elif is_play(node) or is_queue(node) or is_stop(node): audio(node) elif is_voice(node): p = node.parsed[1] self.kind = "voice" self.sensitive = False self.channel = voice_channel try: self.audio = eval(p) except: return self.audio = audio_code_to_filename(self.channel, self.audio) self.sensitive = True def get_sensitive(self): return self.sensitive def __call__(self): state.filename = self.lle.filename state.linenumber = self.lle.line state.showing = self.lle.showing state.kind = self.kind state.original_kind = self.kind if self.kind == "with": state.mode = "with" elif self.kind == "hide": state.mode = "tag" elif self.kind == "play" or self.kind == "queue": state.mode = "audio" elif self.kind == "stop": state.mode = "channel" elif self.kind == "voice": state.mode = "audio" else: if self.tag is None: state.mode = "tag" else: state.mode = "attributes" state.tag = self.tag state.original_tag = self.tag state.attributes = self.attributes state.original_attributes = list(self.attributes) state.transforms = list(self.transforms) state.original_transforms = list(self.transforms) state.transition = self.transition state.original_transition = self.transition state.behind = self.behind state.original_behind = list(self.behind) state.channel = self.channel state.original_channel = self.channel state.audio = self.audio state.original_audio = self.audio state.added_statement = True state.change = True update_ast() class SetKind(Action): def __init__(self, kind): self.kind = kind def __call__(self): if self.kind != state.kind: state.kind = self.kind state.tag = None state.attributes = [ ] if self.kind in ("scene", "show", "hide"): state.mode = "tag" pick_tag() if self.kind == "with": state.mode = "with" if self.kind in ("play", "queue", "stop"): state.mode = "channel" if self.kind == "voice": state.channel = "voice" state.mode = "audio" update_ast() class SetTag(Action): """ An action that sets the image tag. """ def __init__(self, tag): self.tag = tag def __call__(self): if state.tag != self.tag: state.tag = self.tag state.attributes = [ ] if state.kind != "hide": state.mode = "attributes" update_ast() def get_selected(self): return self.tag == state.tag class ToggleAttribute(Action): """ This action toggles on and off an attribute. If an attribute being toggled on conflicts with other attributes, those attributes are removed. Then the AST is updated. """ def __init__(self, attribute): self.attribute = attribute def __call__(self): if self.attribute in state.attributes: state.attributes.remove(self.attribute) else: state.attributes.append(self.attribute) compatible = set() for i in renpy.get_ordered_image_attributes(state.tag, [ self.attribute ]): compatible.add(i) state.attributes = [ i for i in state.attributes if i in compatible ] update_ast() def get_selected(self): return self.attribute in state.attributes class SetList(Action): """ When clicked once, sets l to [ v ]. When clicked again, sets l to [ ] """ def __init__(self, l, v): self.l = l self.v = v def __call__(self): if self.v in self.l: self.l.remove(self.v) else: self.l[:] = [ self.v ] update_ast() def get_selected(self): return self.v in self.l class ToggleList(Action): """ Toggles the presence or absence of v in l, appending it to the end of the list when necessary. """ def __init__(self, l, v): self.l = l self.v = v def __call__(self): if self.v in self.l: self.l.remove(self.v) else: self.l.append(self.v) update_ast() def get_selected(self): return self.v in self.l class SetTransition(Action): """ This sets the transition used by a with statement. """ def __init__(self, transition): self.transition = transition def __call__(self): state.transition = self.transition update_ast() def get_selected(self): return self.transition == state.transition class SetChannel(Action): """ This sets the channel used by an audio statement. """ def __init__(self, channel): self.channel = channel def __call__(self): if state.kind != "stop": state.mode = "audio" state.channel = self.channel update_ast() def get_selected(self): return self.channel == state.channel class SetAudio(Action): """ This sets the audio file played by a statement. """ def __init__(self, filename): self.filename = filename def __call__(self): state.audio = self.filename update_ast() def get_selected(self): return state.audio == self.filename class Commit(Action): """ Commits the current statement to the .rpy files. """ def __call__(self): statement = get_statement() if statement: renpy.scriptedit.insert_line_before(statement, state.filename, state.linenumber) if state.change: if statement: renpy.scriptedit.remove_line(state.filename, state.linenumber + 1) else: renpy.scriptedit.remove_line(state.filename, state.linenumber) if not state.change and statement: add_spacing(state.filename, state.linenumber) if state.change and not statement: remove_spacing(state.filename, state.linenumber) state.mode = "lines" renpy.clear_line_log() renpy.rollback(checkpoints=0, force=True, greedy=True) def get_sensitive(self): return get_statement() class Reset(Action): """ This action resets the AST to what it was when we started adjusting the statement. """ def __call__(self): state.kind = state.original_kind state.tag = state.original_tag state.attributes = state.original_attributes state.transforms = state.original_transforms if state.kind is None: state.mode = "kind" elif state.tag is None: state.mode = "tag" update_ast() class Cancel(Action): """ This action cancels the operation, resetting the AST and returning to the lines screen. """ def __call__(self): state.kind = state.original_kind state.tag = state.original_tag state.attributes = state.original_attributes state.transforms = state.original_transforms state.transition = state.original_transition state.channel = state.original_channel state.audio = state.original_audio state.mode = "lines" update_ast() class Remove(Action): """ This action removes the current line from the AST and the script. """ def __call__(self): state.kind = None state.tag = None state.mode = "lines" try: update_ast(force=True) finally: if state.change: renpy.scriptedit.remove_line(state.filename, state.linenumber) # Displayables ############################################################# class SemiModal(renpy.Displayable): """ This wraps a displayable, and ignores """ def __init__(self, child): renpy.Displayable.__init__(self) self.child = child self.w = 0 self.h = 0 def per_interact(self): renpy.display.render.redraw(self, 0) def render(self, width, height, st, at): if renpy.get_screen("confirm"): return renpy.Render(0, 0) surf = renpy.render(self.child, width, height, st, at) w, h = surf.get_size() self.w = w self.h = h rv = renpy.Render(w, h) rv.blit(surf, (0, 0)) return rv def event(self, ev, x, y, st): if renpy.get_screen("confirm"): return rv = self.child.event(ev, x, y, st) if rv is not None: return rv if ev.type == renpy.display.core.TIMEEVENT: return None if state.mode != "lines": if renpy.map_event(ev, "rollback") or renpy.map_event(ev, "rollforward"): raise renpy.IgnoreEvent() raise renpy.display.layout.IgnoreLayers() if (0 <= x < self.w) and (0 <= y < self.h): raise renpy.display.layout.IgnoreLayers() return None def get_placement(self): return self.child.get_placement() def visit(self): return [ self.child ] # Sort ##################################################################### import re def component_key(s): """ Sorts l in a way that groups numbers together and treats them as numbers (so c10 comes after c9, not c1.) """ rv = [ ] for i, v in enumerate(re.split(r'(\d+)', s.lower())): if not v: continue if i & 1: v = int(v) rv.append(v) return tuple(rv) init 2202 python hide in director: if state.active: for name, file, _line in renpy.dump.transforms: if file.startswith("renpy/common/"): continue if file == "game/screens.rpy": continue transforms.append(name) transforms.sort(key=component_key) import fnmatch for c in audio_channels + [ voice_channel ]: patterns = audio_channel_patterns.get(c, audio_patterns) if c not in audio_files: audio_files[c] = [ ] for fn in renpy.list_files(): for p in patterns: if fnmatch.fnmatch(fn, p): break else: continue audio_files[c].append(fn) audio_files[c].sort() if blacklist_prefixed_tags: available = set() for i in sorted(renpy.get_available_image_tags()): blacklist = False prefix = i while prefix: prefix, _, _ = prefix.rpartition("_") if prefix in available: blacklist = True if i in scene_tags: blacklist = False if i in show_tags: blacklist = False if blacklist: tag_blacklist.add(i) else: available.add(i) # Styles and screens ########################################################### style director_frame is _frame: xfill True yfill False ypadding 0 style director_top_frame is director_frame: background "#d0d0d0d0" yalign 0.0 style director_bottom_frame is director_frame: background "#d0d0d0f0" yalign 1.0 style director_text is _text: size 18 style director_label style director_label_text is director_text: bold True style director_button is empty style director_button_text is director_text: color "#405060" hover_color "#048" insensitive_color "#00000020" selected_color "#0099cc" style director_edit_button is director_button: xsize 18 style director_edit_button_text is director_button_text: font "DejaVuSans.ttf" xalign 0.5 style director_action_button is director_button style director_action_button_text is director_button_text: size 26 style director_icon_action_button is director_action_button: xpadding 10 style director_icon_action_button_text is director_action_button_text: font "DejaVuSans.ttf" style director_statement_text is director_text: size 20 style director_statement_button is director_button style director_statement_button_text is director_button_text: size 20 style director_vscrollbar is _vscrollbar screen director_move_button(): if persistent._director_bottom: textbutton _("⬆"): style "director_icon_action_button" action SetField(persistent, "_director_bottom", False) xalign 1.0 else: textbutton _("⬇"): style "director_icon_action_button" action SetField(persistent, "_director_bottom", True) xalign 1.0 screen director_lines(state): frame: style "empty" background Solid("#fff8", xsize=20, xpos=gui._scale(300)) has vbox: xfill True viewport: scrollbars "vertical" ymaximum director.viewport_height mousewheel True yinitial 1.0 viewport_yfill False has vbox: xfill True for line_pos, line_text, add_action, change_action in state.lines: fixed: yfit True textbutton "+": xpos gui._scale(300) action add_action style "director_edit_button" alt ("add before " + line_text) fixed: yfit True text "[line_pos]": xpos (gui._scale(300) - 10) xalign 1.0 text_align 1.0 style "director_text" if change_action: textbutton "✎": action change_action xpos gui._scale(300) style "director_edit_button" alt ("change " + line_text) frame: style "empty" left_padding (gui._scale(300) + 30) text "[line_text!q]" style "director_text" null height 14 fixed: yfit True hbox: xpos (gui._scale(300) + 30) textbutton _("Done"): action director.Stop() style "director_action_button" use director_move_button() screen director_statement(state): $ kind = state.kind or __("(statement)") $ tag = state.tag or __("(tag)") $ attributes = " ".join(director.get_ordered_attributes()) or __("(attributes)") $ transforms = ", ".join(state.transforms) or __("(transform)") $ behind = ", ".join(state.behind) or __("(tag)") $ behind_tags = director.get_behind_tags(state.tag) hbox: style_prefix "director_statement" textbutton "[kind] " action SetField(state, "mode", "kind") textbutton "[tag] " action SetField(state, "mode", "tag") if state.attributes or state.kind in { "scene", "show"}: textbutton "[attributes] " action SetField(state, "mode", "attributes") if state.transforms or state.kind in { "scene", "show"}: text "at " textbutton "[transforms] " action SetField(state, "mode", "transform") if behind_tags and (state.kind in { "show" }): text "behind " textbutton "[behind]" action SetField(state, "mode", "behind") null height 14 screen director_with_statement(state): $ transition = state.transition or __("(transition)") hbox: style_prefix "director_statement" text "with " textbutton "[transition]" action SetField(state, "mode", "with") null height 14 screen director_audio_statement(state): $ channel = state.channel or __("(channel)") $ audio = director.quote_audio() or __("(filename)") hbox: style_prefix "director_statement" textbutton "[state.kind] " action SetField(state, "mode", "kind") if state.kind != "voice": textbutton "[channel] " action SetField(state, "mode", "channel") if state.kind != "stop": textbutton "[audio!q]" action SetField(state, "mode", "audio") null height 14 screen director_footer(state): null height 14 fixed: yfit True hbox: style_prefix "director_action" spacing 26 if state.change: textbutton _("Change") action director.Commit() else: textbutton _("Add") action director.Commit() textbutton _("Cancel") action director.Cancel() if state.change: textbutton _("Remove") action director.Remove() use director_move_button() # Formats the choices. screen director_choices(title): text title size 20 frame: style "empty" left_margin 10 viewport: scrollbars "vertical" ymaximum director.viewport_height mousewheel True yinitial 0.0 viewport_yfill False hbox: box_wrap True spacing 20 transclude screen director_kind(state): vbox: xfill True use director_statement(state) use director_choices(_("Statement:")): textbutton "scene" action director.SetKind("scene") textbutton "show" action director.SetKind("show") textbutton "hide" action director.SetKind("hide") textbutton "with" action director.SetKind("with") textbutton "play" action director.SetKind("play") textbutton "queue" action director.SetKind("queue") textbutton "stop" action director.SetKind("stop") textbutton "voice" action director.SetKind("voice") use director_footer(state) screen director_tag(state): vbox: xfill True use director_statement(state) use director_choices(_("Tag:")): for t in director.get_tags(): textbutton "[t]": action director.SetTag(t) use director_footer(state) screen director_attributes(state): vbox: xfill True use director_statement(state) use director_choices(_("Attributes:")): for t in director.get_attributes(): textbutton "[t]": action director.ToggleAttribute(t) style "director_button" ypadding 0 use director_footer(state) screen director_transform(state): vbox: xfill True use director_statement(state) use director_choices(_("Transforms:")): for t in director.get_transforms(): textbutton "[t]": action director.SetList(state.transforms, t) alternate director.ToggleList(state.transforms, t) style "director_button" ypadding 0 use director_footer(state) screen director_behind(state): vbox: xfill True use director_statement(state) use director_choices(_("Behind:")): for t in director.get_behind_tags(state.tag): textbutton "[t]": action director.SetList(state.behind, t) alternate director.ToggleList(state.behind, t) style "director_button" ypadding 0 use director_footer(state) screen director_with(state): vbox: xfill True use director_with_statement(state) use director_choices(_("Transition:")): for t in director.transitions: textbutton "[t]": action director.SetTransition(t) style "director_button" ypadding 0 use director_footer(state) screen director_channel(state): vbox: xfill True use director_audio_statement(state) use director_choices(_("Channel:")): for c in director.audio_channels: textbutton "[c]": action director.SetChannel(c) style "director_button" ypadding 0 use director_footer(state) screen director_audio(state): vbox: xfill True use director_audio_statement(state) use director_choices(_("Audio Filename:")): for fn in director.audio_files.get(state.channel, [ ]): $ elided_fn = director.audio_filename_to_display(state.channel, fn) textbutton "[elided_fn]": action director.SetAudio(fn) style "director_button" ypadding 0 use director_footer(state) screen director(): zorder 1400 $ state = director.state if renpy.loadable("id/" + state.mode + ".png"): add ("id/" + state.mode + ".png") frame: style_prefix "director" style ("director_bottom_frame" if persistent._director_bottom else "director_top_frame") xpadding ( 0 if state.mode == "lines" else gui._scale(20) ) at director.SemiModal has fixed: fit_first True if state.mode == "lines": use director_lines(state) elif state.mode == "kind": use director_kind(state) elif state.mode == "tag": use director_tag(state) elif state.mode == "attributes": use director_attributes(state) elif state.mode == "transform": use director_transform(state) elif state.mode == "behind": use director_behind(state) elif state.mode == "with": use director_with(state) elif state.mode == "channel": use director_channel(state) elif state.mode == "audio": use director_audio(state) if state.mode == "lines": key "director" action director.Stop()