def test_dictproperty_is_none(): from kivy.properties import DictProperty d1 = DictProperty(None) d1.link(wid, 'd1') assert d1.get(wid) is None d2 = DictProperty({'a': 1, 'b': 2}, allownone=True) d2.link(wid, 'd2') d2.set(wid, None) assert d2.get(wid) is None
def test_dictcheck(self): from kivy.properties import DictProperty a = DictProperty() a.link(wid, 'a') a.link_deps(wid, 'a') self.assertEqual(a.get(wid), {}) a.set(wid, {'foo': 'bar'}) self.assertEqual(a.get(wid), {'foo': 'bar'})
class MainApp(App): ''' The Main app class, This class is called in the beginning. ''' date = StringProperty() ''' Reference to Date. ''' time = StringProperty() ''' Reference to time. ''' ip = StringProperty() ''' reference to current IP. ''' database_content = ListProperty() ''' Reference to database content. ''' images = DictProperty({ 'home': 'images/home.png', 'report': 'images/report.png', 'alert': 'images/alert.png', 'wifi_on': 'images/wifi_on.png', 'wifi_off': 'images/wifi_off.png', 'help': 'images/help.png', 'sai_off': 'images/sai_off.png', 'back': 'images/back.png', 'sai_charged': 'images/sai_charged.png', 'sai_plugged': 'images/sai_plugged.png', 'logout': 'images/logout.png' }) ''' Rerference to all the images in the software. ''' icons = DictProperty({ 'search': 'icons/search.png', 'move': 'icons/move.png', 'up': 'icons/up.png', 'down': 'icons/down.png', 'group': 'icons/group.png', 'business': 'icons/help.png', 'units': 'icons/units.png', 'trash': 'icons/trash.png', 'ok': 'icons/ok.png' }) ''' Rerference to all the icons in the software. ''' def build(self): ''' builds the application. ''' Clock.schedule_interval(self.get_time, 1) Clock.schedule_once(self.get_date, 0) Clock.schedule_once(self.get_ip, 0) Window.maximize() self.get_database_tables() self.title = "Smart Cabinet" self.root = Manager() def get_date(self, *args): ''' Get the date to be displayed in the main screen. ''' now = datetime.datetime.now() date = '{}/{}/{}'.format(now.month, now.day, now.year) self.date = date def get_time(self, *agrs): ''' Get the time to be displayed in the main screen. ''' now = datetime.datetime.now() time = '{}:{}:{}'.format(now.hour, now.minute, now.second) self.time = time def get_ip(self, *args): ''' Get the current ip. ''' import socket self.ip = socket.gethostbyname(socket.gethostname()) def db_content(self): ''' Return the data from the database ''' return self.database_content def refresh_content(self): ''' Refresh the data from the database. ''' self.get_database_tables() self.db_content() def get_database_tables(self): ''' Extracts data from the database. ''' myConnection = psycopg2.connect(host=hostname, user=username, password=password, dbname=database, port=port) self.database_content = get_table_list(myConnection) myConnection.close()
class Story(Box): '''Story widget''' # completed_tasks = ObjectProperty() fullheight = NumericProperty() creation = ListProperty() postnum = NumericProperty() _text = StringProperty() _title = StringProperty() _data = DictProperty() _tasks = ListProperty() def _set_title(self): self._title = self._text.split('\n')[0][:20] def _set_height(self, *dt): par = self.parent if not par: return height = sum([x.height for x in self.children]) self.height = height par.height = par.collect_height(par, 50) def add_completed(self): p = self.ids['popup_completed'] for x in self._tasks: widget = CompletedTask() widget._data = {**x} widget._text = x['taskname'] widget.disabled = True p.add_widget(widget) self.save_data() def save(self, app): ''' Params: app(obj): Link to running app inst; ''' tasklist = app.root.taskholder for x in self.ids['popup_completed'].children: if x.state == 'down' and x._source in tasklist.children: x.disabled = True x.state = 'normal' tasklist.remove_widget(x._source) self._tasks.append(x._source.save_data()) print(self._tasks) def save_data(self): self._data = { 'storytext': self._text.replace('\n', '\\n'), 'postnum': self.postnum, 'creation': self.creation, 'completed_tasks': self._tasks } return self._data def display_tasks(self): holder = self.ids['completed_tasks'] holder.clear_widgets() for x in self._tasks: widget = CompletedTask() widget.text = x['taskname'] holder.add_widget(widget) def refresh(self): default = 100 self._set_title() if not self._data: self.add_completed() self.display_tasks() self.arrange_completed(self.ids['completed_tasks']) Clock.schedule_once(self._set_height, 0.1) return default def arrange_completed(self, instance): # instance = self.ids.completed_tasks height = instance.children[0].height if instance.children else 30 def _set_vals(*dt): total_width = sum([x.width for x in instance.children]) rows = total_width / instance.width r = height * (int(rows) * 2) if rows > 1 else height instance.height = r Clock.schedule_once(_set_vals, 0.1) return height
class GridLayout(Layout): '''Grid layout class. See module documentation for more information. ''' spacing = VariableListProperty([0, 0], length=2) '''Spacing between children: [spacing_horizontal, spacing_vertical]. spacing also accepts a one argument form [spacing]. :data:`spacing` is a :class:`~kivy.properties.VariableListProperty`, default to [0, 0]. ''' padding = VariableListProperty([0, 0, 0, 0]) '''Padding between the layout box and it's children: [padding_left, padding_top, padding_right, padding_bottom]. padding also accepts a two argument form [padding_horizontal, padding_vertical] and a one argument form [padding]. .. versionchanged:: 1.7.0 Replaced NumericProperty with VariableListProperty. :data:`padding` is a :class:`~kivy.properties.VariableListProperty` and defaults to [0, 0, 0, 0]. ''' cols = BoundedNumericProperty(None, min=0, allownone=True) '''Number of columns in the grid. .. versionadded:: 1.0.8 Changed from a NumericProperty to BoundedNumericProperty. You can no longer set this to a negative value. :data:`cols` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' rows = BoundedNumericProperty(None, min=0, allownone=True) '''Number of rows in the grid. .. versionadded:: 1.0.8 Changed from a NumericProperty to a BoundedNumericProperty. You can no longer set this to a negative value. :data:`rows` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' col_default_width = NumericProperty(0) '''Default minimum size to use for a column. .. versionadded:: 1.0.7 :data:`col_default_width` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' row_default_height = NumericProperty(0) '''Default minimum size to use for row. .. versionadded:: 1.0.7 :data:`row_default_height` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' col_force_default = BooleanProperty(False) '''If True, ignore the width and size_hint_x of the child and use the default column width. .. versionadded:: 1.0.7 :data:`col_force_default` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' row_force_default = BooleanProperty(False) '''If True, ignore the height and size_hint_y of the child and use the default row height. .. versionadded:: 1.0.7 :data:`row_force_default` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' cols_minimum = DictProperty({}) '''List of minimum sizes for each column. .. versionadded:: 1.0.7 :data:`cols_minimum` is a :class:`~kivy.properties.DictProperty` and defaults to {}. ''' rows_minimum = DictProperty({}) '''List of minimum sizes for each row. .. versionadded:: 1.0.7 :data:`rows_minimum` is a :class:`~kivy.properties.DictProperty` and defaults to {}. ''' minimum_width = NumericProperty(0) '''Minimum width needed to contain all children. .. versionadded:: 1.0.8 :data:`minimum_width` is a :class:`kivy.properties.NumericProperty` and defaults to 0. ''' minimum_height = NumericProperty(0) '''Minimum height needed to contain all children. .. versionadded:: 1.0.8 :data:`minimum_height` is a :class:`kivy.properties.NumericProperty` and defaults to 0. ''' minimum_size = ReferenceListProperty(minimum_width, minimum_height) '''Minimum size needed to contain all children. .. versionadded:: 1.0.8 :data:`minimum_size` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`minimum_width`, :data:`minimum_height`) properties. ''' def __init__(self, **kwargs): self._cols = self._rows = None super(GridLayout, self).__init__(**kwargs) self.bind(col_default_width=self._trigger_layout, row_default_height=self._trigger_layout, col_force_default=self._trigger_layout, row_force_default=self._trigger_layout, cols=self._trigger_layout, rows=self._trigger_layout, parent=self._trigger_layout, spacing=self._trigger_layout, padding=self._trigger_layout, children=self._trigger_layout, size=self._trigger_layout, pos=self._trigger_layout) def get_max_widgets(self): if self.cols and not self.rows: return None if self.rows and not self.cols: return None if not self.cols and not self.rows: return None return self.rows * self.cols def on_children(self, instance, value): # if that makes impossible to construct things with deffered method, # migrate this test in do_layout, and/or issue a warning. smax = self.get_max_widgets() if smax and len(value) > smax: raise GridLayoutException( 'Too many children in GridLayout. Increase rows/cols!') def update_minimum_size(self, *largs): # the goal here is to calculate the minimum size of every cols/rows # and determine if they have stretch or not current_cols = self.cols current_rows = self.rows children = self.children len_children = len(children) # if no cols or rows are set, we can't calculate minimum size. # the grid must be contrained at least on one side if not current_cols and not current_rows: Logger.warning('%r have no cols or rows set, ' 'layout is not triggered.' % self) return None if current_cols is None: current_cols = int(ceil(len_children / float(current_rows))) elif current_rows is None: current_rows = int(ceil(len_children / float(current_cols))) current_cols = max(1, current_cols) current_rows = max(1, current_rows) cols = [self.col_default_width] * current_cols cols_sh = [None] * current_cols rows = [self.row_default_height] * current_rows rows_sh = [None] * current_rows # update minimum size from the dicts # FIXME index might be outside the bounds ? for index, value in self.cols_minimum.items(): cols[index] = value for index, value in self.rows_minimum.items(): rows[index] = value # calculate minimum size for each columns and rows i = len_children - 1 for row in range(current_rows): for col in range(current_cols): # don't go further is we don't have child left if i < 0: break # get initial information from the child c = children[i] shw = c.size_hint_x shh = c.size_hint_y w = c.width h = c.height # compute minimum size / maximum stretch needed if shw is None: cols[col] = nmax(cols[col], w) else: cols_sh[col] = nmax(cols_sh[col], shw) if shh is None: rows[row] = nmax(rows[row], h) else: rows_sh[row] = nmax(rows_sh[row], shh) # next child i = i - 1 # calculate minimum width/height needed, starting from padding + spacing padding_x = self.padding[0] + self.padding[2] padding_y = self.padding[1] + self.padding[3] spacing_x, spacing_y = self.spacing width = padding_x + spacing_x * (current_cols - 1) height = padding_y + spacing_y * (current_rows - 1) # then add the cell size width += sum(cols) height += sum(rows) # remember for layout self._cols = cols self._rows = rows self._cols_sh = cols_sh self._rows_sh = rows_sh # finally, set the minimum size self.minimum_size = (width, height) def do_layout(self, *largs): self.update_minimum_size() if self._cols is None: return if self.cols is None and self.rows is None: raise GridLayoutException('Need at least cols or rows constraint.') children = self.children len_children = len(children) if len_children == 0: return # speedup padding_left = self.padding[0] padding_top = self.padding[1] spacing_x, spacing_y = self.spacing selfx = self.x selfw = self.width selfh = self.height # resolve size for each column if self.col_force_default: cols = [self.col_default_width] * len(self._cols) for index, value in self.cols_minimum.items(): cols[index] = value else: cols = self._cols[:] cols_sh = self._cols_sh cols_weigth = sum([x for x in cols_sh if x]) strech_w = max(0, selfw - self.minimum_width) for index in range(len(cols)): # if the col don't have strech information, nothing to do col_stretch = cols_sh[index] if col_stretch is None: continue # calculate the column stretch, and take the maximum from # minimum size and the calculated stretch col_width = cols[index] col_width = max(col_width, strech_w * col_stretch / cols_weigth) cols[index] = col_width # same algo for rows if self.row_force_default: rows = [self.row_default_height] * len(self._rows) for index, value in self.rows_minimum.items(): rows[index] = value else: rows = self._rows[:] rows_sh = self._rows_sh rows_weigth = sum([x for x in rows_sh if x]) strech_h = max(0, selfh - self.minimum_height) for index in range(len(rows)): # if the row don't have strech information, nothing to do row_stretch = rows_sh[index] if row_stretch is None: continue # calculate the row stretch, and take the maximum from minimum # size and the calculated stretch row_height = rows[index] row_height = max(row_height, strech_h * row_stretch / rows_weigth) rows[index] = row_height # reposition every child i = len_children - 1 y = self.top - padding_top for row_height in rows: x = selfx + padding_left for col_width in cols: if i < 0: break c = children[i] c.x = x c.y = y - row_height c.width = col_width c.height = row_height i = i - 1 x = x + col_width + spacing_x y -= row_height + spacing_y
class Card(FloatLayout): """A trading card, similar to the kind you use to play games like _Magic: the Gathering_. Its appearance is determined by several properties, the most important being: * ``headline_text``, a string to be shown at the top of the card; may be styled with eg. ``headline_font_name`` or ``headline_color`` * ``art_source``, the path to an image to be displayed below the headline; may be hidden by setting ``show_art`` to ``False`` * ``midline_text``, similar to ``headline_text`` but appearing below the art * ``text``, shown in a box the same size as the art. Styleable like ``headline_text`` and you can customize the box with eg. ``foreground_color`` and ``foreground_source`` * ``footer_text``, like ``headline_text`` but at the bottom :class:`Card` is particularly useful when put in a :class:`DeckLayout`, allowing the user to drag cards in between any number of piles, into particular positions within a particular pile, and so forth. """ dragging = BooleanProperty(False) deck = NumericProperty() idx = NumericProperty() ud = DictProperty({}) collide_x = NumericProperty() collide_y = NumericProperty() collide_pos = ReferenceListProperty(collide_x, collide_y) foreground = ObjectProperty() foreground_source = StringProperty('') foreground_color = ListProperty([1, 1, 1, 1]) foreground_image = ObjectProperty(None, allownone=True) foreground_texture = ObjectProperty(None, allownone=True) background_source = StringProperty('') background_color = ListProperty([.7, .7, .7, 1]) background_image = ObjectProperty(None, allownone=True) background_texture = ObjectProperty(None, allownone=True) outline_color = ListProperty([0, 0, 0, 1]) content_outline_color = ListProperty([0, 0, 0, 0]) foreground_outline_color = ListProperty([0, 0, 0, 1]) art_outline_color = ListProperty([0, 0, 0, 0]) art = ObjectProperty() art_source = StringProperty('') art_color = ListProperty([1, 1, 1, 1]) art_image = ObjectProperty(None, allownone=True) art_texture = ObjectProperty(None, allownone=True) show_art = BooleanProperty(True) headline = ObjectProperty() headline_text = StringProperty('Headline') headline_markup = BooleanProperty(True) headline_font_name = StringProperty('Roboto-Regular') headline_font_size = NumericProperty(18) headline_color = ListProperty([0, 0, 0, 1]) midline = ObjectProperty() midline_text = StringProperty('') midline_markup = BooleanProperty(True) midline_font_name = StringProperty('Roboto-Regular') midline_font_size = NumericProperty(14) midline_color = ListProperty([0, 0, 0, 1]) footer = ObjectProperty() footer_text = StringProperty('') footer_markup = BooleanProperty(True) footer_font_name = StringProperty('Roboto-Regular') footer_font_size = NumericProperty(10) footer_color = ListProperty([0, 0, 0, 1]) text = StringProperty('') text_color = ListProperty([0, 0, 0, 1]) markup = BooleanProperty(True) font_name = StringProperty('Roboto-Regular') font_size = NumericProperty(12) def on_background_source(self, *args): """When I get a new ``background_source``, load it as an :class:`Image` and store that in ``background_image``. """ if self.background_source: self.background_image = Image(source=self.background_source) def on_background_image(self, *args): """When I get a new ``background_image``, store its texture in ``background_texture``. """ if self.background_image is not None: self.background_texture = self.background_image.texture def on_foreground_source(self, *args): """When I get a new ``foreground_source``, load it as an :class:`Image` and store that in ``foreground_image``. """ if self.foreground_source: self.foreground_image = Image(source=self.foreground_source) def on_foreground_image(self, *args): """When I get a new ``foreground_image``, store its texture in my ``foreground_texture``. """ if self.foreground_image is not None: self.foreground_texture = self.foreground_image.texture def on_art_source(self, *args): """When I get a new ``art_source``, load it as an :class:`Image` and store that in ``art_image``. """ if self.art_source: self.art_image = Image(source=self.art_source) def on_art_image(self, *args): """When I get a new ``art_image``, store its texture in ``art_texture``. """ if self.art_image is not None: self.art_texture = self.art_image.texture def on_touch_down(self, touch): """If I'm the first card to collide this touch, grab it, store my metadata in its userdict, and store the relative coords upon me where the collision happened. """ if not self.collide_point(*touch.pos): return if 'card' in touch.ud: return touch.grab(self) self.dragging = True touch.ud['card'] = self touch.ud['idx'] = self.idx touch.ud['deck'] = self.deck touch.ud['layout'] = self.parent self.collide_x = touch.x - self.x self.collide_y = touch.y - self.y def on_touch_move(self, touch): """If I'm being dragged, move so as to be always positioned the same relative to the touch. """ if not self.dragging: touch.ungrab(self) return self.pos = (touch.x - self.collide_x, touch.y - self.collide_y) def on_touch_up(self, touch): """Stop dragging if needed.""" if not self.dragging: return touch.ungrab(self) self.dragging = False def copy(self): """Return a new :class:`Card` just like me.""" d = {} for att in ('deck', 'idx', 'ud', 'foreground_source', 'foreground_color', 'foreground_image', 'foreground_texture', 'background_source', 'background_color', 'background_image', 'background_texture', 'outline_color', 'content_outline_color', 'foreground_outline_color', 'art_outline_color', 'art_source', 'art_color', 'art_image', 'art_texture', 'show_art', 'headline_text', 'headline_markup', 'headline_font_name', 'headline_font_size', 'headline_color', 'midline_text', 'midline_markup', 'midline_font_name', 'midline_font_size', 'midline_color', 'footer_text', 'footer_markup', 'footer_font_name', 'footer_font_size', 'footer_color', 'text', 'text_color', 'markup', 'font_name', 'font_size'): v = getattr(self, att) if v is not None: d[att] = v return Card(**d)
class MDTabbedPanel(ThemableBehavior, BackgroundColorBehavior, BoxLayout): """ 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([]) # List of all the tabs so you can dynamically change them tabs = ListProperty([]) # Current tab name current = StringProperty(None) def __init__(self, **kwargs): super(MDTabbedPanel, self).__init__(**kwargs) self.previous_tab = None self.prev_dir = None 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_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. """ d = {} 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(MDTabbedPanel, self).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(MDTabbedPanel, self).remove_widget(widget)
class ListAdapter(Adapter, EventDispatcher): ''' A base class for adapters interfacing with lists, dictionaries, or other collection type data, adding selection and view creation and management functonality. ''' selection = ListProperty([]) '''The selection list property is the container for selected items. :data:`selection` is a :class:`~kivy.properties.ListProperty`, default to []. ''' selection_mode = OptionProperty('single', options=('none', 'single', 'multiple')) '''Selection modes: * *none*, use the list as a simple list (no select action). This option is here so that selection can be turned off, momentarily or permanently, for an existing list adapter. :class:`~kivy.adapters.listadapter.ListAdapter` is not meant to be used as a primary no-selection list adapter. Use :class:`~kivy.adapters.simplelistadapter.SimpleListAdapter` for that. * *single*, multi-touch/click ignored. single item selecting only * *multiple*, multi-touch / incremental addition to selection allowed; may be limited to a count by selection_limit :data:`selection_mode` is an :class:`~kivy.properties.OptionProperty`, default to 'single'. ''' propagate_selection_to_data = BooleanProperty(False) '''Normally, data items are not selected/deselected, because the data items might not have an is_selected boolean property -- only the item view for a given data item is selected/deselected, as part of the maintained selection list. However, if the data items do have an is_selected property, or if they mix in :class:`~kivy.adapters.models.SelectableDataItem`, the selection machinery can propagate selection to data items. This can be useful for storing selection state in a local database or backend database for maintaining state in game play or other similar needs. It is a convenience function. To propagate selection or not? Consider a shopping list application for shopping for fruits at the market. The app allows selection of fruits to buy for each day of the week, presenting seven lists, one for each day of the week. Each list is loaded with all the available fruits, but the selection for each is a subset. There is only one set of fruit data shared between the lists, so it would not make sense to propagate selection to the data, because selection in any of the seven lists would clobber and mix with that of the others. However, consider a game that uses the same fruits data for selecting fruits available for fruit-tossing. A given round of play could have a full fruits list, with fruits available for tossing shown selected. If the game is saved and rerun, the full fruits list, with selection marked on each item, would be reloaded fine if selection is always propagated to the data. You could accomplish the same functionality by writing code to operate on list selection, but having selection stored on the data might prove convenient in some cases. :data:`propagate_selection_to_data` is a :class:`~kivy.properties.BooleanProperty`, default to False. ''' allow_empty_selection = BooleanProperty(True) '''The allow_empty_selection may be used for cascading selection between several list views, or between a list view and an observing view. Such automatic maintainence of selection is important for all but simple list displays. Set allow_empty_selection False, so that selection is auto-initialized, and always maintained, and so that any observing views may likewise be updated to stay in sync. :data:`allow_empty_selection` is a :class:`~kivy.properties.BooleanProperty`, default to True. ''' selection_limit = NumericProperty(-1) '''When selection_mode is multiple, if selection_limit is non-negative, this number will limit the number of selected items. It can even be 1, which is equivalent to single selection. This is because a program could be programmatically changing selection_limit on the fly, and all possible values should be included. If selection_limit is not set, the default is -1, meaning that no limit will be enforced. :data:`selection_limit` is a :class:`~kivy.properties.NumericProperty`, default to -1 (no limit). ''' cached_views = DictProperty({}) '''View instances for data items are instantiated and managed in the adapter. Here we maintain a dictionary containing the view instances keyed to the indices in the data. This dictionary works as a cache. get_view() only asks for a view from the adapter if one is not already stored for the requested index. :data:`cached_views` is a :class:`~kivy.properties.DictProperty`, default to {}. ''' def __init__(self, **kwargs): super(ListAdapter, self).__init__(**kwargs) self.register_event_type('on_selection_change') self.bind(selection_mode=self.selection_mode_changed, allow_empty_selection=self.check_for_empty_selection, data=self.update_for_new_data) self.update_for_new_data() def delete_cache(self, *args): self.cached_views = {} def get_count(self): return len(self.data) def get_data_item(self, index): if index < 0 or index >= len(self.data): return None return self.data[index] def selection_mode_changed(self, *args): if self.selection_mode == 'none': for selected_view in self.selection: self.deselect_item_view(selected_view) else: self.check_for_empty_selection() def get_view(self, index): if index in self.cached_views: return self.cached_views[index] item_view = self.create_view(index) if item_view: self.cached_views[index] = item_view return item_view def create_view(self, index): '''This method is more complicated than the one in :class:`kivy.adapters.adapter.Adapter` and :class:`kivy.adapters.simplelistadapter.SimpleListAdapter`, because here we create bindings for the data item, and its children back to self.handle_selection(), and do other selection-related tasks to keep item views in sync with the data. ''' item = self.get_data_item(index) if item is None: return None item_args = self.args_converter(index, item) item_args['index'] = index if self.cls: view_instance = self.cls(**item_args) else: view_instance = Builder.template(self.template, **item_args) if self.propagate_selection_to_data: # The data item must be a subclass of SelectableDataItem, or must # have an is_selected boolean or function, so it has is_selected # available. If is_selected is unavailable on the data item, an # exception is raised. # if isinstance(item, SelectableDataItem): if item.is_selected: self.handle_selection(view_instance) elif type(item) == dict and 'is_selected' in item: if item['is_selected']: self.handle_selection(view_instance) elif hasattr(item, 'is_selected'): if (inspect.isfunction(item.is_selected) or inspect.ismethod(item.is_selected)): if item.is_selected(): self.handle_selection(view_instance) else: if item.is_selected: self.handle_selection(view_instance) else: msg = "ListAdapter: unselectable data item for {0}" raise Exception(msg.format(index)) view_instance.bind(on_release=self.handle_selection) for child in view_instance.children: child.bind(on_release=self.handle_selection) return view_instance def on_selection_change(self, *args): '''on_selection_change() is the default handler for the on_selection_change event. ''' pass def handle_selection(self, view, hold_dispatch=False, *args): if view not in self.selection: if self.selection_mode in ['none', 'single'] and \ len(self.selection) > 0: for selected_view in self.selection: self.deselect_item_view(selected_view) if self.selection_mode != 'none': if self.selection_mode == 'multiple': if self.allow_empty_selection: # If < 0, selection_limit is not active. if self.selection_limit < 0: self.select_item_view(view) else: if len(self.selection) < self.selection_limit: self.select_item_view(view) else: self.select_item_view(view) else: self.select_item_view(view) else: self.deselect_item_view(view) if self.selection_mode != 'none': # If the deselection makes selection empty, the following call # will check allows_empty_selection, and if False, will # select the first item. If view happens to be the first item, # this will be a reselection, and the user will notice no # change, except perhaps a flicker. # self.check_for_empty_selection() if not hold_dispatch: self.dispatch('on_selection_change') def select_data_item(self, item): self.set_data_item_selection(item, True) def deselect_data_item(self, item): self.set_data_item_selection(item, False) def set_data_item_selection(self, item, value): if isinstance(item, SelectableDataItem): item.is_selected = value elif type(item) == dict: item['is_selected'] = value elif hasattr(item, 'is_selected'): if (inspect.isfunction(item.is_selected) or inspect.ismethod(item.is_selected)): item.is_selected() else: item.is_selected = value def select_item_view(self, view): view.select() view.is_selected = True self.selection.append(view) # [TODO] sibling selection for composite items # Needed? Or handled from parent? # (avoid circular, redundant selection) #if hasattr(view, 'parent') and hasattr(view.parent, 'children'): #siblings = [child for child in view.parent.children if child != view] #for sibling in siblings: #if hasattr(sibling, 'select'): #sibling.select() if self.propagate_selection_to_data: data_item = self.get_data_item(view.index) self.select_data_item(data_item) def select_list(self, view_list, extend=True): '''The select call is made for the items in the provided view_list. Arguments: view_list: the list of item views to become the new selection, or to add to the existing selection extend: boolean for whether or not to extend the existing list ''' if not extend: self.selection = [] for view in view_list: self.handle_selection(view, hold_dispatch=True) self.dispatch('on_selection_change') def deselect_item_view(self, view): view.deselect() view.is_selected = False self.selection.remove(view) # [TODO] sibling deselection for composite items # Needed? Or handled from parent? # (avoid circular, redundant selection) #if hasattr(view, 'parent') and hasattr(view.parent, 'children'): #siblings = [child for child in view.parent.children if child != view] #for sibling in siblings: #if hasattr(sibling, 'deselect'): #sibling.deselect() if self.propagate_selection_to_data: item = self.get_data_item(view.index) self.deselect_data_item(item) def deselect_list(self, l): for view in l: self.handle_selection(view, hold_dispatch=True) self.dispatch('on_selection_change') # [TODO] Could easily add select_all() and deselect_all(). def update_for_new_data(self, *args): self.delete_cache() self.initialize_selection() def initialize_selection(self, *args): if len(self.selection) > 0: self.selection = [] self.dispatch('on_selection_change') self.check_for_empty_selection() def check_for_empty_selection(self, *args): if not self.allow_empty_selection: if len(self.selection) == 0: # Select the first item if we have it. v = self.get_view(0) if v is not None: self.handle_selection(v) # [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 selection. ''' if len(self.selection) > 0: first_sel_index = min([sel.index for sel in self.selection]) self.data = self.data[first_sel_index:] 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 selection. ''' if len(self.selection) > 0: last_sel_index = max([sel.index for sel in self.selection]) print 'last_sel_index', last_sel_index self.data = self.data[:last_sel_index + 1] 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 selection. This preserves intervening list items within the selected range. ''' if len(self.selection) > 0: sel_indices = [sel.index for sel in self.selection] first_sel_index = min(sel_indices) last_sel_index = max(sel_indices) self.data = self.data[first_sel_index:last_sel_index + 1] def cut_to_sel(self, *args): '''Same as trim_to_sel, but intervening list items within the selected range are cut also, leaving only list items that are selected. ''' if len(self.selection) > 0: self.data = self.selection
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) self._trigger_populate = Clock.create_trigger(self._spopulate, -1) self._trigger_reset_populate = \ Clock.create_trigger(self._reset_spopulate, -1) self.bind(size=self._trigger_populate, pos=self._trigger_populate, item_strings=self.item_strings_changed, adapter=self._trigger_populate) # 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. self.adapter.bind_triggers_to_view(self._trigger_reset_populate) # 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 MainScreen(Screen): """A master layout that contains one graph and some menus. This contains three elements: a scrollview (containing the graph), a menu, and the time control panel. This class has some support methods for handling interactions with the menu and the character sheet, but if neither of those happen, the scrollview handles touches on its own. """ manager = ObjectProperty() graphboards = DictProperty() gridboards = DictProperty() boardview = ObjectProperty() mainview = ObjectProperty() charmenu = ObjectProperty() statlist = ObjectProperty() statpanel = ObjectProperty() timepanel = ObjectProperty() kv = StringProperty() use_kv = BooleanProperty() play_speed = NumericProperty() playbut = ObjectProperty() portaladdbut = ObjectProperty() portaldirbut = ObjectProperty() dummyplace = ObjectProperty() dummything = ObjectProperty() dummies = ReferenceListProperty(dummyplace, dummything) dialoglayout = ObjectProperty() visible = BooleanProperty() _touch = ObjectProperty(None, allownone=True) rules_per_frame = BoundedNumericProperty(10, min=1) app = ObjectProperty() tmp_block = BooleanProperty(False) def on_mainview(self, *args): if None in (self.statpanel, self.charmenu, self.app) or None in (self.app.character_name, self.charmenu.portaladdbut): Clock.schedule_once(self.on_mainview, 0) return self.boardview = GraphBoardView( scale_min=0.2, scale_max=4.0, size=self.mainview.size, pos=self.mainview.pos, board=self.graphboards[self.app.character_name], adding_portal=self.charmenu.portaladdbut.state == 'down') def update_adding_portal(*args): self.boardview.adding_portal = self.charmenu.portaladdbut.state == 'down' def update_board(*args): self.boardview.board = self.graphboards[self.app.character_name] self.mainview.bind(size=self.boardview.setter('size'), pos=self.boardview.setter('pos')) self.charmenu.portaladdbut.bind(state=update_adding_portal) self.app.bind(character_name=update_board) self.calendar = Agenda(update_mode='present') self.calendar_view = ScrollView(size=self.mainview.size, pos=self.mainview.pos) self.gridview = GridBoardView( scale_min=0.2, scale_max=4.0, size=self.mainview.size, pos=self.mainview.pos, board=self.gridboards[self.app.character_name]) self.mainview.bind( size=self.calendar_view.setter('size'), pos=self.calendar_view.setter('pos'), ) self.mainview.bind(size=self.gridview.setter('size'), pos=self.gridview.setter('pos')) self.calendar_view.add_widget(self.calendar) self.mainview.add_widget(self.boardview) def on_statpanel(self, *args): if not self.app: Clock.schedule_once(self.on_statpanel, 0) return self._update_statlist() self.app.bind(selected_proxy=self._update_statlist, branch=self._update_statlist, turn=self._update_statlist, tick=self._update_statlist) @trigger def _update_statlist(self, *args): if not self.app.selected_proxy: self._update_statlist() return self.app.update_calendar(self.statpanel.statlist, past_turns=0, future_turns=0) def pull_visibility(self, *args): self.visible = self.manager.current == 'main' def on_manager(self, *args): self.pull_visibility() self.manager.bind(current=self.pull_visibility) def on_play_speed(self, *args): """Change the interval at which ``self.play`` is called to match my current ``play_speed``. """ if hasattr(self, '_play_scheduled'): Clock.unschedule(self._play_scheduled) self._play_scheduled = Clock.schedule_interval(self.play, 1.0 / self.play_speed) def remake_display(self, *args): """Remake any affected widgets after a change in my ``kv``. """ Builder.load_string(self.kv) if hasattr(self, '_kv_layout'): self.remove_widget(self._kv_layout) del self._kv_layout self._kv_layout = KvLayout() self.add_widget(self._kv_layout) _trigger_remake_display = trigger(remake_display) def on_touch_down(self, touch): if self.visible: touch.grab(self) for interceptor in (self.timepanel, self.charmenu, self.statpanel, self.dummyplace, self.dummything): if interceptor.collide_point(*touch.pos): interceptor.dispatch('on_touch_down', touch) self.boardview.keep_selection = \ self.gridview.keep_selection = True return True if self.dialoglayout.dispatch('on_touch_down', touch): return True return self.mainview.dispatch('on_touch_down', touch) def on_touch_up(self, touch): if self.timepanel.collide_point(*touch.pos): return self.timepanel.dispatch('on_touch_up', touch) elif self.charmenu.collide_point(*touch.pos): return self.charmenu.dispatch('on_touch_up', touch) elif self.statpanel.collide_point(*touch.pos): return self.statpanel.dispatch('on_touch_up', touch) return self.mainview.dispatch('on_touch_up', touch) def on_dummies(self, *args): """Give the dummies numbers such that, when appended to their names, they give a unique name for the resulting new :class:`graph.Pawn` or :class:`graph.Spot`. """ if not self.app.character: Clock.schedule_once(self.on_dummies, 0) return def renum_dummy(dummy, *args): dummy.num = dummynum(self.app.character, dummy.prefix) + 1 for dummy in self.dummies: if dummy is None or hasattr(dummy, '_numbered'): continue if dummy == self.dummything: self.app.pawncfg.bind(imgpaths=self._propagate_thing_paths) if dummy == self.dummyplace: self.app.spotcfg.bind(imgpaths=self._propagate_place_paths) dummy.num = dummynum(self.app.character, dummy.prefix) + 1 Logger.debug("MainScreen: dummy #{}".format(dummy.num)) dummy.bind(prefix=partial(renum_dummy, dummy)) dummy._numbered = True def _propagate_thing_paths(self, *args): # horrible hack self.dummything.paths = self.app.pawncfg.imgpaths def _propagate_place_paths(self, *args): # horrible hack self.dummyplace.paths = self.app.spotcfg.imgpaths def _update_from_time_travel(self, command, branch, turn, tick, result, **kwargs): self._update_from_delta(command, branch, turn, tick, result[-1]) def _update_from_delta(self, cmd, branch, turn, tick, delta, **kwargs): self.app.branch = branch self.app.turn = turn self.app.tick = tick chardelta = delta.get(self.boardview.board.character.name, {}) for unwanted in ('character_rulebook', 'avatar_rulebook', 'character_thing_rulebook', 'character_place_rulebook', 'character_portal_rulebook'): if unwanted in chardelta: del chardelta[unwanted] self.boardview.board.trigger_update_from_delta(chardelta) self.gridview.board.trigger_update_from_delta(chardelta) self.statpanel.statlist.mirror = dict(self.app.selected_proxy) def play(self, *args): """If the 'play' button is pressed, advance a turn. If you want to disable this, set ``engine.universal['block'] = True`` """ if self.playbut.state == 'normal': return self.next_turn() def _update_from_next_turn(self, command, branch, turn, tick, result): todo, deltas = result if isinstance(todo, list): self.dialoglayout.todo = todo self.dialoglayout.idx = 0 self._update_from_delta(command, branch, turn, tick, deltas) self.dialoglayout.advance_dialog() self.app.bind(branch=self.app._push_time, turn=self.app._push_time, tick=self.app._push_time) self.tmp_block = False def next_turn(self, *args): """Advance time by one turn, if it's not blocked. Block time by setting ``engine.universal['block'] = True``""" if self.tmp_block: return eng = self.app.engine dial = self.dialoglayout if eng.universal.get('block'): Logger.info( "MainScreen: next_turn blocked, delete universal['block'] to unblock" ) return if dial.idx < len(dial.todo): Logger.info( "MainScreen: not advancing time while there's a dialog") return self.tmp_block = True self.app.unbind(branch=self.app._push_time, turn=self.app._push_time, tick=self.app._push_time) eng.next_turn(cb=self._update_from_next_turn) def switch_to_calendar(self, *args): self.app.update_calendar(self.calendar) self.mainview.clear_widgets() self.mainview.add_widget(self.calendar_view) def switch_to_boardview(self, *args): self.mainview.clear_widgets() self.app.engine.handle('apply_choices', choices=[self.calendar.get_track()]) self.mainview.add_widget(self.boardview) def toggle_gridview(self, *args): if self.gridview in self.mainview.children: self.mainview.clear_widgets() self.mainview.add_widget(self.boardview) else: self.mainview.clear_widgets() self.mainview.add_widget(self.gridview) def toggle_calendar(self, *args): # TODO decide how to handle switching between >2 view types if self.boardview in self.mainview.children: self.switch_to_calendar() else: self.switch_to_boardview()
class Atlas(EventDispatcher): '''Manage texture atlas. See module documentation for more information. ''' original_textures = ListProperty([]) '''List of original atlas textures (which contain the :attr:`textures`). :attr:`original_textures` is a :class:`~kivy.properties.ListProperty` and defaults to []. .. versionadded:: 1.9.1 ''' textures = DictProperty({}) '''List of available textures within the atlas. :attr:`textures` is a :class:`~kivy.properties.DictProperty` and defaults to {}. ''' def _get_filename(self): return self._filename filename = AliasProperty(_get_filename, None) '''Filename of the current Atlas. :attr:`filename` is an :class:`~kivy.properties.AliasProperty` and defaults 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) atlas_texture = ci.texture self.original_textures.append(atlas_texture) # 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] = atlas_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 an atlas manually from a set of images. :Parameters: `outname`: str Basename to use for ``.atlas`` creation and ``-<idx>.png`` associated images. `filenames`: list List of filenames to put in the atlas. `size`: int or list (width, height) Size of the atlas image. `padding`: int, defaults to 2 Padding to put around each image. Be careful. If you're using a padding < 2, you might have issues with the borders of the images. Because of the OpenGL linearization, it might use the pixels of the adjacent image. If you're using a padding >= 2, we'll automatically generate a "border" of 1px around your image. If you look at the result, don't be scared if the image inside is not exactly the same as yours :). `use_path`: bool, defaults to False If True, the relative path of the source png file names will be included in the atlas ids rather that just in the file names. Leading dots and slashes will be excluded and all other slashes in the path will be replaced with underscores. For example, if `use_path` is False (the default) and the file name is ``../data/tiles/green_grass.png``, the id will be ``green_grass``. If `use_path` is True, it will be ``data_tiles_green_grass``. .. 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 if isinstance(size, (tuple, list)): size_w, size_h = list(map(int, size)) else: size_w = size_h = int(size) # open all of the images ims = list() for f in filenames: fp = open(f, 'rb') im = Image.open(fp) im.load() fp.close() ims.append((f, im)) # 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_w, size_h)] 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_w or imh > size_h: Logger.error( 'Atlas: image %s (%d by %d) is larger than the atlas size!' % (imageinfo[0], imw, imh)) 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_w, size_h)) numoutimages += 1 # now that we've figured out where everything goes, make the output # images and blit the source images to the appropriate locations Logger.info('Atlas: create an {0}x{1} rgba image'.format(size_w, size_h)) outimages = [Image.new('RGBA', (size_w, size_h)) 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] # remove leading dots and slashes uid = uid.lstrip('./\\') # replace remaining slashes with _ uid = uid.replace('/', '_').replace('\\', '_') 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_h - 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', ), cache=True) '''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 Pallet(StackLayout): """Many :class:`SwatchButton`, gathered from an :class:`kivy.atlas.Atlas`.""" atlas = ObjectProperty() """:class:`kivy.atlas.Atlas` object I'll make :class:`SwatchButton` from.""" filename = StringProperty() """Path to an atlas; will construct :class:`kivy.atlas.Atlas` when set""" swatches = DictProperty({}) """:class:`SwatchButton` widgets here, keyed by name of their graphic""" swatch_width = NumericProperty(100) """Width of each and every :class:`SwatchButton` here""" swatch_height = NumericProperty(75) """Height of each and every :class:`SwatchButton` here""" swatch_size = ReferenceListProperty(swatch_width, swatch_height) """Size of each and every :class:`SwatchButton` here""" selection = ListProperty([]) """List of :class:`SwatchButton`s that are selected""" selection_mode = OptionProperty('single', options=['single', 'multiple']) """Whether to allow only a 'single' selected :class:`SwatchButton` (default), or 'multiple'""" def on_selection(self, *args): Logger.debug('Pallet: {} got selection {}'.format( self.filename, self.selection)) def on_filename(self, *args): if not self.filename: return resource = resource_find(self.filename) if not resource: raise ValueError("Couldn't find atlas: {}".format(self.filename)) self.atlas = Atlas(resource) def on_atlas(self, *args): if self.atlas is None: return self.upd_textures() self.atlas.bind(textures=self._trigger_upd_textures) def upd_textures(self, *args): """Create one :class:`SwatchButton` for each texture""" if self.canvas is None: Clock.schedule_once(self.upd_textures, 0) return swatches = self.swatches atlas_textures = self.atlas.textures remove_widget = self.remove_widget add_widget = self.add_widget swatch_size = self.swatch_size for name, swatch in list(swatches.items()): if name not in atlas_textures: remove_widget(swatch) del swatches[name] for (name, tex) in atlas_textures.items(): if name in swatches and swatches[name] != tex: remove_widget(swatches[name]) if name not in swatches or swatches[name] != tex: swatches[name] = SwatchButton(text=name, tex=tex, size_hint=(None, None), size=swatch_size) add_widget(swatches[name]) def _trigger_upd_textures(self, *args): if hasattr(self, '_scheduled_upd_textures'): Clock.unschedule(self._scheduled_upd_textures) self._scheduled_upd_textures = Clock.schedule_once( self._trigger_upd_textures)
class TidesSummary(Screen, BlackHole): tidesurl = "https://www.worldtides.info/api?extremes&lat={lat}&lon={lon}&length=172800&key={key}" timedata = DictProperty(None) next_t = DictProperty(None) prev_t = DictProperty(None) location = DictProperty(None) def __init__(self, **kwargs): # Init data by checking cache then calling API self.location = kwargs["location"] self.key = kwargs["key"] self.language = kwargs["language"] if self.language == "french": locale.setlocale(locale.LC_ALL, 'fr_FR.UTF-8') self.get_data() self.get_time() self.get_next() super(TidesSummary, self).__init__(**kwargs) self.timer = None self.tides_list = self.ids.tides_list self.build_tides_list() def buildURL(self, location): lon = location['coords']['lon'] lat = location['coords']['lat'] return self.tidesurl.format(key=self.key, lon=lon, lat=lat) def get_data(self): self.url_tides = self.buildURL(self.location) #with open('screens/tides/result.json') as data_file: # self.tides = json.load(data_file) self.tides = requests.get(self.url_tides).json() if self.tides == None or not 'status' in self.tides: raise TideException("Unknown error") if self.tides['status'] != 200: if 'error' in self.tides: raise TideException(self.tides['error']) else: raise TideException("Unknown error") return True def get_time(self): """Sets self.timedata to current time.""" n = datetime.now() self.timedata["h"] = n.hour self.timedata["m"] = n.minute self.timedata["s"] = n.second n = datetime.utcnow() if hasattr(self, "next_extreme") and n >= self.next_extreme: self.get_next() def get_next(self): if self.tides == None or self.tides['status'] != 200: self.prev_t = {} self.next_t = {} return False found = False prev = None oldentries = [] for extreme in self.tides['extremes']: date = dateutil.parser.parse(extreme['date']) if date > datetime.now(tz=tz.tzutc()): next = extreme date = date.astimezone(tz.tzlocal()) next["h"] = date.hour next["m"] = date.minute next["s"] = date.second next["type_i18n"] = TYPES_MAP[self.language][next["type"]] self.next_extreme = dateutil.parser.parse( extreme['date']).replace(tzinfo=None) date = dateutil.parser.parse(prev['date']) date = date.astimezone(tz.tzlocal()) prev["h"] = date.hour prev["m"] = date.minute prev["s"] = date.second prev["type_i18n"] = TYPES_MAP[self.language][prev["type"]] self.next_t = next self.prev_t = prev break else: if prev: oldentries.append(prev) prev = extreme # clean up old entries self.tides['extremes'] = [ x for x in self.tides['extremes'] if x not in oldentries ] # fetch new one if our set is small if len(self.tides['extremes']) <= MIN_TIDES: try: self.get_data() except: pass if hasattr(self, "tides_list"): self.build_tides_list() return True def build_tides_list(self): if self.tides == None: return self.tides_list.clear_widgets() w = (len(self.tides['extremes']) - 1) * 150 tl = BoxLayout(orientation="horizontal", size=(w, 60), size_hint=(None, 1), spacing=5) sv = ScrollView(size_hint=(1, 1.1), bar_margin=-5, do_scroll_y=False) sv.add_widget(tl) for tide in self.tides['extremes']: if self.next_t["dt"] < tide["dt"]: uptide = Tide(summary=tide, language=self.language) tl.add_widget(uptide) self.tides_list.add_widget(sv) def update(self, dt): self.get_time() def on_enter(self): # We only need to update the clock every second. self.timer = Clock.schedule_interval(self.update, 1) def on_pre_enter(self): self.get_time() def on_pre_leave(self): # Save resource by unscheduling the updates. Clock.unschedule(self.timer) def is_setup(self): if self.tides: return True return False
class ServerReadingListsScreen(Screen): reading_list_title = StringProperty() page_number = NumericProperty() max_books_page = NumericProperty() dynamic_ids = DictProperty({}) # declare class attribute, dynamic_ids sync_bool = BooleanProperty(False) so = BooleanProperty() new_readinglist = ObjectProperty() def __init__(self, **kwargs): super(ServerReadingListsScreen, self).__init__(**kwargs) self.app = App.get_running_app() self.fetch_data = None self.readinglist_Id = StringProperty() self.readinglist_name = "" # self.bind_to_resize() self.bind(width=self.my_width_callback) self.m_grid = "" self.main_stack = "" self.prev_button = "" self.next_button = "" self.base_url = self.app.base_url self.api_url = self.app.api_url self.api_key = self.app.config.get("General", "api_key") self.list_count = "" self.paginator_obj = ObjectProperty() self.current_page = ObjectProperty() self.list_loaded = BooleanProperty() self.page_number = 1 self.list_loaded = False self.comic_thumb_height = 240 self.comic_thumb_width = 156 self.file_download = True self.num_file_done = 0 self.max_books_page = self.app.max_books_page self.please_wait_dialog = None self.dialog_load_comic_data = None def callback_for_menu_items(self, *args): pass def setup_screen(self): self.api_key = self.app.config.get("General", "api_key") self.api_url = self.app.api_url self.main_stack = self.ids["main_stack"] self.m_grid = self.ids["main_grid"] self.prev_button = self.ids["prev_button"] self.next_button = self.ids["next_button"] def on_pre_enter(self, *args): self.app.show_action_bar() return super().on_pre_enter(*args) def on_leave(self, *args): self.app.list_previous_screens.append(self.name) return super().on_leave(*args) def my_width_callback(self, obj, value): for key, val in self.ids.items(): if key == "main_grid": c = val c.cols = (Window.width - 10) // self.comic_thumb_width def page_turn(self, c_id, new_UserLastPageRead): grid = self.m_grid for child in grid.children: if child.comic_obj.Id == c_id: if new_UserLastPageRead == 0: child.percent_read = 0 else: child.percent_read = round( new_UserLastPageRead / (child.comic_obj.PageCount - 1) * 100) child.page_count_text = f"{child.percent_read}%" def file_sync_update(self, c_id, state): grid = self.m_grid for child in grid.children: if child.comic_obj.Id == c_id: child.has_localfile = state def collect_readinglist_data( self, readinglist_name="", readinglist_Id="", mode="From Server", current_page_num=1, *largs, ): async def collect_readinglist_data(): self.readinglist_name = readinglist_name self.app.set_screen(self.readinglist_name + " Page 1") self.reading_list_title = self.readinglist_name + " Page 1" self.readinglist_Id = readinglist_Id self.page_number = current_page_num self.mode = mode if self.mode == "From Server": self.fetch_data = ComicServerConn() lsit_count_url = "{}/Lists/{}/Comics/".format( self.api_url, readinglist_Id) # self.fetch_data.get_list_count(lsit_count_url,self) self.fetch_data.get_server_data(lsit_count_url, self) elif self.mode == "From DataBase": self.got_db_data() asynckivy.start(collect_readinglist_data()) def get_page(self, instance): page_num = instance.page_num self.app.set_screen(self.readinglist_name + f" Page {page_num}") self.reading_list_title = self.readinglist_name + f" Page {page_num}" page = self.paginator_obj.page(page_num) self.current_page = page if page.has_next(): self.next_button.opacity = 1 self.next_button.disabled = False self.next_button.page_num = page.next_page_number() else: self.next_button.opacity = 0 self.next_button.disabled = True self.next_button.page_num = "" if page.has_previous(): self.prev_button.opacity = 1 self.prev_button.disabled = False self.prev_button.page_num = page.previous_page_number() else: self.prev_button.opacity = 0 self.prev_button.disabled = True self.prev_button.page_num = "" self.build_page(page.object_list) def build_page(self, object_lsit): async def _build_page(): grid = self.m_grid grid.clear_widgets() for comic in object_lsit: await asynckivy.sleep(0) c = ReadingListComicImage(comic_obj=comic) c.lines = 2 c.readinglist_obj = self.new_readinglist c.paginator_obj = self.paginator_obj y = self.comic_thumb_height thumb_filename = f"{comic.Id}.jpg" id_folder = self.app.store_dir my_thumb_dir = os.path.join(id_folder, "comic_thumbs") t_file = os.path.join(my_thumb_dir, thumb_filename) if os.path.isfile(t_file): c_image_source = t_file else: part_url = f"/Comics/{comic.Id}/Pages/0?" part_api = "&apiKey={}&height={}".format( self.api_key, round(dp(y))) c_image_source = f"{self.api_url}{part_url}{part_api}" asynckivy.start(save_thumb(comic.Id, c_image_source)) c.source = c_image_source c.PageCount = comic.PageCount c.pag_pagenum = self.current_page.number grid.add_widget(c) grid.cols = (Window.width - 10) // self.comic_thumb_width self.dynamic_ids[id] = c self.ids.page_count.text = "Page #\n{} of {}".format( self.current_page.number, self.paginator_obj.num_pages()) self.loading_done = True asynckivy.start(_build_page()) def refresh_callback(self, *args): """A method that updates the state of reading list""" def __refresh_callback(interval): self.ids.main_grid.clear_widgets() self.collect_readinglist_data( self.readinglist_name, self.readinglist_Id, current_page_num=self.page_number, mode="From DataBase", ) # self.build_page(page.object_list) # self.ids.main_scroll.refresh_done() self.tick = 0 Clock.schedule_once(__refresh_callback, 1) def got_db_data(self): """ used if rl data is already stored in db. """ async def _do_readinglist(): self.new_readinglist = ComicReadingList( name=self.readinglist_name, data="db_data", slug=self.readinglist_Id, ) await asynckivy.sleep(0) self.so = self.new_readinglist.sw_syn_this_active self.setup_options() new_readinglist_reversed = self.new_readinglist.comics self.paginator_obj = Paginator(new_readinglist_reversed, self.max_books_page) page = self.paginator_obj.page(self.page_number) self.current_page = page if page.has_next(): self.next_button.opacity = 1 self.next_button.disabled = False self.next_button.page_num = page.next_page_number() else: self.next_button.opacity = 0 self.next_button.disabled = True self.next_button.page_num = "" if page.has_previous(): self.prev_button.opacity = 1 self.prev_button.disabled = False self.prev_button.page_num = page.previous_page_number() else: self.prev_button.opacity = 0 self.prev_button.disabled = True self.prev_button.page_num = "" self.build_page(page.object_list) self.list_loaded = True asynckivy.start(_do_readinglist()) def got_json(self, req, results): print("Start got_json") async def _got_json(): print("Start _got_json") self.new_readinglist = ComicReadingList( name=self.readinglist_name, data=results, slug=self.readinglist_Id, ) totalCount = self.new_readinglist.totalCount i = 1 for item in self.new_readinglist.comic_json: await asynckivy.sleep(0) str_name = "{} #{}".format(item["Series"], item["Number"]) self.dialog_load_comic_data.name_kv_file = str_name self.dialog_load_comic_data.percent = str(i * 100 // int(totalCount)) comic_index = self.new_readinglist.comic_json.index(item) new_comic = ComicBook( item, readlist_obj=self.new_readinglist, comic_index=comic_index, ) self.new_readinglist.add_comic(new_comic) i += 1 print("End for item in self.new_readinglist.comic_json:") self.setup_options() new_readinglist_reversed = self.new_readinglist.comics[::-1] self.paginator_obj = Paginator(new_readinglist_reversed, self.max_books_page) page = self.paginator_obj.page(self.page_number) self.current_page = page if page.has_next(): self.next_button.opacity = 1 self.next_button.disabled = False self.next_button.page_num = page.next_page_number() else: self.next_button.opacity = 0 self.next_button.disabled = True self.next_button.page_num = "" if page.has_previous(): self.prev_button.opacity = 1 self.prev_button.disabled = False self.prev_button.page_num = page.previous_page_number() else: self.prev_button.opacity = 0 self.prev_button.disabled = True self.prev_button.page_num = "" self.build_page(page.object_list) self.list_loaded = True self.dialog_load_comic_data.dismiss() self.dialog_load_comic_data = DialogLoadKvFiles() self.dialog_load_comic_data.open() asynckivy.start(_got_json()) def show_please_wait_dialog(self): def __callback_for_please_wait_dialog(*args): pass self.please_wait_dialog = MDDialog( title="No ReadingList loaded.", size_hint=(0.8, 0.4), text_button_ok="Ok", text=f"No ReadingList loaded.", events_callback=__callback_for_please_wait_dialog, ) self.please_wait_dialog.open() def setup_options(self): self.sync_options = SyncOptionsPopup( size_hint=(0.76, 0.76), cb_limit_active=self.new_readinglist.cb_limit_active, limit_num_text=str(self.new_readinglist.limit_num), cb_only_read_active=self.new_readinglist.cb_only_read_active, cb_purge_active=self.new_readinglist.cb_purge_active, # noqa cb_optimize_size_active=self.new_readinglist. cb_optimize_size_active, # noqa sw_syn_this_active=bool(self.new_readinglist.sw_syn_this_active), ) self.sync_options.ids.limit_num.bind( on_text_validate=self.sync_options.check_input, focus=self.sync_options.check_input, ) self.sync_options.title = self.new_readinglist.name def open_sync_options(self): if self.sync_options.ids.sw_syn_this.active is True: self.sync_options.ids.syn_on_off_label.text = f"" self.sync_options.ids.syn_on_off_label.theme_text_color = "Primary" self.sync_options.open() def sync_readinglist(self): if self.sync_options.ids.sw_syn_this.active is False: self.sync_options.ids.syn_on_off_label.text = f"Sync Not Turned On" self.open_sync_options() elif self.sync_options.ids.sw_syn_this.active is True: toast(f"Starting sync of {self.new_readinglist.name}") self.new_readinglist.do_sync()
class TableData(RecycleView): recycle_data = ListProperty() # kivy.uix.recycleview.RecycleView.data data_first_cells = ListProperty() # list of first row cells row_data = ListProperty() # MDDataTable.row_data total_col_headings = NumericProperty(0) # TableHeader.col_headings cols_minimum = DictProperty() # TableHeader.cols_minimum table_header = ObjectProperty() # <TableHeader object> pagination_menu = ObjectProperty() # <MDDropdownMenu object> pagination = ObjectProperty() # <TablePagination object> check = ObjectProperty() # MDDataTable.check rows_num = NumericProperty() # number of rows displayed on the table page # Open or close the menu for selecting the number of rows displayed # on the table page. pagination_menu_open = BooleanProperty(False) # List of indexes of marked checkboxes. current_selection_check = DictProperty() sort = BooleanProperty() _parent = ObjectProperty() _rows_number = NumericProperty(0) _rows_num = NumericProperty() _current_value = NumericProperty(1) _to_value = NumericProperty() _row_data_parts = ListProperty() def __init__(self, table_header, **kwargs): super().__init__(**kwargs) self.table_header = table_header self.total_col_headings = len(table_header.col_headings) self.cols_minimum = table_header.cols_minimum self.set_row_data() Clock.schedule_once(self.set_default_first_row, 0) def get_select_row(self, index): """Returns the current row with all elements.""" row = [] for data in self.recycle_data: if index in data["range"]: row.append(data["text"]) self._parent.dispatch("on_check_press", row) def set_default_first_row(self, dt): """Set default first row as selected.""" self.ids.row_controller.select_next(self) def sort_by_name(self): """Sorts table data.""" # TODO: implement a rows sorting method. def set_row_data(self): data = [] low = 0 high = self.total_col_headings - 1 self.recycle_data = [] self.data_first_cells = [] if self._row_data_parts: # for row in self.row_data: for row in self._row_data_parts[self._rows_number]: for i in range(len(row)): data.append([row[i], row[0], [low, high]]) low += self.total_col_headings high += self.total_col_headings for j, x in enumerate(data): if x[0] == x[1]: self.data_first_cells.append(x[0]) self.recycle_data.append({ "text": str(x[0]), "Index": str(x[1]), "range": x[2], "selectable": True, "viewclass": "CellRow", "table": self, }) if not self.table_header.column_data: raise ValueError( f"Set value for column_data in class TableData") self.data_first_cells.append(self.table_header.column_data[0][0]) def set_text_from_of(self, direction): """Sets the text of the numbers of displayed pages in table.""" if self.pagination: if direction == "forward": if (len(self._row_data_parts[self._rows_number]) < self._to_value): self._current_value = self._current_value + self.rows_num else: self._current_value = self._current_value + len( self._row_data_parts[self._rows_number]) self._to_value = self._to_value + len( self._row_data_parts[self._rows_number]) if direction == "back": self._current_value = self._current_value - len( self._row_data_parts[self._rows_number]) self._to_value = self._to_value - len( self._row_data_parts[self._rows_number]) if direction == "increment": self._current_value = 1 self._to_value = self.rows_num + self._current_value - 1 self.pagination.ids.label_rows_per_page.text = f"{self._current_value}-{self._to_value} of {len(self.row_data)}" def select_all(self, state): """Sets the checkboxes of all rows to the active/inactive position.""" # FIXME: fix the work of selecting all cells.. for i, data in enumerate(self.recycle_data): opts = self.layout_manager.view_opts cell_row_obj = self.view_adapter.get_view(i, self.data[i], opts[i]["viewclass"]) cell_row_obj.ids.check.state = state self.on_mouse_select(cell_row_obj) cell_row_obj.select_check(True if state == "down" else False) def close_pagination_menu(self, *args): """Called when the pagination menu window is closed.""" self.pagination_menu_open = False def open_pagination_menu(self): """Open pagination menu window.""" if self.pagination_menu.items: self.pagination_menu_open = True self.pagination_menu.open() def set_number_displayed_lines(self, instance_menu_item): """ Called when the user sets the number of pages displayed in the table. """ self.rows_num = int(instance_menu_item.text) self.set_row_data() self.set_text_from_of("increment") def set_next_row_data_parts(self, direction): """Called when switching the pages of the table.""" if direction == "forward": self._rows_number += 1 self.pagination.ids.button_back.disabled = False elif direction == "back": self._rows_number -= 1 self.pagination.ids.button_forward.disabled = False self.set_row_data() self.set_text_from_of(direction) if self._to_value == len(self.row_data): self.pagination.ids.button_forward.disabled = True if self._current_value == 1: self.pagination.ids.button_back.disabled = True def _split_list_into_equal_parts(self, lst, parts): for i in range(0, len(lst), parts): yield lst[i:i + parts] def on_mouse_select(self, instance): """Called on the ``on_enter`` event of the :class:`~CellRow` class""" if not self.pagination_menu_open: if self.ids.row_controller.selected_row != instance.index: self.ids.row_controller.selected_row = instance.index self.ids.row_controller.select_current(self) def on_rows_num(self, instance, value): if not self._to_value: self._to_value = value self._rows_number = 0 self._row_data_parts = list( self._split_list_into_equal_parts(self.row_data, value))
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. :Parameters: `panel`: :class:`SettingsPanel` It should be stored and displayed when requested. `name`: The name of the panel as a string. It may be used to represent the panel. `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. :Parameters: `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)
class VideoPlayer(GridLayout): '''VideoPlayer class, see module documentation for more information. ''' source = StringProperty(None) '''Source of the video to read :data:`source` a :class:`~kivy.properties.StringProperty`, default to None. ''' thumbnail = StringProperty(None) '''Thumbnail of the video to show. If None, it will try to search the thumbnail from the :data:`source` + .png. :data:`thumbnail` a :class:`~kivy.properties.StringProperty`, default to None. ''' duration = NumericProperty(-1) '''Duration of the video. The duration is default to -1, and set to real duration when the video is loaded. :data:`duration` is a :class:`~kivy.properties.NumericProperty`, default to -1. ''' position = NumericProperty(0) '''Position of the video between 0 and :data:`duration`. The position is default to -1, and set to real position when the video is loaded. :data:`position` is a :class:`~kivy.properties.NumericProperty`, default to -1. ''' volume = NumericProperty(1.0) '''Volume of the video, in the range 0-1. 1 mean full volume, 0 mean mute. :data:`volume` is a :class:`~kivy.properties.NumericProperty`, default to 1. ''' play = BooleanProperty(False) '''Boolean, indicates if the video is playing. 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 :data:`play` is a :class:`~kivy.properties.BooleanProperty`, default to False. ''' image_overlay_play = StringProperty( 'atlas://data/images/defaulttheme/player-play-overlay') '''Image filename used to show an "play" overlay when the video is not yet started. :data:`image_overlay_play` a :class:`~kivy.properties.StringProperty` ''' image_loading = StringProperty('data/images/image-loading.gif') '''Image filename used when the video is loading. :data:`image_loading` a :class:`~kivy.properties.StringProperty` ''' image_play = StringProperty( 'atlas://data/images/defaulttheme/media-playback-start') '''Image filename used for the "Play" button. :data:`image_loading` a :class:`~kivy.properties.StringProperty` ''' image_pause = StringProperty( 'atlas://data/images/defaulttheme/media-playback-pause') '''Image filename used for the "Pause" button. :data:`image_pause` a :class:`~kivy.properties.StringProperty` ''' image_volumehigh = StringProperty( 'atlas://data/images/defaulttheme/audio-volume-high') '''Image filename used for the volume icon, when the volume is high. :data:`image_volumehigh` a :class:`~kivy.properties.StringProperty` ''' image_volumemedium = StringProperty( 'atlas://data/images/defaulttheme/audio-volume-medium') '''Image filename used for the volume icon, when the volume is medium. :data:`image_volumemedium` a :class:`~kivy.properties.StringProperty` ''' image_volumelow = StringProperty( 'atlas://data/images/defaulttheme/audio-volume-low') '''Image filename used for the volume icon, when the volume is low. :data:`image_volumelow` a :class:`~kivy.properties.StringProperty` ''' image_volumemuted = StringProperty( 'atlas://data/images/defaulttheme/audio-volume-muted') '''Image filename used for the volume icon, when the volume is muted. :data:`image_volumemuted` a :class:`~kivy.properties.StringProperty` ''' annotations = StringProperty(None) '''If set, it will be used for reading annotations box. ''' fullscreen = BooleanProperty(False) '''Switch to a fullscreen view. This must be used with care. When activated, the widget will remove itself from its parent, remove all children from the window and add itself to it. When fullscreen is unset, all the previous children are restored, and the widget is readded to its previous parent. .. warning:: The re-add operation doesn't care about it's children index position within the parent. :data:`fullscreen` a :class:`~kivy.properties.BooleanProperty`, default 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. :data:`allow_fullscreen` a :class:`~kivy.properties.BooleanProperty`, default to True ''' options = DictProperty({}) '''Optionals parameters can be passed to :class:`~kivy.uix.video.Video` instance with this property. :data:`options` a :class:`~kivy.properties.DictProperty`, default to {} ''' # internals container = ObjectProperty(None) def __init__(self, **kwargs): self._video = None self._image = None self._annotations = None self._annotations_labels = [] super(VideoPlayer, self).__init__(**kwargs) self._load_thumbnail() self._load_annotations() 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() 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 thumbnail is None: 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 annotations is None: 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_play(self, instance, value): if self._video is None: self._video = Video(source=self.source, play=True, 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')) self._video.play = 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 duration. Percentage must be a value between 0-1. .. warning:: Calling seek() before video is loaded have no impact. ''' 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 ChannelChatTab(MDTab): app = ObjectProperty(None) irc_message = ObjectProperty(None) irc_message_send_btn = ObjectProperty(None) nick_data = DictProperty() def __init__(self, **kw): super(ChannelChatTab, self).__init__(**kw) self.app = App.get_running_app() Clock.schedule_once(self.__post_init__) def __post_init__(self, *args): self.irc_message._hint_lbl.text = '@' + self.app.config.get( 'irc', 'nickname') self.app.connection.on_privmsg(self.text, self.on_privmsg) self.app.connection.on_usr_action(self.text, self.on_usr_action) def update_irc_message_text(self, dt): self.irc_message.text = '' self.irc_message.on_focus() def send_message(self): Clock.schedule_once(self.update_irc_message_text) self.app.connection.msg("#" + self.text, self.irc_message.text) self.msg_list.add_widget( MultiLineListItem( text="[b][color=1A237E]@" + self.app.config.get('irc', 'nickname') + "[/color][/b] " + self.irc_message.text, font_style='Subhead', )) Logger.info( "IRC: <%s> %s" % (self.app.config.get('irc', 'nickname'), self.irc_message.text)) def on_privmsg(self, user, channel, msg): user = user.split('!', 1)[0] self.msg_list.add_widget( MultiLineListItem( text="[b][color=F44336]@" + user + "[/color][/b] " + msg, font_style='Subhead', )) Logger.info("IRC: <%s> %s" % (user, msg)) def on_usr_action(self, user, channel, quit_message, action): if action == 0: self.msg_list.add_widget( MultiLineListItem( text="[color=9C27B0]" + user + "[/color] has joined #" + self.text, font_style='Subhead', )) Logger.info("IRC: %s -> %s" % (user, 'joined')) elif action == 1: self.msg_list.add_widget( MultiLineListItem( text="[color=9C27B0]" + user + "[/color] has left #" + self.text, font_style='Subhead', )) Logger.info("IRC: %s <- %s" % (user, 'left')) elif action == 2: self.msg_list.add_widget( MultiLineListItem( text="[color=9A2FB0]" + user + "[/color] has quit &bl;" + quit_message + "&bt;", font_style='Subhead', )) Logger.info("IRC: %s <- %s" % (user, 'quit')) self.app.connection.who(self.text).addCallback(self.who_callback) def nick_details(self, nick_list_item): self.app.connection.signedOn() nick_item_data = self.nick_data[nick_list_item.text] bs = MDListBottomSheet() bs.add_item("WHOIS ({})".format(nick_list_item.text), lambda x: x) bs.add_item( "{} ({}@{})".format(nick_item_data[7].split(' ')[1], nick_item_data[3], nick_item_data[2]), lambda x: x) bs.add_item( "{} is connected via {}".format(nick_list_item.text, nick_item_data[4]), lambda x: x) bs.open() def who_callback(self, nick_data): self.nick_data = nick_data nick_list = nick_data.keys() nick_list.sort() self.nick_list.clear_widgets() for nick in nick_list: list_item = MultiLineListItem(text=nick) list_item.bind(on_press=self.nick_details) self.nick_list.add_widget(list_item) Logger.info("IRC: <%s> -> nicks -> %s" % (self.text, nick_list)) def __post_connection__(self, connection): pass def __post_joined__(self, connection): connection.who(self.text).addCallback(self.who_callback)
class OpeningLabel(Label): instances = set() sizeFactor = NumericProperty() line_width = NumericProperty(1.5) has_variant = BooleanProperty(False) has_children = BooleanProperty(False) selected = BooleanProperty(False) box_width = NumericProperty() box_height = NumericProperty() box_left = NumericProperty() box_right = NumericProperty() box_bottom = NumericProperty() box_top = NumericProperty() top_node: chess.pgn.GameNode = ObjectProperty(rebind=True, allownone=True) bottom_node: chess.pgn.GameNode = ObjectProperty(rebind=True, allownone=True) actual_node: chess.pgn.GameNode = ObjectProperty(rebind=True, allownone=True) mapNodeToChild = DictProperty(rebind=True) parent_node = ObjectProperty(rebind=True) top_node_label = ObjectProperty(rebind=True, allownone=True) bottom_node_label = ObjectProperty(rebind=True, allownone=True) def __init__(self, gameNode, parent_node=None, **kwargs): self.heights = [] self.mapNodeToChild = {} self.cache_y = 0 self.callback_on_select = lambda x: None if (parent_node is not None): self.parent_node = parent_node self.actual_node = gameNode super().__init__(**kwargs) if (self.parent_node is not None and self.parent_node.parent is not None): self.parent_node.parent.add_widget(self) self.on_parent(self, self.parent) self.on_actual_node(self, self.actual_node, True) self.calculate_heights() OpeningLabel.instances.add(self) def set_y(self, _0=None, _1=None): self.y = ((self.mapNodeToChild[self.top_node].y + self.mapNodeToChild[self.bottom_node].y) / 2) if self.has_children else ( self.parent_node.pos_child(self.actual_node) if self.parent_node is not None and self.actual_node is not None else 0) def on_parent(self, instance, value): if (self.parent is not None): for label in self.mapNodeToChild.values(): self.parent.add_widget(label) def calculate_heights(self): if (self.actual_node.variations == []): self.heights = [] else: self.heights = [ self.mapNodeToChild[child_node].calculate_heights() for child_node in self.actual_node.variations ] OpeningLabel.set_all_y_coord() return (1 if self.heights == [] else sum(self.heights)) @staticmethod def set_all_y_coord(): for label in OpeningLabel.instances: label.set_y() def pos_child(self, child_node): y_pos = 0 index = self.actual_node.variations.index(child_node) if (self.parent_node is not None): y_pos += self.parent_node.pos_child(self.actual_node) if (index != 0): y_pos += sum(self.heights[:index]) * self.height return y_pos def on_actual_node(self, instance=None, value=None, init=False): flag_updated = False for variation_node in self.actual_node.variations: if (variation_node not in self.mapNodeToChild): self.mapNodeToChild.update([ (variation_node, OpeningLabel(variation_node, self)) ]) flag_updated = True if (self.actual_node.variations != []): self.top_node = self.actual_node.variations[-1] self.bottom_node = self.actual_node.variations[0] if (flag_updated and not init): label_iter = self while (label_iter.parent_node is not None): label_iter = label_iter.parent_node label_iter.calculate_heights() def on_selected(self, instance, value): if (value): for label in OpeningLabel.instances: if (label is not instance): label.selected = False def on_touch_up(self, touch: MotionEvent): if (touch.button == "left" and abs(touch.dx) + abs(touch.dy) == 0): rx, ry = self.to_window(touch.x, touch.y, False) if (self.parent.parent.collide_point(rx, ry)): t_x, t_y = touch.x + self.parent.x, touch.y + self.parent.y t_x -= self.parent.parent.center_x t_y -= self.parent.parent.center_y t_x, t_y = t_x / self.parent.getScalingFactor( ), t_y / self.parent.getScalingFactor() t_x += self.parent.parent.center_x t_y += self.parent.parent.center_y t_x, t_y = t_x - self.parent.x, t_y - self.parent.y if (self.collide_point(t_x, t_y)): self.selected = True if (self.callback_on_select is not None): self.callback_on_select(self.actual_node) return True # stop spreading in widget tree return super().on_touch_up(touch)
class MeshLinePlot(Plot): '''MeshLinePlot class which displays a set of points similar to a mesh. ''' # mesh which forms the plot _mesh = ObjectProperty(None) # color of the plot _color = ObjectProperty(None) _trigger = ObjectProperty(None) # most recent values of the params used to draw the plot _params = DictProperty({ 'xlog': False, 'xmin': 0, 'xmax': 100, 'ylog': False, 'ymin': 0, 'ymax': 100, 'size': (0, 0, 0, 0) }) def __init__(self, **kwargs): self._color = Color(1, 1, 1, group='LinePlot%d' % id(self)) self._mesh = Mesh(mode='line_strip', group='LinePlot%d' % id(self)) super(MeshLinePlot, self).__init__(**kwargs) self._trigger = Clock.create_trigger(self._redraw) self.bind(_params=self._trigger, points=self._trigger) def _update(self, xlog, xmin, xmax, ylog, ymin, ymax, size): self._params = { 'xlog': xlog, 'xmin': xmin, 'xmax': xmax, 'ylog': ylog, 'ymin': ymin, 'ymax': ymax, 'size': size } def _redraw(self, *args): points = self.points mesh = self._mesh vert = mesh.vertices ind = mesh.indices params = self._params funcx = log10 if params['xlog'] else lambda x: x funcy = log10 if params['ylog'] else lambda x: x xmin = funcx(params['xmin']) ymin = funcy(params['ymin']) diff = len(points) - len(vert) / 4 size = params['size'] ratiox = (size[2] - size[0]) / float(funcx(params['xmax']) - xmin) ratioy = (size[3] - size[1]) / float(funcy(params['ymax']) - ymin) if diff < 0: del vert[4 * len(points):] del ind[len(points):] elif diff > 0: ind.extend(xrange(len(ind), len(ind) + diff)) vert.extend([0] * (diff * 4)) for k in xrange(len(points)): vert[k * 4] = (funcx(points[k][0]) - xmin) * ratiox + size[0] vert[k * 4 + 1] = (funcy(points[k][1]) - ymin) * ratioy + size[1] mesh.vertices = vert def _get_group(self): return 'LinePlot%d' % id(self) def _get_drawings(self): return [self._color, self._mesh] def _set_mode(self, value): self._mesh.mode = value mode = AliasProperty(lambda self: self._mesh.mode, _set_mode) '''VBO Mode used for drawing the points. Can be one of: 'points', 'line_strip', 'line_loop', 'lines', 'triangle_strip', 'triangle_fan'. See :class:`~kivy.graphics.Mesh` for more details. Defaults to 'line_strip'. ''' def _set_color(self, value): self._color.rgba = value color = AliasProperty(lambda self: self._color.rgba, _set_color) '''Plot color, in the format [r, g, b, a] with values between 0-1. Defaults to [1, 1, 1, 1]. ''' points = ListProperty([]) '''List of x, y points to be displayed in the plot.
class DeckBuilderLayout(Layout): """Sizes and positions :class:`Card` objects based on their order within ``decks``, a list of lists where each sublist is a deck of cards. """ direction = OptionProperty('ascending', options=['ascending', 'descending']) """Should the beginning card of each deck appear on the bottom ('ascending'), or the top ('descending')? """ card_size_hint_x = BoundedNumericProperty(1, min=0, max=1) """Each card's width, expressed as a proportion of my width.""" card_size_hint_y = BoundedNumericProperty(1, min=0, max=1) """Each card's height, expressed as a proportion of my height.""" card_size_hint = ReferenceListProperty(card_size_hint_x, card_size_hint_y) """Size hint of cards, relative to my size.""" starting_pos_hint = DictProperty({'x': 0, 'y': 0}) """Pos hint at which to place the initial card of the initial deck.""" card_x_hint_step = NumericProperty(0) """Each time I put another card on a deck, I'll move it this much of my width to the right of the previous card. """ card_y_hint_step = NumericProperty(-1) """Each time I put another card on a deck, I'll move it this much of my height above the previous card. """ card_hint_step = ReferenceListProperty(card_x_hint_step, card_y_hint_step) """An offset, expressed in proportion to my size, applied to each successive card in a given deck. """ deck_x_hint_step = NumericProperty(1) """When I start a new deck, it will be this far to the right of the previous deck, expressed as a proportion of my width. """ deck_y_hint_step = NumericProperty(0) """When I start a new deck, it will be this far above the previous deck, expressed as a proportion of my height. """ deck_hint_step = ReferenceListProperty(deck_x_hint_step, deck_y_hint_step) """Offset of each deck with respect to the previous, as a proportion of my size. """ decks = ListProperty([[]]) # list of lists of cards """Put a list of lists of :class:`Card` objects here and I'll position them appropriately. Please don't use ``add_widget``. """ deck_x_hint_offsets = ListProperty([]) """An additional proportional x-offset for each deck, defaulting to 0.""" deck_y_hint_offsets = ListProperty([]) """An additional proportional y-offset for each deck, defaulting to 0.""" foundation_color = ListProperty([1, 1, 1, 1]) """Color to use for the outline showing where a deck is when it's empty. """ insertion_deck = BoundedNumericProperty(None, min=0, allownone=True) """Index of the deck that a card is being dragged into.""" insertion_card = BoundedNumericProperty(None, min=0, allownone=True) """Index within the current deck that a card is being dragged into.""" _foundations = ListProperty([]) """Private. A list of :class:`Foundation` widgets, one per deck.""" def __init__(self, **kwargs): """Bind most of my custom properties to ``_trigger_layout``.""" super().__init__(**kwargs) self.bind(card_size_hint=self._trigger_layout, starting_pos_hint=self._trigger_layout, card_hint_step=self._trigger_layout, deck_hint_step=self._trigger_layout, decks=self._trigger_layout, deck_x_hint_offsets=self._trigger_layout, deck_y_hint_offsets=self._trigger_layout, insertion_deck=self._trigger_layout, insertion_card=self._trigger_layout) def scroll_deck_x(self, decknum, scroll_x): """Move a deck left or right.""" if decknum >= len(self.decks): raise IndexError("I have no deck at {}".format(decknum)) if decknum >= len(self.deck_x_hint_offsets): self.deck_x_hint_offsets = list(self.deck_x_hint_offsets) + [0] * ( decknum - len(self.deck_x_hint_offsets) + 1) self.deck_x_hint_offsets[decknum] += scroll_x self._trigger_layout() def scroll_deck_y(self, decknum, scroll_y): """Move a deck up or down.""" if decknum >= len(self.decks): raise IndexError("I have no deck at {}".format(decknum)) if decknum >= len(self.deck_y_hint_offsets): self.deck_y_hint_offsets = list(self.deck_y_hint_offsets) + [0] * ( decknum - len(self.deck_y_hint_offsets) + 1) self.deck_y_hint_offsets[decknum] += scroll_y self._trigger_layout() def scroll_deck(self, decknum, scroll_x, scroll_y): """Move a deck.""" self.scroll_deck_x(decknum, scroll_x) self.scroll_deck_y(decknum, scroll_y) def _get_foundation_pos(self, i): """Private. Get the absolute coordinates to use for a deck's foundation, based on the ``starting_pos_hint``, the ``deck_hint_step``, ``deck_x_hint_offsets``, and ``deck_y_hint_offsets``. """ (phx, phy) = get_pos_hint(self.starting_pos_hint, *self.card_size_hint) phx += self.deck_x_hint_step * i + self.deck_x_hint_offsets[i] phy += self.deck_y_hint_step * i + self.deck_y_hint_offsets[i] x = phx * self.width + self.x y = phy * self.height + self.y return (x, y) def _get_foundation(self, i): """Return a :class:`Foundation` for some deck, creating it if needed. """ if i >= len(self._foundations) or self._foundations[i] is None: oldfound = list(self._foundations) extend = i - len(oldfound) + 1 if extend > 0: oldfound += [None] * extend width = self.card_size_hint_x * self.width height = self.card_size_hint_y * self.height found = Foundation(pos=self._get_foundation_pos(i), size=(width, height), deck=i) self.bind(pos=found.upd_pos, card_size_hint=found.upd_pos, deck_hint_step=found.upd_pos, size=found.upd_pos, deck_x_hint_offsets=found.upd_pos, deck_y_hint_offsets=found.upd_pos) self.bind(size=found.upd_size, card_size_hint=found.upd_size) oldfound[i] = found self._foundations = oldfound return self._foundations[i] def on_decks(self, *args): """Inform the cards of their deck and their index within the deck; extend the ``_hint_offsets`` properties as needed; and trigger a layout. """ if None in (self.canvas, self.decks, self.deck_x_hint_offsets, self.deck_y_hint_offsets): Clock.schedule_once(self.on_decks, 0) return self.clear_widgets() decknum = 0 for deck in self.decks: cardnum = 0 for card in deck: if not isinstance(card, Card): raise TypeError("You must only put Card in decks") if card not in self.children: self.add_widget(card) if card.deck != decknum: card.deck = decknum if card.idx != cardnum: card.idx = cardnum cardnum += 1 decknum += 1 if len(self.deck_x_hint_offsets) < len(self.decks): self.deck_x_hint_offsets = list(self.deck_x_hint_offsets) + [0] * ( len(self.decks) - len(self.deck_x_hint_offsets)) if len(self.deck_y_hint_offsets) < len(self.decks): self.deck_y_hint_offsets = list(self.deck_y_hint_offsets) + [0] * ( len(self.decks) - len(self.deck_y_hint_offsets)) self._trigger_layout() def point_before_card(self, card, x, y): """Return whether ``(x, y)`` is somewhere before ``card``, given how I know cards to be arranged. If the cards are being stacked down and to the right, that means I'm testing whether ``(x, y)`` is above or to the left of the card. """ def ycmp(): if self.card_y_hint_step == 0: return False elif self.card_y_hint_step > 0: # stacking upward return y < card.y else: # stacking downward return y > card.top if self.card_x_hint_step > 0: # stacking to the right if x < card.x: return True return ycmp() elif self.card_x_hint_step == 0: return ycmp() else: # stacking to the left if x > card.right: return True return ycmp() def point_after_card(self, card, x, y): """Return whether ``(x, y)`` is somewhere after ``card``, given how I know cards to be arranged. If the cards are being stacked down and to the right, that means I'm testing whether ``(x, y)`` is below or to the left of ``card``. """ def ycmp(): if self.card_y_hint_step == 0: return False elif self.card_y_hint_step > 0: # stacking upward return y > card.top else: # stacking downward return y < card.y if self.card_x_hint_step > 0: # stacking to the right if x > card.right: return True return ycmp() elif self.card_x_hint_step == 0: return ycmp() else: # stacking to the left if x < card.x: return True return ycmp() def on_touch_move(self, touch): """If a card is being dragged, move other cards out of the way to show where the dragged card will go if you drop it. """ if ('card' not in touch.ud or 'layout' not in touch.ud or touch.ud['layout'] != self): return if (touch.ud['layout'] == self and not hasattr(touch.ud['card'], '_topdecked')): touch.ud['card']._topdecked = InstructionGroup() touch.ud['card']._topdecked.add(touch.ud['card'].canvas) self.canvas.after.add(touch.ud['card']._topdecked) i = 0 for deck in self.decks: cards = [card for card in deck if not card.dragging] maxidx = max(card.idx for card in cards) if cards else 0 if self.direction == 'descending': cards.reverse() cards_collided = [ card for card in cards if card.collide_point(*touch.pos) ] if cards_collided: collided = cards_collided.pop() for card in cards_collided: if card.idx > collided.idx: collided = card if collided.deck == touch.ud['deck']: self.insertion_card = ( 1 if collided.idx == 0 else maxidx + 1 if collided.idx == maxidx else collided.idx + 1 if collided.idx > touch.ud['idx'] else collided.idx) else: dropdeck = self.decks[collided.deck] maxidx = max(card.idx for card in dropdeck) self.insertion_card = ( 1 if collided.idx == 0 else maxidx + 1 if collided.idx == maxidx else collided.idx + 1) if self.insertion_deck != collided.deck: self.insertion_deck = collided.deck return else: if self.insertion_deck == i: if self.insertion_card in (0, len(deck)): pass elif self.point_before_card(cards[0], *touch.pos): self.insertion_card = 0 elif self.point_after_card(cards[-1], *touch.pos): self.insertion_card = cards[-1].idx else: j = 0 for found in self._foundations: if (found is not None and found.collide_point(*touch.pos)): self.insertion_deck = j self.insertion_card = 0 return j += 1 i += 1 def on_touch_up(self, touch): """If a card is being dragged, put it in the place it was just dropped and trigger a layout. """ if ('card' not in touch.ud or 'layout' not in touch.ud or touch.ud['layout'] != self): return if hasattr(touch.ud['card'], '_topdecked'): self.canvas.after.remove(touch.ud['card']._topdecked) del touch.ud['card']._topdecked if None not in (self.insertion_deck, self.insertion_card): # need to sync to adapter.data?? card = touch.ud['card'] del card.parent.decks[card.deck][card.idx] for i in range(0, len(card.parent.decks[card.deck])): card.parent.decks[card.deck][i].idx = i deck = self.decks[self.insertion_deck] if self.insertion_card >= len(deck): deck.append(card) else: deck.insert(self.insertion_card, card) card.deck = self.insertion_deck card.idx = self.insertion_card self.decks[self.insertion_deck] = deck self.insertion_deck = self.insertion_card = None self._trigger_layout() def on_insertion_card(self, *args): """Trigger a layout""" if self.insertion_card is not None: self._trigger_layout() def do_layout(self, *args): """Layout each of my decks""" if self.size == [1, 1]: return for i in range(0, len(self.decks)): self.layout_deck(i) def layout_deck(self, i): """Stack the cards, starting at my deck's foundation, and proceeding by ``card_pos_hint`` """ def get_dragidx(cards): j = 0 for card in cards: if card.dragging: return j j += 1 # Put a None in the card list in place of the card you're # hovering over, if you're dragging another card. This will # result in an empty space where the card will go if you drop # it now. cards = list(self.decks[i]) dragidx = get_dragidx(cards) if dragidx is not None: del cards[dragidx] if self.insertion_deck == i and self.insertion_card is not None: insdx = self.insertion_card if dragidx is not None and insdx > dragidx: insdx -= 1 cards.insert(insdx, None) if self.direction == 'descending': cards.reverse() # Work out the initial pos_hint for this deck (phx, phy) = get_pos_hint(self.starting_pos_hint, *self.card_size_hint) phx += self.deck_x_hint_step * i + self.deck_x_hint_offsets[i] phy += self.deck_y_hint_step * i + self.deck_y_hint_offsets[i] (w, h) = self.size (x, y) = self.pos # start assigning pos and size to cards found = self._get_foundation(i) if found in self.children: self.remove_widget(found) self.add_widget(found) for card in cards: if card is not None: if card in self.children: self.remove_widget(card) (shw, shh) = self.card_size_hint card.pos = (x + phx * w, y + phy * h) card.size = (w * shw, h * shh) self.add_widget(card) phx += self.card_x_hint_step phy += self.card_y_hint_step
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', 'halign', 'valign', 'padding_x', 'padding_y', 'text_size', 'shorten', 'mipmap', 'markup', 'line_height', 'max_lines', 'strip', 'shorten_from', 'split_str', 'unicode_errors') 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('DroidSans') '''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 'DroidSans'. ''' 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. ''' 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]. ''' 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
class AccordionItem(FloatLayout): '''AccordionItem class that must be used in conjunction with the :class:`Accordion` class. See the module documentation for more information. ''' title = StringProperty('') '''Title string of the item. The title might be used in conjunction with the `AccordionItemTitle` template. If you are using a custom template, you can use that property as a text entry, or not. By default, it's used for the title text. See title_template and the example below. :attr:`title` is a :class:`~kivy.properties.StringProperty` and defaults to ''. ''' title_template = StringProperty('AccordionItemTitle') '''Template to use for creating the title part of the accordion item. The default template is a simple Label, not customizable (except the text) that supports vertical and horizontal orientation and different backgrounds for collapse and selected mode. It's better to create and use your own template if the default template does not suffice. :attr:`title` is a :class:`~kivy.properties.StringProperty` and defaults to 'AccordionItemTitle'. The current default template lives in the `kivy/data/style.kv` file. Here is the code if you want to build your own template:: [AccordionItemTitle@Label]: text: ctx.title canvas.before: Color: rgb: 1, 1, 1 BorderImage: source: ctx.item.background_normal \ if ctx.item.collapse \ else ctx.item.background_selected pos: self.pos size: self.size PushMatrix Translate: xy: self.center_x, self.center_y Rotate: angle: 90 if ctx.item.orientation == 'horizontal' else 0 axis: 0, 0, 1 Translate: xy: -self.center_x, -self.center_y canvas.after: PopMatrix ''' title_args = DictProperty({}) '''Default arguments that will be passed to the :meth:`kivy.lang.Builder.template` method. :attr:`title_args` is a :class:`~kivy.properties.DictProperty` and defaults to {}. ''' collapse = BooleanProperty(True) '''Boolean to indicate if the current item is collapsed or not. :attr:`collapse` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. ''' collapse_alpha = NumericProperty(1.) '''Value between 0 and 1 to indicate how much the item is collapsed (1) or whether it is selected (0). It's mostly used for animation. :attr:`collapse_alpha` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. ''' accordion = ObjectProperty(None) '''Instance of the :class:`Accordion` that the item belongs to. :attr:`accordion` is an :class:`~kivy.properties.ObjectProperty` and defaults to None. ''' background_normal = StringProperty( 'atlas://data/images/defaulttheme/button') '''Background image of the accordion item used for the default graphical representation when the item is collapsed. :attr:`background_normal` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/button'. ''' background_disabled_normal = StringProperty( 'atlas://data/images/defaulttheme/button_disabled') '''Background image of the accordion item used for the default graphical representation when the item is collapsed and disabled. .. versionadded:: 1.8.0 :attr:`background__disabled_normal` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/button_disabled'. ''' background_selected = StringProperty( 'atlas://data/images/defaulttheme/button_pressed') '''Background image of the accordion item used for the default graphical representation when the item is selected (not collapsed). :attr:`background_normal` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/button_pressed'. ''' background_disabled_selected = StringProperty( 'atlas://data/images/defaulttheme/button_disabled_pressed') '''Background image of the accordion item used for the default graphical representation when the item is selected (not collapsed) and disabled. .. versionadded:: 1.8.0 :attr:`background_disabled_selected` is a :class:`~kivy.properties.StringProperty` and defaults to 'atlas://data/images/defaulttheme/button_disabled_pressed'. ''' orientation = OptionProperty('vertical', options=( 'horizontal', 'vertical')) '''Link to the :attr:`Accordion.orientation` property. ''' min_space = NumericProperty('44dp') '''Link to the :attr:`Accordion.min_space` property. ''' content_size = ListProperty([100, 100]) '''(internal) Set by the :class:`Accordion` to the size allocated for the content. ''' container = ObjectProperty(None) '''(internal) Property that will be set to the container of children inside the AccordionItem representation. ''' container_title = ObjectProperty(None) '''(internal) Property that will be set to the container of title inside the AccordionItem representation. ''' def __init__(self, **kwargs): self._trigger_title = Clock.create_trigger(self._update_title, -1) self._anim_collapse = None super(AccordionItem, self).__init__(**kwargs) trigger_title = self._trigger_title fbind = self.fbind fbind('title', trigger_title) fbind('title_template', trigger_title) fbind('title_args', trigger_title) trigger_title() def add_widget(self, *args, **kwargs): if self.container is None: super(AccordionItem, self).add_widget(*args, **kwargs) return self.container.add_widget(*args, **kwargs) def remove_widget(self, *args, **kwargs): if self.container: self.container.remove_widget(*args, **kwargs) return super(AccordionItem, self).remove_widget(*args, **kwargs) def on_collapse(self, instance, value): accordion = self.accordion if accordion is None: return if not value: self.accordion.select(self) collapse_alpha = float(value) if self._anim_collapse: self._anim_collapse.stop(self) self._anim_collapse = None if self.collapse_alpha != collapse_alpha: self._anim_collapse = Animation( collapse_alpha=collapse_alpha, t=accordion.anim_func, d=accordion.anim_duration) self._anim_collapse.start(self) def on_collapse_alpha(self, instance, value): self.accordion._trigger_layout() def on_touch_down(self, touch): if not self.collide_point(*touch.pos): return if self.disabled: return True if self.collapse: self.collapse = False return True else: return super(AccordionItem, self).on_touch_down(touch) def _update_title(self, dt): if not self.container_title: self._trigger_title() return c = self.container_title c.clear_widgets() instance = Builder.template(self.title_template, title=self.title, item=self, **self.title_args) c.add_widget(instance)
class MDApp(BaseApp): """HotReload Application class.""" DEBUG = BooleanProperty("DEBUG" in os.environ) """ Control either we activate debugging in the app or not. Defaults depend if 'DEBUG' exists in os.environ. :attr:`DEBUG` is a :class:`~kivy.properties.BooleanProperty`. """ FOREGROUND_LOCK = BooleanProperty(False) """ If `True` it will require the foreground lock on windows. :attr:`FOREGROUND_LOCK` is a :class:`~kivy.properties.BooleanProperty` and defaults to `False`. """ KV_FILES = ListProperty() """ List of KV files under management for auto reloader. :attr:`KV_FILES` is a :class:`~kivy.properties.ListProperty` and defaults to `[]`. """ KV_DIRS = ListProperty() """ List of managed KV directories for autoloader. :attr:`KV_DIRS` is a :class:`~kivy.properties.ListProperty` and defaults to `[]`. """ AUTORELOADER_PATHS = ListProperty([(".", {"recursive": True})]) """ List of path to watch for auto reloading. :attr:`AUTORELOADER_PATHS` is a :class:`~kivy.properties.ListProperty` and defaults to `([(".", {"recursive": True})]`. """ AUTORELOADER_IGNORE_PATTERNS = ListProperty(["*.pyc", "*__pycache__*"]) """ List of extensions to ignore. :attr:`AUTORELOADER_IGNORE_PATTERNS` is a :class:`~kivy.properties.ListProperty` and defaults to `['*.pyc', '*__pycache__*']`. """ CLASSES = DictProperty() """ Factory classes managed by hotreload. :attr:`CLASSES` is a :class:`~kivy.properties.DictProperty` and defaults to `{}`. """ IDLE_DETECTION = BooleanProperty(False) """ Idle detection (if True, event on_idle/on_wakeup will be fired). Rearming idle can also be done with `rearm_idle()`. :attr:`IDLE_DETECTION` is a :class:`~kivy.properties.BooleanProperty` and defaults to `False`. """ IDLE_TIMEOUT = NumericProperty(60) """ Default idle timeout. :attr:`IDLE_TIMEOUT` is a :class:`~kivy.properties.NumericProperty` and defaults to `60`. """ RAISE_ERROR = BooleanProperty(True) """ Raise error. When the `DEBUG` is activated, it will raise any error instead of showing it on the screen. If you still want to show the error when not in `DEBUG`, put this to `False`. :attr:`RAISE_ERROR` is a :class:`~kivy.properties.BooleanProperty` and defaults to `True`. """ __events__ = ["on_idle", "on_wakeup"] def build(self): if self.DEBUG: Logger.info("{}: Debug mode activated".format(self.appname)) self.enable_autoreload() self.patch_builder() self.bind_key(32, self.rebuild) if self.FOREGROUND_LOCK: self.prepare_foreground_lock() self.state = None self.approot = None self.root = self.get_root() self.rebuild(first=True) if self.IDLE_DETECTION: self.install_idle(timeout=self.IDLE_TIMEOUT) return super().build() def get_root(self): """ Return a root widget, that will contains your application. It should not be your application widget itself, as it may be destroyed and recreated from scratch when reloading. By default, it returns a RelativeLayout, but it could be a Viewport. """ return Factory.RelativeLayout() def get_root_path(self): """Return the root file path.""" return realpath(os.getcwd()) def build_app(self, first=False): """ Must return your application widget. If `first` is set, it means that will be your first time ever that the application is built. Act according to it. """ raise NotImplementedError() def unload_app_dependencies(self): """ Called when all the application dependencies must be unloaded. Usually happen before a reload """ for path_to_kv_file in self.KV_FILES: path_to_kv_file = realpath(path_to_kv_file) Builder.unload_file(path_to_kv_file) for name, module in self.CLASSES.items(): Factory.unregister(name) for path in self.KV_DIRS: for path_to_dir, dirs, files in os.walk(path): for name_file in files: if os.path.splitext(name_file)[1] == ".kv": path_to_kv_file = os.path.join(path_to_dir, name_file) Builder.unload_file(path_to_kv_file) def load_app_dependencies(self): """ Load all the application dependencies. This is called before rebuild. """ for path_to_kv_file in self.KV_FILES: path_to_kv_file = realpath(path_to_kv_file) Builder.load_file(path_to_kv_file) for name, module in self.CLASSES.items(): Factory.register(name, module=module) for path in self.KV_DIRS: for path_to_dir, dirs, files in os.walk(path): for name_file in files: if os.path.splitext(name_file)[1] == ".kv": path_to_kv_file = os.path.join(path_to_dir, name_file) Builder.load_file(path_to_kv_file) def rebuild(self, *args, **kwargs): print("{}: Rebuild the application".format(self.appname)) first = kwargs.get("first", False) try: if not first: self.unload_app_dependencies() # In case the loading fail in the middle of building a widget # there will be existing rules context that will break later # instanciation. # Just clean it. Builder.rulectx = {} self.load_app_dependencies() self.set_widget(None) self.approot = self.build_app() self.set_widget(self.approot) self.apply_state(self.state) except Exception as exc: import traceback Logger.exception("{}: Error when building app".format( self.appname)) self.set_error(repr(exc), traceback.format_exc()) if not self.DEBUG and self.RAISE_ERROR: raise @mainthread def set_error(self, exc, tb=None): print(tb) from kivy.core.window import Window from kivy.utils import get_color_from_hex Window.clearcolor = get_color_from_hex("#e50000") scroll = Factory.ScrollView(scroll_y=0) lbl = Factory.Label( text_size=(Window.width - 100, None), size_hint_y=None, text="{}\n\n{}".format(exc, tb or ""), ) lbl.bind(texture_size=lbl.setter("size")) scroll.add_widget(lbl) self.set_widget(scroll) def bind_key(self, key, callback): """Bind a key (keycode) to a callback (cannot be unbind).""" from kivy.core.window import Window def _on_keyboard(window, keycode, *args): if key == keycode: return callback() Window.bind(on_keyboard=_on_keyboard) @property def appname(self): """Return the name of the application class.""" return self.__class__.__name__ def enable_autoreload(self): """ Enable autoreload manually. It is activated automatically if "DEBUG" exists in environ. It requires the `watchdog` module. """ try: from watchdog.events import FileSystemEventHandler from watchdog.observers import Observer except ImportError: Logger.warn("{}: Autoreloader is missing watchdog".format( self.appname)) return Logger.info("{}: Autoreloader activated".format(self.appname)) rootpath = self.get_root_path() self.w_handler = handler = FileSystemEventHandler() handler.dispatch = self._reload_from_watchdog self._observer = observer = Observer() for path in self.AUTORELOADER_PATHS: options = {"recursive": True} if isinstance(path, (tuple, list)): path, options = path observer.schedule(handler, join(rootpath, path), **options) observer.start() def prepare_foreground_lock(self): """ Try forcing app to front permanently to avoid windows pop ups and notifications etc.app. Requires fake full screen and borderless. .. note:: This function is called automatically if `FOREGROUND_LOCK` is set """ try: import ctypes LSFW_LOCK = 1 ctypes.windll.user32.LockSetForegroundWindow(LSFW_LOCK) Logger.info("App: Foreground lock activated") except Exception: Logger.warn("App: No foreground lock available") def set_widget(self, wid): """ Clear the root container, and set the new approot widget to `wid`. """ self.root.clear_widgets() self.approot = wid if wid is None: return self.root.add_widget(self.approot) try: wid.do_layout() except Exception: pass # State management. def apply_state(self, state): """Whatever the current state is, reapply the current state.""" # Idle management leave. def install_idle(self, timeout=60): """ Install the idle detector. Default timeout is 60s. Once installed, it will check every second if the idle timer expired. The timer can be rearm using :func:`rearm_idle`. """ if monotonic is None: Logger.exception( "{}: Cannot use idle detector, monotonic is missing".format( self.appname)) self.idle_timer = None self.idle_timeout = timeout Logger.info("{}: Install idle detector, {} seconds".format( self.appname, timeout)) Clock.schedule_interval(self._check_idle, 1) self.root.bind(on_touch_down=self.rearm_idle, on_touch_up=self.rearm_idle) def rearm_idle(self, *args): """Rearm the idle timer.""" if not hasattr(self, "idle_timer"): return if self.idle_timer is None: self.dispatch("on_wakeup") self.idle_timer = monotonic() # Internals. def patch_builder(self): Builder.orig_load_string = Builder.load_string Builder.load_string = self._builder_load_string def on_idle(self, *args): """Event fired when the application enter the idle mode.""" def on_wakeup(self, *args): """Event fired when the application leaves idle mode.""" @mainthread def _reload_from_watchdog(self, event): from watchdog.events import FileModifiedEvent if not isinstance(event, FileModifiedEvent): return for pat in self.AUTORELOADER_IGNORE_PATTERNS: if fnmatch(event.src_path, pat): return if event.src_path.endswith(".py"): # source changed, reload it try: Builder.unload_file(event.src_path) self._reload_py(event.src_path) except Exception as e: import traceback self.set_error(repr(e), traceback.format_exc()) return Clock.unschedule(self.rebuild) Clock.schedule_once(self.rebuild, 0.1) def _builder_load_string(self, string, **kwargs): if "filename" not in kwargs: from inspect import getframeinfo, stack caller = getframeinfo(stack()[1][0]) kwargs["filename"] = caller.filename return Builder.orig_load_string(string, **kwargs) def _check_idle(self, *args): if not hasattr(self, "idle_timer"): return if self.idle_timer is None: return if monotonic() - self.idle_timer > self.idle_timeout: self.idle_timer = None self.dispatch("on_idle") def _reload_py(self, filename): # We don't have dependency graph yet, so if the module actually exists # reload it. filename = realpath(filename) # Check if it's our own application file. try: mod = sys.modules[self.__class__.__module__] mod_filename = realpath(mod.__file__) except Exception: mod_filename = None # Detect if it's the application class // main. if mod_filename == filename: return self._restart_app(mod) module = self._filename_to_module(filename) if module in sys.modules: Logger.debug("{}: Module exist, reload it".format(self.appname)) Factory.unregister_from_filename(filename) self._unregister_factory_from_module(module) reload(sys.modules[module]) def _unregister_factory_from_module(self, module): # Check module directly. to_remove = [ x for x in Factory.classes if Factory.classes[x]["module"] == module ] # Check class name. for x in Factory.classes: cls = Factory.classes[x]["cls"] if not cls: continue if getattr(cls, "__module__", None) == module: to_remove.append(x) for name in set(to_remove): del Factory.classes[name] def _filename_to_module(self, filename): orig_filename = filename rootpath = self.get_root_path() if filename.startswith(rootpath): filename = filename[len(rootpath):] if filename.startswith("/"): filename = filename[1:] module = filename[:-3].replace("/", ".") Logger.debug("{}: Translated {} to {}".format(self.appname, orig_filename, module)) return module def _restart_app(self, mod): _has_execv = sys.platform != "win32" cmd = [sys.executable] + original_argv if not _has_execv: import subprocess subprocess.Popen(cmd) sys.exit(0) else: try: os.execv(sys.executable, cmd) except OSError: os.spawnv(os.P_NOWAIT, sys.executable, cmd) os._exit(0)
class VKeyboard(Scatter): ''' VKeyboard is an onscreen keyboard with multitouch support. Its layout is entirely customizable and you can switch between available layouts using a button in the bottom right of the widget. :Events: `on_key_down`: keycode, internal, modifiers Fired when the keyboard received a key down event (key press). `on_key_up`: keycode, internal, modifiers Fired when the keyboard received a key up event (key release). ''' target = ObjectProperty(None, allownone=True) '''Target widget associated with the VKeyboard. If set, it will be used to send keyboard events. If the VKeyboard mode is "free", it will also be used to set the initial position. :attr:`target` is an :class:`~kivy.properties.ObjectProperty` instance and defaults to None. ''' callback = ObjectProperty(None, allownone=True) '''Callback can be set to a function that will be called if the VKeyboard is closed by the user. :attr:`target` is an :class:`~kivy.properties.ObjectProperty` instance and defaults to None. ''' layout = StringProperty(None) '''Layout to use for the VKeyboard. By default, it will be the layout set in the configuration, according to the `keyboard_layout` in `[kivy]` section. .. versionchanged:: 1.8.0 If layout is a .json filename, it will loaded and added to the available_layouts. :attr:`layout` is a :class:`~kivy.properties.StringProperty` and defaults to None. ''' layout_path = StringProperty(default_layout_path) '''Path from which layouts are read. :attr:`layout` is a :class:`~kivy.properties.StringProperty` and defaults to :file:`<kivy_data_dir>/keyboards/` ''' available_layouts = DictProperty({}) '''Dictionary of all available layouts. Keys are the layout ID, and the value is the JSON (translated into a Python object). :attr:`available_layouts` is a :class:`~kivy.properties.DictProperty` and defaults to {}. ''' docked = BooleanProperty(False) '''Indicate whether the VKeyboard is docked on the screen or not. If you change it, you must manually call :meth:`setup_mode` otherwise it will have no impact. If the VKeyboard is created by the Window, the docked mode will be automatically set by the configuration, using the `keyboard_mode` token in `[kivy]` section. :attr:`docked` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' margin_hint = ListProperty([.05, .06, .05, .06]) '''Margin hint, used as spacing between keyboard background and keys content. The margin is composed of four values, between 0 and 1:: margin_hint = [top, right, bottom, left] The margin hints will be multiplied by width and height, according to their position. :attr:`margin_hint` is a :class:`~kivy.properties.ListProperty` and defaults to [.05, .06, .05, .06] ''' key_margin = ListProperty([2, 2, 2, 2]) '''Key margin, used to create space between keys. The margin is composed of four values, in pixels:: key_margin = [top, right, bottom, left] :attr:`key_margin` is a :class:`~kivy.properties.ListProperty` and defaults to [2, 2, 2, 2] ''' font_size = NumericProperty(20.) '''font_size, specifies the size of the text on the virtual keyboard keys. It should be kept within limits to ensure the text does not extend beyond the bounds of the key or become too small to read. .. versionadded:: 1.10.0 :attr:`font_size` is a :class:`~kivy.properties.NumericProperty` and defaults to 20. ''' 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` is 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 the 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` is 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` is 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` is 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_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 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 separate 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 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] z = Label(text=text, font_size=self.font_size, pos=pos, size=size, font_name=self.font_name) self.add_widget(z) 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 Widget(WidgetBase): '''Widget class. See module documentation for more information. :Events: `on_touch_down`: Fired when a new touch event occurs `on_touch_move`: Fired when an existing touch moves `on_touch_up`: Fired when an existing touch disappears .. warning:: Adding a `__del__` method to a class derived from Widget with Python prior to 3.4 will disable automatic garbage collection for instances of that class. This is because the Widget class creates reference cycles, thereby `preventing garbage collection <https://docs.python.org/2/library/gc.html#gc.garbage>`_. .. versionchanged:: 1.0.9 Everything related to event properties has been moved to the :class:`~kivy.event.EventDispatcher`. Event properties can now be used when contructing a simple class without subclassing :class:`Widget`. .. versionchanged:: 1.5.0 The constructor now accepts on_* arguments to automatically bind callbacks to properties or events, as in the Kv language. ''' __metaclass__ = WidgetMetaclass __events__ = ('on_touch_down', 'on_touch_move', 'on_touch_up') _proxy_ref = None def __init__(self, **kwargs): # Before doing anything, ensure the windows exist. EventLoop.ensure_window() # Assign the default context of the widget creation. if not hasattr(self, '_context'): self._context = get_current_context() no_builder = '__no_builder' in kwargs if no_builder: del kwargs['__no_builder'] on_args = {k: v for k, v in kwargs.items() if k[:3] == 'on_'} for key in on_args: del kwargs[key] super(Widget, self).__init__(**kwargs) # Create the default canvas if it does not exist. if self.canvas is None: self.canvas = Canvas(opacity=self.opacity) # Apply all the styles. if not no_builder: #current_root = Builder.idmap.get('root') #Builder.idmap['root'] = self Builder.apply(self) #if current_root is not None: # Builder.idmap['root'] = current_root #else: # Builder.idmap.pop('root') # Bind all the events. if on_args: self.bind(**on_args) @property def proxy_ref(self): '''Return a proxy reference to the widget, i.e. without creating a reference to the widget. See `weakref.proxy <http://docs.python.org/2/library/weakref.html?highlight\ =proxy#weakref.proxy>`_ for more information. .. versionadded:: 1.7.2 ''' _proxy_ref = self._proxy_ref if _proxy_ref is not None: return _proxy_ref f = partial(_widget_destructor, self.uid) self._proxy_ref = _proxy_ref = WeakProxy(self, f) # Only f should be enough here, but it appears that is a very # specific case, the proxy destructor is not called if both f and # _proxy_ref are not together in a tuple. _widget_destructors[self.uid] = (f, _proxy_ref) return _proxy_ref def __hash__(self): return id(self) @property def __self__(self): return self # # Collision # def collide_point(self, x, y): '''Check if a point (x, y) is inside the widget's axis aligned bounding box. :Parameters: `x`: numeric X position of the point (in window coordinates) `y`: numeric Y position of the point (in window coordinates) :Returns: bool, True if the point is inside the bounding box. .. code-block:: python >>> Widget(pos=(10, 10), size=(50, 50)).collide_point(40, 40) True ''' return self.x <= x <= self.right and self.y <= y <= self.top def collide_widget(self, wid): '''Check if the other widget collides with this widget. Performs an axis-aligned bounding box intersection test by default. :Parameters: `wid`: :class:`Widget` class Widget to collide with. :Returns: bool, True if the other widget collides with this widget. .. code-block:: python >>> wid = Widget(size=(50, 50)) >>> wid2 = Widget(size=(50, 50), pos=(25, 25)) >>> wid.collide_widget(wid2) True >>> wid2.pos = (55, 55) >>> wid.collide_widget(wid2) False ''' if self.right < wid.x: return False if self.x > wid.right: return False if self.top < wid.y: return False if self.y > wid.top: return False return True # # Default event handlers # def on_touch_down(self, touch): '''Receive a touch down event. :Parameters: `touch`: :class:`~kivy.input.motionevent.MotionEvent` class Touch received. The touch is in parent coordinates. See :mod:`~kivy.uix.relativelayout` for a discussion on coordinate systems. :Returns: bool. If True, the dispatching of the touch event will stop. ''' if self.disabled and self.collide_point(*touch.pos): return True for child in self.children[:]: if child.dispatch('on_touch_down', touch): return True def on_touch_move(self, touch): '''Receive a touch move event. The touch is in parent coordinates. See :meth:`on_touch_down` for more information. ''' if self.disabled: return for child in self.children[:]: if child.dispatch('on_touch_move', touch): return True def on_touch_up(self, touch): '''Receive a touch up event. The touch is in parent coordinates. See :meth:`on_touch_down` for more information. ''' if self.disabled: return for child in self.children[:]: if child.dispatch('on_touch_up', touch): return True def on_disabled(self, instance, value): for child in self.children: child.disabled = value # # Tree management # def add_widget(self, widget, index=0, canvas=None): '''Add a new widget as a child of this widget. :Parameters: `widget`: :class:`Widget` Widget to add to our list of children. `index`: int, defaults to 0 Index to insert the widget in the list. .. versionadded:: 1.0.5 `canvas`: str, defaults to None Canvas to add widget's canvas to. Can be 'before', 'after' or None for the default canvas. .. versionadded:: 1.9.0 .. code-block:: python >>> from kivy.uix.button import Button >>> from kivy.uix.slider import Slider >>> root = Widget() >>> root.add_widget(Button()) >>> slider = Slider() >>> root.add_widget(slider) ''' if not isinstance(widget, Widget): raise WidgetException( 'add_widget() can be used only with instances' ' of the Widget class.') widget = widget.__self__ if widget is self: raise WidgetException( 'Widget instances cannot be added to themselves.') parent = widget.parent # Check if the widget is already a child of another widget. if parent: raise WidgetException('Cannot add %r, it already has a parent %r' % (widget, parent)) widget.parent = parent = self # Child will be disabled if added to a disabled parent. if parent.disabled: widget.disabled = True canvas = self.canvas.before if canvas == 'before' else \ self.canvas.after if canvas == 'after' else self.canvas if index == 0 or len(self.children) == 0: self.children.insert(0, widget) canvas.add(widget.canvas) else: canvas = self.canvas children = self.children if index >= len(children): index = len(children) next_index = 0 else: next_child = children[index] next_index = canvas.indexof(next_child.canvas) if next_index == -1: next_index = canvas.length() else: next_index += 1 children.insert(index, widget) # We never want to insert widget _before_ canvas.before. if next_index == 0 and canvas.has_before: next_index = 1 canvas.insert(next_index, widget.canvas) def remove_widget(self, widget): '''Remove a widget from the children of this widget. :Parameters: `widget`: :class:`Widget` Widget to remove from our children list. .. code-block:: python >>> from kivy.uix.button import Button >>> root = Widget() >>> button = Button() >>> root.add_widget(button) >>> root.remove_widget(button) ''' if widget not in self.children: return self.children.remove(widget) if widget.canvas in self.canvas.children: self.canvas.remove(widget.canvas) elif widget.canvas in self.canvas.after.children: self.canvas.after.remove(widget.canvas) elif widget.canvas in self.canvas.before.children: self.canvas.before.remove(widget.canvas) widget.parent = None def clear_widgets(self, children=None): '''Remove all widgets added to this widget. .. versionchanged:: 1.8.0 `children` argument can be used to select the children we want to remove. It should be a list of children (or filtered list) of the current widget. ''' if not children: children = self.children remove_widget = self.remove_widget for child in children[:]: remove_widget(child) def export_to_png(self, filename, *args): '''Saves an image of the widget and its children in png format at the specified filename. Works by removing the widget canvas from its parent, rendering to an :class:`~kivy.graphics.fbo.Fbo`, and calling :meth:`~kivy.graphics.texture.Texture.save`. .. note:: The image includes only this widget and its children. If you want to include widgets elsewhere in the tree, you must call :meth:`~Widget.export_to_png` from their common parent, or use :meth:`~kivy.core.window.Window.screenshot` to capture the whole window. .. note:: The image will be saved in png format, you should include the extension in your filename. .. versionadded:: 1.9.0 ''' if self.parent is not None: canvas_parent_index = self.parent.canvas.indexof(self.canvas) self.parent.canvas.remove(self.canvas) fbo = Fbo(size=self.size, with_stencilbuffer=True) with fbo: ClearColor(0, 0, 0, 1) ClearBuffers() Scale(1, -1, 1) Translate(-self.x, -self.y - self.height, 0) fbo.add(self.canvas) fbo.draw() fbo.texture.save(filename, flipped=False) fbo.remove(self.canvas) if self.parent is not None: self.parent.canvas.insert(canvas_parent_index, self.canvas) return True def get_root_window(self): '''Return the root window. :Returns: Instance of the root window. Can be a :class:`~kivy.core.window.WindowBase` or :class:`Widget`. ''' if self.parent: return self.parent.get_root_window() def get_parent_window(self): '''Return the parent window. :Returns: Instance of the parent window. Can be a :class:`~kivy.core.window.WindowBase` or :class:`Widget`. ''' if self.parent: return self.parent.get_parent_window() def _walk(self, restrict=False, loopback=False, index=None): # We pass index only when we are going on the parent # so don't yield the parent as well. if index is None: index = len(self.children) yield self for child in reversed(self.children[:index]): for walk_child in child._walk(restrict=True): yield walk_child # If we want to continue with our parent, just do it. if not restrict: parent = self.parent try: if parent is None or not isinstance(parent, Widget): raise ValueError index = parent.children.index(self) except ValueError: # Self is root, if we want to loopback from the first element: if not loopback: return # If we started with root (i.e. index==None), then we have to # start from root again, so we return self again. Otherwise, we # never returned it, so return it now starting with it. parent = self index = None for walk_child in parent._walk(loopback=loopback, index=index): yield walk_child def walk(self, restrict=False, loopback=False): ''' Iterator that walks the widget tree starting with this widget and goes forward returning widgets in the order in which layouts display them. :Parameters: `restrict`: bool, defaults to False If True, it will only iterate through the widget and its children (or children of its children etc.). Defaults to False. `loopback`: bool, defaults to False If True, when the last widget in the tree is reached, it'll loop back to the uppermost root and start walking until we hit this widget again. Naturally, it can only loop back when `restrict` is False. Defaults to False. :return: A generator that walks the tree, returning widgets in the forward layout order. For example, given a tree with the following structure:: GridLayout: Button BoxLayout: id: box Widget Button Widget walking this tree: .. code-block:: python >>> # Call walk on box with loopback True, and restrict False >>> [type(widget) for widget in box.walk(loopback=True)] [<class 'BoxLayout'>, <class 'Widget'>, <class 'Button'>, <class 'Widget'>, <class 'GridLayout'>, <class 'Button'>] >>> # Now with loopback False, and restrict False >>> [type(widget) for widget in box.walk()] [<class 'BoxLayout'>, <class 'Widget'>, <class 'Button'>, <class 'Widget'>] >>> # Now with restrict True >>> [type(widget) for widget in box.walk(restrict=True)] [<class 'BoxLayout'>, <class 'Widget'>, <class 'Button'>] .. versionadded:: 1.9.0 ''' gen = self._walk(restrict, loopback) yield next(gen) for node in gen: if node is self: return yield node def _walk_reverse(self, loopback=False, go_up=False): # process is walk up level, walk down its children tree, then walk up # next level etc. # default just walk down the children tree root = self index = 0 # we need to go up a level before walking tree if go_up: root = self.parent try: if root is None or not isinstance(root, Widget): raise ValueError index = root.children.index(self) + 1 except ValueError: if not loopback: return index = 0 go_up = False root = self # now walk children tree starting with last-most child for child in islice(root.children, index, None): for walk_child in child._walk_reverse(loopback=loopback): yield walk_child # we need to return ourself last, in all cases yield root # if going up, continue walking up the parent tree if go_up: for walk_child in root._walk_reverse(loopback=loopback, go_up=go_up): yield walk_child def walk_reverse(self, loopback=False): ''' Iterator that walks the widget tree backwards starting with the widget before this, and going backwards returning widgets in the reverse order in which layouts display them. This walks in the opposite direction of :meth:`walk`, so a list of the tree generated with :meth:`walk` will be in reverse order compared to the list generated with this, provided `loopback` is True. :Parameters: `loopback`: bool, defaults to False If True, when the uppermost root in the tree is reached, it'll loop back to the last widget and start walking back until after we hit widget again. Defaults to False. :return: A generator that walks the tree, returning widgets in the reverse layout order. For example, given a tree with the following structure:: GridLayout: Button BoxLayout: id: box Widget Button Widget walking this tree: .. code-block:: python >>> # Call walk on box with loopback True >>> [type(widget) for widget in box.walk_reverse(loopback=True)] [<class 'Button'>, <class 'GridLayout'>, <class 'Widget'>, <class 'Button'>, <class 'Widget'>, <class 'BoxLayout'>] >>> # Now with loopback False >>> [type(widget) for widget in box.walk_reverse()] [<class 'Button'>, <class 'GridLayout'>] >>> forward = [w for w in box.walk(loopback=True)] >>> backward = [w for w in box.walk_reverse(loopback=True)] >>> forward == backward[::-1] True .. versionadded:: 1.9.0 ''' for node in self._walk_reverse(loopback=loopback, go_up=True): yield node if node is self: return def to_widget(self, x, y, relative=False): '''Convert the given coordinate from window to local widget coordinates. See :mod:`~kivy.uix.relativelayout` for details on the coordinate systems. ''' if self.parent: x, y = self.parent.to_widget(x, y) return self.to_local(x, y, relative=relative) def to_window(self, x, y, initial=True, relative=False): '''Transform local coordinates to window coordinates. See :mod:`~kivy.uix.relativelayout` for details on the coordinate systems. ''' if not initial: x, y = self.to_parent(x, y, relative=relative) if self.parent: return self.parent.to_window(x, y, initial=False, relative=relative) return (x, y) def to_parent(self, x, y, relative=False): '''Transform local coordinates to parent coordinates. See :mod:`~kivy.uix.relativelayout` for details on the coordinate systems. :Parameters: `relative`: bool, defaults to False Change to True if you want to translate relative positions from a widget to its parent coordinates. ''' if relative: return (x + self.x, y + self.y) return (x, y) def to_local(self, x, y, relative=False): '''Transform parent coordinates to local coordinates. See :mod:`~kivy.uix.relativelayout` for details on the coordinate systems. :Parameters: `relative`: bool, defaults to False Change to True if you want to translate coordinates to relative widget coordinates. ''' if relative: return (x - self.x, y - self.y) return (x, y) def _apply_transform(self, m, pos=None): if self.parent: x, y = self.parent.to_widget(relative=True, *self.to_window(*(pos or self.pos))) m.translate(x, y, 0) m = self.parent._apply_transform(m) if self.parent else m return m def get_window_matrix(self, x=0, y=0): '''Calculate the transformation matrix to convert between window and widget coordinates. :Parameters: `x`: float, defaults to 0 Translates the matrix on the x axis. `y`: float, defaults to 0 Translates the matrix on the y axis. ''' m = Matrix() m.translate(x, y, 0) m = self._apply_transform(m) return m x = NumericProperty(0) '''X position of the widget. :attr:`x` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' y = NumericProperty(0) '''Y position of the widget. :attr:`y` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' width = NumericProperty(100) '''Width of the widget. :attr:`width` is a :class:`~kivy.properties.NumericProperty` and defaults to 100. .. warning:: Keep in mind that the `width` property is subject to layout logic and that this has not yet happened at the time of the widget's `__init__` method. ''' height = NumericProperty(100) '''Height of the widget. :attr:`height` is a :class:`~kivy.properties.NumericProperty` and defaults to 100. .. warning:: Keep in mind that the `height` property is subject to layout logic and that this has not yet happened at the time of the widget's `__init__` method. ''' pos = ReferenceListProperty(x, y) '''Position of the widget. :attr:`pos` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`x`, :attr:`y`) properties. ''' size = ReferenceListProperty(width, height) '''Size of the widget. :attr:`size` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`width`, :attr:`height`) properties. ''' def get_right(self): return self.x + self.width def set_right(self, value): self.x = value - self.width right = AliasProperty(get_right, set_right, bind=('x', 'width')) '''Right position of the widget. :attr:`right` is an :class:`~kivy.properties.AliasProperty` of (:attr:`x` + :attr:`width`). ''' def get_top(self): return self.y + self.height def set_top(self, value): self.y = value - self.height top = AliasProperty(get_top, set_top, bind=('y', 'height')) '''Top position of the widget. :attr:`top` is an :class:`~kivy.properties.AliasProperty` of (:attr:`y` + :attr:`height`). ''' def get_center_x(self): return self.x + self.width / 2. def set_center_x(self, value): self.x = value - self.width / 2. center_x = AliasProperty(get_center_x, set_center_x, bind=('x', 'width')) '''X center position of the widget. :attr:`center_x` is an :class:`~kivy.properties.AliasProperty` of (:attr:`x` + :attr:`width` / 2.). ''' def get_center_y(self): return self.y + self.height / 2. def set_center_y(self, value): self.y = value - self.height / 2. center_y = AliasProperty(get_center_y, set_center_y, bind=('y', 'height')) '''Y center position of the widget. :attr:`center_y` is an :class:`~kivy.properties.AliasProperty` of (:attr:`y` + :attr:`height` / 2.). ''' center = ReferenceListProperty(center_x, center_y) '''Center position of the widget. :attr:`center` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`center_x`, :attr:`center_y`) properties. ''' cls = ListProperty([]) '''Class of the widget, used for styling. ''' id = StringProperty(None, allownone=True) '''Unique identifier of the widget in the tree. :attr:`id` is a :class:`~kivy.properties.StringProperty` and defaults to None. .. warning:: If the :attr:`id` is already used in the tree, an exception will be raised. ''' children = ListProperty([]) '''List of children of this widget. :attr:`children` is a :class:`~kivy.properties.ListProperty` and defaults to an empty list. Use :meth:`add_widget` and :meth:`remove_widget` for manipulating the children list. Don't manipulate the children list directly unless you know what you are doing. ''' parent = ObjectProperty(None, allownone=True) '''Parent of this widget. :attr:`parent` is an :class:`~kivy.properties.ObjectProperty` and defaults to None. The parent of a widget is set when the widget is added to another widget and unset when the widget is removed from its parent. ''' size_hint_x = NumericProperty(1, allownone=True) '''X size hint. Represents how much space the widget should use in the direction of the X axis relative to its parent's width. Only the :class:`~kivy.uix.layout.Layout` and :class:`~kivy.core.window.Window` classes make use of the hint. The size_hint is used by layouts for two purposes: - When the layout considers widgets on their own rather than in relation to its other children, the size_hint_x is a direct proportion of the parent width, normally between 0.0 and 1.0. For instance, a widget with ``size_hint_x=0.5`` in a vertical BoxLayout will take up half the BoxLayout's width, or a widget in a FloatLayout with ``size_hint_x=0.2`` will take up 20% of the FloatLayout width. If the size_hint is greater than 1, the widget will be wider than the parent. - When multiple widgets can share a row of a layout, such as in a horizontal BoxLayout, their widths will be their size_hint_x as a fraction of the sum of widget size_hints. For instance, if the size_hint_xs are (0.5, 1.0, 0.5), the first widget will have a width of 25% of the parent width. :attr:`size_hint_x` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. ''' size_hint_y = NumericProperty(1, allownone=True) '''Y size hint. :attr:`size_hint_y` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. See :attr:`size_hint_x` for more information, but with widths and heights swapped. ''' size_hint = ReferenceListProperty(size_hint_x, size_hint_y) '''Size hint. :attr:`size_hint` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`size_hint_x`, :attr:`size_hint_y`) properties. See :attr:`size_hint_x` for more information. ''' pos_hint = ObjectProperty({}) '''Position hint. This property allows you to set the position of the widget inside its parent layout, in percent (similar to size_hint). For example, if you want to set the top of the widget to be at 90% height of its parent layout, you can write:: widget = Widget(pos_hint={'top': 0.9}) The keys 'x', 'right' and 'center_x' will use the parent width. The keys 'y', 'top' and 'center_y' will use the parent height. See :doc:`api-kivy.uix.floatlayout` for further reference. .. note:: :attr:`pos_hint` is not used by all layouts. Check the documentation of the layout in question to see if it supports pos_hint. :attr:`pos_hint` is an :class:`~kivy.properties.ObjectProperty` containing a dict. ''' ids = DictProperty({}) '''This is a dictionary of ids defined in your kv language. This will only be populated if you use ids in your kv language code. .. versionadded:: 1.7.0 :attr:`ids` is a :class:`~kivy.properties.DictProperty` and defaults to an empty dict {}. The :attr:`ids` are populated for each root level widget definition. For example:: # in kv <MyWidget@Widget>: id: my_widget Label: id: label_widget Widget: id: inner_widget Label: id: inner_label TextInput: id: text_input OtherWidget: id: other_widget <OtherWidget@Widget> id: other_widget Label: id: other_label TextInput: id: other_textinput Then, in python: .. code-block:: python >>> widget = MyWidget() >>> print(widget.ids) {'other_widget': <weakproxy at 041CFED0 to OtherWidget at 041BEC38>, 'inner_widget': <weakproxy at 04137EA0 to Widget at 04138228>, 'inner_label': <weakproxy at 04143540 to Label at 04138260>, 'label_widget': <weakproxy at 04137B70 to Label at 040F97A0>, 'text_input': <weakproxy at 041BB5D0 to TextInput at 041BEC00>} >>> print(widget.ids['other_widget'].ids) {'other_textinput': <weakproxy at 041DBB40 to TextInput at 041BEF48>, 'other_label': <weakproxy at 041DB570 to Label at 041BEEA0>} >>> print(widget.ids['label_widget'].ids) {} ''' opacity = NumericProperty(1.0) '''Opacity of the widget and all its children. .. versionadded:: 1.4.1 The opacity attribute controls the opacity of the widget and its children. Be careful, it's a cumulative attribute: the value is multiplied by the current global opacity and the result is applied to the current context color. For example, if the parent has an opacity of 0.5 and a child has an opacity of 0.2, the real opacity of the child will be 0.5 * 0.2 = 0.1. Then, the opacity is applied by the shader as: .. code-block:: python frag_color = color * vec4(1.0, 1.0, 1.0, opacity); :attr:`opacity` is a :class:`~kivy.properties.NumericProperty` and defaults to 1.0. ''' def on_opacity(self, instance, value): canvas = self.canvas if canvas is not None: canvas.opacity = value canvas = None '''Canvas of the widget. The canvas is a graphics object that contains all the drawing instructions for the graphical representation of the widget. There are no general properties for the Widget class, such as background color, to keep the design simple and lean. Some derived classes, such as Button, do add such convenience properties but generally the developer is responsible for implementing the graphics representation for a custom widget from the ground up. See the derived widget classes for patterns to follow and extend. See :class:`~kivy.graphics.Canvas` for more information about the usage. ''' disabled = BooleanProperty(False) '''Indicates whether this widget can interact with input or not.
class CompletedTask(ToggleButton): _source = ObjectProperty() _text = StringProperty() _data = DictProperty()
class ThemeManager(EventDispatcher): primary_palette = OptionProperty("Blue", options=palette) """ The name of the color scheme that the application will use. All major `material` components will have the color of the specified color theme. Available options are: `'Red'`, `'Pink'`, `'Purple'`, `'DeepPurple'`, `'Indigo'`, `'Blue'`, `'LightBlue'`, `'Cyan'`, `'Teal'`, `'Green'`, `'LightGreen'`, `'Lime'`, `'Yellow'`, `'Amber'`, `'Orange'`, `'DeepOrange'`, `'Brown'`, `'Gray'`, `'BlueGray'`. To change the color scheme of an application: .. code-block:: python from kivymd.app import MDApp from kivymd.uix.screen import MDScreen from kivymd.uix.button import MDRectangleFlatButton class MainApp(MDApp): def build(self): self.theme_cls.primary_palette = "Green" # "Purple", "Red" screen = MDScreen() screen.add_widget( MDRectangleFlatButton( text="Hello, World", pos_hint={"center_x": 0.5, "center_y": 0.5}, ) ) return screen MainApp().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/primary-palette.png :attr:`primary_palette` is an :class:`~kivy.properties.OptionProperty` and defaults to `'Blue'`. """ primary_hue = OptionProperty("500", options=hue) """ The color hue of the application. Available options are: `'50'`, `'100'`, `'200'`, `'300'`, `'400'`, `'500'`, `'600'`, `'700'`, `'800'`, `'900'`, `'A100'`, `'A200'`, `'A400'`, `'A700'`. To change the hue color scheme of an application: .. code-block:: python from kivymd.app import MDApp from kivymd.uix.screen import MDScreen from kivymd.uix.button import MDRectangleFlatButton class MainApp(MDApp): def build(self): self.theme_cls.primary_palette = "Green" # "Purple", "Red" self.theme_cls.primary_hue = "200" # "500" screen = MDScreen() screen.add_widget( MDRectangleFlatButton( text="Hello, World", pos_hint={"center_x": 0.5, "center_y": 0.5}, ) ) return screen MainApp().run() With a value of ``self.theme_cls.primary_hue = "500"``: .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/primary-palette.png With a value of ``self.theme_cls.primary_hue = "200"``: .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/primary-hue.png :attr:`primary_hue` is an :class:`~kivy.properties.OptionProperty` and defaults to `'500'`. """ primary_light_hue = OptionProperty("200", options=hue) """ Hue value for :attr:`primary_light`. :attr:`primary_light_hue` is an :class:`~kivy.properties.OptionProperty` and defaults to `'200'`. """ primary_dark_hue = OptionProperty("700", options=hue) """ Hue value for :attr:`primary_dark`. :attr:`primary_light_hue` is an :class:`~kivy.properties.OptionProperty` and defaults to `'700'`. """ def _get_primary_color(self) -> list: return get_color_from_hex( self.colors[self.primary_palette][self.primary_hue]) primary_color = AliasProperty(_get_primary_color, bind=("primary_palette", "primary_hue")) """ The color of the current application theme in ``rgba`` format. :attr:`primary_color` is an :class:`~kivy.properties.AliasProperty` that returns the value of the current application theme, property is readonly. """ def _get_primary_light(self) -> list: return get_color_from_hex( self.colors[self.primary_palette][self.primary_light_hue]) primary_light = AliasProperty(_get_primary_light, bind=("primary_palette", "primary_light_hue")) """ Colors of the current application color theme in ``rgba`` format (in lighter color). .. code-block:: python from kivy.lang import Builder from kivymd.app import MDApp KV = ''' MDScreen: MDRaisedButton: text: "primary_light" pos_hint: {"center_x": 0.5, "center_y": 0.7} md_bg_color: app.theme_cls.primary_light MDRaisedButton: text: "primary_color" pos_hint: {"center_x": 0.5, "center_y": 0.5} MDRaisedButton: text: "primary_dark" pos_hint: {"center_x": 0.5, "center_y": 0.3} md_bg_color: app.theme_cls.primary_dark ''' class MainApp(MDApp): def build(self): self.theme_cls.primary_palette = "Green" return Builder.load_string(KV) MainApp().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/primary-colors-light-dark.png :align: center :attr:`primary_light` is an :class:`~kivy.properties.AliasProperty` that returns the value of the current application theme (in lighter color), property is readonly. """ def _get_primary_dark(self) -> list: return get_color_from_hex( self.colors[self.primary_palette][self.primary_dark_hue]) primary_dark = AliasProperty(_get_primary_dark, bind=("primary_palette", "primary_dark_hue")) """ Colors of the current application color theme in ``rgba`` format (in darker color). :attr:`primary_dark` is an :class:`~kivy.properties.AliasProperty` that returns the value of the current application theme (in darker color), property is readonly. """ accent_palette = OptionProperty("Amber", options=palette) """ The application color palette used for items such as the tab indicator in the :attr:`MDTabsBar` class and so on... The image below shows the color schemes with the values ``self.theme_cls.accent_palette = 'Blue'``, ``Red'`` and ``Yellow'``: .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/accent-palette.png :attr:`accent_palette` is an :class:`~kivy.properties.OptionProperty` and defaults to `'Amber'`. """ accent_hue = OptionProperty("500", options=hue) """ Similar to :attr:`primary_hue`, but returns a value for :attr:`accent_palette`. :attr:`accent_hue` is an :class:`~kivy.properties.OptionProperty` and defaults to `'500'`. """ accent_light_hue = OptionProperty("200", options=hue) """ Hue value for :attr:`accent_light`. :attr:`accent_light_hue` is an :class:`~kivy.properties.OptionProperty` and defaults to `'200'`. """ accent_dark_hue = OptionProperty("700", options=hue) """ Hue value for :attr:`accent_dark`. :attr:`accent_dark_hue` is an :class:`~kivy.properties.OptionProperty` and defaults to `'700'`. """ def _get_accent_color(self) -> list: return get_color_from_hex( self.colors[self.accent_palette][self.accent_hue]) accent_color = AliasProperty(_get_accent_color, bind=["accent_palette", "accent_hue"]) """ Similar to :attr:`primary_color`, but returns a value for :attr:`accent_color`. :attr:`accent_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`accent_color`, property is readonly. """ def _get_accent_light(self) -> list: return get_color_from_hex( self.colors[self.accent_palette][self.accent_light_hue]) accent_light = AliasProperty(_get_accent_light, bind=["accent_palette", "accent_light_hue"]) """ Similar to :attr:`primary_light`, but returns a value for :attr:`accent_light`. :attr:`accent_light` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`accent_light`, property is readonly. """ def _get_accent_dark(self) -> list: return get_color_from_hex( self.colors[self.accent_palette][self.accent_dark_hue]) accent_dark = AliasProperty(_get_accent_dark, bind=["accent_palette", "accent_dark_hue"]) """ Similar to :attr:`primary_dark`, but returns a value for :attr:`accent_dark`. :attr:`accent_dark` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`accent_dark`, property is readonly. """ material_style = OptionProperty("M2", options=["M2", "M3"]) """ Material design style. Available options are: 'M2', 'M3'. .. versionadded:: 1.0.0 .. seealso:: `Material Design 2 <https://material.io/>`_ and `Material Design 3 <https://m3.material.io>`_ :attr:`material_style` is an :class:`~kivy.properties.OptionProperty` and defaults to `'M2'`. """ theme_style = OptionProperty("Light", options=["Light", "Dark"]) """ App theme style. .. code-block:: python from kivymd.app import MDApp from kivymd.uix.screen import MDScreen from kivymd.uix.button import MDRectangleFlatButton class MainApp(MDApp): def build(self): self.theme_cls.theme_style = "Dark" # "Light" screen = MDScreen() screen.add_widget( MDRectangleFlatButton( text="Hello, World", pos_hint={"center_x": 0.5, "center_y": 0.5}, ) ) return screen MainApp().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/theme-style.png :attr:`theme_style` is an :class:`~kivy.properties.OptionProperty` and defaults to `'Light'`. """ def _get_theme_style(self, opposite: bool) -> str: if opposite: return "Light" if self.theme_style == "Dark" else "Dark" else: return self.theme_style def _get_bg_darkest(self, opposite: bool = False) -> list: theme_style = self._get_theme_style(opposite) if theme_style == "Light": return get_color_from_hex(self.colors["Light"]["StatusBar"]) elif theme_style == "Dark": return get_color_from_hex(self.colors["Dark"]["StatusBar"]) bg_darkest = AliasProperty(_get_bg_darkest, bind=["theme_style"]) """ Similar to :attr:`bg_dark`, but the color values are a tone lower (darker) than :attr:`bg_dark`. .. code-block:: python KV = ''' MDBoxLayout: MDBoxLayout: md_bg_color: app.theme_cls.bg_light MDBoxLayout: md_bg_color: app.theme_cls.bg_normal MDBoxLayout: md_bg_color: app.theme_cls.bg_dark MDBoxLayout: md_bg_color: app.theme_cls.bg_darkest ''' from kivy.lang import Builder from kivymd.app import MDApp class MainApp(MDApp): def build(self): self.theme_cls.theme_style = "Dark" # "Light" return Builder.load_string(KV) MainApp().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bg-normal-dark-darkest.png :attr:`bg_darkest` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`bg_darkest`, property is readonly. """ def _get_op_bg_darkest(self) -> list: return self._get_bg_darkest(True) opposite_bg_darkest = AliasProperty(_get_op_bg_darkest, bind=["theme_style"]) """ The opposite value of color in the :attr:`bg_darkest`. :attr:`opposite_bg_darkest` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`opposite_bg_darkest`, property is readonly. """ def _get_bg_dark(self, opposite: bool = False) -> list: theme_style = self._get_theme_style(opposite) if theme_style == "Light": return get_color_from_hex(self.colors["Light"]["AppBar"]) elif theme_style == "Dark": return get_color_from_hex(self.colors["Dark"]["AppBar"]) bg_dark = AliasProperty(_get_bg_dark, bind=["theme_style"]) """ Similar to :attr:`bg_normal`, but the color values are one tone lower (darker) than :attr:`bg_normal`. :attr:`bg_dark` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`bg_dark`, property is readonly. """ def _get_op_bg_dark(self) -> list: return self._get_bg_dark(True) opposite_bg_dark = AliasProperty(_get_op_bg_dark, bind=["theme_style"]) """ The opposite value of color in the :attr:`bg_dark`. :attr:`opposite_bg_dark` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`opposite_bg_dark`, property is readonly. """ def _get_bg_normal(self, opposite: bool = False) -> list: theme_style = self._get_theme_style(opposite) if theme_style == "Light": return get_color_from_hex(self.colors["Light"]["Background"]) elif theme_style == "Dark": return get_color_from_hex(self.colors["Dark"]["Background"]) bg_normal = AliasProperty(_get_bg_normal, bind=["theme_style"]) """ Similar to :attr:`bg_light`, but the color values are one tone lower (darker) than :attr:`bg_light`. :attr:`bg_normal` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`bg_normal`, property is readonly. """ def _get_op_bg_normal(self) -> list: return self._get_bg_normal(True) opposite_bg_normal = AliasProperty(_get_op_bg_normal, bind=["theme_style"]) """ The opposite value of color in the :attr:`bg_normal`. :attr:`opposite_bg_normal` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`opposite_bg_normal`, property is readonly. """ def _get_bg_light(self, opposite: bool = False) -> list: theme_style = self._get_theme_style(opposite) if theme_style == "Light": return get_color_from_hex(self.colors["Light"]["CardsDialogs"]) elif theme_style == "Dark": return get_color_from_hex(self.colors["Dark"]["CardsDialogs"]) bg_light = AliasProperty(_get_bg_light, bind=["theme_style"]) """" Depending on the style of the theme (`'Dark'` or `'Light`') that the application uses, :attr:`bg_light` contains the color value in ``rgba`` format for the widgets background. :attr:`bg_light` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`bg_light`, property is readonly. """ def _get_op_bg_light(self) -> list: return self._get_bg_light(True) opposite_bg_light = AliasProperty(_get_op_bg_light, bind=["theme_style"]) """ The opposite value of color in the :attr:`bg_light`. :attr:`opposite_bg_light` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`opposite_bg_light`, property is readonly. """ def _get_divider_color(self, opposite: bool = False) -> list: theme_style = self._get_theme_style(opposite) if theme_style == "Light": color = get_color_from_hex("000000") elif theme_style == "Dark": color = get_color_from_hex("FFFFFF") color[3] = 0.12 return color divider_color = AliasProperty(_get_divider_color, bind=["theme_style"]) """ Color for dividing lines such as :class:`~kivymd.uix.card.MDSeparator`. :attr:`divider_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`divider_color`, property is readonly. """ def _get_op_divider_color(self) -> list: return self._get_divider_color(True) opposite_divider_color = AliasProperty(_get_op_divider_color, bind=["theme_style"]) """ The opposite value of color in the :attr:`divider_color`. :attr:`opposite_divider_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`opposite_divider_color`, property is readonly. """ def _get_disabled_primary_color(self, opposite: bool = False) -> list: theme_style = self._get_theme_style(opposite) lum = sum(self.primary_color[0:3]) / 3.0 if theme_style == "Light": a = 0.38 elif theme_style == "Dark": a = 0.50 return [lum, lum, lum, a] disabled_primary_color = AliasProperty(_get_disabled_primary_color, bind=["theme_style"]) """ The greyscale disabled version of the current application theme color in ``rgba`` format. .. versionadded:: 1.0.0 :attr:`disabled_primary_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`disabled_primary_color`, property is readonly. """ def _get_op_disabled_primary_color(self) -> list: return self._get_disabled_primary_color(True) opposite_disabled_primary_color = AliasProperty( _get_op_disabled_primary_color, bind=["theme_style"]) """ The opposite value of color in the :attr:`disabled_primary_color`. .. versionadded:: 1.0.0 :attr:`opposite_disabled_primary_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`opposite_disabled_primary_color`, property is readonly. """ def _get_text_color(self, opposite: bool = False) -> list: theme_style = self._get_theme_style(opposite) if theme_style == "Light": color = get_color_from_hex("000000") color[3] = 0.87 elif theme_style == "Dark": color = get_color_from_hex("FFFFFF") return color text_color = AliasProperty(_get_text_color, bind=["theme_style"]) """ Color of the text used in the :class:`~kivymd.uix.label.MDLabel`. :attr:`text_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`text_color`, property is readonly. """ def _get_op_text_color(self) -> list: return self._get_text_color(True) opposite_text_color = AliasProperty(_get_op_text_color, bind=["theme_style"]) """ The opposite value of color in the :attr:`text_color`. :attr:`opposite_text_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`opposite_text_color`, property is readonly. """ def _get_secondary_text_color(self, opposite: bool = False) -> list: theme_style = self._get_theme_style(opposite) if theme_style == "Light": color = get_color_from_hex("000000") color[3] = 0.54 elif theme_style == "Dark": color = get_color_from_hex("FFFFFF") color[3] = 0.70 return color secondary_text_color = AliasProperty(_get_secondary_text_color, bind=["theme_style"]) """ The color for the secondary text that is used in classes from the module :class:`~kivymd/uix/list.TwoLineListItem`. :attr:`secondary_text_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`secondary_text_color`, property is readonly. """ def _get_op_secondary_text_color(self) -> list: return self._get_secondary_text_color(True) opposite_secondary_text_color = AliasProperty(_get_op_secondary_text_color, bind=["theme_style"]) """ The opposite value of color in the :attr:`secondary_text_color`. :attr:`opposite_secondary_text_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`opposite_secondary_text_color`, property is readonly. """ def _get_icon_color(self, opposite: bool = False) -> list: theme_style = self._get_theme_style(opposite) if theme_style == "Light": color = get_color_from_hex("000000") color[3] = 0.54 elif theme_style == "Dark": color = get_color_from_hex("FFFFFF") return color icon_color = AliasProperty(_get_icon_color, bind=["theme_style"]) """ Color of the icon used in the :class:`~kivymd.uix.button.MDIconButton`. :attr:`icon_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`icon_color`, property is readonly. """ def _get_op_icon_color(self) -> list: return self._get_icon_color(True) opposite_icon_color = AliasProperty(_get_op_icon_color, bind=["theme_style"]) """ The opposite value of color in the :attr:`icon_color`. :attr:`opposite_icon_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`opposite_icon_color`, property is readonly. """ def _get_disabled_hint_text_color(self, opposite: bool = False) -> list: theme_style = self._get_theme_style(opposite) if theme_style == "Light": color = get_color_from_hex("000000") color[3] = 0.38 elif theme_style == "Dark": color = get_color_from_hex("FFFFFF") color[3] = 0.50 return color disabled_hint_text_color = AliasProperty(_get_disabled_hint_text_color, bind=["theme_style"]) """ Color of the disabled text used in the :class:`~kivymd.uix.textfield.MDTextField`. :attr:`disabled_hint_text_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`disabled_hint_text_color`, property is readonly. """ def _get_op_disabled_hint_text_color(self) -> list: return self._get_disabled_hint_text_color(True) opposite_disabled_hint_text_color = AliasProperty( _get_op_disabled_hint_text_color, bind=["theme_style"]) """ The opposite value of color in the :attr:`disabled_hint_text_color`. :attr:`opposite_disabled_hint_text_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`opposite_disabled_hint_text_color`, property is readonly. """ # Hardcoded because muh standard def _get_error_color(self) -> list: return get_color_from_hex(self.colors["Red"]["A700"]) error_color = AliasProperty(_get_error_color, bind=["theme_style"]) """ Color of the error text used in the :class:`~kivymd.uix.textfield.MDTextField`. :attr:`error_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`error_color`, property is readonly. """ def _get_ripple_color(self) -> list: return self._ripple_color def _set_ripple_color(self, value) -> None: self._ripple_color = value _ripple_color = ColorProperty(get_color_from_hex(colors["Gray"]["400"])) """Private value.""" ripple_color = AliasProperty(_get_ripple_color, _set_ripple_color, bind=["_ripple_color"]) """ Color of ripple effects. :attr:`ripple_color` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`ripple_color`, property is readonly. """ def _determine_device_orientation(self, _, window_size) -> None: if window_size[0] > window_size[1]: self.device_orientation = "landscape" elif window_size[1] >= window_size[0]: self.device_orientation = "portrait" device_orientation = StringProperty("") """ Device orientation. :attr:`device_orientation` is an :class:`~kivy.properties.StringProperty`. """ def _get_standard_increment(self) -> float: if DEVICE_TYPE == "mobile": if self.device_orientation == "landscape": return dp(48) else: return dp(56) else: return dp(64) standard_increment = AliasProperty(_get_standard_increment, bind=["device_orientation"]) """ Value of standard increment. :attr:`standard_increment` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`standard_increment`, property is readonly. """ def _get_horizontal_margins(self) -> float: if DEVICE_TYPE == "mobile": return dp(16) else: return dp(24) horizontal_margins = AliasProperty(_get_horizontal_margins) """ Value of horizontal margins. :attr:`horizontal_margins` is an :class:`~kivy.properties.AliasProperty` that returns the value in ``rgba`` format for :attr:`horizontal_margins`, property is readonly. """ def on_theme_style(self, interval: int, theme_style: str) -> None: if (hasattr(App.get_running_app(), "theme_cls") and App.get_running_app().theme_cls == self): self.set_clearcolor_by_theme_style(theme_style) set_clearcolor = BooleanProperty(True) def set_clearcolor_by_theme_style(self, theme_style): if not self.set_clearcolor: return Window.clearcolor = get_color_from_hex( self.colors[theme_style]["Background"]) # Font name, size (sp), always caps, letter spacing (sp). font_styles = DictProperty({ "H1": ["RobotoLight", 96, False, -1.5], "H2": ["RobotoLight", 60, False, -0.5], "H3": ["Roboto", 48, False, 0], "H4": ["Roboto", 34, False, 0.25], "H5": ["Roboto", 24, False, 0], "H6": ["RobotoMedium", 20, False, 0.15], "Subtitle1": ["Roboto", 16, False, 0.15], "Subtitle2": ["RobotoMedium", 14, False, 0.1], "Body1": ["Roboto", 16, False, 0.5], "Body2": ["Roboto", 14, False, 0.25], "Button": ["RobotoMedium", 14, True, 1.25], "Caption": ["Roboto", 12, False, 0.4], "Overline": ["Roboto", 10, True, 1.5], "Icon": ["Icons", 24, False, 0], }) """ Data of default font styles. Add custom font: .. code-block:: python KV = ''' MDScreen: MDLabel: text: "JetBrainsMono" halign: "center" font_style: "JetBrainsMono" ''' from kivy.core.text import LabelBase from kivy.lang import Builder from kivymd.app import MDApp from kivymd.font_definitions import theme_font_styles class MainApp(MDApp): def build(self): LabelBase.register( name="JetBrainsMono", fn_regular="JetBrainsMono-Regular.ttf") theme_font_styles.append('JetBrainsMono') self.theme_cls.font_styles["JetBrainsMono"] = [ "JetBrainsMono", 16, False, 0.15, ] return Builder.load_string(KV) MainApp().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/font-styles.png :attr:`font_styles` is an :class:`~kivy.properties.DictProperty`. """ def set_colors( self, primary_palette: str, primary_hue: str, primary_light_hue: str, primary_dark_hue: str, accent_palette: str, accent_hue: str, accent_light_hue: str, accent_dark_hue: str, ) -> None: """ Courtesy method to allow all of the theme color attributes to be set in one call. :attr:`set_colors` allows all of the following to be set in one method call: * primary palette color, * primary hue, * primary light hue, * primary dark hue, * accent palette color, * accent hue, * accent ligth hue, and * accent dark hue. Note that all values *must* be provided. If you only want to set one or two values use the appropriate method call for that. .. code-block:: python from kivymd.app import MDApp from kivymd.uix.screen import MDScreen from kivymd.uix.button import MDRectangleFlatButton class MainApp(MDApp): def build(self): self.theme_cls.set_colors( "Blue", "600", "50", "800", "Teal", "600", "100", "800" ) screen = MDScreen() screen.add_widget( MDRectangleFlatButton( text="Hello, World", pos_hint={"center_x": 0.5, "center_y": 0.5}, ) ) return screen MainApp().run() """ self.primary_palette = primary_palette self.primary_hue = primary_hue self.primary_light_hue = primary_light_hue self.primary_dark_hue = primary_dark_hue self.accent_palette = accent_palette self.accent_hue = accent_hue self.accent_light_hue = accent_light_hue self.accent_dark_hue = accent_dark_hue def __init__(self, **kwargs): super().__init__(**kwargs) self.rec_shadow = Atlas(f"{images_path}rec_shadow.atlas") self.rec_st_shadow = Atlas(f"{images_path}rec_st_shadow.atlas") self.quad_shadow = Atlas(f"{images_path}quad_shadow.atlas") self.round_shadow = Atlas(f"{images_path}round_shadow.atlas") Clock.schedule_once(lambda x: self.on_theme_style(0, self.theme_style)) self._determine_device_orientation(None, Window.size) Window.bind(size=self._determine_device_orientation) self.bind(font_styles=self.sync_theme_styles) self.colors = colors Clock.schedule_once(self.sync_theme_styles) def sync_theme_styles(self, *args) -> None: # Syncs the values from self.font_styles to theme_font_styles # this will ensure continuity when someone registers a new font_style. for num, style in enumerate(theme_font_styles): if style not in self.font_styles: theme_font_styles.pop(num) for style in self.font_styles.keys(): theme_font_styles.append(style)
class ConfigurationsPopup(Popup): axis_resolution = NumericProperty(10) axis_max_value = NumericProperty(10) task_domain_file_name = StringProperty('...') save_problem_file_name = StringProperty('...') load_error_label = ObjectProperty(None) file_loaded = DictProperty(None) def __init__(self, display, **kwargs): super(ConfigurationsPopup, self).__init__(**kwargs) self.display = display self.axis_resolution = self.display.axis_resolution self.axis_max_value = self.display.axis_max_value def axis_resolution_on_focus(self, instance, value): if not value: try: new_resolution = int(instance.text) if new_resolution > 0: self.display.axis_resolution = int(instance.text) else: instance.text = str(self.display.axis_resolution) except ValueError: instance.text = str(self.display.axis_resolution) def axis_max_value_on_focus(self, instance, value): if not value: try: new_max_value = int(instance.text) if new_max_value > 0: self.display.axis_max_value = int(instance.text) else: instance.text = str(self.display.axis_max_value) except ValueError: instance.text = str(self.display.axis_max_value) def show_load_domain_file(self): content = LoadDialog(load=self.load_domain_file, cancel=self.dismiss_popup_domain_file) self._popup_domain_file = Popup(title="Load file", content=content, size_hint=(0.9, 0.9)) self._popup_domain_file.open() def show_choose_save_problem_file(self): content = LoadDialog(load=self.choose_problem_file, cancel=self.dismiss_popup_problem_file) self._popup_problem_file = Popup(title="Choose problem file", content=content) self._popup_problem_file.open() def load_domain_file(self, path, filename): if (len(filename) > 0): self.task_domain_file_name = os.path.join(path, filename[0]) self.ids.choose_task_domain_file_text_input.do_cursor_movement( 'cursor_end') try: parsed_file = TaskDomainParser.parse( os.path.join(path, filename[0])) self.load_error_label.size_hint = 1, 0 self.load_error_label.text = '' self.file_loaded = parsed_file except json.decoder.JSONDecodeError as identifier: self.load_error_label.size_hint = 1, 0.12 self.load_error_label.text = 'File must be a JSON file' except Exception as identifier: self.load_error_label.size_hint = 1, 0.12 self.load_error_label.text = str(identifier) self.dismiss_popup_domain_file() def choose_problem_file(self, path, filename): if (len(filename) > 0): self.save_problem_file_name = os.path.join(path, filename[0]) self.ids.save_problem_file_text_input.do_cursor_movement( 'cursor_end') self.dismiss_popup_problem_file() def dismiss_popup_domain_file(self): self._popup_domain_file.dismiss() def dismiss_popup_problem_file(self): self._popup_problem_file.dismiss() def save_problem_file(self): if self.save_problem_file_name.endswith('.json'): self.ids.problem_file_error_label.size_hint_y = 0 self.ids.problem_file_error_label.text = '' TaskProblemParser.save_problem(self.save_problem_file_name, self.display.robots, self.display.tasks, self.display.mu_tasks) else: self.ids.problem_file_error_label.text = 'File must end with .json' self.ids.problem_file_error_label.size_hint_y = 0.12 def load_problem_file(self): if self.save_problem_file_name.endswith('.json'): self.ids.problem_file_error_label.size_hint_y = 0 self.ids.problem_file_error_label.text = '' try: robots, mu_tasks, tasks = TaskProblemParser.load( self.save_problem_file_name) self.display.clear_display() for robot in robots: self.display.add_robot(robot) for task in tasks: self.display.add_task(task) except Exception as exception: self.ids.problem_file_error_label.text = str(exception) self.ids.problem_file_error_label.size_hint_y = 0.12 else: self.ids.problem_file_error_label.text = 'File must end with .json' self.ids.problem_file_error_label.size_hint_y = 0.12
class CircularTimePicker(BoxLayout): """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. """ 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={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]) """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): return datetime.time(*self.time_list) def _set_time(self, dt): 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(*self.selector_color) if self.picker == "hours" else rgb_to_hex(*self.color) mc = rgb_to_hex(*self.selector_color) if self.picker == "minutes" else rgb_to_hex(*self.color) h = self.hours == 0 and 12 or self.hours <= 12 and self.hours or self.hours - 12 m = self.minutes return self.time_format.format(hours_color=hc, minutes_color=mc, hours=h, minutes=m) time_text = AliasProperty(_get_time_text, None, bind=("hours", "minutes", "time_format", "picker")) def _get_ampm_text(self): amc = rgb_to_hex(*self.selector_color) if self._am else rgb_to_hex(*self.color) pmc = rgb_to_hex(*self.selector_color) if not self._am else rgb_to_hex(*self.color) 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) if self.hours >= 12: self._am = False self.bind(time_list=self.on_time_list, picker=self._switch_picker, _am=self.on_ampm) self._h_picker = CircularHourPicker() self._m_picker = CircularMinutePicker() 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)) #print "TIMEee", self.time 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 ref == "hours": self.picker = "hours" elif ref == "minutes": self.picker = "minutes" elif 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): #print "TIME", self.time if not self._picker: return if self.picker == "hours": self._picker.selected = self.hours == 0 and 12 or self._am and self.hours or self.hours - 12 elif self.picker == "minutes": self._picker.selected = self.minutes 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 _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: # print "noanim" if prevpicker in container.children: container.remove_widget(prevpicker) if picker.parent: picker.parent.remove_widget(picker) container.add_widget(picker) else: 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 *a: 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") Clock.schedule_once(lambda *a: anim.start(picker), .3)
def test_dict(self): from kivy.properties import DictProperty x = DictProperty() x.link(wid, 'x') x.link_deps(wid, 'x') # test observer global observe_called observe_called = 0 def observe(obj, value): global observe_called observe_called = 1 x.bind(wid, observe) observe_called = 0 x.get(wid)['toto'] = 1 self.assertEqual(observe_called, 1) observe_called = 0 x.get(wid)['toto'] = 2 self.assertEqual(observe_called, 1) observe_called = 0 x.get(wid)['youupi'] = 2 self.assertEqual(observe_called, 1) observe_called = 0 del x.get(wid)['toto'] self.assertEqual(observe_called, 1) observe_called = 0 x.get(wid).update({'bleh': 5}) self.assertEqual(observe_called, 1)
class VendingDisplay(BoxLayout): amount = ObjectProperty('$0.13') feedback = ObjectProperty('') pubsub = None redis = None slots = DictProperty({ 1: '', 2: '', 3: '', 4: '', 5: '', 6: '', }) thread = None def add_item(self, item): self.redis.publish('vendingmachine001-channel', "add_item_{}".format(item)) self.feedback = '' def add_two_bits(self): self.redis.publish('vendingmachine001-channel', 'add_two_bits') self.feedback = '' def channel_handler(self, message): # print("VendingDisplay.channel_handler() | message: {}".format(message)) data = message['data'].decode() if data == 'purchase_complete': self.feedback = 'Purchase Complete' elif data == 'clear_feedback': self.feedback = '' elif data == 'low_funds': self.feedback = 'Need More Bits' elif data == 'item_added': self.feedback = 'Item Added' elif data == 'money_added': self.feedback = 'Bits Added' def make_purchase(self): self.redis.publish('vendingmachine001-channel', 'make_purchase') self.feedback = '' def on_pause(self): self.thread.stop() def on_resume(self): self.redis_link() def on_stop(self): self.thread.stop() def redis_link(self): self.redis = redis.StrictRedis(host='localhost', port=6379, db=0) self.pubsub = self.redis.pubsub(ignore_subscribe_messages=True) self.pubsub.subscribe( **{'vendingremote-channel': self.channel_handler}) self.thread = self.pubsub.run_in_thread(sleep_time=0.001) pipe = self.redis.pipeline() pipe.get("vendingmachine001:amount") pipe.hgetall("vendingmachine001:slot1") pipe.hgetall("vendingmachine001:slot2") pipe.hgetall("vendingmachine001:slot3") pipe.hgetall("vendingmachine001:slot4") pipe.hgetall("vendingmachine001:slot5") pipe.hgetall("vendingmachine001:slot6") data = pipe.execute() count = 0 for datum in data: if count == 0: self.amount = "${}".format(datum.decode()) else: self.slots[count] = "Order {}: ${}".format( datum[b'name'].decode(), datum[b'price'].decode()) count += 1