# 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)