Compare commits
1 Commits
renpy-5.3.2
...
renpy-4.8
| Author | SHA1 | Date | |
|---|---|---|---|
| eb60f83e4d |
+100
@@ -1,3 +1,103 @@
|
||||
New in 4.8
|
||||
----------
|
||||
|
||||
A new system was implemented to manage widget focus. This new system
|
||||
allows the focus to be moved from widget to widget using the keyboard,
|
||||
without requiring the keymouse behavior be used. Because of this new
|
||||
system, the keymouse behavior is now a no-op.
|
||||
|
||||
To simplify the code and make every widget compatible with the new
|
||||
focus code, a number of widgets were reimplemented in terms of
|
||||
buttons. This includes the ui.menu widget, which is the widget that is
|
||||
used when presenting the user with choices. Menu buttons are of the
|
||||
new style style.menu_choice_button, while their labels remain of style
|
||||
style.menu_choice. Background images and padding can now be styled
|
||||
onto these menu buttons.
|
||||
|
||||
A number of new image manipulators have been made available in the
|
||||
new im package. These are objects that can load an image (im.Image),
|
||||
crop an image (im.Crop), scale images (im.Scale), rotozoom images
|
||||
(im.Rotozoom) and composite images (im.Composite). The latter is much
|
||||
more flexible than the previous image composition system, as it now
|
||||
supports placing images at arbitrary locations on an arbitrarily big
|
||||
canvas. Image (by itself) is now a function that creates the
|
||||
appropriate image manipulators, retaining compatibility with its
|
||||
behavior in previous releases.
|
||||
|
||||
The image cache is now based on image size, rather than number of
|
||||
images. This prevents a large number of small images from filling up
|
||||
the cache.
|
||||
|
||||
We now load images before starting the timer for a transition. This
|
||||
prevents us from popping into the middle of a transition because we
|
||||
spent some time loading images. This behavior can be controlled by
|
||||
config.load_before_transition.
|
||||
|
||||
The ui.imagemap widget has been rewritten in terms of ui.imagebuttons
|
||||
and im.Crops. Some styles have changed names as a result.
|
||||
|
||||
Ren'Py now keeps track of the images that have been shown to the
|
||||
user. This is the basis of this release's new extra, gallery.rpy,
|
||||
which manages a gallery of unlockable CG.
|
||||
|
||||
The audio system has been rewritten, and a number of low-level audio
|
||||
functions have been exposed in the new audio file. These functions
|
||||
control audio directly, but do not save the state of audio in a save
|
||||
file. The high-level music functions have been rewritten and placed in
|
||||
the music.rpy file, allowing a sufficently adventurous game author to
|
||||
change the way music is played.
|
||||
|
||||
Two changes have been made to the renpy.music_start() function in the
|
||||
high-level sound api. The startpos argument was removed (as it was a
|
||||
pain to support), and a new fadeout argument was added, which lets the
|
||||
fadeout time be controlled for each new song that is played.
|
||||
|
||||
The low-level sound api now lets us do the following things:
|
||||
|
||||
[list][*] Play up to eight sound effects at once.
|
||||
[*] Cancel a playing sound effect.
|
||||
[*] Pause and unpause the playing music.
|
||||
[/list]
|
||||
|
||||
There is now a new, state that a widget can be in. Along with hover,
|
||||
idle, and activate, a widget can now be insensitive. This is the state
|
||||
that all non-focused widgets are in. Having the new insensitive state
|
||||
means that we can eliminate the various disabled_button styles, so we
|
||||
did.
|
||||
|
||||
In general, in this release, many styles have changed names.
|
||||
|
||||
In prior versions of Ren'Py, the bar widget could respond to
|
||||
clicks. This was never used, and won't work well with the
|
||||
keyboard focus control system. So, it's been eliminated.
|
||||
|
||||
Now, if the label "enter_game_menu" exists, it is called when entering
|
||||
the game menu. It's expected that the main purpose of this label will
|
||||
be to play music when in the game menu. Using the default music
|
||||
system, the code to do this would look like:
|
||||
|
||||
[code]
|
||||
label enter_game_menu:
|
||||
$ renpy.music_start("game_menu_music.mid")
|
||||
return
|
||||
[/code]
|
||||
|
||||
A number of bugs were fixed, including bugs with the style system, and
|
||||
with animation refresh.
|
||||
|
||||
Tab characters are now expanded to tab stop at eight spaces, rather
|
||||
than to a fixed spacing of eight spaces. Old code using tabs may need
|
||||
to be reformatted.
|
||||
|
||||
The 'h' key now hides the windows on the screen. This means that all
|
||||
Ren'Py functionality is now available through the keyboard, at least
|
||||
for people with a 1-button mouse. (cough ... Mac ... cough)
|
||||
|
||||
Finally, setting the environment variable RENPY_DISABLE_FULLSCREEN to
|
||||
a non-empty value will disable fullscreen support, for those people
|
||||
(that person?) who had it crash their systems.
|
||||
|
||||
|
||||
New in 4.7.2
|
||||
------------
|
||||
|
||||
|
||||
+9
-6
@@ -113,11 +113,11 @@ init -500:
|
||||
|
||||
style = type
|
||||
|
||||
if selected:
|
||||
if selected and not disabled:
|
||||
style += "_selected"
|
||||
|
||||
if disabled:
|
||||
style += "_disabled"
|
||||
clicked = None
|
||||
|
||||
style = style + "_button"
|
||||
text_style = style + "_text"
|
||||
@@ -302,7 +302,7 @@ init -500:
|
||||
ui.fixed()
|
||||
|
||||
ui.window(style='gm_nav_window')
|
||||
ui.vbox()
|
||||
ui.vbox(focus='gm_nav')
|
||||
|
||||
for key, label, target, enabled in library.game_menu:
|
||||
|
||||
@@ -311,7 +311,7 @@ init -500:
|
||||
|
||||
if not eval(enabled):
|
||||
disabled = True
|
||||
clicked = ui.returns(None)
|
||||
clicked = None
|
||||
|
||||
_button_factory(label, "gm_nav", selected=(key==selected),
|
||||
disabled=disabled, clicked=clicked)
|
||||
@@ -331,8 +331,8 @@ init -500:
|
||||
clicked=ui.returns(("return", (name, False)))
|
||||
enable_hover = True
|
||||
else:
|
||||
clicked=None
|
||||
enable_hover = False
|
||||
clicked = None
|
||||
enable_hover = True
|
||||
|
||||
ui.button(style='file_picker_entry',
|
||||
clicked=clicked,
|
||||
@@ -517,6 +517,9 @@ label _enter_game_menu:
|
||||
if library.enter_transition:
|
||||
$ renpy.transition(library.enter_transition)
|
||||
|
||||
if renpy.has_label("enter_game_menu"):
|
||||
call enter_game_menu
|
||||
|
||||
return
|
||||
|
||||
# Entry points from the game into menu-space.
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
# This file contains code to manage the playing of music. It's here,
|
||||
# in an rpy file, because we now optionally let the user override all
|
||||
# of this to implement his or her own music system, using calls to
|
||||
# various functions found in audio.
|
||||
|
||||
init -1000:
|
||||
python hide:
|
||||
|
||||
config.debug_sound = True
|
||||
|
||||
# This becomes renpy.music_start.
|
||||
def music_start(filename, loops=-1, fadeout=None):
|
||||
"""
|
||||
This starts music playing. If a music track is already
|
||||
playing, this pauses that music track in favor of this
|
||||
one.
|
||||
|
||||
@param filename: The file that the music will be played from.
|
||||
This is relative to the game directory, and must be a real
|
||||
file (so it cannot be stored in an archive).
|
||||
|
||||
@param loops: The number of times the music will loop
|
||||
after it finishes playing for the first time. This is
|
||||
made somewhat less accurate by rollback and loading. If
|
||||
this number is less than zero, the song will loop
|
||||
forever.
|
||||
|
||||
@param fadeout: If this parameter is not None, it is
|
||||
interpreted as a time in seconds, which gives how long
|
||||
the fade will take.
|
||||
"""
|
||||
|
||||
if loops >= 0:
|
||||
loops += 1
|
||||
|
||||
ctx = renpy.context()
|
||||
ctx._music_name = filename
|
||||
ctx._music_loops = loops
|
||||
|
||||
if fadeout and audio.music_enabled() and not config.skipping:
|
||||
audio.music_fadeout(fadeout)
|
||||
|
||||
def music_stop():
|
||||
"""
|
||||
This stops the currently playing music track.
|
||||
"""
|
||||
|
||||
ctx = renpy.context()
|
||||
ctx._music_name = None
|
||||
ctx._music_loops = None
|
||||
|
||||
renpy.music_start = music_start
|
||||
renpy.music_stop = music_stop
|
||||
|
||||
# This is called once for each interaction, to ensure that the
|
||||
# music is appropriate for that interaction.
|
||||
def music_interact():
|
||||
|
||||
if not audio.music_enabled():
|
||||
return
|
||||
|
||||
ctx = renpy.context()
|
||||
|
||||
if not hasattr(ctx, '_music_name'):
|
||||
ctx._music_name = None
|
||||
ctx._music_loops = None
|
||||
|
||||
playing, queued = audio.music_filenames()
|
||||
|
||||
# If music is disabled, ensure that it is stopped.
|
||||
if not _preferences.music:
|
||||
if playing:
|
||||
audio.music_stop()
|
||||
|
||||
return
|
||||
|
||||
# If we do not match what is playing, stop what's currently
|
||||
# playing and get ready to play something else.
|
||||
if ctx._music_name != playing:
|
||||
|
||||
# If we're not playing anything, immediately start the
|
||||
# new track by calling music_end_event.
|
||||
if not playing:
|
||||
config.music_end_event()
|
||||
|
||||
# Otherwise, we will start a fade to the new music if no
|
||||
# such fade is already in progress, and we're not skipping.
|
||||
elif not audio.music_fading() and not config.skipping:
|
||||
audio.music_fadeout(config.fade_music)
|
||||
|
||||
# This is called whenever a track of music ends, or also from the
|
||||
# above when we want to start a new track when nothing else is
|
||||
# playing.
|
||||
def music_end_event():
|
||||
|
||||
if not _preferences.music:
|
||||
return
|
||||
|
||||
ctx = renpy.context()
|
||||
playing, queued = audio.music_filenames()
|
||||
|
||||
if not hasattr(ctx, '_music_name'):
|
||||
ctx._music_name = None
|
||||
ctx._music_loops = None
|
||||
|
||||
if not ctx._music_name:
|
||||
return
|
||||
|
||||
if not playing and ctx._music_loops:
|
||||
ctx._music_loops -= 1
|
||||
audio.music_play(ctx._music_name)
|
||||
|
||||
if not queued and ctx._music_loops:
|
||||
ctx._music_loops -= 1
|
||||
audio.music_queue(ctx._music_name)
|
||||
|
||||
config.music_interact = music_interact
|
||||
config.music_end_event = music_end_event
|
||||
|
||||
|
||||
|
||||
+16
-38
@@ -66,8 +66,7 @@ init -1000:
|
||||
style.default.yanchor = 'top'
|
||||
|
||||
# Sound properties.
|
||||
style.default.hover_sound = None
|
||||
style.default.activate_sound = None
|
||||
style.default.sound = None
|
||||
|
||||
# The base style for the large windows.
|
||||
style.create('window', 'default',
|
||||
@@ -123,13 +122,18 @@ init -1000:
|
||||
# Styles that are used for menus.
|
||||
|
||||
style.create('menu', 'default',
|
||||
"(sound, position) The style that is used for menus themselves.")
|
||||
"(position) The style that is used for the vbox containing a menu.")
|
||||
|
||||
style.create('menu_caption', 'default',
|
||||
"(text) The style that is used to render a menu caption.")
|
||||
|
||||
style.create('menu_choice', 'default',
|
||||
"""(text, hover, sound) The style that is used to render a menu choice.""")
|
||||
"""(text, hover) The style that is used to render
|
||||
the text of a menu choice.""")
|
||||
|
||||
style.create('menu_choice_button', 'default',
|
||||
"""(window, hover, sound) The style that is used
|
||||
to render the button containing a menu choice.""")
|
||||
|
||||
style.menu_choice.hover_color = (255, 255, 0, 255) # yellow
|
||||
style.menu_choice.activate_color = (255, 255, 0, 255) # yellow
|
||||
@@ -175,9 +179,12 @@ init -1000:
|
||||
|
||||
# Styles that are used by imagemaps
|
||||
style.create('imagemap', 'image_placement',
|
||||
'(sound, position) The style that is used for imagemaps.')
|
||||
'(position) The style that is used for imagemaps.')
|
||||
|
||||
# Style that is used by imagebutttons.
|
||||
style.create('imagemap_button', 'default',
|
||||
'(window, sound, hover) The style that is used for buttons inside imagemaps.')
|
||||
|
||||
# Styles that are used by imagebutttons.
|
||||
style.create('image_button', 'default',
|
||||
'(window, sound, hover) The default style used for image buttons.')
|
||||
|
||||
@@ -201,6 +208,7 @@ init -1000:
|
||||
style.button_text.color = dark_cyan
|
||||
style.button_text.hover_color = bright_cyan
|
||||
style.button_text.activate_color = bright_cyan
|
||||
style.button_text.insensitive_color = (192, 192, 192, 255)
|
||||
style.button_text.drop_shadow = (2, 2)
|
||||
|
||||
# Selected button.
|
||||
@@ -214,30 +222,12 @@ init -1000:
|
||||
style.selected_button_text.hover_color = bright_red
|
||||
style.selected_button_text.activate_color = bright_red
|
||||
|
||||
# Disabled button.
|
||||
|
||||
style.create('disabled_button', 'button',
|
||||
'(window, hover) The style that is used for a disabled button.')
|
||||
|
||||
style.disabled_button.hover_sound = None
|
||||
style.disabled_button.activate_sound = None
|
||||
|
||||
style.create('disabled_button_text', 'button_text',
|
||||
'(text, hover) The style that is used for the label of a disabled button.')
|
||||
|
||||
style.disabled_button_text.color = (128, 128, 128, 255)
|
||||
style.disabled_button_text.hover_color = (128, 128, 128, 255)
|
||||
style.disabled_button_text.activate_color = (128, 128, 128, 255)
|
||||
|
||||
|
||||
# Bar.
|
||||
style.create('bar', 'default',
|
||||
'(bar) The style that is used by default for bars.')
|
||||
|
||||
style.bar.left_bar = Solid(bright_cyan)
|
||||
style.bar.right_bar = Solid((0, 0, 0, 128))
|
||||
style.bar.left_gutter = 0
|
||||
style.bar.right_gutter = 0
|
||||
|
||||
# Styles that are used when laying out the main menu.
|
||||
style.create('mm_root_window', 'default',
|
||||
@@ -286,13 +276,6 @@ init -1000:
|
||||
|
||||
style.create('gm_nav_selected_button_text', 'selected_button_text',
|
||||
'(text, hover) The style of the text of a selected game menu navigation button.')
|
||||
|
||||
style.create('gm_nav_disabled_button', 'disabled_button',
|
||||
'(window, hover) The style of a disabled game menu navigation button.')
|
||||
|
||||
style.create('gm_nav_disabled_button_text', 'disabled_button_text',
|
||||
'(text, hover) The style of the text of a disabled game menu navigation button.')
|
||||
|
||||
|
||||
style.create('file_picker_window', 'default',
|
||||
'(window, position) A window containing the file picker that is used to choose slots for loading and saving.')
|
||||
@@ -312,11 +295,6 @@ init -1000:
|
||||
'(window, hover) The style that is used for enabled file picker navigation buttons.')
|
||||
style.create('file_picker_nav_button_text', 'button_text',
|
||||
'(text) The style that is used for the label of enabled file picker navigation buttons.')
|
||||
|
||||
style.create('file_picker_nav_disabled_button', 'disabled_button',
|
||||
'(window, hover) The style that is used for disabled file picker navigation buttons.')
|
||||
style.create('file_picker_nav_disabled_button_text', 'disabled_button_text',
|
||||
'(text) The style that is used for the label of disabled file picker navigation buttons.')
|
||||
|
||||
style.create('file_picker_entry', 'button',
|
||||
'(window, hover) The style that is used for each of the slots in the file picker.')
|
||||
@@ -327,7 +305,7 @@ init -1000:
|
||||
style.file_picker_entry.xminimum = 400
|
||||
style.file_picker_entry.ymargin = 2
|
||||
|
||||
style.file_picker_entry.idle_background = Solid((255, 255, 255, 255))
|
||||
style.file_picker_entry.background = Solid((255, 255, 255, 255))
|
||||
style.file_picker_entry.hover_background = Solid((255, 255, 192, 255))
|
||||
style.file_picker_entry.activate_background = Solid((255, 255, 192, 255))
|
||||
|
||||
@@ -335,7 +313,7 @@ init -1000:
|
||||
'(text) A base style for all text that is displayed in the file picker.')
|
||||
|
||||
style.file_picker_text.size = 18
|
||||
style.file_picker_text.idle_color = dark_cyan
|
||||
style.file_picker_text.color = dark_cyan
|
||||
style.file_picker_text.hover_color = bright_cyan
|
||||
|
||||
style.create('file_picker_new', 'file_picker_text',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
@@ -132,6 +133,14 @@ def main():
|
||||
s = f.read()
|
||||
f.close()
|
||||
|
||||
os.chdir("..")
|
||||
|
||||
try:
|
||||
renpy.main.main("dump_styles")
|
||||
except "foo":
|
||||
pass
|
||||
|
||||
os.chdir("doc")
|
||||
|
||||
s = re.sub(r"<!-- func (\S+) -->", function, s)
|
||||
s = re.sub(r"<!-- include (\S+) -->", include, s)
|
||||
|
||||
+158
-57
@@ -1722,14 +1722,16 @@ e "Pleased to meet you, %(name)s."
|
||||
|
||||
<!-- func renpy.watch -->
|
||||
|
||||
<!-- func renpy.windows -->
|
||||
<!-- func renpy.loadable -->
|
||||
|
||||
<!-- func renpy.exists -->
|
||||
|
||||
<!-- func renpy.context -->
|
||||
|
||||
<!-- func renpy.clear_game_runtime -->
|
||||
|
||||
<!-- func renpy.get_game_runtime -->
|
||||
|
||||
|
||||
<!-- func color -->
|
||||
|
||||
<p>
|
||||
@@ -2020,10 +2022,11 @@ init:
|
||||
stdout (wherever that may go to).
|
||||
</var>
|
||||
|
||||
<var name="config.image_cache_size" value="10">
|
||||
This is the number of images that can be in the image cache at
|
||||
once. This is mostly a suggestion, as if Ren'Py ever needs to
|
||||
display more than this number of images at once, it will cache
|
||||
<var name="config.image_cache_size" value="8">
|
||||
|
||||
This is used to set the size of the image cache, as a multiple of the
|
||||
screen size. This is mostly a suggestion, as if Ren'Py ever needs to
|
||||
display more than this amount of images at once, it will cache
|
||||
them all. But it will stop predictively loading images when the
|
||||
cache is filled beyond this, and it will try to drop images from
|
||||
the cache until it shrinks below this size. If this is too
|
||||
@@ -2047,6 +2050,11 @@ init:
|
||||
the image cache change.
|
||||
</var>
|
||||
|
||||
<var name="config.load_before_transitions" value="True">
|
||||
If True, the start of transitions will be delayed until the images
|
||||
used by said transitions have finished loading.
|
||||
</var>
|
||||
|
||||
<var name="config.allow_skipping" value="True">
|
||||
If set to False, the user is not able to skip over the text of the
|
||||
game.
|
||||
@@ -2079,9 +2087,9 @@ load scripts, as scripts will be loaded before it can be set.
|
||||
is slightly increased.
|
||||
</var>
|
||||
|
||||
<var name="config.keymouse_distance" value="5">
|
||||
This gives the number of pixels that the mouse is moved every
|
||||
1/20th of a second when the keymouse is being used.
|
||||
<var name="config.focus_crossrange_penalty" value="1024">
|
||||
This is the amount of penalty to apply to moves perpendicular to the
|
||||
selected direction of motion, when moving focus with the keyboard.
|
||||
</var>
|
||||
|
||||
<var name="config.sound_sample_rate" value="44100">
|
||||
@@ -2560,12 +2568,7 @@ top_padding and bottom_padding to the same value.
|
||||
|
||||
<p>
|
||||
The ui.bar() widget has a few properties that are specific to
|
||||
it. These properties control the look of the bars, and the size of
|
||||
the gutters on either side of a clickable widget. The gutters are
|
||||
empty space on the left and right of a clickable bar, and serve to
|
||||
make it easy to set the maximum and minimum value of the bar
|
||||
widget. Clicking on the left gutter sets the minimum value, while the
|
||||
right gutter sets the maximum value.
|
||||
it, that control the look of the bars.
|
||||
</p>
|
||||
|
||||
<prop name="left_bar">
|
||||
@@ -2580,29 +2583,22 @@ Displayable needs to be one that can change its size, such as a Solid
|
||||
or a Frame.
|
||||
</prop>
|
||||
|
||||
<prop name="left_gutter">
|
||||
The size of the left gutter of a clickable bar, in pixels.
|
||||
</prop>
|
||||
|
||||
<prop name="right_gutter">
|
||||
The size of the left gutter of a clickable bar, in pixels.
|
||||
</prop>
|
||||
|
||||
|
||||
|
||||
<h4>Hovering</h4>
|
||||
|
||||
<p>
|
||||
In Ren'Py, buttons and their contents support the idea of hovering:
|
||||
using different sets of properties for different states of the
|
||||
widget. If the widget has been selected, it is considered to be
|
||||
activated. Otherwise, if the mouse is over the widget, then that widget
|
||||
is hovered, else it is idle. On these widgets, Ren'Py will look up
|
||||
a property prefixed with "activate_", "hover_", "idle_", (as appropriate) in a
|
||||
style, before looking up an unprefixed property. If neither form
|
||||
is found on a style, the process is repeated again on the parent
|
||||
style. (The search pattern for background on a hovered button
|
||||
goes: button.hover_background, button.background,
|
||||
In Ren'Py, buttons and their contents support the idea of
|
||||
hovering: using different sets of properties for different states
|
||||
of the widget. If the widget cannot be given focus, it is
|
||||
considered to be insensitive. If the widget has been selected by
|
||||
the user, it is considered to be activated. Otherwise, if the
|
||||
widget has focus, then that widget is hovered, else it is idle. On
|
||||
these widgets, Ren'Py will look up a property prefixed with
|
||||
"insensitive_", "activate_", "hover_", or "idle_" (as appropriate)
|
||||
in a style, before looking up an unprefixed property. If neither
|
||||
form is found on a style, the process is repeated again on the
|
||||
parent style. (The search pattern for background on a hovered
|
||||
button goes: button.hover_background, button.background,
|
||||
default.hover_background, default.background.)
|
||||
</p>
|
||||
|
||||
@@ -2655,7 +2651,7 @@ init:
|
||||
init:
|
||||
$ style.button.color = (128, 255, 128, 255)
|
||||
$ style.button.xpos = 0
|
||||
$ style.button_idled.background = \
|
||||
$ style.button.idle_background = \
|
||||
renpy.Frame(renpy.Image("button_idled.png"),
|
||||
xborder=10, yborder=10)
|
||||
$ style.button_hover.background = \
|
||||
@@ -2954,31 +2950,26 @@ Ren'Py, and as of version 4.5 is as follows:
|
||||
|
||||
<example>
|
||||
keymap = dict(
|
||||
|
||||
# Bindings present almost everywhere, unless explicitly
|
||||
# disabled.
|
||||
rollback = [ 'K_PAGEUP', 'mousedown_4' ],
|
||||
screenshot = [ 's' ],
|
||||
toggle_fullscreen = [ 'f' ],
|
||||
toggle_music = [ 'm' ],
|
||||
game_menu = [ 'K_ESCAPE', 'mousedown_3' ],
|
||||
hide_windows = [ 'mouseup_2' ],
|
||||
game_menu = [ 'K_ESCAPE', 'mouseup_3' ],
|
||||
hide_windows = [ 'mouseup_2', 'h' ],
|
||||
|
||||
# Say.
|
||||
rollforward = [ 'mousedown_5', 'K_PAGEDOWN' ],
|
||||
dismiss = [ 'mouseup_1', 'K_RETURN', 'K_SPACE', 'K_KP_ENTER' ],
|
||||
|
||||
# Keymouse.
|
||||
keymouse_left = [ 'K_LEFT' ],
|
||||
keymouse_right = [ 'K_RIGHT' ],
|
||||
keymouse_up = [ 'K_UP' ],
|
||||
keymouse_down = [ 'K_DOWN' ],
|
||||
|
||||
# Menu.
|
||||
menu_mouseselect = [ 'mouseup_1' ],
|
||||
menu_keyselect = ['K_RETURN', 'K_KP_ENTER' ],
|
||||
menu_keyup = [ 'K_UP' ],
|
||||
menu_keydown = [ 'K_DOWN' ],
|
||||
|
||||
# Focus.
|
||||
focus_left = [ 'K_LEFT' ],
|
||||
focus_right = [ 'K_RIGHT' ],
|
||||
focus_up = [ 'K_UP' ],
|
||||
focus_down = [ 'K_DOWN' ],
|
||||
|
||||
# Button.
|
||||
button_select = [ 'mouseup_1', 'K_RETURN', 'K_KP_ENTER' ],
|
||||
|
||||
@@ -2986,12 +2977,6 @@ keymap = dict(
|
||||
input_backspace = [ 'K_BACKSPACE' ],
|
||||
input_enter = [ 'K_RETURN', 'K_KP_ENTER' ],
|
||||
|
||||
# Imagemap.
|
||||
imagemap_select = [ 'K_RETURN', 'K_KP_ENTER', 'mouseup_1' ],
|
||||
|
||||
# Bar.
|
||||
bar_click = [ 'mouseup_1' ],
|
||||
|
||||
# These keys control skipping.
|
||||
skip = [ 'K_LCTRL', 'K_RCTRL' ],
|
||||
toggle_skip = [ 'K_TAB' ],
|
||||
@@ -3079,6 +3064,18 @@ changed by a call to ui.layer(), followed by a matching call to
|
||||
ui.close() when done adding widgets to the new layer.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Widgets can be added to a focus group by supplying the name of the
|
||||
focus group as the focus parameter to a ui widget. If the same focus
|
||||
group exists on two interactions, Ren'Py will ensure that if the nth
|
||||
widget is selected in the first interaction, the nth widget will be
|
||||
given focus at the start of the second interaction. If no widget is
|
||||
given focus in this way, then Ren'Py looks for one that has had the
|
||||
default=True argument supplied to it. If it finds such a widget, then
|
||||
it focuses it. Otherwise, it uses fallback rules to determine if a
|
||||
widget should be given focus.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The following are the functions available in the ui module, which is
|
||||
automatically present in the game namespace.
|
||||
@@ -3122,8 +3119,6 @@ automatically present in the game namespace.
|
||||
|
||||
<!-- func ui.pausebehavior -->
|
||||
|
||||
<!-- func ui.keymousebehavior -->
|
||||
|
||||
<h4>Functions</h4>
|
||||
|
||||
<!-- func ui.interact -->
|
||||
@@ -3151,6 +3146,112 @@ relying on this function.)
|
||||
</p>
|
||||
</function>
|
||||
|
||||
<h3>Image Manipulators</h3>
|
||||
|
||||
<p>
|
||||
Image manipulators are objects that can return images. Every image
|
||||
manipulator can be used as a widget, but the opposite is not
|
||||
true. Image manipulators are used to implement the Image function,
|
||||
but constructing them directly can give you finer control.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
When an image manipulator requires another as input, the second image
|
||||
manipulator can be specified in any of a number of ways. The first way
|
||||
is as a string, which is turned into an im.Image. The second is a
|
||||
tuple, which is turned into an im.Composite using the rules for tuples
|
||||
given in Image. The third is that an image manipulator can be
|
||||
specified directly, in which case it is passed through unchanged.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Widgets may not be used as arguments to image manipulators. This is
|
||||
because image manipulators are very efficent, caching the computed
|
||||
images on their first load.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The image manipulators are:
|
||||
</p>
|
||||
|
||||
<!-- func im.Image -->
|
||||
|
||||
<!-- func im.Crop -->
|
||||
|
||||
<!-- func im.Composite -->
|
||||
|
||||
<!-- func im.Scale -->
|
||||
|
||||
<!-- func im.Rotozoom -->
|
||||
|
||||
<h3>Low-Level Audio Functions</h3>
|
||||
|
||||
<p>
|
||||
These functions manipulate Ren'Py audio subsystem directly. Unlike the
|
||||
high-level music functions, these functions do not manipulate
|
||||
persistent state. As a result, calls to these functions will not
|
||||
persistent across sessions.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Still, these functions are useful to allow access to behavior that
|
||||
is not yet exposed through the high-level interface. They are also
|
||||
useful for a person who wants to change the high-level music or sound
|
||||
interfaces. So, we've documented them here.
|
||||
</p>
|
||||
|
||||
<h4>Sound Functions</h4>
|
||||
|
||||
<!-- func audio.sound_enabled -->
|
||||
|
||||
<!-- func audio.sound_play -->
|
||||
|
||||
<!-- func audio.sound_stop -->
|
||||
|
||||
<h4>Music Functions</h4>
|
||||
|
||||
<p>
|
||||
The low-level music system deals with two music files at once, a
|
||||
playing file and a queued file. When the playing file finishes
|
||||
playing, the queued file is begun, and queued file becomes
|
||||
empty.
|
||||
</p>
|
||||
|
||||
<!-- func audio.music_enabled -->
|
||||
|
||||
<!-- func audio.music_play -->
|
||||
|
||||
<!-- func audio.music_queue -->
|
||||
|
||||
<!-- func audio.music_stop -->
|
||||
|
||||
<!-- func audio.music_fadeout -->
|
||||
|
||||
<!-- func audio.music_fading -->
|
||||
|
||||
<!-- func audio.music_filenames -->
|
||||
|
||||
<!-- func audio.music_pause -->
|
||||
|
||||
<!-- func audio.music_unpause -->
|
||||
|
||||
<h4>Music Callbacks</h4>
|
||||
|
||||
<var name="config.music_interact" value="...">
|
||||
This callback is called (without any arguments) at the start (or
|
||||
restart) of each interaction. It's intended to allow the system to
|
||||
restore music after a context change, or to implement changes in
|
||||
the music that occured before that interaction.
|
||||
</var>
|
||||
|
||||
<var name="config.music_end_event" value="...">
|
||||
This callback is called (without any arguments) when a track has
|
||||
finished playing. If a second track has been queued, it will have
|
||||
started before this callback is called. Usually, this callback is
|
||||
used to queue up another track when one ends. It can also be used
|
||||
to start the music playing again after a fadeout.
|
||||
</var>
|
||||
|
||||
|
||||
<h3>Function Index</h3>
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
init:
|
||||
image black = Solid((0, 0, 0, 255))
|
||||
|
||||
label start:
|
||||
label main_menu:
|
||||
|
||||
$ style._write_docs("doc/styles.xml")
|
||||
"Dumped style."
|
||||
$ renpy.renpy.style.write_docs("doc/styles.xml")
|
||||
$ raise "foo"
|
||||
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
# This extra implements an image gallery, complete with automatic
|
||||
# unlocking of the images that have been shown to the user. The images
|
||||
# are divided into pages, with a fixed number of images on each page.
|
||||
|
||||
# Right now, this is configured to used the images found in the demo.
|
||||
# The images are repeated quite a bit, so that we can get four pages
|
||||
# of them. You probably wouldn't do that if you were using this in a
|
||||
# real game.
|
||||
|
||||
# To see this in action, drop it into the game directory of the
|
||||
# demo.
|
||||
|
||||
# Configuration.
|
||||
init:
|
||||
|
||||
python hide:
|
||||
|
||||
|
||||
# The number of columns and rows of images to show in the
|
||||
# gallery.
|
||||
store.gallery_cols = 3
|
||||
store.gallery_rows = 4
|
||||
|
||||
# The size that each image should be scaled to, and that
|
||||
# thumbnails should be.
|
||||
store.gallery_width = 160
|
||||
store.gallery_height = 120
|
||||
|
||||
# The contents of each of the page. Each of these is a list of
|
||||
# tuples with the first element of the tuple being the image
|
||||
# filename and the second element being the name of the image
|
||||
# that will unlock the image with this filename. (That is,
|
||||
# the name that is used in show or scene statements, as a
|
||||
# string.)
|
||||
#
|
||||
# When displaying an image as a thumbnail, this code first
|
||||
# looks for the file thumbnail_<filename>. If that file
|
||||
# exists, it should be a gallery_width x gallery_height
|
||||
# thumbnail. Otherwise, a thumbnail is automatically
|
||||
# generated, but it may screw up the aspect ratio of the
|
||||
# image.
|
||||
#
|
||||
# You probably want to create thumbnails for most images, to
|
||||
# limit memory consumption.
|
||||
page1 = [
|
||||
( "whitehouse.jpg", "whitehouse" ),
|
||||
( "washington.jpg", "washington" ),
|
||||
( "carillon.jpg", "carillon" ),
|
||||
( "whitehouse.jpg", "whitehouse" ),
|
||||
( "washington.jpg", "washington" ),
|
||||
( "carillon.jpg", "carillon" ),
|
||||
( "whitehouse.jpg", "whitehouse" ),
|
||||
( "washington.jpg", "washington" ),
|
||||
( "carillon.jpg", "carillon" ),
|
||||
( "whitehouse.jpg", "whitehouse" ),
|
||||
( "washington.jpg", "washington" ),
|
||||
( "carillon.jpg", "carillon" ),
|
||||
]
|
||||
|
||||
page2 = [
|
||||
( "whitehouse.jpg", "whitehouse" ),
|
||||
( "washington.jpg", "washington" ),
|
||||
( "carillon.jpg", "carillon" ),
|
||||
( "whitehouse.jpg", "whitehouse" ),
|
||||
( "washington.jpg", "washington" ),
|
||||
( "carillon.jpg", "carillon" ),
|
||||
( "whitehouse.jpg", "whitehouse" ),
|
||||
( "washington.jpg", "washington" ),
|
||||
( "carillon.jpg", "carillon" ),
|
||||
( "whitehouse.jpg", "whitehouse" ),
|
||||
( "washington.jpg", "washington" ),
|
||||
( "carillon.jpg", "carillon" ),
|
||||
]
|
||||
|
||||
page3 = [
|
||||
( "whitehouse.jpg", "whitehouse" ),
|
||||
( "washington.jpg", "washington" ),
|
||||
]
|
||||
|
||||
page4 = [
|
||||
( "9a_happy.png", "eileen happy" ),
|
||||
( "9a_vhappy.png", "eileen vhappy" ),
|
||||
( "9a_concerned.png", "eileen concerned" ),
|
||||
]
|
||||
|
||||
# This is the actual list of gallery pages. It's a list
|
||||
# of tuples, with the first element being the name of the
|
||||
# page, the second being the contents of the page (one of
|
||||
# the lists created above), and the third image being the
|
||||
# image used as the background of the page.
|
||||
store.gallery_pages = [
|
||||
("Backgrounds 1", page1, "washington.jpg"),
|
||||
("Backgrounds 2", page2, "whitehouse.jpg"),
|
||||
("Backgrounds 3", page3, "carillon.jpg"),
|
||||
("Character Art", page4, "washington.jpg"),
|
||||
]
|
||||
|
||||
# A window containing the gallery page buttons.
|
||||
style.create('gallery_pages', 'default')
|
||||
style.gallery_pages.xpos = 0.99
|
||||
style.gallery_pages.xanchor='right'
|
||||
style.gallery_pages.ypos = 0.02
|
||||
style.gallery_pages.yanchor = 'top'
|
||||
|
||||
# A button that links to a gallery page.
|
||||
style.create('gallery_page_button', 'button')
|
||||
style.create('gallery_page_button_text', 'button_text')
|
||||
|
||||
# A button that returns us to from whence we came.
|
||||
style.create('gallery_return_button', 'button')
|
||||
style.create('gallery_return_button_text', 'button_text')
|
||||
|
||||
style.gallery_return_button.xpos = 0.99
|
||||
style.gallery_return_button.xanchor='right'
|
||||
style.gallery_return_button.ypos = 0.98
|
||||
style.gallery_return_button.yanchor = 'bottom'
|
||||
|
||||
|
||||
# The grid containing the gallery image buttons.
|
||||
style.create('gallery_grid', 'default')
|
||||
|
||||
# The style of the buttons in the gallery.
|
||||
style.create('gallery_button', 'default')
|
||||
|
||||
# Right now, the backgrounds are all solids, but in a more
|
||||
# professional version, the insensitive background would
|
||||
# probably be a placeholder image that indicates that a
|
||||
# picture has yet to be unlocked.
|
||||
style.gallery_button.insensitive_background = Solid((192, 192, 192, 255))
|
||||
style.gallery_button.idle_background = Solid((255, 255, 255, 255))
|
||||
style.gallery_button.hover_background = Solid((255, 255, 192, 255))
|
||||
|
||||
style.gallery_button.left_margin = 5
|
||||
style.gallery_button.right_margin = 5
|
||||
style.gallery_button.top_margin = 5
|
||||
style.gallery_button.bottom_margin = 5
|
||||
|
||||
style.gallery_button.left_padding = 5
|
||||
style.gallery_button.right_padding = 5
|
||||
style.gallery_button.top_padding = 5
|
||||
style.gallery_button.bottom_padding = 5
|
||||
|
||||
# The style of the images in the buttons in the gallery.
|
||||
style.create('gallery_button_image', 'default')
|
||||
|
||||
# The style of the images that are being shown to the user.
|
||||
style.create('gallery_image', 'image_placement')
|
||||
|
||||
# The transition used when switching gallery pages.
|
||||
store.gallery_transition = Dissolve(0.5)
|
||||
|
||||
python:
|
||||
|
||||
# The function that actually manages the display of the image
|
||||
# gallery.
|
||||
def gallery():
|
||||
|
||||
page = 0
|
||||
|
||||
while True:
|
||||
|
||||
images = gallery_pages[page][1]
|
||||
ui.image(gallery_pages[page][2])
|
||||
|
||||
# Show the names of the various gallery pages.
|
||||
|
||||
ui.window(style='gallery_pages')
|
||||
ui.vbox(focus="gallery_pages")
|
||||
|
||||
for i in range(0, len(gallery_pages)):
|
||||
if i == page:
|
||||
clicked = None
|
||||
else:
|
||||
clicked = ui.returns(("page", i))
|
||||
|
||||
ui.textbutton(gallery_pages[i][0],
|
||||
style='gallery_page_button',
|
||||
text_style='gallery_page_button_text',
|
||||
clicked=clicked)
|
||||
ui.close()
|
||||
|
||||
# Show the return button.
|
||||
ui.textbutton('Return',
|
||||
style='gallery_return_button',
|
||||
text_style='gallery_return_button_text',
|
||||
clicked=ui.returns(("return", None)))
|
||||
|
||||
# Show the grid for this page.
|
||||
ui.grid(gallery_cols, gallery_rows, style='gallery_grid')
|
||||
|
||||
# For each grid cell.
|
||||
for i in range(0, gallery_cols * gallery_rows):
|
||||
|
||||
# Fill empty space with nulls.
|
||||
if i >= len(images):
|
||||
ui.null()
|
||||
continue
|
||||
|
||||
# Otherwise, get the filename and spec and see if
|
||||
# we've unlocked it.
|
||||
filename, spec = images[i]
|
||||
|
||||
if spec:
|
||||
spec = spec.split()
|
||||
|
||||
if spec and tuple(spec) not in persistent._seen_images:
|
||||
filename = None
|
||||
clicked = None
|
||||
else:
|
||||
clicked = ui.returns(('show', filename))
|
||||
|
||||
# Create the button, containing the appropriate
|
||||
# image or a null if we haven't unlocked it yet.
|
||||
|
||||
ui.button(style='gallery_button', clicked=clicked)
|
||||
|
||||
if not filename:
|
||||
ui.null(width=gallery_width, height=gallery_height)
|
||||
else:
|
||||
|
||||
if renpy.loadable("thumbnail_" + filename):
|
||||
ui.image("thumbnail_" + filename,
|
||||
style="gallery_button_image")
|
||||
else:
|
||||
ui.add(im.Scale(filename,
|
||||
gallery_width,
|
||||
gallery_height))
|
||||
|
||||
ui.close()
|
||||
|
||||
# Interact with the user.
|
||||
renpy.transition(gallery_transition)
|
||||
cmd, arg = ui.interact(suppress_overlay=True, suppress_underlay=True)
|
||||
|
||||
# Process the user's commands.
|
||||
if cmd == "show":
|
||||
ui.add(Solid((0, 0, 0, 255)))
|
||||
ui.image(arg)
|
||||
ui.saybehavior()
|
||||
renpy.transition(gallery_transition)
|
||||
ui.interact(suppress_overlay=True, suppress_underlay=True)
|
||||
|
||||
if cmd == "page":
|
||||
page = arg
|
||||
|
||||
if cmd == "return":
|
||||
renpy.transition(gallery_transition)
|
||||
return
|
||||
|
||||
|
||||
library.main_menu.insert(2, ( "CG Gallery", "gallery"))
|
||||
|
||||
|
||||
label gallery:
|
||||
|
||||
$ gallery()
|
||||
|
||||
jump _main_menu
|
||||
|
||||
|
||||
|
||||
|
||||
+6
-4
@@ -2,9 +2,9 @@
|
||||
# order.
|
||||
|
||||
# Some version numbers and things.
|
||||
version = "Ren'Py 4.7.2"
|
||||
script_version = 7
|
||||
savegame_suffix = "-7.save"
|
||||
version = "Ren'Py 4.8"
|
||||
script_version = 8000
|
||||
savegame_suffix = "-8.save"
|
||||
|
||||
|
||||
# Can be first, because has no dependencies, and may be imported
|
||||
@@ -32,8 +32,10 @@ import renpy.display.text # core
|
||||
import renpy.display.layout # core
|
||||
import renpy.display.behavior # layout
|
||||
import renpy.display.transition # core
|
||||
import renpy.display.image # core, behavior
|
||||
import renpy.display.im
|
||||
import renpy.display.image # core, behavior, im
|
||||
import renpy.display.video
|
||||
import renpy.display.focus
|
||||
|
||||
import renpy.ui
|
||||
|
||||
|
||||
@@ -286,6 +286,10 @@ def imspec_common(imspec, hide=False):
|
||||
for i in at_list:
|
||||
img = renpy.python.py_eval(i)(img)
|
||||
|
||||
# Update the set of images that have ever been seen.
|
||||
if not hide:
|
||||
renpy.game.persistent._seen_images[tuple(name)] = True
|
||||
|
||||
return key, img
|
||||
|
||||
def predict_imspec(imspec, callback):
|
||||
|
||||
+26
-26
@@ -47,9 +47,9 @@ profile = False
|
||||
# The directory save files will be saved to.
|
||||
savedir = None
|
||||
|
||||
# The number of images that are allowed to live in the image cache
|
||||
# at once.
|
||||
image_cache_size = 10
|
||||
# The number of screens worth of images that are allowed to live in the image
|
||||
# cache at once.
|
||||
image_cache_size = 8
|
||||
|
||||
# The number of statements we will analyze when doing predictive
|
||||
# loading. Please note that this is a total number of statements in a
|
||||
@@ -83,9 +83,6 @@ force_archives = False
|
||||
# An image file containing the mouse cursor, if one is defined.
|
||||
mouse = None
|
||||
|
||||
# The distance the keyboard moves the mouse, per 50 ms tick, in pixels.
|
||||
keymouse_distance = 5
|
||||
|
||||
# The default sound playback sample rate.
|
||||
sound_sample_rate = 44100
|
||||
|
||||
@@ -113,6 +110,14 @@ overlay_layers = [ 'overlay' ]
|
||||
# false otherwise.
|
||||
overlay_during_wait = True
|
||||
|
||||
# When using the keyboard to navigate, how much we penalize
|
||||
# distance out of the preferred direction.
|
||||
focus_crossrange_penalty = 1024
|
||||
|
||||
# If True, then we force all loading to occur before transitions
|
||||
# start.
|
||||
load_before_transition = True
|
||||
|
||||
# The keymap that is used to change keypresses and mouse events.
|
||||
keymap = dict(
|
||||
|
||||
@@ -123,24 +128,18 @@ keymap = dict(
|
||||
toggle_fullscreen = [ 'f' ],
|
||||
toggle_music = [ 'm' ],
|
||||
game_menu = [ 'K_ESCAPE', 'mouseup_3' ],
|
||||
hide_windows = [ 'mouseup_2' ],
|
||||
hide_windows = [ 'mouseup_2', 'h' ],
|
||||
|
||||
# Say.
|
||||
rollforward = [ 'mousedown_5', 'K_PAGEDOWN' ],
|
||||
dismiss = [ 'mouseup_1', 'K_RETURN', 'K_SPACE', 'K_KP_ENTER' ],
|
||||
|
||||
# Keymouse.
|
||||
keymouse_left = [ 'K_LEFT' ],
|
||||
keymouse_right = [ 'K_RIGHT' ],
|
||||
keymouse_up = [ 'K_UP' ],
|
||||
keymouse_down = [ 'K_DOWN' ],
|
||||
|
||||
# Menu.
|
||||
menu_mouseselect = [ 'mouseup_1' ],
|
||||
menu_keyselect = ['K_RETURN', 'K_KP_ENTER' ],
|
||||
menu_keyup = [ 'K_UP' ],
|
||||
menu_keydown = [ 'K_DOWN' ],
|
||||
|
||||
# Focus.
|
||||
focus_left = [ 'K_LEFT' ],
|
||||
focus_right = [ 'K_RIGHT' ],
|
||||
focus_up = [ 'K_UP' ],
|
||||
focus_down = [ 'K_DOWN' ],
|
||||
|
||||
# Button.
|
||||
button_select = [ 'mouseup_1', 'K_RETURN', 'K_KP_ENTER' ],
|
||||
|
||||
@@ -148,18 +147,19 @@ keymap = dict(
|
||||
input_backspace = [ 'K_BACKSPACE' ],
|
||||
input_enter = [ 'K_RETURN', 'K_KP_ENTER' ],
|
||||
|
||||
# Imagemap.
|
||||
imagemap_select = [ 'K_RETURN', 'K_KP_ENTER', 'mouseup_1' ],
|
||||
|
||||
# Bar.
|
||||
bar_click = [ 'mouseup_1' ],
|
||||
|
||||
|
||||
# These keys control skipping.
|
||||
skip = [ 'K_LCTRL', 'K_RCTRL' ],
|
||||
toggle_skip = [ 'K_TAB' ],
|
||||
)
|
||||
|
||||
# A function that is called before each interaction, to update the
|
||||
# music that is currently playing.
|
||||
music_interact = None
|
||||
|
||||
# A function that is called when a music track ends, perhaps to
|
||||
# play another track.
|
||||
music_end_event = None
|
||||
|
||||
# The number of frames that Ren'Py has shown.
|
||||
frames = 0
|
||||
|
||||
|
||||
+392
-157
@@ -168,164 +168,9 @@ def init():
|
||||
|
||||
playing_midi = False
|
||||
|
||||
# This detects if the filename is a midi, and sets playing_midi
|
||||
# appropriately.
|
||||
def detect_midi(fn):
|
||||
if mixer_works:
|
||||
pygame.mixer.music.set_endevent(renpy.display.core.MUSICEND)
|
||||
|
||||
fn = fn.lower()
|
||||
|
||||
global playing_midi
|
||||
playing_midi = fn.endswith(".mid") or fn.endswith(".midi")
|
||||
|
||||
|
||||
# Information about the currently playing track.
|
||||
current_music = None
|
||||
|
||||
def music_delay(offset):
|
||||
"""
|
||||
Returns the time left until the current music has been playing for
|
||||
offset seconds. If music is not playing, return None. May return
|
||||
a negative time.
|
||||
"""
|
||||
|
||||
if not mixer_works:
|
||||
return None
|
||||
|
||||
mo = pygame.mixer.music.get_pos()
|
||||
if mo < 0:
|
||||
return None
|
||||
|
||||
mo /= 1000.0
|
||||
|
||||
return offset - mo
|
||||
|
||||
|
||||
|
||||
def music_start(filename, loops=-1, startpos=0.0):
|
||||
"""
|
||||
This starts music playing. If a music track is already playing,
|
||||
stops that track in favor of this one.
|
||||
|
||||
@param filename: The file that the music will be played from. This
|
||||
is relative to the game directory, and must be a real file (so it
|
||||
cannot be stored in an archive.)
|
||||
|
||||
@param loops: The number of times the music will loop after it
|
||||
finishes playing. If negative, the music will loop indefinitely.
|
||||
Please note that even once the song has finished, rollback or load
|
||||
may cause it to start playing again. So it may not be safe to have
|
||||
this set to a non-negative value.
|
||||
|
||||
@param startpos: The number of seconds into the music to start playing.
|
||||
"""
|
||||
|
||||
if not mixer_works:
|
||||
return
|
||||
|
||||
music_stop()
|
||||
renpy.game.context().scene_lists.music = (filename, loops, startpos)
|
||||
restore_music()
|
||||
|
||||
|
||||
def music_stop():
|
||||
"""
|
||||
Stops the currently playing music track.
|
||||
"""
|
||||
|
||||
if not mixer_works:
|
||||
return
|
||||
|
||||
renpy.game.context().scene_lists.music = None
|
||||
restore_music()
|
||||
|
||||
def restore_music():
|
||||
"""
|
||||
This makes sure that the current music matches the music found in
|
||||
the context.
|
||||
"""
|
||||
|
||||
if not mixer_works:
|
||||
return
|
||||
|
||||
global current_music
|
||||
global fading
|
||||
|
||||
compute_midi_msf()
|
||||
set_music_volume(1.0)
|
||||
|
||||
new_music = renpy.game.context().scene_lists.music
|
||||
|
||||
if not renpy.game.preferences.music:
|
||||
new_music = None
|
||||
|
||||
if current_music == new_music:
|
||||
return
|
||||
|
||||
if not mixer_enabled:
|
||||
return
|
||||
|
||||
# Usually, ignore errors.
|
||||
try:
|
||||
if current_music != new_music and current_music:
|
||||
current_music = None
|
||||
pygame.mixer.music.fadeout(int(renpy.config.fade_music * 1000))
|
||||
fading = True
|
||||
else:
|
||||
if not pygame.mixer.music.get_busy():
|
||||
fn, loops, startpos = new_music
|
||||
|
||||
fading = False
|
||||
detect_midi(fn)
|
||||
|
||||
pygame.mixer.music.load(renpy.game.basepath + "/" + fn)
|
||||
pygame.mixer.music.play(loops, startpos)
|
||||
|
||||
set_music_volume(master_music_volume)
|
||||
|
||||
current_music = new_music
|
||||
|
||||
except pygame.error, e:
|
||||
if renpy.config.debug_sound:
|
||||
raise
|
||||
else:
|
||||
print "Error while trying to play music:", str(e)
|
||||
# Plays sounds.
|
||||
|
||||
def play(fn, loops=0):
|
||||
"""
|
||||
This plays the given sound. The sound must be in a wav file,
|
||||
and expected to have a sample rate 44100hz (changable with
|
||||
config.sound_sample_rate), 16 bit, stereo. These expectations may
|
||||
be violated, but that may lead to conversion delays.
|
||||
|
||||
Once a sound has been started, there's no way to stop it.
|
||||
|
||||
@param fn: The name of the file that the sound is read from. This
|
||||
file may be contained in a game directory or an archive.
|
||||
|
||||
@param loops: The number of extra times the sound will be
|
||||
played. (The default, 0, will play the sound once.)
|
||||
"""
|
||||
|
||||
if not mixer_works:
|
||||
return
|
||||
|
||||
if not fn:
|
||||
return
|
||||
|
||||
if not renpy.game.preferences.sound:
|
||||
return
|
||||
|
||||
if not mixer_enabled:
|
||||
return
|
||||
|
||||
try:
|
||||
sound = pygame.mixer.Sound(renpy.loader.load(fn))
|
||||
sound.play()
|
||||
except:
|
||||
if renpy.config.debug_sound:
|
||||
raise
|
||||
|
||||
def pre_init():
|
||||
try:
|
||||
bufsize = 4096
|
||||
@@ -356,6 +201,7 @@ def disable_mixer():
|
||||
|
||||
if mixer_enabled:
|
||||
try:
|
||||
music_stop()
|
||||
pygame.mixer.quit()
|
||||
except:
|
||||
if renpy.config.debug_sound:
|
||||
@@ -383,3 +229,392 @@ def enable_mixer():
|
||||
|
||||
mixer_enabled = True
|
||||
|
||||
|
||||
# This detects if the filename is a midi, and sets playing_midi
|
||||
# appropriately.
|
||||
def detect_midi(fn):
|
||||
|
||||
fn = fn.lower()
|
||||
|
||||
global playing_midi
|
||||
playing_midi = fn.endswith(".mid") or fn.endswith(".midi")
|
||||
|
||||
|
||||
def music_delay(offset):
|
||||
"""
|
||||
Returns the time left until the current music has been playing for
|
||||
offset seconds. If music is not playing, return None. May return
|
||||
a negative time.
|
||||
"""
|
||||
|
||||
if not mixer_works:
|
||||
return None
|
||||
|
||||
mo = pygame.mixer.music.get_pos()
|
||||
if mo < 0:
|
||||
return None
|
||||
|
||||
mo /= 1000.0
|
||||
|
||||
return offset - mo
|
||||
|
||||
|
||||
|
||||
# def music_start(filename, loops=-1, startpos=0.0):
|
||||
# """
|
||||
# This starts music playing. If a music track is already playing,
|
||||
# stops that track in favor of this one.
|
||||
|
||||
# @param filename: The file that the music will be played from. This
|
||||
# is relative to the game directory, and must be a real file (so it
|
||||
# cannot be stored in an archive.)
|
||||
|
||||
# @param loops: The number of times the music will loop after it
|
||||
# finishes playing. If negative, the music will loop indefinitely.
|
||||
# Please note that even once the song has finished, rollback or load
|
||||
# may cause it to start playing again. So it may not be safe to have
|
||||
# this set to a non-negative value.
|
||||
|
||||
# @param startpos: The number of seconds into the music to start playing.
|
||||
# """
|
||||
|
||||
# if not mixer_works:
|
||||
# return
|
||||
|
||||
# music_stop()
|
||||
# renpy.game.context().scene_lists.music = (filename, loops, startpos)
|
||||
# restore_music()
|
||||
|
||||
|
||||
# def music_stop():
|
||||
# """
|
||||
# Stops the currently playing music track.
|
||||
# """
|
||||
|
||||
# if not mixer_works:
|
||||
# return
|
||||
|
||||
# renpy.game.context().scene_lists.music = None
|
||||
# restore_music()
|
||||
|
||||
|
||||
# The filename of the currently playing piece of music.
|
||||
playing_filename = None
|
||||
|
||||
# The filename of the currently queued piece of music.
|
||||
queued_filename = None
|
||||
|
||||
# True if the music is in the process of fading out, or
|
||||
# False otherwise.
|
||||
fading = False
|
||||
|
||||
def music_update_volume():
|
||||
"""
|
||||
Sets the volume as appropriate for a midi.
|
||||
"""
|
||||
|
||||
if not playing_filename:
|
||||
return
|
||||
|
||||
detect_midi(playing_filename)
|
||||
set_music_volume(master_music_volume)
|
||||
|
||||
def music_interact():
|
||||
"""
|
||||
This is called before each interaction, to update the playing music
|
||||
(if necessary).
|
||||
"""
|
||||
|
||||
# Call the appropriate config function.
|
||||
if renpy.config.music_interact:
|
||||
renpy.config.music_interact()
|
||||
|
||||
def music_end_event():
|
||||
"""
|
||||
This is called by renpy.display.core when a track of music has
|
||||
endend.
|
||||
"""
|
||||
|
||||
if not music_enabled():
|
||||
return
|
||||
|
||||
try:
|
||||
|
||||
# shift the filenames.
|
||||
global playing_filename
|
||||
global queued_filename
|
||||
global fading
|
||||
|
||||
playing_filename = queued_filename
|
||||
queued_filename = None
|
||||
fading = False
|
||||
|
||||
music_update_volume()
|
||||
|
||||
# Call the appropriate function.
|
||||
if renpy.config.music_end_event:
|
||||
renpy.config.music_end_event()
|
||||
|
||||
except:
|
||||
if renpy.config.debug_sound:
|
||||
raise
|
||||
|
||||
def music_enabled():
|
||||
"""
|
||||
This should be called to check to see if music is enabled. If this
|
||||
returns False, then no music call should be made. Please note that
|
||||
this does not check preferences.music_enabled, so user code should
|
||||
also check that to see if music should be played.
|
||||
|
||||
This will return True if the mixer works and it has not been
|
||||
pre-empted by the video player, and False otherwise.
|
||||
|
||||
If this does not return True, none of the other music functions should
|
||||
be called.
|
||||
"""
|
||||
|
||||
return mixer_works and mixer_enabled
|
||||
|
||||
def music_play(filename):
|
||||
"""
|
||||
This causes the named music filename to be loaded in and played.
|
||||
Music loaded in this way immediately replaces the currently
|
||||
playing music.
|
||||
|
||||
The track is played once, and then stops. It's up to the higher-level
|
||||
music layer to ensure that a track that needs to be looped actually
|
||||
is.
|
||||
|
||||
The filename must refer to a real file, and not a file hidden in an
|
||||
archive.
|
||||
"""
|
||||
|
||||
if not music_enabled():
|
||||
return
|
||||
|
||||
try:
|
||||
|
||||
global playing_filename
|
||||
global queued_filename
|
||||
global fading
|
||||
|
||||
playing_filename = filename
|
||||
queued_filename = None
|
||||
fading = False
|
||||
|
||||
pygame.mixer.music.load(renpy.loader.transfn(filename))
|
||||
|
||||
if not pygame.mixer.music.get_busy():
|
||||
pygame.mixer.music.play()
|
||||
|
||||
music_update_volume()
|
||||
|
||||
except:
|
||||
if renpy.config.debug_sound:
|
||||
raise
|
||||
|
||||
def music_queue(filename):
|
||||
"""
|
||||
This causes the given filename to be placed into the queue, to be
|
||||
played immediately after the currently playing track finishes.
|
||||
|
||||
The filename must refer to a real file, and not a file hidden in an
|
||||
archive.
|
||||
"""
|
||||
|
||||
if not music_enabled():
|
||||
return
|
||||
|
||||
try:
|
||||
global queued_filename
|
||||
queued_filename = filename
|
||||
|
||||
pygame.mixer.music.queue(renpy.loader.transfn(filename))
|
||||
|
||||
|
||||
except:
|
||||
if renpy.config.debug_sound:
|
||||
raise
|
||||
|
||||
def music_stop():
|
||||
"""
|
||||
This causes the music to be stopped immediately.
|
||||
"""
|
||||
|
||||
if not music_enabled():
|
||||
return
|
||||
|
||||
try:
|
||||
|
||||
global playing_filename
|
||||
global queued_filename
|
||||
global fading
|
||||
|
||||
playing_filename = None
|
||||
queued_filename = None
|
||||
fading = False
|
||||
|
||||
pygame.mixer.music.stop()
|
||||
|
||||
except:
|
||||
if renpy.config.debug_sound:
|
||||
raise
|
||||
|
||||
def music_fadeout(seconds):
|
||||
"""
|
||||
This causes the music to be faded out over a period of
|
||||
time.
|
||||
"""
|
||||
|
||||
if not music_enabled():
|
||||
return
|
||||
|
||||
try:
|
||||
global queued_filename
|
||||
global fading
|
||||
|
||||
queued_filename = None
|
||||
fading = True
|
||||
|
||||
pygame.mixer.music.fadeout(int(1000 * seconds))
|
||||
|
||||
except:
|
||||
if renpy.config.debug_sound:
|
||||
raise
|
||||
|
||||
def music_filenames():
|
||||
"""
|
||||
Returns a tuple giving the currently playing music filename and
|
||||
the filename of the track in the queue. It returns None if there
|
||||
is no filename in either slot.
|
||||
"""
|
||||
|
||||
if not music_enabled():
|
||||
return None, None
|
||||
|
||||
try:
|
||||
|
||||
return playing_filename, queued_filename
|
||||
|
||||
except:
|
||||
if renpy.config.debug_sound:
|
||||
raise
|
||||
else:
|
||||
return None, None
|
||||
|
||||
def music_fading():
|
||||
"""
|
||||
Returns True if the music is in the process of fading out, or False
|
||||
otherwise.
|
||||
"""
|
||||
|
||||
return fading
|
||||
|
||||
def music_pause():
|
||||
"""
|
||||
Causes the currently playing music to be paused.
|
||||
"""
|
||||
|
||||
if not music_enabled():
|
||||
return
|
||||
|
||||
try:
|
||||
|
||||
pygame.mixer.music.pause()
|
||||
|
||||
except:
|
||||
if renpy.config.debug_sound:
|
||||
raise
|
||||
|
||||
def music_unpause():
|
||||
"""
|
||||
Causes the currently playing music to be unpaused, if it is
|
||||
currently paused.
|
||||
"""
|
||||
|
||||
if not music_enabled():
|
||||
return
|
||||
|
||||
try:
|
||||
|
||||
pygame.mixer.music.unpause()
|
||||
|
||||
except:
|
||||
if renpy.config.debug_sound:
|
||||
raise
|
||||
|
||||
def sound_enabled():
|
||||
"""
|
||||
Returns True if it's possible to play sound, or False if sound
|
||||
should not be played.
|
||||
|
||||
Please note that this does not check preferences.sound. It's up
|
||||
to higher-level code (like renpy.play) to do that.
|
||||
"""
|
||||
|
||||
return mixer_works and mixer_enabled
|
||||
|
||||
def sound_play(filename, loops=0, channel=0):
|
||||
"""
|
||||
This causes the sound contained in the given filename to be played.
|
||||
|
||||
@param loops: The number of extra times that the sound will be
|
||||
played. If -1, the sound is played forever (until stopped).
|
||||
|
||||
@param channel: The channel that the sound will be played on, an
|
||||
integer from 0 to 7. This allows us to support playing up to 8
|
||||
sounds at once.
|
||||
"""
|
||||
|
||||
if not sound_enabled():
|
||||
return
|
||||
|
||||
try:
|
||||
chan = pygame.mixer.Channel(channel)
|
||||
chan.play(pygame.mixer.Sound(renpy.loader.load(filename)), loops)
|
||||
except:
|
||||
if renpy.config.debug_sound:
|
||||
raise
|
||||
|
||||
|
||||
def sound_stop(channel=0):
|
||||
"""
|
||||
This causes the sound currently playing in the specified channel
|
||||
to be stopped.
|
||||
"""
|
||||
|
||||
if not music_enabled():
|
||||
return
|
||||
|
||||
try:
|
||||
chan = pygame.mixer.Channel(channel)
|
||||
chan.stop()
|
||||
except:
|
||||
if renpy.config.debug_sound:
|
||||
raise
|
||||
|
||||
def play(fn, loops=0):
|
||||
"""
|
||||
This plays the given sound. The sound must be in a wav file,
|
||||
and expected to have a sample rate 44100hz (changeable with
|
||||
config.sound_sample_rate), 16 bit, stereo. These expectations may
|
||||
be violated, but that may lead to conversion delays.
|
||||
|
||||
Once a sound has been started, there's no way to stop it.
|
||||
|
||||
@param fn: The name of the file that the sound is read from. This
|
||||
file may be contained in a game directory or an archive.
|
||||
|
||||
@param loops: The number of extra times the sound will be
|
||||
played. (The default, 0, will play the sound once.)
|
||||
|
||||
This plays the sound on channel 0.
|
||||
"""
|
||||
|
||||
if not fn:
|
||||
return
|
||||
|
||||
if not renpy.game.preferences.sound:
|
||||
return
|
||||
|
||||
sound_play(fn, loops=loops)
|
||||
|
||||
+50
-291
@@ -111,35 +111,6 @@ class Keymap(renpy.display.layout.Null):
|
||||
# def render(self, width, height, st):
|
||||
# return None
|
||||
|
||||
class KeymouseBehavior(renpy.display.layout.Null):
|
||||
"""
|
||||
This is a class that causes the keyboard to move the mouse. It's
|
||||
useful on the game and key menus, as well as in imagemaps and the
|
||||
like.
|
||||
"""
|
||||
|
||||
def event(self, ev, x, y):
|
||||
if ev.type == renpy.display.core.DISPLAYTIME:
|
||||
|
||||
pressed = pygame.key.get_pressed()
|
||||
|
||||
x, y = pygame.mouse.get_pos()
|
||||
ox, oy = x, y
|
||||
|
||||
if is_pressed(pressed, "keymouse_left"):
|
||||
x -= renpy.config.keymouse_distance
|
||||
if is_pressed(pressed, "keymouse_right"):
|
||||
x += renpy.config.keymouse_distance
|
||||
if is_pressed(pressed, "keymouse_up"):
|
||||
y -= renpy.config.keymouse_distance
|
||||
if is_pressed(pressed, "keymouse_down"):
|
||||
y += renpy.config.keymouse_distance
|
||||
|
||||
if (x, y) != (ox, oy):
|
||||
pygame.mouse.set_pos((x, y))
|
||||
|
||||
return None
|
||||
|
||||
class PauseBehavior(renpy.display.layout.Null):
|
||||
"""
|
||||
This is a class implementing the Pause behavior, which is to
|
||||
@@ -168,10 +139,11 @@ class SayBehavior(renpy.display.layout.Null):
|
||||
mouse button.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(SayBehavior, self).__init__()
|
||||
|
||||
focusable = True
|
||||
|
||||
def __init__(self, default=True, **properties):
|
||||
super(SayBehavior, self).__init__(default=default, **properties)
|
||||
|
||||
def event(self, ev, x, y):
|
||||
|
||||
if ev.type == renpy.display.core.DISPLAYTIME and \
|
||||
@@ -183,8 +155,7 @@ class SayBehavior(renpy.display.layout.Null):
|
||||
elif renpy.game.context().seen_current(True):
|
||||
return True
|
||||
|
||||
|
||||
if map_event(ev, "dismiss"):
|
||||
if map_event(ev, "dismiss") and self.is_focused():
|
||||
return True
|
||||
|
||||
if map_event(ev, "rollforward"):
|
||||
@@ -192,211 +163,70 @@ class SayBehavior(renpy.display.layout.Null):
|
||||
return True
|
||||
|
||||
return None
|
||||
|
||||
class Menu(renpy.display.layout.VBox):
|
||||
|
||||
def __init__(self, menuitems, style='menu', **properties):
|
||||
"""
|
||||
@param menuitems: A list of menuitem tuples. The first element
|
||||
of each tuple is the string that should be displayed to the
|
||||
user. The second item is the value that should be returned if
|
||||
this item is selected, or None to indicate that this item is a
|
||||
caption.
|
||||
"""
|
||||
|
||||
super(Menu, self).__init__(style=style, **properties)
|
||||
|
||||
self.selected = None
|
||||
self.results = [ ]
|
||||
|
||||
# self.caption_style = renpy.style.Style('menu_caption', { })
|
||||
# self.selected_style = renpy.style.Style('menu_choice', { })
|
||||
# self.unselected_style = renpy.style.Style('menu_choice', { })
|
||||
|
||||
# self.selected_style.set_prefix('hover_')
|
||||
# self.unselected_style.set_prefix('idle_')
|
||||
|
||||
for i, (caption, result) in enumerate(menuitems):
|
||||
|
||||
if result is not None:
|
||||
style = 'menu_choice'
|
||||
else:
|
||||
style = 'menu_caption'
|
||||
|
||||
self.add(renpy.display.text.Text(caption, style=style))
|
||||
|
||||
if self.selected is None and result is not None:
|
||||
self.selected = i
|
||||
|
||||
self.results.append(result)
|
||||
|
||||
self.update_styles()
|
||||
|
||||
def update_styles(self):
|
||||
"""
|
||||
This updates the colors of our children to reflect the
|
||||
one that has been selected by the user.
|
||||
"""
|
||||
|
||||
for i, (child, result) in enumerate(zip(self.children, self.results)):
|
||||
|
||||
# Captions should stay the default text color.
|
||||
if result is None:
|
||||
continue
|
||||
|
||||
# Actual choices change color if they are selected or not.
|
||||
if i == self.selected:
|
||||
child.set_style_prefix('hover_')
|
||||
else:
|
||||
child.set_style_prefix('idle_')
|
||||
|
||||
|
||||
def event(self, ev, x, y):
|
||||
"""
|
||||
Processes events.
|
||||
"""
|
||||
|
||||
# print ev
|
||||
# print x, y
|
||||
|
||||
old_selected = self.selected
|
||||
mouse_select = False
|
||||
|
||||
# Change selection based on mouse position.
|
||||
if ev.type == MOUSEMOTION or map_event(ev, "menu_mouseselect"):
|
||||
target = self.child_at_point(x, y)
|
||||
if target is None:
|
||||
return None
|
||||
|
||||
if self.results[target] is not None:
|
||||
self.selected = target
|
||||
mouse_select = True
|
||||
|
||||
# Change selection based on keypress.
|
||||
if map_event(ev, "menu_keydown"):
|
||||
|
||||
selected = self.selected
|
||||
|
||||
while selected < len(self.results) - 1:
|
||||
selected += 1
|
||||
if self.results[selected] is not None:
|
||||
self.selected = selected
|
||||
break
|
||||
|
||||
# Change selection based on keypress.
|
||||
if map_event(ev, "menu_keyup"):
|
||||
|
||||
selected = self.selected
|
||||
|
||||
while selected > 0:
|
||||
selected -= 1
|
||||
if self.results[selected] is not None:
|
||||
self.selected = selected
|
||||
break
|
||||
|
||||
# If the selected item changed, update the display.
|
||||
if self.selected != old_selected:
|
||||
|
||||
self.children[self.selected].set_style_prefix("hover_")
|
||||
self.children[old_selected].set_style_prefix("idle_")
|
||||
|
||||
renpy.display.audio.play(self.style.hover_sound)
|
||||
# renpy.display.render.redraw(self, 0)
|
||||
|
||||
# Make selection based on keypress or mouse click.
|
||||
if map_event(ev, "menu_keyselect") or \
|
||||
(mouse_select and map_event(ev, "menu_mouseselect")):
|
||||
|
||||
self.children[self.selected].set_style_prefix("activate_")
|
||||
|
||||
renpy.display.audio.play(self.style.activate_sound)
|
||||
return self.results[self.selected]
|
||||
|
||||
return None
|
||||
|
||||
class Button(renpy.display.layout.Window):
|
||||
|
||||
|
||||
def __init__(self, child, style='button', clicked=None,
|
||||
hovered=None, **properties):
|
||||
|
||||
super(Button, self).__init__(child, style=style, **properties)
|
||||
self.style.set_prefix('idle_')
|
||||
|
||||
self.activated = False
|
||||
|
||||
self.old_hover = False
|
||||
self.clicked = clicked
|
||||
self.hovered = hovered
|
||||
self.focusable = clicked is not None
|
||||
|
||||
def render(self, width, height, st):
|
||||
|
||||
if self.activated:
|
||||
self.set_style_prefix('activate_')
|
||||
elif self.old_hover:
|
||||
self.set_style_prefix('hover_')
|
||||
else:
|
||||
self.set_style_prefix('idle_')
|
||||
rv = super(Button, self).render(width, height, st)
|
||||
|
||||
if self.clicked:
|
||||
rv.add_focus(self,
|
||||
None,
|
||||
self.style.left_margin,
|
||||
self.style.top_margin,
|
||||
rv.width - self.style.right_margin,
|
||||
rv.height - self.style.bottom_margin)
|
||||
|
||||
return rv
|
||||
|
||||
return super(Button, self).render(width, height, st)
|
||||
|
||||
def set_hover(self, hover):
|
||||
"""
|
||||
Called when we change from hovered to un-hovered, or
|
||||
vice-versa.
|
||||
"""
|
||||
|
||||
# if hover:
|
||||
# self.style.set_prefix('hover_')
|
||||
# else:
|
||||
# self.style.set_prefix('idle_')
|
||||
|
||||
renpy.display.render.redraw(self, 0)
|
||||
|
||||
def event(self, ev, x, y):
|
||||
|
||||
# We deactivate on an event.
|
||||
self.activated = False
|
||||
if self.activated:
|
||||
self.activated = False
|
||||
|
||||
inside = False
|
||||
|
||||
width, height = self.window_size
|
||||
|
||||
if x >= 0 and x < width and y >= 0 and y < height:
|
||||
inside = True
|
||||
|
||||
if self.style.enable_hover and ev.type == MOUSEMOTION:
|
||||
|
||||
if self.old_hover != inside:
|
||||
self.old_hover = inside
|
||||
self.set_hover(inside)
|
||||
|
||||
if inside:
|
||||
if self.hovered:
|
||||
self.hovered()
|
||||
|
||||
renpy.display.audio.play(self.style.hover_sound)
|
||||
|
||||
|
||||
if map_event(ev, "button_select"):
|
||||
if inside and self.clicked:
|
||||
renpy.display.audio.play(self.style.activate_sound)
|
||||
|
||||
self.activated = True
|
||||
renpy.display.render.redraw(self, 0)
|
||||
|
||||
rv = self.clicked()
|
||||
|
||||
if rv is not None:
|
||||
return rv
|
||||
if self.focusable:
|
||||
if self.is_focused():
|
||||
self.set_style_prefix('hover_')
|
||||
else:
|
||||
raise renpy.display.core.IgnoreEvent()
|
||||
self.set_style_prefix('idle_')
|
||||
else:
|
||||
self.set_style_prefix('insensitive_')
|
||||
|
||||
# If not focused, ignore all events.
|
||||
if not self.is_focused():
|
||||
return None
|
||||
|
||||
# If clicked,
|
||||
if map_event(ev, "button_select") and self.clicked:
|
||||
|
||||
self.activated = True
|
||||
|
||||
self.set_style_prefix('activate_')
|
||||
renpy.display.audio.play(self.style.sound)
|
||||
|
||||
rv = self.clicked()
|
||||
|
||||
if rv is not None:
|
||||
return rv
|
||||
else:
|
||||
raise renpy.display.core.IgnoreEvent()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
return super(Button, self).event(ev, x, y)
|
||||
|
||||
# Reimplementation of the TextButton widget as a Button and a Text
|
||||
# widget.
|
||||
def TextButton(text, style='button', text_style='button_text',
|
||||
@@ -405,29 +235,6 @@ def TextButton(text, style='button', text_style='button_text',
|
||||
text = renpy.display.text.Text(text, style=text_style)
|
||||
return Button(text, style=style, clicked=clicked, **properties)
|
||||
|
||||
|
||||
|
||||
# class TextButton(Button):
|
||||
|
||||
# def __init__(self, text, style='button', text_style='button_text',
|
||||
# clicked=None):
|
||||
|
||||
# self.text_widget = renpy.display.text.Text(text, style=text_style)
|
||||
|
||||
# super(TextButton, self).__init__(self.text_widget,
|
||||
# style=style,
|
||||
# clicked=clicked)
|
||||
|
||||
# self.text_widget.style.set_prefix('idle_')
|
||||
|
||||
# def set_hover(self, hover):
|
||||
# super(TextButton, self).set_hover(hover)
|
||||
|
||||
# if hover:
|
||||
# self.text_widget.style.set_prefix("hover_")
|
||||
# else:
|
||||
# self.text_widget.style.set_prefix("idle_")
|
||||
|
||||
|
||||
class Input(renpy.display.text.Text):
|
||||
"""
|
||||
@@ -457,13 +264,12 @@ class Input(renpy.display.text.Text):
|
||||
self.set_text(self.content.replace("{", "{{") + "_")
|
||||
renpy.display.render.redraw(self, 0)
|
||||
|
||||
|
||||
elif map_event(ev, "input_enter"):
|
||||
return self.content
|
||||
|
||||
elif ev.type == KEYDOWN and ev.unicode:
|
||||
if ord(ev.unicode[0]) < 32:
|
||||
return None
|
||||
return None
|
||||
|
||||
if self.length and len(self.content) >= self.length:
|
||||
raise renpy.display.core.IgnoreEvent()
|
||||
@@ -488,7 +294,7 @@ class Bar(renpy.display.core.Displayable):
|
||||
to clicks on that value.
|
||||
"""
|
||||
|
||||
def __init__(self, width, height, range, value, clicked=None,
|
||||
def __init__(self, width, height, range, value,
|
||||
style='bar', **properties):
|
||||
|
||||
super(Bar, self).__init__()
|
||||
@@ -500,61 +306,14 @@ class Bar(renpy.display.core.Displayable):
|
||||
self.range = range
|
||||
self.value = value
|
||||
|
||||
self.clicked = clicked
|
||||
|
||||
|
||||
def event(self, ev, x, y):
|
||||
|
||||
if not self.clicked:
|
||||
return
|
||||
|
||||
if not map_event(ev, 'bar_click'):
|
||||
return
|
||||
|
||||
if not (0 <= x < self.width and 0 <= y <= self.height):
|
||||
return
|
||||
|
||||
# print x, y
|
||||
|
||||
lgutter = self.style.left_gutter
|
||||
rgutter = self.style.right_gutter
|
||||
|
||||
if x < lgutter:
|
||||
value = 0
|
||||
elif x > self.width - rgutter:
|
||||
value = self.range
|
||||
else:
|
||||
barwidth = self.width - lgutter - rgutter
|
||||
|
||||
# This makes it easier to select 100%.
|
||||
x = x - lgutter
|
||||
x = x + (barwidth / self.range // 2)
|
||||
|
||||
value = x * self.range / barwidth
|
||||
|
||||
value = max(value, 0)
|
||||
|
||||
|
||||
rv = self.clicked(value)
|
||||
|
||||
if rv is not None:
|
||||
return rv
|
||||
else:
|
||||
raise renpy.display.core.IgnoreEvent()
|
||||
|
||||
def render(self, width, height, st):
|
||||
|
||||
width = self.width
|
||||
height = self.height
|
||||
|
||||
# The amount of space taken up by the bars.
|
||||
|
||||
if self.clicked:
|
||||
lgutter = self.style.left_gutter
|
||||
rgutter = self.style.right_gutter
|
||||
else:
|
||||
lgutter = 0
|
||||
rgutter = 0
|
||||
lgutter = 0
|
||||
rgutter = 0
|
||||
|
||||
barwidth = width - lgutter - rgutter
|
||||
|
||||
@@ -587,7 +346,7 @@ class Conditional(renpy.display.layout.Container):
|
||||
self.condition = condition
|
||||
self.null = renpy.display.layout.Null()
|
||||
|
||||
self.state = eval(self.condition, renpy.game.store)
|
||||
self.state = eval(self.condition, vars(renpy.store))
|
||||
|
||||
def render(self, width, height, st):
|
||||
if self.state:
|
||||
@@ -597,7 +356,7 @@ class Conditional(renpy.display.layout.Container):
|
||||
|
||||
def event(self, ev, x, y):
|
||||
|
||||
state = eval(self.condition, renpy.game.store)
|
||||
state = eval(self.condition, vars(renpy.store))
|
||||
|
||||
if state != self.state:
|
||||
renpy.display.render.redraw(self, 0)
|
||||
|
||||
+98
-42
@@ -2,15 +2,16 @@
|
||||
# window.
|
||||
|
||||
import renpy
|
||||
from renpy.display.render import render
|
||||
|
||||
import pygame
|
||||
from pygame.constants import *
|
||||
import os
|
||||
import time
|
||||
import cStringIO
|
||||
|
||||
# KEYREPEATEVENT = USEREVENT + 1
|
||||
DISPLAYTIME = USEREVENT + 2
|
||||
MUSICEND = USEREVENT + 3
|
||||
|
||||
# The number of msec
|
||||
DISPLAYTIME_INTERVAL = 50
|
||||
@@ -33,9 +34,40 @@ class Displayable(renpy.object.Object):
|
||||
their fields.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.style = None
|
||||
self.style_prefix = None
|
||||
focusable = False
|
||||
|
||||
def __init__(self, focus=None, default=False, style='default', **properties):
|
||||
self.style = renpy.style.Style(style, properties)
|
||||
self.style_prefix = 'insensitive_'
|
||||
self.focus_name = focus
|
||||
self.default = default
|
||||
|
||||
def find_focusable(self, callback, focus_name):
|
||||
if self.focusable:
|
||||
callback(self, self.focus_name or focus_name)
|
||||
|
||||
|
||||
def focus(self, default=False):
|
||||
"""
|
||||
Called to indicate that this widget has the focus.
|
||||
"""
|
||||
|
||||
if self.style.enable_hover:
|
||||
self.set_style_prefix("hover_")
|
||||
|
||||
if not default:
|
||||
renpy.display.audio.play(self.style.sound)
|
||||
|
||||
def unfocus(self):
|
||||
"""
|
||||
Called to indicate that this widget has become unfocused.
|
||||
"""
|
||||
|
||||
if self.style.enable_hover:
|
||||
self.set_style_prefix("idle_")
|
||||
|
||||
def is_focused(self):
|
||||
return renpy.game.context().scene_lists.focused is self
|
||||
|
||||
def set_style_prefix(self, prefix):
|
||||
"""
|
||||
@@ -46,9 +78,7 @@ class Displayable(renpy.object.Object):
|
||||
if prefix == self.style_prefix:
|
||||
return
|
||||
|
||||
if self.style:
|
||||
self.style.set_prefix(prefix)
|
||||
|
||||
self.style.set_prefix(prefix)
|
||||
self.style_prefix = prefix
|
||||
renpy.display.render.redraw(self, 0)
|
||||
|
||||
@@ -106,8 +136,8 @@ class Displayable(renpy.object.Object):
|
||||
the images it may want to load.
|
||||
"""
|
||||
|
||||
return
|
||||
|
||||
if self.style and self.style.background:
|
||||
self.style.background.predict(callback)
|
||||
|
||||
def place(self, dest, x, y, width, height, surf):
|
||||
"""
|
||||
@@ -195,6 +225,7 @@ class SceneLists(object):
|
||||
|
||||
@ivar music: Opaque information about the music that is being played.
|
||||
|
||||
@ivar focused: The widget that is currently focused.
|
||||
"""
|
||||
|
||||
def __init__(self, oldsl=None):
|
||||
@@ -211,6 +242,7 @@ class SceneLists(object):
|
||||
self.music = oldsl.music
|
||||
self.sticky_positions = oldsl.sticky_positions.copy()
|
||||
self.movie = oldsl.movie
|
||||
self.focused = oldsl.focused
|
||||
|
||||
else:
|
||||
for i in renpy.config.layers:
|
||||
@@ -219,6 +251,7 @@ class SceneLists(object):
|
||||
self.music = None
|
||||
self.movie = None
|
||||
self.sticky_positions = { }
|
||||
self.focused = None
|
||||
|
||||
def rollback_copy(self):
|
||||
"""
|
||||
@@ -231,6 +264,8 @@ class SceneLists(object):
|
||||
for i in renpy.config.overlay_layers:
|
||||
rv.layers[i] = [ ]
|
||||
|
||||
rv.focused = None
|
||||
|
||||
# rv.master = self.master[:]
|
||||
# rv.transient = self.transient[:]
|
||||
|
||||
@@ -350,7 +385,9 @@ class Display(object):
|
||||
self.fullscreen = renpy.game.preferences.fullscreen
|
||||
fsflag = 0
|
||||
|
||||
if self.fullscreen:
|
||||
fullscreen = self.fullscreen and not os.environ.get('RENPY_DISABLE_FULLSCREEN', False)
|
||||
|
||||
if fullscreen:
|
||||
fsflag = FULLSCREEN
|
||||
|
||||
# The window we display things in.
|
||||
@@ -372,12 +409,12 @@ class Display(object):
|
||||
pygame.display.set_caption(renpy.config.window_title)
|
||||
|
||||
if renpy.config.window_icon:
|
||||
pygame.display.set_icon(renpy.display.image.cache.load_image(renpy.config.window_icon))
|
||||
pygame.display.set_icon(renpy.display.im.load_image(renpy.config.window_icon))
|
||||
|
||||
|
||||
# Load the mouse image, if any.
|
||||
if renpy.config.mouse:
|
||||
self.mouse = renpy.display.image.cache.load_image(renpy.config.mouse)
|
||||
self.mouse = renpy.display.im.load_image(renpy.config.mouse)
|
||||
pygame.mouse.set_visible(False)
|
||||
else:
|
||||
self.mouse = None
|
||||
@@ -461,6 +498,7 @@ class Display(object):
|
||||
|
||||
self.suppress_mouse = suppress_blit
|
||||
|
||||
renpy.display.focus.take_focuses(surftree.focuses)
|
||||
|
||||
def save_screenshot(self, filename):
|
||||
"""
|
||||
@@ -611,12 +649,12 @@ class Interface(object):
|
||||
return rv
|
||||
|
||||
# We load at most one image per wait.
|
||||
if renpy.display.image.cache.needs_preload():
|
||||
ev = pygame.event.poll()
|
||||
if ev.type != NOEVENT:
|
||||
return ev
|
||||
if renpy.display.im.cache.needs_preload():
|
||||
ev = pygame.event.poll()
|
||||
if ev.type != NOEVENT:
|
||||
return ev
|
||||
|
||||
renpy.display.image.cache.preload()
|
||||
renpy.display.im.cache.preload()
|
||||
|
||||
return pygame.event.wait()
|
||||
|
||||
@@ -633,7 +671,7 @@ class Interface(object):
|
||||
root = renpy.display.layout.Fixed()
|
||||
|
||||
for layer in renpy.config.layers:
|
||||
f = renpy.display.layout.Fixed()
|
||||
f = renpy.display.layout.Fixed(focus=layer)
|
||||
f.append_scene_list(scene_lists.layers[layer])
|
||||
|
||||
rv[layer] = f
|
||||
@@ -652,16 +690,21 @@ class Interface(object):
|
||||
|
||||
# These things can be done once per interaction.
|
||||
|
||||
repeat = True
|
||||
try:
|
||||
|
||||
while repeat:
|
||||
repeat, rv = self.interact_core(**kwargs)
|
||||
repeat = True
|
||||
|
||||
while repeat:
|
||||
repeat, rv = self.interact_core(**kwargs)
|
||||
|
||||
# Clean out transient stuff at the end of an interaction.
|
||||
scene_lists = renpy.game.context().scene_lists
|
||||
scene_lists.replace_transient()
|
||||
return rv
|
||||
|
||||
finally:
|
||||
|
||||
# Clean out transient stuff at the end of an interaction.
|
||||
scene_lists = renpy.game.context().scene_lists
|
||||
scene_lists.replace_transient()
|
||||
|
||||
return rv
|
||||
|
||||
|
||||
def interact_core(self,
|
||||
@@ -699,8 +742,14 @@ class Interface(object):
|
||||
|
||||
# frames = 0
|
||||
|
||||
# Update the music, if necessary.
|
||||
for i in pygame.event.get([ MUSICEND ]):
|
||||
renpy.display.audio.music_end_event()
|
||||
|
||||
renpy.display.audio.music_interact()
|
||||
|
||||
# Tick time forward.
|
||||
renpy.display.image.cache.tick()
|
||||
renpy.display.im.cache.tick()
|
||||
|
||||
# Set up key repeats.
|
||||
# pygame.time.set_timer(KEYREPEATEVENT, renpy.config.skip_delay)
|
||||
@@ -711,12 +760,9 @@ class Interface(object):
|
||||
# Clear some events.
|
||||
pygame.event.clear((MOUSEMOTION, DISPLAYTIME,
|
||||
MOUSEBUTTONUP, MOUSEBUTTONDOWN))
|
||||
|
||||
|
||||
# Figure out the scene list we want to show.
|
||||
start_time = time.time()
|
||||
# Figure out the scene list we want to show.
|
||||
scene_lists = renpy.game.context().scene_lists
|
||||
|
||||
|
||||
# Figure out what the overlay layer should look like.
|
||||
for i in renpy.config.overlay_layers:
|
||||
@@ -742,7 +788,15 @@ class Interface(object):
|
||||
|
||||
# Figure out the scene. (All of the layers, and the root.)
|
||||
scene = self.compute_scene(scene_lists)
|
||||
|
||||
|
||||
# If necessary, load all images here.
|
||||
if renpy.config.load_before_transition:
|
||||
for w in scene.itervalues():
|
||||
w.predict(renpy.display.im.cache.get)
|
||||
|
||||
# The start time of transitions.
|
||||
start_time = time.time()
|
||||
|
||||
# The root widget of all of the layers.
|
||||
layers_root = renpy.display.layout.Fixed()
|
||||
|
||||
@@ -784,13 +838,16 @@ class Interface(object):
|
||||
# Okay, from here on we now have a single root widget (root_widget),
|
||||
# which we will try to show to the user.
|
||||
|
||||
# Figure out what should be focused.
|
||||
renpy.display.focus.before_interact(root_widget)
|
||||
|
||||
# Redraw the screen.
|
||||
renpy.display.render.process_redraws()
|
||||
needs_redraw = True
|
||||
|
||||
# Post an event that moves us to the current mouse position.
|
||||
pygame.event.post(pygame.event.Event(MOUSEMOTION,
|
||||
pos=pygame.mouse.get_pos()))
|
||||
# pygame.event.post(pygame.event.Event(MOUSEMOTION,
|
||||
# pos=pygame.mouse.get_pos()))
|
||||
|
||||
# We only want to do prediction once, but we will defer it as
|
||||
# long as possible.
|
||||
@@ -848,14 +905,18 @@ class Interface(object):
|
||||
|
||||
# Predict images, if we haven't done so already.
|
||||
if not did_prediction and not self.event_peek():
|
||||
renpy.game.context().predict(renpy.display.image.cache.preload_image)
|
||||
root_widget.predict(renpy.display.image.cache.preload_image)
|
||||
root_widget.predict(renpy.display.im.cache.preload_image)
|
||||
renpy.game.context().predict(renpy.display.im.cache.preload_image)
|
||||
did_prediction = True
|
||||
|
||||
try:
|
||||
ev = self.event_wait()
|
||||
self.profile_time = time.time()
|
||||
|
||||
# A song just ended.
|
||||
if ev.type == MUSICEND:
|
||||
renpy.display.audio.music_end_event()
|
||||
|
||||
if ev.type == DISPLAYTIME:
|
||||
|
||||
events = 1 + len(pygame.event.get([DISPLAYTIME]))
|
||||
@@ -865,12 +926,6 @@ class Interface(object):
|
||||
ev = pygame.event.Event(DISPLAYTIME, {},
|
||||
duration=(time.time() - start_time))
|
||||
|
||||
# Update the playing music, if necessary.
|
||||
|
||||
# This needs to be here so that we eventually start a
|
||||
# new song at the end of a fadeout.
|
||||
renpy.display.audio.restore_music()
|
||||
|
||||
# Handle skipping.
|
||||
renpy.display.behavior.skipping(ev)
|
||||
|
||||
@@ -890,8 +945,9 @@ class Interface(object):
|
||||
if len(evs):
|
||||
ev = evs[-1]
|
||||
|
||||
# x, y = getattr(ev, 'pos', (0, 0))
|
||||
renpy.display.focus.event_handler(ev)
|
||||
|
||||
# x, y = getattr(ev, 'pos', (0, 0))
|
||||
x, y = pygame.mouse.get_pos()
|
||||
|
||||
rv = root_widget.event(ev, x, y)
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
# This file contains code to manage focus on the display.
|
||||
|
||||
import renpy
|
||||
import pygame
|
||||
from pygame.constants import *
|
||||
|
||||
class Focus(object):
|
||||
|
||||
def __init__(self, widget, arg, x, y, w, h):
|
||||
self.widget = widget
|
||||
self.arg = arg
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.w = w
|
||||
self.h = h
|
||||
|
||||
def __iter__(self):
|
||||
return iter((self.widget, self.arg, self.x, self.y, self.w, self.h))
|
||||
|
||||
|
||||
# Sets the currently focused widget.
|
||||
def set_focused(widget):
|
||||
renpy.game.context().scene_lists.focused = widget
|
||||
|
||||
# Gets the currently focused widget.
|
||||
def get_focused():
|
||||
return renpy.game.context().scene_lists.focused
|
||||
|
||||
|
||||
# The current list of focuses that we know about.
|
||||
focus_list = [ ]
|
||||
|
||||
# This takes in a focus list from the rendering system.
|
||||
def take_focuses(fl):
|
||||
global focus_list
|
||||
focus_list = fl
|
||||
|
||||
# This is called before each interaction. It's purpose is to choose
|
||||
# the widget that is focused, and to mark it as focused and all of
|
||||
# the other widgets as unfocused.
|
||||
|
||||
def before_interact(root):
|
||||
|
||||
# a list of focusable, name tuples.
|
||||
fwn = [ ]
|
||||
|
||||
def callback(f, n):
|
||||
fwn.append((f, n))
|
||||
|
||||
root.find_focusable(callback, None)
|
||||
|
||||
# Assign a full name to each focusable.
|
||||
|
||||
namecount = { }
|
||||
|
||||
for f, n in fwn:
|
||||
serial = namecount.get(n, 0)
|
||||
namecount[n] = serial + 1
|
||||
|
||||
f.full_focus_name = n, serial
|
||||
|
||||
# If there's something with the same full name as the current widget,
|
||||
# it becomes the new current widget.
|
||||
|
||||
current = get_focused()
|
||||
if current is not None:
|
||||
current_name = current.full_focus_name
|
||||
|
||||
for f, n in fwn:
|
||||
if f.full_focus_name == current.full_focus_name:
|
||||
current = f
|
||||
set_focused(f)
|
||||
break
|
||||
else:
|
||||
current = None
|
||||
|
||||
# Otherwise, focus the default widget, or nothing.
|
||||
if current is None:
|
||||
|
||||
for f, n in fwn:
|
||||
if f.default:
|
||||
current = f
|
||||
set_focused(f)
|
||||
break
|
||||
else:
|
||||
set_focused(None)
|
||||
|
||||
|
||||
# Finally, mark the current widget as the focused widget, and
|
||||
# all other widgets as unfocused.
|
||||
for f, n in fwn:
|
||||
if f is current:
|
||||
f.focus(default=True)
|
||||
else:
|
||||
f.unfocus()
|
||||
|
||||
|
||||
|
||||
# This changes the focus to be the widget contained inside the new
|
||||
# focus object.
|
||||
def change_focus(newfocus):
|
||||
|
||||
if newfocus is None:
|
||||
widget = None
|
||||
else:
|
||||
widget = newfocus.widget
|
||||
|
||||
current = get_focused()
|
||||
|
||||
# Nothing to do.
|
||||
if current is widget:
|
||||
return
|
||||
|
||||
if current is not None:
|
||||
current.unfocus()
|
||||
|
||||
current = widget
|
||||
if widget is not None:
|
||||
widget.focus()
|
||||
|
||||
set_focused(current)
|
||||
|
||||
# This handles mouse events, to see if they change the focus.
|
||||
def mouse_handler(ev):
|
||||
x, y = ev.pos
|
||||
|
||||
newfocus = None
|
||||
default = None
|
||||
|
||||
for f in focus_list:
|
||||
|
||||
if f.x is None:
|
||||
default = f
|
||||
continue
|
||||
|
||||
if f.x <= x <= f.x + f.w and f.y <= y <= f.y + f.h:
|
||||
newfocus = f
|
||||
break
|
||||
else:
|
||||
newfocus = default
|
||||
|
||||
change_focus(newfocus)
|
||||
|
||||
|
||||
# This focuses an extreme widget, which is one of the widgets that's
|
||||
# at an edge. To do this, we multiply the x, y, width, and height by
|
||||
# the supplied multiplers, add them all up, and take the focus with
|
||||
# the largest value.
|
||||
def focus_extreme(xmul, ymul, wmul, hmul):
|
||||
|
||||
max_focus = None
|
||||
max_score = -(65536**2)
|
||||
|
||||
for f in focus_list:
|
||||
|
||||
if not f.x:
|
||||
continue
|
||||
|
||||
score = (f.x * xmul +
|
||||
f.y * ymul +
|
||||
f.w * wmul +
|
||||
f.h * hmul)
|
||||
|
||||
if score > max_score:
|
||||
max_score = score
|
||||
max_focus = f
|
||||
|
||||
if max_focus:
|
||||
change_focus(max_focus)
|
||||
|
||||
|
||||
# This calculates the distance between two points, applying
|
||||
# the given fudge factors. The distance is left squared.
|
||||
def points_dist(x0, y0, x1, y1, xfudge, yfudge):
|
||||
return (( x0 - x1 ) * xfudge ) ** 2 + \
|
||||
(( y0 - y1 ) * yfudge ) ** 2
|
||||
|
||||
|
||||
# This computes the distance between two horizontal lines. (So the
|
||||
# distance is either vertical, or has a vertical component to it.)
|
||||
#
|
||||
# The distance is left squared.
|
||||
def horiz_line_dist(ax0, ay0, ax1, ay1, bx0, by0, bx1, by1):
|
||||
|
||||
# The lines overlap in x.
|
||||
if bx0 <= ax0 <= ax1 <= bx1 or \
|
||||
ax0 <= bx0 <= bx1 <= ax1 or \
|
||||
ax0 <= bx0 <= ax1 <= bx1 or \
|
||||
bx0 <= ax0 <= bx1 <= ax1:
|
||||
return (ay0 - by0) ** 2
|
||||
|
||||
# The right end of a is to the left of the left end of b.
|
||||
if ax0 <= ax1 <= bx0 <= bx1:
|
||||
return points_dist(ax1, ay1, bx0, by0, renpy.config.focus_crossrange_penalty, 1.0)
|
||||
|
||||
if bx0 <= bx1 <= ax0 <= ax1:
|
||||
return points_dist(ax0, ay0, bx1, by1, renpy.config.focus_crossrange_penalty, 1.0)
|
||||
|
||||
assert False
|
||||
|
||||
# This computes the distance between two vertical lines. (So the
|
||||
# distance is either hortizontal, or has a horizontal component to it.)
|
||||
#
|
||||
# The distance is left squared.
|
||||
def verti_line_dist(ax0, ay0, ax1, ay1, bx0, by0, bx1, by1):
|
||||
|
||||
# The lines overlap in x.
|
||||
if by0 <= ay0 <= ay1 <= by1 or \
|
||||
ay0 <= by0 <= by1 <= ay1 or \
|
||||
ay0 <= by0 <= ay1 <= by1 or \
|
||||
by0 <= ay0 <= by1 <= ay1:
|
||||
return (ax0 - bx0) ** 2
|
||||
|
||||
# The right end of a is to the left of the left end of b.
|
||||
if ay0 <= ay1 <= by0 <= by1:
|
||||
return points_dist(ax1, ay1, bx0, by0, 1.0, renpy.config.focus_crossrange_penalty)
|
||||
|
||||
if by0 <= by1 <= ay0 <= ay1:
|
||||
return points_dist(ax0, ay0, bx1, by1, 1.0, renpy.config.focus_crossrange_penalty)
|
||||
|
||||
assert False
|
||||
|
||||
|
||||
|
||||
# This focuses the widget that is nearest to the current widget. To
|
||||
# determine nearest, we compute points on the widgets using the
|
||||
# {from,to}_{x,y}off values. We pick the nearest, applying a fudge
|
||||
# multiplier to the distances in each direction, that satisfies
|
||||
# the condition (which is given a Focus object to evaluate).
|
||||
#
|
||||
# If no focus can be found matching the above, we look for one
|
||||
# with an x of None, and make that the focus. Otherwise, we do
|
||||
# nothing.
|
||||
#
|
||||
# If no widget is focused, we pick one and focus it.
|
||||
#
|
||||
# If the current widget has an x of None, we pass things off to
|
||||
# focus_extreme to deal with.
|
||||
def focus_nearest(from_x0, from_y0, from_x1, from_y1,
|
||||
to_x0, to_y0, to_x1, to_y1,
|
||||
line_dist,
|
||||
condition,
|
||||
xmul, ymul, wmul, hmul):
|
||||
|
||||
if not focus_list:
|
||||
return
|
||||
|
||||
# No widget focused.
|
||||
current = get_focused()
|
||||
|
||||
if not current:
|
||||
change_focus(focus_list[0])
|
||||
return
|
||||
|
||||
# Find the current focus.
|
||||
for f in focus_list:
|
||||
if f.widget == current:
|
||||
from_focus = f
|
||||
break
|
||||
else:
|
||||
# If we can't pick something.
|
||||
change_focus(focus_list[0])
|
||||
return
|
||||
|
||||
# If placeless, focus_extreme.
|
||||
if from_focus.x is None:
|
||||
focus_extreme(xmul, ymul, wmul, hmul)
|
||||
return
|
||||
|
||||
fx0 = from_focus.x + from_focus.w * from_x0
|
||||
fy0 = from_focus.y + from_focus.h * from_y0
|
||||
fx1 = from_focus.x + from_focus.w * from_x1
|
||||
fy1 = from_focus.y + from_focus.h * from_y1
|
||||
|
||||
placeless = None
|
||||
new_focus = None
|
||||
|
||||
# a really big number.
|
||||
new_focus_dist = (65536.0 * renpy.config.focus_crossrange_penalty) ** 2
|
||||
|
||||
for f in focus_list:
|
||||
if f is from_focus:
|
||||
continue
|
||||
|
||||
if f.x is None:
|
||||
placeless = f
|
||||
continue
|
||||
|
||||
if not condition(from_focus, f):
|
||||
continue
|
||||
|
||||
tx0 = f.x + f.w * to_x0
|
||||
ty0 = f.y + f.h * to_y0
|
||||
tx1 = f.x + f.w * to_x1
|
||||
ty1 = f.y + f.h * to_y1
|
||||
|
||||
dist = line_dist(fx0, fy0, fx1, fy1,
|
||||
tx0, ty0, tx1, ty1)
|
||||
|
||||
if dist < new_focus_dist:
|
||||
new_focus = f
|
||||
new_focus_dist = dist
|
||||
|
||||
# If we couldn't find anything, try the placeless focus.
|
||||
new_focus = new_focus or placeless
|
||||
|
||||
# If we have something, switch to it.
|
||||
if new_focus:
|
||||
change_focus(new_focus)
|
||||
|
||||
# And, we're done.
|
||||
|
||||
|
||||
|
||||
def key_handler(ev):
|
||||
|
||||
if renpy.display.behavior.map_event(ev, 'focus_right'):
|
||||
focus_nearest(0.9, 0.1, 0.9, 0.9,
|
||||
0.1, 0.1, 0.1, 0.9,
|
||||
verti_line_dist,
|
||||
lambda old, new : old.x + old.w <= new.x,
|
||||
-1, 0, 0, 0)
|
||||
|
||||
if renpy.display.behavior.map_event(ev, 'focus_left'):
|
||||
focus_nearest(0.1, 0.1, 0.1, 0.9,
|
||||
0.9, 0.1, 0.9, 0.9,
|
||||
verti_line_dist,
|
||||
lambda old, new : new.x + new.w <= old.x,
|
||||
1, 0, 1, 0)
|
||||
|
||||
if renpy.display.behavior.map_event(ev, 'focus_up'):
|
||||
focus_nearest(0.1, 0.1, 0.9, 0.1,
|
||||
0.1, 0.9, 0.9, 0.9,
|
||||
horiz_line_dist,
|
||||
lambda old, new : new.y + new.h <= old.y,
|
||||
0, 1, 0, 1)
|
||||
|
||||
if renpy.display.behavior.map_event(ev, 'focus_down'):
|
||||
focus_nearest(0.1, 0.9, 0.9, 0.9,
|
||||
0.1, 0.1, 0.9, 0.1,
|
||||
horiz_line_dist,
|
||||
lambda old, new : old.y + old.h <= new.y,
|
||||
0, -1, 0, 0)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# This handles pygame events that may change focus.
|
||||
def event_handler(ev):
|
||||
|
||||
if ev.type in (MOUSEMOTION, MOUSEBUTTONUP, MOUSEBUTTONDOWN):
|
||||
mouse_handler(ev)
|
||||
|
||||
key_handler(ev)
|
||||
@@ -0,0 +1,576 @@
|
||||
# This file contains the new image code, which includes provisions for
|
||||
# size-based caching and constructing images from operations (like
|
||||
# cropping and scaling).
|
||||
|
||||
import renpy
|
||||
|
||||
import pygame
|
||||
from pygame.constants import *
|
||||
|
||||
# This is an entry in the image cache.
|
||||
class CacheEntry(object):
|
||||
|
||||
def __init__(self, what, surf):
|
||||
|
||||
# The object that is being cached (which needs to be
|
||||
# hashable and comparable).
|
||||
self.what = what
|
||||
|
||||
# The pygame surface corresponding to the cached object.
|
||||
self.surf = surf
|
||||
|
||||
# The size of this image.
|
||||
w, h = surf.get_size()
|
||||
self.size = w * h
|
||||
|
||||
# The time when this cache entry was last used.
|
||||
self.time = 0
|
||||
|
||||
# This is the singleton image cache.
|
||||
class Cache(object):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
# The current arbitrary time. (Increments by one for each
|
||||
# interaction.)
|
||||
self.time = 0
|
||||
|
||||
# A map from Image object to CacheEntry.
|
||||
self.cache = { }
|
||||
|
||||
# A list of Image objects that we want to preload.
|
||||
self.preloads = [ ]
|
||||
|
||||
# False if this is not the first preload in this tick.
|
||||
self.first_preload_in_tick = True
|
||||
|
||||
# The total size of the current generation of images.
|
||||
self.size_of_current_generation = 0
|
||||
|
||||
# The total size of everything in the cache.
|
||||
self.total_cache_size = 0
|
||||
|
||||
# Returns the maximum size of the cache, after which we start
|
||||
# tossing things out.
|
||||
def cache_limit(self):
|
||||
return renpy.config.image_cache_size * renpy.config.screen_width * renpy.config.screen_height
|
||||
|
||||
# Increments time, and clears the list of images to be
|
||||
# preloaded.
|
||||
def tick(self):
|
||||
self.time += 1
|
||||
self.preloads = [ ]
|
||||
self.first_preload_in_tick = True
|
||||
self.size_of_current_generation = 0
|
||||
|
||||
# Called to report that a given image would like to be preloaded.
|
||||
def preload_image(self, image):
|
||||
self.preloads.append(image)
|
||||
|
||||
# Do we need to preload an image?
|
||||
def needs_preload(self):
|
||||
return (self.preloads and True) or self.first_preload_in_tick
|
||||
|
||||
# This returns the pygame surface corresponding to the provided
|
||||
# image. It also takes care of updating the age of images in the
|
||||
# cache to be current, and maintaining the size of the current
|
||||
# generation of images.
|
||||
def get(self, image):
|
||||
|
||||
if not isinstance(image, ImageBase):
|
||||
raise Exception("Expected an image of some sort, but got something else.")
|
||||
|
||||
if image in self.cache:
|
||||
ce = self.cache[image]
|
||||
|
||||
if ce.time == self.time:
|
||||
return ce.surf
|
||||
else:
|
||||
ce = CacheEntry(image, image.load())
|
||||
self.total_cache_size += ce.size
|
||||
self.cache[image] = ce
|
||||
|
||||
# Indicate that this surface had changed.
|
||||
renpy.display.render.mutated_surface(ce.surf)
|
||||
|
||||
if renpy.config.debug_image_cache:
|
||||
print "IC Added", ce.what
|
||||
|
||||
|
||||
# Move it into the current generation.
|
||||
ce.time = self.time
|
||||
self.size_of_current_generation += ce.size
|
||||
|
||||
return ce.surf
|
||||
|
||||
# This kills off a given cache entry.
|
||||
def kill(self, ce):
|
||||
|
||||
# Should never happen... but...
|
||||
if ce.time == self.time:
|
||||
self.size_of_current_generation -= ce.size
|
||||
|
||||
self.total_cache_size -= ce.size
|
||||
del self.cache[ce.what]
|
||||
|
||||
if renpy.config.debug_image_cache:
|
||||
print "IC Removed", ce.what
|
||||
|
||||
# Calling this cleans out the image cache if it has gotten too large.
|
||||
def cleanout(self):
|
||||
cache_limit = self.cache_limit()
|
||||
|
||||
# If we're within the limit, return.
|
||||
if self.total_cache_size <= cache_limit:
|
||||
return
|
||||
|
||||
# If we're outside the cache limit, we need to go and start
|
||||
# killing off some of the entries until we're back inside it.
|
||||
|
||||
# A list of time, cache_entry pairs.
|
||||
ace = [ (ce.time, ce) for ce in self.cache.itervalues() ]
|
||||
ace.sort()
|
||||
|
||||
while ace and self.total_cache_size > cache_limit:
|
||||
|
||||
time, ce = ace.pop(0)
|
||||
|
||||
if time == self.time:
|
||||
# If we're bigger than the limit, and there's nothing
|
||||
# to remove, we should stop the preloading right away.
|
||||
|
||||
self.preloads = [ ]
|
||||
break
|
||||
|
||||
|
||||
# Otherwise, kill off the given cache entry.
|
||||
self.kill(ce)
|
||||
|
||||
if renpy.config.debug_image_cache:
|
||||
print "IC is:", self.cache.keys()
|
||||
print "IC size:", self.total_cache_size, "/", cache_limit
|
||||
|
||||
|
||||
# This actually performs preloading.
|
||||
def preload(self):
|
||||
|
||||
if self.first_preload_in_tick:
|
||||
self.first_preload_in_tick = False
|
||||
|
||||
# Triage into stuff that's already in the cache and should
|
||||
# be kept there, and stuff that isn't there already.
|
||||
|
||||
new_preloads = [ ]
|
||||
|
||||
for i in self.preloads:
|
||||
if i in self.cache:
|
||||
self.get(i)
|
||||
else:
|
||||
new_preloads.append(i)
|
||||
|
||||
self.preloads = new_preloads
|
||||
|
||||
# Clean out the cache.
|
||||
self.cleanout()
|
||||
|
||||
# Return after doing said triage.
|
||||
return
|
||||
|
||||
# Otherwise, new_preloads contains things that aren't in the
|
||||
# cache already. So load one of them into the cache, maybe.
|
||||
|
||||
cache_limit = self.cache_limit()
|
||||
|
||||
# If the size of the current generation is bigger than the
|
||||
# total cache size, stop preloading.
|
||||
if self.size_of_current_generation > cache_limit:
|
||||
self.preloads = [ ]
|
||||
return
|
||||
|
||||
# Otherwise, preload the next image.
|
||||
image = self.preloads.pop(0)
|
||||
self.get(image)
|
||||
|
||||
# And, we're done.
|
||||
self.cleanout()
|
||||
|
||||
cache = Cache()
|
||||
|
||||
|
||||
class ImageBase(renpy.display.core.Displayable):
|
||||
"""
|
||||
This is the base class for all of the various kinds of images that
|
||||
we can possibly have.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **properties):
|
||||
|
||||
if 'style' not in properties:
|
||||
properties = properties.copy()
|
||||
properties['style'] = 'image_placement'
|
||||
|
||||
super(ImageBase, self).__init__(**properties)
|
||||
self.identity = (type(self).__name__, ) + args
|
||||
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.identity)
|
||||
|
||||
def __eq__(self, other):
|
||||
|
||||
if not isinstance(other, ImageBase):
|
||||
return False
|
||||
|
||||
return self.identity == other.identity
|
||||
|
||||
def __repr__(self):
|
||||
return "<" + " ".join([repr(i) for i in self.identity]) + ">"
|
||||
|
||||
def load(self):
|
||||
"""
|
||||
This function is called by the image cache code to cause this
|
||||
image to be loaded. It's expected that children of this class
|
||||
would override this.
|
||||
"""
|
||||
|
||||
assert False
|
||||
|
||||
def render(self, w, h, st):
|
||||
im = cache.get(self)
|
||||
w, h = im.get_size()
|
||||
rv = renpy.display.render.Render(w, h)
|
||||
rv.blit(im, (0, 0))
|
||||
return rv
|
||||
|
||||
def get_placement(self):
|
||||
return self.style
|
||||
|
||||
def predict(self, callback):
|
||||
callback(self)
|
||||
|
||||
class Image(ImageBase):
|
||||
"""
|
||||
This image manipulator loads an image from a file.
|
||||
"""
|
||||
|
||||
def __init__(self, filename, **properties):
|
||||
"""
|
||||
@param filename: The filename that the image will be loaded from.
|
||||
"""
|
||||
|
||||
super(Image, self).__init__(filename, **properties)
|
||||
self.filename = filename
|
||||
|
||||
def load(self):
|
||||
im = pygame.image.load(renpy.loader.load(self.filename), self.filename)
|
||||
|
||||
if im.get_flags() & SRCALPHA:
|
||||
im = im.convert_alpha()
|
||||
else:
|
||||
im = im.convert()
|
||||
|
||||
return im
|
||||
|
||||
class Composite(ImageBase):
|
||||
"""
|
||||
This image manipulator composites one or more images together.
|
||||
"""
|
||||
|
||||
def __init__(self, size, *args, **properties):
|
||||
"""
|
||||
The first argument that this takes is size, which is either the
|
||||
desired size of the image (in pixels), or None to indicate that
|
||||
the size should be
|
||||
|
||||
This takes an even number of position arguments. Odd numbered
|
||||
(starting the count with 1) arguments are positions, which
|
||||
give the position of the image, in pixels, with the origin in
|
||||
the upper-left corner of the image. The even-numbered
|
||||
arguments give the images (image manipulators) that will be
|
||||
composited in those positions. The images are composited in
|
||||
bottom-to-top order.
|
||||
|
||||
@param size: If given, this will be the size of the new
|
||||
image. Otherwise, the size will be the same as that of the
|
||||
first image.
|
||||
"""
|
||||
|
||||
super(Composite, self).__init__(size, *args, **properties)
|
||||
|
||||
if len(args) % 2 != 0:
|
||||
raise Exception("Composite requires an even number of arguments.")
|
||||
|
||||
self.size = size
|
||||
self.positions = args[0::2]
|
||||
self.images = [ image(i) for i in args[1::2] ]
|
||||
|
||||
def load(self):
|
||||
|
||||
if self.size:
|
||||
size = self.size
|
||||
else:
|
||||
size = cache.get(self.images[0]).get_size()
|
||||
|
||||
rv = pygame.Surface(size, 0,
|
||||
renpy.game.interface.display.sample_surface)
|
||||
|
||||
for pos, im in zip(self.positions, self.images):
|
||||
rv.blit(cache.get(im), pos)
|
||||
|
||||
return rv
|
||||
|
||||
class FrameImage(ImageBase):
|
||||
"""
|
||||
This is an image that implements a frame with a given size. Instances
|
||||
of this are used by the frame object to return a new frame when
|
||||
such a new frame is needed.
|
||||
"""
|
||||
|
||||
def __init__(self, im, xborder, yborder, width, height):
|
||||
"""
|
||||
@param image: The image that will be used as the base of this
|
||||
frame.
|
||||
|
||||
@param xborder: The number of pixels in the x direction to use as
|
||||
a border.
|
||||
|
||||
@param yborder: The number of pixels in the y direction to use as
|
||||
a border.
|
||||
|
||||
@param width: The width we are being rendered at.
|
||||
|
||||
@param height: The height we are being rendered at.
|
||||
"""
|
||||
|
||||
im = image(im)
|
||||
|
||||
super(FrameImage, self).__init__(im, xborder, yborder, width, height)
|
||||
|
||||
self.image = im
|
||||
self.xborder = xborder
|
||||
self.yborder = yborder
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
def load(self):
|
||||
|
||||
dw = self.width
|
||||
dh = self.height
|
||||
|
||||
dest = pygame.Surface((dw, dh), 0,
|
||||
renpy.game.interface.display.sample_surface)
|
||||
|
||||
source = cache.get(self.image)
|
||||
sw, sh = source.get_size()
|
||||
|
||||
def draw(x0, x1, y0, y1):
|
||||
|
||||
# Quick exit.
|
||||
if x0 == x1 or y0 == y1:
|
||||
return
|
||||
|
||||
# Compute the coordinates of the left, right, top, and
|
||||
# bottom sides of the region, for both the source and
|
||||
# destination surfaces.
|
||||
|
||||
# left side.
|
||||
if x0 >= 0:
|
||||
dx0 = x0
|
||||
sx0 = x0
|
||||
else:
|
||||
dx0 = dw + x0
|
||||
sx0 = sw + x0
|
||||
|
||||
# right side.
|
||||
if x1 > 0:
|
||||
dx1 = x1
|
||||
sx1 = x1
|
||||
else:
|
||||
dx1 = dw + x1
|
||||
sx1 = sw + x1
|
||||
|
||||
# top side.
|
||||
if y0 >= 0:
|
||||
dy0 = y0
|
||||
sy0 = y0
|
||||
else:
|
||||
dy0 = dh + y0
|
||||
sy0 = sh + y0
|
||||
|
||||
# bottom side
|
||||
if y1 > 0:
|
||||
dy1 = y1
|
||||
sy1 = y1
|
||||
else:
|
||||
dy1 = dh + y1
|
||||
sy1 = sh + y1
|
||||
|
||||
# Compute sizes.
|
||||
srcsize = (sx1 - sx0, sy1 - sy0)
|
||||
dstsize = (dx1 - dx0, dy1 - dy0)
|
||||
|
||||
# Get a subsurface.
|
||||
surf = source.subsurface((sx0, sy0, srcsize[0], srcsize[1]))
|
||||
|
||||
# Scale if we have to.
|
||||
if dstsize != srcsize:
|
||||
surf = pygame.transform.scale(surf, dstsize)
|
||||
|
||||
# Blit.
|
||||
dest.blit(surf, (dx0, dy0))
|
||||
|
||||
xb = self.xborder
|
||||
yb = self.yborder
|
||||
|
||||
# Top row.
|
||||
draw(0, xb, 0, yb)
|
||||
draw(xb, -xb, 0, yb)
|
||||
draw(-xb, 0, 0, yb)
|
||||
|
||||
# Middle row.
|
||||
draw(0, xb, yb, -yb)
|
||||
draw(xb, -xb, yb, -yb)
|
||||
draw(-xb, 0, yb, -yb)
|
||||
|
||||
# Bottom row.
|
||||
draw(0, xb, -yb, 0)
|
||||
draw(xb, -xb, -yb, 0)
|
||||
draw(-xb, 0, -yb, 0)
|
||||
|
||||
# And, finish up.
|
||||
return dest
|
||||
|
||||
class SolidImage(ImageBase):
|
||||
"""
|
||||
This is an image that is a solid rectangle with a given size. It's
|
||||
used to implement Solid.
|
||||
"""
|
||||
|
||||
def __init__(self, color, width, height):
|
||||
super(SolidImage, self).__init__(color, width, height)
|
||||
self.color = color
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
def load(self):
|
||||
|
||||
rv = pygame.Surface((self.width, self.height), 0,
|
||||
renpy.game.interface.display.sample_surface)
|
||||
rv.fill(self.color)
|
||||
|
||||
return rv
|
||||
|
||||
class Scale(ImageBase):
|
||||
"""
|
||||
This is an image manipulator that scales another image manipulator
|
||||
to the specified width and height. This scalling is unfiltered, so
|
||||
you can expect your image to look a bit jagged.
|
||||
"""
|
||||
|
||||
def __init__(self, im, width, height):
|
||||
|
||||
im = image(im)
|
||||
super(Scale, self).__init__(im, width, height)
|
||||
|
||||
self.image = im
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
def load(self):
|
||||
return pygame.transform.scale(cache.get(self.image),
|
||||
(self.width, self.height))
|
||||
|
||||
class Rotozoom(ImageBase):
|
||||
"""
|
||||
This is an image manipulator that is a smooth rotation and zoom of another image manipulator.
|
||||
"""
|
||||
|
||||
def __init__(self, im, angle, zoom):
|
||||
"""
|
||||
@param im: The image to be rotozoomed.
|
||||
|
||||
@param angle: The number of degrees counterclockwise the image is
|
||||
to be rotated.
|
||||
|
||||
@param zoom: The zoom factor. Numbers that are greater than 1.0
|
||||
lead to the image becoming larger.
|
||||
"""
|
||||
|
||||
im = image(im)
|
||||
super(Rotozoom, self).__init__(im, angle, zoom)
|
||||
|
||||
self.image = im
|
||||
self.angle = angle
|
||||
self.zoom = zoom
|
||||
|
||||
def load(self):
|
||||
|
||||
return pygame.transform.rotozoom(cache.get(self.image),
|
||||
self.angle, self.zoom)
|
||||
|
||||
|
||||
class Crop(ImageBase):
|
||||
"""
|
||||
This crops the image that is its child.
|
||||
"""
|
||||
|
||||
def __init__(self, im, x, y, w, h):
|
||||
|
||||
im = image(im)
|
||||
|
||||
super(Crop, self).__init__(im, x, y, w, h)
|
||||
|
||||
self.image = im
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.w = w
|
||||
self.h = h
|
||||
|
||||
def load(self):
|
||||
return cache.get(self.image).subsurface((self.x, self.y,
|
||||
self.w, self.h))
|
||||
|
||||
|
||||
def image(arg, **properties):
|
||||
"""
|
||||
This takes as input one of a number of ways of specifying an
|
||||
image, and returns the Displayable image object that has been so
|
||||
specified. Specifically, this can take as input:
|
||||
|
||||
<ul>
|
||||
<li> An image object. In that case, it's returned unchanged.</li>
|
||||
<li> A string. If a string is given, then the string is interpreted
|
||||
as a filename, and what is returned is an im.Image object, which
|
||||
loads the image from disk.</li>
|
||||
<li> A tuple. If this is the case, then what is returned is an
|
||||
im.Composite object, which aligns the upper-left corner of all
|
||||
of the images supplied as arguments. </li>
|
||||
</ul>
|
||||
"""
|
||||
|
||||
if isinstance(arg, ImageBase):
|
||||
return arg
|
||||
|
||||
elif isinstance(arg, basestring):
|
||||
return Image(arg, **properties)
|
||||
|
||||
elif isinstance(arg, tuple):
|
||||
params = [ ]
|
||||
|
||||
for i in arg:
|
||||
params.append((0, 0))
|
||||
params.append(i)
|
||||
|
||||
return Composite(None, *params)
|
||||
|
||||
elif isinstance(arg, renpy.display.core.Displayable):
|
||||
raise Exception("Expected an image, but got a general displayable.")
|
||||
else:
|
||||
raise Exception("Could not construct image from argument.")
|
||||
|
||||
def load_image(fn):
|
||||
"""
|
||||
This loads an image from the given filename, using the cache.
|
||||
"""
|
||||
|
||||
return cache.get(image(fn))
|
||||
+43
-398
@@ -1,209 +1,14 @@
|
||||
# This file contains some miscellanious displayables that involve images.
|
||||
# Most of the guts of this file have been moved into im.py, with only some
|
||||
# of the stuff thar uses images remaining.
|
||||
|
||||
import renpy
|
||||
from renpy.display.render import render
|
||||
|
||||
import pygame
|
||||
from pygame.constants import *
|
||||
|
||||
class ImageCache(object):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
# A monotonically increasing time.
|
||||
self.time = 0
|
||||
|
||||
# A map from image filename to surface.
|
||||
self.surface_map = { }
|
||||
|
||||
# A map from image filename to last access time.
|
||||
self.time_map = { }
|
||||
|
||||
# The list of things we want to preload.
|
||||
self.preloads = [ ]
|
||||
|
||||
|
||||
def tick(self):
|
||||
self.time += 1
|
||||
self.preloads = [ ]
|
||||
|
||||
def really_load_image(self, fn):
|
||||
"""
|
||||
This is called by load_image, and does the actual loading of
|
||||
images. This may be a load of an image, or perhaps the
|
||||
compositing of images if fn is a tuple.
|
||||
"""
|
||||
|
||||
# If fn is not a single filename but a tuple, we composite the
|
||||
# elements of the tuple.
|
||||
if isinstance(fn, tuple):
|
||||
if not tuple:
|
||||
raise Exception("Trying to create a composite image from an empty tuple.")
|
||||
|
||||
base = self.load_image(fn[0])
|
||||
|
||||
rv = pygame.Surface(base.get_size(), 0,
|
||||
renpy.game.interface.display.sample_surface)
|
||||
|
||||
rv.blit(base, (0, 0))
|
||||
|
||||
for i in fn[1:]:
|
||||
layer = self.load_image(i)
|
||||
rv.blit(layer, (0, 0))
|
||||
|
||||
renpy.display.render.mutated_surface(rv)
|
||||
|
||||
return rv
|
||||
|
||||
im = pygame.image.load(renpy.loader.load(fn), fn)
|
||||
|
||||
if im.get_flags() & SRCALPHA:
|
||||
im = im.convert_alpha()
|
||||
else:
|
||||
im = im.convert()
|
||||
|
||||
renpy.display.render.mutated_surface(im)
|
||||
|
||||
return im
|
||||
|
||||
|
||||
# Forces an image load, regardless of if the cache is full or not.
|
||||
def load_image(self, fn):
|
||||
self.time_map[fn] = self.time
|
||||
|
||||
if fn in self.surface_map:
|
||||
return self.surface_map[fn]
|
||||
|
||||
if fn in self.preloads:
|
||||
self.preloads.remove(fn)
|
||||
|
||||
# iw, ih = im.get_size()
|
||||
|
||||
# surf = renpy.display.render.Render(iw, ih)
|
||||
# surf.blit(im, (0, 0))
|
||||
|
||||
im = self.really_load_image(fn)
|
||||
|
||||
self.surface_map[fn] = im
|
||||
|
||||
if renpy.config.debug_image_cache:
|
||||
print "Image cache:", self.surface_map.keys()
|
||||
|
||||
return im
|
||||
|
||||
# Queues an image to be preloaded if not already loaded and there's
|
||||
# room in the cache for it.
|
||||
def preload_image(self, fn):
|
||||
|
||||
self.time_map[fn] = self.time
|
||||
|
||||
if fn in self.surface_map:
|
||||
return
|
||||
|
||||
if fn not in self.preloads:
|
||||
self.preloads.append(fn)
|
||||
|
||||
# This tries to ensure that there are n empty spaces in the image
|
||||
# cache. Returns the number of empty spaces that are actually in
|
||||
# the image cache. (A number that may be negative.)
|
||||
def clear_image_cache(self, n):
|
||||
|
||||
rv = renpy.config.image_cache_size - len(self.surface_map)
|
||||
|
||||
if rv >= n:
|
||||
return rv
|
||||
|
||||
# The number of images to remove. (This is the amount we are over
|
||||
# the cache limit + the number of images we have been requested to
|
||||
# pull.)
|
||||
num_to_remove = len(self.surface_map) - renpy.config.image_cache_size + n
|
||||
|
||||
time_files = [ (self.time_map[fn], fn) for fn in self.surface_map ]
|
||||
time_files = [ (time, fn) for time, fn in time_files if time != self.time ]
|
||||
time_files.sort()
|
||||
time_files = time_files[:num_to_remove]
|
||||
|
||||
for time, fn in time_files:
|
||||
del self.surface_map[fn]
|
||||
del self.time_map[fn]
|
||||
|
||||
if renpy.config.debug_image_cache:
|
||||
print "Image cache:", self.surface_map.keys()
|
||||
|
||||
rv = renpy.config.image_cache_size - len(self.surface_map)
|
||||
|
||||
return rv
|
||||
|
||||
def needs_preload(self):
|
||||
"""
|
||||
Returns True if calling preload would do anything.
|
||||
"""
|
||||
|
||||
return self.preloads and True
|
||||
|
||||
def preload(self):
|
||||
|
||||
# If we have nothing to preload, bail early.
|
||||
if not self.preloads:
|
||||
return
|
||||
|
||||
# Try to clear up enough space for the preloads.
|
||||
avail = self.clear_image_cache(len(self.preloads))
|
||||
|
||||
if avail < 0:
|
||||
avail = 0
|
||||
|
||||
self.preloads = self.preloads[:avail]
|
||||
|
||||
# If no space is available, bail here.
|
||||
if not self.preloads:
|
||||
return
|
||||
|
||||
# Get the first thing to preload.
|
||||
fn = self.preloads[0]
|
||||
|
||||
# Actually load the image.
|
||||
try:
|
||||
self.load_image(fn)
|
||||
except:
|
||||
if renpy.config.debug:
|
||||
raise
|
||||
|
||||
cache = ImageCache()
|
||||
|
||||
class Image(renpy.display.core.Displayable):
|
||||
"""
|
||||
Returns a Displayable that is an image that is loaded from a file
|
||||
on disk.
|
||||
"""
|
||||
|
||||
def __init__(self, filename, style='image_placement', **properties):
|
||||
"""
|
||||
@param filename: The filename that the image is loaded
|
||||
from. Many common file formats are supported.
|
||||
|
||||
If the filename is not a single string but instead a tuple of
|
||||
strings, the image is considered to be"layered". In this case,
|
||||
the image will be the size of the first image in the tuple, and
|
||||
other images will be aligned with the upper-left corner of the
|
||||
image.
|
||||
"""
|
||||
|
||||
super(Image, self).__init__()
|
||||
|
||||
self.filename = filename
|
||||
self.style = renpy.style.Style(style, properties)
|
||||
|
||||
def render(self, w, h, st):
|
||||
im = cache.load_image(self.filename)
|
||||
w, h = im.get_size()
|
||||
rv = renpy.display.render.Render(w, h)
|
||||
rv.blit(im, (0, 0))
|
||||
return rv
|
||||
|
||||
def get_placement(self):
|
||||
return self.style
|
||||
|
||||
def predict(self, callback):
|
||||
callback(self.filename)
|
||||
Image = renpy.display.im.image
|
||||
|
||||
class UncachedImage(renpy.display.core.Displayable):
|
||||
"""
|
||||
@@ -265,8 +70,7 @@ class ImageReference(renpy.display.core.Displayable):
|
||||
parameters = [ ]
|
||||
|
||||
def error(msg):
|
||||
self.target = renpy.display.text.Text(msg,
|
||||
color=(255, 0, 0, 255))
|
||||
self.target = renpy.display.text.Text(msg, color=(255, 0, 0, 255))
|
||||
|
||||
if renpy.config.debug:
|
||||
raise Exception(msg)
|
||||
@@ -323,7 +127,8 @@ class Solid(renpy.display.core.Displayable):
|
||||
|
||||
def __init__(self, color):
|
||||
"""
|
||||
@param color: An RGBA tuple, giving the color that the display will be filled with.
|
||||
@param color: An RGBA tuple, giving the color that the display
|
||||
will be filled with.
|
||||
"""
|
||||
|
||||
super(Solid, self).__init__()
|
||||
@@ -331,10 +136,11 @@ class Solid(renpy.display.core.Displayable):
|
||||
|
||||
def render(self, width, height, st):
|
||||
|
||||
rv = renpy.display.render.Render(width, height)
|
||||
rv.fill(self.color)
|
||||
si = renpy.display.im.SolidImage(self.color,
|
||||
width,
|
||||
height)
|
||||
|
||||
return rv
|
||||
return render(si, width, height, st)
|
||||
|
||||
class Frame(renpy.display.core.Displayable):
|
||||
"""
|
||||
@@ -348,11 +154,10 @@ class Frame(renpy.display.core.Displayable):
|
||||
the center of the image is scaled in both x and y directions.
|
||||
"""
|
||||
|
||||
nosave = [ 'cache' ]
|
||||
|
||||
def __init__(self, filename, xborder, yborder):
|
||||
def __init__(self, image, xborder, yborder):
|
||||
"""
|
||||
@param filename: The file that the original image will be read from.
|
||||
@param image: The image (which may be a filename or image
|
||||
object) that will be scaled.
|
||||
|
||||
@param xborder: The number of pixels in the x direction to use as
|
||||
a border.
|
||||
@@ -360,105 +165,29 @@ class Frame(renpy.display.core.Displayable):
|
||||
@param yborder: The number of pixels in the y direction to use as
|
||||
a border.
|
||||
|
||||
For better performance, have the image file share a dimension
|
||||
For better performance, have the image share a dimension
|
||||
length in common with the size the frame will be rendered
|
||||
at. We detect this and avoid scaling if possible.
|
||||
"""
|
||||
|
||||
super(Frame, self).__init__()
|
||||
|
||||
self.filename = filename
|
||||
self.image = Image(image)
|
||||
self.xborder = xborder
|
||||
self.yborder = yborder
|
||||
|
||||
|
||||
def render(self, width, height, st):
|
||||
|
||||
dest = renpy.display.render.Render(width, height)
|
||||
dw, dh = width, height
|
||||
|
||||
source = cache.load_image(self.filename)
|
||||
sw, sh = source.get_size()
|
||||
fi = renpy.display.im.FrameImage(self.image,
|
||||
self.xborder,
|
||||
self.yborder,
|
||||
width,
|
||||
height)
|
||||
|
||||
def draw(x0, x1, y0, y1):
|
||||
|
||||
# Quick exit.
|
||||
if x0 == x1 or y0 == y1:
|
||||
return
|
||||
|
||||
# Compute the coordinates of the left, right, top, and
|
||||
# bottom sides of the region, for both the source and
|
||||
# destination surfaces.
|
||||
|
||||
# left side.
|
||||
if x0 >= 0:
|
||||
dx0 = x0
|
||||
sx0 = x0
|
||||
else:
|
||||
dx0 = dw + x0
|
||||
sx0 = sw + x0
|
||||
|
||||
# right side.
|
||||
if x1 > 0:
|
||||
dx1 = x1
|
||||
sx1 = x1
|
||||
else:
|
||||
dx1 = dw + x1
|
||||
sx1 = sw + x1
|
||||
|
||||
# top side.
|
||||
if y0 >= 0:
|
||||
dy0 = y0
|
||||
sy0 = y0
|
||||
else:
|
||||
dy0 = dh + y0
|
||||
sy0 = sh + y0
|
||||
|
||||
# bottom side
|
||||
if y1 > 0:
|
||||
dy1 = y1
|
||||
sy1 = y1
|
||||
else:
|
||||
dy1 = dh + y1
|
||||
sy1 = sh + y1
|
||||
|
||||
# Compute sizes.
|
||||
srcsize = (sx1 - sx0, sy1 - sy0)
|
||||
dstsize = (dx1 - dx0, dy1 - dy0)
|
||||
|
||||
# Get a subsurface.
|
||||
surf = source.subsurface((sx0, sy0, srcsize[0], srcsize[1]))
|
||||
|
||||
# Scale if we have to.
|
||||
if dstsize != srcsize:
|
||||
surf = pygame.transform.scale(surf, dstsize)
|
||||
|
||||
# Blit.
|
||||
dest.blit(surf, (dx0, dy0))
|
||||
|
||||
xb = self.xborder
|
||||
yb = self.yborder
|
||||
|
||||
# Top row.
|
||||
draw(0, xb, 0, yb)
|
||||
draw(xb, -xb, 0, yb)
|
||||
draw(-xb, 0, 0, yb)
|
||||
|
||||
# Middle row.
|
||||
draw(0, xb, yb, -yb)
|
||||
draw(xb, -xb, yb, -yb)
|
||||
draw(-xb, 0, yb, -yb)
|
||||
|
||||
# Bottom row.
|
||||
draw(0, xb, -yb, 0)
|
||||
draw(xb, -xb, -yb, 0)
|
||||
draw(-xb, 0, -yb, 0)
|
||||
|
||||
# And, finish up.
|
||||
return dest
|
||||
return render(fi, width, height, st)
|
||||
|
||||
def predict(self, callback):
|
||||
callback(self.filename)
|
||||
self.image.predict(callback)
|
||||
|
||||
class Animation(renpy.display.core.Displayable):
|
||||
"""
|
||||
@@ -476,7 +205,7 @@ class Animation(renpy.display.core.Displayable):
|
||||
animation will restart after the final delay time.
|
||||
"""
|
||||
|
||||
super(Animation, self).__init__()
|
||||
super(Animation, self).__init__(style='image_placement')
|
||||
|
||||
self.images = [ ]
|
||||
self.delays = [ ]
|
||||
@@ -484,7 +213,7 @@ class Animation(renpy.display.core.Displayable):
|
||||
for i, arg in enumerate(args):
|
||||
|
||||
if i % 2 == 0:
|
||||
self.images.append(arg)
|
||||
self.images.append(Image(arg))
|
||||
else:
|
||||
self.delays.append(arg)
|
||||
|
||||
@@ -499,7 +228,7 @@ class Animation(renpy.display.core.Displayable):
|
||||
if t < delay:
|
||||
renpy.display.render.redraw(self, delay - t)
|
||||
|
||||
im = cache.load_image(image)
|
||||
im = render(image, width, height, st)
|
||||
width, height = im.get_size()
|
||||
rv = renpy.display.render.Render(width, height)
|
||||
rv.blit(im, (0, 0))
|
||||
@@ -511,105 +240,21 @@ class Animation(renpy.display.core.Displayable):
|
||||
|
||||
def predict(self, callback):
|
||||
for i in self.images:
|
||||
callback(i)
|
||||
i.predict(callback)
|
||||
|
||||
def get_placement(self):
|
||||
return renpy.game.style.image_placement
|
||||
|
||||
class ImageMap(renpy.display.core.Displayable):
|
||||
"""
|
||||
The displayable that implements renpy.imagemap.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, ground, selected, hotspots, unselected=None,
|
||||
style='imagemap', **properties):
|
||||
|
||||
super(ImageMap, self).__init__()
|
||||
|
||||
self.ground = ground
|
||||
self.selected = selected
|
||||
self.hotspots = hotspots
|
||||
|
||||
if not unselected:
|
||||
self.unselected = self.ground
|
||||
else:
|
||||
self.unselected = unselected
|
||||
|
||||
self.active = None
|
||||
self.last_active = None
|
||||
|
||||
self.style = renpy.style.Style(style, properties)
|
||||
|
||||
def get_placement(self):
|
||||
return self.style
|
||||
|
||||
# This doesn't do anything quite yet.
|
||||
def predict(self, callback):
|
||||
callback(self.ground)
|
||||
callback(self.selected)
|
||||
callback(self.unselected)
|
||||
|
||||
def render(self, width, height, st):
|
||||
|
||||
ground = cache.load_image(self.ground)
|
||||
selected = cache.load_image(self.selected)
|
||||
unselected = cache.load_image(self.unselected)
|
||||
|
||||
width, height = ground.get_size()
|
||||
rv = renpy.display.render.Render(width, height)
|
||||
rv.blit(ground, (0, 0))
|
||||
|
||||
for i, hotspot in enumerate(self.hotspots):
|
||||
|
||||
x0, y0, x1, y1, result = hotspot
|
||||
|
||||
if i == self.active:
|
||||
source = selected
|
||||
else:
|
||||
source = unselected
|
||||
|
||||
subsurface = source.subsurface((x0, y0, x1-x0, y1-y0))
|
||||
renpy.display.render.mutated_surface(subsurface)
|
||||
|
||||
rv.blit(subsurface, (x0, y0))
|
||||
|
||||
return rv
|
||||
|
||||
def event(self, ev, x, y):
|
||||
|
||||
old_active = self.active
|
||||
active = None
|
||||
|
||||
for i, (x0, y0, x1, y1, result) in enumerate(self.hotspots):
|
||||
if x >= x0 and x <= x1 and y >= y0 and y <= y1:
|
||||
active = i
|
||||
break
|
||||
|
||||
# result stays set.
|
||||
|
||||
if old_active != active:
|
||||
self.active = active
|
||||
renpy.display.render.redraw(self, 0)
|
||||
|
||||
if active is not None:
|
||||
renpy.display.audio.play(self.style.hover_sound)
|
||||
|
||||
|
||||
if active is None:
|
||||
return None
|
||||
|
||||
if renpy.display.behavior.map_event(ev, "imagemap_select"):
|
||||
renpy.display.audio.play(self.style.activate_sound)
|
||||
return result
|
||||
|
||||
return None
|
||||
|
||||
class ImageButton(renpy.display.behavior.Button):
|
||||
"""
|
||||
Used to implement the guts of an image button.
|
||||
"""
|
||||
|
||||
def __init__(self, idle_image, hover_image, style='image_button',
|
||||
def __init__(self, idle_image, hover_image,
|
||||
style='image_button',
|
||||
image_style='image_button_image',
|
||||
clicked=None, hovered=None):
|
||||
clicked=None, hovered=None, **properties):
|
||||
|
||||
self.idle_image = Image(idle_image, style=image_style)
|
||||
self.idle_image.style.set_prefix("idle_")
|
||||
@@ -619,18 +264,18 @@ class ImageButton(renpy.display.behavior.Button):
|
||||
super(ImageButton, self).__init__(self.idle_image,
|
||||
style=style,
|
||||
clicked=clicked,
|
||||
hovered=hovered)
|
||||
|
||||
|
||||
hovered=hovered,
|
||||
**properties)
|
||||
|
||||
def predict(self, callback):
|
||||
self.idle_image.predict(callback)
|
||||
self.hover_image.predict(callback)
|
||||
|
||||
def set_hover(self, hover):
|
||||
super(ImageButton, self).set_hover(hover)
|
||||
def focus(self, default=False):
|
||||
self.child = self.hover_image
|
||||
super(ImageButton, self).focus(default=default)
|
||||
|
||||
if hover:
|
||||
self.child = self.hover_image
|
||||
else:
|
||||
self.child = self.idle_image
|
||||
def unfocus(self):
|
||||
self.child = self.idle_image
|
||||
super(ImageButton, self).unfocus()
|
||||
|
||||
|
||||
+24
-16
@@ -27,9 +27,7 @@ class Null(renpy.display.core.Displayable):
|
||||
"""
|
||||
|
||||
def __init__(self, width=0, height=0, style='default', **properties):
|
||||
super(Null, self).__init__()
|
||||
|
||||
self.style = renpy.style.Style(style, properties)
|
||||
super(Null, self).__init__(style=style, **properties)
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
@@ -37,7 +35,12 @@ class Null(renpy.display.core.Displayable):
|
||||
return self.style
|
||||
|
||||
def render(self, width, height, st):
|
||||
return renpy.display.render.Render(self.width, self.height)
|
||||
rv = renpy.display.render.Render(self.width, self.height)
|
||||
|
||||
if self.focusable:
|
||||
rv.add_focus(self, None, None, None, None, None)
|
||||
|
||||
return rv
|
||||
|
||||
|
||||
class Container(renpy.display.core.Displayable):
|
||||
@@ -60,16 +63,24 @@ class Container(renpy.display.core.Displayable):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args):
|
||||
def __init__(self, *args, **properties):
|
||||
|
||||
super(Container, self).__init__()
|
||||
|
||||
self.children = []
|
||||
self.child = None
|
||||
|
||||
for i in args:
|
||||
self.add(i)
|
||||
|
||||
super(Container, self).__init__(**properties)
|
||||
|
||||
|
||||
def find_focusable(self, callback, focus_name):
|
||||
super(Container, self).find_focusable(callback, focus_name)
|
||||
|
||||
for i in self.children:
|
||||
i.find_focusable(callback, self.focus_name or focus_name)
|
||||
|
||||
|
||||
def set_style_prefix(self, prefix):
|
||||
super(Container, self).set_style_prefix(prefix)
|
||||
|
||||
@@ -132,6 +143,8 @@ class Container(renpy.display.core.Displayable):
|
||||
|
||||
def predict(self, callback):
|
||||
|
||||
super(Container, self).predict(callback)
|
||||
|
||||
for i in self.children:
|
||||
i.predict(callback)
|
||||
|
||||
@@ -150,8 +163,7 @@ class Fixed(Container):
|
||||
"""
|
||||
|
||||
def __init__(self, style='default', **properties):
|
||||
super(Fixed, self).__init__()
|
||||
self.style = renpy.style.Style(style, properties)
|
||||
super(Fixed, self).__init__(style=style, **properties)
|
||||
self.times = [ ]
|
||||
|
||||
def add(self, widget, time=None):
|
||||
@@ -214,9 +226,7 @@ class Position(Container):
|
||||
child of this widget is placed.
|
||||
"""
|
||||
|
||||
super(Position, self).__init__()
|
||||
|
||||
self.style = renpy.style.Style(style, properties)
|
||||
super(Position, self).__init__(style=style, **properties)
|
||||
self.add(child)
|
||||
|
||||
def render(self, width, height, st):
|
||||
@@ -456,17 +466,15 @@ class Window(Container):
|
||||
|
||||
def __init__(self, child, style='window', **properties):
|
||||
|
||||
super(Window, self).__init__()
|
||||
|
||||
super(Window, self).__init__(style=style, **properties)
|
||||
self.add(child)
|
||||
self.style = renpy.style.Style(style, properties)
|
||||
|
||||
def get_placement(self):
|
||||
return self.style
|
||||
|
||||
def render(self, width, height, st):
|
||||
|
||||
# save typing and screen space.
|
||||
# save some typing.
|
||||
style = self.style
|
||||
|
||||
xminimum = scale(style.xminimum, width)
|
||||
|
||||
+69
-6
@@ -132,7 +132,6 @@ def render_screen(widget, width, height, st):
|
||||
global new_renders
|
||||
global mutated_surfaces
|
||||
|
||||
redraw_queue = [ ]
|
||||
mutated_surfaces = { }
|
||||
|
||||
rv = render(widget, width, height, st)
|
||||
@@ -149,6 +148,15 @@ def render_screen(widget, width, height, st):
|
||||
old_renders.update(new_renders)
|
||||
new_renders.clear()
|
||||
|
||||
# Figure out which widgets are still alive.
|
||||
live_widgets = sets.Set()
|
||||
for widget, height, width in old_renders:
|
||||
live_widgets.add(widget)
|
||||
|
||||
# Filter dead widgets from the redraw queue.
|
||||
redraw_queue = [ (when, widget) for when, widget in redraw_queue if
|
||||
widget in live_widgets ]
|
||||
|
||||
return rv
|
||||
|
||||
old_blits = [ ]
|
||||
@@ -295,9 +303,11 @@ class Render(object):
|
||||
# entries in old_renders that this render is in.
|
||||
self.render_of = [ ]
|
||||
|
||||
# The width and height of this render.
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
# The parents of this render.
|
||||
self.parents = [ ]
|
||||
|
||||
self.blittables = [ ]
|
||||
@@ -310,6 +320,10 @@ class Render(object):
|
||||
|
||||
self.subsurfaces = { }
|
||||
|
||||
# The list of focusable widgets collected from this render
|
||||
# and the children of this render. A list of (widget, arg, x, y, w, h,)
|
||||
self.focuses = [ ]
|
||||
|
||||
# def __del__(self):
|
||||
# Render.renders -= 1
|
||||
# print "Render del", Render.renders, Render.liverenders, self
|
||||
@@ -371,8 +385,9 @@ class Render(object):
|
||||
|
||||
# Removes cycles.
|
||||
self.render_of = [ ]
|
||||
self.focuses = [ ]
|
||||
|
||||
def blit(self, source, (x, y)):
|
||||
def blit(self, source, (xo, yo), focus=True):
|
||||
"""
|
||||
Adds the source to the list of things that need to be blitted
|
||||
to the screen. The source should be either a pygame.Surface,
|
||||
@@ -385,8 +400,18 @@ class Render(object):
|
||||
|
||||
source.parents.append(self)
|
||||
self.children.append(source)
|
||||
|
||||
self.blittables.append((x, y, source))
|
||||
|
||||
if focus and xo == 0 and yo == 0:
|
||||
self.focuses.extend(source.focuses)
|
||||
elif focus:
|
||||
for widget, arg, x, y, w, h in source.focuses:
|
||||
if x is not None:
|
||||
x += xo
|
||||
y += yo
|
||||
|
||||
self.add_focus(widget, arg, x, y, w, h)
|
||||
|
||||
self.blittables.append((xo, yo, source))
|
||||
|
||||
|
||||
def blit_to(self, dest, x, y):
|
||||
@@ -459,7 +484,7 @@ class Render(object):
|
||||
|
||||
return rv
|
||||
|
||||
def subsurface(self, pos):
|
||||
def subsurface(self, pos, focus=False):
|
||||
"""
|
||||
Returns a subsurface of this render.
|
||||
"""
|
||||
@@ -477,6 +502,24 @@ class Render(object):
|
||||
|
||||
rv = Render(width, height)
|
||||
|
||||
if focus:
|
||||
for fwidget, farg, fx, fy, fw, fh in self.focuses:
|
||||
if fx is not None:
|
||||
fx -= x
|
||||
fx = max(fx, 0)
|
||||
fy -= y
|
||||
fy = max(fy, 0)
|
||||
|
||||
fw -= x
|
||||
fw = min(fw, width)
|
||||
fh -= y
|
||||
fh = min(fh, height)
|
||||
|
||||
if fw <= 0 or fh <= 0:
|
||||
continue
|
||||
|
||||
rv.add_focus(fwidget, farg, fx, fy, fw, fh)
|
||||
|
||||
for xo, yo, source in self.blittables:
|
||||
|
||||
# ulx, uly -- the coordinates of the upper-left hand corner of
|
||||
@@ -539,4 +582,24 @@ class Render(object):
|
||||
self.depends.append(child)
|
||||
child.parents.append(self)
|
||||
|
||||
|
||||
def add_focus(self, widget, arg=None, x=0, y=0, w=None, h=None):
|
||||
"""
|
||||
This is called to indicate a region of the screen that can be
|
||||
focused.
|
||||
|
||||
@param widget: The widget that will be focused.
|
||||
@param arg: A focus argument, which can be checked by the widget.
|
||||
|
||||
The rest of the parameters are a rectangle giving the portion of
|
||||
this region corresponding to the focus. If they are all None, than
|
||||
this focus is assumed to be the singular full-screen focus.
|
||||
"""
|
||||
|
||||
if x is not None:
|
||||
if w is None:
|
||||
w = self.width
|
||||
|
||||
if h is None:
|
||||
h = self.height
|
||||
|
||||
self.focuses.append(renpy.display.focus.Focus(widget, arg, x, y, w, h))
|
||||
|
||||
@@ -452,7 +452,7 @@ class Text(renpy.display.core.Displayable):
|
||||
|
||||
self.layout(width - absxo)
|
||||
|
||||
rv = renpy.display.render.Render(self.laidout_width + absxo, self.laidout_height + absxo)
|
||||
rv = renpy.display.render.Render(self.laidout_width + absxo, self.laidout_height + absyo)
|
||||
|
||||
if self.style.drop_shadow:
|
||||
self.render_pass(rv, dsxo, dsyo, self.style.drop_shadow_color, False, length)
|
||||
|
||||
@@ -60,6 +60,7 @@ class Transition(renpy.display.core.Displayable):
|
||||
"""
|
||||
|
||||
def __init__(self, delay):
|
||||
super(Transition, self).__init__()
|
||||
self.delay = delay
|
||||
self.offsets = [ ]
|
||||
self.events = True
|
||||
@@ -70,6 +71,9 @@ class Transition(renpy.display.core.Displayable):
|
||||
else:
|
||||
return None
|
||||
|
||||
def find_focusable(self, callback, focus_name):
|
||||
self.new_widget.find_focusable(callback, focus_name)
|
||||
|
||||
class Fade(Transition):
|
||||
"""
|
||||
This returns an object that can be used as an argument to a with
|
||||
@@ -134,7 +138,7 @@ class Fade(Transition):
|
||||
if widget:
|
||||
surf = render(widget, width, height, st)
|
||||
|
||||
rv.blit(surf, (0, 0))
|
||||
rv.blit(surf, (0, 0), focus=events)
|
||||
|
||||
self.events = events
|
||||
|
||||
@@ -253,6 +257,8 @@ class Dissolve(Transition):
|
||||
surf = top.pygame_surface(False)
|
||||
renpy.display.render.mutated_surface(surf)
|
||||
|
||||
rv.focuses.extend(top.focuses)
|
||||
|
||||
if id(top) == self.old_top and id(bottom) == self.old_bottom:
|
||||
|
||||
# Fast rendering path.
|
||||
@@ -271,7 +277,7 @@ class Dissolve(Transition):
|
||||
|
||||
# Complete rendering path.
|
||||
|
||||
rv.blit(bottom, (0, 0))
|
||||
rv.blit(bottom, (0, 0), focus=False)
|
||||
surf.set_alpha(alpha, RLEACCEL)
|
||||
rv.blit(surf, (0, 0))
|
||||
|
||||
@@ -352,7 +358,7 @@ class CropMove(Transition):
|
||||
image. Otherwise, the top layer contains the old image.
|
||||
"""
|
||||
|
||||
|
||||
super(CropMove, self).__init__(time)
|
||||
self.time = time
|
||||
|
||||
if mode == "wiperight":
|
||||
@@ -503,11 +509,11 @@ class CropMove(Transition):
|
||||
|
||||
rv = renpy.display.render.Render(width, height)
|
||||
|
||||
rv.blit(render(self.bottom, width, height, st), (0, 0))
|
||||
rv.blit(render(self.bottom, width, height, st), (0, 0), focus=not self.topnew)
|
||||
|
||||
top = render(self.top, width, height, st)
|
||||
ss = top.subsurface(crop)
|
||||
rv.blit(ss, pos)
|
||||
ss = top.subsurface(crop, focus=self.topnew)
|
||||
rv.blit(ss, pos, focus=self.topnew)
|
||||
|
||||
renpy.display.render.redraw(self, 0)
|
||||
return rv
|
||||
|
||||
@@ -160,7 +160,6 @@ class Movie(renpy.display.layout.Null):
|
||||
def render(self, width, height, st):
|
||||
renpy.display.render.redraw(self, 0)
|
||||
|
||||
|
||||
if surface:
|
||||
renpy.display.render.mutated_surface(surface)
|
||||
|
||||
|
||||
+7
-1
@@ -22,6 +22,8 @@ class Context(object):
|
||||
@ivar rollback: True if this context participates in rollbacks.
|
||||
|
||||
@ivar runtime: The time spent in this context, in milliseconds.
|
||||
|
||||
@ivar info: A RevertableObject, which is made available to user code.
|
||||
"""
|
||||
|
||||
def __init__(self, rollback, context=None):
|
||||
@@ -30,12 +32,15 @@ class Context(object):
|
||||
self.return_stack = [ ]
|
||||
self.rollback = rollback
|
||||
self.runtime = 0
|
||||
|
||||
self.info = renpy.python.RevertableObject()
|
||||
|
||||
oldsl = None
|
||||
if context:
|
||||
oldsl = context.scene_lists
|
||||
self.runtime = context.runtime
|
||||
|
||||
vars(self.info).update(vars(context.info))
|
||||
|
||||
self.scene_lists = renpy.display.core.SceneLists(oldsl)
|
||||
|
||||
def goto_label(self, node_name):
|
||||
@@ -115,6 +120,7 @@ class Context(object):
|
||||
rv.current = self.current
|
||||
rv.scene_lists = self.scene_lists.rollback_copy()
|
||||
rv.runtime = self.runtime
|
||||
rv.info = self.info
|
||||
|
||||
return rv
|
||||
|
||||
|
||||
+32
-10
@@ -6,16 +6,16 @@
|
||||
import renpy
|
||||
|
||||
# Many of these shouldn't be used directly.
|
||||
from renpy.display.layout import *
|
||||
from renpy.display.text import *
|
||||
from renpy.display.behavior import *
|
||||
from renpy.display.image import *
|
||||
# from renpy.display.layout import *
|
||||
from renpy.display.text import ParameterizedText
|
||||
from renpy.display.behavior import Keymap
|
||||
# from renpy.display.image import *
|
||||
|
||||
from renpy.curry import curry
|
||||
from renpy.display.audio import music_start, music_stop
|
||||
# from renpy.display.audio import music_start, music_stop
|
||||
from renpy.display.audio import play
|
||||
from renpy.display.video import movie_start_fullscreen, movie_start_displayable, movie_stop
|
||||
from renpy.loadsave import *
|
||||
from renpy.loadsave import load, save, saved_games
|
||||
from renpy.python import py_eval as eval
|
||||
from renpy.python import rng as random
|
||||
|
||||
@@ -202,8 +202,11 @@ def display_menu(items, window_style='menu_window'):
|
||||
|
||||
class TagQuotingDict(object):
|
||||
def __getitem__(self, key):
|
||||
if key in renpy.game.store:
|
||||
rv = renpy.game.store[key]
|
||||
|
||||
store = vars(renpy.store)
|
||||
|
||||
if key in store:
|
||||
rv = store[key]
|
||||
|
||||
if isinstance(rv, (str, unicode)):
|
||||
rv = rv.replace("{", "{{")
|
||||
@@ -310,8 +313,6 @@ def imagemap(ground, selected, hotspots, unselected=None, overlays=False,
|
||||
renpy.ui.imagemap(ground, selected, hotspots, unselected=unselected,
|
||||
style=style, **properties)
|
||||
|
||||
renpy.ui.keymousebehavior()
|
||||
|
||||
rv = renpy.ui.interact(suppress_overlay=(not overlays))
|
||||
checkpoint()
|
||||
return rv
|
||||
@@ -524,6 +525,19 @@ def get_game_runtime():
|
||||
|
||||
return renpy.game.context().runtime / 1000.0
|
||||
|
||||
def loadable(filename):
|
||||
"""
|
||||
Returns True if the given filename is loadable, meaning that it
|
||||
can be loaded from the disk or from inside an archive. Returns
|
||||
False if this is not the case.
|
||||
"""
|
||||
|
||||
try:
|
||||
renpy.loader.load(filename)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def exists(filename):
|
||||
"""
|
||||
Returns true if the given filename can be found in the
|
||||
@@ -549,6 +563,14 @@ def restart_interaction():
|
||||
|
||||
renpy.game.interface.restart_interaction = True
|
||||
|
||||
def context():
|
||||
"""
|
||||
Returns an object that is unique to the current context, that
|
||||
participates in rollback and the like.
|
||||
"""
|
||||
|
||||
return renpy.game.context().info
|
||||
|
||||
|
||||
call_in_new_context = renpy.game.call_in_new_context
|
||||
curried_call_in_new_context = renpy.curry.curry(renpy.game.call_in_new_context)
|
||||
|
||||
@@ -21,11 +21,6 @@ searchpath = [ ]
|
||||
# A Script object, giving the script of the currently executing game.
|
||||
script = None
|
||||
|
||||
# The store is where Ren'Py python results are stored. We first need
|
||||
# to import in the module, and then we use the module's dictionary
|
||||
# directly.
|
||||
store = None
|
||||
|
||||
# A shallow copy of the store made at the end of the init phase. If
|
||||
# a key in here points to the same value here as it does in the store,
|
||||
# it is not saved.
|
||||
|
||||
+8
-3
@@ -54,6 +54,11 @@ def run(restart=False):
|
||||
game.persistent._seen_ever = { }
|
||||
|
||||
game.seen_ever = game.persistent._seen_ever
|
||||
|
||||
# Initialize the set of images seen ever.
|
||||
if not game.persistent._seen_images:
|
||||
game.persistent._seen_images = { }
|
||||
|
||||
|
||||
# Clear the list of seen statements in this game.
|
||||
game.seen_session = { }
|
||||
@@ -66,11 +71,11 @@ def run(restart=False):
|
||||
|
||||
# Initialize the store.
|
||||
renpy.store.store = renpy.store
|
||||
game.store = vars(renpy.store)
|
||||
renpy.store.persistent = game.persistent
|
||||
renpy.store._preferences = game.preferences
|
||||
|
||||
# Set up styles.
|
||||
renpy.style.reset()
|
||||
game.style = renpy.style.StyleManager()
|
||||
renpy.store.style = game.style
|
||||
|
||||
@@ -88,14 +93,14 @@ def run(restart=False):
|
||||
renpy.game.exception_info = 'After initialization, but before game start.'
|
||||
|
||||
# Rebuild the various style caches.
|
||||
game.style._build_style_caches()
|
||||
renpy.style.build_styles()
|
||||
|
||||
# Index the archive files. We should not have loaded an image
|
||||
# before this point. (As pygame will not have been initialized.)
|
||||
renpy.loader.index_archives()
|
||||
|
||||
# Make a clean copy of the store.
|
||||
game.clean_store = game.store.copy()
|
||||
game.clean_store = vars(renpy.store).copy()
|
||||
|
||||
# Re-Initialize the log.
|
||||
game.log = renpy.python.RollbackLog()
|
||||
|
||||
+10
-7
@@ -143,7 +143,7 @@ def list_logical_lines(filename):
|
||||
|
||||
|
||||
if line != "":
|
||||
raise ParseError(filename, number, "is not terminated with a newline.")
|
||||
raise ParseError(filename, start_number, "is not terminated with a newline (check quotes and parenthesis).")
|
||||
|
||||
return rv
|
||||
|
||||
@@ -172,13 +172,11 @@ def group_logical_lines(lines):
|
||||
|
||||
if l[index] == '\t':
|
||||
index += 1
|
||||
depth = depth + 8 - (16 % 8)
|
||||
depth = depth + 8 - (depth % 8)
|
||||
continue
|
||||
|
||||
break
|
||||
|
||||
# TODO: Fix to handle tabs properly. Or else update the docs to
|
||||
# forbid tabs entirely.
|
||||
return depth, l[depth:]
|
||||
|
||||
# i, min_depth -> block, new_i
|
||||
@@ -467,12 +465,17 @@ class Lexer(object):
|
||||
if c == 'u':
|
||||
self.pos += 1
|
||||
|
||||
if self.eol():
|
||||
if self.pos == len(self.text):
|
||||
self.pos -= 1
|
||||
return False
|
||||
|
||||
c = self.text[self.pos]
|
||||
|
||||
if c not in ('"', "'"):
|
||||
if c not in ('"', "'"):
|
||||
self.pos -= 1
|
||||
return False
|
||||
|
||||
elif c not in ('"', "'"):
|
||||
return False
|
||||
|
||||
delim = c
|
||||
@@ -514,7 +517,7 @@ class Lexer(object):
|
||||
while self.match(r'\.'):
|
||||
n = self.name()
|
||||
if not n:
|
||||
self.parse_error('expecting name.')
|
||||
self.error('expecting name.')
|
||||
|
||||
rv += "." + n
|
||||
|
||||
|
||||
+28
-19
@@ -62,7 +62,7 @@ def reached(obj, path, reachable):
|
||||
def reached_vars(store, reachable):
|
||||
"""
|
||||
Marks everything reachable from the variables in the store
|
||||
as reachable.
|
||||
or from the context info objects as reachable.
|
||||
|
||||
@param store: A map from variable name to variable value.
|
||||
@param reachable: A dictionary mapping reached object ids to
|
||||
@@ -72,7 +72,9 @@ def reached_vars(store, reachable):
|
||||
for k, v in store.iteritems():
|
||||
reached(v, k, reachable)
|
||||
|
||||
|
||||
for c in renpy.game.contexts:
|
||||
reached(c.info, "#context", reachable)
|
||||
|
||||
|
||||
##### Code that replaces literals will calls to magic constructors.
|
||||
|
||||
@@ -356,10 +358,10 @@ class Rollback(renpy.object.Object):
|
||||
for t in self.store:
|
||||
if len(t) == 2:
|
||||
k, v = t
|
||||
renpy.game.store[k] = v
|
||||
vars(renpy.store)[k] = v
|
||||
else:
|
||||
k, = t
|
||||
del renpy.game.store[k]
|
||||
del vars(renpy.store)[k]
|
||||
|
||||
renpy.game.contexts = [ self.context ]
|
||||
rng.pushback(self.random)
|
||||
@@ -428,7 +430,7 @@ class RollbackLog(renpy.object.Object):
|
||||
self.log.append(self.current)
|
||||
|
||||
self.mutated = { }
|
||||
self.old_store = renpy.game.store.copy()
|
||||
self.old_store = vars(renpy.store).copy()
|
||||
|
||||
def complete(self):
|
||||
"""
|
||||
@@ -439,7 +441,7 @@ class RollbackLog(renpy.object.Object):
|
||||
occurs.
|
||||
"""
|
||||
|
||||
new_store = renpy.game.store
|
||||
new_store = vars(renpy.store)
|
||||
store = [ ]
|
||||
|
||||
|
||||
@@ -484,9 +486,11 @@ class RollbackLog(renpy.object.Object):
|
||||
|
||||
rv = { }
|
||||
|
||||
store = vars(renpy.store)
|
||||
|
||||
for k in self.ever_been_changed.keys():
|
||||
if k in renpy.game.store:
|
||||
rv[k] = renpy.game.store[k]
|
||||
if k in store:
|
||||
rv[k] = store[k]
|
||||
|
||||
return rv
|
||||
|
||||
@@ -617,14 +621,15 @@ class RollbackLog(renpy.object.Object):
|
||||
renpy.game.log = self
|
||||
|
||||
# Restore the store.
|
||||
renpy.game.store.clear()
|
||||
renpy.game.store.update(renpy.game.clean_store)
|
||||
store = vars(renpy.store)
|
||||
store.clear()
|
||||
store.update(renpy.game.clean_store)
|
||||
|
||||
for k in self.ever_been_changed:
|
||||
if k in renpy.game.store:
|
||||
del renpy.game.store[k]
|
||||
if k in store:
|
||||
del store[k]
|
||||
|
||||
renpy.game.store.update(self.frozen_roots)
|
||||
store.update(self.frozen_roots)
|
||||
self.frozen_roots = None
|
||||
|
||||
# Now, rollback to an acceptable point.
|
||||
@@ -634,31 +639,35 @@ class RollbackLog(renpy.object.Object):
|
||||
|
||||
def py_exec_bytecode(bytecode, hide=False):
|
||||
|
||||
store = vars(renpy.store)
|
||||
|
||||
if hide:
|
||||
locals = { }
|
||||
else:
|
||||
locals = renpy.game.store
|
||||
locals = store
|
||||
|
||||
exec marshal.loads(bytecode) in renpy.game.store, locals
|
||||
exec marshal.loads(bytecode) in store, locals
|
||||
|
||||
|
||||
def py_exec(source, hide=False):
|
||||
|
||||
store = vars(renpy.store)
|
||||
|
||||
if hide:
|
||||
locals = { }
|
||||
else:
|
||||
locals = renpy.game.store
|
||||
locals = store
|
||||
|
||||
|
||||
exec py_compile(source, 'exec') in renpy.game.store, locals
|
||||
exec py_compile(source, 'exec') in store, locals
|
||||
|
||||
def py_eval_bytecode(bytecode):
|
||||
|
||||
return eval(marshal.loads(bytecode), renpy.game.store)
|
||||
return eval(marshal.loads(bytecode), vars(renpy.store))
|
||||
|
||||
def py_eval(source):
|
||||
source = source.strip()
|
||||
|
||||
return eval(py_compile(source, 'eval'),
|
||||
renpy.game.store)
|
||||
vars(renpy.store))
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
import renpy
|
||||
|
||||
import renpy.ui as ui
|
||||
import renpy.display.im as im
|
||||
import renpy.display.audio as audio
|
||||
|
||||
from renpy.python import RevertableList as __renpy__list__
|
||||
list = __renpy__list__
|
||||
|
||||
+155
-121
@@ -1,158 +1,192 @@
|
||||
import renpy
|
||||
|
||||
# A list of style prefixes we care about, including no prefix.
|
||||
prefixes = [ 'hover_', 'idle_', 'activate_', '' ]
|
||||
prefixes = [ 'hover_', 'idle_', 'activate_' , 'insensitive_' ]
|
||||
|
||||
def startswith_prefix(s):
|
||||
for i in prefixes:
|
||||
if s.startswith(i):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
substitutes = dict(
|
||||
xmargin = [ 'left_margin', 'right_margin' ],
|
||||
ymargin = [ 'top_margin', 'bottom_margin' ],
|
||||
|
||||
xpadding = [ 'left_padding', 'right_padding' ],
|
||||
ypadding = [ 'top_padding', 'bottom_padding' ],
|
||||
)
|
||||
|
||||
# Expand out substitutes:
|
||||
for k in substitutes.keys():
|
||||
for p in prefixes:
|
||||
substitutes[p + k] = [ p + i for i in substitutes[k] ]
|
||||
|
||||
# A map from a style name to the style associated with that name.
|
||||
style_map = { }
|
||||
|
||||
# True if we have expanded all of the style caches, False otherwise.
|
||||
styles_built = False
|
||||
|
||||
# A list of styles that are pending expansion.
|
||||
styles_pending = [ ]
|
||||
|
||||
# A list of created styles giving the style's name, parent, and description.
|
||||
style_info = [ ]
|
||||
|
||||
def reset():
|
||||
"""
|
||||
This resets all of the data structures associated with style
|
||||
management.
|
||||
"""
|
||||
|
||||
global style_map
|
||||
global styles_built
|
||||
global styles_pending
|
||||
global style_info
|
||||
|
||||
style_map = { }
|
||||
styles_built = False
|
||||
styles_pending = [ ]
|
||||
style_info = [ ]
|
||||
|
||||
|
||||
class StyleManager(object):
|
||||
"""
|
||||
This is the singleton object that is exported into the store as
|
||||
'style', and to everyone as renpy.game.style. It's responsible for
|
||||
mapping style names to styles.
|
||||
This is the singleton object that is exported into the store
|
||||
as style
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._style_list = [ ]
|
||||
def __getattr__(self, name):
|
||||
try:
|
||||
return style_map[name]
|
||||
except:
|
||||
raise Exception('The style %s does not exist.' % name)
|
||||
|
||||
def create(self, name, parent='default', description=''):
|
||||
"""
|
||||
Creates a new style with the given parent and description, and
|
||||
adds it to the StyleManager.
|
||||
"""
|
||||
def create(self, name, parent, description=None):
|
||||
style_map[name] = Style(parent, { })
|
||||
|
||||
if parent and not hasattr(self, parent):
|
||||
raise Exception("Style '%s' has non-existent parent '%s'." % (name, parent))
|
||||
if description:
|
||||
style_info.append((name, parent, description))
|
||||
|
||||
# This expands out property names and adds them to style's property
|
||||
# dictionary.
|
||||
def compute_properties(style, properties):
|
||||
|
||||
props = { }
|
||||
|
||||
# Expand substitutions.
|
||||
for k, v in properties.items():
|
||||
if k in substitutes:
|
||||
for j in substitutes[k]:
|
||||
props[j] = v
|
||||
else:
|
||||
props[k] = v
|
||||
|
||||
# Expand prefixes, where necessary.
|
||||
for k, v in props.items():
|
||||
if startswith_prefix(k):
|
||||
continue
|
||||
|
||||
del props[k]
|
||||
|
||||
for p in prefixes:
|
||||
props[p + k] = v
|
||||
|
||||
style.properties.update(props)
|
||||
style.cache.update(props)
|
||||
|
||||
# This builds the style. If recurse is True, this also builds the
|
||||
# parent style.
|
||||
def build_style(style, recurse=False):
|
||||
|
||||
style.cache.clear()
|
||||
|
||||
if style.parent:
|
||||
|
||||
try:
|
||||
parent = style_map[style.parent]
|
||||
except:
|
||||
raise Exception('Style %s is not known.' % style.parent)
|
||||
|
||||
if recurse:
|
||||
build_style(parent)
|
||||
|
||||
s = Style(parent, defer=True)
|
||||
s.name = name
|
||||
s.parent = parent
|
||||
s.description = description
|
||||
style.cache.update(parent.cache)
|
||||
|
||||
setattr(self, name, s)
|
||||
style.cache.update(style.properties)
|
||||
|
||||
self._style_list.append(s)
|
||||
# This builds all pending styles, recursing to ensure that they are built
|
||||
# in the right order.
|
||||
def build_styles():
|
||||
|
||||
def _build_style_caches(self):
|
||||
global styles_pending
|
||||
global styles_built
|
||||
|
||||
for i in self._style_list:
|
||||
i.build_cache()
|
||||
for s in styles_pending:
|
||||
build_style(s, True)
|
||||
|
||||
def _write_docs(self, filename):
|
||||
|
||||
f = file(filename, "w")
|
||||
|
||||
import re
|
||||
|
||||
for s in self._style_list:
|
||||
f.write(' <renpy_style name="%s">' % s.name)
|
||||
|
||||
if s.parent:
|
||||
f.write('<renpy_style_inherits>%s</renpy_style_inherits>' % s.parent)
|
||||
|
||||
f.write(re.sub(r'\s+', ' ', s.description))
|
||||
f.write("</renpy_style>\n\n")
|
||||
|
||||
f.close()
|
||||
styles_pending = None
|
||||
styles_built = True
|
||||
|
||||
|
||||
class Style(object):
|
||||
"""
|
||||
This is an individual style object, which can have properties
|
||||
looked up on it or its parent. Call the constructor of this
|
||||
to create an anonymous style.
|
||||
"""
|
||||
|
||||
def __getstate__(self):
|
||||
|
||||
return dict(properties=self.properties,
|
||||
prefix=self.prefix,
|
||||
parent=self.parent)
|
||||
|
||||
return dict(prefix = self.prefix,
|
||||
parent = self.parent,
|
||||
cache = { },
|
||||
properties = self.properties)
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.__dict__.update(state)
|
||||
vars(self).update(state)
|
||||
build_style(self)
|
||||
|
||||
# This should always work, as only one layer of these styles will
|
||||
# be serialized.
|
||||
def __init__(self, parent, properties):
|
||||
|
||||
self.build_cache()
|
||||
fields = dict(
|
||||
prefix = 'insensitive_',
|
||||
parent = parent,
|
||||
cache = { },
|
||||
properties = { },
|
||||
)
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
|
||||
if key in substitutes:
|
||||
for i in substitutes[key]:
|
||||
self.__setattr__(i, value)
|
||||
|
||||
|
||||
return
|
||||
|
||||
for prefix in prefixes:
|
||||
prefkey = prefix + key
|
||||
|
||||
self.properties[prefkey] = value
|
||||
self.cache[prefkey] = value
|
||||
|
||||
def __getattr__(self, key):
|
||||
|
||||
cache = self.cache
|
||||
|
||||
try:
|
||||
return cache[self.prefix + key]
|
||||
except KeyError:
|
||||
raise AttributeError("Style property '%s' not found." % key)
|
||||
|
||||
def __delattr__(self, key):
|
||||
del self.properties[key]
|
||||
del self.cache[key]
|
||||
|
||||
def lookup(self, key, prefix):
|
||||
|
||||
cache = self.cache
|
||||
|
||||
try:
|
||||
return cache.get(prefix + key, cache[key])
|
||||
except KeyError:
|
||||
raise AttributeError("Style property '%s' not found." % key)
|
||||
vars(self).update(fields)
|
||||
|
||||
if styles_built:
|
||||
build_style(self)
|
||||
else:
|
||||
styles_pending.append(self)
|
||||
|
||||
compute_properties(self, properties)
|
||||
|
||||
def set_prefix(self, prefix):
|
||||
vars(self)["prefix"] = prefix
|
||||
self.prefix = prefix
|
||||
|
||||
|
||||
def build_cache(self):
|
||||
vars(self)["cache"] = { }
|
||||
|
||||
self.cache = { }
|
||||
|
||||
if self.parent:
|
||||
self.cache.update(getattr(renpy.game.style, self.parent).cache)
|
||||
|
||||
self.cache.update(self.properties)
|
||||
|
||||
def __init__(self, parent, properties=None, defer=False):
|
||||
|
||||
if parent and not hasattr(renpy.game.style, parent):
|
||||
raise Exception("Style '%s' is not known." % parent)
|
||||
|
||||
if not properties:
|
||||
properties = { }
|
||||
|
||||
for k in properties.keys():
|
||||
for p in prefixes:
|
||||
if p + k not in properties:
|
||||
properties[p + k] = properties[k]
|
||||
|
||||
vars(self)["parent"] = parent
|
||||
vars(self)["prefix"] = ''
|
||||
vars(self)["properties"] = properties
|
||||
vars(self)["cache"] = { }
|
||||
|
||||
if not defer:
|
||||
self.build_cache()
|
||||
def __getattr__(self, name):
|
||||
return self.cache[self.prefix + name]
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
compute_properties(self, { name : value } )
|
||||
|
||||
|
||||
|
||||
def write_docs(filename):
|
||||
|
||||
f = file(filename, "w")
|
||||
|
||||
import re
|
||||
|
||||
for name, parent, description in style_info:
|
||||
f.write(' <renpy_style name="%s">' % name)
|
||||
|
||||
if parent:
|
||||
f.write('<renpy_style_inherits>%s</renpy_style_inherits>' % parent)
|
||||
|
||||
f.write(re.sub(r'\s+', ' ', description))
|
||||
f.write("</renpy_style>\n\n")
|
||||
|
||||
f.close()
|
||||
|
||||
|
||||
|
||||
|
||||
+48
-10
@@ -169,7 +169,7 @@ def vbox(padding=0, **properties):
|
||||
def grid(cols, rows, padding=0, xfill=False, yfill=False, **properties):
|
||||
"""
|
||||
This creates a layout that places widgets in an evenly spaced
|
||||
grid. New widges are added to this vbox unil ui.close() is called.
|
||||
grid. New widgets are added to this grid unil ui.close() is called.
|
||||
Widgets are added by going from left to right within a single row,
|
||||
and down to the start of the next row when a row is full. All cells
|
||||
must be filled (that is, exactly col * rows widgets must be added to
|
||||
@@ -194,8 +194,6 @@ def grid(cols, rows, padding=0, xfill=False, yfill=False, **properties):
|
||||
|
||||
return add(renpy.display.layout.Grid(cols, rows, padding, xfill=xfill, yfill=yfill, **properties), True)
|
||||
|
||||
|
||||
|
||||
def fixed(**properties):
|
||||
"""
|
||||
This creates a layout that places widgets at fixed locations
|
||||
@@ -244,9 +242,12 @@ def keymousebehavior():
|
||||
screen. The keymouse behavior allows the mouse to be controlled
|
||||
by the keyboard. This widget should not be added to any other
|
||||
widget, but should instead be only added to the screen itself.
|
||||
|
||||
As of 4.8, this does nothing, but is retained for compatability.
|
||||
"""
|
||||
|
||||
return add(renpy.display.behavior.KeymouseBehavior())
|
||||
return
|
||||
|
||||
|
||||
def saybehavior():
|
||||
"""
|
||||
@@ -280,7 +281,12 @@ def pausebehavior(delay, result=False):
|
||||
|
||||
return add(renpy.display.behavior.PauseBehavior(delay, result))
|
||||
|
||||
def menu(menuitems, **properties):
|
||||
def menu(menuitems,
|
||||
style = 'menu',
|
||||
caption_style='menu_caption',
|
||||
choice_style='menu_choice',
|
||||
choice_button_style='menu_choice_button',
|
||||
**properties):
|
||||
"""
|
||||
This creates a new menu widget. Unlike the menu statement or
|
||||
renpy.menu function, this menu widget is not enclosed in any sort
|
||||
@@ -293,7 +299,22 @@ def menu(menuitems, **properties):
|
||||
if this item is a non-selectable caption.
|
||||
"""
|
||||
|
||||
return add(renpy.display.behavior.Menu(menuitems, **properties))
|
||||
# menu is now a conglomeration of other widgets. And bully for it.
|
||||
|
||||
renpy.ui.vbox(style=style, **properties)
|
||||
|
||||
for label, val in menuitems:
|
||||
if val is None:
|
||||
renpy.ui.text(label, style=caption_style)
|
||||
else:
|
||||
renpy.ui.textbutton(label,
|
||||
style=choice_button_style,
|
||||
text_style=choice_style,
|
||||
clicked=renpy.ui.returns(val))
|
||||
|
||||
renpy.ui.close()
|
||||
|
||||
# return add(renpy.display.behavior.Menu(menuitems, **properties))
|
||||
|
||||
def input(default, length=None, allow=None, exclude='{}', **properties):
|
||||
"""
|
||||
@@ -323,16 +344,33 @@ def image(filename, **properties):
|
||||
return add(renpy.display.image.Image(filename, **properties))
|
||||
|
||||
def imagemap(ground, selected, hotspots, unselected=None,
|
||||
style='imagemap', button_style='imagemap_button',
|
||||
**properties):
|
||||
"""
|
||||
This is the widget that implements imagemaps. Parameters are
|
||||
This is called to create imagemaps. Parameters are
|
||||
roughtly the same as renpy.imagemap. The value of the hotspot is
|
||||
returned when ui.interact() returns.
|
||||
"""
|
||||
|
||||
return add(renpy.display.image.ImageMap(ground, selected,
|
||||
hotspots, unselected,
|
||||
**properties))
|
||||
rv = fixed(style=style, **properties)
|
||||
|
||||
if not unselected:
|
||||
unselected = ground
|
||||
|
||||
image(ground)
|
||||
|
||||
for x0, y0, x1, y1, result in hotspots:
|
||||
imagebutton(renpy.display.im.Crop(unselected, x0, y0, x1 - x0, y1 - y0),
|
||||
renpy.display.im.Crop(selected, x0, y0, x1 - x0, y1 - y0),
|
||||
clicked=returns(result),
|
||||
style=button_style,
|
||||
xpos=x0, xanchor='left',
|
||||
ypos=y0, yanchor='top',
|
||||
)
|
||||
|
||||
close()
|
||||
|
||||
return rv
|
||||
|
||||
|
||||
def button(clicked=None, **properties):
|
||||
|
||||
+10
-6
@@ -1,6 +1,6 @@
|
||||
init:
|
||||
$ config.window_title = "Test Game"
|
||||
$ config.profile = True
|
||||
$ config.profile = False
|
||||
|
||||
$ config.debug_sound = True
|
||||
|
||||
@@ -22,11 +22,11 @@ init:
|
||||
# Styles.
|
||||
$ style.window.background = Frame("frame.png", 120, 25)
|
||||
|
||||
$ style.menu.hover_sound = "chev1.wav"
|
||||
$ style.menu.activate_sound = "chev2.wav"
|
||||
$ style.menu_choice_button.hover_sound = "chev1.wav"
|
||||
$ style.menu_choice_button.activate_sound = "chev2.wav"
|
||||
|
||||
$ style.imagemap.hover_sound = "chev3.wav"
|
||||
$ style.imagemap.activate_sound = "chev4.wav"
|
||||
$ style.imagemap_button.hover_sound = "chev3.wav"
|
||||
$ style.imagemap_button.activate_sound = "chev4.wav"
|
||||
|
||||
$ style.button.hover_sound = "chev6.wav"
|
||||
$ style.button.activate_sound = "chev7.wav"
|
||||
@@ -45,7 +45,7 @@ init:
|
||||
|
||||
image eileen anim = Animation("9a_happy.png", 0.25,
|
||||
"9a_vhappy.png", 0.25,
|
||||
"9a_concerned.png")
|
||||
"9a_concerned.png", 60.0)
|
||||
|
||||
image movie = Movie()
|
||||
|
||||
@@ -82,6 +82,10 @@ label main_menu:
|
||||
# Stuff that happens before the real main menu.
|
||||
|
||||
scene black
|
||||
|
||||
show eileen anim
|
||||
|
||||
$ renpy.pause()
|
||||
|
||||
|
||||
show text "American Bishoujo\nPresents" \
|
||||
|
||||
Reference in New Issue
Block a user