Пример #1
0
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
Пример #2
0
    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'})
Пример #3
0
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()
Пример #4
0
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
Пример #5
0
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
Пример #6
0
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)
Пример #7
0
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)
Пример #8
0
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
Пример #9
0
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
Пример #10
0
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()
Пример #11
0
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
Пример #12
0
class RstDocument(ScrollView):
    '''Base widget used to store an Rst document. See module documentation for
    more information.
    '''
    source = StringProperty(None)
    '''Filename of the RST document.

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

    source_encoding = StringProperty('utf-8')
    '''Encoding to be used for the :attr:`source` file.

    :attr:`source_encoding` is a :class:`~kivy.properties.StringProperty` and
    defaults to `utf-8`.

    .. Note::
        It is your responsibility to ensure that the value provided is a
        valid codec supported by python.
    '''

    source_error = OptionProperty('strict',
                                  options=('strict', 'ignore', 'replace',
                                           'xmlcharrefreplace',
                                           'backslashreplac'))
    '''Error handling to be used while encoding the :attr:`source` file.

    :attr:`source_error` is an :class:`~kivy.properties.OptionProperty` and
    defaults to `strict`. Can be one of 'strict', 'ignore', 'replace',
    'xmlcharrefreplace' or 'backslashreplac'.
    '''

    text = StringProperty(None)
    '''RST markup text of the document.

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

    document_root = StringProperty(None)
    '''Root path where :doc: will search for rst documents. If no path is
    given, it will use the directory of the first loaded source file.

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

    base_font_size = NumericProperty(31)
    '''Font size for the biggest title, 31 by default. All other font sizes are
    derived from this.

    .. versionadded:: 1.8.0
    '''

    show_errors = BooleanProperty(False)
    '''Indicate whether RST parsers errors should be shown on the screen
    or not.

    :attr:`show_errors` is a :class:`~kivy.properties.BooleanProperty` and
    defaults to False.
    '''
    def _get_bgc(self):
        return get_color_from_hex(self.colors.background)

    def _set_bgc(self, value):
        self.colors.background = get_hex_from_color(value)[1:]

    background_color = AliasProperty(_get_bgc,
                                     _set_bgc,
                                     bind=('colors', ),
                                     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)
Пример #13
0
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)
Пример #14
0
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
Пример #15
0
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()
Пример #16
0
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))
Пример #17
0
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)
Пример #18
0
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)
Пример #19
0
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)
Пример #20
0
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)
Пример #21
0
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.
Пример #22
0
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
Пример #23
0
class Label(Widget):
    '''Label class, see module documentation for more information.

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

    __events__ = ['on_ref_press']

    _font_properties = ('text', 'font_size', 'font_name', 'bold', 'italic',
                        '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
Пример #24
0
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)
Пример #25
0
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)
Пример #26
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)
Пример #27
0
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.
Пример #28
0
class CompletedTask(ToggleButton):
    _source = ObjectProperty()
    _text = StringProperty()
    _data = DictProperty()
Пример #29
0
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
Пример #31
0
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)
Пример #32
0
    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)
Пример #33
0
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