コード例 #1
0
ファイル: storypixies.py プロジェクト: netpixies/storypixies
class Creator(Screen):
    stories = DictProperty()
    settings_panel = ObjectProperty(allownone=True)
    set_story = ObjectProperty()
    set_library = StringProperty()
    story_items = ListProperty()

    def __init__(self, **kwargs):
        super(Creator, self).__init__(**kwargs)
        self.stories = {}
        self.story_items = {}

    def on_pre_enter(self):
        self.settings_panel = None
        self.stories = {}

        for library in self.app.libraries.keys():
            for story in self.app.libraries[library].stories:
                self.add_story(library, story)

        for library in self.app.templates.keys():
            for story in self.app.templates[library].stories:
                self.add_story(library, story)

    def add_page(self, config):
        pages = self.set_story.pages
        if len(pages) == 0:
            new_page = "1"
        else:
            new_page = str(int(pages[-1]) + 1)

        self.set_story.pages.append(new_page)
        title = config.get('metadata', 'story')
        library = config.get('metadata', 'library')
        config.set('metadata', 'pages', "{}".format(','.join(pages[1:])))
        config.setdefaults(new_page, get_page_defaults(new_page))
        self.settings_panel.add_json_panel(new_page, config,
                                           data=get_story_settings_page(title,
                                                                        new_page,
                                                                        library))
        config.write()

    '''
    Settings Panel Methods Start
    '''
    def setup_settings_panel(self):
        self.settings_panel = LibrarySettings()
        self.settings_panel.bind(on_close=self.dismiss_settings_panel)

    def dismiss_settings_panel(self, _):
        self.app.manager.current = 'creator'

    '''
    Edit Story Methods Start
    '''
    def set_edit_selections(self, library, story, _):
        self.edit_story_id.text = "{}: {}".format(library, story.title)
        self.edit_story_id.story = story
        self.edit_story_id.library = library

    def get_edit_stories(self):
        story_item_list = []
        for library in self.app.libraries.keys():
            for story in self.app.libraries[library].stories:
                story_item_list.append({'viewclass': 'MDMenuItem',
                                        'text': "{}: {}".format(library, story.title),
                                        'callback': partial(self.set_edit_selections, library, story)})

        for library in self.app.templates.keys():
            for story in self.app.templates[library].stories:
                story_item_list.append({'viewclass': 'MDMenuItem',
                                        'text': "{}: {}".format(library, story.title),
                                        'callback': partial(self.set_edit_selections, library, story)})
        return story_item_list

    def edit_story(self, edit_story_id):
        if edit_story_id.library is None or edit_story_id.story is None:
            Snackbar("Please select a story to edit first.").show()
            return

        self.set_library = edit_story_id.library
        self.set_story = edit_story_id.story


        self.setup_settings_panel()
        self.app.story_title_screen()

    '''
    Create Story Methods Start
    '''
    def set_new_selections(self, library, _):
        self.new_story_library_id.library = library

    def get_libraries(self):
        library_item_list = []
        for library in self.app.libraries.keys():
            library_item_list.append({'viewclass': 'MDMenuItem',
                                      'text': "{}".format(library),
                                      'callback': partial(self.set_new_selections, library)})
        for library in self.app.templates.keys():
            library_item_list.append({'viewclass': 'MDMenuItem',
                                      'text': "{}".format(library),
                                      'callback': partial(self.set_new_selections, library)})

        return library_item_list

    def add_story(self, library, story):
        self.stories["{}: {}".format(library, story.title)] = {'library': library,
                                                               'story': story}

    def create_new_story(self, name, library):
        if len(name) == 0:
            Snackbar("Please enter a new story name first!").show()
            return

        if library is None:
            Snackbar("Please select a library to add your new story!").show()
            return

        self.set_library = library
        if "{}: {}".format(library, name) in self.stories:
            Snackbar("Cannot create story. '{}' already exists in {}.".format(name, library)).show()
            return

        new_story = self.app.libraries[library].add_new_story(name)

        if new_story is None:
            Snackbar("Could not create new story.").show()
            return

        self.set_story = name

        self.add_story(library, new_story)
        self.set_story = new_story
        self.set_library = library
        self.setup_settings_panel()

        new_page = str(int(self.set_story.pages[-1]))

        self.set_story.story_config.setdefaults(new_page, get_page_defaults(new_page))
        self.settings_panel.add_json_panel(new_page, self.set_story.story_config,
                                           data=get_story_settings_page(self.set_story.title,
                                                                        new_page,
                                                                        library))
        self.set_story.story_config.write()
        self.setup_settings_panel()
        self.app.story_title_screen()

    '''
    Copy Story Methods Start
    '''
    def set_copy_library_selections(self, library, _):
        self.copy_story_library_id.library = library

    def get_copy_libraries(self):
        library_item_list = []
        for library in self.app.libraries.keys():
            library_item_list.append({'viewclass': 'MDMenuItem',
                                      'text': "{}".format(library),
                                      'callback': partial(self.set_copy_library_selections, library)})
        for library in self.app.templates.keys():
            library_item_list.append({'viewclass': 'MDMenuItem',
                                      'text': "{}".format(library),
                                      'callback': partial(self.set_copy_library_selections, library)})

        return library_item_list

    def set_copy_selections(self, library, story, _):
        self.copy_story_from_box.text = "{}: {}".format(library, story.title)
        self.copy_story_from_box.story = story
        self.copy_story_from_box.library = library

    def get_copy_stories(self):
        story_item_list = []
        for library in self.app.libraries.keys():
            for story in self.app.libraries[library].stories:
                story_item_list.append({'viewclass': 'MDMenuItem',
                                        'text': "{}: {}".format(library, story.title),
                                        'callback': partial(self.set_copy_selections, library, story)})

        for library in self.app.templates.keys():
            for story in self.app.templates[library].stories:
                story_item_list.append({'viewclass': 'MDMenuItem',
                                        'text': "{}: {}".format(library, story.title),
                                        'callback': partial(self.set_copy_selections, library, story)})
        return story_item_list

    def copy_story_from_ids(self, copy_story_from_box, copy_story_library_id, copy_story_box):
        source_story = copy_story_from_box.text
        dest_library = copy_story_library_id.library
        new_name = copy_story_box.text

        if source_story is None:
            Snackbar("Please select a source story to copy.").show()
            return

        if dest_library is None:
            Snackbar("Please select a destination library.").show()
            return

        if len(new_name) == 0:
            Snackbar("Please enter a new story name.").show()
            return

        self.copy_story(source_story, dest_library, new_name)

    def copy_story(self, story, library, new_name):
        if "{}: {}".format(library, new_name) in self.stories:
            Snackbar("Cannot create story. '{}' already exists in {}.".format(new_name, library)).show()
            return

        if self.stories[story]['library'] == library:
            source_story_file = Path(self.stories[story]['story'].story_config_file)
            dest_story_file = source_story_file.parent.joinpath("{}.ini".format(new_name))
            dest_story_file.write_bytes(source_story_file.read_bytes())
            new_story = self.app.libraries[library].add_new_story(new_name)
            if new_story is None:
                return None

            self.add_story(library, new_story)
            self.set_story = new_story
            self.set_library = library
            self.setup_settings_panel()
        else:
            source_story_file = Path(self.stories[story]['story'].story_config_file)
            dest_story_file = self.app.library_dir.joinpath(library).joinpath("{}.ini".format(new_name))
            dest_story_file.write_bytes(source_story_file.read_bytes())
            new_story = self.app.libraries[library].add_new_story(new_name)
            if new_story is None:
                return None

            self.add_story(library, new_story)
            self.set_story = new_story
            self.set_library = library
            self.setup_settings_panel()

        self.setup_settings_panel()
        self.app.story_title_screen()

    '''
    Create Library Methods Start
    '''
    def create_new_library(self, library):
        if len(library) == 0:
            Snackbar("Please enter a new library name.").show()
            return

        library_path = self.app.library_dir.joinpath(library)
        if library_path.exists():
            Snackbar("Library already exists. Select new name.").show()
            return

        library_path.mkdir()

        self.app.libraries[library] = SingleLibrary(name=library, library_dir=library_path)
        self.copy_story("Templates: All About You", library, "All About You")
        self.setup_settings_panel()
        self.app.story_title_screen()
コード例 #2
0
class Label(Widget):
    '''Label class, see module documentation for more information.

    :Events:
        `on_ref_press`
            Fired when the user clicks on a word referenced with a
            ``[ref]`` tag in a text markup.
    '''

    __events__ = ['on_ref_press']

    _font_properties = ('text', 'font_size', 'font_name', 'bold', 'italic',
                        'underline', 'strikethrough',
                        'halign', 'valign', 'padding_x', 'padding_y',
                        'outline', 'outline_color', 'outline_width',
                        'text_size', 'shorten', 'mipmap', 'markup',
                        'line_height', 'max_lines', 'strip', 'shorten_from',
                        'split_str', 'unicode_errors',
                        'font_hinting', 'font_kerning', 'font_blended')

    def __init__(self, **kwargs):
        self._trigger_texture = Clock.create_trigger(self.texture_update, -1)
        super(Label, self).__init__(**kwargs)

        # bind all the property for recreating the texture
        d = Label._font_properties
        fbind = self.fbind
        update = self._trigger_texture_update
        for x in d:
            fbind(x, update, x)

        self._label = None
        self._create_label()

        fbind('markup', self._bind_for_markup)
        if self.markup:
            self._bind_for_markup(self, self.markup)

        # force the texture creation
        self._trigger_texture()

    def _bind_for_markup(self, inst, markup):
        if markup:
            self.fbind('color', self._trigger_texture_update, 'color')
        else:
            self.funbind('color', self._trigger_texture_update, 'color')

    def _create_label(self):
        # create the core label class according to markup value
        if self._label is not None:
            cls = self._label.__class__
        else:
            cls = None
        markup = self.markup
        if (markup and cls is not CoreMarkupLabel) or \
           (not markup and cls is not CoreLabel):
            # markup have change, we need to change our rendering method.
            d = Label._font_properties
            dkw = dict(list(zip(d, [getattr(self, x) for x in d])))
            if markup:
                self._label = CoreMarkupLabel(**dkw)
            else:
                self._label = CoreLabel(**dkw)

    def _trigger_texture_update(self, name=None, source=None, value=None):
        # check if the label core class need to be switch to a new one
        if name == 'markup':
            self._create_label()
        if source:
            if name == 'text':
                self._label.text = value
            elif name == 'text_size':
                self._label.usersize = value
            elif name == 'font_size':
                self._label.options[name] = value
            else:
                self._label.options[name] = value
        self._trigger_texture()

    def texture_update(self, *largs):
        '''Force texture recreation with the current Label properties.

        After this function call, the :attr:`texture` and :attr:`texture_size`
        will be updated in this order.
        '''
        mrkup = self._label.__class__ is CoreMarkupLabel
        self.texture = None

        if (not self._label.text or (self.halign[-1] == 'y' or self.strip) and
            not self._label.text.strip()):
            self.texture_size = (0, 0)
            if mrkup:
                self.refs, self._label._refs = {}, {}
                self.anchors, self._label._anchors = {}, {}
        else:
            if mrkup:
                text = self.text
                # we must strip here, otherwise, if the last line is empty,
                # markup will retain the last empty line since it only strips
                # line by line within markup
                if self.halign[-1] == 'y' or self.strip:
                    text = text.strip()
                self._label.text = ''.join(('[color=',
                                            get_hex_from_color(self.color),
                                            ']', text, '[/color]'))
                self._label.refresh()
                # force the rendering to get the references
                if self._label.texture:
                    self._label.texture.bind()
                self.refs = self._label.refs
                self.anchors = self._label.anchors
            else:
                self._label.refresh()
            texture = self._label.texture
            if texture is not None:
                self.texture = self._label.texture
                self.texture_size = list(self.texture.size)

    def on_touch_down(self, touch):
        if super(Label, self).on_touch_down(touch):
            return True
        if not len(self.refs):
            return False
        tx, ty = touch.pos
        tx -= self.center_x - self.texture_size[0] / 2.
        ty -= self.center_y - self.texture_size[1] / 2.
        ty = self.texture_size[1] - ty
        for uid, zones in self.refs.items():
            for zone in zones:
                x, y, w, h = zone
                if x <= tx <= w and y <= ty <= h:
                    self.dispatch('on_ref_press', uid)
                    return True
        return False

    def on_ref_press(self, ref):
        pass

    #
    # Properties
    #

    disabled_color = ListProperty([1, 1, 1, .3])
    '''Text color, in the format (r, g, b, a)

    .. versionadded:: 1.8.0

    :attr:`disabled_color` is a :class:`~kivy.properties.ListProperty` and
    defaults to [1, 1, 1, .5].
    '''

    text = StringProperty('')
    '''Text of the label.

    Creation of a simple hello world::

        widget = Label(text='Hello world')

    If you want to create the widget with an unicode string, use::

        widget = Label(text=u'My unicode string')

    :attr:`text` is a :class:`~kivy.properties.StringProperty` and defaults to
    ''.
    '''

    text_size = ListProperty([None, None])
    '''By default, the label is not constrained to any bounding box.
    You can set the size constraint of the label with this property.
    The text will autoflow into the constrains. So although the font size
    will not be reduced, the text will be arranged to fit into the box as best
    as possible, with any text still outside the box clipped.

    This sets and clips :attr:`texture_size` to text_size if not None.

    .. versionadded:: 1.0.4

    For example, whatever your current widget size is, if you want the label to
    be created in a box with width=200 and unlimited height::

        Label(text='Very big big line', text_size=(200, None))

    .. note::

        This text_size property is the same as the
        :attr:`~kivy.core.text.Label.usersize` property in the
        :class:`~kivy.core.text.Label` class. (It is named size= in the
        constructor.)

    :attr:`text_size` is a :class:`~kivy.properties.ListProperty` and
    defaults to (None, None), meaning no size restriction by default.
    '''

    font_name = StringProperty('Roboto')
    '''Filename of the font to use. The path can be absolute or relative.
    Relative paths are resolved by the :func:`~kivy.resources.resource_find`
    function.

    .. warning::

        Depending of your text provider, the font file can be ignored. However,
        you can mostly use this without problems.

        If the font used lacks the glyphs for the particular language/symbols
        you are using, you will see '[]' blank box characters instead of the
        actual glyphs. The solution is to use a font that has the glyphs you
        need to display. For example, to display |unicodechar|, use a font such
        as freesans.ttf that has the glyph.

        .. |unicodechar| image:: images/unicode-char.png

    :attr:`font_name` is a :class:`~kivy.properties.StringProperty` and
    defaults to 'Roboto'.
    '''

    font_size = NumericProperty('15sp')
    '''Font size of the text, in pixels.

    :attr:`font_size` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 15sp.
    '''

    line_height = NumericProperty(1.0)
    '''Line Height for the text. e.g. line_height = 2 will cause the spacing
    between lines to be twice the size.

    :attr:`line_height` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 1.0.

    .. versionadded:: 1.5.0
    '''

    bold = BooleanProperty(False)
    '''Indicates use of the bold version of your font.

    .. note::

        Depending of your font, the bold attribute may have no impact on your
        text rendering.

    :attr:`bold` is a :class:`~kivy.properties.BooleanProperty` and defaults to
    False.
    '''

    italic = BooleanProperty(False)
    '''Indicates use of the italic version of your font.

    .. note::

        Depending of your font, the italic attribute may have no impact on your
        text rendering.

    :attr:`italic` is a :class:`~kivy.properties.BooleanProperty` and defaults
    to False.
    '''

    underline = BooleanProperty(False)
    '''Adds an underline to the text.

    .. note::
        This feature requires a SDL2 window provider.

    .. versionadded:: 1.9.2

    :attr:`underline` is a :class:`~kivy.properties.BooleanProperty` and defaults
    to False.
    '''

    strikethrough = BooleanProperty(False)
    '''Adds a strikethrough line to the text.

    .. note::
        This feature requires a SDL2 window provider.

    .. versionadded:: 1.9.2

    :attr:`strikethrough` is a :class:`~kivy.properties.BooleanProperty` and defaults
    to False.
    '''

    padding_x = NumericProperty(0)
    '''Horizontal padding of the text inside the widget box.

    :attr:`padding_x` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 0.

    .. versionchanged:: 1.9.0
        `padding_x` has been fixed to work as expected.
        In the past, the text was padded by the negative of its values.
    '''

    padding_y = NumericProperty(0)
    '''Vertical padding of the text inside the widget box.

    :attr:`padding_y` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 0.

    .. versionchanged:: 1.9.0
        `padding_y` has been fixed to work as expected.
        In the past, the text was padded by the negative of its values.
    '''

    padding = ReferenceListProperty(padding_x, padding_y)
    '''Padding of the text in the format (padding_x, padding_y)

    :attr:`padding` is a :class:`~kivy.properties.ReferenceListProperty` of
    (:attr:`padding_x`, :attr:`padding_y`) properties.
    '''

    halign = OptionProperty('left', options=['left', 'center', 'right',
                            'justify'])
    '''Horizontal alignment of the text.

    :attr:`halign` is an :class:`~kivy.properties.OptionProperty` and
    defaults to 'left'. Available options are : left, center, right and
    justify.

    .. warning::

        This doesn't change the position of the text texture of the Label
        (centered), only the position of the text in this texture. You probably
        want to bind the size of the Label to the :attr:`texture_size` or set a
        :attr:`text_size`.

    .. versionchanged:: 1.6.0
        A new option was added to :attr:`halign`, namely `justify`.
    '''

    valign = OptionProperty('bottom', options=['bottom', 'middle', 'top'])
    '''Vertical alignment of the text.

    :attr:`valign` is an :class:`~kivy.properties.OptionProperty` and defaults
    to 'bottom'. Available options are : bottom, middle and top.

    .. warning::

        This doesn't change the position of the text texture of the Label
        (centered), only the position of the text within this texture. You
        probably want to bind the size of the Label to the :attr:`texture_size`
        or set a :attr:`text_size` to change this behavior.
    '''

    color = ListProperty([1, 1, 1, 1])
    '''Text color, in the format (r, g, b, a)

    :attr:`color` is a :class:`~kivy.properties.ListProperty` and defaults to
    [1, 1, 1, 1].
    '''

    outline = BooleanProperty(False)
    '''Indicates addition of an outline around the font.

    .. note::

        SDL2 only

    :attr:`outline` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to False.

    .. versionadded:: 1.9.2
    '''

    outline_width = NumericProperty('1dp')
    '''Width in pixels for the outline. e.g. line_height = 2dp will cause
    a two pixel outline to be rendered around the font.

    :attr:`outline_width` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 1dp.

    .. versionadded:: 1.9.2
    '''

    outline_color = ListProperty([0, 0, 0, 1])
    '''SDL2 outline color, in the format (r, g, b, a)

    :attr:`outline_color` is a :class:`~kivy.properties.ListProperty` and
    defaults to [0, 0, 0, 1].

    .. versionadded:: 1.9.2
    '''

    texture = ObjectProperty(None, allownone=True)
    '''Texture object of the text.
    The text is rendered automatically when a property changes. The OpenGL
    texture created in this operation is stored in this property. You can use
    this :attr:`texture` for any graphics elements.

    Depending on the texture creation, the value will be a
    :class:`~kivy.graphics.texture.Texture` or
    :class:`~kivy.graphics.texture.TextureRegion` object.

    .. warning::

        The :attr:`texture` update is scheduled for the next frame. If you need
        the texture immediately after changing a property, you have to call
        the :meth:`texture_update` method before accessing :attr:`texture`::

            l = Label(text='Hello world')
            # l.texture is good
            l.font_size = '50sp'
            # l.texture is not updated yet
            l.texture_update()
            # l.texture is good now.

    :attr:`texture` is an :class:`~kivy.properties.ObjectProperty` and defaults
    to None.
    '''

    texture_size = ListProperty([0, 0])
    '''Texture size of the text. The size is determined by the font size and
    text. If :attr:`text_size` is [None, None], the texture will be the size
    required to fit the text, otherwise it's clipped to fit :attr:`text_size`.

    When :attr:`text_size` is [None, None], one can bind to texture_size
    and rescale it proportionally to fit the size of the label in order to
    make the text fit maximally in the label.

    .. warning::

        The :attr:`texture_size` is set after the :attr:`texture`
        property. If you listen for changes to :attr:`texture`,
        :attr:`texture_size` will not be up-to-date in your callback.
        Bind to :attr:`texture_size` instead.
    '''

    mipmap = BooleanProperty(False)
    '''Indicates whether OpenGL mipmapping is applied to the texture or not.
    Read :ref:`mipmap` for more information.

    .. versionadded:: 1.0.7

    :attr:`mipmap` is a :class:`~kivy.properties.BooleanProperty` and defaults
    to False.
    '''

    shorten = BooleanProperty(False)
    '''
    Indicates whether the label should attempt to shorten its textual contents
    as much as possible if a :attr:`text_size` is given. Setting this to True
    without an appropriately set :attr:`text_size` will lead to unexpected
    results.

    :attr:`shorten_from` and :attr:`split_str` control the direction from
    which the :attr:`text` is split, as well as where in the :attr:`text` we
    are allowed to split.

    :attr:`shorten` is a :class:`~kivy.properties.BooleanProperty` and defaults
    to False.
    '''

    shorten_from = OptionProperty('center', options=['left', 'center',
                                                     'right'])
    '''The side from which we should shorten the text from, can be left,
    right, or center.

    For example, if left, the ellipsis will appear towards the left side and we
    will display as much text starting from the right as possible. Similar to
    :attr:`shorten`, this option only applies when :attr:`text_size` [0] is
    not None, In this case, the string is shortened to fit within the specified
    width.

    .. versionadded:: 1.9.0

    :attr:`shorten_from` is a :class:`~kivy.properties.OptionProperty` and
    defaults to `center`.
    '''

    split_str = StringProperty('')
    '''The string used to split the :attr:`text` while shortening the string
    when :attr:`shorten` is True.

    For example, if it's a space, the string will be broken into words and as
    many whole words that can fit into a single line will be displayed. If
    :attr:`shorten_from` is the empty string, `''`, we split on every character
    fitting as much text as possible into the line.

    .. versionadded:: 1.9.0

    :attr:`split_str` is a :class:`~kivy.properties.StringProperty` and
    defaults to `''` (the empty string).
    '''

    unicode_errors = OptionProperty(
        'replace', options=('strict', 'replace', 'ignore'))
    '''How to handle unicode decode errors. Can be `'strict'`, `'replace'` or
    `'ignore'`.

    .. versionadded:: 1.9.0

    :attr:`unicode_errors` is an :class:`~kivy.properties.OptionProperty` and
    defaults to `'replace'`.
    '''

    markup = BooleanProperty(False)
    '''
    .. versionadded:: 1.1.0

    If True, the text will be rendered using the
    :class:`~kivy.core.text.markup.MarkupLabel`: you can change the
    style of the text using tags. Check the
    :doc:`api-kivy.core.text.markup` documentation for more information.

    :attr:`markup` is a :class:`~kivy.properties.BooleanProperty` and defaults
    to False.
    '''

    refs = DictProperty({})
    '''
    .. versionadded:: 1.1.0

    List of ``[ref=xxx]`` markup items in the text with the bounding box of
    all the words contained in a ref, available only after rendering.

    For example, if you wrote::

        Check out my [ref=hello]link[/ref]

    The refs will be set with::

        {'hello': ((64, 0, 78, 16), )}

    The references marked "hello" have a bounding box at (x1, y1, x2, y2).
    These co-ordinates are relative to the top left corner of the text, with
    the y value increasing downwards. You can define multiple refs with the same
    name: each occurence will be added as another (x1, y1, x2, y2) tuple to
    this list.

    The current Label implementation uses these references if they exist in
    your markup text, automatically doing the collision with the touch and
    dispatching an `on_ref_press` event.

    You can bind a ref event like this::

        def print_it(instance, value):
            print('User click on', value)
        widget = Label(text='Hello [ref=world]World[/ref]', markup=True)
        widget.on_ref_press(print_it)

    .. note::

        This works only with markup text. You need :attr:`markup` set to
        True.
    '''

    anchors = DictProperty({})
    '''
    .. versionadded:: 1.1.0

    Position of all the ``[anchor=xxx]`` markup in the text.
    These co-ordinates are relative to the top left corner of the text, with
    the y value increasing downwards. Anchors names should be unique and only
    the first occurence of any duplicate anchors will be recorded.


    You can place anchors in your markup text as follows::

        text = """
            [anchor=title1][size=24]This is my Big title.[/size]
            [anchor=content]Hello world
        """

    Then, all the ``[anchor=]`` references will be removed and you'll get all
    the anchor positions in this property (only after rendering)::

        >>> widget = Label(text=text, markup=True)
        >>> widget.texture_update()
        >>> widget.anchors
        {"content": (20, 32), "title1": (20, 16)}

    .. note::

        This works only with markup text. You need :attr:`markup` set to
        True.

    '''

    max_lines = NumericProperty(0)
    '''Maximum number of lines to use, defaults to 0, which means unlimited.
    Please note that :attr:`shorten` take over this property. (with
    shorten, the text is always one line.)

    .. versionadded:: 1.8.0

    :attr:`max_lines` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 0.
    '''

    strip = BooleanProperty(False)
    '''Whether leading and trailing spaces and newlines should be stripped from
    each displayed line. If True, every line will start at the right or left
    edge, depending on :attr:`halign`. If :attr:`halign` is `justify` it is
    implicitly True.

    .. versionadded:: 1.9.0

    :attr:`strip` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to False.
    '''

    font_hinting = OptionProperty(
        'normal', options=[None, 'normal', 'light', 'mono'], allownone=True)
    '''What hinting option to use for font rendering.
    Can be one of `'normal'`, `'light'`, `'mono'` or None.

    .. note::
        This feature requires a SDL2 window provider.

    .. versionadded:: 1.9.2

    :attr:`font_hinting` is an :class:`~kivy.properties.OptionProperty` and
    defaults to `'normal'`.
    '''

    font_kerning = BooleanProperty(True)
    '''Whether kerning is enabled for font rendering.

    .. note::
        This feature requires a SDL2 window provider.

    .. versionadded:: 1.9.2

    :attr:`font_kerning` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to True.
    '''

    font_blended = BooleanProperty(True)
    '''Whether blended or solid font rendering should be used.
コード例 #3
0
class VKeyboard(Scatter):
    '''
    VKeyboard is an onscreen keyboard with multitouch support.
    Its layout is entirely customizable and you can switch between available
    layouts using a button in the bottom right of the widget.

    :Events:
        `on_key_down`: keycode, internal, modifiers
            Fired when the keyboard received a key down event (key press).
        `on_key_up`: keycode, internal, modifiers
            Fired when the keyboard received a key up event (key release).
    '''

    target = ObjectProperty(None, allownone=True)
    '''Target widget associated with the VKeyboard. If set, it will be used to
    send keyboard events. If the VKeyboard mode is "free", it will also be used
    to set the initial position.

    :attr:`target` is an :class:`~kivy.properties.ObjectProperty` instance and
    defaults to None.
    '''

    callback = ObjectProperty(None, allownone=True)
    '''Callback can be set to a function that will be called if the
    VKeyboard is closed by the user.

    :attr:`target` is an :class:`~kivy.properties.ObjectProperty` instance and
    defaults to None.
    '''

    layout = StringProperty(None)
    '''Layout to use for the VKeyboard. By default, it will be the
    layout set in the configuration, according to the `keyboard_layout`
    in `[kivy]` section.

    .. versionchanged:: 1.8.0
        If layout is a .json filename, it will loaded and added to the
        available_layouts.

    :attr:`layout` is a :class:`~kivy.properties.StringProperty` and defaults
    to None.
    '''

    layout_path = StringProperty(default_layout_path)
    '''Path from which layouts are read.

    :attr:`layout` is a :class:`~kivy.properties.StringProperty` and
    defaults to :file:`<kivy_data_dir>/keyboards/`
    '''

    available_layouts = DictProperty({})
    '''Dictionary of all available layouts. Keys are the layout ID, and the
    value is the JSON (translated into a Python object).

    :attr:`available_layouts` is a :class:`~kivy.properties.DictProperty` and
    defaults to {}.
    '''

    docked = BooleanProperty(False)
    '''Indicate whether the VKeyboard is docked on the screen or not. If you
    change it, you must manually call :meth:`setup_mode` otherwise it will have
    no impact. If the VKeyboard is created by the Window, the docked mode will
    be automatically set by the configuration, using the `keyboard_mode` token
    in `[kivy]` section.

    :attr:`docked` is a :class:`~kivy.properties.BooleanProperty` and defaults
    to False.
    '''

    margin_hint = ListProperty([.05, .06, .05, .06])
    '''Margin hint, used as spacing between keyboard background and keys
    content. The margin is composed of four values, between 0 and 1::

        margin_hint = [top, right, bottom, left]

    The margin hints will be multiplied by width and height, according to their
    position.

    :attr:`margin_hint` is a :class:`~kivy.properties.ListProperty` and
    defaults to [.05, .06, .05, .06]
    '''

    key_margin = ListProperty([2, 2, 2, 2])
    '''Key margin, used to create space between keys. The margin is composed of
    four values, in pixels::

        key_margin = [top, right, bottom, left]

    :attr:`key_margin` is a :class:`~kivy.properties.ListProperty` and defaults
    to [2, 2, 2, 2]
    '''

    background_color = ListProperty([1, 1, 1, 1])
    '''Background color, in the format (r, g, b, a). If a background is
    set, the color will be combined with the background texture.

    :attr:`background_color` is a :class:`~kivy.properties.ListProperty` and
    defaults to [1, 1, 1, 1].
    '''

    background = StringProperty(
        'atlas://data/images/defaulttheme/vkeyboard_background')
    '''Filename of the background image.

    :attr:`background` a :class:`~kivy.properties.StringProperty` and defaults
    to :file:`atlas://data/images/defaulttheme/vkeyboard_background`.
    '''

    background_disabled = StringProperty(
        'atlas://data/images/defaulttheme/vkeyboard_disabled_background')
    '''Filename of the background image when vkeyboard is disabled.

    .. versionadded:: 1.8.0

    :attr:`background_disabled` is a
    :class:`~kivy.properties.StringProperty` and defaults to
    :file:`atlas://data/images/defaulttheme/vkeyboard__disabled_background`.

    '''

    key_background_color = ListProperty([1, 1, 1, 1])
    '''Key background color, in the format (r, g, b, a). If a key background is
    set, the color will be combined with the key background texture.

    :attr:`key_background_color` is a :class:`~kivy.properties.ListProperty`
    and defaults to [1, 1, 1, 1].
    '''

    key_background_normal = StringProperty(
        'atlas://data/images/defaulttheme/vkeyboard_key_normal')
    '''Filename of the key background image for use when no touches are active
    on the widget.

    :attr:`key_background_normal` a :class:`~kivy.properties.StringProperty`
    and defaults to
    :file:`atlas://data/images/defaulttheme/vkeyboard_key_normal`.
    '''

    key_disabled_background_normal = StringProperty(
        'atlas://data/images/defaulttheme/vkeyboard_key_normal')
    '''Filename of the key background image for use when no touches are active
    on the widget and vkeyboard is disabled.

    .. versionadded:: 1.8.0

    :attr:`key_disabled_background_normal` a
    :class:`~kivy.properties.StringProperty` and defaults to
    :file:`atlas://data/images/defaulttheme/vkeyboard_disabled_key_normal`.

    '''

    key_background_down = StringProperty(
        'atlas://data/images/defaulttheme/vkeyboard_key_down')
    '''Filename of the key background image for use when a touch is active
    on the widget.

    :attr:`key_background_down` a :class:`~kivy.properties.StringProperty`
    and defaults to
    :file:`atlas://data/images/defaulttheme/vkeyboard_key_down`.
    '''

    background_border = ListProperty([16, 16, 16, 16])
    '''Background image border. Used for controlling the
    :attr:`~kivy.graphics.vertex_instructions.BorderImage.border` property of
    the background.

    :attr:`background_border` is a :class:`~kivy.properties.ListProperty` and
    defaults to [16, 16, 16, 16]
    '''

    key_border = ListProperty([8, 8, 8, 8])
    '''Key image border. Used for controlling the
    :attr:`~kivy.graphics.vertex_instructions.BorderImage.border` property of
    the key.

    :attr:`key_border` is a :class:`~kivy.properties.ListProperty` and
    defaults to [16, 16, 16, 16]
    '''

    # XXX internal variables
    layout_mode = OptionProperty('normal',
                                 options=('normal', 'shift', 'special'))
    layout_geometry = DictProperty({})
    have_capslock = BooleanProperty(False)
    have_shift = BooleanProperty(False)
    have_special = BooleanProperty(False)
    active_keys = DictProperty({})
    font_size = NumericProperty('20dp')
    font_name = StringProperty('data/fonts/DejaVuSans.ttf')
    repeat_touch = ObjectProperty(allownone=True)

    _start_repeat_key_ev = None
    _repeat_key_ev = None

    __events__ = ('on_key_down', 'on_key_up', 'on_textinput')

    def __init__(self, **kwargs):
        # XXX move to style.kv
        if 'size_hint' not in kwargs:
            if 'size_hint_x' not in kwargs:
                self.size_hint_x = None
            if 'size_hint_y' not in kwargs:
                self.size_hint_y = None
        if 'size' not in kwargs:
            if 'width' not in kwargs:
                self.width = 700
            if 'height' not in kwargs:
                self.height = 200
        if 'scale_min' not in kwargs:
            self.scale_min = .4
        if 'scale_max' not in kwargs:
            self.scale_max = 1.6
        if 'docked' not in kwargs:
            self.docked = False

        layout_mode = self._trigger_update_layout_mode = Clock.create_trigger(
            self._update_layout_mode)
        layouts = self._trigger_load_layouts = Clock.create_trigger(
            self._load_layouts)
        layout = self._trigger_load_layout = Clock.create_trigger(
            self._load_layout)
        fbind = self.fbind

        fbind('docked', self.setup_mode)
        fbind('have_shift', layout_mode)
        fbind('have_capslock', layout_mode)
        fbind('have_special', layout_mode)
        fbind('layout_path', layouts)
        fbind('layout', layout)
        super(VKeyboard, self).__init__(**kwargs)

        # load all the layouts found in the layout_path directory
        self._load_layouts()

        # ensure we have default layouts
        available_layouts = self.available_layouts
        if not available_layouts:
            Logger.critical('VKeyboard: unable to load default layouts')

        # load the default layout from configuration
        if self.layout is None:
            self.layout = Config.get('kivy', 'keyboard_layout')
        else:
            # ensure the current layout is found on the available layout
            self._trigger_load_layout()

        # update layout mode (shift or normal)
        self._trigger_update_layout_mode()

        # create a top layer to draw active keys on
        with self.canvas:
            self.background_key_layer = Canvas()
            self.active_keys_layer = Canvas()

    def on_disabled(self, intance, value):
        self.refresh_keys()

    def _update_layout_mode(self, *l):
        # update mode according to capslock and shift key
        mode = self.have_capslock != self.have_shift
        mode = 'shift' if mode else 'normal'
        if self.have_special:
            mode = "special"
        if mode != self.layout_mode:
            self.layout_mode = mode
            self.refresh(False)

    def _load_layout(self, *largs):
        # ensure new layouts are loaded first
        if self._trigger_load_layouts.is_triggered:
            self._load_layouts()
            self._trigger_load_layouts.cancel()

        value = self.layout
        available_layouts = self.available_layouts

        # it's a filename, try to load it directly
        if self.layout[-5:] == '.json':
            if value not in available_layouts:
                fn = resource_find(self.layout)
                self._load_layout_fn(fn, self.layout)

        if not available_layouts:
            return
        if value not in available_layouts and value != 'qwerty':
            Logger.error('Vkeyboard: <%s> keyboard layout mentioned in '
                         'conf file was not found, fallback on qwerty' % value)
            self.layout = 'qwerty'
        self.refresh(True)

    def _load_layouts(self, *largs):
        # first load available layouts from json files
        # XXX fix to be able to reload layout when path is changing
        value = self.layout_path
        for fn in listdir(value):
            self._load_layout_fn(join(value, fn), basename(splitext(fn)[0]))

    def _load_layout_fn(self, fn, name):
        available_layouts = self.available_layouts
        if fn[-5:] != '.json':
            return
        with open(fn, 'r') as fd:
            json_content = fd.read()
            layout = loads(json_content)
        available_layouts[name] = layout

    def setup_mode(self, *largs):
        '''Call this method when you want to readjust the keyboard according to
        options: :attr:`docked` or not, with attached :attr:`target` or not:

        * If :attr:`docked` is True, it will call :meth:`setup_mode_dock`
        * If :attr:`docked` is False, it will call :meth:`setup_mode_free`

        Feel free to overload these methods to create new
        positioning behavior.
        '''
        if self.docked:
            self.setup_mode_dock()
        else:
            self.setup_mode_free()

    def setup_mode_dock(self, *largs):
        '''Setup the keyboard in docked mode.

        Dock mode will reset the rotation, disable translation, rotation and
        scale. Scale and position will be automatically adjusted to attach the
        keyboard to the bottom of the screen.

        .. note::
            Don't call this method directly, use :meth:`setup_mode` instead.
        '''
        self.do_translation = False
        self.do_rotation = False
        self.do_scale = False
        self.rotation = 0
        win = self.get_parent_window()
        scale = win.width / float(self.width)
        self.scale = scale
        self.pos = 0, 0
        win.bind(on_resize=self._update_dock_mode)

    def _update_dock_mode(self, win, *largs):
        scale = win.width / float(self.width)
        self.scale = scale
        self.pos = 0, 0

    def setup_mode_free(self):
        '''Setup the keyboard in free mode.

        Free mode is designed to let the user control the position and
        orientation of the keyboard. The only real usage is for a multiuser
        environment, but you might found other ways to use it.
        If a :attr:`target` is set, it will place the vkeyboard under the
        target.

        .. note::
            Don't call this method directly, use :meth:`setup_mode` instead.
        '''
        self.do_translation = True
        self.do_rotation = True
        self.do_scale = True
        target = self.target
        if not target:
            return

        # NOTE all math will be done in window point of view
        # determine rotation of the target
        a = Vector(1, 0)
        b = Vector(target.to_window(0, 0))
        c = Vector(target.to_window(1, 0)) - b
        self.rotation = -a.angle(c)

        # determine the position of center/top of the keyboard
        dpos = Vector(self.to_window(self.width / 2., self.height))

        # determine the position of center/bottom of the target
        cpos = Vector(target.to_window(target.center_x, target.y))

        # the goal now is to map both point, calculate the diff between them
        diff = dpos - cpos

        # we still have an issue, self.pos represent the bounding box,
        # not the 0,0 coordinate of the scatter. we need to apply also
        # the diff between them (inside and outside coordinate matrix).
        # It's hard to explain, but do a scheme on a paper, write all
        # the vector i'm calculating, and you'll understand. :)
        diff2 = Vector(self.x + self.width / 2., self.y + self.height) - \
            Vector(self.to_parent(self.width / 2., self.height))
        diff -= diff2

        # now we have a good "diff", set it as a pos.
        self.pos = -diff

    def change_layout(self):
        # XXX implement popup with all available layouts
        pass

    def refresh(self, force=False):
        '''(internal) Recreate the entire widget and graphics according to the
        selected layout.
        '''
        self.clear_widgets()
        if force:
            self.refresh_keys_hint()
        self.refresh_keys()
        self.refresh_active_keys_layer()

    def refresh_active_keys_layer(self):
        self.active_keys_layer.clear()

        active_keys = self.active_keys
        layout_geometry = self.layout_geometry
        background = resource_find(self.key_background_down)
        texture = Image(background, mipmap=True).texture

        with self.active_keys_layer:
            Color(1, 1, 1)
            for line_nb, index in active_keys.values():
                pos, size = layout_geometry['LINE_%d' % line_nb][index]
                BorderImage(texture=texture,
                            pos=pos,
                            size=size,
                            border=self.key_border)

    def refresh_keys_hint(self):
        layout = self.available_layouts[self.layout]
        layout_cols = layout['cols']
        layout_rows = layout['rows']
        layout_geometry = self.layout_geometry
        mtop, mright, mbottom, mleft = self.margin_hint

        # get relative EFFICIENT surface of the layout without external margins
        el_hint = 1. - mleft - mright
        eh_hint = 1. - mtop - mbottom
        ex_hint = 0 + mleft
        ey_hint = 0 + mbottom

        # get relative unit surface
        uw_hint = (1. / layout_cols) * el_hint
        uh_hint = (1. / layout_rows) * eh_hint
        layout_geometry['U_HINT'] = (uw_hint, uh_hint)

        # calculate individual key RELATIVE surface and pos (without key
        # margin)
        current_y_hint = ey_hint + eh_hint
        for line_nb in range(1, layout_rows + 1):
            current_y_hint -= uh_hint
            # get line_name
            line_name = '%s_%d' % (self.layout_mode, line_nb)
            line_hint = 'LINE_HINT_%d' % line_nb
            layout_geometry[line_hint] = []
            current_x_hint = ex_hint
            # go through the list of keys (tuples of 4)
            for key in layout[line_name]:
                # calculate relative pos, size
                layout_geometry[line_hint].append([
                    (current_x_hint, current_y_hint),
                    (key[3] * uw_hint, uh_hint)
                ])
                current_x_hint += key[3] * uw_hint

        self.layout_geometry = layout_geometry

    def refresh_keys(self):
        layout = self.available_layouts[self.layout]
        layout_rows = layout['rows']
        layout_geometry = self.layout_geometry
        w, h = self.size
        kmtop, kmright, kmbottom, kmleft = self.key_margin
        uw_hint, uh_hint = layout_geometry['U_HINT']

        for line_nb in range(1, layout_rows + 1):
            llg = layout_geometry['LINE_%d' % line_nb] = []
            llg_append = llg.append
            for key in layout_geometry['LINE_HINT_%d' % line_nb]:
                x_hint, y_hint = key[0]
                w_hint, h_hint = key[1]
                kx = x_hint * w
                ky = y_hint * h
                kw = w_hint * w
                kh = h_hint * h

                # now adjust, considering the key margin
                kx = int(kx + kmleft)
                ky = int(ky + kmbottom)
                kw = int(kw - kmleft - kmright)
                kh = int(kh - kmbottom - kmtop)

                pos = (kx, ky)
                size = (kw, kh)
                llg_append((pos, size))

        self.layout_geometry = layout_geometry
        self.draw_keys()

    def draw_keys(self):
        layout = self.available_layouts[self.layout]
        layout_rows = layout['rows']
        layout_geometry = self.layout_geometry
        layout_mode = self.layout_mode

        # draw background
        w, h = self.size

        background = resource_find(
            self.background_disabled if self.disabled else self.background)
        texture = Image(background, mipmap=True).texture
        self.background_key_layer.clear()
        with self.background_key_layer:
            Color(*self.background_color)
            BorderImage(texture=texture,
                        size=self.size,
                        border=self.background_border)

        # XXX seperate drawing the keys and the fonts to avoid
        # XXX reloading the texture each time

        # first draw keys without the font
        key_normal = resource_find(self.key_background_disabled_normal if self.
                                   disabled else self.key_background_normal)
        texture = Image(key_normal, mipmap=True).texture
        with self.background_key_layer:
            for line_nb in range(1, layout_rows + 1):
                for pos, size in layout_geometry['LINE_%d' % line_nb]:
                    BorderImage(texture=texture,
                                pos=pos,
                                size=size,
                                border=self.key_border)

        # then draw the text
        # calculate font_size
        font_size = int(w) / 46
        # draw
        for line_nb in range(1, layout_rows + 1):
            key_nb = 0
            for pos, size in layout_geometry['LINE_%d' % line_nb]:
                # retrieve the relative text
                text = layout[layout_mode + '_' + str(line_nb)][key_nb][0]
                l = Label(text=text,
                          font_size=font_size,
                          pos=pos,
                          size=size,
                          font_name=self.font_name)
                self.add_widget(l)
                key_nb += 1

    def on_key_down(self, *largs):
        pass

    def on_key_up(self, *largs):
        pass

    def on_textinput(self, *largs):
        pass

    def get_key_at_pos(self, x, y):
        w, h = self.size
        x_hint = x / w
        # focus on the surface without margins
        layout_geometry = self.layout_geometry
        layout = self.available_layouts[self.layout]
        layout_rows = layout['rows']
        mtop, mright, mbottom, mleft = self.margin_hint

        # get the line of the layout
        e_height = h - (mbottom + mtop) * h  # efficient height in pixels
        line_height = e_height / layout_rows  # line height in px
        y = y - mbottom * h
        line_nb = layout_rows - int(y / line_height)

        if line_nb > layout_rows:
            line_nb = layout_rows
        if line_nb < 1:
            line_nb = 1

        # get the key within the line
        key_index = ''
        current_key_index = 0
        for key in layout_geometry['LINE_HINT_%d' % line_nb]:
            if x_hint >= key[0][0] and x_hint < key[0][0] + key[1][0]:
                key_index = current_key_index
                break
            else:
                current_key_index += 1
        if key_index == '':
            return None

        # get the full character
        key = layout['%s_%d' % (self.layout_mode, line_nb)][key_index]

        return [key, (line_nb, key_index)]

    def collide_margin(self, x, y):
        '''Do a collision test, and return True if the (x, y) is inside the
        vkeyboard margin.
        '''
        mtop, mright, mbottom, mleft = self.margin_hint
        x_hint = x / self.width
        y_hint = y / self.height
        if x_hint > mleft and x_hint < 1. - mright \
                and y_hint > mbottom and y_hint < 1. - mtop:
            return False
        return True

    def process_key_on(self, touch):
        if not touch:
            return
        x, y = self.to_local(*touch.pos)
        key = self.get_key_at_pos(x, y)
        if not key:
            return

        key_data = key[0]
        displayed_char, internal, special_char, size = key_data
        line_nb, key_index = key[1]

        # save pressed key on the touch
        ud = touch.ud[self.uid] = {}
        ud['key'] = key

        # for caps lock or shift only:
        uid = touch.uid
        if special_char is not None:
            # Do not repeat special keys
            if special_char in ('capslock', 'shift', 'layout', 'special'):
                if self._start_repeat_key_ev is not None:
                    self._start_repeat_key_ev.cancel()
                    self._start_repeat_key_ev = None
                self.repeat_touch = None
            if special_char == 'capslock':
                self.have_capslock = not self.have_capslock
                uid = -1
            elif special_char == 'shift':
                self.have_shift = True
            elif special_char == 'special':
                self.have_special = True
            elif special_char == 'layout':
                self.change_layout()

        # send info to the bus
        b_keycode = special_char
        b_modifiers = self._get_modifiers()
        if self.get_parent_window().__class__.__module__ == \
            'kivy.core.window.window_sdl2' and internal:
            self.dispatch('on_textinput', internal)
        else:
            self.dispatch('on_key_down', b_keycode, internal, b_modifiers)

        # save key as an active key for drawing
        self.active_keys[uid] = key[1]
        self.refresh_active_keys_layer()

    def process_key_up(self, touch):
        uid = touch.uid
        if self.uid not in touch.ud:
            return

        # save pressed key on the touch
        key_data, key = touch.ud[self.uid]['key']
        displayed_char, internal, special_char, size = key_data

        # send info to the bus
        b_keycode = special_char
        b_modifiers = self._get_modifiers()
        self.dispatch('on_key_up', b_keycode, internal, b_modifiers)

        if special_char == 'capslock':
            uid = -1

        if uid in self.active_keys:
            self.active_keys.pop(uid, None)
            if special_char == 'shift':
                self.have_shift = False
            elif special_char == 'special':
                self.have_special = False
            if special_char == 'capslock' and self.have_capslock:
                self.active_keys[-1] = key
            self.refresh_active_keys_layer()

    def _get_modifiers(self):
        ret = []
        if self.have_shift:
            ret.append('shift')
        if self.have_capslock:
            ret.append('capslock')
        return ret

    def _start_repeat_key(self, *kwargs):
        self._repeat_key_ev = Clock.schedule_interval(self._repeat_key, 0.05)

    def _repeat_key(self, *kwargs):
        self.process_key_on(self.repeat_touch)

    def on_touch_down(self, touch):
        x, y = touch.pos
        if not self.collide_point(x, y):
            return
        if self.disabled:
            return True

        x, y = self.to_local(x, y)
        if not self.collide_margin(x, y):
            if self.repeat_touch is None:
                self._start_repeat_key_ev = Clock.schedule_once(
                    self._start_repeat_key, 0.5)
            self.repeat_touch = touch

            self.process_key_on(touch)
            touch.grab(self, exclusive=True)

        else:
            super(VKeyboard, self).on_touch_down(touch)
        return True

    def on_touch_up(self, touch):
        if touch.grab_current is self:
            self.process_key_up(touch)

            if self._start_repeat_key_ev is not None:
                self._start_repeat_key_ev.cancel()
                self._start_repeat_key_ev = None
            if touch == self.repeat_touch:
                if self._repeat_key_ev is not None:
                    self._repeat_key_ev.cancel()
                    self._repeat_key_ev = None
                self.repeat_touch = None

        return super(VKeyboard, self).on_touch_up(touch)
コード例 #4
0
class Board(RelativeLayout):
    """A graphical view onto a :class:`LiSE.Character`, resembling a game
    board.

    """
    app = ObjectProperty()
    character = ObjectProperty()
    wallpaper_path = StringProperty()
    spot = DictProperty({})
    pawn = DictProperty({})
    arrow = DictProperty({})
    wallpaper = ObjectProperty()
    kvlayoutback = ObjectProperty()
    arrowlayout = ObjectProperty()
    spotlayout = ObjectProperty()
    kvlayoutfront = ObjectProperty()
    wids = ReferenceListProperty(wallpaper, kvlayoutback, arrowlayout,
                                 spotlayout, kvlayoutfront)
    spots_unposd = ListProperty([])
    layout_tries = NumericProperty(5)
    tracking_vel = BooleanProperty(False)
    selection_candidates = ListProperty([])
    selection = ObjectProperty(allownone=True)
    keep_selection = ObjectProperty(False)
    adding_portal = BooleanProperty(False)
    reciprocal_portal = BooleanProperty(False)
    grabbing = BooleanProperty(True)
    grabbed = ObjectProperty(None, allownone=True)
    spot_cls = ObjectProperty(Spot)
    pawn_cls = ObjectProperty(Pawn)
    arrow_cls = ObjectProperty(Arrow, allownone=True)
    proto_arrow_cls = ObjectProperty(ArrowWidget)

    @property
    def widkwargs(self):
        return {'size_hint': (None, None), 'size': self.size, 'pos': (0, 0)}

    def on_touch_down(self, touch):
        """Check for collisions and select an appropriate entity."""
        if hasattr(self, '_lasttouch') and self._lasttouch == touch:
            return
        if not self.collide_point(*touch.pos):
            return
        touch.push()
        touch.apply_transform_2d(self.to_local)
        if self.app.selection:
            if self.app.selection.collide_point(*touch.pos):
                Logger.debug("Board: hit selection")
                touch.grab(self.app.selection)
        pawns = list(self.pawns_at(*touch.pos))
        if pawns:
            Logger.debug("Board: hit {} pawns".format(len(pawns)))
            self.selection_candidates = pawns
            if self.app.selection in self.selection_candidates:
                self.selection_candidates.remove(self.app.selection)
            touch.pop()
            return True
        spots = list(self.spots_at(*touch.pos))
        if spots:
            Logger.debug("Board: hit {} spots".format(len(spots)))
            self.selection_candidates = spots
            if self.adding_portal:
                self.origspot = self.selection_candidates.pop(0)
                self.protodest = Dummy(name='protodest',
                                       pos=touch.pos,
                                       size=(0, 0))
                self.add_widget(self.protodest)
                self.protodest.on_touch_down(touch)
                self.protoportal = self.proto_arrow_cls(
                    origin=self.origspot, destination=self.protodest)
                self.add_widget(self.protoportal)
                if self.reciprocal_portal:
                    self.protoportal2 = self.proto_arrow_cls(
                        destination=self.origspot, origin=self.protodest)
                    self.add_widget(self.protoportal2)
            touch.pop()
            return True
        arrows = list(self.arrows_at(*touch.pos))
        if arrows:
            Logger.debug("Board: hit {} arrows".format(len(arrows)))
            self.selection_candidates = arrows
            if self.app.selection in self.selection_candidates:
                self.selection_candidates.remove(self.app.selection)
            touch.pop()
            return True
        touch.pop()

    def on_touch_move(self, touch):
        """If an entity is selected, drag it."""
        if hasattr(self, '_lasttouch') and self._lasttouch == touch:
            return
        if self.app.selection in self.selection_candidates:
            self.selection_candidates.remove(self.app.selection)
        if self.app.selection:
            if not self.selection_candidates:
                self.keep_selection = True
            ret = super().on_touch_move(touch)
            return ret
        elif self.selection_candidates:
            for cand in self.selection_candidates:
                if cand.collide_point(*touch.pos):
                    self.app.selection = cand
                    cand.selected = True
                    touch.grab(cand)
                    ret = super().on_touch_move(touch)
                    return ret

    def portal_touch_up(self, touch):
        """Try to create a portal between the spots the user chose."""
        try:
            # If the touch ended upon a spot, and there isn't
            # already a portal between the origin and this
            # destination, create one.
            destspot = next(self.spots_at(*touch.pos))
            orig = self.origspot.proxy
            dest = destspot.proxy
            if not (orig.name in self.character.portal
                    and dest.name in self.character.portal[orig.name]):
                port = self.character.new_portal(orig.name, dest.name)
                self.arrowlayout.add_widget(self.make_arrow(port))
                # And another in the opposite direction if needed
                if (hasattr(self, 'protoportal2')
                        and not (orig.name in self.character.preportal
                                 and dest.name
                                 in self.character.preportal[orig.name])):
                    deport = self.character.new_portal(dest.name, orig.name)
                    self.arrowlayout.add_widget(self.make_arrow(deport))
        except StopIteration:
            pass
        self.remove_widget(self.protoportal)
        if hasattr(self, 'protoportal2'):
            self.remove_widget(self.protoportal2)
            del self.protoportal2
        self.remove_widget(self.protodest)
        del self.protoportal
        del self.protodest

    def on_touch_up(self, touch):
        """Delegate touch handling if possible, else select something."""
        if hasattr(self, '_lasttouch') and self._lasttouch == touch:
            return
        self._lasttouch = touch
        touch.push()
        touch.apply_transform_2d(self.to_local)
        if hasattr(self, 'protodest'):
            Logger.debug("Board: on_touch_up making a portal")
            touch.ungrab(self)
            ret = self.portal_touch_up(touch)
            touch.pop()
            return ret
        if self.app.selection and hasattr(self.app.selection, 'on_touch_up'):
            self.app.selection.dispatch('on_touch_up', touch)
        for candidate in self.selection_candidates:
            if candidate == self.app.selection:
                continue
            if candidate.collide_point(*touch.pos):
                Logger.debug("Board: selecting " + repr(candidate))
                if hasattr(candidate, 'selected'):
                    candidate.selected = True
                if hasattr(self.app.selection, 'selected'):
                    self.app.selection.selected = False
                self.app.selection = candidate
                self.keep_selection = True
                parent = candidate.parent
                parent.remove_widget(candidate)
                parent.add_widget(candidate)
                break
        if not self.keep_selection:
            Logger.debug("Board: deselecting " + repr(self.app.selection))
            if hasattr(self.app.selection, 'selected'):
                self.app.selection.selected = False
            self.app.selection = None
        self.keep_selection = False
        touch.ungrab(self)
        touch.pop()
        return

    def _pull_size(self, *args):
        if self.wallpaper.texture is None:
            Clock.schedule_once(self._pull_size, 0.001)
            return
        self.size = self.wallpaper.size = self.wallpaper.texture.size

    def _pull_image(self, *args):
        self.wallpaper.source = self.wallpaper_path
        self._pull_size()

    def on_parent(self, *args):
        """Create some subwidgets and trigger the first update."""
        if not self.parent or hasattr(self, '_parented'):
            return
        if not self.wallpaper_path:
            Logger.debug("Board: waiting for wallpaper_path")
            Clock.schedule_once(self.on_parent, 0)
            return
        self._parented = True
        self.wallpaper = Image(source=self.wallpaper_path)
        self.bind(wallpaper_path=self._pull_image)
        self._pull_size()
        self.kvlayoutback = KvLayoutBack(**self.widkwargs)
        self.arrowlayout = ArrowLayout(**self.widkwargs)
        self.spotlayout = FinalLayout(**self.widkwargs)
        self.kvlayoutfront = KvLayoutFront(**self.widkwargs)
        for wid in self.wids:
            self.add_widget(wid)
            wid.pos = 0, 0
            wid.size = self.size
            if wid is not self.wallpaper:
                self.bind(size=wid.setter('size'))
        self.update()

    def on_character(self, *args):
        if self.character is None:
            return
        if self.parent is None:
            Clock.schedule_once(self.on_character, 0)
            return

        self.engine = self.character.engine
        self.wallpaper_path = self.character.stat.setdefault(
            'wallpaper', 'wallpape.jpg')
        if '_control' not in self.character.stat or 'wallpaper' not in self.character.stat[
                '_control']:
            control = self.character.stat.setdefault('_control', {})
            control['wallpaper'] = 'textinput'
            self.character.stat['_control'] = control
        self.character.stat.connect(self._trigger_pull_wallpaper)
        self.trigger_update()

    def pull_wallpaper(self, *args):
        self.wallpaper_path = self.character.stat.setdefault(
            'wallpaper', 'wallpape.jpg')

    def _trigger_pull_wallpaper(self, *args, **kwargs):
        if kwargs['key'] != 'wallpaper':
            return
        Clock.unschedule(self.pull_wallpaper)
        Clock.schedule_once(self.pull_wallpaper, 0)

    @trigger
    def kv_updated(self, *args):
        self.unbind(wallpaper_path=self.kvlayoutback.setter('wallpaper_path'))
        for wid in self.wids:
            self.remove_widget(wid)
        self.kvlayoutback = KvLayoutBack(pos=(0, 0),
                                         wallpaper_path=self.wallpaper_path)
        self.bind(wallpaper_path=self.kvlayoutback.setter('wallpaper_path'))
        self.kvlayoutfront = KvLayoutFront(**self.widkwargs)
        self.size = self.kvlayoutback.size
        self.kvlayoutback.bind(size=self.setter('size'))
        for wid in self.wids:
            self.add_widget(wid)

    def make_pawn(self, thing):
        """Make a :class:`Pawn` to represent a :class:`Thing`, store it, and
        return it.

        """
        if thing["name"] in self.pawn:
            raise KeyError("Already have a Pawn for this Thing")
        r = self.pawn_cls(board=self, thing=thing)
        self.pawn[thing["name"]] = r
        return r

    def make_spot(self, place):
        """Make a :class:`Spot` to represent a :class:`Place`, store it, and
        return it.

        """
        if place["name"] in self.spot:
            raise KeyError("Already have a Spot for this Place")
        r = self.spot_cls(board=self, place=place)
        self.spot[place["name"]] = r
        if '_x' in place and '_y' in place:
            r.pos = (self.width * place['_x'], self.height * place['_y'])
        return r

    def make_arrow(self, portal):
        """Make an :class:`Arrow` to represent a :class:`Portal`, store it,
        and return it.

        """
        if (portal["origin"] not in self.spot
                or portal["destination"] not in self.spot):
            raise ValueError("An :class:`Arrow` should only be made after "
                             "the :class:`Spot`s it connects")
        if (portal["origin"] in self.arrow
                and portal["destination"] in self.arrow[portal["origin"]]):
            raise KeyError("Already have an Arrow for this Portal")
        r = self.arrow_cls(board=self,
                           portal=portal,
                           origspot=self.spot[portal['origin']],
                           destspot=self.spot[portal['destination']])
        if portal["origin"] not in self.arrow:
            self.arrow[portal["origin"]] = {}
        self.arrow[portal["origin"]][portal["destination"]] = r
        return r

    def rm_arrows_to_and_from(self, name):
        origs = list(self.arrow.keys())
        if name in origs:
            origs.remove(name)
            for dest in list(self.arrow[name].keys()):
                self.rm_arrow(name, dest)
        for orig in origs:
            if name in self.arrow[orig]:
                self.rm_arrow(orig, name)

    def rm_pawn(self, name, *args):
        """Remove the :class:`Pawn` by the given name."""
        if name not in self.pawn:
            raise KeyError("No Pawn named {}".format(name))
        # Currently there's no way to connect Pawns with Arrows but I
        # think there will be, so, insurance
        self.rm_arrows_to_and_from(name)
        pwn = self.pawn.pop(name)
        if pwn in self.selection_candidates:
            self.selection_candidates.remove(pwn)
        pwn.parent.remove_widget(pwn)

    def _trigger_rm_pawn(self, name):
        Clock.schedule_once(partial(self.rm_pawn, name), 0)

    def rm_spot(self, name, *args):
        """Remove the :class:`Spot` by the given name."""
        if name not in self.spot:
            raise KeyError("No Spot named {}".format(name))
        spot = self.spot.pop(name)
        if spot in self.selection_candidates:
            self.selection_candidates.remove(spot)
        pawns_here = list(spot.children)
        self.rm_arrows_to_and_from(name)
        self.spotlayout.remove_widget(spot)
        spot.canvas.clear()
        for pawn in pawns_here:
            self.rm_pawn(pawn.name)

    def _trigger_rm_spot(self, name):
        part = partial(self.rm_spot, name)
        Clock.unschedule(part)
        Clock.schedule_once(part, 0)

    def rm_arrow(self, orig, dest, *args):
        """Remove the :class:`Arrow` that goes from ``orig`` to ``dest``."""
        if (orig not in self.arrow or dest not in self.arrow[orig]):
            raise KeyError("No Arrow from {} to {}".format(orig, dest))
        arr = self.arrow[orig].pop(dest)
        if arr in self.selection_candidates:
            self.selection_candidates.remove(arr)
        self.arrowlayout.remove_widget(arr)

    def _trigger_rm_arrow(self, orig, dest):
        part = partial(self.rm_arrow, orig, dest)
        Clock.unschedule(part)
        Clock.schedule_once(part, 0)

    def graph_layout(self, graph):
        from networkx.drawing.layout import spring_layout
        return normalize_layout(spring_layout(graph))

    def discard_pawn(self, thingn, *args):
        if thingn in self.pawn:
            self.rm_pawn(thingn)

    def _trigger_discard_pawn(self, thing):
        part = partial(self.discard_pawn, thing)
        Clock.unschedule(part)
        Clock.schedule_once(part, 0)

    def remove_absent_pawns(self, *args):
        Logger.debug("Board: removing pawns absent from {}".format(
            self.character.name))
        for pawn_name in list(self.pawn.keys()):
            if pawn_name not in self.character.thing:
                self.rm_pawn(pawn_name)

    def discard_spot(self, placen, *args):
        if placen in self.spot:
            self.rm_spot(placen)

    def _trigger_discard_spot(self, place):
        Clock.schedule_once(partial(self.discard_spot, place), 0)

    def remove_absent_spots(self, *args):
        Logger.debug("Board: removing spots absent from {}".format(
            self.character.name))
        for spot_name in list(self.spot.keys()):
            if spot_name not in self.character.place:
                self.rm_spot(spot_name)

    def discard_arrow(self, orign, destn, *args):
        if (orign in self.arrow and destn in self.arrow[orign]):
            self.rm_arrow(orign, destn)

    def _trigger_discard_arrow(self, orig, dest):
        Clock.schedule_once(partial(self.discard_arrow, orig, dest), 0)

    def remove_absent_arrows(self, *args):
        Logger.debug("Board: removing arrows absent from {}".format(
            self.character.name))
        for arrow_origin in list(self.arrow.keys()):
            for arrow_destination in list(self.arrow[arrow_origin].keys()):
                if (arrow_origin not in self.character.portal
                        or arrow_destination
                        not in self.character.portal[arrow_origin]):
                    self.rm_arrow(arrow_origin, arrow_destination)

    def add_spot(self, placen, *args):
        if (placen in self.character.place and placen not in self.spot):
            self.spotlayout.add_widget(
                self.make_spot(self.character.place[placen]))

    def _trigger_add_spot(self, placen):
        Clock.schedule_once(partial(self.add_spot, placen), 0)

    def add_new_spots(self, *args):
        Logger.debug("Board: adding new spots to {}".format(
            self.character.name))
        places2add = []
        spots_unposd = []
        nodes_patch = {}
        for place_name in self.character.place:
            if place_name not in self.spot:
                place = self.character.place[place_name]
                places2add.append(place)
                patch = {}
                if '_image_paths' in place:
                    zeroes = [0] * len(place['_image_paths'])
                else:
                    patch['_image_paths'] = Spot.default_image_paths
                    zeroes = [0]
                if '_offxs' not in place:
                    patch['_offxs'] = zeroes
                if '_offys' not in place:
                    patch['_offys'] = zeroes
                if patch:
                    nodes_patch[place_name] = patch
        if nodes_patch:
            self.character.node.patch(nodes_patch)
        for place in places2add:
            spot = self.make_spot(place)
            self.spotlayout.add_widget(spot)
            if '_x' not in place or '_y' not in place:
                spots_unposd.append(spot)
        self.spots_unposd = spots_unposd

    def add_arrow(self, orign, destn, *args):
        if not (orign in self.character.portal
                and destn in self.character.portal[orign]):
            raise ValueError("No portal for arrow {}->{}".format(orign, destn))
        if not (orign in self.arrow and destn in self.arrow[orign]):
            self.arrowlayout.add_widget(
                self.make_arrow(self.character.portal[orign][destn]))
        assert self.arrow[orign][destn] in self.arrowlayout.children

    def _trigger_add_arrow(self, orign, destn):
        part = partial(self.add_arrow, orign, destn)
        Clock.unschedule(part)
        Clock.schedule_once(part, 0)

    def add_new_arrows(self, *args):
        Logger.debug("Board: adding new arrows to {}".format(
            self.character.name))
        for arrow_orig in self.character.portal:
            for arrow_dest in self.character.portal[arrow_orig]:
                if (arrow_orig not in self.arrow
                        or arrow_dest not in self.arrow[arrow_orig]):
                    self.arrowlayout.add_widget(
                        self.make_arrow(
                            self.character.portal[arrow_orig][arrow_dest]))

    def add_pawn(self, thingn, *args):
        if (thingn in self.character.thing and thingn not in self.pawn):
            pwn = self.make_pawn(self.character.thing[thingn])
            whereat = self.spot[pwn.thing['location']]
            whereat.add_widget(pwn)
            self.pawn[thingn] = pwn

    def _trigger_add_pawn(self, thingn):
        part = partial(self.add_pawn, thingn)
        Clock.unschedule(part)
        Clock.schedule_once(part, 0)

    def add_new_pawns(self, *args):
        Logger.debug("Board: adding new pawns to {}".format(
            self.character.name))
        nodes_patch = {}
        things2add = []
        pawns_added = []
        for (thing_name, thing) in self.character.thing.items():
            if thing_name not in self.pawn:
                things2add.append(thing)
                patch = {}
                if '_image_paths' in thing:
                    zeroes = [0] * len(thing['_image_paths'])
                else:
                    patch['_image_paths'] = Pawn.default_image_paths
                    zeroes = [0] * len(Pawn.default_image_paths)
                if '_offxs' not in thing:
                    patch['_offxs'] = zeroes
                if '_offys' not in thing:
                    patch['_offys'] = zeroes
                if '_stackhs' not in thing:
                    patch['_stackhs'] = zeroes
                if patch:
                    nodes_patch[thing_name] = patch
        if nodes_patch:
            self.character.node.patch(nodes_patch)
        for thing in things2add:
            pwn = self.make_pawn(thing)
            pawns_added.append(pwn)
            whereat = self.spot[thing['location']]
            whereat.add_widget(pwn)

    def update(self, *args):
        """Force an update to match the current state of my character.

        This polls every element of the character, and therefore
        causes me to sync with the LiSE core for a long time. Avoid
        when possible.

        """

        # remove widgets that don't represent anything anymore
        Logger.debug("Board: updating")
        self.remove_absent_pawns()
        self.remove_absent_spots()
        self.remove_absent_arrows()
        # add widgets to represent new stuff
        self.add_new_spots()
        if self.arrow_cls:
            self.add_new_arrows()
        self.add_new_pawns()
        Logger.debug("Board: updated")

    trigger_update = trigger(update)

    def update_from_delta(self, delta, *args):
        """Apply the changes described in the dict ``delta``."""
        for (node, extant) in delta.get('nodes', {}).items():
            if extant:
                if node in delta.get('node_val', {}) \
                        and 'location' in delta['node_val'][node]\
                        and node not in self.pawn:
                    self.add_pawn(node)
                elif node not in self.spot:
                    self.add_spot(node)
                    spot = self.spot[node]
                    if '_x' not in spot.place or '_y' not in spot.place:
                        self.spots_unposd.append(spot)
            else:
                if node in self.pawn:
                    self.rm_pawn(node)
                if node in self.spot:
                    self.rm_spot(node)
        for (node, stats) in delta.get('node_val', {}).items():
            if node in self.spot:
                spot = self.spot[node]
                x = stats.get('_x')
                y = stats.get('_y')
                if x is not None:
                    spot.x = x * self.width
                if y is not None:
                    spot.y = y * self.height
                if '_image_paths' in stats:
                    spot.paths = stats[
                        '_image_paths'] or spot.default_image_paths
            elif node in self.pawn:
                pawn = self.pawn[node]
                if 'location' in stats:
                    pawn.loc_name = stats['location']
                if '_image_paths' in stats:
                    pawn.paths = stats[
                        '_image_paths'] or pawn.default_image_paths
            else:
                Logger.warning("Board: diff tried to change stats of node {} "
                               "but I don't have a widget for it".format(node))
        for (orig, dests) in delta.get('edges', {}).items():
            for (dest, extant) in dests.items():
                if extant and (orig not in self.arrow
                               or dest not in self.arrow[orig]):
                    self.add_arrow(orig, dest)
                elif not extant and orig in self.arrow and dest in self.arrow[
                        orig]:
                    self.rm_arrow(orig, dest)

    def trigger_update_from_delta(self, delta, *args):
        part = partial(self.update_from_delta, delta)
        Clock.unschedule(part)
        Clock.schedule_once(part, 0)

    def on_spots_unposd(self, *args):
        # TODO: If only some spots are unpositioned, and they remain
        # that way for several frames, put them somewhere that the
        # user will be able to find.
        if not self.spots_unposd:
            return
        try:
            self.grid_layout()
        except (TypeError, ValueError):
            self.nx_layout()

    def _apply_node_layout(self, l, *args):
        if self.width == 1 or self.height == 1:
            Clock.schedule_once(partial(self._apply_node_layout, l), 0.01)
            return
        node_upd = {}
        for spot in self.spots_unposd:
            (x, y) = l[spot.name]
            assert 0 <= x <= 0.99, "{} has invalid x: {}".format(spot.name, x)
            assert 0 <= y <= 0.99, "{} has invalid y: {}".format(spot.name, y)
            assert spot in self.spotlayout.children
            assert self.spotlayout.width == self.width
            assert self.spotlayout.height == self.height
            node_upd[spot.name] = {'_x': x, '_y': y}
            spot.pos = (x * self.width, y * self.height)
        if node_upd:
            self.character.node.patch(node_upd)
        self.spots_unposd = []

    def grid_layout(self, *args):
        self._apply_node_layout(
            normalize_layout(
                {spot.name: spot.name
                 for spot in self.spots_unposd}))

    def nx_layout(self, *args):
        for spot in self.spots_unposd:
            if not (spot.name and spot.proxy):
                Clock.schedule_once(self.nx_layout, 0)
                return
        spots_only = self.character.facade()
        for thing in list(spots_only.thing.keys()):
            del spots_only.thing[thing]
        self._apply_node_layout(self.graph_layout(spots_only))

    def arrows(self):
        """Iterate over all my arrows."""
        for o in self.arrow.values():
            for arro in o.values():
                yield arro

    def pawns_at(self, x, y):
        """Iterate over pawns that collide the given point."""
        for pawn in self.pawn.values():
            if pawn.collide_point(x, y):
                yield pawn

    def spots_at(self, x, y):
        """Iterate over spots that collide the given point."""
        for spot in self.spot.values():
            if spot.collide_point(x, y):
                yield spot

    def arrows_at(self, x, y):
        """Iterate over arrows that collide the given point."""
        for arrow in self.arrows():
            if arrow.collide_point(x, y):
                yield arrow
class GestureContainer(EventDispatcher):
    '''Container object that stores information about a gesture. It has
    various properties that are updated by `GestureSurface` as drawing
    progresses.

    :Arguments:
        `touch`
            Touch object (as received by on_touch_down) used to initialize
            the gesture container. Required.

    :Properties:
        `active`
            Set to False once the gesture is complete (meets
            `max_stroke` setting or `GestureSurface.temporal_window`)

            :attr:`active` is a
            :class:`~kivy.properties.BooleanProperty`

        `active_strokes`
            Number of strokes currently active in the gesture, ie
            concurrent touches associated with this gesture.

            :attr:`active_strokes` is a
            :class:`~kivy.properties.NumericProperty`

        `max_strokes`
            Max number of strokes allowed in the gesture. This
            is set by `GestureSurface.max_strokes` but can
            be overridden for example from `on_gesture_start`.

            :attr:`max_strokes` is a
            :class:`~kivy.properties.NumericProperty`

        `was_merged`
            Indicates that this gesture has been merged with another
            gesture and should be considered discarded.

            :attr:`was_merged` is a
            :class:`~kivy.properties.BooleanProperty`

        `bbox`
            Dictionary with keys minx, miny, maxx, maxy. Represents the size
            of the gesture bounding box.

            :attr:`bbox` is a
            :class:`~kivy.properties.DictProperty`

        `width`
            Represents the width of the gesture.

            :attr:`width` is a
            :class:`~kivy.properties.NumericProperty`

        `height`
            Represents the height of the gesture.

            :attr:`height` is a
            :class:`~kivy.properties.NumericProperty`
    '''
    active = BooleanProperty(True)
    active_strokes = NumericProperty(0)
    max_strokes = NumericProperty(0)
    was_merged = BooleanProperty(False)
    bbox = DictProperty({
        'minx': float('inf'),
        'miny': float('inf'),
        'maxx': float('-inf'),
        'maxy': float('-inf')
    })
    width = NumericProperty(0)
    height = NumericProperty(0)

    def __init__(self, touch, **kwargs):
        super(GestureContainer, self).__init__(**kwargs)

        # This is the touch.uid of the oldest touch represented
        self.id = str(touch.uid)

        # Store various timestamps for decision making
        self._create_time = Clock.get_time()
        self._update_time = None
        self._cleanup_time = None
        self._cache_time = 0

        # We can cache the candidate here to save zip()/Vector instantiation
        self._vectors = None

        # The color is applied to all canvas items of this gesture
        col = kwargs.get('color', None)
        if col is not None:
            self.color = col
        else:
            self.color = [1.0, 1.0, 1.0]

        # Key is touch.uid; value is a kivy.graphics.Line(); it's used even
        # if line_width is 0 (ie not actually drawn anywhere)
        self._strokes = {}

        # Make sure the bbox is up to date with the first touch position
        self.update_bbox(touch)

    def get_vectors(self, **kwargs):
        '''Return strokes in a format that is acceptable for
        `kivy.multistroke.Recognizer` as a gesture candidate or template. The
        result is cached automatically; the cache is invalidated at the start
        and end of a stroke and if `update_bbox` is called. If you are going
        to analyze a gesture mid-stroke, you may need to set the `no_cache`
        argument to True.'''
        if self._cache_time == self._update_time and not kwargs.get(
                'no_cache'):
            return self._vectors

        vecs = []
        append = vecs.append
        for tuid, l in self._strokes.items():
            lpts = l.points
            append([Vector(*pts) for pts in zip(lpts[::2], lpts[1::2])])

        self._vectors = vecs
        self._cache_time = self._update_time
        return vecs

    def handles(self, touch):
        '''Returns True if this container handles the given touch'''
        if not self.active:
            return False
        return str(touch.uid) in self._strokes

    def accept_stroke(self, count=1):
        '''Returns True if this container can accept `count` new strokes'''
        if not self.max_strokes:
            return True
        return len(self._strokes) + count <= self.max_strokes

    def update_bbox(self, touch):
        '''Update gesture bbox from a touch coordinate'''
        x, y = touch.x, touch.y
        bb = self.bbox
        if x < bb['minx']:
            bb['minx'] = x
        if y < bb['miny']:
            bb['miny'] = y
        if x > bb['maxx']:
            bb['maxx'] = x
        if y > bb['maxy']:
            bb['maxy'] = y
        self.width = bb['maxx'] - bb['minx']
        self.height = bb['maxy'] - bb['miny']
        self._update_time = Clock.get_time()

    def add_stroke(self, touch, line):
        '''Associate a list of points with a touch.uid; the line itself is
        created by the caller, but subsequent move/up events look it
        up via us. This is done to avoid problems during merge.'''
        self._update_time = Clock.get_time()
        self._strokes[str(touch.uid)] = line
        self.active_strokes += 1

    def complete_stroke(self):
        '''Called on touch up events to keep track of how many strokes
        are active in the gesture (we only want to dispatch event when
        the *last* stroke in the gesture is released)'''
        self._update_time = Clock.get_time()
        self.active_strokes -= 1

    def single_points_test(self):
        '''Returns True if the gesture consists only of single-point strokes,
        we must discard it in this case, or an exception will be raised'''
        for tuid, l in self._strokes.items():
            if len(l.points) > 2:
                return False
        return True
コード例 #6
0
class ShowSections(Screen):

    dict_links = DictProperty({
        "userful_programs": "https://pcompstart.com/useful",
        "advice_on_computer": "https://pcompstart.com/tips",
        "user_windows": "https://pcompstart.com/windows",
        "articles_in_theme": "https://pcompstart.com/topic",
        "popular_program": "https://pcompstart.com/popular",
        "other": "https://pcompstart.com/other"
    })

    url = StringProperty()

    loadback = ObjectProperty(None)

    events_callback = ObjectProperty(None)

    scrollwid = ObjectProperty(None)
    '''ScrollView + GridLayout'''

    dt = ListProperty()
    '''Список для функции `get_link(self, args)`,
    с конечными значениями для создания свойства перехода по ссылке.
    '''

    vit = ListProperty()
    '''`List` базы данных  для внесения значений в БД,
     в функцие `write_response(self, *args)`. 
     И затем последующее сравнение с запросом от браузера.
    '''
    def __init__(self, **kwargs):
        super(ShowSections, self).__init__(**kwargs)

        for lnk in self.dict_links.items():
            if self.name in lnk:
                self.url = lnk[1]

        self.scrollwid = ScrollWid(size_hint_y=1)

        self.loadback = LoadBack()
        self.add_widget(self.loadback)

        if not os.path.exists('urlfiles'):
            os.mkdir('urlfiles')

        self.list_fold = os.listdir('urlfiles')

        # Подключение базы данных
        self.dbman = DataBaseMnager('urlfiles/pcomp.db')

        # Подгрузка функции проверки старниц сайта
        Clock.schedule_once(self.check_sqlpages, 0.55)

    def parse_url(self, args):
        """Разборка ссылки запроса страницы из браузера,
         для получения названия файла.
        """
        return args.split('/')[-1]

    def get_html(self, link, imag_path=None):
        """Получение ответа от браузера, с данными страниц сайта.
        """
        try:
            zagolov = {
                'Content-type': 'application/x-www-form-urlencoded',
                'Accept': '*/*'
            }
            req = UrlRequest(link,
                             on_error=self.gethtml_error,
                             req_headers=zagolov,
                             chunk_size=12000,
                             timeout=self.time_out,
                             file_path=imag_path)
            while not req.is_finished:
                req.wait(0)
            return req.result.encode("utf-8")
        except HTTPError as err:
            return BugReporter(txt_report=str(err))

    def gethtml_error(self, req, error):
        return BugReporter(txt_report=str(error))

    def time_out(self, req, time):
        if req.time > 60:
            get_rootapp(
            ).prime_screen.ids.screen_manager.current = 'primescreen'

    def get_allpages(self):
        """Вытягивает и фильтрует все страницы сайта,
        из текушего `self.url`
        """
        r = self.get_html(self.url)
        res = r.split('class="pagination"')[1].split('id="right-bar"')[0]
        page = re.findall('href="?\'?([^"\'>]*)', res)
        respag = [self.url]
        i = 1
        while i < int(max(page)[-1]):
            i += 1
            respag.append(self.url + '/page={}'.format(i))
        return respag

    def parse_content(self, largs):
        """Парсинг получеенного контента,
         по заданым данным.
        """
        try:
            res = self.get_html(largs)
            res = res.split('class="conetnt-table"')[1].split(
                'class="pagination"')[0]
            return res
        except HTTPError:
            return None

    def check_sqlpages(self, *args):
        """Проверка на существование страниц в базе данных,
        и их запись/дозапись в б.д.
        """
        self.dbman.cur.execute('''create table if not exists loccat (
                id integer primary key autoincrement,
                cat_type varchar(100),
                url varchar(100))''')
        var = self.dbman.query(
            "select url from loccat where cat_type='{}'".format(
                self.parse_url(self.url))).fetchall()
        '''Переменные url_pages и val,
        обработка для сравнения результатов
        '''
        respage = self.get_allpages()
        url_pages = '\n'.join(respage)
        val = '\n'.join(str(v) for v in var).replace("('",
                                                     "").replace("',)", "")
        if not var or url_pages not in val:
            for pag in self.get_allpages():
                if pag not in val:
                    self.dbman.querymany(
                        "insert into loccat values (null, ?, ?)",
                        [(pag.split('/')[3], pag)])
            self.parse_html()
            self.load_view()
        else:
            self.write_response(self.parse_content(respage[-1]))
            self.load_view()

    def parse_html(self):
        """Парсинг страниц сайта по текущему,
        `self.url`.
        """
        pageurl = self.dbman.query(
            "select url from loccat where cat_type='{}'".format(
                self.url.split('/')[-1])).fetchall()
        for p in pageurl:
            self.write_response(self.parse_content(p[0]))

    def write_response(self, *args):
        """Запись контента полученого из запроса от браузера,
        в базу данных.
        """
        self.dbman.cur.execute('''create table if not exists categories (
                id integer primary key autoincrement,
                category_type varchar(50),
                cat_title varchar(255),
                cat_imglink varchar(255),
                cat_text text,
                link_product varchar(255))''')
        resi = [a for a in args]
        result = resi[0].split('div class="key-content"')
        del result[0]
        var = self.dbman.query("select cat_title from categories "
                               "where category_type='{}'".format(
                                   self.parse_url(self.url))).fetchall()
        for v in var:
            self.vit.append(v[0])
        list_vars = {}
        for i in result:
            '''Поиск и выборка по регулярному выражению,
            всех названий статей заключёных между тегами <title></title>.
            '''
            for title in re.findall('<h3.*?><a.*?>(.+?)</a></h3>', i):
                list_vars.update({"cat_title": title})
                break
            '''Поиск по регулярному выражению, ссылок всех картинок,
            и присваивание им суффикса `https:` с начала ссылки.
            '''
            for img in re.findall('img src="?\'?([^"\'>]*)', i):
                im = 'https:{}'.format(img)
                list_vars.update({"cat_imglink": im})
                break
            '''Поиск и выборка по регулярному выражению,
            всего текста заключёного между тегами <p></p>.
            '''
            for content in re.findall('<[p][^>]*>(.+?)</[p]>', i):
                content = re.sub('<.*?>', '', content)[:200]
                con = re.sub('&.*?;', ' ', content).replace('  ', '')
                c = con.rfind(' ')
                cont = con[:c].rstrip(',') + '...'
                list_vars.update({"cat_text": cont})
                break
            '''Поиск и выборка по регулярному выражению,
             всех ссылок, ведущих на страницу с описанием
             данного компонента.
            '''
            for link in re.findall('a href="?\'?([^"\'>]*)', i):
                link = link.replace('&amp;', '&')
                link = 'https:{}'.format(link)
                list_vars.update({"link_product": link})
                break
            '''Проверка на нахождении материала в б.д.
            если нет то, запись/дозапись.
            '''
            if list_vars["cat_title"] not in self.vit:
                try:
                    self.dbman.querymany(
                        '''insert into categories values (
                            null, ?, ?, ?, ?, ?)''',
                        [(self.parse_url(self.url), list_vars["cat_title"],
                          list_vars["cat_imglink"], list_vars["cat_text"],
                          list_vars["link_product"])])
                except KeyError:
                    pass

    def load_view(self):
        """Подгрузка и вывод всего контента.
        """
        con = self.dbman.query(
            "select cat_title, cat_imglink, cat_text, link_product"
            " from categories "
            "where category_type='{}'".format(self.parse_url(
                self.url))).fetchall()
        if not con:
            self.check_sqlpages()
        if not os.path.exists('urlfiles/urlimg'):
            os.makedirs('urlfiles/urlimg')
        for content in con:
            file_img = content[1].split('/')[-1]
            if not os.path.exists('urlfiles/urlimg/{}'.format(file_img)):
                self.get_html(link=content[1],
                              imag_path='urlfiles/urlimg/{}'.format(file_img))
            self.scrollwid.layout.add_widget(
                MyMadenButton(icon='urlfiles/urlimg/{}'.format(file_img),
                              size_x_image=.25,
                              text="[size=13sp][color=#0000ff][b]" +
                              content[0] + "[/b][/color][/size]" + "\n" +
                              "[size=12sp][color=#000000]" + content[2] +
                              "[/color][/size]",
                              radius_button=[0],
                              height_but='115dp',
                              shad_but=True,
                              events_callback=self.get_link))
        self.add_widget(self.scrollwid)
        self.remove_widget(self.loadback)
        self.dt = con
        self.dbman.__del__()

    def get_link(self, args):
        for lnk in self.dt:
            if parse_link(args) in lnk:
                launch_webbrowser(lnk[3])
コード例 #7
0
class DeviceFamily:

    device = ObjectProperty(None)

    # these three need to be set in each subclass
    family = StringProperty('Unknown')
    modes = DictProperty({})
    default_mode = StringProperty('')

    def __init__(self, **kwargs):
        self.app = App.get_running_app()
        try:
            with open(self.app.get_path('{:}.json'.format(self.family)),
                      'r') as f:
                self.settings = json.load(f)
        except:
            self.settings = {}
        Clock.schedule_once(self.post_init, 0)

    def post_init(self, dt):
        self.set_mode(self.settings.get('current_mode', self.default_mode))
        self.connect()

    def save(self):
        with open(self.app.get_path('{:}.json'.format(self.family)), 'w') as f:
            json.dump(self.settings, f, indent=1)

    def set_mode(self, mode):
        self.disconnect()
        try:
            if mode in self.modes:
                devmod = importlib.import_module('jocular.{:}'.format(
                    self.family.lower()))
                devclass = getattr(devmod, self.modes[mode])
                self.device = devclass()
                self.settings['current_mode'] = mode
                self.device.settings_have_changed()
                # self.save()
        except Exception as e:
            logger.exception(e)

    def get_configurables(self):
        if self.device is not None:
            return self.device.configurables

    def configure(self):
        if self.device is not None:
            logger.debug('family {:} settings {:}'.format(
                self.family, self.settings['current_mode']))
            self.device.configure()

    def connect(self):
        logger.debug('Connecting {:} (current mode: {:})'.format(
            self.family, self.settings['current_mode']))
        if self.device is not None:
            self.device.connect()
            # only save current mode if we are able to connect
            if self.device.connected:
                self.save()
                self.device_connected()
                self.device.on_new_object()

    def disconnect(self):
        if self.device is None:
            return
        if self.connected():
            self.device.disconnect()
            self.device_disconnected()

    def connected(self):
        if self.device is None:
            return False
        return self.device.connected

    def device_connected(self):
        pass

    def device_disconnected(self):
        pass

    def on_close(self, *args):
        if self.connected():
            self.disconnect()

    def choose(self, *args):
        if self.device is not None:
            self.device.choose()
コード例 #8
0
ファイル: lists.py プロジェクト: alyoung/VKGroups
class Lists(BoxLayout):
    events_callback = ObjectProperty()
    '''Функция обработки сигналов экрана.'''

    dict_items = DictProperty()
    '''{'Name item': ['Desc item', 'icon_item.png', True/False}.'''

    list_items = ListProperty()
    '''['Desc item', 'icon_item.png', True/False]...'''

    right_icons = ListProperty()
    '''Список путей к иконкам для кнопок,
    использующихся в пункте списка справа.'''

    flag = StringProperty('single_list')

    def __init__(self, **kvargs):
        super(Lists, self).__init__(**kvargs)

        if self.flag == 'two_list_icon_check':
            for name_item in self.dict_items.keys():
                desc_item, icon_item, state_item = \
                    self.dict_items[name_item]
                self.ids.list_items.add_widget(
                    CheckItem(text=name_item,
                              secondary_text=desc_item,
                              icon=icon_item,
                              active=state_item,
                              events_callback=self.events_callback,
                              id=name_item))
        elif self.flag == 'two_list_custom_icon':
            self.two_list_custom_icon(self.dict_items, IconItem)
        elif self.flag == 'two_list_custom_icon_async':
            self.two_list_custom_icon(self.dict_items, IconItemAsync)
        elif self.flag == 'three_list_custom_icon':
            self.three_list_custom_icon(self.dict_items)
        elif self.flag == 'single_list' or self.flag == 'single_list_icon':
            self.single_list(self.list_items)
        elif self.flag == 'one_select_check':
            self.one_select_check()

    def one_select_check(self):
        for text_item in self.dict_items.keys():
            self.ids.list_items.add_widget(
                OneSelectCheckItem(text=text_item,
                                   id=text_item,
                                   events_callback=self.events_callback,
                                   group=self.dict_items[text_item][0],
                                   active=self.dict_items[text_item][1]))

    def single_list(self, list_items):
        '''
        :param list_items: ['Item one', 'Item two', ...];
                           [['Item one', 'name icon', True/False], ...];

        '''

        if self.flag == 'single_list':
            for name_item in list_items:
                self.ids.list_items.add_widget(
                    Item(text=name_item, events_callback=self.events_callback))
        elif self.flag == 'single_list_icon':
            for name_item in list_items:
                self.ids.list_items.add_widget(
                    SingleIconItem(icon=name_item[1],
                                   text=name_item[0],
                                   events_callback=self.events_callback))

    def three_list_custom_icon(self, dict_items):
        '''
        :param dict_items: {'Name item': ['Desc item', 'icon_item.png'], ...};

        '''

        list_items = self.ids.list_items

        for name_item in dict_items.keys():
            desc_item, icon_item = dict_items[name_item]
            if desc_item == '':
                name_item += '\n'

            icon_item = IconItemThree(text=name_item,
                                      secondary_text=desc_item,
                                      id=name_item,
                                      icon=icon_item,
                                      events_callback=self.events_callback)

            for image in self.right_icons:
                icon_item.add_widget(
                    RightButton(id='{}, {}'.format(
                        name_item,
                        os.path.split(image)[1].split('.')[0]),
                                source=image,
                                on_release=self.events_callback))
            list_items.add_widget(icon_item)

    def two_list_custom_icon(self, dict_items, instance_icon):
        for name_item in dict_items.keys():
            desc_item, icon_item = dict_items[name_item]
            icon_item = instance_icon(text=name_item,
                                      secondary_text=desc_item,
                                      id=name_item,
                                      icon=icon_item,
                                      events_callback=self.events_callback)

            for image in self.right_icons:
                right_button = RightButton(source=image)
                icon_item.add_widget(right_button)
            self.ids.list_items.add_widget(icon_item)
コード例 #9
0
class BookMetadataScreen(TooltipScreen, TooltipBehavior, Screen):

    books_db = ObjectProperty(allownone=True)
    task_scheduler = ObjectProperty(allownone=True)
    camera_system = ObjectProperty(allownone=True)

    input_disabled = BooleanProperty(False)
    target_extra = ObjectProperty(allownone=True)
    catalogs = ListProperty(['none',])
    collection_sets_mapping = DictProperty()
    formatted_collection_sets = ListProperty()
    is_current_rcs_default = BooleanProperty()
    collection_string = StringProperty()
    current_catalog = StringProperty('none')
    can_reprint_slip = BooleanProperty(False)
    _marc_popup = ObjectProperty(None, allownone=True)
    object_type = StringProperty('item')
    OLD_PALLET_VALIDATION_REGEX = StringProperty()
    BOXID_VALIDATION_REGEX = StringProperty()

    __events__ = ('on_done', 'on_cancel')

    def __init__(self, **kwargs):
        self._search_option = 'identifier'
        self._new_book = False
        self._input_trigger = Clock.create_trigger(self._on_input_disabled, -1)
        self.fbind('disabled', self._input_trigger)
        self.fbind('input_disabled', self._input_trigger)
        self.OLD_PALLET_VALIDATION_REGEX = config.get('old_pallet_validation_regex', '(IA-..-\d{7})|(IA|CH)[\dxX]{4,5}')
        self.BOXID_VALIDATION_REGEX = config.get('boxid_validation_regex', '^IA\d{6,7}$')
        super(BookMetadataScreen, self).__init__(**kwargs)
        self._new_metadata_field_popup = NewMetadataFieldPopup()
        self._progress_popup = ProgressPopup()
        self.backend = BookMetadataScreenBackend(
            task_scheduler=self.task_scheduler
        )
        self._bind_backend_events()
        Clock.schedule_once(self._postponed_init, -1)

    def _postponed_init(self, *args):
        self._setup_new_metadata_field_popup()
        self._wonderfetch_popup = WonderfetchDialog()
        self.catalogs = self._wonderfetch_popup.catalogs
        self.current_catalog = self._wonderfetch_popup.default_catalog
        self._bind_wonderfetch_popup_events()

    def _bind_backend_events(self):
        md_loaded_trigger = Clock.create_trigger(self._on_metadata_loaded, -1)
        bk = self.backend
        bk.fbind(bk.EVENT_INIT, self._on_backend_init)
        bk.fbind(bk.EVENT_INIT, self._input_trigger)
        bk.fbind(bk.EVENT_BOOK_STATE, self._on_book_state)
        bk.fbind(bk.EVENT_ERROR, self._on_error)
        bk.fbind(bk.EVENT_METADATA_ERROR, self._on_metadata_error)
        bk.fbind(bk.EVENT_METADATA_DEFERRED, self._on_metadata_deferred)
        bk.fbind(bk.EVENT_IDENTIFIER_LOADED, self._on_identifier)
        bk.fbind(bk.EVENT_OFFLINE_ITEM_CREATED, self._on_offline_item_created)
        bk.fbind(bk.EVENT_METADATA_LOADED, md_loaded_trigger)
        bk.fbind(bk.EVENT_SELECT_IDENTIFIER, self._on_select_identifier)
        bk.fbind(bk.EVENT_START_MARC, self._on_start_marc)
        bk.fbind(bk.EVENT_TASK_START, self._progress_popup.open)
        bk.fbind(bk.EVENT_TASK_END, self._progress_popup.dismiss)
        bk.fbind(bk.EVENT_TASK_PROGRESS, self._on_task_progress)
        bk.fbind(bk.EVENT_BOOK_REJECTED, self._on_book_rejected)
        bk.fbind(bk.EVENT_START_WONDERFETCH, self._on_start_wonderfetch)
        bk.fbind(bk.EVENT_END_WONDERFETCH_SUCCESS, self._on_wonderfetch_success)
        bk.fbind(bk.EVENT_SLIP_PRINTED, self._on_slip_printed)
        bk.fbind(bk.EVENT_RCS_UPDATED, self._on_rcs_updated)

    def _setup_new_metadata_field_popup(self):
        popup = self._new_metadata_field_popup
        popup.target_widget = self.ids.metadata_form
        popup.fbind('on_submit', self._on_new_metadata_field_popup_submit)

    def _bind_wonderfetch_popup_events(self):
        popup = self._wonderfetch_popup
        popup.fbind(popup.EVENT_CANNOT_SCAN_BOOK, self._on_wonderfetch_cannot_scan_book)
        popup.fbind(popup.EVENT_SCAN_BOOK, self._on_wonderfetch_scan_book)
        popup.fbind(popup.EVENT_REJECT_BOOK, self._on_wonderfetch_reject_book)
        popup.fbind(popup.EVENT_SCAN_EXISTING_BOOK, self._on_wonderfetch_scan_book)
        popup.fbind(popup.EVENT_RETRIEVAL_ERROR, self._on_wonderfetch_retrieval_error)

    def _bind_marc_popup_events(self, popup):
        popup.fbind(popup.EVENT_RECORD_SELECTED,
                    self.on_marc_selected_record)

    def _ensure_book_path_and_init(self, *args):
        if not self.backend.book_path:
            self.backend.create_new_book()
            self.backend.init()

    def start_load_metadata(self, identifier, volume):
        option = self._search_option
        if option == 'identifier':
            self.backend.load_metadata_via_identifier(identifier)
        elif option in ['isbn', 'openlibrary']:
            catalog = self.current_catalog
            if self.is_loading_allowed():
                self.backend.wonderfetch_search(option, identifier, volume, catalog)

    def generate_identifier(self):
        self._save_metadata_if_valid(callback=self.do_generate_identifier)

    def do_generate_identifier(self):
        self.backend.make_identifier()

    def start_print_slip(self, identifier):
        self.action = ColoredYesNoActionPopupMixin(
            action_function=self.actually_reprint_slip,
            title='Reprint slip?',
            message='Are you sure you want to PRINT AGAIN this slip?',
            extra=identifier,
        )
        self.action.display()

    def actually_reprint_slip(self, action, *args, **kwargs):
        identifier = action.extra_args
        self.save_metadata()
        self.backend.generate_and_print_slip(identifier)

    def start_print_slip_and_upload(self, identifier, next_action=None):
        self.save_metadata()
        self.backend.generate_reserve_print_slip(identifier, next_action)

    def start_marc_search(self, identifier):
        self.backend.marc_search(identifier)

    def reject_button_action(self):
        self._save_metadata_if_valid(callback=self.show_book_reject_popup,
                                     force=True, # this suppresses the "insufficient metadata" window
                                     ignore_fields=['old_pallet'])

    def show_book_reject_popup(self):
        popup = RejectBookPopup(title="Reject book",
                                message="Please indicate the rejection reason")
        popup.fbind('on_submit', self._on_reject_book_popup_submit)
        popup.open()

    def _on_reject_book_popup_submit(self, popup, data):
        popup.dismiss()
        self.backend.reject_book(data)

    def show_slip(self, *args):
        if not self.backend.book_obj.has_slip():
            return
        slip_image = Image(source=self.backend.book_obj.get_slip(full_path=True),
                           size_hint=(None, None),
                           allow_stretch=True,
                           )
        slip_image.size = slip_image.texture_size
        slip_name = self.backend.book_obj.get_slip_type()
        self.action = ShowGenericInfoAction(
            additional_content=slip_image,
            title='Slip type #{}'.format(slip_name)
        )
        self.action.display()

    def save_metadata(self):
        Logger.info('BMDS: User pressed Save button')
        self._save_metadata_if_valid()

    def save_metadata_and_done(self):
        Logger.info('BMDS: User pressed Save&Done button')
        self._save_metadata_if_valid(callback=self.done)

    def is_loading_allowed(self):
        if self.backend.is_this_a_supercenter() and not self._ensure_boxid_and_old_pallet():
            self.show_error('You must supply a compliant boxid\n'
                            'value in order to use this feature.')
            return False
        success, md = self.is_metadata_valid()
        # test boxid is not empty here
        return success

    def is_metadata_valid(self, ignore_fields=[]):
        metadata = self.collect_metadata()
        self.ids.metadata_form.validate()
        modern_books_section_ok, payload = self._are_modern_books_items_valid(ignore_fields=ignore_fields)
        if not modern_books_section_ok and self.backend.is_this_a_supercenter():
            self.show_error('Your {} seems to be invalid.'.format(payload['key']))
            return False, None

        identifier_list = list(filter(lambda entry: entry['key'] == 'user_identifier', metadata))
        if len(identifier_list) > 0:
            identifier = identifier_list[0]['value']
            if identifier:
                if not self.is_identifier_valid(identifier):
                    self.show_error('Identifier{} is invalid.'.format(identifier))
                    return False, None
        if self._are_metadata_items_valid(metadata):
            return True, metadata
        else:
            return False, None

    def _save_metadata_if_valid(self, callback=None, force=False, ignore_fields=[]):
        success, metadata = self.is_metadata_valid(ignore_fields=ignore_fields)
        if success:
            self.backend.set_metadata_from_form(metadata)
            self.backend.save_metadata()
            if not self.backend._has_minimum_acceptable_metadata() and not force:
                msg = "Your metadata was saved. However, it appears you are attempting to create an " \
                      "item without some key fields." \
                      "\nAt least [b]title and creator[/b], or [b]isbn[/b] should be present.\n" \
                      "\n[b]Would you like to continue with insufficient metadata?[/b]\n" \
                      "\nClick NO to go back and review the currently available metadata."
                self.show_choice('Insufficient metadata', msg, callback)
                return False
            if callback:
                callback()
            return True
        else:
            self.show_error('Ensure that all fields are non-empty and valid.')
            return False

    def collect_metadata(self):
        md = self.ids.metadata_form.collect_data()
        md.extend(self.collect_global_book_settings())
        if not self.backend.book_obj.is_preloaded():
            md.extend(self.collect_collection())
        md.extend(self.collect_identifier())
        camera = self.backend.get_metadata_item('camera')
        if not camera and self.camera_system:
            camera = self.camera_system.get_name()
            if camera:
                md.append({'key': 'camera', 'value': camera})
        return md

    def _are_metadata_items_valid(self, metadata):
        new_book = self._new_book
        for_removal = []
        for index, item in enumerate(metadata):
            if item.get('deleted', False):
                continue
            value = item['value']
            if not item.get('valid', True):
                return False
        for index, md_index in enumerate(for_removal):
            metadata.pop(md_index - index)
        return True

    def _are_modern_books_items_valid(self, ignore_fields):
        for index, item in enumerate(self.collect_global_book_settings()):
            #if item.get('deleted', False):
            #    continue
            value = item['value']
            if not item.get('valid', True):
                if item['key'] in ignore_fields:
                    continue
                return False, item
        return True, None

    def open_new_metadata_field_popup(self):
        skip_keys = MD_SKIP_KEYS | MD_READONLY_KEYS
        all_data_keys = self.ids.metadata_form.all_data_keys
        for key in all_data_keys:
            if key in MD_KEYS_WITH_SINGLE_VALUE \
                    and all_data_keys[key] == 1:
                skip_keys.add(key)
        self._new_metadata_field_popup.skip_keys = skip_keys
        self._new_metadata_field_popup.open()

    def collect_collection(self):
        collection_set_name = self.ids.collections_spinner.text #will be rcs
        collection_set = self.collection_sets_mapping.get(collection_set_name)
        if collection_set:
            return [
                {'key': 'collection_set', 'value': collection_set.get('name')},
                {'key': 'sponsor', 'value': collection_set.get('sponsor')},
                {'key': 'contributor', 'value': collection_set.get('contributor')},
                {'key': 'partner', 'value': collection_set.get('partner')},
                {'key': 'rcs_key', 'value': u'{}'.format(collection_set.get('rcs_key'))},
                {'key': 'collection',
                 'value': self.backend.create_collections(collection_set.get('name'))}
            ]
        else:
            self.action = ShowErrorAction(
                message='No collection set defined! Saving without collection string information.')
            self.action.display()
            return []

    def add_collection_set(self):
        #Only temporary, use app.get_popup instead
        from ia_scribe.uix.components.poppers.popups import WidgetPopup
        from ia_scribe.uix.widgets.rcs.rcs_widget import RCSSelectionWidget
        self.widget = WidgetPopup(content_class=RCSSelectionWidget,
                                  title='Add new RCS',
                                  size_hint=(None, None),
                                  size=('800dp', '500dp')
                                  )
        self.widget.bind_events({
            'EVENT_CLOSED': self.widget.dismiss,
            'EVENT_RCS_SELECTED': self.backend.add_collection_set,
        })
        self.widget.content.set_center(get_scanner_property('scanningcenter'))
        self.widget.content.fbind(self.widget.content.EVENT_RCS_SELECTED, self._add_collection_set_callback)
        self.widget.open()

    def _add_collection_set_callback(self, *args, **kwargs):
        self.widget.dismiss()

    def is_selection_the_default_collection_set(self, *args, **kwargs):
        spinner_name = self.ids.collections_spinner.text
        selected_rcs = self.collection_sets_mapping.get(spinner_name)
        return selected_rcs == self.backend.rcs_manager.get_default(wrap=False)

    def set_default_collection_set(self, *args, **kwargs):
        spinner_name = self.ids.collections_spinner.text
        selected_rcs = self.collection_sets_mapping[spinner_name]
        if selected_rcs != self.backend.rcs_manager.get_default(wrap=False):
            self.backend.rcs_manager.set_default(selected_rcs)
            self.refresh_collection_star()

    def is_boxid_valid(self, input, old_pallet=False):
        if config.is_true('disable_mandatory_fields_in_dwwi'):
            return True
        res = None

        if old_pallet:
            pattern = self.OLD_PALLET_VALIDATION_REGEX
        else:
            pattern = self.BOXID_VALIDATION_REGEX

        res = re.match(pattern, input)
        if res:
            if len(input) != res.span()[1]:
                res = False
            else:
                res = True
        return res

    def is_identifier_valid(self, identifier):
        pattern = '^[a-zA-Z0-9][a-zA-Z0-9\.\-_]{4,99}$'
        match_result = re.match(pattern, identifier)
        if not match_result:
            return False
        return match_result.string == identifier

    def collect_global_book_settings(self):
        ids = self.ids
        return [
            {'key': 'ppi', 'value': ids.ppi_input.value},
            {'key': 'boxid',
             'value': ids.box_id_input.text,
             'deleted': not ids.box_id_input.text,
             'valid': self.is_boxid_valid(ids.box_id_input.text),},
            {'key': 'old_pallet',
             'value': ids.box_id2_input.text,
             'deleted': not ids.box_id2_input.text,
             'valid': self.is_boxid_valid(ids.box_id2_input.text, old_pallet=True),},
            {'key': 'volume',
             'value': ids.volume_input.text,
             'deleted': not ids.volume_input.text},
        ]

    def collect_identifier(self):
        if self.ids.identifier_input.text:
            return [{'key': 'user_identifier',
                     'value': self.ids.identifier_input.text,}]
        return []

    def reload(self):
        Logger.info('BMDS: User pressed Reload button')
        self.backend.reinit()

    def done(self):
        Logger.info('BMDS: User pressed Done button')
        self.dispatch('on_done')

    def cance_from_user(self):
        Logger.info('BMDS: User pressed Cancel button')
        self.cancel()

    def cancel(self):
        self.dispatch('on_cancel')

    def open_book_path(self):
        book_path = self.backend.book_path
        if book_path and exists(book_path):
            subprocess.check_call(['xdg-open', book_path.encode('utf-8')])

    def show_info(self, message):
        popup = InfoPopup(title='Book metadata information',
                          message=str(message))
        popup.open()

    def show_error(self, message):
        popup = InfoPopup(title='Book metadata error',
                          message=str(message))
        popup.open()

    def show_choice(self, title, message, ok_callback, payload=None):
        popup = QuestionPopup(title=title,
                              message=message,
                              extra={'callback': ok_callback,
                                     'payload': payload},
                              size=(400, 300))
        popup.bind(on_submit=self.ok_callback)
        popup.open()

    def ok_callback(self, popup, option):
        if option == popup.OPTION_YES:
            if 'callback' in popup.extra and popup.extra['callback'] is not None:
                popup.extra['callback']()

    def _on_backend_init(self, backend):
        self._new_metadata_field_popup.dismiss()
        self._progress_popup.dismiss()
        self._on_book_state(backend, backend.get_book_state())
        self._disable_by_state_if_necessary(backend)
        self._on_metadata_loaded(backend)
        self.object_type = self.backend.book_obj.get_type().lower()

    def _on_slip_printed(self, *args):
        ids = self.ids
        ids.slip_present_label.text = 'Yes' if self.backend.book_obj.has_slip() else 'No'
        ids.display_slip_button.opacity = 1 if self.backend.book_obj.has_slip() else 0

    def _on_book_state(self, backend, state):
        self._on_slip_printed()
        ids = self.ids
        ids.marc_xml_label.text = 'Yes' if state & ST_HAS_MARC_XML else 'No'
        ids.marc_bin_label.text = 'Yes' if state & ST_HAS_MARC_BIN else 'No'
        ids.metasource_label.text = \
            'Yes' if state & ST_HAS_METASOURCE else 'No'
        ids.dwwi_and_marc_panel.disabled = bool(state & ST_PRELOADED)
        if state != 0:
            self.input_disabled = (bool(state & ST_DOWNLOADED)
                                   or not bool(state & ST_MARC_DOWNLOAD_FAILED))
        else:
            self.input_disabled = False
        if state & ST_QUEUED_FOR_DELETE:
            Logger.info('BMDS: Cancelling screen because book is flagged for '
                        'deletion')
            self.cancel()

    def _disable_by_state_if_necessary(self, backend):
        if backend.book_obj.status in ['loading_deferred']:
            self.input_disabled = True

    def _on_metadata_loaded(self, *args):
        backend = self.backend
        if not backend.is_initialized():
            return
        self._new_book = not exists(backend.book_path)
        md = backend.create_form_metadata(MD_SKIP_KEYS)
        self._insert_separators_to_metadata(md)
        ids = self.ids
        ids.metadata_form.set_data(md)
        ids.ppi_input.value = backend.book_obj.scandata.get_bookdata('ppi')
        ids.box_id_input.text = backend.get_metadata_item('boxid') or u''
        ids.box_id2_input.text = backend.get_metadata_item('old_pallet') or u''
        ids.volume_input.text = backend.get_metadata_item('volume') or u''
        ids.path_label.text = backend.book_path or ''
        self._populate_collection_sets()
        self.can_reprint_slip = self.backend.can_reprint_slip()

    def _populate_collection_sets(self):
        ret = {}
        default_collection_set = None
        for entry in self.backend.rcs_manager.as_list():
            name_human_readable = '{} ({})'.format(entry.get('name'),
                                                   entry.get('partner'))
            ret[name_human_readable] = entry
            if entry.get('default') is True:
                default_collection_set = name_human_readable
        self.collection_sets_mapping = ret
        self.formatted_collection_sets = ret.keys()

        # select correct entry
        book_metadata = self.backend.get_metadata()
        if 'rcs_key' in book_metadata:
            actual_collection = [human_name for human_name, entry
                                 in self.collection_sets_mapping.items()
                                 if str(entry.get('rcs_key')) ==
                                 str(book_metadata.get('rcs_key'))]
            if len(actual_collection) > 0:
                self.ids.collections_spinner.text = actual_collection[0]
        elif 'collection_set' in book_metadata:
            # If a 'collection set' entry is present, try and match
            actual_collection = [human_name for human_name, entry
                                 in self.collection_sets_mapping.items()
                                 if entry.get('name') == book_metadata.get('collection_set')]
            if len(actual_collection) > 0:
                self.ids.collections_spinner.text = actual_collection[0]
        elif default_collection_set:
            self.ids.collections_spinner.text = default_collection_set

        self.refresh_collection_star()

    def refresh_collection_star(self):
        self.is_current_rcs_default = self.is_selection_the_default_collection_set()
        self.refresh_collection_string()

    def refresh_collection_string(self):
        spinner_name = self.ids.collections_spinner.text
        selected_rcs = self.collection_sets_mapping.get(spinner_name, {})
        self.collection_string = selected_rcs.get('collections', '')

    def _insert_separators_to_metadata(self, metadata):
        separator = {'view_class': 'SeparatorLabelItem',
                     'size': (None, dp(30))}
        last_required_item_index = -1
        for index, item in enumerate(metadata):
            if 'required' in item:
                last_required_item_index = index
        if 0 < last_required_item_index < len(metadata) - 1:
            metadata.insert(last_required_item_index + 1, separator)

    def _on_identifier(self, backend, identifier, *args, **kwargs):
        self.ids.identifier_input.text = identifier or u''
        #self.input_disabled = bool(identifier)

    def _on_new_metadata_field_popup_submit(self, popup, data):
        self.ids.metadata_form.add_item(data)

    def _ensure_boxid_and_old_pallet(self):
        gbs = self.collect_global_book_settings()
        MANDATORY_FIELDS = ['boxid',]
        for item in gbs:
            if item['key'] in MANDATORY_FIELDS:
                if 'valid' in item and not item['valid']:
                    return False
        return True

    def _on_start_marc(self, backend, identifier):
        self._ensure_book_path_and_init()
        app = App.get_running_app()
        marc_popup = app.get_popup(MARCPopup, size=(1200, 700), size_hint=(None, None))
        self._bind_marc_popup_events(marc_popup)
        marc_popup.open()

    def _on_start_wonderfetch(self, backend, method, identifier, volume=None, catalog=None):
        self._ensure_book_path_and_init()
        self._wonderfetch_popup.book_dir = backend.book_path
        old_pallet = self.ids.box_id2_input.text
        if identifier:
            self._wonderfetch_popup.open_search(method, identifier, volume, catalog, old_pallet)
        else:
            self._wonderfetch_popup.open()

    def _on_task_progress(self, backend, report):
        self._progress_popup.message = report.get('message', None) or ''
        self._progress_popup.progress = report.get('progress', 0)

    def _on_error(self, backend, error_message):
        self.show_error(error_message)

    def _on_metadata_error(self, backend, identifier):
        message = (
            'show_create_confirm: Cannot verify item with archive.org\n'
            'Would you like to create the item\n[b]{id}[/b] locally?'
                .format(id=identifier)
        )
        popup = QuestionPopup(title='Offline mode',
                              message=message,
                              extra={'identifier': identifier})
        popup.bind(on_submit=self._on_create_offline_item_popup_submit)
        popup.open()

    def _on_metadata_deferred(self, *args, **kwargs):
        self.done()

    def _on_create_offline_item_popup_submit(self, popup, option):
        if option == popup.OPTION_YES:
            self.backend.create_offline_item(popup.extra['identifier'])

    def _on_offline_item_created(self, backend):
        popup = InfoPopup(
            title='Offline mode',
            message=('Ok, created {} successfully!'
                     .format(backend.get_identifier())),
        )
        popup.open()

    def _on_select_identifier(self, backend, identifiers):
        popup = UniversalIDPopup(identifiers=identifiers)
        popup.fbind('on_submit', self._on_universal_id_popup_submit)
        popup.open()

    def _on_universal_id_popup_submit(self, popup, identifier):
        self.backend.load_metadata_via_identifier(identifier)

    def _on_wonderfetch_scan_book(self, dwwi_popup, payload, search_id, volume, *args):
        if payload:
            extra = self.collect_global_book_settings()
            self.backend.load_metadata_via_openlibrary(payload, extra, search_id, volume)

    def _on_wonderfetch_cannot_scan_book(self, dwwi_widget, catalog, search_key, response):
        self._save_metadata_if_valid(force=True)
        if search_key:
            self.backend.cannot_scan_book(catalog, search_key, response)

    def _on_wonderfetch_reject_book(self, dwwi_popup, isbn, volume, *args):
        self._save_metadata_if_valid(force=True)

    def _on_wonderfetch_success(self, *args, **kwargs):
        self.save_metadata()

    def _on_wonderfetch_retrieval_error(self, popup, error, wid, method, catalog):
        popup.dismiss()
        msg = '[b]We are currently unable to retrieve\nthe metadata for this book.[/b]\n\n' \
              'This could be due to a local or archive.org network issue. ' \
              'You can defer the retrieval to begin shooting the ' \
              'book right now and retry fetching the metadata later.' \
              '\n\n[b]Do you want to defer retrieving ' \
              'metadata and continue to scanning[/b].'

        # prolly wanna use a local method to dismiss, alert and stuff
        ok_callback = partial(self.backend.on_end_wonderfetch_failure,
                              error, wid, method, catalog)
        self.show_choice(title='Defer metadata retrieval?', message=msg,
                         ok_callback=ok_callback)

    def on_marc_selected_record(self, widget, query, data):
        formatted_value = lambda v: v.encode('utf-8').decode('utf-8').encode('ascii', 'ignore') \
                                    if v is not None else 'unknown'
        title = formatted_value(data['dc_meta']['metadata'].get('title', 'title unknown'))
        author = formatted_value(data['dc_meta']['metadata'].get('creator', 'creator unknown'))
        date = formatted_value(data['dc_meta']['metadata'].get('year', 'year unknown'))
        msg = 'Would you like to load metadata for ' \
              '[b]{title}[/b] by [b]{author}[b] ({date})?'.format(
                title=title,
                author=author,
                date=date,
                )
        ok_callback = partial(self._on_marc_record_confirmation,
                              marc_widget=widget, query=query, data=data)
        self.show_choice('Load this MARC?',
                         msg, ok_callback=ok_callback)

    def _on_marc_record_confirmation(self, marc_widget, query, data):
        marc_widget.close()
        self.backend.extract_metadata_from_marc_search(query, data)

    def _on_marc_downloaded_metadata(self, marc_popup, metadata, *args):
        # TODO: Validate downloaded metadata
        self.backend.set_metadata(metadata)
        self.backend.save_metadata()
        self.backend.reinit()

    def _on_input_disabled(self, *args):
        ids = self.ids
        if self.disabled or not self.backend.is_initialized():
            return
        disabled = self.input_disabled
        ids.preloaded_panel.disabled = disabled
        ids.dwwi_and_marc_panel.disabled = disabled
        ids.metadata_form.layout_manager.disabled = disabled
        ids.add_new_field_button.disabled = disabled
        if not disabled:
            # It's possible that all views did not set disabled to False,
            # and therefore we have to refresh the form when disabled is False
            ids.metadata_form.refresh_from_layout()

    def _on_book_rejected(self, *args, **kwargs):
        self.target_extra = None
        if self._progress_popup:
            self._progress_popup.dismiss()
        self.cancel()

    def on_pre_enter(self, *args):
        self.ids.preloaded_id_input.text = u''
        self.ids.identifier_input.text = u''
        extra = self.target_extra
        if extra and extra.get('should_create_new_book', False):
            self.backend.create_new_book()
        self.backend.init()

    def on_leave(self, *args):
        if self._progress_popup:
            self._progress_popup.dismiss()
        self.target_extra = None
        self.backend.reset()
        self.backend.book_path = None

    def on_books_db(self, screen, books_db):
        self.backend.books_db = books_db

    def on_task_scheduler(self, screen, task_scheduler):
        self.backend.task_scheduler = task_scheduler

    def on_done(self):
        pass

    def on_cancel(self):
        pass

    def on_catalogs(self, *args, **kwargs):
        self.ids.catalog_spinner.values[:] = self.catalogs
        if self.current_catalog == 'none':
            self.ids.catalog_spinner.text = self._wonderfetch_popup.default_catalog

    def _on_rcs_updated(self, *args, **kwargs):
        self._populate_collection_sets()
コード例 #10
0
ファイル: statlist.py プロジェクト: colinsongf/LiSE
class StatRowListItemContainer(BoxLayout):
    key = ObjectProperty()
    reg = ObjectProperty()
    unreg = ObjectProperty()
    gett = ObjectProperty()
    sett = ObjectProperty()
    listen = ObjectProperty()
    unlisten = ObjectProperty()
    config = DictProperty()
    control = OptionProperty(
        'readout', options=['readout', 'textinput', 'togglebutton', 'slider']
    )
    licls = {
        'readout': StatRowLabel,
        'textinput': StatRowTextInput,
        'togglebutton': StatRowToggleButton,
        'slider': StatRowSlider
    }

    def set_value(self, *args):
        self.sett(self.key, self.value)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.bind(
            key=self.remake,
            control=self.remake,
            config=self.remake,
            parent=self.remake
        )

    @trigger
    def remake(self, *args):
        if not hasattr(self, 'label'):
            self.label = Label(text=str(self.key))

            def updlabel(*args):
                self.label.text = str(self.key)
            self.bind(key=updlabel)
            self.add_widget(self.label)
        if hasattr(self, 'wid'):
            self.remove_widget(self.wid)
            del self.wid
        cls = self.licls[self.control]
        self.wid = cls(
            key=self.key,
            gett=self.gett,
            sett=self.sett,
            config=self.config,
            listen=self.listen,
            unlisten=self.unlisten
        )
        self.bind(
            key=self.wid.setter('key'),
            gett=self.wid.setter('gett'),
            sett=self.wid.setter('sett'),
            config=self.wid.setter('config'),
            listen=self.wid.setter('listen'),
            unlisten=self.wid.setter('unlisten')
        )
        self.add_widget(self.wid)
コード例 #11
0
ファイル: statlist.py プロジェクト: colinsongf/LiSE
class BaseStatListView(RecycleView):
    control = DictProperty({})
    config = DictProperty({})
    mirror = DictProperty({})
    proxy = ObjectProperty()
    engine = ObjectProperty()
    app = ObjectProperty()

    def __init__(self, **kwargs):
        self._listeners = {}
        self.bind(
            proxy=self.refresh_mirror,
            mirror=self._trigger_upd_data
        )
        super().__init__(**kwargs)

    def on_app(self, *args):
        self.app.bind(
            branch=self.refresh_mirror,
            turn=self.refresh_mirror,
            tick=self.refresh_mirror
        )

    def del_key(self, k):
        if k not in self.mirror:
            raise KeyError
        del self.proxy[k]
        del self.mirror[k]
        if k in self.control:
            del self.proxy['_control'][k]
            del self.control[k]
        if k in self.config:
            del self.proxy['_config'][k]
            del self.config[k]

    def set_value(self, k, v):
        if self.engine is None or self.proxy is None:
            self._trigger_set_value(k, v)
            return
        if v is None:
            del self.proxy[k]
            del self.mirror[k]
        else:
            try:
                vv = self.engine.json_load(v)
            except (TypeError, ValueError):
                vv = v
            self.proxy[k] = self.mirror[k] = vv

    def _trigger_set_value(self, k, v, *args):
        todo = partial(self.set_value, k, v)
        Clock.unschedule(todo)
        Clock.schedule_once(todo, 0)

    def init_control_config(self, key):
        if key not in self.control:
            self.set_control(key, 'readout')
        if key not in self.config:
            cfgd = dict(self.config)
            cfgd[key] = default_cfg
            self.proxy['_config'] = cfgd
        else:
            cfgd = dict(self.config)
            for option in default_cfg:
                if option not in cfgd[key]:
                    cfgd[key][option] = default_cfg[option]
            self.proxy['_config'] = cfgd

    def set_control(self, key, control):
        if '_control' not in self.mirror:
            ctrld = {key: control}
        else:
            ctrld = dict(self.control)
            ctrld[key] = control
        self.proxy['_control'] \
            = self.mirror['_control'] \
            = self.control \
            = ctrld

    def set_config(self, key, option, value):
        if '_config' not in self.mirror:
            self.proxy['_config'] \
                = self.config \
                = {key: {option: value}}
        elif key in self.config:
            newcfg = dict(self.config)
            newcfg[key][option] = value
            self.proxy['_config'] = self.config = newcfg
        else:
            newcfg = dict(default_cfg)
            newcfg[option] = value
            self.proxy['_config'][key] = self.config = newcfg

    def set_configs(self, key, d):
        if '_config' in self.mirror:
            self.mirror['_config'][key] = self.proxy['_config'][key] = d
        else:
            self.mirror['_config'] = self.proxy['_config'] = {key: d}
        self.config[key] = d

    def iter_data(self):
        for (k, v) in self.mirror.items():
            if (
                not (isinstance(k, str) and k[0] == '_') and
                k not in (
                    'character',
                    'name',
                    'location',
                    'next_location',
                    'locations',
                    'arrival_time',
                    'next_arrival_time'
                )
            ):
                yield k, v

    @trigger
    def refresh_mirror(self, *args):
        Logger.debug('{}: refreshing mirror'.format(type(self)))
        if self.proxy is None:
            return
        new = dict(self.proxy)
        if '_control' in new and new['_control'] != self.control:
            self.control = new['_control']
        if '_config' in new and new['_config'] != self.config:
            self.config = new['_config']
        if self.mirror != new:
            self.mirror = new

    def munge(self, k, v):
        return {
            'key': k,
            'reg': self._reg_widget,
            'unreg': self._unreg_widget,
            'gett': self.proxy.__getitem__,
            'sett': self.set_value,
            'listen': self.proxy.connect,
            'unlisten': self.proxy.disconnect,
            'control': self.control.get(k, 'readout'),
            'config': self.config.get(k, default_cfg)
        }

    def upd_data(self, *args):
        self.data = [self.munge(k, v) for k, v in self.iter_data()]
    _trigger_upd_data = trigger(upd_data)

    def _reg_widget(self, w, *args):
        if not self.mirror:
            Clock.schedule_once(partial(self._reg_widget, w), 0)
            return

        def listen(*args):
            if w.key not in self.mirror:
                return
            if w.value != self.mirror[w.key]:
                w.value = self.mirror[w.key]
        self._listeners[w.key] = listen
        self.bind(mirror=listen)

    def _unreg_widget(self, w):
        if w.key in self._listeners:
            self.unbind(mirror=self._listeners[w.key])
コード例 #12
0
class wfpiconsole(App):

    # Define App class observation dictionary properties
    Obs = DictProperty      ([('rapidSpd','--'),       ('rapidDir','----'),    ('rapidShift','-'),
                              ('WindSpd','-----'),     ('WindGust','--'),      ('WindDir','---'),
                              ('AvgWind','--'),        ('MaxGust','--'),       ('RainRate','---'),
                              ('TodayRain','--'),      ('YesterdayRain','--'), ('MonthRain','--'),
                              ('YearRain','--'),       ('Radiation','----'),   ('UVIndex','----'),
                              ('peakSun','-----'),     ('outTemp','--'),       ('outTempMin','---'),
                              ('outTempMax','---'),    ('inTemp','--'),        ('inTempMin','---'),
                              ('inTempMax','---'),     ('Humidity','--'),      ('DewPoint','--'),
                              ('Pres','---'),          ('MaxPres','---'),      ('MinPres','---'),
                              ('PresTrend','----'),    ('FeelsLike','----'),   ('StrikeDeltaT','-----'),
                              ('StrikeDist','--'),     ('StrikeFreq','----'),  ('Strikes3hr','-'),
                              ('StrikesToday','-'),    ('StrikesMonth','-'),   ('StrikesYear','-')
                             ])
    Astro = DictProperty    ([('Sunrise',['-','-',0]), ('Sunset',['-','-',0]), ('Dawn',['-','-',0]),
                              ('Dusk',['-','-',0]),    ('sunEvent','----'),    ('sunIcon',['-',0,0]),
                              ('Moonrise',['-','-']), ('Moonset',['-','-']),   ('NewMoon','--'),
                              ('FullMoon','--'),      ('Phase','---'),         ('Reformat','-'),
                             ])
    MetData = DictProperty  ([('Weather','Building'),  ('Temp','--'),          ('Precip','--'),
                              ('WindSpd','--'),        ('WindDir','--'),       ('Valid','--')
                             ])
    Sager = DictProperty    ([('Forecast','--'),       ('Issued','--')])
    System = DictProperty   ([('Time','-'),            ('Date','-')])
    Version = DictProperty  ([('Latest','-')])

    # Define App class configParser properties
    BarometerMax = ConfigParserProperty('-','System', 'BarometerMax','wfpiconsole')
    BarometerMin = ConfigParserProperty('-','System', 'BarometerMin','wfpiconsole')
    IndoorTemp   = ConfigParserProperty('-','Display','IndoorTemp',  'wfpiconsole')

    # BUILD 'WeatherFlowPiConsole' APP CLASS
    # --------------------------------------------------------------------------
    def build(self):

        # Load user configuration from wfpiconsole.ini and define Settings panel
        # type
        self.config = ConfigParser(allow_no_value=True,name='wfpiconsole')
        self.config.optionxform = str
        self.config.read('wfpiconsole.ini')
        self.settings_cls = SettingsWithSidebar

        # Force window size if required based on hardware type
        if self.config['System']['Hardware'] == 'Pi4':
            Window.size = (800,480)
            Window.borderless = 1
            Window.top = 0
        elif self.config['System']['Hardware'] == 'Other':
            Window.size = (800,480)

        # Initialise real time clock
        Clock.schedule_interval(partial(system.realtimeClock,self.System,self.config),1.0)

        # Initialise Sunrise and Sunset time, Moonrise and Moonset time, and
        # MetOffice or DarkSky weather forecast
        astro.SunriseSunset(self.Astro,self.config)
        astro.MoonriseMoonset(self.Astro,self.config)
        forecast.Download(self.MetData,self.config)

        # Generate Sager Weathercaster forecast
        Thread(target=sagerForecast.Generate, args=(self.Sager,self.config), name="Sager", daemon=True).start()

        # Initialise websocket connection
        self.WebsocketConnect()

        # Check for latest version
        Clock.schedule_once(partial(system.checkVersion,self.Version,self.config,updateNotif))

        # Initialise Station class, and set device status to be checked every
        # second
        self.Station = Station()
        Clock.schedule_interval(self.Station.getDeviceStatus,1.0)

        # Schedule function calls
        Clock.schedule_interval(self.UpdateMethods,1.0)
        Clock.schedule_interval(partial(astro.sunTransit,self.Astro,self.config),1.0)
        Clock.schedule_interval(partial(astro.moonPhase ,self.Astro,self.config),1.0)

    # BUILD 'WeatherFlowPiConsole' APP CLASS SETTINGS
    # --------------------------------------------------------------------------
    def build_settings(self,settingsScreen):

        # Register setting types
        settingsScreen.register_type('ScrollOptions',     SettingScrollOptions)
        settingsScreen.register_type('FixedOptions',      SettingFixedOptions)
        settingsScreen.register_type('ToggleTemperature', SettingToggleTemperature)

        # Add required panels to setting screen. Remove Kivy settings panel
        settingsScreen.add_json_panel('Display',          self.config, data = settings.JSON('Display'))
        settingsScreen.add_json_panel('Primary Panels',   self.config, data = settings.JSON('Primary'))
        settingsScreen.add_json_panel('Secondary Panels', self.config, data = settings.JSON('Secondary'))
        settingsScreen.add_json_panel('Units',            self.config, data = settings.JSON('Units'))
        settingsScreen.add_json_panel('Feels Like',       self.config, data = settings.JSON('FeelsLike'))
        self.use_kivy_settings = False

    # OVERLOAD 'on_config_change' TO MAKE NECESSARY CHANGES TO CONFIG VALUES
    # WHEN REQUIRED
    # --------------------------------------------------------------------------
    def on_config_change(self,config,section,key,value):

        # Update current weather forecast and Sager Weathercaster forecast when
        # temperature or wind speed units are changed
        if section == 'Units' and key in ['Temp','Wind']:
            if self.config['Station']['Country'] == 'GB':
                forecast.ExtractMetOffice(self.MetData,self.config)
            else:
                forecast.ExtractDarkSky(self.MetData,self.config)
            if key == 'Wind' and 'Dial' in self.Sager:
                self.Sager['Dial']['Units'] = value
                self.Sager['Forecast'] = sagerForecast.getForecast(self.Sager['Dial'])

        # Update "Feels Like" temperature cutoffs in wfpiconsole.ini and the
        # settings screen when temperature units are changed
        if section == 'Units' and key == 'Temp':
            for Field in self.config['FeelsLike']:
                if 'c' in value:
                    Temp = str(round((float(self.config['FeelsLike'][Field])-32) * 5/9))
                    self.config.set('FeelsLike',Field,Temp)
                elif 'f' in value:
                    Temp = str(round(float(self.config['FeelsLike'][Field])*9/5 + 32))
                    self.config.set('FeelsLike',Field,Temp)
            self.config.write()
            panels = self._app_settings.children[0].content.panels
            for Field in self.config['FeelsLike']:
                for panel in panels.values():
                    if panel.title == 'Feels Like':
                        for item in panel.children:
                            if isinstance(item,Factory.SettingToggleTemperature):
                                if item.title.replace(' ','') == Field:
                                    item.value = self.config['FeelsLike'][Field]

        # Update barometer limits when pressure units are changed
        if section == 'Units' and key == 'Pressure':
            Units = ['mb','hpa','inhg','mmhg']
            Max = ['1050','1050','31.0','788']
            Min = ['950','950','28.0','713']
            self.config.set('System','BarometerMax',Max[Units.index(value)])
            self.config.set('System','BarometerMin',Min[Units.index(value)])

        # Update primary and secondary panels displayed on CurrentConditions
        # screen
        if section in ['PrimaryPanels','SecondaryPanels']:
            for Panel,Type in App.get_running_app().config['PrimaryPanels'].items():
                if Panel == key:
                    self.CurrentConditions.ids[Panel].clear_widgets()
                    self.CurrentConditions.ids[Panel].add_widget(eval(Type + 'Panel')())
                    break

        # Update button layout displayed on CurrentConditions screen
        if section == 'SecondaryPanels':
            ii = 0
            self.CurrentConditions.buttonList = []
            Button = ['Button' + Num for Num in ['One','Two','Three','Four','Five','Six']]
            for Panel, Type in App.get_running_app().config['SecondaryPanels'].items():
                self.CurrentConditions.ids[Button[ii]].clear_widgets()
                if Type and Type != 'None':
                    self.CurrentConditions.ids[Button[ii]].add_widget(eval(Type + 'Button')())
                    self.CurrentConditions.buttonList.append([Button[ii],Panel,Type,'Primary'])
                    ii += 1

            # Change 'None' for secondary panel selection to blank in config
            # file
            if value == 'None':
                self.config.set(section,key,'')
                self.config.write()
                panels = self._app_settings.children[0].content.panels
                for panel in panels.values():
                    if panel.title == 'Secondary Panels':
                        for item in panel.children:
                            if isinstance(item,Factory.SettingOptions):
                                if item.title.replace(' ','') == key:
                                    item.value = ''
                                    break

    # CONNECT TO THE SECURE WEATHERFLOW WEBSOCKET SERVER
    # --------------------------------------------------------------------------
    def WebsocketConnect(self):
        Server = 'wss://ws.weatherflow.com/swd/data?api_key=' + self.config['Keys']['WeatherFlow']
        self._factory = WeatherFlowClientFactory(Server,self)
        reactor.connectTCP('ws.weatherflow.com',80,self._factory,)

    # SEND MESSAGE TO THE WEATHERFLOW WEBSOCKET SERVER
    # --------------------------------------------------------------------------
    def WebsocketSendMessage(self,Message):
        Message = Message.encode('utf8')
        proto = self._factory._proto
        if Message and proto:
            proto.sendMessage(Message)

    # DECODE THE WEATHERFLOW WEBSOCKET MESSAGE
    # --------------------------------------------------------------------------
    def WebsocketDecodeMessage(self,Msg):

        # Extract type of received message
        Type = Msg['type']

        # Start listening for device observations and events upon connection of
        # websocket based on device IDs specified in user configuration file
        if Type == 'connection_opened':
            if self.config['Station']['TempestID']:
                self.WebsocketSendMessage('{"type":"listen_start",' +
                                          ' "device_id":' + self.config['Station']['TempestID'] + ',' +
                                          ' "id":"Sky"}')
                self.WebsocketSendMessage('{"type":"listen_rapid_start",' +
                                          ' "device_id":' + self.config['Station']['TempestID'] + ',' +
                                          ' "id":"rapidWind"}')
            elif self.config['Station']['SkyID']:
                self.WebsocketSendMessage('{"type":"listen_start",' +
                                          ' "device_id":' + self.config['Station']['SkyID'] + ',' +
                                          ' "id":"Sky"}')
                self.WebsocketSendMessage('{"type":"listen_rapid_start",' +
                                          ' "device_id":' + self.config['Station']['SkyID'] + ',' +
                                          ' "id":"rapidWind"}')
            if self.config['Station']['OutAirID']:
                self.WebsocketSendMessage('{"type":"listen_start",' +
                                          ' "device_id":' + self.config['Station']['OutAirID'] + ',' +
                                          ' "id":"OutdoorAir"}')
            if self.config['Station']['InAirID']:
                self.WebsocketSendMessage('{"type":"listen_start",' +
                                          ' "device_id":' + self.config['Station']['InAirID'] + ',' +
                                          ' "id":"IndoorAir"}')

        # Extract observations from obs_st websocket message
        elif Type == 'obs_st':
            Thread(target=websocket.Tempest, args=(Msg,self), name="Tempest", daemon=True).start()

        # Extract observations from obs_sky websocket message
        elif Type == 'obs_sky':
            Thread(target=websocket.Sky, args=(Msg,self), name="Sky", daemon=True).start()

        # Extract observations from obs_air websocket message based on device
        # ID
        elif Type == 'obs_air':
            if self.config['Station']['InAirID'] and Msg['device_id'] == int(self.config['Station']['InAirID']):
                Thread(target=websocket.indoorAir, args=(Msg,self),  name="indoorAir",  daemon=True).start()
            if self.config['Station']['OutAirID'] and Msg['device_id'] == int(self.config['Station']['OutAirID']):
                Thread(target=websocket.outdoorAir, args=(Msg,self), name="outdoorAir", daemon=True).start()

        # Extract observations from rapid_wind websocket message
        elif Type == 'rapid_wind':
            websocket.rapidWind(Msg,self)

        # Extract observations from evt_strike websocket message
        elif Type == 'evt_strike':
            websocket.evtStrike(Msg,self)

    # UPDATE 'WeatherFlowPiConsole' APP CLASS METHODS AT REQUIRED INTERVALS
    # --------------------------------------------------------------------------
    def UpdateMethods(self,dt):

        # Get current time in station timezone
        Tz = pytz.timezone(self.config['Station']['Timezone'])
        Now = datetime.now(pytz.utc).astimezone(Tz)
        Now = Now.replace(microsecond=0)

        # At 5 minutes past each hour, download a new forecast for the Station
        # location
        if (Now.minute,Now.second) == (5,0):
            forecast.Download(self.MetData,self.config)

        # At the top of each hour update the on-screen forecast for the Station
        # location
        if self.config['Station']['Country'] == 'GB':
            if Now.hour > self.MetData['Time'].hour or Now.date() > self.MetData['Time'].date():
                forecast.ExtractMetOffice(self.MetData,self.config)
                self.MetData['Time'] = Now
        elif self.config['Keys']['DarkSky']:
            if Now.hour > self.MetData['Time'].hour or Now.date() > self.MetData['Time'].date():
                forecast.ExtractDarkSky(self.MetData,self.config)
                self.MetData['Time'] = Now

        # Once dusk has passed, calculate new sunrise/sunset times
        if Now >= self.Astro['Dusk'][0]:
            self.Astro = astro.SunriseSunset(self.Astro,self.config)

        # Once moonset has passed, calculate new moonrise/moonset times
        if Now > self.Astro['Moonset'][0]:
            self.Astro = astro.MoonriseMoonset(self.Astro,self.config)

        # At midnight, update Sunset, Sunrise, Moonrise and Moonset Kivy Labels
        if self.Astro['Reformat'] and Now.replace(second=0).time() == time(0,0,0):
            self.Astro = astro.Format(self.Astro,self.config,"Sun")
            self.Astro = astro.Format(self.Astro,self.config,"Moon")
コード例 #13
0
ファイル: listview.py プロジェクト: zwjwhxz/kivy
class ListView(AbstractView, EventDispatcher):
    ''':class:`~kivy.uix.listview.ListView` is a primary high-level widget,
    handling the common task of presenting items in a scrolling list.
    Flexibility is afforded by use of a variety of adapters to interface with
    data.

    The adapter property comes via the mixed in
    :class:`~kivy.uix.abstractview.AbstractView` class.

    :class:`~kivy.uix.listview.ListView` also subclasses
    :class:`EventDispatcher` for scrolling. The event *on_scroll_complete* is
    used in refreshing the main view.

    For a simple list of string items, without selection, use
    :class:`~kivy.adapters.simplelistadapter.SimpleListAdapter`. For list items
    that respond to selection, ranging from simple items to advanced
    composites, use :class:`~kivy.adapters.listadapter.ListAdapter`. For an
    alternate powerful adapter, use
    :class:`~kivy.adapters.dictadapter.DictAdapter`, rounding out the choice
    for designing highly interactive lists.

    :Events:
        `on_scroll_complete`: (boolean, )
            Fired when scrolling completes.
    '''

    divider = ObjectProperty(None)
    '''[TODO] Not used.
    '''

    divider_height = NumericProperty(2)
    '''[TODO] Not used.
    '''

    container = ObjectProperty(None)
    '''The container is a :class:`~kivy.uix.gridlayout.GridLayout` widget held
    within a :class:`~kivy.uix.scrollview.ScrollView` widget.  (See the
    associated kv block in the Builder.load_string() setup). Item view
    instances managed and provided by the adapter are added to this container.
    The container is cleared with a call to clear_widgets() when the list is
    rebuilt by the populate() method. A padding
    :class:`~kivy.uix.widget.Widget` instance is also added as needed,
    depending on the row height calculations.

    :attr:`container` is an :class:`~kivy.properties.ObjectProperty` and
    defaults to None.
    '''

    row_height = NumericProperty(None)
    '''The row_height property is calculated on the basis of the height of the
    container and the count of items.

    :attr:`row_height` is a :class:`~kivy.properties.NumericProperty` and
    defaults to None.
    '''

    item_strings = ListProperty([])
    '''If item_strings is provided, create an instance of
    :class:`~kivy.adapters.simplelistadapter.SimpleListAdapter` with this list
    of strings, and use it to manage a no-selection list.

    :attr:`item_strings` is a :class:`~kivy.properties.ListProperty` and
    defaults to [].
    '''

    scrolling = BooleanProperty(False)
    '''If the scroll_to() method is called while scrolling operations are
    happening, a call recursion error can occur. scroll_to() checks to see that
    scrolling is False before calling populate(). scroll_to() dispatches a
    scrolling_complete event, which sets scrolling back to False.

    :attr:`scrolling` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to False.
    '''

    _index = NumericProperty(0)
    _sizes = DictProperty({})
    _count = NumericProperty(0)

    _wstart = NumericProperty(0)
    _wend = NumericProperty(-1)

    __events__ = ('on_scroll_complete', )

    def __init__(self, **kwargs):
        # Check for an adapter argument. If it doesn't exist, we
        # check for item_strings in use with SimpleListAdapter
        # to make a simple list.
        if 'adapter' not in kwargs:
            if 'item_strings' not in kwargs:
                # Could be missing, or it could be that the ListView is
                # declared in a kv file. If kv is in use, and item_strings is
                # declared there, then item_strings will not be set until after
                # __init__(). So, the data=[] set will temporarily serve for
                # SimpleListAdapter instantiation, with the binding to
                # item_strings_changed() handling the eventual set of the
                # item_strings property from the application of kv rules.
                list_adapter = SimpleListAdapter(data=[], cls=Label)
            else:
                list_adapter = SimpleListAdapter(data=kwargs['item_strings'],
                                                 cls=Label)
            kwargs['adapter'] = list_adapter

        super(ListView, self).__init__(**kwargs)

        populate = self._trigger_populate = Clock.create_trigger(
            self._spopulate, -1)
        self._trigger_reset_populate = \
            Clock.create_trigger(self._reset_spopulate, -1)

        fbind = self.fbind
        fbind('size', populate)
        fbind('pos', populate)
        fbind('item_strings', self.item_strings_changed)
        fbind('adapter', populate)

        bind_adapter = self._trigger_bind_adapter = Clock.create_trigger(
            lambda dt: self.adapter.bind_triggers_to_view(
                self._trigger_reset_populate), -1)
        fbind('adapter', bind_adapter)

        # The bindings setup above sets self._trigger_populate() to fire
        # when the adapter changes, but we also need this binding for when
        # adapter.data and other possible triggers change for view updating.
        # We don't know that these are, so we ask the adapter to set up the
        # bindings back to the view updating function here.
        bind_adapter()

    # Added to set data when item_strings is set in a kv template, but it will
    # be good to have also if item_strings is reset generally.
    def item_strings_changed(self, *args):
        self.adapter.data = self.item_strings

    def _scroll(self, scroll_y):
        if self.row_height is None:
            return
        self._scroll_y = scroll_y
        scroll_y = 1 - min(1, max(scroll_y, 0))
        container = self.container
        mstart = (container.height - self.height) * scroll_y
        mend = mstart + self.height

        # convert distance to index
        rh = self.row_height
        istart = int(ceil(mstart / rh))
        iend = int(floor(mend / rh))

        istart = max(0, istart - 1)
        iend = max(0, iend - 1)

        if istart < self._wstart:
            rstart = max(0, istart - 10)
            self.populate(rstart, iend)
            self._wstart = rstart
            self._wend = iend
        elif iend > self._wend:
            self.populate(istart, iend + 10)
            self._wstart = istart
            self._wend = iend + 10

    def _spopulate(self, *args):
        self.populate()

    def _reset_spopulate(self, *args):
        self._wend = -1
        self.populate()
        # simulate the scroll again, only if we already scrolled before
        # the position might not be the same, mostly because we don't know the
        # size of the new item.
        if hasattr(self, '_scroll_y'):
            self._scroll(self._scroll_y)

    def populate(self, istart=None, iend=None):
        container = self.container
        sizes = self._sizes
        rh = self.row_height

        # ensure we know what we want to show
        if istart is None:
            istart = self._wstart
            iend = self._wend

        # clear the view
        container.clear_widgets()

        # guess only ?
        if iend is not None and iend != -1:

            # fill with a "padding"
            fh = 0
            for x in range(istart):
                fh += sizes[x] if x in sizes else rh
            container.add_widget(Widget(size_hint_y=None, height=fh))

            # now fill with real item_view
            index = istart
            while index <= iend:
                item_view = self.adapter.get_view(index)
                index += 1
                if item_view is None:
                    continue
                sizes[index] = item_view.height
                container.add_widget(item_view)
        else:
            available_height = self.height
            real_height = 0
            index = self._index
            count = 0
            while available_height > 0:
                item_view = self.adapter.get_view(index)
                if item_view is None:
                    break
                sizes[index] = item_view.height
                index += 1
                count += 1
                container.add_widget(item_view)
                available_height -= item_view.height
                real_height += item_view.height

            self._count = count

            # extrapolate the full size of the container from the size
            # of view instances in the adapter
            if count:
                container.height = \
                    real_height / count * self.adapter.get_count()
                if self.row_height is None:
                    self.row_height = real_height / count

    def scroll_to(self, index=0):
        if not self.scrolling:
            self.scrolling = True
            self._index = index
            self.populate()
            self.dispatch('on_scroll_complete')

    def on_scroll_complete(self, *args):
        self.scrolling = False
コード例 #14
0
ファイル: storypixies.py プロジェクト: netpixies/storypixies
class StoryPixiesApp(App):
    theme_cls = ThemeManager()
    title = 'Story Pixies'
    manager = ObjectProperty(None)
    creator = ObjectProperty(None)

    story = ObjectProperty(None)
    story_title = ObjectProperty(None)
    story_pages = ObjectProperty(None)

    libraries = DictProperty()
    templates = DictProperty()
    selected_library = StringProperty(None)

    library_dir = ObjectProperty(None)

    def __init__(self, **kwargs):
        """
        Initializes the root widget.
        Sets property defaults.
        Sets selected library as the first found library.
        :param kwargs:
        """
        super(StoryPixiesApp, self).__init__(**kwargs)

        # Initialize libraries from files
        self.creator_disabled = kwargs.get('creator_disabled')
        self.library_dir = (Path(__file__).parents[0].absolute()).joinpath("libraries")
        self.add_libraries()

    def build(self):
        kv_file = (Path(__file__).parents[0].absolute()).joinpath("layout.kv")
        root_widget = Builder.load_file(str(kv_file))
        self.title = 'Story Pixies'
        # self.settings_cls = LibrarySettings
        self.use_kivy_settings = False
        self.creator = root_widget.ids.creator
        self.manager = root_widget.ids.manager
        self.use_kivy_settings = False
        return root_widget

    def add_libraries(self):
        self.libraries = {}
        self.templates = {}

        for library in self.library_dir.iterdir():
            if library.stem != 'Templates':
                self.libraries[library.stem] = SingleLibrary(name=library.stem,
                                                             library_dir=library)
            else:
                self.templates[library.stem] = SingleLibrary(name=library.stem,
                                                             library_dir=library)
        if len(self.libraries.keys()) == 0:
            self.set_selected_library(None)
        else:
            self.set_selected_library(self.libraries.keys()[0])

    def get_library_object(self):
        return self.libraries.get(self.selected_library)

    def set_selected_library(self, library, _=None):
        if library in self.libraries.keys():
            self.selected_library = library
        else:
            self.selected_library = None

    def creator_screen(self):
        if not self.creator_disabled:
            self.switch_screen('creator')
        else:
            Snackbar(text="Creator mode is disabled").show()

    def switch_screen(self, screen_name):
        self.manager.current = screen_name

    def library_screen(self, _):
        self.switch_screen('library_screen')

    def story_screen(self):
        self.switch_screen('story_screen')

    def story_title_screen(self):
        self.switch_screen('story_title')

    def story_pages_screen(self):
        self.creator.setup_settings_panel()
        self.switch_screen('story_pages')

    @staticmethod
    def intro_text():
        return """
コード例 #15
0
 class DictWidgetFalse(Label):
     button = DictProperty({'button': None}, rebind=False)
コード例 #16
0
class SelectableRecycleLayout(LayoutSelectionBehavior):
    """Adds selection and focus behavior to the view."""
    owner = ObjectProperty()
    selected = DictProperty()
    selects = ListProperty()
    multiselect = BooleanProperty(False)

    def clear_selects(self):
        self.selects = []

    def refresh_selection(self):
        for node in self.children:
            try:  #possible for nodes to not be synched with data
                data = self.parent.data[node.index]
                node.selected = data['selected']
            except:
                pass

    def deselect_all(self):
        for data in self.parent.data:
            data['selected'] = False
        self.refresh_selection()
        self.selects = []
        self.selected = {}

    def select_all(self):
        self.selects = []
        selects = []
        for data in self.parent.data:
            if data['selectable']:
                data['selected'] = True
                selects.append(data)
        self.selects = selects
        self.selected = selects[-1]
        self.refresh_selection()

    def select_node(self, node):
        super().select_node(node)
        if not self.multiselect:
            self.deselect_all()
        node.selected = True
        self.selects.append(node.data)
        if node.data not in self.parent.data:
            return
        self.parent.data[self.parent.data.index(node.data)]['selected'] = True
        node.data['selected'] = True
        self.selected = node.data

    def deselect_node(self, node):
        super().deselect_node(node)
        if node.data in self.selects:
            self.selects.remove(node.data)
        if self.selected == node.data:
            if self.selects:
                self.selected = self.selects[-1]
            else:
                self.selected = {}
        if node.data in self.parent.data:
            parent_index = self.parent.data.index(node.data)
            parent_data = self.parent.data[parent_index]
            parent_data['selected'] = False
        node.selected = False
        node.data['selected'] = False

    def click_node(self, node):
        #Called by a child widget when it is clicked on
        if node.selected:
            if self.multiselect:
                self.deselect_node(node)
            else:
                pass
                #self.deselect_all()
        else:
            if not self.multiselect:
                self.deselect_all()
            self.select_node(node)
            self.selected = node.data

    def remove_node(self, node):
        self.parent.data.pop(node.index)

    def select_range(self, *_):
        if self.multiselect and self.selected and self.selected['selectable']:
            select_index = self.parent.data.index(self.selected)
            selected_nodes = []
            if self.selects:
                for select in self.selects:
                    if select['selectable']:
                        if select not in self.parent.data:
                            continue
                        index = self.parent.data.index(select)
                        if index != select_index:
                            selected_nodes.append(index)
            else:
                selected_nodes = [0, len(self.parent.data)]
            if not selected_nodes:
                return
            closest_node = min(selected_nodes,
                               key=lambda x: abs(x - select_index))
            for index in range(min(select_index, closest_node),
                               max(select_index, closest_node)):
                selected = self.parent.data[index]
                selected['selected'] = True
                if selected not in self.selects:
                    self.selects.append(selected)
            self.parent.refresh_from_data()

    def toggle_select(self, *_):
        if self.multiselect:
            if self.selects:
                self.deselect_all()
            else:
                self.select_all()
        else:
            if self.selected:
                self.selected = {}
コード例 #17
0
class MainWindow(Widget):
    # UI stuff
    top_layout = ObjectProperty(None)
    response_label = ObjectProperty(None)
    output_label = ObjectProperty(None)
    output_layout = ObjectProperty(None)
    confirm_layout = ObjectProperty(None)
    confirm_confirm_layout = ObjectProperty(None)
    confirm_choose_layout = ObjectProperty(None)

    bottom_layout = ObjectProperty(None)
    functions_layout = ObjectProperty(None)
    options_layout = ObjectProperty(None)
    input_layout = ObjectProperty(None)
    text_input = ObjectProperty(None)
    ask_layout = ObjectProperty(None)
    ask_label = ObjectProperty(None)

    robot_image = ObjectProperty(None)
    
    message = DictProperty(msg)

    # Function state
    state = OptionProperty("NONE", options=["NONE", "WAIT_SERVE", "HINT_SHOWN", "SELECT_FUNC", \
    "SELECT_OPTION", "ENTER_INPUT", "WAIT_OUTPUT", "OUTPUT_SHOWN_CONFIRM", "OUTPUT_SHOWN_CHOOSE"])
    

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.hideAll()
        self.state = "WAIT_SERVE"
        self.current_func = None
        self.current_option = None
        self.diary_guess_cache = None

    def hideAll(self):
        self.top_layout.remove_widget(self.response_label)
        self.top_layout.remove_widget(self.output_layout)
        self.confirm_layout.remove_widget(self.confirm_confirm_layout)
        self.confirm_layout.remove_widget(self.confirm_choose_layout)
        self.top_layout.remove_widget(self.confirm_layout)

        self.toggleInterface('input', False)
        self.toggleInterface('functions', False)
        self.toggleInterface('options', False)
        self.toggleInterface('ask', False)

        self.robot_image.anim_delay = -1

    def toggleInterface(self, which, on):
        if (which == 'response'):
            if (on):
                self.top_layout.add_widget(self.response_label)
                self.top_layout.size_hint_y = 0.5
            else:
                self.top_layout.remove_widget(self.response_label)

        elif (which == 'output_confirm'):
            if (on):
                self.top_layout.add_widget(self.output_layout)
                self.top_layout.add_widget(self.confirm_layout)
                self.confirm_layout.add_widget(self.confirm_confirm_layout)
                self.top_layout.size_hint_y = 1.0
            else:
                self.top_layout.remove_widget(self.output_layout)
                self.confirm_layout.remove_widget(self.confirm_confirm_layout)
                self.top_layout.remove_widget(self.confirm_layout)
        
        elif (which == 'output_choose'):
            if (on):
                self.top_layout.add_widget(self.output_layout)
                self.top_layout.add_widget(self.confirm_layout)
                self.confirm_layout.add_widget(self.confirm_choose_layout)
                self.top_layout.size_hint_y = 1.0
            else:
                self.top_layout.remove_widget(self.output_layout)
                self.confirm_layout.remove_widget(self.confirm_choose_layout)
                self.top_layout.remove_widget(self.confirm_layout)
        
        elif (which == 'input'):
            if (on):
                self.bottom_layout.add_widget(self.input_layout)
            else:
                self.bottom_layout.remove_widget(self.input_layout)

        elif (which == 'functions'):
            if (on):
                self.bottom_layout.add_widget(self.functions_layout)
            else:
                self.bottom_layout.remove_widget(self.functions_layout)

        elif (which == 'options'):
            if (on):
                self.bottom_layout.add_widget(self.options_layout)
            else:
                self.bottom_layout.remove_widget(self.options_layout)

        elif (which == 'ask'):
            if (on):
                self.bottom_layout.add_widget(self.ask_layout)
            else:
                self.bottom_layout.remove_widget(self.ask_layout)

    def on_click_bot(self):
        if (self.state == "WAIT_SERVE"):
            self.state = "HINT_SHOWN"
            self.toggleInterface('response', True)
            self.response_label.text = self.message['welcome']

    def on_click_hint(self):
        if (self.state == "HINT_SHOWN"):
            self.state = "SELECT_FUNC"
            self.toggleInterface('response', False)
            self.toggleInterface('functions', True)

    def on_click_a(self):
        if (self.state == "SELECT_FUNC"):
            self.state = "SELECT_OPTION"
            self.current_func = 'a'
            self.toggleInterface('functions', False)
            self.toggleInterface('options', True)
            self.toggleInterface('response', True)
            self.response_label.text = self.message['func_a_style']

    def on_click_option1(self):
        if (self.state == "SELECT_OPTION"):
            self.state = "ENTER_INPUT"
            self.current_option = '1'
            self.toggleInterface('options', False)
            self.toggleInterface('input', True)
            self.response_label.text = self.message['func_a_hint']

    def on_click_option2(self):
        if (self.state == "SELECT_OPTION"):
            self.state = "ENTER_INPUT"
            self.current_option = '2'
            self.toggleInterface('options', False)
            self.toggleInterface('input', True)
            self.response_label.text = self.message['func_a_hint']

    def on_click_option3(self):
        if (self.state == "SELECT_OPTION"):
            self.state = "ENTER_INPUT"
            self.current_option = '3'
            self.toggleInterface('options', False)
            self.toggleInterface('input', True)
            self.response_label.text = self.message['func_a_hint']
            
    def on_click_b(self):
        if (self.state == "SELECT_FUNC"):
            self.state = "ENTER_INPUT"
            self.current_func = 'b'
            self.toggleInterface('functions', False)
            self.toggleInterface('input', True)
            self.toggleInterface('response', True)
            self.response_label.text = self.message['func_b_hint']

    def on_click_c(self):
        if (self.state == "SELECT_FUNC"):
            self.state = "ENTER_INPUT"
            self.current_func = 'c'
            self.toggleInterface('functions', False)
            self.toggleInterface('input', True)
            self.toggleInterface('response', True)
            self.response_label.text = self.message['func_c_hint']
    
    def on_click_say(self):
        if (self.state == 'ENTER_INPUT'):
            self.state = "WAIT_OUTPUT"
            self.response_label.text = self.message['calculate']
            self.toggleInterface('input', False)
            self.toggleInterface('ask', True)
            self.ask_label.text = self.text_input.text
            self.text_input.text = ''

            self.robot_image.anim_delay = 0.04
            self.robot_image.anim_loop = 0

            if (self.current_func == 'a'):
                if (self.current_option == '1'):
                    mode = 'sherlock'
                elif (self.current_option == '2'):
                    mode = 'shakespeare'
                elif (self.current_option == '3'):
                    mode = 'mys_island'

                thread_func_a = Thread(
                    target=self.func_a_concurrent,
                    args=(self.ask_label.text, mode, ))
                thread_func_a.start()
                
            elif (self.current_func == 'b'):
                thread_func_b = Thread(
                    target=self.func_b_concurrent,
                    args=(self.ask_label.text, ))
                thread_func_b.start()

            elif (self.current_func == 'c'):
                thread_func_c = Thread(
                    target=self.func_c_concurrent,
                    args=(self.ask_label.text, ))
                thread_func_c.start()

    def func_a_concurrent(self, text_str, mode):
        self.output_label.text = wrap_text_length(
            gen_story(mode, text_str), 50)
        self.state = "OUTPUT_SHOWN_CONFIRM"
        self.toggleInterface('response', False)
        self.toggleInterface('output_confirm', True)
        self.robot_image.anim_loop = 1

    def func_b_concurrent(self, text_str):
        emotions = identify_emotion(text_str)
        print(emotions)
        text = ''
        if len(emotions) == 1:
            text = self.message['func_b_result'] + emotions[0] + '.'
        elif len(emotions) == 2:
            text = self.message['func_b_result'] + emotions[0] +\
                ' and ' + emotions[1] + '.'
        self.output_label.text = wrap_text_length(text, 50)

        self.state = "OUTPUT_SHOWN_CONFIRM"
        self.toggleInterface('response', False)
        self.toggleInterface('output_confirm', True)
        self.robot_image.anim_loop = 1

    def func_c_concurrent(self, text_str):
        g1, g2, g3 = guess_diary(text_str, use355M=False, iteration=4)
        self.diary_guess_cache = [g1, g2, g3]
        text = wrap_text_length(self.diary_guess_cache[0], 50)
        self.output_label.text = self.message['func_c_guess_1'] + '\n' + text
        self.diary_guess_cache = self.diary_guess_cache[1:]

        self.state = "OUTPUT_SHOWN_CHOOSE"
        self.toggleInterface('response', False)
        self.toggleInterface('output_choose', True)
        self.robot_image.anim_loop = 1
        
    def on_click_confirm(self):
        if (self.state == 'OUTPUT_SHOWN_CONFIRM' or self.state == 'OUTPUT_SHOWN_CHOOSE'):
            self.state = 'WAIT_SERVE'
            self.toggleInterface('ask', False)
            self.toggleInterface('output_confirm', False)
    
    def on_click_yes(self):
        if (self.state == 'OUTPUT_SHOWN_CHOOSE'):
            self.toggleInterface('output_choose', False)
            self.output_label.text = self.message['func_c_robot_win']
            self.toggleInterface('output_confirm', True)
        
    def on_click_no(self):
        if (self.state == 'OUTPUT_SHOWN_CHOOSE'):
            guess_time = 4 - len(self.diary_guess_cache)
            if guess_time <= 3:
                text = wrap_text_length(self.diary_guess_cache[0], 50)
                self.output_label.text = self.message['func_c_guess_' + str(guess_time)] +\
                     '\n' + text
                self.diary_guess_cache = self.diary_guess_cache[1:]

            else:
                self.toggleInterface('output_choose', False)
                self.output_label.text = self.message['func_c_robot_final_say']
                self.toggleInterface('output_confirm', True)

    def on_click_home(self):
        if (self.state == 'WAIT_SERVE'):
            pass
        elif (self.state == 'HINT_SHOWN'):
            self.toggleInterface('response', False)
        elif (self.state == 'SELECT_FUNC'):
            self.toggleInterface('functions', False)
        elif (self.state == 'SELECT_OPTION'):
            self.toggleInterface('options', False)
            self.toggleInterface('response', False)
        elif (self.state == 'ENTER_INPUT'):
            self.toggleInterface('input', False)
            self.toggleInterface('response', False)
        elif (self.state == 'OUTPUT_SHOWN_CONFIRM'):
            self.toggleInterface('output_confirm', False)
            self.toggleInterface('ask', False)
        elif (self.state == 'OUTPUT_SHOWN_CHOOSE'):
            self.toggleInterface('output_choose', False)
            self.toggleInterface('ask', False)

        self.state = 'WAIT_SERVE'
        return


    # For debug
    def on_state(self, instance, value):
        print("state beocme: " + value)
コード例 #18
0
class VidhubEditLabelList(BoxLayout):
    app = ObjectProperty(None)
    text = StringProperty()
    label_list_widget = ObjectProperty(None)
    list_items = DictProperty()
    vidhub = ObjectProperty(None, allownone=True)
    vidhub_bound = BooleanProperty(False)
    vidhub_prop_get = StringProperty('')
    vidhub_prop_set = StringProperty('')
    kv_bind_props = [
        'app', 'label_list_widget', 'vidhub_prop_set', 'vidhub_prop_get',
    ]
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        bind_kwargs = {key:self._on_kv_prop_set for key in self.kv_bind_props}
        if not self._check_kv_props():
            self.bind(**bind_kwargs)
        else:
            if self.vidhub is not None:
                if not self.vidhub_bound:
                    self.bind_vidhub()
                self.build_items()
    def _on_kv_prop_set(self, *args, **kwargs):
        if not self._check_kv_props():
            return
        bind_kwargs = {key:self._on_kv_prop_set for key in self.kv_bind_props}
        self.unbind(**bind_kwargs)
        if self.vidhub is not None:
            if not self.vidhub_bound:
                self.bind_vidhub()
            self.build_items()
    def _check_kv_props(self):
        if None in [self.app, self.label_list_widget]:
            return False
        if '' in [self.vidhub_prop_get, self.vidhub_prop_set]:
            return False
        return True
    def on_label_list_widget(self, *args):
        if self.label_list_widget is None:
            return
        self.label_list_widget.bind(minimum_height=self.label_list_widget.setter('height'))
    def on_app(self, *args):
        if self.app is None:
            return
        device = self.app.selected_device
        if device is not None and device.device_type == 'vidhub':
            self.vidhub = device
        self.app.bind(selected_device=self.on_app_selected_device)
    def on_app_selected_device(self, instance, value):
        if self.vidhub is not None:
            self.unbind_vidhub(self.vidhub)
        if value.device_type != 'vidhub':
            value = None
        self.vidhub = value
        if value is not None:
            self.bind_vidhub()
    def bind_vidhub(self):
        self.app.bind_events(self.vidhub, **{self.vidhub_prop_get:self.on_vidhub_labels})
        self.vidhub_bound = True
    def unbind_vidhub(self, vidhub):
        vidhub.unbind(self.on_vidhub_labels)
        self.vidhub_bound = False
    def on_vidhub(self, *args):
        if self.vidhub is None:
            return
        if self._check_kv_props():
            if not self.vidhub_bound:
                self.bind_vidhub()
            self.build_items()
    def build_items(self):
        self.label_list_widget.clear_widgets()
        self.list_items.clear()
        if self.vidhub is None:
            return
        l = getattr(self.vidhub, self.vidhub_prop_get)
        for i, lbl in enumerate(l):
            item = VidhubEditLabelItem(index=i, text=lbl)
            item.bind(text=self.on_label_item_text)
            self.list_items[i] = item
            self.label_list_widget.add_widget(item)
    @mainthread
    def on_vidhub_labels(self, instance, value, **kwargs):
        for i, lbl in enumerate(value):
            item = self.list_items[i]
            item.text = lbl
    def on_label_item_text(self, instance, value):
        l = getattr(self.vidhub, self.vidhub_prop_set)
        l[instance.index] = value
コード例 #19
0
class Label(Widget):
    '''Label class, see module documentation for more information.

    :Events:
        `on_ref_press`
            Fired when the user clicks on a word referenced with a
            ``[ref]`` tag in a text markup.
    '''

    __events__ = ['on_ref_press']

    _font_properties = ('text', 'font_size', 'font_name', 'bold', 'italic',
                        'underline', 'strikethrough', 'font_family', 'color',
                        'disabled_color', 'halign', 'valign', 'padding_x',
                        'padding_y', 'outline_width', 'disabled_outline_color',
                        'outline_color', 'text_size', 'shorten', 'mipmap',
                        'line_height', 'max_lines', 'strip', 'shorten_from',
                        'split_str', 'ellipsis_options', 'unicode_errors',
                        'markup', 'font_hinting', 'font_kerning',
                        'font_blended', 'font_context', 'font_features',
                        'base_direction', 'text_language')

    def __init__(self, **kwargs):
        self._trigger_texture = Clock.create_trigger(self.texture_update, -1)
        super(Label, self).__init__(**kwargs)

        # bind all the property for recreating the texture
        d = Label._font_properties
        fbind = self.fbind
        update = self._trigger_texture_update
        fbind('disabled', update, 'disabled')
        for x in d:
            fbind(x, update, x)

        self._label = None
        self._create_label()

        # force the texture creation
        self._trigger_texture()

    def _create_label(self):
        # create the core label class according to markup value
        if self._label is not None:
            cls = self._label.__class__
        else:
            cls = None
        markup = self.markup
        if (markup and cls is not CoreMarkupLabel) or \
           (not markup and cls is not CoreLabel):
            # markup have change, we need to change our rendering method.
            d = Label._font_properties
            dkw = dict(list(zip(d, [getattr(self, x) for x in d])))
            if markup:
                self._label = CoreMarkupLabel(**dkw)
            else:
                self._label = CoreLabel(**dkw)

    def _trigger_texture_update(self, name=None, source=None, value=None):
        # check if the label core class need to be switch to a new one
        if name == 'markup':
            self._create_label()
        if source:
            if name == 'text':
                self._label.text = value
            elif name == 'text_size':
                self._label.usersize = value
            elif name == 'font_size':
                self._label.options[name] = value
            elif name == 'disabled_color' and self.disabled:
                self._label.options['color'] = value
            elif name == 'disabled_outline_color' and self.disabled:
                self._label.options['outline_color'] = value
            elif name == 'disabled':
                self._label.options['color'] = self.disabled_color if value \
                    else self.color
                self._label.options['outline_color'] = (
                    self.disabled_outline_color
                    if value else self.outline_color)
            else:
                self._label.options[name] = value
        self._trigger_texture()

    def texture_update(self, *largs):
        '''Force texture recreation with the current Label properties.

        After this function call, the :attr:`texture` and :attr:`texture_size`
        will be updated in this order.
        '''
        mrkup = self._label.__class__ is CoreMarkupLabel
        self.texture = None

        if (not self._label.text or (self.halign == 'justify' or self.strip)
                and not self._label.text.strip()):
            self.texture_size = (0, 0)
            self.is_shortened = False
            if mrkup:
                self.refs, self._label._refs = {}, {}
                self.anchors, self._label._anchors = {}, {}
        else:
            if mrkup:
                text = self.text
                # we must strip here, otherwise, if the last line is empty,
                # markup will retain the last empty line since it only strips
                # line by line within markup
                if self.halign == 'justify' or self.strip:
                    text = text.strip()
                self._label.text = ''.join(
                    ('[color=',
                     get_hex_from_color(
                         self.disabled_color if self.disabled else self.color),
                     ']', text, '[/color]'))
                self._label.refresh()
                # force the rendering to get the references
                if self._label.texture:
                    self._label.texture.bind()
                self.refs = self._label.refs
                self.anchors = self._label.anchors
            else:
                self._label.refresh()
            texture = self._label.texture
            if texture is not None:
                self.texture = self._label.texture
                self.texture_size = list(self.texture.size)
            self.is_shortened = self._label.is_shortened

    def on_touch_down(self, touch):
        if super(Label, self).on_touch_down(touch):
            return True
        if not len(self.refs):
            return False
        tx, ty = touch.pos
        tx -= self.center_x - self.texture_size[0] / 2.
        ty -= self.center_y - self.texture_size[1] / 2.
        ty = self.texture_size[1] - ty
        for uid, zones in self.refs.items():
            for zone in zones:
                x, y, w, h = zone
                if x <= tx <= w and y <= ty <= h:
                    self.dispatch('on_ref_press', uid)
                    return True
        return False

    def on_ref_press(self, ref):
        pass

    #
    # Properties
    #

    disabled_color = ListProperty([1, 1, 1, .3])
    '''The color of the text when the widget is disabled, in the (r, g, b, a)
    format.

    .. versionadded:: 1.8.0

    :attr:`disabled_color` is a :class:`~kivy.properties.ListProperty` and
    defaults to [1, 1, 1, .3].
    '''

    text = StringProperty('')
    '''Text of the label.

    Creation of a simple hello world::

        widget = Label(text='Hello world')

    If you want to create the widget with an unicode string, use::

        widget = Label(text=u'My unicode string')

    :attr:`text` is a :class:`~kivy.properties.StringProperty` and defaults to
    ''.
    '''

    text_size = ListProperty([None, None])
    '''By default, the label is not constrained to any bounding box.
    You can set the size constraint of the label with this property.
    The text will autoflow into the constraints. So although the font size
    will not be reduced, the text will be arranged to fit into the box as best
    as possible, with any text still outside the box clipped.

    This sets and clips :attr:`texture_size` to text_size if not None.

    .. versionadded:: 1.0.4

    For example, whatever your current widget size is, if you want the label to
    be created in a box with width=200 and unlimited height::

        Label(text='Very big big line', text_size=(200, None))

    .. note::

        This text_size property is the same as the
        :attr:`~kivy.core.text.Label.usersize` property in the
        :class:`~kivy.core.text.Label` class. (It is named size= in the
        constructor.)

    :attr:`text_size` is a :class:`~kivy.properties.ListProperty` and
    defaults to (None, None), meaning no size restriction by default.
    '''

    base_direction = OptionProperty(
        None,
        options=['ltr', 'rtl', 'weak_rtl', 'weak_ltr', None],
        allownone=True)
    '''Base direction of text, this impacts horizontal alignment when
    :attr:`halign` is `auto` (the default). Available options are: None,
    "ltr" (left to right), "rtl" (right to left) plus "weak_ltr" and
    "weak_rtl".

    .. note::
        This feature requires the Pango text provider.

    .. note::
        Weak modes are currently not implemented in Kivy text layout, and
        have the same effect as setting strong mode.

    .. versionadded:: 1.11.0

    :attr:`base_direction` is an :class:`~kivy.properties.OptionProperty` and
    defaults to None (autodetect RTL if possible, otherwise LTR).
    '''

    text_language = StringProperty(None, allownone=True)
    '''Language of the text, if None Pango will determine it from locale.
    This is an RFC-3066 format language tag (as a string), for example
    "en_US", "zh_CN", "fr" or "ja". This can impact font selection, metrics
    and rendering. For example, the same bytes of text can look different
    for `ur` and `ar` languages, though both use Arabic script.

    .. note::
        This feature requires the Pango text provider.

    .. versionadded:: 1.11.0

    :attr:`text_language` is a :class:`~kivy.properties.StringProperty` and
    defaults to None.
    '''

    font_context = StringProperty(None, allownone=True)
    '''Font context. `None` means the font is used in isolation, so you are
    guaranteed to be drawing with the TTF file resolved by :attr:`font_name`.
    Specifying a value here will load the font file into a named context,
    enabling fallback between all fonts in the same context. If a font
    context is set, you are not guaranteed that rendering will actually use
    the specified TTF file for all glyphs (Pango will pick the one it
    thinks is best).

    If Kivy is linked against a system-wide installation of FontConfig,
    you can load the system fonts by specifying a font context starting
    with the special string `system://`. This will load the system
    fontconfig configuration, and add your application-specific fonts on
    top of it (this imposes a signifficant risk of family name collision,
    Pango may not use your custom font file, but pick one from the system)

    .. note::
        This feature requires the Pango text provider.

    .. versionadded:: 1.11.0

    :attr:`font_context` is a :class:`~kivy.properties.StringProperty` and
    defaults to None.
    '''

    font_family = StringProperty(None, allownone=True)
    '''Font family, this is only applicable when using :attr:`font_context`
    option. The specified font family will be requested, but note that it may
    not be available, or there could be multiple fonts registered with the
    same family. The value can be a family name (string) available in the
    font context (for example a system font in a `system://` context, or a
    custom font file added using :class:`kivy.core.text.FontContextManager`).
    If set to `None`, font selection is controlled by the :attr:`font_name`
    setting.

    .. note::
        If using :attr:`font_name` to reference a custom font file, you
        should leave this as `None`. The family name is managed automatically
        in this case.

    .. note::
        This feature requires the Pango text provider.

    .. versionadded:: 1.11.0

    :attr:`font_family` is a :class:`~kivy.properties.StringProperty` and
    defaults to None.
    '''

    font_name = StringProperty(DEFAULT_FONT)
    '''Filename of the font to use. The path can be absolute or relative.
    Relative paths are resolved by the :func:`~kivy.resources.resource_find`
    function.

    .. warning::

        Depending of your text provider, the font file can be ignored. However,
        you can mostly use this without problems.

        If the font used lacks the glyphs for the particular language/symbols
        you are using, you will see '[]' blank box characters instead of the
        actual glyphs. The solution is to use a font that has the glyphs you
        need to display. For example, to display |unicodechar|, use a font such
        as freesans.ttf that has the glyph.

        .. |unicodechar| image:: images/unicode-char.png

    :attr:`font_name` is a :class:`~kivy.properties.StringProperty` and
    defaults to 'Roboto'. This value is taken
    from :class:`~kivy.config.Config`.
    '''

    font_size = NumericProperty('15sp')
    '''Font size of the text, in pixels.

    :attr:`font_size` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 15sp.
    '''

    font_features = StringProperty()
    '''OpenType font features, in CSS format, this is passed straight
    through to Pango. The effects of requesting a feature depends on loaded
    fonts, library versions, etc. For a complete list of features, see:

    https://en.wikipedia.org/wiki/List_of_typographic_features

    .. note::
        This feature requires the Pango text provider, and Pango library
        v1.38 or later.

    .. versionadded:: 1.11.0

    :attr:`font_features` is a :class:`~kivy.properties.StringProperty` and
    defaults to an empty string.
    '''

    line_height = NumericProperty(1.0)
    '''Line Height for the text. e.g. line_height = 2 will cause the spacing
    between lines to be twice the size.

    :attr:`line_height` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 1.0.

    .. versionadded:: 1.5.0
    '''

    bold = BooleanProperty(False)
    '''Indicates use of the bold version of your font.

    .. note::

        Depending of your font, the bold attribute may have no impact on your
        text rendering.

    :attr:`bold` is a :class:`~kivy.properties.BooleanProperty` and defaults to
    False.
    '''

    italic = BooleanProperty(False)
    '''Indicates use of the italic version of your font.

    .. note::

        Depending of your font, the italic attribute may have no impact on your
        text rendering.

    :attr:`italic` is a :class:`~kivy.properties.BooleanProperty` and defaults
    to False.
    '''

    underline = BooleanProperty(False)
    '''Adds an underline to the text.

    .. note::
        This feature requires the SDL2 text provider.

    .. versionadded:: 1.10.0

    :attr:`underline` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to False.
    '''

    strikethrough = BooleanProperty(False)
    '''Adds a strikethrough line to the text.

    .. note::
        This feature requires the SDL2 text provider.

    .. versionadded:: 1.10.0

    :attr:`strikethrough` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to False.
    '''

    padding_x = NumericProperty(0)
    '''Horizontal padding of the text inside the widget box.

    :attr:`padding_x` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 0.

    .. versionchanged:: 1.9.0
        `padding_x` has been fixed to work as expected.
        In the past, the text was padded by the negative of its values.
    '''

    padding_y = NumericProperty(0)
    '''Vertical padding of the text inside the widget box.

    :attr:`padding_y` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 0.

    .. versionchanged:: 1.9.0
        `padding_y` has been fixed to work as expected.
        In the past, the text was padded by the negative of its values.
    '''

    padding = ReferenceListProperty(padding_x, padding_y)
    '''Padding of the text in the format (padding_x, padding_y)

    :attr:`padding` is a :class:`~kivy.properties.ReferenceListProperty` of
    (:attr:`padding_x`, :attr:`padding_y`) properties.
    '''

    halign = OptionProperty(
        'auto', options=['left', 'center', 'right', 'justify', 'auto'])
    '''Horizontal alignment of the text.

    :attr:`halign` is an :class:`~kivy.properties.OptionProperty` and
    defaults to 'auto'. Available options are : auto, left, center, right and
    justify. Auto will attempt to autodetect horizontal alignment for RTL text
    (Pango only), otherwise it behaves like `left`.

    .. warning::

        This doesn't change the position of the text texture of the Label
        (centered), only the position of the text in this texture. You probably
        want to bind the size of the Label to the :attr:`texture_size` or set a
        :attr:`text_size`.

    .. versionchanged:: 1.10.1
        Added `auto` option

    .. versionchanged:: 1.6.0
        A new option was added to :attr:`halign`, namely `justify`.
    '''

    valign = OptionProperty('bottom',
                            options=['bottom', 'middle', 'center', 'top'])
    '''Vertical alignment of the text.

    :attr:`valign` is an :class:`~kivy.properties.OptionProperty` and defaults
    to 'bottom'. Available options are : `'bottom'`,
    `'middle'` (or `'center'`) and `'top'`.

    .. versionchanged:: 1.10.0
        The `'center'` option has been added as an alias of `'middle'`.

    .. warning::

        This doesn't change the position of the text texture of the Label
        (centered), only the position of the text within this texture. You
        probably want to bind the size of the Label to the :attr:`texture_size`
        or set a :attr:`text_size` to change this behavior.
    '''

    color = ListProperty([1, 1, 1, 1])
    '''Text color, in the format (r, g, b, a).

    :attr:`color` is a :class:`~kivy.properties.ListProperty` and defaults to
    [1, 1, 1, 1].
    '''

    outline_width = NumericProperty(None, allownone=True)
    '''Width in pixels for the outline around the text. No outline will be
    rendered if the value is None.

    .. note::
        This feature requires the SDL2 text provider.

    .. versionadded:: 1.10.0

    :attr:`outline_width` is a :class:`~kivy.properties.NumericProperty` and
    defaults to None.
    '''

    outline_color = ListProperty([0, 0, 0])
    '''The color of the text outline, in the (r, g, b) format.

    .. note::
        This feature requires the SDL2 text provider.

    .. versionadded:: 1.10.0

    :attr:`outline_color` is a :class:`~kivy.properties.ListProperty` and
    defaults to [0, 0, 0].
    '''

    disabled_outline_color = ListProperty([0, 0, 0])
    '''The color of the text outline when the widget is disabled, in the
    (r, g, b) format.

    .. note::
        This feature requires the SDL2 text provider.

    .. versionadded:: 1.10.0

    :attr:`disabled_outline_color` is a :class:`~kivy.properties.ListProperty`
    and defaults to [0, 0, 0].
    '''

    texture = ObjectProperty(None, allownone=True)
    '''Texture object of the text.
    The text is rendered automatically when a property changes. The OpenGL
    texture created in this operation is stored in this property. You can use
    this :attr:`texture` for any graphics elements.

    Depending on the texture creation, the value will be a
    :class:`~kivy.graphics.texture.Texture` or
    :class:`~kivy.graphics.texture.TextureRegion` object.

    .. warning::

        The :attr:`texture` update is scheduled for the next frame. If you need
        the texture immediately after changing a property, you have to call
        the :meth:`texture_update` method before accessing :attr:`texture`::

            l = Label(text='Hello world')
            # l.texture is good
            l.font_size = '50sp'
            # l.texture is not updated yet
            l.texture_update()
            # l.texture is good now.

    :attr:`texture` is an :class:`~kivy.properties.ObjectProperty` and defaults
    to None.
    '''

    texture_size = ListProperty([0, 0])
    '''Texture size of the text. The size is determined by the font size and
    text. If :attr:`text_size` is [None, None], the texture will be the size
    required to fit the text, otherwise it's clipped to fit :attr:`text_size`.

    When :attr:`text_size` is [None, None], one can bind to texture_size
    and rescale it proportionally to fit the size of the label in order to
    make the text fit maximally in the label.

    .. warning::

        The :attr:`texture_size` is set after the :attr:`texture`
        property. If you listen for changes to :attr:`texture`,
        :attr:`texture_size` will not be up-to-date in your callback.
        Bind to :attr:`texture_size` instead.
    '''

    mipmap = BooleanProperty(False)
    '''Indicates whether OpenGL mipmapping is applied to the texture or not.
    Read :ref:`mipmap` for more information.

    .. versionadded:: 1.0.7

    :attr:`mipmap` is a :class:`~kivy.properties.BooleanProperty` and defaults
    to False.
    '''

    shorten = BooleanProperty(False)
    '''
    Indicates whether the label should attempt to shorten its textual contents
    as much as possible if a :attr:`text_size` is given. Setting this to True
    without an appropriately set :attr:`text_size` will lead to unexpected
    results.

    :attr:`shorten_from` and :attr:`split_str` control the direction from
    which the :attr:`text` is split, as well as where in the :attr:`text` we
    are allowed to split.

    :attr:`shorten` is a :class:`~kivy.properties.BooleanProperty` and defaults
    to False.
    '''

    shorten_from = OptionProperty('center',
                                  options=['left', 'center', 'right'])
    '''The side from which we should shorten the text from, can be left,
    right, or center.

    For example, if left, the ellipsis will appear towards the left side and we
    will display as much text starting from the right as possible. Similar to
    :attr:`shorten`, this option only applies when :attr:`text_size` [0] is
    not None, In this case, the string is shortened to fit within the specified
    width.

    .. versionadded:: 1.9.0

    :attr:`shorten_from` is a :class:`~kivy.properties.OptionProperty` and
    defaults to `center`.
    '''

    is_shortened = BooleanProperty(False)
    '''This property indicates if :attr:`text` was rendered with or without
    shortening when :attr:`shorten` is True.

    .. versionadded:: 1.10.0

    :attr:`is_shortened` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to False.
    '''

    split_str = StringProperty('')
    '''The string used to split the :attr:`text` while shortening the string
    when :attr:`shorten` is True.

    For example, if it's a space, the string will be broken into words and as
    many whole words that can fit into a single line will be displayed. If
    :attr:`split_str` is the empty string, `''`, we split on every character
    fitting as much text as possible into the line.

    .. versionadded:: 1.9.0

    :attr:`split_str` is a :class:`~kivy.properties.StringProperty` and
    defaults to `''` (the empty string).
    '''

    ellipsis_options = DictProperty({})
    '''Font options for the ellipsis string('...') used to split the text.

    Accepts a dict as option name with the value. Only applied when
    :attr:`markup` is true and text is shortened. All font options which work
    for :class:`Label` will work for :attr:`ellipsis_options`. Defaults for
    the options not specified are taken from the surronding text.

    .. code-block:: kv

        Label:
            text: 'Some very long line which will be cut'
            markup: True
            shorten: True
            ellipsis_options: {'color':(1,0.5,0.5,1),'underline':True}

    .. versionadded:: 2.0.0

    :attr:`ellipsis_options` is a :class:`~kivy.properties.DictProperty` and
    defaults to `{}` (the empty dict).
    '''

    unicode_errors = OptionProperty('replace',
                                    options=('strict', 'replace', 'ignore'))
    '''How to handle unicode decode errors. Can be `'strict'`, `'replace'` or
    `'ignore'`.

    .. versionadded:: 1.9.0

    :attr:`unicode_errors` is an :class:`~kivy.properties.OptionProperty` and
    defaults to `'replace'`.
    '''

    markup = BooleanProperty(False)
    '''
    .. versionadded:: 1.1.0

    If True, the text will be rendered using the
    :class:`~kivy.core.text.markup.MarkupLabel`: you can change the
    style of the text using tags. Check the
    :doc:`api-kivy.core.text.markup` documentation for more information.

    :attr:`markup` is a :class:`~kivy.properties.BooleanProperty` and defaults
    to False.
    '''

    refs = DictProperty({})
    '''
    .. versionadded:: 1.1.0

    List of ``[ref=xxx]`` markup items in the text with the bounding box of
    all the words contained in a ref, available only after rendering.

    For example, if you wrote::

        Check out my [ref=hello]link[/ref]

    The refs will be set with::

        {'hello': ((64, 0, 78, 16), )}

    The references marked "hello" have a bounding box at (x1, y1, x2, y2).
    These co-ordinates are relative to the top left corner of the text, with
    the y value increasing downwards. You can define multiple refs with the
    same name: each occurrence will be added as another (x1, y1, x2, y2) tuple
    to this list.

    The current Label implementation uses these references if they exist in
    your markup text, automatically doing the collision with the touch and
    dispatching an `on_ref_press` event.

    You can bind a ref event like this::

        def print_it(instance, value):
            print('User click on', value)
        widget = Label(text='Hello [ref=world]World[/ref]', markup=True)
        widget.on_ref_press(print_it)

    .. note::

        This works only with markup text. You need :attr:`markup` set to
        True.
    '''

    anchors = DictProperty({})
    '''
    .. versionadded:: 1.1.0

    Position of all the ``[anchor=xxx]`` markup in the text.
    These co-ordinates are relative to the top left corner of the text, with
    the y value increasing downwards. Anchors names should be unique and only
    the first occurrence of any duplicate anchors will be recorded.


    You can place anchors in your markup text as follows::

        text = """
            [anchor=title1][size=24]This is my Big title.[/size]
            [anchor=content]Hello world
        """

    Then, all the ``[anchor=]`` references will be removed and you'll get all
    the anchor positions in this property (only after rendering)::

        >>> widget = Label(text=text, markup=True)
        >>> widget.texture_update()
        >>> widget.anchors
        {"content": (20, 32), "title1": (20, 16)}

    .. note::

        This works only with markup text. You need :attr:`markup` set to
        True.

    '''

    max_lines = NumericProperty(0)
    '''Maximum number of lines to use, defaults to 0, which means unlimited.
    Please note that :attr:`shorten` take over this property. (with
    shorten, the text is always one line.)

    .. versionadded:: 1.8.0

    :attr:`max_lines` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 0.
    '''

    strip = BooleanProperty(False)
    '''Whether leading and trailing spaces and newlines should be stripped from
    each displayed line. If True, every line will start at the right or left
    edge, depending on :attr:`halign`. If :attr:`halign` is `justify` it is
    implicitly True.

    .. versionadded:: 1.9.0

    :attr:`strip` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to False.
    '''

    font_hinting = OptionProperty('normal',
                                  options=[None, 'normal', 'light', 'mono'],
                                  allownone=True)
    '''What hinting option to use for font rendering.
    Can be one of `'normal'`, `'light'`, `'mono'` or None.

    .. note::
        This feature requires SDL2 or Pango text provider.

    .. versionadded:: 1.10.0

    :attr:`font_hinting` is an :class:`~kivy.properties.OptionProperty` and
    defaults to `'normal'`.
    '''

    font_kerning = BooleanProperty(True)
    '''Whether kerning is enabled for font rendering. You should normally
    only disable this if rendering is broken with a particular font file.

    .. note::
        This feature requires the SDL2 text provider.

    .. versionadded:: 1.10.0

    :attr:`font_kerning` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to True.
    '''

    font_blended = BooleanProperty(True)
    '''Whether blended or solid font rendering should be used.
コード例 #20
0
ファイル: __init__.py プロジェクト: matham/filers2
class PlayersContainerWidget(GridLayout):

    _config_children_ = {'players': 'players'}

    players: List[FilersPlayer] = ListProperty([])

    player_id_mapping: Dict[int, FilersPlayer] = DictProperty({})

    def apply_config_child(self, name, prop, obj, config):
        if prop != 'players':
            apply_config(obj, config)
            return

        while len(config) < len(self.players):
            player = self.players.pop()
            self.remove_widget(player.player_widget)
            player.clean_up()

        while len(config) > len(self.players):
            self.add_player()

        for player, config in zip(self.players, config):
            apply_config(player, config)

        self.recompute_player_id_mapping()

        return True

    def recompute_player_id_mapping(self):
        self.player_id_mapping = {p.player_id: p for p in self.players}

    def start_recording(self, player: FilersPlayer):
        seen = set()
        mapping = self.player_id_mapping

        while player is not None and player not in seen:
            player.recorder.record(player.player)
            seen.add(player)
            player = mapping.get(player.records_with, None)

    def stop_recording(self, player: FilersPlayer):
        seen = set()
        mapping = self.player_id_mapping

        while player is not None and player not in seen:
            player.recorder.stop()
            seen.add(player)
            player = mapping.get(player.records_with, None)

    def add_player(self, player_id=0):
        player = FilersPlayer(player_id=player_id)
        player.create_widgets()

        player.player_widget = widget = PlayerWidget(player=player)
        self.add_widget(widget)
        self.players.append(player)

        self.recompute_player_id_mapping()

    def remove_player(self, player: FilersPlayer):
        self.remove_widget(player.player_widget)
        self.players.remove(player)
        player.clean_up()

        self.recompute_player_id_mapping()

    def clean_up(self):
        for player in self.players:
            player.clean_up()
コード例 #21
0
ファイル: tabs.py プロジェクト: hosler/KivyMD
class MDTabbedPanel(TabbedPanelBase):
    """ A tab panel that is implemented by delegating all tabs
        to a ScreenManager.
    """
    # If tabs should fill space
    tab_width_mode = OptionProperty('stacked', options=['stacked', 'fixed'])

    # Where the tabs go
    tab_orientation = OptionProperty('top',
                                     options=['top'
                                              ])  # ,'left','bottom','right'])

    # How tabs are displayed
    tab_display_mode = OptionProperty('text', options=['text',
                                                       'icons'])  # ,'both'])
    _tab_display_height = DictProperty({
        'text': dp(46),
        'icons': dp(46),
        'both': dp(72)
    })

    # Tab background color (leave empty for theme color)
    tab_color = ListProperty([])

    # Tab text color in normal state (leave empty for theme color)
    tab_text_color = ListProperty([])

    # Tab text color in active state (leave empty for theme color)
    tab_text_color_active = ListProperty([])

    # Tab indicator color  (leave empty for theme color)
    tab_indicator_color = ListProperty([])

    # Tab bar bottom border color (leave empty for theme color)
    tab_border_color = ListProperty([])

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.index = 0
        self._refresh_tabs()

    def on_tab_width_mode(self, *args):
        self._refresh_tabs()

    def on_tab_display_mode(self, *args):
        self._refresh_tabs()

    def _refresh_tabs(self):
        """ Refresh all tabs """
        # if fixed width, use a box layout
        if not self.ids:
            return
        tab_bar = self.ids.tab_bar
        tab_bar.clear_widgets()
        tab_manager = self.ids.tab_manager
        for tab in tab_manager.screens:
            tab_header = MDTabHeader(tab=tab,
                                     panel=self,
                                     height=tab_bar.height,
                                     _tab_width_mode=self.tab_width_mode)
            tab_bar.add_widget(tab_header)

    def add_widget(self, widget, **kwargs):
        """ Add tabs to the screen or the layout.
        :param widget: The widget to add.
        """
        if isinstance(widget, MDTab):
            self.index += 1
            if self.index == 1:
                self.previous_tab = widget
            widget.index = self.index
            widget.parent_widget = self
            self.ids.tab_manager.add_widget(widget)
            self._refresh_tabs()
        else:
            super().add_widget(widget)

    def remove_widget(self, widget):
        """ Remove tabs from the screen or the layout.
        :param widget: The widget to remove.
        """
        self.index -= 1
        if isinstance(widget, MDTab):
            self.ids.tab_manager.remove_widget(widget)
            self._refresh_tabs()
        else:
            super().remove_widget(widget)
コード例 #22
0
class VideoPlayer(GridLayout):
    '''VideoPlayer class. See module documentation for more information.
    '''

    source = StringProperty('')
    '''Source of the video to read.

    :attr:`source` is a :class:`~kivy.properties.StringProperty` and
    defaults to ''.

    .. versionchanged:: 1.4.0
    '''

    thumbnail = StringProperty('')
    '''Thumbnail of the video to show. If None, VideoPlayer will try to find
    the thumbnail from the :attr:`source` + '.png'.

    :attr:`thumbnail` a :class:`~kivy.properties.StringProperty` and defaults
    to ''.

    .. versionchanged:: 1.4.0
    '''

    duration = NumericProperty(-1)
    '''Duration of the video. The duration defaults to -1 and is set to the
    real duration when the video is loaded.

    :attr:`duration` is a :class:`~kivy.properties.NumericProperty` and
    defaults to -1.
    '''

    position = NumericProperty(0)
    '''Position of the video between 0 and :attr:`duration`. The position
    defaults to -1 and is set to the real position when the video is loaded.

    :attr:`position` is a :class:`~kivy.properties.NumericProperty` and
    defaults to -1.
    '''

    volume = NumericProperty(1.0)
    '''Volume of the video in the range 0-1. 1 means full volume and 0 means
    mute.

    :attr:`volume` is a :class:`~kivy.properties.NumericProperty` and defaults
    to 1.
    '''

    state = OptionProperty('stop', options=('play', 'pause', 'stop'))
    '''String, indicates whether to play, pause, or stop the video::

        # start playing the video at creation
        video = VideoPlayer(source='movie.mkv', state='play')

        # create the video, and start later
        video = VideoPlayer(source='movie.mkv')
        # and later
        video.state = 'play'

    :attr:`state` is an :class:`~kivy.properties.OptionProperty` and defaults
    to 'play'.
    '''

    play = BooleanProperty(False)
    '''
    .. deprecated:: 1.4.0
        Use :attr:`state` instead.

    Boolean, indicates whether the video is playing or not. You can start/stop
    the video by setting this property::

        # start playing the video at creation
        video = VideoPlayer(source='movie.mkv', play=True)

        # create the video, and start later
        video = VideoPlayer(source='movie.mkv')
        # and later
        video.play = True

    :attr:`play` is a :class:`~kivy.properties.BooleanProperty` and defaults
    to False.
    '''

    image_overlay_play = StringProperty(
        'atlas://data/images/defaulttheme/player-play-overlay')
    '''Image filename used to show a "play" overlay when the video has not yet
    started.

    :attr:`image_overlay_play` is a
    :class:`~kivy.properties.StringProperty` and
    defaults to 'atlas://data/images/defaulttheme/player-play-overlay'.

    '''

    image_loading = StringProperty('data/images/image-loading.gif')
    '''Image filename used when the video is loading.

    :attr:`image_loading` is a :class:`~kivy.properties.StringProperty` and
    defaults to 'data/images/image-loading.gif'.
    '''

    image_play = StringProperty(
        'atlas://data/images/defaulttheme/media-playback-start')
    '''Image filename used for the "Play" button.

    :attr:`image_play` is a :class:`~kivy.properties.StringProperty` and
    defaults to 'atlas://data/images/defaulttheme/media-playback-start'.
    '''

    image_stop = StringProperty(
        'atlas://data/images/defaulttheme/media-playback-stop')
    '''Image filename used for the "Stop" button.

    :attr:`image_stop` is a :class:`~kivy.properties.StringProperty` and
    defaults to 'atlas://data/images/defaulttheme/media-playback-stop'.
    '''

    image_pause = StringProperty(
        'atlas://data/images/defaulttheme/media-playback-pause')
    '''Image filename used for the "Pause" button.

    :attr:`image_pause` is a :class:`~kivy.properties.StringProperty` and
    defaults to 'atlas://data/images/defaulttheme/media-playback-pause'.
    '''

    image_volumehigh = StringProperty(
        'atlas://data/images/defaulttheme/audio-volume-high')
    '''Image filename used for the volume icon when the volume is high.

    :attr:`image_volumehigh` is a :class:`~kivy.properties.StringProperty` and
    defaults to 'atlas://data/images/defaulttheme/audio-volume-high'.
    '''

    image_volumemedium = StringProperty(
        'atlas://data/images/defaulttheme/audio-volume-medium')
    '''Image filename used for the volume icon when the volume is medium.

    :attr:`image_volumemedium` is a :class:`~kivy.properties.StringProperty`
    and defaults to 'atlas://data/images/defaulttheme/audio-volume-medium'.
    '''

    image_volumelow = StringProperty(
        'atlas://data/images/defaulttheme/audio-volume-low')
    '''Image filename used for the volume icon when the volume is low.

    :attr:`image_volumelow` is a :class:`~kivy.properties.StringProperty`
    and defaults to 'atlas://data/images/defaulttheme/audio-volume-low'.
    '''

    image_volumemuted = StringProperty(
        'atlas://data/images/defaulttheme/audio-volume-muted')
    '''Image filename used for the volume icon when the volume is muted.

    :attr:`image_volumemuted` is a :class:`~kivy.properties.StringProperty`
    and defaults to 'atlas://data/images/defaulttheme/audio-volume-muted'.
    '''

    annotations = StringProperty('')
    '''If set, it will be used for reading annotations box.

    :attr:`annotations` is a :class:`~kivy.properties.StringProperty`
    and defaults to ''.
    '''

    fullscreen = BooleanProperty(False)
    '''Switch to fullscreen view. This should be used with care. When
    activated, the widget will remove itself from its parent, remove all
    children from the window and will add itself to it. When fullscreen is
    unset, all the previous children are restored and the widget is restored to
    its previous parent.

    .. warning::

        The re-add operation doesn't care about the index position of it's
        children within the parent.

    :attr:`fullscreen` is a :class:`~kivy.properties.BooleanProperty`
    and defaults to False.
    '''

    allow_fullscreen = BooleanProperty(True)
    '''By default, you can double-tap on the video to make it fullscreen. Set
    this property to False to prevent this behavior.

    :attr:`allow_fullscreen` is a :class:`~kivy.properties.BooleanProperty`
    defaults to True.
    '''

    options = DictProperty({})
    '''Optional parameters can be passed to a :class:`~kivy.uix.video.Video`
    instance with this property.

    :attr:`options` a :class:`~kivy.properties.DictProperty` and
    defaults to {}.
    '''

    # internals
    container = ObjectProperty(None)

    def __init__(self, **kwargs):
        self._video = None
        self._image = None
        self._annotations = ''
        self._annotations_labels = []
        super(VideoPlayer, self).__init__(**kwargs)
        self._load_thumbnail()
        self._load_annotations()

        if self.source:
            self._trigger_video_load()

    def _trigger_video_load(self, *largs):
        Clock.unschedule(self._do_video_load)
        Clock.schedule_once(self._do_video_load, -1)

    def on_source(self, instance, value):
        # we got a value, try to see if we have an image for it
        self._load_thumbnail()
        self._load_annotations()
        if self._video is not None:
            self._video.unload()
            self._video = None
        if value:
            self._trigger_video_load()

    def _load_thumbnail(self):
        if not self.container:
            return
        self.container.clear_widgets()
        # get the source, remove extension, and use png
        thumbnail = self.thumbnail
        if not thumbnail:
            filename = self.source.rsplit('.', 1)
            thumbnail = filename[0] + '.png'
        self._image = VideoPlayerPreview(source=thumbnail, video=self)
        self.container.add_widget(self._image)

    def _load_annotations(self):
        if not self.container:
            return
        self._annotations_labels = []
        annotations = self.annotations
        if not annotations:
            filename = self.source.rsplit('.', 1)
            annotations = filename[0] + '.jsa'
        if exists(annotations):
            with open(annotations, 'r') as fd:
                self._annotations = load(fd)
        if self._annotations:
            for ann in self._annotations:
                self._annotations_labels.append(
                    VideoPlayerAnnotation(annotation=ann))

    def on_state(self, instance, value):
        if self._video is not None:
            self._video.state = value

    def _set_state(self, instance, value):
        self.state = value

    def _do_video_load(self, *largs):
        self._video = Video(source=self.source,
                            state=self.state,
                            volume=self.volume,
                            pos_hint={
                                'x': 0,
                                'y': 0
                            },
                            **self.options)
        self._video.bind(texture=self._play_started,
                         duration=self.setter('duration'),
                         position=self.setter('position'),
                         volume=self.setter('volume'),
                         state=self._set_state)

    def on_play(self, instance, value):
        value = 'play' if value else 'stop'
        return self.on_state(instance, value)

    def on_volume(self, instance, value):
        if not self._video:
            return
        self._video.volume = value

    def on_position(self, instance, value):
        labels = self._annotations_labels
        if not labels:
            return
        for label in labels:
            start = label.start
            duration = label.duration
            if start > value or (start + duration) < value:
                if label.parent:
                    label.parent.remove_widget(label)
            elif label.parent is None:
                self.container.add_widget(label)

    def seek(self, percent):
        '''Change the position to a percentage of the duration. Percentage must
        be a value between 0-1.

        .. warning::

            Calling seek() before video is loaded has no effect.
        '''
        if not self._video:
            return
        self._video.seek(percent)

    def _play_started(self, instance, value):
        self.container.clear_widgets()
        self.container.add_widget(self._video)

    def on_touch_down(self, touch):
        if not self.collide_point(*touch.pos):
            return False
        if touch.is_double_tap and self.allow_fullscreen:
            self.fullscreen = not self.fullscreen
            return True
        return super(VideoPlayer, self).on_touch_down(touch)

    def on_fullscreen(self, instance, value):
        window = self.get_parent_window()
        if not window:
            Logger.warning('VideoPlayer: Cannot switch to fullscreen, '
                           'window not found.')
            if value:
                self.fullscreen = False
            return
        if not self.parent:
            Logger.warning('VideoPlayer: Cannot switch to fullscreen, '
                           'no parent.')
            if value:
                self.fullscreen = False
            return

        if value:
            self._fullscreen_state = state = {
                'parent': self.parent,
                'pos': self.pos,
                'size': self.size,
                'pos_hint': self.pos_hint,
                'size_hint': self.size_hint,
                'window_children': window.children[:]
            }

            # remove all window children
            for child in window.children[:]:
                window.remove_widget(child)

            # put the video in fullscreen
            if state['parent'] is not window:
                state['parent'].remove_widget(self)
            window.add_widget(self)

            # ensure the video widget is in 0, 0, and the size will be
            # reajusted
            self.pos = (0, 0)
            self.size = (100, 100)
            self.pos_hint = {}
            self.size_hint = (1, 1)
        else:
            state = self._fullscreen_state
            window.remove_widget(self)
            for child in state['window_children']:
                window.add_widget(child)
            self.pos_hint = state['pos_hint']
            self.size_hint = state['size_hint']
            self.pos = state['pos']
            self.size = state['size']
            if state['parent'] is not window:
                state['parent'].add_widget(self)
コード例 #23
0
ファイル: atlas.py プロジェクト: ire-and-curses/kivy
class Atlas(EventDispatcher):
    '''Manage texture atlas. See module documentation for more information.
    '''

    textures = DictProperty({})
    '''List of available textures within the atlas.

    :data:`textures` is a :class:`~kivy.properties.DictProperty`, default to {}
    '''

    def _get_filename(self):
        return self._filename

    filename = AliasProperty(_get_filename, None)
    '''Filename of the current Atlas

    :data:`filename` is a :class:`~kivy.properties.AliasProperty`, default to
    None
    '''

    def __init__(self, filename):
        self._filename = filename
        super(Atlas, self).__init__()
        self._load()

    def __getitem__(self, key):
        return self.textures[key]

    def _load(self):
        # late import to prevent recursive import.
        global CoreImage
        if CoreImage is None:
            from kivy.core.image import Image as CoreImage

        # must be a name finished by .atlas ?
        filename = self._filename
        assert(filename.endswith('.atlas'))
        filename = filename.replace('/', os.sep)

        Logger.debug('Atlas: Load <%s>' % filename)
        with open(filename, 'r') as fd:
            meta = json.load(fd)

        Logger.debug('Atlas: Need to load %d images' % len(meta))
        d = dirname(filename)
        textures = {}
        for subfilename, ids in meta.items():
            subfilename = join(d, subfilename)
            Logger.debug('Atlas: Load <%s>' % subfilename)

            # load the image
            ci = CoreImage(subfilename)

            # for all the uid, load the image, get the region, and put it in our
            # dict.
            for meta_id, meta_coords in ids.items():
                x, y, w, h = meta_coords
                textures[meta_id] = ci.texture.get_region(*meta_coords)

        self.textures = textures

    @staticmethod
    def create(outname, filenames, size, padding=2, use_path=False):
        '''This method can be used to create manually an atlas from a set of
        images.

        :Parameters:
            `outname`: str
                Basename to use for ``.atlas`` creation and ``-<idx>.png``
                associated images.
            `filenames`: list
                List of filename to put in the atlas
            `size`: int
                Size of an atlas image
            `padding`: int, default to 2
                Padding to put around each image.

                Be careful. If you're using a padding < 2, you might get issues
                with border of the images. Because of the OpenGL linearization,
                it might take the pixels of the adjacent image.

                If you're using a padding >= 2, we'll automatically generate a
                "border" of 1px of your image, around the image. If you look at
                the result, don't be scared if the image inside it are not
                exactly the same as yours :).
            `use_path`: bool, if true, the relative path of the source png
                filenames will be included in their atlas ids, rather
                that just the filename. Leading dots and slashes will be excluded
                and all other slashes in the path will be replaced with underscores,
                so for example, if the path and filename is ``../data/tiles/green_grass.png``
                then the id will be ``green_grass`` if use_path is False, and it will be
                ``data_tiles_green_grass`` if use_path is True

            .. versionchanged:: 1.8.0
                Parameter use_path added
        '''
        # Thanks to
        # omnisaurusgames.com/2011/06/texture-atlas-generation-using-python/
        # for its initial implementation.
        try:
            from PIL import Image
        except ImportError:
            Logger.critical('Atlas: Imaging/PIL are missing')
            raise

        size = int(size)

        # open all of the images
        ims = [(f, Image.open(f)) for f in filenames]

        # sort by image area
        ims = sorted(ims, key=lambda im: im[1].size[0] * im[1].size[1],
                reverse=True)

        # free boxes are empty space in our output image set
        # the freebox tuple format is: outidx, x, y, w, h
        freeboxes = [(0, 0, 0, size, size)]
        numoutimages = 1

        # full boxes are areas where we have placed images in the atlas
        # the full box tuple format is: image, outidx, x, y, w, h, filename
        fullboxes = []

        # do the actual atlasing by sticking the largest images we can have into
        # the smallest valid free boxes
        for imageinfo in ims:
            im = imageinfo[1]
            imw, imh = im.size
            imw += padding
            imh += padding
            if imw > size or imh > size:
                Logger.error('Atlas: image %s is larger than the atlas size!' %
                    imageinfo[0])
                return

            inserted = False
            while not inserted:
                for idx, fb in enumerate(freeboxes):
                    # find the smallest free box that will contain this image
                    if fb[3] >= imw and fb[4] >= imh:
                        # we found a valid spot! Remove the current freebox, and
                        # split the leftover space into (up to) two new
                        # freeboxes
                        del freeboxes[idx]
                        if fb[3] > imw:
                            freeboxes.append((
                                fb[0], fb[1] + imw, fb[2],
                                fb[3] - imw, imh))

                        if fb[4] > imh:
                            freeboxes.append((
                                fb[0], fb[1], fb[2] + imh,
                                fb[3], fb[4] - imh))

                        # keep this sorted!
                        freeboxes = sorted(freeboxes,
                                key=lambda fb: fb[3] * fb[4])
                        fullboxes.append((im,
                            fb[0], fb[1] + padding,
                            fb[2] + padding, imw - padding,
                            imh - padding, imageinfo[0]))
                        inserted = True
                        break

                if not inserted:
                    # oh crap - there isn't room in any of our free boxes, so we
                    # have to add a new output image
                    freeboxes.append((numoutimages, 0, 0, size, size))
                    numoutimages += 1

        # now that we've figured out where everything goes, make the output
        # images and blit the source images to the approriate locations
        Logger.info('Atlas: create an {0}x{0} rgba image'.format(size))
        outimages = [Image.new('RGBA', (size, size))
                for i in range(0, int(numoutimages))]
        for fb in fullboxes:
            x, y = fb[2], fb[3]
            out = outimages[fb[1]]
            out.paste(fb[0], (fb[2], fb[3]))
            w, h = fb[0].size
            if padding > 1:
                out.paste(fb[0].crop((0, 0, w, 1)), (x, y - 1))
                out.paste(fb[0].crop((0, h - 1, w, h)), (x, y + h))
                out.paste(fb[0].crop((0, 0, 1, h)), (x - 1, y))
                out.paste(fb[0].crop((w - 1, 0, w, h)), (x + w, y))

        # save the output images
        for idx, outimage in enumerate(outimages):
            outimage.save('%s-%d.png' % (outname, idx))

        # write out an json file that says where everything ended up
        meta = {}
        for fb in fullboxes:
            fn = '%s-%d.png' % (basename(outname), fb[1])
            if fn not in meta:
                d = meta[fn] = {}
            else:
                d = meta[fn]

            # fb[6] contain the filename
            if use_path:
                # use the path with separators replaced by _
                # example '../data/tiles/green_grass.png' becomes 'data_tiles_green_grass'
                uid = splitext(fb[6])[0]
                uid = uid.lstrip('./\\') # remove leading dots and slashes
                uid = uid.replace('/','_').replace('\\', '_') # replace remaining slashes with _
            else:
                # for example, '../data/tiles/green_grass.png'
                # just get only 'green_grass' as the uniq id.
                uid = splitext(basename(fb[6]))[0]

            x, y, w, h = fb[2:6]
            d[uid] = x, size - y - h, w, h

        outfn = '%s.atlas' % outname
        with open(outfn, 'w') as fd:
            json.dump(meta, fd)

        return outfn, meta
コード例 #24
0
class RstDocument(ScrollView):
    '''Base widget used to store an Rst document. See module documentation for
    more information.
    '''
    source = StringProperty(None)
    '''Filename of the RST document.

    :attr:`source` is a :class:`~kivy.properties.StringProperty` and
    defaults to None.
    '''

    source_encoding = StringProperty('utf-8')
    '''Encoding to be used for the :attr:`source` file.

    :attr:`source_encoding` is a :class:`~kivy.properties.StringProperty` and
    defaults to `utf-8`.

    .. Note::
        It is your responsibility to ensure that the value provided is a
        valid codec supported by python.
    '''

    source_error = OptionProperty('strict',
                                  options=('strict', 'ignore', 'replace',
                                           'xmlcharrefreplace',
                                           'backslashreplac'))
    '''Error handling to be used while encoding the :attr:`source` file.

    :attr:`source_error` is an :class:`~kivy.properties.OptionProperty` and
    defaults to `strict`. Can be one of 'strict', 'ignore', 'replace',
    'xmlcharrefreplace' or 'backslashreplac'.
    '''

    text = StringProperty(None)
    '''RST markup text of the document.

    :attr:`text` is a :class:`~kivy.properties.StringProperty` and defaults to
    None.
    '''

    document_root = StringProperty(None)
    '''Root path where :doc: will search for rst documents. If no path is
    given, it will use the directory of the first loaded source file.

    :attr:`document_root` is a :class:`~kivy.properties.StringProperty` and
    defaults to None.
    '''

    base_font_size = NumericProperty(31)
    '''Font size for the biggest title, 31 by default. All other font sizes are
    derived from this.

    .. versionadded:: 1.8.0
    '''

    show_errors = BooleanProperty(False)
    '''Indicate whether RST parsers errors should be shown on the screen
    or not.

    :attr:`show_errors` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to False.
    '''
    def _get_bgc(self):
        return get_color_from_hex(self.colors.background)

    def _set_bgc(self, value):
        self.colors.background = get_hex_from_color(value)[1:]

    background_color = AliasProperty(_get_bgc, _set_bgc, bind=('colors', ))
    '''Specifies the background_color to be used for the RstDocument.

    .. versionadded:: 1.8.0

    :attr:`background_color` is an :class:`~kivy.properties.AliasProperty`
    for colors['background'].
    '''

    colors = DictProperty({
        'background': 'e5e6e9ff',
        'link': 'ce5c00ff',
        'paragraph': '202020ff',
        'title': '204a87ff',
        'bullet': '000000ff'
    })
    '''Dictionary of all the colors used in the RST rendering.

    .. warning::

        This dictionary is needs special handling. You also need to call
        :meth:`RstDocument.render` if you change them after loading.

    :attr:`colors` is a :class:`~kivy.properties.DictProperty`.
    '''

    title = StringProperty('')
    '''Title of the current document.

    :attr:`title` is a :class:`~kivy.properties.StringProperty` and defaults to
    ''. It is read-only.
    '''

    toctrees = DictProperty({})
    '''Toctree of all loaded or preloaded documents. This dictionary is filled
    when a rst document is explicitly loaded or where :meth:`preload` has been
    called.

    If the document has no filename, e.g. when the document is loaded from a
    text file, the key will be ''.

    :attr:`toctrees` is a :class:`~kivy.properties.DictProperty` and defaults
    to {}.
    '''

    underline_color = StringProperty('204a9699')
    '''underline color of the titles, expressed in html color notation

    :attr:`underline_color` is a
    :class:`~kivy.properties.StringProperty` and defaults to '204a9699'.

    .. versionadded: 1.9.0
    '''

    # internals.
    content = ObjectProperty(None)
    scatter = ObjectProperty(None)
    anchors_widgets = ListProperty([])
    refs_assoc = DictProperty({})

    def __init__(self, **kwargs):
        self._trigger_load = Clock.create_trigger(self._load_from_text, -1)
        self._parser = rst.Parser()
        self._settings = frontend.OptionParser(
            components=(rst.Parser, )).get_default_values()
        super(RstDocument, self).__init__(**kwargs)

    def on_source(self, instance, value):
        if not value:
            return
        if self.document_root is None:
            # set the documentation root to the directory name of the
            # first tile
            self.document_root = abspath(dirname(value))
        self._load_from_source()

    def on_text(self, instance, value):
        self._trigger_load()

    def render(self):
        '''Force document rendering.
        '''
        self._load_from_text()

    def resolve_path(self, filename):
        '''Get the path for this filename. If the filename doesn't exist,
        it returns the document_root + filename.
        '''
        if exists(filename):
            return filename
        return join(self.document_root, filename)

    def preload(self, filename, encoding='utf-8', errors='strict'):
        '''Preload a rst file to get its toctree and its title.

        The result will be stored in :attr:`toctrees` with the ``filename`` as
        key.
        '''

        with open(filename, 'rb') as fd:
            text = fd.read().decode(encoding, errors)
        # parse the source
        document = utils.new_document('Document', self._settings)
        self._parser.parse(text, document)
        # fill the current document node
        visitor = _ToctreeVisitor(document)
        document.walkabout(visitor)
        self.toctrees[filename] = visitor.toctree
        return text

    def _load_from_source(self):
        filename = self.resolve_path(self.source)
        self.text = self.preload(filename, self.source_encoding,
                                 self.source_error)

    def _load_from_text(self, *largs):
        try:
            # clear the current widgets
            self.content.clear_widgets()
            self.anchors_widgets = []
            self.refs_assoc = {}

            # parse the source
            document = utils.new_document('Document', self._settings)
            text = self.text
            if PY2 and type(text) is str:
                text = text.decode('utf-8')
            self._parser.parse(text, document)

            # fill the current document node
            visitor = _Visitor(self, document)
            document.walkabout(visitor)

            self.title = visitor.title or 'No title'
        except:
            Logger.exception('Rst: error while loading text')

    def on_ref_press(self, node, ref):
        self.goto(ref)

    def goto(self, ref, *largs):
        '''Scroll to the reference. If it's not found, nothing will be done.

        For this text::

            .. _myref:

            This is something I always wanted.

        You can do::

            from kivy.clock import Clock
            from functools import partial

            doc = RstDocument(...)
            Clock.schedule_once(partial(doc.goto, 'myref'), 0.1)

        .. note::

            It is preferable to delay the call of the goto if you just loaded
            the document because the layout might not be finished or the
            size of the RstDocument has not yet been determined. In
            either case, the calculation of the scrolling would be
            wrong.

            You can, however, do a direct call if the document is already
            loaded.

        .. versionadded:: 1.3.0
        '''
        # check if it's a file ?
        if ref.endswith('.rst'):
            # whether it's a valid or invalid file, let source deal with it
            self.source = ref
            return

        # get the association
        ref = self.refs_assoc.get(ref, ref)

        # search into all the nodes containing anchors
        ax = ay = None
        for node in self.anchors_widgets:
            if ref in node.anchors:
                ax, ay = node.anchors[ref]
                break

        # not found, stop here
        if ax is None:
            return

        # found, calculate the real coordinate

        # get the anchor coordinate inside widget space
        ax += node.x
        ay = node.top - ay
        # ay += node.y

        # what's the current coordinate for us?
        sx, sy = self.scatter.x, self.scatter.top
        # ax, ay = self.scatter.to_parent(ax, ay)

        ay -= self.height

        dx, dy = self.convert_distance_to_scroll(0, ay)
        dy = max(0, min(1, dy))
        Animation(scroll_y=dy, d=.25, t='in_out_expo').start(self)

    def add_anchors(self, node):
        self.anchors_widgets.append(node)
コード例 #25
0
class HoveringBehavior(EventDispatcher):
    hovering = BooleanProperty(False)
    hovering_attrs = DictProperty()
    anim_kw = DictProperty()
    _orig_attrs = {}
    _last_pos = (0, 0)

    def __init__(self, **kw):
        self.register_event_type('on_enter')
        self.register_event_type('on_leave')

        super().__init__(**kw)

        self.bind_hovering()

    def bind_hovering(self, *args):
        Window.bind(mouse_pos=self.on_mouse_pos)

    def unbind_hovering(self, *args):
        Window.unbind(mouse_pos=self.on_mouse_pos)

    def on_mouse_pos(self, __, pos):
        self._last_pos = pos

        if not self.get_root_window():
            return

        self.hovering = self.collide_point(*self.to_widget(*pos))

    def refresh_hovering(self):
        self.on_mouse_pos(None, self._last_pos)

    def on_hovering(self, __, hovering):
        self.dispatch('on_enter' if hovering else 'on_leave')

    def update_orig_attrs(self, *args):
        self._orig_attrs = {
            key: getattr(self, key)
            for key in self.hovering_attrs.keys()
        }

    def on_enter(self):
        if getattr(self, 'disabled', False):
            return

        if not self._orig_attrs:
            self.update_orig_attrs()

        try:
            self.on_leave_anim.stop(self)
        except AttributeError:
            pass

        self.on_enter_anim = Animation(**self.hovering_attrs, **self.anim_kw)
        self.on_enter_anim.start(self)

    def on_leave(self):
        try:
            self.on_enter_anim.stop(self)
        except AttributeError:
            pass

        self.on_leave_anim = Animation(**self._orig_attrs, **self.anim_kw)
        self.on_leave_anim.start(self)

    def on_hovering_attrs(self, *args):
        self.on_hovering(self, self.hovering)
コード例 #26
0
class CircularTimePicker(BoxLayout, ThemableBehavior):
    """Widget that makes use of :class:`CircularHourPicker` and
    :class:`CircularMinutePicker` to create a user-friendly, animated
    time picker like the one seen on Android.

    See module documentation for more details.
    """

    primary_dark = ListProperty([1, 1, 1])

    hours = NumericProperty(0)
    """The hours, in military format (0-23).

    :attr:`hours` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 0 (12am).
    """

    minutes = NumericProperty(0)
    """The minutes.

    :attr:`minutes` is a :class:`~kivy.properties.NumericProperty` and
    defaults to 0.
    """

    time_list = ReferenceListProperty(hours, minutes)
    """Packs :attr:`hours` and :attr:`minutes` in a list for convenience.

    :attr:`time_list` is a :class:`~kivy.properties.ReferenceListProperty`.
    """

    # military = BooleanProperty(False)
    time_format = StringProperty(
        "[color={hours_color}][ref=hours]{hours}[/ref][/color][color={primary_dark}][ref=colon]:[/ref][/color]\
[color={minutes_color}][ref=minutes]{minutes:02d}[/ref][/color]")
    """String that will be formatted with the time and shown in the time label.
    Can be anything supported by :meth:`str.format`. Make sure you don't
    remove the refs. See the default for the arguments passed to format.
    :attr:`time_format` is a :class:`~kivy.properties.StringProperty` and
    defaults to "[color={hours_color}][ref=hours]{hours}[/ref][/color]:[color={minutes_color}][ref=minutes]\
        {minutes:02d}[/ref][/color]".
    """

    ampm_format = StringProperty("[color={am_color}][ref=am]AM[/ref][/color]\n"
                                 "[color={pm_color}][ref=pm]PM[/ref][/color]")
    """String that will be formatted and shown in the AM/PM label.
    Can be anything supported by :meth:`str.format`. Make sure you don't
    remove the refs. See the default for the arguments passed to format.

    :attr:`ampm_format` is a :class:`~kivy.properties.StringProperty` and
    defaults to "[color={am_color}][ref=am]AM[/ref][/color]\n
                 [color={pm_color}][ref=pm]PM[/ref][/color]".
    """

    picker = OptionProperty("hours", options=("minutes", "hours"))
    """Currently shown time picker. Can be one of "minutes", "hours".

    :attr:`picker` is a :class:`~kivy.properties.OptionProperty` and
    defaults to "hours".
    """

    # selector_color = ListProperty([.337, .439, .490])
    selector_color = ListProperty([0, 0, 0])
    """Color of the number selector and of the highlighted text. RGB.

    :attr:`selector_color` is a :class:`~kivy.properties.ListProperty` and
    defaults to [.337, .439, .490] (material green).
    """

    color = ListProperty([1, 1, 1])
    """Color of the number labels and of the center dot. RGB.

    :attr:`color` is a :class:`~kivy.properties.ListProperty` and
    defaults to [1, 1, 1] (white).
    """

    selector_alpha = BoundedNumericProperty(.3, min=0, max=1)
    """Alpha value for the transparent parts of the selector.

    :attr:`selector_alpha`
    is a :class:`~kivy.properties.BoundedNumericProperty` and
    defaults to 0.3 (min=0, max=1).
    """

    _am = BooleanProperty(True)
    _h_picker = ObjectProperty(None)
    _m_picker = ObjectProperty(None)
    _bound = DictProperty({})

    def _get_time(self):
        try:
            return datetime.time(*self.time_list)
        except ValueError:
            self.time_list = [self.hours, 0]
            return datetime.time(*self.time_list)

    def set_time(self, dt):
        if dt.hour >= 12:
            dt.strftime("%I:%M")
            self._am = False
        self.time_list = [dt.hour, dt.minute]

    time = AliasProperty(_get_time, set_time, bind=("time_list", ))
    """Selected time as a datetime.time object.

    :attr:`time` is an :class:`~kivy.properties.AliasProperty`.
    """

    def _get_picker(self):
        if self.picker == "hours":
            return self._h_picker
        return self._m_picker

    _picker = AliasProperty(_get_picker, None)

    def _get_time_text(self):
        hc = rgb_to_hex(0, 0, 0) if self.picker == "hours" \
            else rgb_to_hex(*self.primary_dark)
        mc = rgb_to_hex(0, 0, 0) if self.picker == "minutes" \
            else rgb_to_hex(*self.primary_dark)
        h = self.hours == 0 and 12 or self.hours <= 12 and \
            self.hours or self.hours - 12
        m = self.minutes
        primary_dark = rgb_to_hex(*self.primary_dark)
        return self.time_format.format(hours_color=hc,
                                       minutes_color=mc,
                                       hours=h,
                                       minutes=m,
                                       primary_dark=primary_dark)

    time_text = AliasProperty(_get_time_text,
                              None,
                              bind=("hours", "minutes", "time_format",
                                    "picker"))

    def _get_ampm_text(self, *args):
        amc = rgb_to_hex(0, 0, 0) if self._am \
            else rgb_to_hex(*self.primary_dark)
        pmc = rgb_to_hex(0, 0, 0) if not self._am \
            else rgb_to_hex(*self.primary_dark)
        return self.ampm_format.format(am_color=amc, pm_color=pmc)

    ampm_text = AliasProperty(_get_ampm_text,
                              None,
                              bind=("hours", "ampm_format", "_am"))

    def __init__(self, **kw):
        super(CircularTimePicker, self).__init__(**kw)
        self.selector_color = self.theme_cls.primary_color[0], \
                              self.theme_cls.primary_color[1], \
                              self.theme_cls.primary_color[2]
        self.color = self.theme_cls.text_color
        self.primary_dark = self.theme_cls.primary_dark[0] / 2,\
                            self.theme_cls.primary_dark[1] / 2, \
                            self.theme_cls.primary_dark[2] / 2
        self.on_ampm()
        if self.hours >= 12:
            self._am = False
        self.bind(time_list=self.on_time_list,
                  picker=self._switch_picker,
                  _am=self.on_ampm,
                  primary_dark=self._get_ampm_text)
        self._h_picker = CircularHourPicker()
        self.h_picker_touch = False
        self._m_picker = CircularMinutePicker()
        self.animating = False
        Clock.schedule_once(self.on_selected)
        Clock.schedule_once(self.on_time_list)
        Clock.schedule_once(self._init_later)
        Clock.schedule_once(lambda *a: self._switch_picker(noanim=True))

    def _init_later(self, *args):
        self.ids.timelabel.bind(on_ref_press=self.on_ref_press)
        self.ids.ampmlabel.bind(on_ref_press=self.on_ref_press)

    def on_ref_press(self, ign, ref):
        if not self.animating:
            if ref == "hours":
                self.picker = "hours"
            elif ref == "minutes":
                self.picker = "minutes"
        if ref == "am":
            self._am = True
        elif ref == "pm":
            self._am = False

    def on_selected(self, *a):
        if not self._picker:
            return
        if self.picker == "hours":
            hours = self._picker.selected if self._am \
                else self._picker.selected + 12
            if hours == 24 and not self._am:
                hours = 12
            elif hours == 12 and self._am:
                hours = 0
            self.hours = hours
        elif self.picker == "minutes":
            self.minutes = self._picker.selected

    def on_time_list(self, *a):
        if not self._picker:
            return
        self._h_picker.selected = self.hours == 0 and 12 or self._am and \
                                  self.hours or self.hours - 12
        self._m_picker.selected = self.minutes
        self.on_selected()

    def on_ampm(self, *a):
        if self._am:
            self.hours = self.hours if self.hours < 12 else self.hours - 12
        else:
            self.hours = self.hours if self.hours >= 12 else self.hours + 12

    def is_animating(self, *args):
        self.animating = True

    def is_not_animating(self, *args):
        self.animating = False

    def on_touch_down(self, touch):
        if not self._h_picker.collide_point(*touch.pos):
            self.h_picker_touch = False
        else:
            self.h_picker_touch = True
        super(CircularTimePicker, self).on_touch_down(touch)

    def on_touch_up(self, touch):
        try:
            if not self.h_picker_touch:
                return
            if not self.animating:
                if touch.grab_current is not self:
                    if self.picker == "hours":
                        self.picker = "minutes"
        except AttributeError:
            pass
        super(CircularTimePicker, self).on_touch_up(touch)

    def _switch_picker(self, *a, **kw):
        noanim = "noanim" in kw
        if noanim:
            noanim = kw["noanim"]

        try:
            container = self.ids.picker_container
        except (AttributeError, NameError):
            Clock.schedule_once(lambda *a: self._switch_picker(noanim=noanim))

        if self.picker == "hours":
            picker = self._h_picker
            prevpicker = self._m_picker
        elif self.picker == "minutes":
            picker = self._m_picker
            prevpicker = self._h_picker

        if len(self._bound) > 0:
            prevpicker.unbind(selected=self.on_selected)
            self.unbind(**self._bound)
        picker.bind(selected=self.on_selected)
        self._bound = {
            "selector_color": picker.setter("selector_color"),
            "color": picker.setter("color"),
            "selector_alpha": picker.setter("selector_alpha")
        }
        self.bind(**self._bound)

        if len(container._bound) > 0:
            container.unbind(**container._bound)
        container._bound = {
            "size": picker.setter("size"),
            "pos": picker.setter("pos")
        }
        container.bind(**container._bound)

        picker.pos = container.pos
        picker.size = container.size
        picker.selector_color = self.selector_color
        picker.color = self.color
        picker.selector_alpha = self.selector_alpha
        if noanim:
            if prevpicker in container.children:
                container.remove_widget(prevpicker)
            if picker.parent:
                picker.parent.remove_widget(picker)
            container.add_widget(picker)
        else:
            self.is_animating()
            if prevpicker in container.children:
                anim = Animation(scale=1.5, d=.5, t="in_back") & Animation(
                    opacity=0, d=.5, t="in_cubic")
                anim.start(prevpicker)
                Clock.schedule_once(
                    lambda *y: container.remove_widget(prevpicker), .5)  # .31)
            picker.scale = 1.5
            picker.opacity = 0
            if picker.parent:
                picker.parent.remove_widget(picker)
            container.add_widget(picker)
            anim = Animation(scale=1, d=.5, t="out_back") & Animation(
                opacity=1, d=.5, t="out_cubic")
            anim.bind(on_complete=self.is_not_animating)
            Clock.schedule_once(lambda *y: anim.start(picker), .3)
コード例 #27
0
ファイル: dictadapter.py プロジェクト: ach5910/KivyApp
class DictAdapter(ListAdapter):
    '''A :class:`~kivy.adapters.dictadapter.DictAdapter` is an adapter around a
    python dictionary of records. It extends the list-like capabilities of
    the :class:`~kivy.adapters.listadapter.ListAdapter`.
    '''

    sorted_keys = ListProperty([])
    '''The sorted_keys list property contains a list of hashable objects (can
    be strings) that will be used directly if no args_converter function is
    provided. If there is an args_converter, the record received from a
    lookup of the data, using keys from sorted_keys, will be passed
    to it for instantiation of list item view class instances.

    :attr:`sorted_keys` is a :class:`~kivy.properties.ListProperty` and
    defaults to [].
    '''

    data = DictProperty(None)
    '''A dict that indexes records by keys that are equivalent to the keys in
    sorted_keys, or they are a superset of the keys in sorted_keys.

    The values can be strings, class instances, dicts, etc.

    :attr:`data` is a :class:`~kivy.properties.DictProperty` and defaults
    to None.
    '''
    @deprecated
    def __init__(self, **kwargs):
        if 'sorted_keys' in kwargs:
            if type(kwargs['sorted_keys']) not in (tuple, list):
                msg = 'DictAdapter: sorted_keys must be tuple or list'
                raise Exception(msg)
        else:
            self.sorted_keys = sorted(kwargs['data'].keys())

        super(DictAdapter, self).__init__(**kwargs)

        self.fbind('sorted_keys', self.initialize_sorted_keys)

    def bind_triggers_to_view(self, func):
        self.bind(sorted_keys=func)
        self.bind(data=func)

    # self.data is paramount to self.sorted_keys. If sorted_keys is reset to
    # mismatch data, force a reset of sorted_keys to data.keys(). So, in order
    # to do a complete reset of data and sorted_keys, data must be reset
    # first, followed by a reset of sorted_keys, if needed.
    def initialize_sorted_keys(self, *args, **kwargs):
        stale_sorted_keys = False
        for key in self.sorted_keys:
            if key not in self.data:
                stale_sorted_keys = True
                break
        else:
            if kwargs.get('new_data'):
                if len(self.sorted_keys) != len(self.data):
                    stale_sorted_keys = True
        if stale_sorted_keys:
            self.sorted_keys = sorted(self.data.keys())
        self.delete_cache()
        self.initialize_selection()

    # Override ListAdapter.update_for_new_data().
    def update_for_new_data(self, *args):
        self.initialize_sorted_keys(new_data=True)

    # Note: this is not len(self.data).
    def get_count(self):
        return len(self.sorted_keys)

    def get_data_item(self, index):
        if index < 0 or index >= len(self.sorted_keys):
            return None
        return self.data[self.sorted_keys[index]]

    # [TODO] Also make methods for scroll_to_sel_start, scroll_to_sel_end,
    #        scroll_to_sel_middle.

    def trim_left_of_sel(self, *args):
        '''Cut list items with indices in sorted_keys that are less than the
        index of the first selected item, if there is a selection.

        sorted_keys will be updated by update_for_new_data().
        '''
        if len(self.selection) > 0:
            selected_keys = [sel.text for sel in self.selection]
            first_sel_index = self.sorted_keys.index(selected_keys[0])
            desired_keys = self.sorted_keys[first_sel_index:]
            self.data = dict([(key, self.data[key]) for key in desired_keys])

    def trim_right_of_sel(self, *args):
        '''Cut list items with indices in sorted_keys that are greater than
        the index of the last selected item, if there is a selection.

        sorted_keys will be updated by update_for_new_data().
        '''
        if len(self.selection) > 0:
            selected_keys = [sel.text for sel in self.selection]
            last_sel_index = self.sorted_keys.index(selected_keys[-1])
            desired_keys = self.sorted_keys[:last_sel_index + 1]
            self.data = dict([(key, self.data[key]) for key in desired_keys])

    def trim_to_sel(self, *args):
        '''Cut list items with indices in sorted_keys that are les than or
        greater than the index of the last selected item, if there is a
        selection. This preserves intervening list items within the selected
        range.

        sorted_keys will be updated by update_for_new_data().
        '''
        if len(self.selection) > 0:
            selected_keys = [sel.text for sel in self.selection]
            first_sel_index = self.sorted_keys.index(selected_keys[0])
            last_sel_index = self.sorted_keys.index(selected_keys[-1])
            desired_keys = self.sorted_keys[first_sel_index:last_sel_index + 1]
            self.data = dict([(key, self.data[key]) for key in desired_keys])

    def cut_to_sel(self, *args):
        '''Same as trim_to_sel, but intervening list items within the selected
        range are also cut, leaving only list items that are selected.

        sorted_keys will be updated by update_for_new_data().
        '''
        if len(self.selection) > 0:
            selected_keys = [sel.text for sel in self.selection]
            self.data = dict([(key, self.data[key]) for key in selected_keys])
コード例 #28
0
 class DictWidget(Label):
     button = DictProperty({'button': None},
                           rebind=True,
                           allownone=True)
コード例 #29
0
class PathFinder(EventDispatcher):
    best_path = DictProperty()
    tiles = ListProperty()
    departure_tile = ObjectProperty(allownone=True)
    arrival_tile = ObjectProperty(allownone=True)
    diagonals = True
    level = NumericProperty()

    def _special_copy(self, dictn):
        """Because Kivy doesn't accept deepcopy.

        Special use case do NOT use for recursive for recursive copy operation.
        """

        new_dict = dict()

        new_dict = {
            copy.copy(key): copy.copy(value)
            for key, value in dictn.items()
        }

        return new_dict

    @mainthread
    def on_arrival_tile(self, *_):
        if not self.arrival_tile:
            return

        self.best_weight = self.xmap * self.ymap * 100
        current_path = {'path': [], 'weight': 0}
        self.best_path = self.pathfinder(
            self.departure_tile,
            self.arrival_tile,
            current_path,
        )
        print([tile for tile in self.best_path['path']])
        print(self.best_path['weight'])

    def pathfinder(self, tile, dst_tile, current_path, g_dist=0):
        current_path['path'].append(tile.tile_pos)
        tile.get_h_dist(dst_tile)
        h_dist = tile.distance_to_arrival
        total_weight = g_dist + h_dist
        #optional
        tile.f = round(total_weight, 2)
        tile.h = round(h_dist, 2)
        tile.g = round(g_dist, 2)
        tile.color = total_weight / 100, h_dist / 100, g_dist, 1
        ###
        current_path['weight'] += total_weight
        accessible_tiles = self._list_accessible_tiles(tile, current_path,
                                                       g_dist, dst_tile)
        current_path = self._get_better_accessible(accessible_tiles,
                                                   current_path)

        if not accessible_tiles or current_path['weight'] > self.best_weight:
            return None

        elif dst_tile in accessible_tiles:
            self.best_weight = current_path['weight']
            current_path['path'].append(dst_tile.tile_pos)
            return current_path

        paths = list()

        b_path = None

        for a_tile in accessible_tiles:
            copied_cpath = self._special_copy(current_path)
            b_path = self.pathfinder(
                a_tile,
                dst_tile,
                copied_cpath,
                g_dist + tile._distance_from_tile(a_tile),
            )
            if b_path is not None:
                break

        return b_path

    def _keep_best_path(self, paths):
        if not paths:
            return None

        lighter = paths[0]

        for path in paths:
            if path['weight'] < lighter['weight']:
                lighter = path

        return lighter

    def _get_better_accessible(self, accessible, current_path):
        already_in_path = []

        for tile in accessible:
            if tile.tile_pos in current_path['path']:
                already_in_path.append(tile)

        already_in_path.sort(key=lambda tile: tile.g)
        for tile in accessible:
            if tile.tile_pos in current_path['path']:
                accessible.remove(tile)

        if already_in_path:
            already_in_path.pop(0).tile_pos

        for tile in already_in_path:
            current_path['path'].remove(tile.tile_pos)

        return current_path

    def _list_accessible_tiles(self, tile, current_path, g_dist, dst):
        accessible = []
        tx, ty = tile.tile_pos

        top_tile = self.get_tile_from_coordinate(tx, ty - 1)
        bottom_tile = self.get_tile_from_coordinate(tx, ty + 1)
        left_tile = self.get_tile_from_coordinate(tx - 1, ty)
        right_tile = self.get_tile_from_coordinate(tx + 1, ty)

        accessible = [top_tile, bottom_tile, left_tile, right_tile]
        accessible = [
            atile for atile in accessible
            if atile is not None and self._is_tile_wakable(atile)
        ]
        accessible_tuples = list(zip(accessible, [1] * len(accessible)))

        if not self.diagonals:
            sorted_list = self._sort_accessible(
                [(atile, g) for atile, g in accessible_tuples], g_dist, dst,
                current_path)
            if not self._is_tiles_wakable_in_list(sorted_list):
                return None
            return sorted_list

        if top_tile in accessible or right_tile in accessible:
            dtile = self.get_tile_from_coordinate(tx + 1, ty - 1)
            if dtile is not None:
                accessible_tuples.append((dtile, math.sqrt(2)))

        if top_tile in accessible or left_tile in accessible:
            dtile = self.get_tile_from_coordinate(tx - 1, ty - 1)
            if dtile is not None:
                accessible_tuples.append((dtile, math.sqrt(2)))

        if bottom_tile in accessible or right_tile in accessible:
            dtile = self.get_tile_from_coordinate(tx + 1, ty + 1)
            if dtile is not None:
                accessible_tuples.append((dtile, math.sqrt(2)))

        if bottom_tile in accessible or left_tile in accessible:
            dtile = self.get_tile_from_coordinate(tx - 1, ty + 1)
            if dtile is not None:
                accessible_tuples.append((dtile, math.sqrt(2)))

        sorted_list = self._sort_accessible([(atile, g)
                                             for atile, g in accessible_tuples
                                             if self._is_tile_wakable(atile)],
                                            g_dist, dst, current_path)
        return sorted_list

    def _sort_accessible(self, accessible, g_dist, dst, current_path):
        acces = list()
        for tile, g in accessible:
            if tile.tile_pos not in current_path['path']:
                tile.g = round(g_dist + g, 2)
            acces.append(tile)

        sort = sorted(
            acces, key=lambda src: self._distance_from_tile(src, dst) + src.g)

        return sort

    def _distance_from_tiles(self, src_tile, *tiles):
        return [(tile, self._distance_from_tile(src_tile, tile))
                for tile in tiles]

    def _distance_from_tile(self, tile1, tile2):
        tx1, ty1 = tile1.tile_pos
        tx2, ty2 = tile2.tile_pos

        return math.sqrt((tx2 - tx1)**2 + (ty2 - ty1)**2)

    def _is_tiles_wakable_in_list(self, tiles):
        for tile in tiles:
            if not self._is_tile_wakable(tile):
                return False
        return True

    def _is_tile_wakable(self, tile):
        if tile.level > self.level:
            return False
        return True

    def get_tile_from_coordinate(self, tx, ty):
        if not (0 <= ty < len(self.tiles)) or not (0 <= tx < len(
                self.tiles[0])):
            return None

        return self.tiles[ty][tx]
コード例 #30
0
ファイル: settings.py プロジェクト: youprofit/kivy
class ContentPanel(ScrollView):
    '''A class for displaying settings panels. It displays a single
    settings panel at a time, taking up the full size and shape of the
    ContentPanel. It is used by :class:`InterfaceWithSidebar` and
    :class:`InterfaceWithSpinner` to display settings.

    '''

    panels = DictProperty({})
    '''(internal) Stores a dictionary mapping settings panels to their uids.

    :attr:`panels` is a :class:`~kivy.properties.DictProperty` and
    defaults to {}.

    '''

    container = ObjectProperty()
    '''(internal) A reference to the GridLayout that contains the
    settings panel.

    :attr:`container` is an :class:`~kivy.properties.ObjectProperty` and
    defaults to None.

    '''

    current_panel = ObjectProperty(None)
    '''(internal) A reference to the current settings panel.

    :attr:`current_panel` is an :class:`~kivy.properties.ObjectProperty` and
    defaults to None.

    '''

    current_uid = NumericProperty(0)
    '''(internal) A reference to the uid of the current settings panel.

    :attr:`current_uid` is a
    :class:`~kivy.properties.NumericProperty` and defaults to 0.

    '''
    def add_panel(self, panel, name, uid):
        '''This method is used by Settings to add new panels for possible
        display. Any replacement for ContentPanel *must* implement
        this method.

        :param panel: A :class:`SettingsPanel`. It should be stored
                      and displayed when requested.

        :param name: The name of the panel as a string. It
                     may be used to represent the panel.

        :param uid: A unique int identifying the panel. It should be
                    stored and used to identify panels when switching.

        '''
        self.panels[uid] = panel
        if not self.current_uid:
            self.current_uid = uid

    def on_current_uid(self, *args):
        '''The uid of the currently displayed panel. Changing this will
        automatically change the displayed panel.

        :param uid: A panel uid. It should be used to retrieve and
                    display a settings panel that has previously been
                    added with :meth:`add_panel`.
        '''
        uid = self.current_uid
        if uid in self.panels:
            if self.current_panel is not None:
                self.remove_widget(self.current_panel)
            new_panel = self.panels[uid]
            self.add_widget(new_panel)
            self.current_panel = new_panel
            return True
        return False  # New uid doesn't exist

    def add_widget(self, widget):
        if self.container is None:
            super(ContentPanel, self).add_widget(widget)
        else:
            self.container.add_widget(widget)

    def remove_widget(self, widget):
        self.container.remove_widget(widget)