Source code for moderngl_window.context.tk.window

import tkinter
from pathlib import Path
from typing import Any

from pyopengltk import OpenGLFrame

from moderngl_window.context.base import BaseWindow
from moderngl_window.context.tk.keys import Keys


[docs] class Window(BaseWindow): #: Name of the window name = "tk" #: tkinter specific key constants keys = Keys _mouse_button_map = { 1: 1, 3: 2, 2: 3, } def __init__(self, **kwargs: Any): super().__init__(**kwargs) self._tk = tkinter.Tk() self._gl_widget = ModernglTkWindow(self._tk, width=self.width, height=self.height) self._gl_widget.pack(fill=tkinter.BOTH, expand=tkinter.YES) self._tk.resizable(self._resizable, self._resizable) if self._fullscreen: self._tk.attributes("-fullscreen", True) self.cursor = self._cursor # Set up events self._gl_widget.bind("<Configure>", self.tk_resize) self._tk.bind("<KeyPress>", self.tk_key_press) self._tk.bind("<KeyRelease>", self.tk_key_release) self._tk.bind("<Motion>", self.tk_mouse_motion) self._tk.bind("<Button>", self.tk_mouse_button_press) self._tk.bind("<ButtonRelease>", self.tk_mouse_button_release) self._tk.bind("<MouseWheel>", self.tk_mouse_wheel) self._tk.bind("<Map>", self.tk_map) self._tk.bind("<Unmap>", self.tk_unmap) self._tk.protocol("WM_DELETE_WINDOW", self.tk_close_window) self.title = self._title # Ensure the window is opened/visible self._tk.update() self._gl_widget.tkMakeCurrent() self.init_mgl_context() self.set_default_viewport() def _set_fullscreen(self, value: bool) -> None: self._tk.attributes("-fullscreen", value) def _set_vsync(self, value: bool) -> None: # TODO: Figure out how to toggle vsync pass @property def size(self) -> tuple[int, int]: """tuple[int, int]: current window size. This property also support assignment:: # Resize the window to 1000 x 1000 window.size = 1000, 1000 """ return self._width, self._height @size.setter def size(self, value: tuple[int, int]) -> None: self._tk.geometry("{}x{}".format(value[0], value[1])) @property def position(self) -> tuple[int, int]: """tuple[int, int]: The current window position. This property can also be set to move the window:: # Move window to 100, 100 window.position = 100, 100 """ _, x, y = self._tk.geometry().split("+") return int(x), int(y) @position.setter def position(self, value: tuple[int, int]) -> None: self._tk.geometry("+{}+{}".format(value[0], value[1])) @property def visible(self) -> bool: """bool: Is the window visible? This property can also be set:: # Hide or show the window window.visible = False """ return self._visible @visible.setter def visible(self, value: bool) -> None: self._visible = value if value: self._tk.deiconify() else: self._tk.withdraw() @property def cursor(self) -> bool: """bool: Should the mouse cursor be visible inside the window? This property can also be assigned to:: # Disable cursor window.cursor = False """ return self._cursor @cursor.setter def cursor(self, value: bool) -> None: if value is True: self._tk.config(cursor="arrow") else: self._tk.config(cursor="none") self._cursor = value @property def title(self) -> str: """str: Window title. This property can also be set:: window.title = "New Title" """ return self._title @title.setter def title(self, value: str) -> None: self._tk.title(value) self._title = value
[docs] def swap_buffers(self) -> None: """Swap buffers, set viewport, trigger events and increment frame counter""" err = self._ctx.error if err != "GL_NO_ERROR": print(err) # Ensure we process events or tkinter will eventually stall. self._tk.update_idletasks() self._tk.update() self._gl_widget.tkSwapBuffers() self._frames += 1
def _set_icon(self, icon_path: Path) -> None: self._tk.iconphoto(False, tkinter.PhotoImage(file=icon_path))
[docs] def tk_key_press(self, event: tkinter.Event) -> None: """Handle all queued key press events in tkinter dispatching events to standard methods""" self._key_event_func(event.keysym, self.keys.ACTION_PRESS, self._modifiers) self._handle_modifiers(event, True) if event.char: self._unicode_char_entered_func(event.char) if self._exit_key is not None and event.keysym == self._exit_key: self.close() if self._fs_key is not None and event.keysym == self._fs_key: self.fullscreen = not self.fullscreen
[docs] def tk_key_release(self, event: tkinter.Event) -> None: """Handle all queued key release events in tkinter dispatching events to standard methods Args: event (tkinter.Event): The key release event """ self._handle_modifiers(event, False) self._key_event_func(event.keysym, self.keys.ACTION_RELEASE, self._modifiers)
[docs] def tk_mouse_motion(self, event: tkinter.Event) -> None: """Handle and translate tkinter mouse position events Args: event (tkinter.Event): The mouse motion event """ x, y = event.x, event.y dx, dy = self._calc_mouse_delta(x, y) if self._mouse_buttons.any: self._mouse_drag_event_func(x, y, dx, dy) else: self._mouse_position_event_func(x, y, dx, dy)
[docs] def tk_mouse_button_press(self, event: tkinter.Event) -> None: """Handle tkinter mouse press events. Args: event (tkinter.Event): The mouse button press event """ self._handle_modifiers(event, True) button = self._mouse_button_map.get(event.num) if not button: return self._handle_mouse_button_state_change(button, True) self._mouse_press_event_func(event.x, event.y, button)
[docs] def tk_mouse_button_release(self, event: tkinter.Event) -> None: """Handle tkinter mouse press events. Args: event (tkinter.Event): The mouse button release event """ self._handle_modifiers(event, True) button = self._mouse_button_map.get(event.num) if not button: return self._handle_mouse_button_state_change(button, False) self._mouse_release_event_func(event.x, event.y, button)
[docs] def tk_mouse_wheel(self, event: tkinter.Event) -> None: """Handle mouse wheel event. Args: event (tkinter.Event): The mouse wheel event """ self._handle_modifiers(event, True) self._mouse_scroll_event_func(0, event.delta / 120.0)
def _handle_modifiers(self, event: tkinter.Event, press: bool) -> None: """Update internal key modifiers Args: event (tkinter.Event): The key event press (bool): Press or release event """ if event.keysym in ["Shift_L", "Shift_R"]: self._modifiers.shift = press elif event.keysym in ["Control_L", "Control_R"]: self._modifiers.ctrl = press elif event.keysym in ["Alt_L", "Alt_R"]: self._modifiers.alt = press
[docs] def tk_resize(self, event: tkinter.Event) -> None: """tkinter specific window resize event. Forwards resize events to the configured resize function. Args: event (tkinter.Event): The resize event """ self._width, self._height = event.width, event.height # TODO: How do we know the actual buffer size? self._buffer_width, self._buffer_height = event.width, event.height # Race condition when going fullscreen mode. # The moderngl context might not be created yet. if not self._ctx: return self.set_default_viewport() self._resize_func(event.width, event.height)
[docs] def tk_close_window(self) -> None: """tkinter close window callback""" self._close_func() self._close = True
def tk_map(self, event: tkinter.Event) -> None: self._visible = True self._iconify_func(False) def tk_unmap(self, event: tkinter.Event) -> None: self._visible = False self._iconify_func(True)
[docs] def destroy(self) -> None: """Destroy logic for tkinter window.""" self._tk.destroy()
class ModernglTkWindow(OpenGLFrame): def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) def redraw(self) -> None: """pyopengltk's own render method.""" pass def initgl(self) -> None: """pyopengltk's user code for initialization.""" pass def tkResize(self, event: tkinter.Event) -> None: """Should never be called. Event overridden.""" raise ValueError("tkResize should never be called. The event is overridden.") def tkMap(self, event: tkinter.Event) -> None: """Called when frame goes onto the screen""" # Only create context once # In a window like this we are not likely to lose the context # even when window is minimized. if not getattr(self, "_wid", None): super().tkMap(event)