Source code for arcade.application

"""
The main window class that all object-oriented applications should
derive from.
"""
import time

from numbers import Number
from typing import Tuple, List, Optional, TYPE_CHECKING

import pyglet.gl as gl
import pyglet

from arcade import get_viewport
from arcade import set_viewport
from arcade import set_window
from arcade import get_display_size

from arcade.context import ArcadeContext

import arcade

if TYPE_CHECKING:
    from arcade import TextBox
    from arcade import TextButton
    from arcade import DialogueBox
    from arcade import TextLabel

import logging
LOG = logging.getLogger(__name__)

MOUSE_BUTTON_LEFT = 1
MOUSE_BUTTON_MIDDLE = 2
MOUSE_BUTTON_RIGHT = 4

_window: 'Window'


def get_screens():
    """
    Return a list of screens. So for a two-monitor setup, this should return
    a list of two screens. Can be used with arcade.Window to select which
    window we full-screen on.
    """
    display = pyglet.canvas.get_display()
    return display.get_screens()


[docs]class NoOpenGLException(Exception): """ Exception when we can't get an OpenGL 3.3+ context """ pass
[docs]class Window(pyglet.window.Window): """ The Window class forms the basis of most advanced games that use Arcade. It represents a window on the screen, and manages events. """ def __init__(self, width: int = 800, height: int = 600, title: str = 'Arcade Window', fullscreen: bool = False, resizable: bool = False, update_rate: Optional[float] = 1/60, antialiasing: bool = True, screen: pyglet.canvas.Screen = None): """ Construct a new window :param int width: Window width :param int height: Window height :param str title: Title (appears in title bar) :param bool fullscreen: Should this be full screen? :param bool resizable: Can the user resize the window? :param float update_rate: How frequently to update the window. :param bool antialiasing: Should OpenGL's anti-aliasing be enabled? """ if antialiasing: config = pyglet.gl.Config(major_version=3, minor_version=3, double_buffer=True, sample_buffers=1, samples=4) else: config = pyglet.gl.Config(major_version=3, minor_version=3, double_buffer=True) try: super().__init__(width=width, height=height, caption=title, resizable=resizable, config=config, vsync=False) except pyglet.window.NoSuchConfigException: raise NoOpenGLException("Unable to create an OpenGL 3.3+ context. " "Check to make sure your system supports OpenGL 3.3 or higher.") if antialiasing: try: gl.glEnable(gl.GL_MULTISAMPLE_ARB) except pyglet.gl.GLException: print("Warning: Anti-aliasing not supported on this computer.") if update_rate: from pyglet import compat_platform if compat_platform == 'darwin' or compat_platform == 'linux': # Set vsync to false, or we'll be limited to a 1/30 sec update rate possibly self.context.set_vsync(False) self.set_update_rate(update_rate) super().set_fullscreen(fullscreen, screen) # This used to be necessary on Linux, but no longer appears to be. # With Pyglet 2.0+, setting this to false will not allow the screen to # update. It does, however, cause flickering if creating a window that # isn't derived from the Window class. # self.invalid = False set_window(self) set_viewport(0, self.width - 1, 0, self.height - 1) self._current_view: Optional[View] = None self.button_list: List[TextButton] = [] self.dialogue_box_list: List[DialogueBox] = [] self.text_list: List[TextLabel] = [] self.textbox_list: List[TextBox] = [] self.textbox_time = 0.0 self.key: Optional[int] = None self.ui_manager = arcade.experimental.gui.UIManager(self) # Representation of the OpenGL context for this window self.ctx = ArcadeContext(self) # Required for transparency self.ctx.enable(self.ctx.BLEND) self.ctx.blend_func = self.ctx.BLEND_DEFAULT @property def current_view(self): """ This property returns the current view being shown. To set a different view, call the :py:meth:`arcade.Window.show_view` method. """ return self._current_view
[docs] def close(self): """ Close the Window. """ super().close() pyglet.clock.unschedule(self.update) pyglet.clock.unschedule(self.on_update)
[docs] def set_fullscreen(self, fullscreen=True, screen=None, mode=None, width=None, height=None): """ Set if we are full screen or not. :param bool fullscreen: :param screen: :param mode: :param int width: :param int height: """ super.set_fullscreen(fullscreen, screen, mode, width, height)
[docs] def center_window(self): """ Center the window on the screen. """ # Get the display screen using pyglet screen_width, screen_height = get_display_size() window_width, window_height = self.get_size() # Center the window self.set_location((screen_width - window_width) // 2, (screen_height - window_height) // 2)
[docs] def update(self, delta_time: float): """ Move everything. For better consistency in naming, use ``on_update`` instead. :param float delta_time: Time interval since the last time the function was called in seconds. """ if self._current_view is not None: self._current_view.update(delta_time)
[docs] def on_update(self, delta_time: float): """ Move everything. Perform collision checks. Do all the game logic here. :param float delta_time: Time interval since the last time the function was called. """ if self._current_view is not None: self._current_view.on_update(delta_time) try: self.textbox_time += delta_time seconds = self.textbox_time % 60 if seconds >= 0.115: if self.textbox_list: for textbox in self.textbox_list: textbox.update(delta_time, self.key) self.textbox_time = 0.0 except AttributeError: pass
[docs] def set_update_rate(self, rate: float): """ Set how often the screen should be updated. For example, self.set_update_rate(1 / 60) will set the update rate to 60 fps :param float rate: Update frequency in seconds """ pyglet.clock.unschedule(self.update) pyglet.clock.schedule_interval(self.update, rate) pyglet.clock.unschedule(self.on_update) pyglet.clock.schedule_interval(self.on_update, rate)
[docs] def on_mouse_motion(self, x: float, y: float, dx: float, dy: float): """ Override this function to add mouse functionality. :param float x: x position of mouse :param float y: y position of mouse :param float dx: Change in x since the last time this method was called :param float dy: Change in y since the last time this method was called """ pass
[docs] def on_mouse_press(self, x: float, y: float, button: int, modifiers: int): """ Override this function to add mouse button functionality. :param float x: x position of the mouse :param float y: y position of the mouse :param int button: What button was hit. One of: arcade.MOUSE_BUTTON_LEFT, arcade.MOUSE_BUTTON_RIGHT, arcade.MOUSE_BUTTON_MIDDLE :param int modifiers: Shift/click, ctrl/click, etc. """ try: if self.button_list: for button_widget in self.button_list: if button_widget.active: button_widget.check_mouse_press(x, y) except AttributeError: pass try: if self.dialogue_box_list: for dialogue_box in self.dialogue_box_list: if dialogue_box.active: dialogue_box.on_mouse_press(x, y, button, modifiers) except AttributeError: pass try: if self.textbox_list: for textbox in self.textbox_list: textbox.check_mouse_press(x, y) except AttributeError: pass
[docs] def on_mouse_drag(self, x: float, y: float, dx: float, dy: float, buttons: int, modifiers: int): """ Override this function to add mouse button functionality. :param float x: x position of mouse :param float y: y position of mouse :param float dx: Change in x since the last time this method was called :param float dy: Change in y since the last time this method was called :param int buttons: Which button is pressed :param int modifiers: Ctrl, shift, etc. """ self.on_mouse_motion(x, y, dx, dy)
[docs] def on_mouse_release(self, x: float, y: float, button: int, modifiers: int): """ Override this function to add mouse button functionality. :param float x: :param float y: :param int button: :param int modifiers: """ try: if self.button_list: for button_widget in self.button_list: if button_widget.active: button_widget.check_mouse_release(x, y) except AttributeError: pass try: if self.dialogue_box_list: for dialogue_box in self.dialogue_box_list: if dialogue_box.active: dialogue_box.on_mouse_release(x, y, button, modifiers) except AttributeError: pass try: if self.textbox_list: for textbox in self.textbox_list: textbox.check_mouse_release(x, y) except AttributeError: pass
[docs] def on_mouse_scroll(self, x: int, y: int, scroll_x: int, scroll_y: int): """ User moves the scroll wheel. :param int x: :param int y: :param int scroll_x: :param int scroll_y: """ pass
[docs] def set_mouse_visible(self, visible: bool = True): """ If true, user can see the mouse cursor while it is over the window. Set false, the mouse is not visible. Default is true. :param bool visible: """ super().set_mouse_visible(visible)
[docs] def on_key_press(self, symbol: int, modifiers: int): """ Override this function to add key press functionality. :param int symbol: Key that was hit :param int modifiers: If it was shift/ctrl/alt """ try: self.key = symbol except AttributeError: pass
[docs] def on_key_release(self, symbol: int, modifiers: int): """ Override this function to add key release functionality. :param int symbol: Key that was hit :param int modifiers: If it was shift/ctrl/alt """ try: self.key = None except AttributeError: pass
[docs] def on_draw(self): """ Override this function to add your custom drawing code. """ try: if self.button_list: for button in self.button_list: if button.active: button.draw() except AttributeError: pass try: if self.text_list: for text in self.text_list: if text.active: text.draw() except AttributeError: pass try: if self.dialogue_box_list: for dialogue_box in self.dialogue_box_list: if dialogue_box.active: dialogue_box.on_draw() except AttributeError: pass try: if self.textbox_list: for textbox in self.textbox_list: textbox.draw() except AttributeError: pass
[docs] def on_resize(self, width: float, height: float): """ Override this function to add custom code to be called any time the window is resized. The only responsibility here is to update the viewport. :param float width: New width :param float height: New height """ try: original_viewport = self.get_viewport() except: print("Error getting viewport") return # unscaled_viewport = self.get_viewport_size() # scaling = unscaled_viewport[0] / width self.set_viewport(original_viewport[0], original_viewport[0] + width, original_viewport[2], original_viewport[2] + height)
[docs] def set_min_size(self, width: float, height: float): """ Wrap the Pyglet window call to set minimum size :param float width: width in pixels. :param float height: height in pixels. """ if self._resizable: super().set_minimum_size(width, height) else: raise ValueError('Cannot set min size on non-resizable window')
[docs] def set_max_size(self, width: float, height: float): """ Wrap the Pyglet window call to set maximum size :param float width: width in pixels. :param float height: height in pixels. :Raises ValueError: """ if self._resizable: super().set_maximum_size(width, height) else: raise ValueError('Cannot set max size on non-resizable window')
[docs] def set_size(self, width: float, height: float): """ Ignore the resizable flag and set the size :param float width: :param float height: """ super().set_size(width, height)
[docs] def get_size(self) -> Tuple[int, int]: """ Get the size of the window. :returns: (width, height) """ return super().get_size()
[docs] def get_location(self) -> Tuple[int, int]: """ Return the X/Y coordinates of the window :returns: x, y of window location """ return super().get_location()
[docs] def set_visible(self, visible=True): """ Set if the window is visible or not. Normally, a program's window is visible. :param bool visible: """ super().set_visible(visible)
# noinspection PyMethodMayBeStatic
[docs] def set_viewport(self, left: float, right: float, bottom: float, top: float): """ Set the viewport. (What coordinates we can see. Used to scale and/or scroll the screen.) :param Number left: :param Number right: :param Number bottom: :param Number top: """ set_viewport(left, right, bottom, top)
# noinspection PyMethodMayBeStatic
[docs] def get_viewport(self) -> Tuple[float, float, float, float]: """ Get the viewport. (What coordinates we can see.) """ return get_viewport()
[docs] def use(self): """Bind the window's framebuffer for rendering commands""" gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0) self.ctx.active_framebuffer = self set_viewport(*get_viewport())
[docs] def test(self, frames: int = 10): """ Used by unit test cases. Runs the event loop a few times and stops. :param int frames: """ start_time = time.time() for i in range(frames): self.switch_to() self.dispatch_events() self.dispatch_event('on_draw') self.flip() current_time = time.time() elapsed_time = current_time - start_time start_time = current_time if elapsed_time < 1. / 60.: sleep_time = (1. / 60.) - elapsed_time time.sleep(sleep_time) self.update(1 / 60)
[docs] def show_view(self, new_view: 'View'): """ Select the view to show. Calling this function is the same as setting the :py:meth:`arcade.Window.current_view` attribute. :param View new_view: View to show """ if not isinstance(new_view, View): raise ValueError("Must pass an arcade.View object to " "Window.show_view()") # Store the Window that is showing the "new_view" View. if new_view.window is None: new_view.window = self elif new_view.window != self: raise RuntimeError("You are attempting to pass the same view " "object between multiple windows. A single " "view object can only be used in one window.") # remove previously shown view's handlers if self._current_view is not None: self.remove_handlers(self._current_view) # push new view's handlers self._current_view = new_view self.push_handlers(self._current_view) self._current_view.on_show()
# Note: After the View has been pushed onto pyglet's stack of event handlers (via push_handlers()), pyglet # will still call the Window's event handlers. (See pyglet's EventDispatcher.dispatch_event() implementation # for details) def _create(self): super()._create() def _recreate(self, changes): super()._recreate(changes)
[docs] def flip(self): """ Swap OpenGL and backing buffers for double-buffered windows. """ super().flip()
[docs] def switch_to(self): """ Switch the this window. """ super().switch_to()
[docs] def set_caption(self, caption): """ Set the caption for the window. """ super().set_caption(caption)
[docs] def set_minimum_size(self, width: int, height: int): """ Set smallest window size. """ super().set_minimum_size(width, height)
[docs] def set_maximum_size(self, width, height): """ Set largest window size. """ super().set_maximum_size(width, height)
[docs] def set_location(self, x, y): """ Set location of the window. """ super().set_location(x, y)
[docs] def activate(self): """ Activate this window. """ super().activate()
[docs] def minimize(self): """ Minimize the window. """ super().minimize()
[docs] def maximize(self): """ Maximize the window. """ super().maximize()
[docs] def set_vsync(self, vsync: bool): """ Set if we sync our draws to the monitors vertical sync rate. """ super().set_vsync(vsync)
[docs] def set_mouse_platform_visible(self, platform_visible=None): """ This does something. """ super().set_mouse_platform_visible(platform_visible)
[docs] def set_exclusive_mouse(self, exclusive=True): """ Capture the mouse. """ super().set_exclusive_mouse(exclusive)
[docs] def set_exclusive_keyboard(self, exclusive=True): """ Capture all keyboard input. """ super().set_exclusive_keyboard(exclusive)
[docs] def get_system_mouse_cursor(self, name): """ Get the system mouse cursor """ return super().get_system_mouse_cursor(name)
[docs] def dispatch_events(self): """ Dispatch events """ super().dispatch_events()
[docs]def open_window(width: int, height: int, window_title: str, resizable: bool = False, antialiasing: bool = True) -> Window: """ This function opens a window. For ease-of-use we assume there will only be one window, and the programmer does not need to keep a handle to the window. This isn't the best architecture, because the window handle is stored in a global, but it makes things easier for programmers if they don't have to track a window pointer. :param Number width: Width of the window. :param Number height: Height of the window. :param str window_title: Title of the window. :param bool resizable: Whether the window can be user-resizable. :param bool antialiasing: Smooth the graphics? :returns: Handle to window :rtype arcade.Window: """ global _window _window = Window(width, height, window_title, resizable=resizable, update_rate=None, antialiasing=antialiasing) _window.invalid = False return _window
[docs]class View: """ Support different views/screens in a window. """ def __init__(self, window: Window = None): if window is None: self.window = arcade.get_window() else: self.window = window self.button_list: List[TextButton] = [] self.dialogue_box_list: List[DialogueBox] = [] self.text_list: List[TextLabel] = [] self.textbox_time = 0.0 self.textbox_list: List[TextBox] = [] self.key = None
[docs] def update(self, delta_time: float): """To be overridden""" try: self.textbox_time += delta_time seconds = self.textbox_time % 60 if seconds >= 0.115: if self.textbox_list: for textbox in self.textbox_list: textbox.update(delta_time, self.key) self.textbox_time = 0.0 except AttributeError: pass
[docs] def on_update(self, delta_time: float): """To be overridden""" pass
[docs] def on_draw(self): """Called when this view should draw""" try: if self.button_list: for button in self.button_list: button.draw() except AttributeError: pass try: if self.text_list: for text in self.text_list: text.draw() except AttributeError: pass pass try: if self.dialogue_box_list: for dialogue_box in self.dialogue_box_list: if dialogue_box.active: dialogue_box.on_draw() except AttributeError: pass try: if self.textbox_list: for textbox in self.textbox_list: textbox.draw() except AttributeError: pass
[docs] def on_show(self): """Called when this view is shown""" pass
[docs] def on_mouse_motion(self, x: float, y: float, dx: float, dy: float): """ Override this function to add mouse functionality. :param float x: x position of mouse :param float y: y position of mouse :param float dx: Change in x since the last time this method was called :param float dy: Change in y since the last time this method was called """ pass
[docs] def on_mouse_press(self, x: float, y: float, button: int, modifiers: int): """ Override this function to add mouse button functionality. :param float x: x position of the mouse :param float y: y position of the mouse :param int button: What button was hit. One of: arcade.MOUSE_BUTTON_LEFT, arcade.MOUSE_BUTTON_RIGHT, arcade.MOUSE_BUTTON_MIDDLE :param int modifiers: Shift/click, ctrl/click, etc. """ try: if self.button_list: for button_widget in self.button_list: button_widget.check_mouse_press(x, y) except AttributeError: pass try: if self.dialogue_box_list: for dialogue_box in self.dialogue_box_list: if dialogue_box.active: dialogue_box.on_mouse_press(x, y, button, modifiers) except AttributeError: pass try: if self.textbox_list: for textbox in self.textbox_list: textbox.check_mouse_press(x, y) except AttributeError: pass
[docs] def on_mouse_drag(self, x: float, y: float, dx: float, dy: float, _buttons: int, _modifiers: int): """ Override this function to add mouse button functionality. :param float x: x position of mouse :param float y: y position of mouse :param float dx: Change in x since the last time this method was called :param float dy: Change in y since the last time this method was called :param int _buttons: Which button is pressed :param int _modifiers: Ctrl, shift, etc. """ self.on_mouse_motion(x, y, dx, dy)
[docs] def on_mouse_release(self, x: float, y: float, button: int, modifiers: int): """ Override this function to add mouse button functionality. :param float x: :param float y: :param int button: :param int modifiers: """ try: if self.button_list: for button_widget in self.button_list: button_widget.check_mouse_release(x, y) except AttributeError: pass try: if self.dialogue_box_list: for dialogue_box in self.dialogue_box_list: if dialogue_box.active: dialogue_box.on_mouse_release(x, y, button, modifiers) except AttributeError: pass try: if self.textbox_list: for textbox in self.textbox_list: textbox.check_mouse_release(x, y) except AttributeError: pass
[docs] def on_mouse_scroll(self, x: int, y: int, scroll_x: int, scroll_y: int): """ User moves the scroll wheel. :param int x: :param int y: :param int scroll_x: :param int scroll_y: """ pass
[docs] def on_key_press(self, symbol: int, modifiers: int): """ Override this function to add key press functionality. :param int symbol: Key that was hit :param int modifiers: If it was shift/ctrl/alt """ try: self.key = symbol except AttributeError: pass
[docs] def on_key_release(self, _symbol: int, _modifiers: int): """ Override this function to add key release functionality. :param int _symbol: Key that was hit :param int _modifiers: If it was shift/ctrl/alt """ try: self.key = None except AttributeError: pass