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()
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.
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)
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
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('&', '&') 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])
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()
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)
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()
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)
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])
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")
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
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 """
class DictWidgetFalse(Label): button = DictProperty({'button': None}, rebind=False)
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 = {}
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)
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
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.
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()
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)
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)
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
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)
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)
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)
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])
class DictWidget(Label): button = DictProperty({'button': None}, rebind=True, allownone=True)
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]
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)