987 lines
34 KiB
Python
987 lines
34 KiB
Python
# found a way to include it more easily.
|
|
'''
|
|
SDL2 Window
|
|
===========
|
|
|
|
Windowing provider directly based on our own wrapped version of SDL.
|
|
|
|
TODO:
|
|
- fix keys
|
|
- support scrolling
|
|
- clean code
|
|
- manage correctly all sdl events
|
|
|
|
'''
|
|
|
|
__all__ = ('WindowSDL', )
|
|
|
|
from os.path import join
|
|
import sys
|
|
from typing import Optional
|
|
from kivy import kivy_data_dir
|
|
from kivy.logger import Logger
|
|
from kivy.base import EventLoop
|
|
from kivy.clock import Clock
|
|
from kivy.config import Config
|
|
from kivy.core.window import WindowBase
|
|
try:
|
|
from kivy.core.window._window_sdl2 import _WindowSDL2Storage
|
|
except ImportError:
|
|
from kivy.core import handle_win_lib_import_error
|
|
handle_win_lib_import_error(
|
|
'window', 'sdl2', 'kivy.core.window._window_sdl2')
|
|
raise
|
|
from kivy.input.provider import MotionEventProvider
|
|
from kivy.input.motionevent import MotionEvent
|
|
from kivy.resources import resource_find
|
|
from kivy.utils import platform, deprecated
|
|
from kivy.compat import unichr
|
|
from collections import deque
|
|
|
|
|
|
# SDL_keycode.h, https://wiki.libsdl.org/SDL_Keymod
|
|
KMOD_NONE = 0x0000
|
|
KMOD_LSHIFT = 0x0001
|
|
KMOD_RSHIFT = 0x0002
|
|
KMOD_LCTRL = 0x0040
|
|
KMOD_RCTRL = 0x0080
|
|
KMOD_LALT = 0x0100
|
|
KMOD_RALT = 0x0200
|
|
KMOD_LGUI = 0x0400
|
|
KMOD_RGUI = 0x0800
|
|
KMOD_NUM = 0x1000
|
|
KMOD_CAPS = 0x2000
|
|
KMOD_MODE = 0x4000
|
|
|
|
SDLK_SHIFTL = 1073742049
|
|
SDLK_SHIFTR = 1073742053
|
|
SDLK_LCTRL = 1073742048
|
|
SDLK_RCTRL = 1073742052
|
|
SDLK_LALT = 1073742050
|
|
SDLK_RALT = 1073742054
|
|
SDLK_LEFT = 1073741904
|
|
SDLK_RIGHT = 1073741903
|
|
SDLK_UP = 1073741906
|
|
SDLK_DOWN = 1073741905
|
|
SDLK_HOME = 1073741898
|
|
SDLK_END = 1073741901
|
|
SDLK_PAGEUP = 1073741899
|
|
SDLK_PAGEDOWN = 1073741902
|
|
SDLK_SUPER = 1073742051
|
|
SDLK_CAPS = 1073741881
|
|
SDLK_INSERT = 1073741897
|
|
SDLK_KEYPADNUM = 1073741907
|
|
SDLK_KP_DIVIDE = 1073741908
|
|
SDLK_KP_MULTIPLY = 1073741909
|
|
SDLK_KP_MINUS = 1073741910
|
|
SDLK_KP_PLUS = 1073741911
|
|
SDLK_KP_ENTER = 1073741912
|
|
SDLK_KP_1 = 1073741913
|
|
SDLK_KP_2 = 1073741914
|
|
SDLK_KP_3 = 1073741915
|
|
SDLK_KP_4 = 1073741916
|
|
SDLK_KP_5 = 1073741917
|
|
SDLK_KP_6 = 1073741918
|
|
SDLK_KP_7 = 1073741919
|
|
SDLK_KP_8 = 1073741920
|
|
SDLK_KP_9 = 1073741921
|
|
SDLK_KP_0 = 1073741922
|
|
SDLK_KP_DOT = 1073741923
|
|
SDLK_F1 = 1073741882
|
|
SDLK_F2 = 1073741883
|
|
SDLK_F3 = 1073741884
|
|
SDLK_F4 = 1073741885
|
|
SDLK_F5 = 1073741886
|
|
SDLK_F6 = 1073741887
|
|
SDLK_F7 = 1073741888
|
|
SDLK_F8 = 1073741889
|
|
SDLK_F9 = 1073741890
|
|
SDLK_F10 = 1073741891
|
|
SDLK_F11 = 1073741892
|
|
SDLK_F12 = 1073741893
|
|
SDLK_F13 = 1073741894
|
|
SDLK_F14 = 1073741895
|
|
SDLK_F15 = 1073741896
|
|
|
|
|
|
class SDL2MotionEvent(MotionEvent):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
kwargs.setdefault('is_touch', True)
|
|
kwargs.setdefault('type_id', 'touch')
|
|
super().__init__(*args, **kwargs)
|
|
self.profile = ('pos', 'pressure')
|
|
|
|
def depack(self, args):
|
|
self.sx, self.sy, self.pressure = args
|
|
super().depack(args)
|
|
|
|
|
|
class SDL2MotionEventProvider(MotionEventProvider):
|
|
win = None
|
|
q = deque()
|
|
touchmap = {}
|
|
|
|
def update(self, dispatch_fn):
|
|
touchmap = self.touchmap
|
|
while True:
|
|
try:
|
|
value = self.q.pop()
|
|
except IndexError:
|
|
return
|
|
|
|
action, fid, x, y, pressure = value
|
|
y = 1 - y
|
|
if fid not in touchmap:
|
|
touchmap[fid] = me = SDL2MotionEvent(
|
|
'sdl', fid, (x, y, pressure)
|
|
)
|
|
else:
|
|
me = touchmap[fid]
|
|
me.move((x, y, pressure))
|
|
if action == 'fingerdown':
|
|
dispatch_fn('begin', me)
|
|
elif action == 'fingerup':
|
|
me.update_time_end()
|
|
dispatch_fn('end', me)
|
|
del touchmap[fid]
|
|
else:
|
|
dispatch_fn('update', me)
|
|
|
|
|
|
class WindowSDL(WindowBase):
|
|
|
|
_win_dpi_watch: Optional['_WindowsSysDPIWatch'] = None
|
|
|
|
_do_resize_ev = None
|
|
|
|
managed_textinput = True
|
|
|
|
def __init__(self, **kwargs):
|
|
self._pause_loop = False
|
|
self._cursor_entered = False
|
|
self._drop_pos = None
|
|
self._win = _WindowSDL2Storage()
|
|
super(WindowSDL, self).__init__()
|
|
self.titlebar_widget = None
|
|
self._mouse_x = self._mouse_y = -1
|
|
self._meta_keys = (
|
|
KMOD_LCTRL, KMOD_RCTRL, KMOD_RSHIFT,
|
|
KMOD_LSHIFT, KMOD_RALT, KMOD_LALT, KMOD_LGUI,
|
|
KMOD_RGUI, KMOD_NUM, KMOD_CAPS, KMOD_MODE)
|
|
self.command_keys = {
|
|
27: 'escape',
|
|
9: 'tab',
|
|
8: 'backspace',
|
|
13: 'enter',
|
|
127: 'del',
|
|
271: 'enter',
|
|
273: 'up',
|
|
274: 'down',
|
|
275: 'right',
|
|
276: 'left',
|
|
278: 'home',
|
|
279: 'end',
|
|
280: 'pgup',
|
|
281: 'pgdown'}
|
|
self._mouse_buttons_down = set()
|
|
self.key_map = {SDLK_LEFT: 276, SDLK_RIGHT: 275, SDLK_UP: 273,
|
|
SDLK_DOWN: 274, SDLK_HOME: 278, SDLK_END: 279,
|
|
SDLK_PAGEDOWN: 281, SDLK_PAGEUP: 280, SDLK_SHIFTR: 303,
|
|
SDLK_SHIFTL: 304, SDLK_SUPER: 309, SDLK_LCTRL: 305,
|
|
SDLK_RCTRL: 306, SDLK_LALT: 308, SDLK_RALT: 307,
|
|
SDLK_CAPS: 301, SDLK_INSERT: 277, SDLK_F1: 282,
|
|
SDLK_F2: 283, SDLK_F3: 284, SDLK_F4: 285, SDLK_F5: 286,
|
|
SDLK_F6: 287, SDLK_F7: 288, SDLK_F8: 289, SDLK_F9: 290,
|
|
SDLK_F10: 291, SDLK_F11: 292, SDLK_F12: 293,
|
|
SDLK_F13: 294, SDLK_F14: 295, SDLK_F15: 296,
|
|
SDLK_KEYPADNUM: 300, SDLK_KP_DIVIDE: 267,
|
|
SDLK_KP_MULTIPLY: 268, SDLK_KP_MINUS: 269,
|
|
SDLK_KP_PLUS: 270, SDLK_KP_ENTER: 271,
|
|
SDLK_KP_DOT: 266, SDLK_KP_0: 256, SDLK_KP_1: 257,
|
|
SDLK_KP_2: 258, SDLK_KP_3: 259, SDLK_KP_4: 260,
|
|
SDLK_KP_5: 261, SDLK_KP_6: 262, SDLK_KP_7: 263,
|
|
SDLK_KP_8: 264, SDLK_KP_9: 265}
|
|
if platform == 'ios':
|
|
# XXX ios keyboard suck, when backspace is hit, the delete
|
|
# keycode is sent. fix it.
|
|
self.key_map[127] = 8
|
|
elif platform == 'android':
|
|
# map android back button to escape
|
|
self.key_map[1073742094] = 27
|
|
|
|
self.bind(minimum_width=self._set_minimum_size,
|
|
minimum_height=self._set_minimum_size)
|
|
|
|
self.bind(allow_screensaver=self._set_allow_screensaver)
|
|
|
|
def get_window_info(self):
|
|
return self._win.get_window_info()
|
|
|
|
def _set_minimum_size(self, *args):
|
|
minimum_width = self.minimum_width
|
|
minimum_height = self.minimum_height
|
|
if minimum_width and minimum_height:
|
|
self._win.set_minimum_size(minimum_width, minimum_height)
|
|
elif minimum_width or minimum_height:
|
|
Logger.warning(
|
|
'Both Window.minimum_width and Window.minimum_height must be '
|
|
'bigger than 0 for the size restriction to take effect.')
|
|
|
|
def _set_allow_screensaver(self, *args):
|
|
self._win.set_allow_screensaver(self.allow_screensaver)
|
|
|
|
def _event_filter(self, action, *largs):
|
|
from kivy.app import App
|
|
if action == 'app_terminating':
|
|
EventLoop.quit = True
|
|
|
|
elif action == 'app_lowmemory':
|
|
self.dispatch('on_memorywarning')
|
|
|
|
elif action == 'app_willenterbackground':
|
|
from kivy.base import stopTouchApp
|
|
app = App.get_running_app()
|
|
if not app:
|
|
Logger.info('WindowSDL: No running App found, pause.')
|
|
|
|
elif not app.dispatch('on_pause'):
|
|
Logger.info(
|
|
'WindowSDL: App doesn\'t support pause mode, stop.')
|
|
stopTouchApp()
|
|
return 0
|
|
|
|
self._pause_loop = True
|
|
|
|
elif action == 'app_didenterforeground':
|
|
# on iOS, the did enter foreground is launched at the start
|
|
# of the application. in our case, we want it only when the app
|
|
# is resumed
|
|
if self._pause_loop:
|
|
self._pause_loop = False
|
|
app = App.get_running_app()
|
|
if app:
|
|
app.dispatch('on_resume')
|
|
|
|
elif action == 'windowresized':
|
|
self._size = largs
|
|
self._win.resize_window(*self._size)
|
|
# Force kivy to render the frame now, so that the canvas is drawn.
|
|
EventLoop.idle()
|
|
|
|
return 0
|
|
|
|
def create_window(self, *largs):
|
|
if self._fake_fullscreen:
|
|
if not self.borderless:
|
|
self.fullscreen = self._fake_fullscreen = False
|
|
elif not self.fullscreen or self.fullscreen == 'auto':
|
|
self.custom_titlebar = \
|
|
self.borderless = self._fake_fullscreen = False
|
|
elif self.custom_titlebar:
|
|
if platform == 'win':
|
|
# use custom behaviour
|
|
# To handle aero snapping and rounded corners
|
|
self.borderless = False
|
|
if self.fullscreen == 'fake':
|
|
self.borderless = self._fake_fullscreen = True
|
|
Logger.warning("The 'fake' fullscreen option has been "
|
|
"deprecated, use Window.borderless or the "
|
|
"borderless Config option instead.")
|
|
|
|
if not self.initialized:
|
|
if self.position == 'auto':
|
|
pos = None, None
|
|
elif self.position == 'custom':
|
|
pos = self.left, self.top
|
|
|
|
# ensure we have an event filter
|
|
self._win.set_event_filter(self._event_filter)
|
|
|
|
# setup window
|
|
w, h = self.system_size
|
|
resizable = Config.getboolean('graphics', 'resizable')
|
|
state = (Config.get('graphics', 'window_state')
|
|
if self._is_desktop else None)
|
|
self.system_size = _size = self._win.setup_window(
|
|
pos[0], pos[1], w, h, self.borderless,
|
|
self.fullscreen, resizable, state,
|
|
self.get_gl_backend_name())
|
|
|
|
# calculate density/dpi
|
|
if platform == 'win':
|
|
from ctypes import windll
|
|
self._density = 1.
|
|
try:
|
|
hwnd = windll.user32.GetActiveWindow()
|
|
self.dpi = float(windll.user32.GetDpiForWindow(hwnd))
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
sz = self._win._get_gl_size()[0]
|
|
self._density = density = sz / _size[0]
|
|
if self._is_desktop and self.size[0] != _size[0]:
|
|
self.dpi = density * 96.
|
|
|
|
# never stay with a None pos, application using w.center
|
|
# will be fired.
|
|
self._pos = (0, 0)
|
|
self._set_minimum_size()
|
|
self._set_allow_screensaver()
|
|
|
|
if state == 'hidden':
|
|
self._focus = False
|
|
else:
|
|
w, h = self.system_size
|
|
self._win.resize_window(w, h)
|
|
if platform == 'win':
|
|
if self.custom_titlebar:
|
|
# check dragging+resize or just dragging
|
|
if Config.getboolean('graphics', 'resizable'):
|
|
import win32con
|
|
import ctypes
|
|
self._win.set_border_state(False)
|
|
# make windows dispatch,
|
|
# WM_NCCALCSIZE explicitly
|
|
ctypes.windll.user32.SetWindowPos(
|
|
self._win.get_window_info().window,
|
|
win32con.HWND_TOP,
|
|
*self._win.get_window_pos(),
|
|
*self.system_size,
|
|
win32con.SWP_FRAMECHANGED
|
|
)
|
|
else:
|
|
self._win.set_border_state(True)
|
|
else:
|
|
self._win.set_border_state(self.borderless)
|
|
else:
|
|
self._win.set_border_state(self.borderless
|
|
or self.custom_titlebar)
|
|
self._win.set_fullscreen_mode(self.fullscreen)
|
|
|
|
super(WindowSDL, self).create_window()
|
|
# set mouse visibility
|
|
self._set_cursor_state(self.show_cursor)
|
|
|
|
if self.initialized:
|
|
return
|
|
|
|
# auto add input provider
|
|
Logger.info('Window: auto add sdl2 input provider')
|
|
from kivy.base import EventLoop
|
|
SDL2MotionEventProvider.win = self
|
|
EventLoop.add_input_provider(SDL2MotionEventProvider('sdl', ''))
|
|
|
|
# set window icon before calling set_mode
|
|
try:
|
|
filename_icon = self.icon or Config.get('kivy', 'window_icon')
|
|
if filename_icon == '':
|
|
logo_size = 32
|
|
if platform == 'macosx':
|
|
logo_size = 512
|
|
elif platform == 'win':
|
|
logo_size = 64
|
|
filename_icon = 'kivy-icon-{}.png'.format(logo_size)
|
|
filename_icon = resource_find(
|
|
join(kivy_data_dir, 'logo', filename_icon))
|
|
self.set_icon(filename_icon)
|
|
except:
|
|
Logger.exception('Window: cannot set icon')
|
|
|
|
if platform == 'win' and self._win_dpi_watch is None:
|
|
self._win_dpi_watch = _WindowsSysDPIWatch(window=self)
|
|
self._win_dpi_watch.start()
|
|
|
|
def close(self):
|
|
self._win.teardown_window()
|
|
super(WindowSDL, self).close()
|
|
if self._win_dpi_watch is not None:
|
|
self._win_dpi_watch.stop()
|
|
self._win_dpi_watch = None
|
|
|
|
self.initialized = False
|
|
|
|
def maximize(self):
|
|
if self._is_desktop:
|
|
self._win.maximize_window()
|
|
else:
|
|
Logger.warning('Window: maximize() is used only on desktop OSes.')
|
|
|
|
def minimize(self):
|
|
if self._is_desktop:
|
|
self._win.minimize_window()
|
|
else:
|
|
Logger.warning('Window: minimize() is used only on desktop OSes.')
|
|
|
|
def restore(self):
|
|
if self._is_desktop:
|
|
self._win.restore_window()
|
|
else:
|
|
Logger.warning('Window: restore() is used only on desktop OSes.')
|
|
|
|
def hide(self):
|
|
if self._is_desktop:
|
|
self._win.hide_window()
|
|
else:
|
|
Logger.warning('Window: hide() is used only on desktop OSes.')
|
|
|
|
def show(self):
|
|
if self._is_desktop:
|
|
self._win.show_window()
|
|
else:
|
|
Logger.warning('Window: show() is used only on desktop OSes.')
|
|
|
|
def raise_window(self):
|
|
if self._is_desktop:
|
|
self._win.raise_window()
|
|
else:
|
|
Logger.warning('Window: show() is used only on desktop OSes.')
|
|
|
|
@deprecated
|
|
def toggle_fullscreen(self):
|
|
if self.fullscreen in (True, 'auto'):
|
|
self.fullscreen = False
|
|
else:
|
|
self.fullscreen = 'auto'
|
|
|
|
def set_title(self, title):
|
|
self._win.set_window_title(title)
|
|
|
|
def set_icon(self, filename):
|
|
self._win.set_window_icon(str(filename))
|
|
|
|
def screenshot(self, *largs, **kwargs):
|
|
filename = super(WindowSDL, self).screenshot(*largs, **kwargs)
|
|
if filename is None:
|
|
return
|
|
|
|
from kivy.graphics.opengl import glReadPixels, GL_RGB, GL_UNSIGNED_BYTE
|
|
width, height = self.size
|
|
data = glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE)
|
|
self._win.save_bytes_in_png(filename, data, width, height)
|
|
Logger.debug('Window: Screenshot saved at <%s>' % filename)
|
|
return filename
|
|
|
|
def flip(self):
|
|
self._win.flip()
|
|
super(WindowSDL, self).flip()
|
|
|
|
def set_system_cursor(self, cursor_name):
|
|
result = self._win.set_system_cursor(cursor_name)
|
|
return result
|
|
|
|
def _get_window_pos(self):
|
|
return self._win.get_window_pos()
|
|
|
|
def _set_window_pos(self, x, y):
|
|
self._win.set_window_pos(x, y)
|
|
|
|
# Transparent Window background
|
|
def _is_shaped(self):
|
|
return self._win.is_window_shaped()
|
|
|
|
def _set_shape(self, shape_image, mode='default',
|
|
cutoff=False, color_key=None):
|
|
modes = ('default', 'binalpha', 'reversebinalpha', 'colorkey')
|
|
color_key = color_key or (0, 0, 0, 1)
|
|
if mode not in modes:
|
|
Logger.warning(
|
|
'Window: shape mode can be only '
|
|
'{}'.format(', '.join(modes))
|
|
)
|
|
return
|
|
if not isinstance(color_key, (tuple, list)):
|
|
return
|
|
if len(color_key) not in (3, 4):
|
|
return
|
|
if len(color_key) == 3:
|
|
color_key = (color_key[0], color_key[1], color_key[2], 1)
|
|
Logger.warning(
|
|
'Window: Shape color_key must be only tuple or list'
|
|
)
|
|
return
|
|
color_key = (
|
|
color_key[0] * 255,
|
|
color_key[1] * 255,
|
|
color_key[2] * 255,
|
|
color_key[3] * 255
|
|
)
|
|
|
|
assert cutoff in (1, 0)
|
|
shape_image = shape_image or Config.get('kivy', 'window_shape')
|
|
shape_image = resource_find(shape_image) or shape_image
|
|
self._win.set_shape(shape_image, mode, cutoff, color_key)
|
|
|
|
def _get_shaped_mode(self):
|
|
return self._win.get_shaped_mode()
|
|
|
|
def _set_shaped_mode(self, value):
|
|
self._set_shape(
|
|
shape_image=self.shape_image,
|
|
mode=value, cutoff=self.shape_cutoff,
|
|
color_key=self.shape_color_key
|
|
)
|
|
return self._win.get_shaped_mode()
|
|
# twb end
|
|
|
|
def _set_cursor_state(self, value):
|
|
self._win._set_cursor_state(value)
|
|
|
|
def _fix_mouse_pos(self, x, y):
|
|
self.mouse_pos = (
|
|
x * self._density,
|
|
(self.system_size[1] - 1 - y) * self._density
|
|
)
|
|
return x, y
|
|
|
|
def mainloop(self):
|
|
# for android/iOS, we don't want to have any event nor executing our
|
|
# main loop while the pause is going on. This loop wait any event (not
|
|
# handled by the event filter), and remove them from the queue.
|
|
# Nothing happen during the pause on iOS, except gyroscope value sent
|
|
# over joystick. So it's safe.
|
|
while self._pause_loop:
|
|
self._win.wait_event()
|
|
if not self._pause_loop:
|
|
break
|
|
event = self._win.poll()
|
|
if event is None:
|
|
continue
|
|
# A drop is send while the app is still in pause.loop
|
|
# we need to dispatch it
|
|
action, args = event[0], event[1:]
|
|
if action.startswith('drop'):
|
|
self._dispatch_drop_event(action, args)
|
|
# app_terminating event might be received while the app is paused
|
|
# in this case EventLoop.quit will be set at _event_filter
|
|
elif EventLoop.quit:
|
|
return
|
|
|
|
while True:
|
|
event = self._win.poll()
|
|
if event is False:
|
|
break
|
|
if event is None:
|
|
continue
|
|
|
|
action, args = event[0], event[1:]
|
|
if action == 'quit':
|
|
if self.dispatch('on_request_close'):
|
|
continue
|
|
EventLoop.quit = True
|
|
break
|
|
|
|
elif action in ('fingermotion', 'fingerdown', 'fingerup'):
|
|
# for finger, pass the raw event to SDL motion event provider
|
|
# XXX this is problematic. On OSX, it generates touches with 0,
|
|
# 0 coordinates, at the same times as mouse. But it works.
|
|
# We have a conflict of using either the mouse or the finger.
|
|
# Right now, we have no mechanism that we could use to know
|
|
# which is the preferred one for the application.
|
|
if platform in ('ios', 'android'):
|
|
SDL2MotionEventProvider.q.appendleft(event)
|
|
pass
|
|
|
|
elif action == 'mousemotion':
|
|
x, y = args
|
|
x, y = self._fix_mouse_pos(x, y)
|
|
self._mouse_x = x
|
|
self._mouse_y = y
|
|
if not self._cursor_entered:
|
|
self._cursor_entered = True
|
|
self.dispatch('on_cursor_enter')
|
|
# don't dispatch motion if no button are pressed
|
|
if len(self._mouse_buttons_down) == 0:
|
|
continue
|
|
self._mouse_meta = self.modifiers
|
|
self.dispatch('on_mouse_move', x, y, self.modifiers)
|
|
|
|
elif action in ('mousebuttondown', 'mousebuttonup'):
|
|
x, y, button = args
|
|
x, y = self._fix_mouse_pos(x, y)
|
|
self._mouse_x = x
|
|
self._mouse_y = y
|
|
if not self._cursor_entered:
|
|
self._cursor_entered = True
|
|
self.dispatch('on_cursor_enter')
|
|
btn = 'left'
|
|
if button == 3:
|
|
btn = 'right'
|
|
elif button == 2:
|
|
btn = 'middle'
|
|
elif button == 4:
|
|
btn = "mouse4"
|
|
elif button == 5:
|
|
btn = "mouse5"
|
|
eventname = 'on_mouse_down'
|
|
self._mouse_buttons_down.add(button)
|
|
if action == 'mousebuttonup':
|
|
eventname = 'on_mouse_up'
|
|
self._mouse_buttons_down.remove(button)
|
|
self.dispatch(eventname, x, y, btn, self.modifiers)
|
|
elif action.startswith('mousewheel'):
|
|
x, y = self._win.get_relative_mouse_pos()
|
|
if not self._collide_and_dispatch_cursor_enter(x, y):
|
|
# Ignore if the cursor position is on the window title bar
|
|
# or on its edges
|
|
continue
|
|
self._update_modifiers()
|
|
x, y, button = args
|
|
btn = 'scrolldown'
|
|
if action.endswith('up'):
|
|
btn = 'scrollup'
|
|
elif action.endswith('right'):
|
|
btn = 'scrollright'
|
|
elif action.endswith('left'):
|
|
btn = 'scrollleft'
|
|
|
|
self._mouse_meta = self.modifiers
|
|
self._mouse_btn = btn
|
|
# times = x if y == 0 else y
|
|
# times = min(abs(times), 100)
|
|
# for k in range(times):
|
|
self._mouse_down = True
|
|
self.dispatch('on_mouse_down',
|
|
self._mouse_x, self._mouse_y, btn, self.modifiers)
|
|
self._mouse_down = False
|
|
self.dispatch('on_mouse_up',
|
|
self._mouse_x, self._mouse_y, btn, self.modifiers)
|
|
|
|
elif action.startswith('drop'):
|
|
self._dispatch_drop_event(action, args)
|
|
# video resize
|
|
elif action == 'windowresized':
|
|
self._size = self._win.window_size
|
|
# don't use trigger here, we want to delay the resize event
|
|
ev = self._do_resize_ev
|
|
if ev is None:
|
|
ev = Clock.schedule_once(self._do_resize, .1)
|
|
self._do_resize_ev = ev
|
|
else:
|
|
ev()
|
|
|
|
elif action == 'windowmoved':
|
|
self.dispatch('on_move')
|
|
|
|
elif action == 'windowrestored':
|
|
self.dispatch('on_restore')
|
|
self.canvas.ask_update()
|
|
|
|
elif action == 'windowexposed':
|
|
self.canvas.ask_update()
|
|
|
|
elif action == 'windowminimized':
|
|
self.dispatch('on_minimize')
|
|
if Config.getboolean('kivy', 'pause_on_minimize'):
|
|
self.do_pause()
|
|
|
|
elif action == 'windowmaximized':
|
|
self.dispatch('on_maximize')
|
|
|
|
elif action == 'windowhidden':
|
|
self.dispatch('on_hide')
|
|
|
|
elif action == 'windowshown':
|
|
self.dispatch('on_show')
|
|
|
|
elif action == 'windowfocusgained':
|
|
self._focus = True
|
|
|
|
elif action == 'windowfocuslost':
|
|
self._focus = False
|
|
|
|
elif action == 'windowenter':
|
|
x, y = self._win.get_relative_mouse_pos()
|
|
self._collide_and_dispatch_cursor_enter(x, y)
|
|
|
|
elif action == 'windowleave':
|
|
self._cursor_entered = False
|
|
self.dispatch('on_cursor_leave')
|
|
|
|
elif action == 'joyaxismotion':
|
|
stickid, axisid, value = args
|
|
self.dispatch('on_joy_axis', stickid, axisid, value)
|
|
elif action == 'joyhatmotion':
|
|
stickid, hatid, value = args
|
|
self.dispatch('on_joy_hat', stickid, hatid, value)
|
|
elif action == 'joyballmotion':
|
|
stickid, ballid, xrel, yrel = args
|
|
self.dispatch('on_joy_ball', stickid, ballid, xrel, yrel)
|
|
elif action == 'joybuttondown':
|
|
stickid, buttonid = args
|
|
self.dispatch('on_joy_button_down', stickid, buttonid)
|
|
elif action == 'joybuttonup':
|
|
stickid, buttonid = args
|
|
self.dispatch('on_joy_button_up', stickid, buttonid)
|
|
|
|
elif action in ('keydown', 'keyup'):
|
|
mod, key, scancode, kstr = args
|
|
|
|
try:
|
|
key = self.key_map[key]
|
|
except KeyError:
|
|
pass
|
|
|
|
if action == 'keydown':
|
|
self._update_modifiers(mod, key)
|
|
else:
|
|
# ignore the key, it has been released
|
|
self._update_modifiers(mod)
|
|
|
|
# if mod in self._meta_keys:
|
|
if (key not in self._modifiers and
|
|
key not in self.command_keys.keys()):
|
|
try:
|
|
kstr_chr = unichr(key)
|
|
try:
|
|
# On android, there is no 'encoding' attribute.
|
|
# On other platforms, if stdout is redirected,
|
|
# 'encoding' may be None
|
|
encoding = getattr(sys.stdout, 'encoding',
|
|
'utf8') or 'utf8'
|
|
kstr_chr.encode(encoding)
|
|
kstr = kstr_chr
|
|
except UnicodeError:
|
|
pass
|
|
except ValueError:
|
|
pass
|
|
# if 'shift' in self._modifiers and key\
|
|
# not in self.command_keys.keys():
|
|
# return
|
|
|
|
if action == 'keyup':
|
|
self.dispatch('on_key_up', key, scancode)
|
|
continue
|
|
|
|
# don't dispatch more key if down event is accepted
|
|
if self.dispatch('on_key_down', key,
|
|
scancode, kstr,
|
|
self.modifiers):
|
|
continue
|
|
self.dispatch('on_keyboard', key,
|
|
scancode, kstr,
|
|
self.modifiers)
|
|
|
|
elif action == 'textinput':
|
|
text = args[0]
|
|
self.dispatch('on_textinput', text)
|
|
|
|
elif action == 'textedit':
|
|
text = args[0]
|
|
self.dispatch('on_textedit', text)
|
|
|
|
# unhandled event !
|
|
else:
|
|
Logger.trace('WindowSDL: Unhandled event %s' % str(event))
|
|
|
|
def _dispatch_drop_event(self, action, args):
|
|
x, y = (0, 0) if self._drop_pos is None else self._drop_pos
|
|
if action == 'dropfile':
|
|
self.dispatch('on_drop_file', args[0], x, y)
|
|
elif action == 'droptext':
|
|
self.dispatch('on_drop_text', args[0], x, y)
|
|
elif action == 'dropbegin':
|
|
self._drop_pos = x, y = self._win.get_relative_mouse_pos()
|
|
self._collide_and_dispatch_cursor_enter(x, y)
|
|
self.dispatch('on_drop_begin', x, y)
|
|
elif action == 'dropend':
|
|
self._drop_pos = None
|
|
self.dispatch('on_drop_end', x, y)
|
|
|
|
def _collide_and_dispatch_cursor_enter(self, x, y):
|
|
# x, y are relative to window left/top position
|
|
w, h = self._win.window_size
|
|
if 0 <= x < w and 0 <= y < h:
|
|
self._mouse_x, self._mouse_y = self._fix_mouse_pos(x, y)
|
|
if not self._cursor_entered:
|
|
self._cursor_entered = True
|
|
self.dispatch('on_cursor_enter')
|
|
return True
|
|
|
|
def _do_resize(self, dt):
|
|
Logger.debug('Window: Resize window to %s' % str(self.size))
|
|
self._win.resize_window(*self._size)
|
|
self.dispatch('on_pre_resize', *self.size)
|
|
|
|
def do_pause(self):
|
|
# should go to app pause mode (desktop style)
|
|
from kivy.app import App
|
|
from kivy.base import stopTouchApp
|
|
app = App.get_running_app()
|
|
if not app:
|
|
Logger.info('WindowSDL: No running App found, pause.')
|
|
elif not app.dispatch('on_pause'):
|
|
Logger.info('WindowSDL: App doesn\'t support pause mode, stop.')
|
|
stopTouchApp()
|
|
return
|
|
|
|
# XXX FIXME wait for sdl resume
|
|
while True:
|
|
event = self._win.poll()
|
|
if event is False:
|
|
continue
|
|
if event is None:
|
|
continue
|
|
|
|
action, args = event[0], event[1:]
|
|
if action == 'quit':
|
|
EventLoop.quit = True
|
|
break
|
|
elif action == 'app_willenterforeground':
|
|
break
|
|
elif action == 'windowrestored':
|
|
break
|
|
|
|
if app:
|
|
app.dispatch('on_resume')
|
|
|
|
def _update_modifiers(self, mods=None, key=None):
|
|
if mods is None and key is None:
|
|
return
|
|
modifiers = set()
|
|
|
|
if mods is not None:
|
|
if mods & (KMOD_RSHIFT | KMOD_LSHIFT):
|
|
modifiers.add('shift')
|
|
if mods & (KMOD_RALT | KMOD_LALT | KMOD_MODE):
|
|
modifiers.add('alt')
|
|
if mods & (KMOD_RCTRL | KMOD_LCTRL):
|
|
modifiers.add('ctrl')
|
|
if mods & (KMOD_RGUI | KMOD_LGUI):
|
|
modifiers.add('meta')
|
|
if mods & KMOD_NUM:
|
|
modifiers.add('numlock')
|
|
if mods & KMOD_CAPS:
|
|
modifiers.add('capslock')
|
|
|
|
if key is not None:
|
|
if key in (KMOD_RSHIFT, KMOD_LSHIFT):
|
|
modifiers.add('shift')
|
|
if key in (KMOD_RALT, KMOD_LALT, KMOD_MODE):
|
|
modifiers.add('alt')
|
|
if key in (KMOD_RCTRL, KMOD_LCTRL):
|
|
modifiers.add('ctrl')
|
|
if key in (KMOD_RGUI, KMOD_LGUI):
|
|
modifiers.add('meta')
|
|
if key == KMOD_NUM:
|
|
modifiers.add('numlock')
|
|
if key == KMOD_CAPS:
|
|
modifiers.add('capslock')
|
|
|
|
self._modifiers = list(modifiers)
|
|
return
|
|
|
|
def request_keyboard(
|
|
self, callback, target, input_type='text', keyboard_suggestions=True
|
|
):
|
|
self._sdl_keyboard = super(WindowSDL, self).\
|
|
request_keyboard(
|
|
callback, target, input_type, keyboard_suggestions
|
|
)
|
|
self._win.show_keyboard(
|
|
self._system_keyboard,
|
|
self.softinput_mode,
|
|
input_type,
|
|
keyboard_suggestions,
|
|
)
|
|
Clock.schedule_interval(self._check_keyboard_shown, 1 / 5.)
|
|
return self._sdl_keyboard
|
|
|
|
def release_keyboard(self, *largs):
|
|
super(WindowSDL, self).release_keyboard(*largs)
|
|
self._win.hide_keyboard()
|
|
self._sdl_keyboard = None
|
|
return True
|
|
|
|
def _check_keyboard_shown(self, dt):
|
|
if self._sdl_keyboard is None:
|
|
return False
|
|
if not self._win.is_keyboard_shown():
|
|
self._sdl_keyboard.release()
|
|
|
|
def map_key(self, original_key, new_key):
|
|
self.key_map[original_key] = new_key
|
|
|
|
def unmap_key(self, key):
|
|
if key in self.key_map:
|
|
del self.key_map[key]
|
|
|
|
def grab_mouse(self):
|
|
self._win.grab_mouse(True)
|
|
|
|
def ungrab_mouse(self):
|
|
self._win.grab_mouse(False)
|
|
|
|
def set_custom_titlebar(self, titlebar_widget):
|
|
if not self.custom_titlebar:
|
|
Logger.warning("Window: Window.custom_titlebar not set to True… "
|
|
"can't set custom titlebar")
|
|
return
|
|
self.titlebar_widget = titlebar_widget
|
|
return self._win.set_custom_titlebar(self.titlebar_widget) == 0
|
|
|
|
|
|
class _WindowsSysDPIWatch:
|
|
|
|
hwnd = None
|
|
|
|
new_windProc = None
|
|
|
|
old_windProc = None
|
|
|
|
window: WindowBase = None
|
|
|
|
def __init__(self, window: WindowBase):
|
|
self.window = window
|
|
|
|
def start(self):
|
|
from kivy.input.providers.wm_common import WNDPROC, \
|
|
SetWindowLong_WndProc_wrapper
|
|
from ctypes import windll
|
|
|
|
self.hwnd = windll.user32.GetActiveWindow()
|
|
|
|
# inject our own handler to handle messages before window manager
|
|
self.new_windProc = WNDPROC(self._wnd_proc)
|
|
self.old_windProc = SetWindowLong_WndProc_wrapper(
|
|
self.hwnd, self.new_windProc)
|
|
|
|
def stop(self):
|
|
from kivy.input.providers.wm_common import \
|
|
SetWindowLong_WndProc_wrapper
|
|
|
|
if self.hwnd is None:
|
|
return
|
|
|
|
self.new_windProc = SetWindowLong_WndProc_wrapper(
|
|
self.hwnd, self.old_windProc)
|
|
self.hwnd = self.new_windProc = self.old_windProc = None
|
|
|
|
def _wnd_proc(self, hwnd, msg, wParam, lParam):
|
|
from kivy.input.providers.wm_common import WM_DPICHANGED, WM_NCCALCSIZE
|
|
from ctypes import windll
|
|
|
|
if msg == WM_DPICHANGED:
|
|
ow, oh = self.window.size
|
|
old_dpi = self.window.dpi
|
|
|
|
def clock_callback(*args):
|
|
if x_dpi != y_dpi:
|
|
raise ValueError(
|
|
'Can only handle DPI that are same for x and y')
|
|
|
|
self.window.dpi = x_dpi
|
|
|
|
# maintain the same window size
|
|
ratio = x_dpi / old_dpi
|
|
self.window.size = ratio * ow, ratio * oh
|
|
|
|
x_dpi = wParam & 0xFFFF
|
|
y_dpi = wParam >> 16
|
|
Clock.schedule_once(clock_callback, -1)
|
|
elif Config.getboolean('graphics', 'resizable') \
|
|
and msg == WM_NCCALCSIZE and self.window.custom_titlebar:
|
|
return 0
|
|
return windll.user32.CallWindowProcW(
|
|
self.old_windProc, hwnd, msg, wParam, lParam)
|