Live Reload Kivy模板

时间:2016-11-28 04:54:27

标签: python kivy livereload kivy-language python-watchdog

我试图创建一些可以在运行时重新加载KivyLang UI文件(.kv文件)的东西。因此,当我在IDE上保存* .kv文件时,UI会在窗口上更新。有点类似于这个http://livereload.com/插件的工作方式。

这是我的尝试。

修改后的框架代码

我稍微修改了以下文件并添加了一些代码: https://github.com/kivy/kivy/blob/master/kivy/core/window/window_pygame.py

新进口

from watchdog.observers import Observer
from watchdog.events import PatternMatchingEventHandler
from kivy.app import App

新班级

class KivyLangWatcher(PatternMatchingEventHandler):
    patterns = ["*.kv"]

    def on_modified(self, event):
        app = App.get_running_app()
        app.load_kv(filename=app.kv_file)
        root = app.build()
        EventLoop.window.canvas.clear()
        EventLoop.window.add_widget(root)

新主循环

我刚刚和观察员夹在一起

    def mainloop(self):
        observer = Observer()
        observer.schedule(KivyLangWatcher(), '.')
        observer.start()

        while not EventLoop.quit and EventLoop.status == 'started':
            try:
                self._mainloop()
                if not pygame.display.get_active():
                    pygame.time.wait(100)
            except BaseException as inst:
                # use exception manager first
                r = ExceptionManager.handle_exception(inst)
                if r == ExceptionManager.RAISE:
                    stopTouchApp()
                    raise
                else:
                    pass
                observer.stop()

        observer.stop()
        observer.join()

完整档案

window_pygame.py

'''
Window Pygame: windowing provider based on Pygame
'''

__all__ = ('WindowPygame', )

# fail early if possible
import pygame

from watchdog.observers import Observer
from watchdog.events import PatternMatchingEventHandler
from kivy.app import App

from kivy.compat import PY2
from kivy.core.window import WindowBase
from kivy.core import CoreCriticalException
from os import environ
from os.path import exists, join
from kivy.config import Config
from kivy import kivy_data_dir
from kivy.base import ExceptionManager
from kivy.logger import Logger
from kivy.base import stopTouchApp, EventLoop
from kivy.utils import platform, deprecated
from kivy.resources import resource_find
from kivy.clock import Clock

try:
    android = None
    if platform == 'android':
        import android
except ImportError:
    pass

# late binding
glReadPixels = GL_RGBA = GL_UNSIGNED_BYTE = None


class KivyLangWatcher(PatternMatchingEventHandler):
    patterns = ["*.kv"]

    def on_modified(self, event):
        app = App.get_running_app()
        app.load_kv(filename=app.kv_file)
        root = app.build()
        EventLoop.window.canvas.clear()
        EventLoop.window.add_widget(root)


class WindowPygame(WindowBase):

    def create_window(self, *largs):
        # ensure the mouse is still not up after window creation, otherwise, we
        # have some weird bugs
        self.dispatch('on_mouse_up', 0, 0, 'all', [])

        # force display to show (available only for fullscreen)
        displayidx = Config.getint('graphics', 'display')
        if not 'SDL_VIDEO_FULLSCREEN_HEAD' in environ and displayidx != -1:
            environ['SDL_VIDEO_FULLSCREEN_HEAD'] = '%d' % displayidx

        # init some opengl, same as before.
        self.flags = pygame.HWSURFACE | pygame.OPENGL | pygame.DOUBLEBUF

        # right now, activate resizable window only on linux.
        # on window / macosx, the opengl context is lost, and we need to
        # reconstruct everything. Check #168 for a state of the work.
        if platform in ('linux', 'macosx', 'win') and \
                Config.getboolean('graphics', 'resizable'):
            self.flags |= pygame.RESIZABLE

        try:
            pygame.display.init()
        except pygame.error as e:
            raise CoreCriticalException(e.message)

        multisamples = Config.getint('graphics', 'multisamples')

        if multisamples > 0:
            pygame.display.gl_set_attribute(pygame.GL_MULTISAMPLEBUFFERS, 1)
            pygame.display.gl_set_attribute(pygame.GL_MULTISAMPLESAMPLES,
                                            multisamples)
        pygame.display.gl_set_attribute(pygame.GL_DEPTH_SIZE, 16)
        pygame.display.gl_set_attribute(pygame.GL_STENCIL_SIZE, 1)
        pygame.display.set_caption(self.title)

        if self.position == 'auto':
            self._pos = None
        elif self.position == 'custom':
            self._pos = self.left, self.top
        else:
            raise ValueError('position token in configuration accept only '
                             '"auto" or "custom"')

        if self._fake_fullscreen:
            if not self.borderless:
                self.fullscreen = self._fake_fullscreen = False
            elif not self.fullscreen or self.fullscreen == 'auto':
                self.borderless = self._fake_fullscreen = 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 self.fullscreen == 'fake' or self.borderless:
            Logger.debug('WinPygame: Set window to borderless mode.')

            self.flags |= pygame.NOFRAME
            # If no position set in borderless mode, we always need
            # to set the position. So use 0, 0.
            if self._pos is None:
                self._pos = (0, 0)
            environ['SDL_VIDEO_WINDOW_POS'] = '%d,%d' % self._pos

        elif self.fullscreen in ('auto', True):
            Logger.debug('WinPygame: Set window to fullscreen mode')
            self.flags |= pygame.FULLSCREEN

        elif self._pos is not None:
            environ['SDL_VIDEO_WINDOW_POS'] = '%d,%d' % self._pos

        # never stay with a None pos, application using w.center will be fired.
        self._pos = (0, 0)

        # prepare keyboard
        repeat_delay = int(Config.get('kivy', 'keyboard_repeat_delay'))
        repeat_rate = float(Config.get('kivy', 'keyboard_repeat_rate'))
        pygame.key.set_repeat(repeat_delay, int(1000. / repeat_rate))

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

        # try to use mode with multisamples
        try:
            self._pygame_set_mode()
        except pygame.error as e:
            if multisamples:
                Logger.warning('WinPygame: Video: failed (multisamples=%d)' %
                               multisamples)
                Logger.warning('WinPygame: trying without antialiasing')
                pygame.display.gl_set_attribute(
                    pygame.GL_MULTISAMPLEBUFFERS, 0)
                pygame.display.gl_set_attribute(
                    pygame.GL_MULTISAMPLESAMPLES, 0)
                multisamples = 0
                try:
                    self._pygame_set_mode()
                except pygame.error as e:
                    raise CoreCriticalException(e.message)
            else:
                raise CoreCriticalException(e.message)

        if pygame.RESIZABLE & self.flags:
            self._pygame_set_mode()

        info = pygame.display.Info()
        self._size = (info.current_w, info.current_h)
        #self.dispatch('on_resize', *self._size)

        # in order to debug futur issue with pygame/display, let's show
        # more debug output.
        Logger.debug('Window: Display driver ' + pygame.display.get_driver())
        Logger.debug('Window: Actual window size: %dx%d',
                     info.current_w, info.current_h)
        if platform != 'android':
            # unsupported platform, such as android that doesn't support
            # gl_get_attribute.
            Logger.debug(
                'Window: Actual color bits r%d g%d b%d a%d',
                pygame.display.gl_get_attribute(pygame.GL_RED_SIZE),
                pygame.display.gl_get_attribute(pygame.GL_GREEN_SIZE),
                pygame.display.gl_get_attribute(pygame.GL_BLUE_SIZE),
                pygame.display.gl_get_attribute(pygame.GL_ALPHA_SIZE))
            Logger.debug(
                'Window: Actual depth bits: %d',
                pygame.display.gl_get_attribute(pygame.GL_DEPTH_SIZE))
            Logger.debug(
                'Window: Actual stencil bits: %d',
                pygame.display.gl_get_attribute(pygame.GL_STENCIL_SIZE))
            Logger.debug(
                'Window: Actual multisampling samples: %d',
                pygame.display.gl_get_attribute(pygame.GL_MULTISAMPLESAMPLES))
        super(WindowPygame, self).create_window()

        # set mouse visibility
        self._set_cursor_state(self.show_cursor)

        # if we are on android platform, automaticly create hooks
        if android:
            from kivy.support import install_android
            install_android()

    def close(self):
        pygame.display.quit()
        self.dispatch('on_close')

    def on_title(self, instance, value):
        if self.initialized:
            pygame.display.set_caption(self.title)

    def set_icon(self, filename):
        if not exists(filename):
            return False
        try:
            if platform == 'win':
                try:
                    if self._set_icon_win(filename):
                        return True
                except:
                    # fallback on standard loading then.
                    pass

            # for all others platform, or if the ico is not available, use the
            # default way to set it.
            self._set_icon_standard(filename)
            super(WindowPygame, self).set_icon(filename)
        except:
            Logger.exception('WinPygame: unable to set icon')

    def _set_icon_standard(self, filename):
        if PY2:
            try:
                im = pygame.image.load(filename)
            except UnicodeEncodeError:
                im = pygame.image.load(filename.encode('utf8'))
        else:
            im = pygame.image.load(filename)
        if im is None:
            raise Exception('Unable to load window icon (not found)')
        pygame.display.set_icon(im)

    def _set_icon_win(self, filename):
        # ensure the window ico is ended by ico
        if not filename.endswith('.ico'):
            filename = '{}.ico'.format(filename.rsplit('.', 1)[0])
        if not exists(filename):
            return False

        import win32api
        import win32gui
        import win32con
        hwnd = pygame.display.get_wm_info()['window']
        icon_big = win32gui.LoadImage(
            None, filename, win32con.IMAGE_ICON,
            48, 48, win32con.LR_LOADFROMFILE)
        icon_small = win32gui.LoadImage(
            None, filename, win32con.IMAGE_ICON,
            16, 16, win32con.LR_LOADFROMFILE)
        win32api.SendMessage(
            hwnd, win32con.WM_SETICON, win32con.ICON_SMALL, icon_small)
        win32api.SendMessage(
            hwnd, win32con.WM_SETICON, win32con.ICON_BIG, icon_big)
        return True

    def _set_cursor_state(self, value):
        pygame.mouse.set_visible(value)

    def screenshot(self, *largs, **kwargs):
        global glReadPixels, GL_RGBA, GL_UNSIGNED_BYTE
        filename = super(WindowPygame, self).screenshot(*largs, **kwargs)
        if filename is None:
            return None
        if glReadPixels is None:
            from kivy.graphics.opengl import (glReadPixels, GL_RGBA,
                                              GL_UNSIGNED_BYTE)
        width, height = self.system_size
        data = glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE)
        if PY2:
            data = str(buffer(data))
        else:
            data = bytes(bytearray(data))
        surface = pygame.image.fromstring(data, (width, height), 'RGBA', True)
        pygame.image.save(surface, filename)
        Logger.debug('Window: Screenshot saved at <%s>' % filename)
        return filename

    def flip(self):
        pygame.display.flip()
        super(WindowPygame, self).flip()

    @deprecated
    def toggle_fullscreen(self):
        if self.flags & pygame.FULLSCREEN:
            self.flags &= ~pygame.FULLSCREEN
        else:
            self.flags |= pygame.FULLSCREEN
        self._pygame_set_mode()

    def _mainloop(self):
        EventLoop.idle()

        for event in pygame.event.get():

            # kill application (SIG_TERM)
            if event.type == pygame.QUIT:
                if self.dispatch('on_request_close'):
                    continue
                EventLoop.quit = True
                self.close()

            # mouse move
            elif event.type == pygame.MOUSEMOTION:
                x, y = event.pos
                self.mouse_pos = x, self.system_size[1] - y
                # don't dispatch motion if no button are pressed
                if event.buttons == (0, 0, 0):
                    continue
                self._mouse_x = x
                self._mouse_y = y
                self._mouse_meta = self.modifiers
                self.dispatch('on_mouse_move', x, y, self.modifiers)

            # mouse action
            elif event.type in (pygame.MOUSEBUTTONDOWN,
                                pygame.MOUSEBUTTONUP):
                self._pygame_update_modifiers()
                x, y = event.pos
                btn = 'left'
                if event.button == 3:
                    btn = 'right'
                elif event.button == 2:
                    btn = 'middle'
                elif event.button == 4:
                    btn = 'scrolldown'
                elif event.button == 5:
                    btn = 'scrollup'
                elif event.button == 6:
                    btn = 'scrollright'
                elif event.button == 7:
                    btn = 'scrollleft'
                eventname = 'on_mouse_down'
                if event.type == pygame.MOUSEBUTTONUP:
                    eventname = 'on_mouse_up'
                self._mouse_x = x
                self._mouse_y = y
                self._mouse_meta = self.modifiers
                self._mouse_btn = btn
                self._mouse_down = eventname == 'on_mouse_down'
                self.dispatch(eventname, x, y, btn, self.modifiers)

            # joystick action
            elif event.type == pygame.JOYAXISMOTION:
                self.dispatch('on_joy_axis', event.joy, event.axis, event.value)

            elif event.type == pygame.JOYHATMOTION:
                self.dispatch('on_joy_hat', event.joy, event.hat, event.value)

            elif event.type == pygame.JOYBALLMOTION:
                self.dispatch('on_joy_ball', event.joy, event.ballid,
                            event.rel[0], event.rel[1])

            elif event.type == pygame.JOYBUTTONDOWN:
                self.dispatch('on_joy_button_down', event.joy, event.button)

            elif event.type == pygame.JOYBUTTONUP:
                self.dispatch('on_joy_button_up', event.joy, event.button)

            # keyboard action
            elif event.type in (pygame.KEYDOWN, pygame.KEYUP):
                self._pygame_update_modifiers(event.mod)
                # atm, don't handle keyup
                if event.type == pygame.KEYUP:
                    self.dispatch('on_key_up', event.key,
                                  event.scancode)
                    continue

                # don't dispatch more key if down event is accepted
                if self.dispatch('on_key_down', event.key,
                                 event.scancode, event.unicode,
                                 self.modifiers):
                    continue
                self.dispatch('on_keyboard', event.key,
                              event.scancode, event.unicode,
                              self.modifiers)

            # video resize
            elif event.type == pygame.VIDEORESIZE:
                self._size = event.size
                self.update_viewport()

            elif event.type == pygame.VIDEOEXPOSE:
                self.canvas.ask_update()

            # ignored event
            elif event.type == pygame.ACTIVEEVENT:
                pass

            # drop file (pygame patch needed)
            elif event.type == pygame.USEREVENT and \
                    hasattr(pygame, 'USEREVENT_DROPFILE') and \
                    event.code == pygame.USEREVENT_DROPFILE:
                self.dispatch('on_dropfile', event.filename)

            '''
            # unhandled event !
            else:
                Logger.debug('WinPygame: Unhandled event %s' % str(event))
            '''

    def mainloop(self):
        observer = Observer()
        observer.schedule(KivyLangWatcher(), '.')
        observer.start()

        while not EventLoop.quit and EventLoop.status == 'started':
            try:
                self._mainloop()
                if not pygame.display.get_active():
                    pygame.time.wait(100)
            except BaseException as inst:
                # use exception manager first
                r = ExceptionManager.handle_exception(inst)
                if r == ExceptionManager.RAISE:
                    stopTouchApp()
                    raise
                else:
                    pass
                observer.stop()

        observer.stop()
        observer.join()

    #
    # Pygame wrapper
    #
    def _pygame_set_mode(self, size=None):
        if size is None:
            size = self.size
        if self.fullscreen == 'auto':
            pygame.display.set_mode((0, 0), self.flags)
        else:
            pygame.display.set_mode(size, self.flags)

    def _pygame_update_modifiers(self, mods=None):
        # Available mod, from dir(pygame)
        # 'KMOD_ALT', 'KMOD_CAPS', 'KMOD_CTRL', 'KMOD_LALT',
        # 'KMOD_LCTRL', 'KMOD_LMETA', 'KMOD_LSHIFT', 'KMOD_META',
        # 'KMOD_MODE', 'KMOD_NONE'
        if mods is None:
            mods = pygame.key.get_mods()
        self._modifiers = []
        if mods & (pygame.KMOD_SHIFT | pygame.KMOD_LSHIFT):
            self._modifiers.append('shift')
        if mods & (pygame.KMOD_ALT | pygame.KMOD_LALT):
            self._modifiers.append('alt')
        if mods & (pygame.KMOD_CTRL | pygame.KMOD_LCTRL):
            self._modifiers.append('ctrl')
        if mods & (pygame.KMOD_META | pygame.KMOD_LMETA):
            self._modifiers.append('meta')

    def request_keyboard(self, callback, target, input_type='text'):
        keyboard = super(WindowPygame, self).request_keyboard(
            callback, target, input_type)
        if android and not self.allow_vkeyboard:
            android.show_keyboard(target, input_type)
        return keyboard

    def release_keyboard(self, *largs):
        super(WindowPygame, self).release_keyboard(*largs)
        if android:
            android.hide_keyboard()
        return True

问题

到目前为止一切顺利。文件观察器工作正常,每次修改* .kv文件时都会触发on_modified函数。

但重装部分仅在我有1个固定小部件时才有效。当我修改.kv文件并添加更多小部件时,画布没有完全清理,之前的小部件仍在那里。

看起来EventLoop.window.canvas.clear()不足以清理以前的内容。

我向错误的方向移动太远了吗? 我注意到这里有类似的东西:https://github.com/kivy/pythonar但我不想停止正在运行的应用程序并重新加载。

示例Hello World App

hello_kivy.py

import kivy

from kivy.app import App
from kivy.uix.button import Button

kivy.require('1.9.1')


class HelloKivyApp(App):
    def build(self):
        return Button()

hello_kivy = HelloKivyApp()
hello_kivy.run()

hellokivy.kv

<Button>:
    text: "Hello Kivy :)"
    color: 0, 1, 0, 1

0 个答案:

没有答案